Monitoring and observability
Monitoring and observability with AI agents means having Claude Code, Codex or Cursor instrument a service with OpenTelemetry, generate the collector, dashboard and alert configuration, and prove each piece with machine checks — promtool rule tests, config validators and live queries through the Sentry and Grafana MCP servers — so nobody has to read the generated YAML line by line.
Your checkout service throws intermittent 504s. Traces stop at the gateway span, the dashboard is green, and the alert that should have fired references a metric name your instrumentation stopped emitting two releases ago. An agent can wire the whole stack in an afternoon; this page is about making sure what it wires is true.
This page is for the developer who owns a service and the tech lead who signs off on its alerting. It covers observing your service. To observe the agents themselves — their traces, tool calls, cost and audit logs — see agent observability.
What an agent-built monitoring setup gives you
Section titled “What an agent-built monitoring setup gives you”- An OpenTelemetry bootstrap for a Node.js service on the current packages, with the metric names those packages actually emit.
- Alert rules that ship with
promtoolunit tests, so “does this alert fire at 10% errors and stay quiet at 1%?” is a CI result, not a code-review opinion. - Validation commands for every generated config (Collector, Loki, Alertmanager) and a prompt that checks every dashboard panel returns data.
- Read-only Sentry and Grafana MCP access, so the agent reads live metric names and errors instead of guessing them.
- A recovery playbook for the five ways agent-built observability goes wrong.
In what order should you build service monitoring with an agent?
Section titled “In what order should you build service monitoring with an agent?”Order matters because each layer is only as good as the one under it. An alert on a metric that does not exist never fires, and nothing reports that it never fires.
| Step | What the agent generates | What proves it without reading it |
|---|---|---|
| 1. Instrument | OpenTelemetry SDK bootstrap | A request’s trace ID found in Tempo; metric names listed from Prometheus |
| 2. Pipeline | Collector, Prometheus, Loki, Tempo, Grafana config | otelcol validate, loki -verify-config=true, promtool check config |
| 3. Alerts | Prometheus rules and Alertmanager routing | promtool test rules, amtool check-config |
| 4. Dashboards | Grafana dashboard JSON | Every panel query returns data (checked through the Grafana MCP) |
| 5. Live access | Sentry and Grafana MCP servers | Read-only tokens; one incident question answered end to end |
Do not let the agent skip ahead to dashboards. Dashboards built before step 1 is proven are built on guessed metric names.
Instrument a Node.js service with OpenTelemetry
Section titled “Instrument a Node.js service with OpenTelemetry”The prompt is the same in all three tools; what differs is where you run it and how edits are approved. The prompt pins the current API because models trained on older tutorials reach for the removed Resource class and for metric names that no longer exist.
Run it from the repository root. Interactively, start claude and paste the prompt. For a scripted run, save the prompt to a file and use print mode. acceptEdits approves file edits only and denies any shell command your permission rules do not allow, so allow the three commands the prompt needs to install packages, start the service and call GET /health (flags checked on Claude Code 2.1.283):
claude -p "$(cat prompts/otel-bootstrap.md)" --permission-mode acceptEdits \ --allowedTools "Bash(npm *)" "Bash(node *)" "Bash(curl *)" --output-format jsonThe JSON result carries the session ID and final message, which you can archive with the pull request.
Interactively, run codex in the repository and paste the prompt; approvals default to asking you. For an unattended run in a disposable checkout, let Codex’s automatic reviewer answer approval requests inside the workspace-write sandbox:
codex exec --approve-for-me "$(cat prompts/otel-bootstrap.md)"--approve-for-me exists on both codex and codex exec in codex-cli 0.157.1. Add --json to stream events as JSONL for a CI log.
Open the Agent panel, paste the prompt, and review the proposed diff before accepting. Expect edits to package.json, a new src/instrumentation.ts and the start script. Ask the agent to run the service and send the test request itself; accepting the diff without that run skips the step that proves the instrumentation works.
The result should look close to this. Package versions current on 2026-09-26: @opentelemetry/sdk-node 0.222.0, @opentelemetry/resources 2.11.0, @opentelemetry/semantic-conventions 1.43.0.
// src/instrumentation.ts — compiled, then loaded with: node --require ./dist/instrumentation.js dist/server.js// (ESM service: node --experimental-loader=@opentelemetry/instrumentation/hook.mjs --import ./dist/instrumentation.js dist/server.js)import { NodeSDK } from '@opentelemetry/sdk-node';import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node';import { resourceFromAttributes } from '@opentelemetry/resources';import { ATTR_DEPLOYMENT_ENVIRONMENT_NAME, ATTR_SERVICE_NAME, ATTR_SERVICE_VERSION,} from '@opentelemetry/semantic-conventions';import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';import { OTLPMetricExporter } from '@opentelemetry/exporter-metrics-otlp-grpc';import { PeriodicExportingMetricReader } from '@opentelemetry/sdk-metrics';
const sdk = new NodeSDK({ resource: resourceFromAttributes({ [ATTR_SERVICE_NAME]: process.env.SERVICE_NAME ?? 'checkout', [ATTR_SERVICE_VERSION]: process.env.SERVICE_VERSION ?? '0.0.0', [ATTR_DEPLOYMENT_ENVIRONMENT_NAME]: process.env.NODE_ENV ?? 'development', }), traceExporter: new OTLPTraceExporter(), metricReader: new PeriodicExportingMetricReader({ exporter: new OTLPMetricExporter(), exportIntervalMillis: 30_000, }), instrumentations: [ getNodeAutoInstrumentations({ '@opentelemetry/instrumentation-fs': { enabled: false } }), ],});
sdk.start();process.on('SIGTERM', () => sdk.shutdown().finally(() => process.exit(0)));The exporters read OTEL_EXPORTER_OTLP_ENDPOINT from the environment on their own. A plain node --require ./src/instrumentation.ts fails on a .ts file, which is why the prompt asks for the compiled file. An ESM service ("type": "module") needs more: --require does not hook ES module imports, so Express and http can run without producing a single span. Load the file with node --import ./dist/instrumentation.js and register the loader hook @opentelemetry/instrumentation/hook.mjs as the OpenTelemetry ESM guide describes, then confirm the setup by finding the request’s trace ID in Tempo.
Which HTTP metric names does current OpenTelemetry emit?
Section titled “Which HTTP metric names does current OpenTelemetry emit?”This gotcha breaks agent-written alerts silently. From @opentelemetry/instrumentation-http 0.222.0 (checked 2026-09-26), the package README says it emits only the stable HTTP semantic conventions: the histogram http.server.request.duration, in seconds, with attributes such as http.route and http.response.status_code. Versions up to 0.221.0 emitted the old http.server.duration in milliseconds by default. Tutorials written before 0.222.0, and so models trained on them, use the old name.
After the OTLP-to-Prometheus translation, the new histogram usually appears as http_server_request_duration_seconds_bucket, _count and _sum, with labels http_route and http_response_status_code. “Usually” matters: the exact names depend on your Prometheus translation settings. So the rule is: the agent reads metric names from the running Prometheus before it writes a single query.
# Terminal: list what Prometheus actually storescurl -s http://localhost:9090/api/v1/label/__name__/values | jq -r '.data[]' | grep '^http_server'If that list is empty, stop. Nothing downstream can be right.
Stand up the collector, Prometheus, Loki, Tempo and Grafana
Section titled “Stand up the collector, Prometheus, Loki, Tempo and Grafana”Ask for the Compose file (or Helm values) in one prompt, then validate each generated config with the component’s own checker before starting anything. Validators are faster than debugging a crash loop and they do not require you to read the file. Replace LOKI_VERSION with the 3.x tag you deploy.
# Terminal: validate the configs the agent generatedotelcol validate --config=otel-collector.yaml # use your distribution's binary, e.g. otelcol-contribpromtool check config prometheus.ymldocker run --rm -v "$PWD":/config grafana/loki:LOKI_VERSION -config.file=/config/loki.yaml -verify-config=trueLoki is where agents most often emit dead syntax. Loki 3.0 removed the shared_store settings for the TSDB and BoltDB shippers, and it enables structured metadata by default, which requires the active schema to use both the tsdb index and schema v13 (per Grafana’s Loki 3.0 upgrade guide). A generated config with boltdb-shipper, schema: v11 or shared_store fails validation. The current shape:
# loki.yaml (Loki 3.x): TSDB index, schema v13, no shared_store anywhereschema_config: configs: - from: 2026-01-01 store: tsdb object_store: s3 schema: v13 index: prefix: index_ period: 24hstorage_config: tsdb_shipper: active_index_directory: /loki/tsdb-index cache_location: /loki/tsdb-cache aws: s3: s3://s3.eu-central-1.amazonaws.com/checkout-logs s3forcepathstyle: truecompactor: working_directory: /loki/compactorGenerate alert rules you can test without reading them
Section titled “Generate alert rules you can test without reading them”An alert rule is a small program, and promtool test rules is its unit-test runner. The agent writes the rule and the test together; the test feeds synthetic series and asserts when the alert fires and when it stays silent. You review the test cases — “fires at 10% errors, silent at 1%” — which are two lines of intent, not the PromQL.
groups: - name: checkout-slo rules: - alert: CheckoutHighErrorRatio expr: | sum by (job) (rate(http_server_request_duration_seconds_count{job="checkout", http_response_status_code=~"5.."}[5m])) / sum by (job) (rate(http_server_request_duration_seconds_count{job="checkout"}[5m])) > 0.05 for: 5m labels: severity: critical annotations: summary: "5xx ratio above 5% on {{ $labels.job }}" runbook_url: "https://runbooks.example.com/checkout-5xx"# alerts_test.yml — run with: promtool test rules alerts_test.ymlrule_files: [alerts.yml]evaluation_interval: 1mtests: # 10% of requests fail: the alert must fire once `for: 5m` has elapsed - interval: 1m input_series: - series: 'http_server_request_duration_seconds_count{job="checkout", http_route="/pay", http_response_status_code="500"}' values: '0+10x20' - series: 'http_server_request_duration_seconds_count{job="checkout", http_route="/pay", http_response_status_code="200"}' values: '0+90x20' alert_rule_test: - eval_time: 3m alertname: CheckoutHighErrorRatio exp_alerts: [] - eval_time: 15m alertname: CheckoutHighErrorRatio exp_alerts: - exp_labels: { severity: critical, job: checkout } exp_annotations: summary: "5xx ratio above 5% on checkout" runbook_url: "https://runbooks.example.com/checkout-5xx" # 1% of requests fail: the alert must stay silent - interval: 1m input_series: - series: 'http_server_request_duration_seconds_count{job="checkout", http_route="/pay", http_response_status_code="500"}' values: '0+1x20' - series: 'http_server_request_duration_seconds_count{job="checkout", http_route="/pay", http_response_status_code="200"}' values: '0+99x20' alert_rule_test: - eval_time: 15m alertname: CheckoutHighErrorRatio exp_alerts: []Keep the test file out of the agent’s reach when it later edits the rule, for the same reason you protect any test oracle: an agent told to “make the test pass” can weaken the assertion instead of fixing the rule. See protecting the oracle.
Alertmanager routing gets the same treatment: amtool check-config alertmanager.yml catches syntax and unknown receivers, and amtool config routes test --config.file=alertmanager.yml severity=critical service=checkout prints which receiver a given label set reaches.
Put the checks in CI
Section titled “Put the checks in CI”The validators only protect you if they run on every change. A job like this makes “the alert fires at 10% errors” a required check on the pull request:
# .github/workflows/observability.yml (excerpt)jobs: alert-tests: runs-on: ubuntu-latest permissions: contents: read steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: persist-credentials: false - name: Test alert rules run: | docker run --rm -v "$PWD/observability:/w" -w /w --entrypoint promtool \ prom/prometheus:PROMETHEUS_VERSION check rules alerts.yml docker run --rm -v "$PWD/observability:/w" -w /w --entrypoint promtool \ prom/prometheus:PROMETHEUS_VERSION test rules alerts_test.ymlReplace PROMETHEUS_VERSION with the tag of the Prometheus you run in production, so the rules are tested by the same parser that loads them. The wider pipeline lives in CI/CD pipelines.
Check every dashboard panel returns data
Section titled “Check every dashboard panel returns data”A generated dashboard fails silently: a wrong metric name renders as “No data”, which looks like a quiet system. Rather than clicking through panels, have the agent run every panel’s query through the Grafana MCP server and report the ones that return nothing.
The table is the evidence you attach to the pull request. A tech lead approves on “every panel returns series” plus a screenshot, not on the JSON.
Give the agent read access to production signals with MCP
Section titled “Give the agent read access to production signals with MCP”Two vendor-official MCP servers turn the agent into an on-call reader: Sentry (issues, events, traces and Seer analysis) and Grafana (dashboards, Prometheus, Loki, incidents). Popularity, read 2026-09-26 from the MCP dossier: grafana/mcp-grafana has about 3.5k GitHub stars and getsentry/sentry-mcp about 862. Sentry is also in the Claude Code and Codex plugin marketplaces; Grafana ships Claude Code plugins (grafana-mcp, grafana-cloud-mcp). Datadog users have an official remote server at https://mcp.datadoghq.com/v1/mcp (US1; other sites change the host). The full catalogue is on observability MCP servers.
# Sentry: remote server with OAuth; authenticate with /mcp inside a sessionclaude mcp add --transport http sentry https://mcp.sentry.dev/mcp
# Grafana: read-only; use a Viewer-role service-account tokenclaude mcp add grafana -e GRAFANA_URL=https://grafana.example.com \ -e GRAFANA_SERVICE_ACCOUNT_TOKEN="$GRAFANA_TOKEN" -- uvx mcp-grafana --disable-writecodex mcp add sentry --url https://mcp.sentry.dev/mcpcodex mcp login sentry
codex mcp add grafana --env GRAFANA_URL=https://grafana.example.com \ --env GRAFANA_SERVICE_ACCOUNT_TOKEN="$GRAFANA_TOKEN" -- uvx mcp-grafana --disable-writeAdd both servers to .cursor/mcp.json, then sign in to Sentry when Cursor prompts:
{ "mcpServers": { "sentry": { "url": "https://mcp.sentry.dev/mcp" }, "grafana": { "command": "uvx", "args": ["mcp-grafana", "--disable-write"], "env": { "GRAFANA_URL": "https://grafana.example.com", "GRAFANA_SERVICE_ACCOUNT_TOKEN": "${env:GRAFANA_TOKEN}" } } }}Cursor interpolates environment variables as ${env:NAME}, not ${NAME}. That syntax comes from Cursor’s MCP docs (SECONDARY), so check it on your Cursor version.
If your team uses Sentry, its skills complement the MCP server: sentry-instrument, sentry-debug-issue and sentry-otel-exporter-setup ship in getsentry/sentry-for-ai. Install them with claude plugin install sentry@claude-plugins-official; in Codex, with /plugins → search “sentry” (the entry is listed in openai-curated; the CLI form codex plugin add sentry@openai-curated needs a signed-in account, because the marketplace is provisioned at sign-in; checked on codex-cli 0.157.1); for Cursor, install Sentry’s Cursor plugin (getsentry/plugin-cursor); other agents can fetch the same skills from skills.sentry.dev. The getsentry/sentry-for-ai repository itself is the skill source, and its README says to install a client plugin rather than the repository (checked 2026-10-02). Reach for a skill when you want a repeatable procedure; reach for the MCP server when the agent needs live data.
Who signs off on agent-generated monitoring?
Section titled “Who signs off on agent-generated monitoring?”The service owner signs off on the evidence; the tech lead owns the alerting policy. Neither reads the generated YAML end to end. The pull request that adds or changes monitoring carries:
- The list of
http_server_*metric names from the running Prometheus, and the trace ID of one request found in Tempo. - Green
otelcol validate,promtool check config,loki -verify-config=trueandamtool check-configoutput. - Green
promtool test rules, with every alert covered by one must-fire and one must-stay-silent case. - The panel table from the dashboard prompt with no empty panels.
- Confirmation that the MCP tokens are read-only.
The tech lead reviews the test cases and thresholds, because that is where the intent lives: the 5% threshold, the for: 5m delay and the SLO target.
What breaks when an agent builds your observability?
Section titled “What breaks when an agent builds your observability?”Symptom: empty panels, alerts that never fire, promtool test rules green but production silent.
Cause: the agent wrote http_server_duration or http_requests_total from memory, while the instrumentation emits http.server.request.duration.
Recovery: run the metric-name curl above, then give the agent the list:
Here is the output of /api/v1/label/__name__/values filtered to http_server_*. Rewrite everyquery in observability/ to use only these names, rerun promtool test rules with series namedthe same way, and list each query you changed.Symptom: Prometheus memory climbs and queries slow down.
Cause: an unbounded label such as a user ID, a request ID or a raw URL path instead of http.route.
Recovery:
Run topk(10, count by (__name__)({__name__=~".+"})) on our Prometheus through the Grafana MCP.For the top three metrics, find the label with the most distinct values. Propose a Collectorattributes processor or a relabel_config that drops or buckets it, and show the series countbefore and after on a staging Prometheus.Symptom: spans exist for the gateway but not the services behind it.
Cause: traceparent is not forwarded (a custom HTTP client or a queue), or a sampler drops the children.
Recovery:
Traces stop at our API gateway. Check that every outbound HTTP client and queue producer inthis repo propagates W3C traceparent (list any that bypass the instrumented clients), show thesampler configuration in the SDK and the Collector, then send one request with a forced-sampletraceparent and confirm through the Grafana MCP that Tempo shows the full trace.Symptom: one incident pages fifty times.
Cause: no grouping or inhibition in Alertmanager; every symptom alert routes on its own.
Recovery: ask for group_by: [alertname, service], an inhibit_rules entry so a critical alert mutes related warnings, and amtool config routes test output that shows each severity reaching the intended receiver.
Symptom: Loki exits at startup after an upgrade or a generated config.
Cause: legacy shared_store, boltdb-shipper or a schema older than v13 while structured metadata is on.
Recovery: add a new period in schema_config with store: tsdb and schema: v13 starting at a future date, keep the old period for existing data, and rerun -verify-config=true. Setting allow_structured_metadata: false is the stopgap until the new period starts.
Tag each of these in your incident log with the class of agent failure behind it — most are “invented API” or “stale training data” — so you can see which control to add next. The classes are defined in the failure taxonomy.
Where to go next with monitoring and observability
Section titled “Where to go next with monitoring and observability”- AI-powered incident response: use these signals and MCP servers when the alert fires.
- Agent observability: the same OpenTelemetry stack, pointed at your agents’ runs, tool calls and cost.
- Scoped AI incident triage: run the triage prompt under a read-only identity while a named human owns mitigation.
- Progressive delivery: the SLO alerts above become the rollback trigger for a canary.
- Production performance optimization: turn the latency histograms into targeted fixes.
- Monitoring and logging patterns: more recipes for logs, golden signals and burn-rate alerts.