with one click
datadog-insights
Investigate Gram production health and post a digest to Slack
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Menu
Investigate Gram production health and post a digest to Slack
Install with Codex or Claude Copy this prompt, paste it into Codex, Claude, or another assistant, and let it review the skill page and install it for you.
Based on SOC occupation classification
Rules when working with ClickHouse database in Gram for analytics and telemetry features, including editing the ClickHouse schema (server/clickhouse/schema.sql) and creating or fixing ClickHouse migrations
Rules and best practices when working on the dashboard and elements React frontend codebases
Concepts, external interfaces, and conventions for Gram's audit logging subsystem — the internal Go API for recording actor/action/subject events and the `/rpc/auditlogs.*` management API that exposes them. Activate whenever the task involves recording or exposing audit events (adding or changing audit coverage on a service, introducing a new audited subject or action, writing tests that assert an event was recorded, changing how entries are displayed or filtered).
Concepts, external interfaces, and conventions for Gram's management API — the Goa-designed HTTP-RPC surface under `/rpc/<service>.<method>` that powers the dashboard, CLI, and public SDK. Activate whenever the task involves designing, implementing, or modifying a management endpoint (new service, new method, payload/result changes, OpenAPI/SDK surface changes, CLI changes, wiring a new service into the server).
Concepts, external interfaces, and conventions for Gram's role-based access control (RBAC) subsystem — scopes, grants, principals, system roles, and the `authz.Engine.Require` enforcement path used inside handlers. Activate whenever the task involves authorization (adding or modifying a scope or resource type, declaring a new role or grant, gating a handler, changing scope inheritance, exposing RBAC state through the dashboard).
Use the Playwright MCP browser to capture a demo (screenshots or a GIF recording) of a user-visible frontend change and post it as a PR comment
| name | datadog-insights |
| description | Investigate Gram production health and post a digest to Slack |
You are producing a health report for Gram's production services. The report must be actionable and visually structured — critical issues must stand out immediately, tabular data must use code blocks, and every section must be separated by a divider.
Before starting: activate the datadog skill for Gram service names, MCP tools, and query guidelines.
⚠️ MANDATORY FORMAT RULES — READ BEFORE COMPOSING THE MESSAGE:
- Every major section MUST be preceded by a Unicode divider line:
──────────────────────────────────────on its own line, with a blank line above and below.- Top endpoints, error type breakdowns, and latency tables MUST use triple-backtick code blocks — never bullet points for tabular data.
- Code block tables must have aligned columns using spaces. Minimum widths: endpoint 38 chars, count 8 chars, err% 6 chars, p95 8 chars.
- Each monitor in alert MUST get its own paragraph — never combine multiple monitors into one block.
- Do NOT collapse or omit data to save space. If there are 8 monitors, show all 8.
These take priority over everything else. If any exist, they become the top of the digest.
search_datadog_incidents for state:(active OR stable) in the last 24hsearch_datadog_monitors with query status:alert (notification:slack-Speakeasy-gram-oncall OR notification:slack-oncall-gram). This filters to Gram-only monitors. Never include monitors that don't notify one of these two channels.analyze_datadog_logs with SQL:
SELECT service, status, count(*) FROM logs GROUP BY service, status ORDER BY count(*) DESC
Filter: env:prod status:(error OR critical OR alert OR emergency), last 24h.
Compare the last 6h vs. the previous 18h to detect spikes.If there are critical issues, investigate each one:
search_datadog_logs)get_datadog_trace to find root causesGrep in server/internal/ for the error message to find the source code locationFor top error message breakdown, use analyze_datadog_logs:
SELECT message, count(*) as cnt
FROM logs
WHERE service = 'gram-server' AND status IN ('error', 'critical')
GROUP BY message
ORDER BY cnt DESC
LIMIT 10
Use search_datadog_spans for service:gram-server env:prod over the last 24h, or:
sum:trace.http.server.request.hits{service:gram-server,env:prod} by {resource_name}.rollup(sum, 86400)
Collect the top 10 endpoints with:
Compare traffic between two 12h windows:
from: now-12h, to: nowfrom: now-24h, to: now-12hUse get_datadog_metric with:
sum:trace.http.server.request.hits{service:gram-server,env:prod}.rollup(sum, 43200)
Report:
gram-server, gram-worker, gram, fly)p50:trace.http.server.request{service:gram-server,env:prod} by {resource_name}
p95:trace.http.server.request{service:gram-server,env:prod} by {resource_name}
p99:trace.http.server.request{service:gram-server,env:prod} by {resource_name}
Over the last 24h with .rollup(avg, 86400).
Report:
Call create_datadog_notebook with name "Gram Health Digest — <DAY> <DATE>" (e.g. "Gram Health Digest — Fri 2026-03-27"). Use absolute_time: true with start_time = 24h ago and end_time = now. One notebook is created per run — old ones accumulate and can be manually deleted periodically.
The notebook cells must be wrapped in {"cells": [...]}. Include:
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "markdown",
"text": "One paragraph verdict with key numbers."
}
}
}
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "timeseries",
"title": "gram-server Error Rate (1h buckets)",
"requests": [
{
"q": "sum:trace.http.server.request.errors{service:gram-server,env:prod}.rollup(sum, 3600)",
"display_type": "bars",
"style": { "palette": "warm" }
}
],
"show_legend": true,
"yaxis": { "scale": "linear" },
"markers": [
{
"value": "y = 500",
"display_type": "warning dashed",
"label": "Elevated"
}
]
}
}
}
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "timeseries",
"title": "gram-server Traffic Volume (1h buckets)",
"requests": [
{
"q": "sum:trace.http.server.request.hits{service:gram-server,env:prod}.rollup(sum, 3600)",
"display_type": "area",
"style": { "palette": "dog_classic" }
}
],
"show_legend": true,
"yaxis": { "scale": "linear" }
}
}
}
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "timeseries",
"title": "Top Endpoint p95 Latency",
"requests": [
{
"q": "p95:trace.http.server.request{service:gram-server,env:prod} by {resource_name}.rollup(avg, 3600)",
"display_type": "line",
"style": { "palette": "dog_classic" }
}
],
"show_legend": true,
"yaxis": { "scale": "linear" },
"markers": [
{
"value": "y = 2",
"display_type": "error dashed",
"label": "2s threshold"
}
]
}
}
}
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "timeseries",
"title": "gram-worker Error Rate (1h buckets)",
"requests": [
{
"q": "sum:trace.http.server.request.errors{service:gram-worker,env:prod}.rollup(sum, 3600)",
"display_type": "bars",
"style": { "palette": "warm" }
}
],
"show_legend": true,
"yaxis": { "scale": "linear" }
}
}
}
gram is an APM service, so use trace metrics:
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "timeseries",
"title": "gram (frontend) Trace Errors (1h buckets)",
"requests": [
{
"q": "sum:trace.http.server.request.errors{service:gram,env:prod}.rollup(sum, 3600)",
"display_type": "bars",
"style": { "palette": "warm" }
}
],
"show_legend": true,
"yaxis": { "scale": "linear" }
}
}
}
fly is a log source (not an APM service), so use a log stream, not a trace metric:
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "log_stream",
"title": "fly (functions) Error Logs (24h)",
"query": "source:fly env:prod status:error",
"columns": ["timestamp", "host", "message"],
"message_display": "inline",
"show_date_column": true,
"show_message_column": true,
"sort": { "column": "timestamp", "order": "desc" }
}
}
}
source:fly for Gram Functions logs:
{
"type": "notebook_cells",
"attributes": {
"definition": {
"type": "log_stream",
"query": "(service:(gram-server OR gram-worker OR gram) OR source:fly) env:prod status:error",
"columns": ["timestamp", "host", "service", "message"],
"message_display": "inline",
"show_date_column": true,
"show_message_column": true,
"sort": { "column": "timestamp", "order": "desc" }
}
}
}
Save the notebook URL — you will link it in the Slack message footer.
Based on all the data gathered, write one concrete recommendation for the on-call engineer. Be specific:
This recommendation goes into the Slack message as a dedicated section.
Build a list of Block Kit blocks. The message is structured around the 4 Golden Signals: Alerts → Errors → Traffic → Latency.
•) with inline backtick formatting for endpoint/service namessection mrkdwn text — they render as aligned monospace in Slack and are much more readable than bullet points for columnar datasection with fields (2-column grid) — never a context block, which is too small to notice1. Header
{
"type": "header",
"text": { "type": "plain_text", "text": "Gram Health Digest — <DAY> <DATE>" }
}
2. Verdict — section with fields (2-column grid)
Always 6 fields: Status, Monitors in Alert, Errors (24h), Traffic (24h), Latency p95, Slow Endpoints.
{
"type": "section",
"fields": [
{ "type": "mrkdwn", "text": "*Status*\n<VERDICT_EMOJI> <one-word status>" },
{ "type": "mrkdwn", "text": "*Monitors in Alert*\n<N (name)> or 0 🟢" },
{ "type": "mrkdwn", "text": "*Errors (24h)*\n<count> · ↑<Nx> last 6h" },
{ "type": "mrkdwn", "text": "*Traffic (24h)*\n~<Xk> · <↑/↓pct%> last 12h" },
{ "type": "mrkdwn", "text": "*Latency p95*\n<Xms> (global)" },
{
"type": "mrkdwn",
"text": "*Slow Endpoints*\n<N endpoints > 2s> or All healthy 🟢"
}
]
}
Follow with a divider.
3. 🚨 Alerts (omit section entirely if no monitors in alert)
Each monitor gets its own paragraph. Do NOT combine monitors.
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "🚨 *Alerts*\n🔴 *<Monitor name>*\n<What it means and why it matters>\n*Notifying:* `#<channel>`\n\n🔴 *<Next monitor name>*\n<What it means>\n*Notifying:* `#<channel>`"
}
}
Follow with a divider.
4. ❌ Errors
Bullet prose for per-service summary, then a code block table for top error types.
{"type": "section", "text": {"type": "mrkdwn", "text": "❌ *Errors*\n• `gram-server`: X errors in last 6h (Y/h) vs Z/h prior — *~Nx spike*\n• `gram-worker`: N errors (stable)\n• `gram` (frontend): N (stable)\n• `fly` (functions): 0 🟢\n\n*Top error types — gram-server (24h):*\n```\nmessage count pct\nnot found 402 31.4%\ntoken value is empty for bearer auth 270 21.1%\nmissing value for env var in api key auth 74 5.8%\nHTTP roundtrip failed 70 5.5%\nno MCP install page metadata for toolset 65 5.1%\n```"}}
Follow with a divider.
5. 📊 Traffic
Bullet prose for trend, then a code block table for top endpoints by volume.
{"type": "section", "text": {"type": "mrkdwn", "text": "📊 *Traffic*\n• Previous 12h: ~Xk requests\n• Current 12h: ~Xk requests — *↑Y%* ⚠️ (flag if >30%)\n• Total 24h: ~Xk\n\n*Top endpoints by volume (24h):*\n```\nendpoint hits\nPOST /mcp/{mcpSlug} 103,784\nPOST /rpc/hooks.otel/v1/logs 16,824\nPOST /rpc/hooks.claude 14,956\nGET /mcp/{mcpSlug} 14,454\nGET /.well-known/oauth-protected-resource 6,789\n```"}}
Follow with a divider.
6. ⏱️ Latency
If any endpoint has p95 > 2s, use a code block table for slow endpoints. Always include "approaching threshold" if any endpoints are 1–2s p95.
{"type": "section", "text": {"type": "mrkdwn", "text": "⏱️ *Latency*\n*Global:* p50: Xms · p95: Xms · p99: Xms\n\n*Slow endpoints (p95 > 2s):*\n```\nendpoint p95 p50 hits\nGET /rpc/toolsets.listfororg 7,275ms 5,766ms 57 ⚠️\nGET /rpc/usage.getperiodusage 5,173ms 3,403ms 49 ⚠️\nPOST /chat/completions 4,713ms 2,615ms 15 (AI)\n```\n*Approaching threshold (p95 > 1s):*\n```\nGET /rpc/environments.list 1,406ms 57\nGET /rpc/access.listgrants 1,281ms 84\n```"}}
If all endpoints are fast:
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "⏱️ *Latency* — All endpoints healthy. p50: Xms · p95: Xms · p99: Xms 🟢"
}
}
Follow with a divider.
7. Recommendation
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": "💡 *Recommendation*\n<Specific, concrete recommendation for the on-call engineer. One or two sentences. Name the action and where to look.>"
}
}
Follow with a divider.
8. Footer — links to Datadog notebook and skill source
{
"type": "context",
"elements": [
{
"type": "mrkdwn",
"text": "🔴 Critical 🟡 Warning 🟢 Healthy | <NOTEBOOK_URL|View in Datadog> | <https://github.com/speakeasy-api/gram/blob/main/.claude/skills/datadog-insights/SKILL.md|Skill source>"
}
]
}
Replace NOTEBOOK_URL with the actual notebook URL from Step 5.
Write and run this Python script via Bash. Post to #gram-datadog-insights by default, unless a different channel was specified in the prompt.
import json, urllib.request, os, datetime
now_utc = datetime.datetime.utcnow()
digest_date = now_utc.strftime("%a %b %-d") # e.g. "Mon Apr 20"
env_path = os.path.expanduser("~/.config/gram/.env")
token = None
with open(env_path) as f:
for line in f:
if line.startswith("SLACK_BOT_TOKEN="):
token = line.split("=", 1)[1].strip().strip('"').strip("'")
break
if not token:
raise RuntimeError("SLACK_BOT_TOKEN not found in ~/.config/gram/.env")
channel = "C0AKLE930BX" # #gram-datadog-insights — override with channel name if specified in prompt
blocks = [] # replace with actual Block Kit blocks from Step 7 — use f"Gram Health Digest — {digest_date}" in the header block
def slack_post(payload):
data = json.dumps(payload).encode()
req = urllib.request.Request(
"https://slack.com/api/chat.postMessage",
data=data,
headers={"Content-Type": "application/json", "Authorization": f"Bearer {token}"},
method="POST",
)
with urllib.request.urlopen(req) as resp:
return json.loads(resp.read())
result = slack_post({
"channel": channel,
"text": "Gram Health Digest",
"blocks": blocks,
})
if not result.get("ok"):
raise RuntimeError(f"Slack error: {result}")
ts = result["ts"]
reply = slack_post({
"channel": channel,
"thread_ts": ts,
"text": "<!subteam^S09EXM6DPCY|dev-mcp-oncall>",
})
if not reply.get("ok"):
raise RuntimeError(f"Thread reply error: {reply}")
print(f"✓ Posted to {channel} (ts={ts}), oncall tagged in thread")
MANDATORY RULES — never violate: