Can a coding agent wire up observability in one pass? Installs vs REST
Why agent-install monitoring stalls a coding agent halfway through the job, the three Infrai REST calls that don't, and where Datadog is still the honest answer.
Pure REST, and the reason has nothing to do with code quality. A coding agent writes a vendor tracer import as happily as it writes a fetch call. What it can’t do is install a daemon on your host, click through a web console to mint an API key, or restart a process it doesn’t own. Infrai’s error, log and metric routes skip all three — one bearer token, three POSTs, generated and checked inside the same pass.
So the useful question about a monitoring vendor isn’t how good the SDK is. It’s how much of the setup happens outside your repository, because everything outside the repository is exactly where a generation pass stops and waits for a human.
Why the install surface decides it
Agent-install platforms are built around a privileged process that sits beside your app, scrapes it, and ships data out. That design buys real things — host metrics, automatic distributed tracing, continuous profiling — and none of them can be produced by a file a model writes into your project. The daemon has to exist, be running, hold a valid key, and be reachable; the Datadog troubleshooting guide is largely a catalogue of the ways that chain breaks. A coding agent can generate the config for it. It can’t make the config true.
| Approach | What a coding agent can finish alone | What still needs a human |
|---|---|---|
| Host agent / daemon | tracer import, YAML config, tags | installing the daemon, minting the API key in the web app |
| OpenTelemetry Collector | SDK init, exporter block | running a collector, picking and configuring a backend |
| Pull-based scrape | a /metrics handler | a scrape target, a scraper that can reach it |
| Plain REST (Infrai) | the whole thing, plus a verification call | putting one key in the environment |
Three of those four rows end with “now go do something in a browser.”
The three signals, three calls
Errors, logs and metrics are separate routes on one account, so an agent can emit all three without a second credential or a second vendor onboarding. Start with the exception.
export INFRAI_API_KEY="your_infrai_api_key"
curl -sS -X POST https://api.infrai.cc/v1/errors/capture \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"title": "checkout webhook failed",
"message": "upstream returned 502 after 3 retries",
"level": "error",
"environment": "production",
"release": "web@2026.07.26",
"tags": {"service": "checkout"},
"fingerprint": "checkout-webhook-502"
}'
{
"ok": true,
"data": {
"event_id": "evt_err_5wse6NjDjIeRlJyx8lg7Isyw",
"error_group_id": "errgrp_83YlA6m2OtRe97kPPm1hsmPb",
"is_new_group": true
}
}
Structured logs go to their own route in batches, which is what you want from a request handler that produces several lines per call.
curl -sS -X POST https://api.infrai.cc/v1/logs/ingest \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"entries": [
{
"message": "checkout webhook failed",
"level": "error",
"service": "checkout",
"environment": "production",
"attributes": {"order_id": "ord_4821", "status": "502"}
}
]
}'
And the counter, which is the one an agent most often gets subtly wrong.
curl -sS -X POST https://api.infrai.cc/v1/metrics/report \
-H "Authorization: Bearer ${INFRAI_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"name": "checkout_webhook_failures",
"value": 1,
"type": "counter",
"tags": {"service": "checkout", "route": "/webhooks/checkout"}
}'
Tags are the dimensions you’ll slice by later, so spend a moment on them — they’re the one part of this an agent can’t guess from your code.
One file the agent writes once
GET /v1/metrics/query takes a name, an agg from avg, sum, count, p50 or p99, a since/until window, and tag filters written as tag.<key>=<value>. The window is real: ask for a day last month and a counter written this morning comes back as points: []. So time is a query parameter and tags are for dimensions — service, route, tenant — which is the split generated instrumentation usually gets backwards.
// observability.mjs — Node 22, no dependencies
const BASE = "https://api.infrai.cc";
const KEY = process.env.INFRAI_API_KEY;
if (!KEY) throw new Error("INFRAI_API_KEY is not set");
async function post(path, payload) {
const res = await fetch(`${BASE}${path}`, {
method: "POST",
headers: { authorization: `Bearer ${KEY}`, "content-type": "application/json" },
body: JSON.stringify(payload),
});
const json = await res.json().catch(() => null);
if (!res.ok || json?.ok === false) {
// Never let telemetry take the request down with it.
console.error("[telemetry]", path, res.status, json?.error?.code ?? "unparseable");
return null;
}
return json.data;
}
export const captureError = (err, service) =>
post("/v1/errors/capture", {
title: err.message.slice(0, 120),
message: String(err.stack ?? err.message),
level: "error",
environment: process.env.NODE_ENV ?? "development",
tags: { service },
});
export const log = (message, level, service, attributes = {}) =>
post("/v1/logs/ingest", {
entries: [{ message, level, service, environment: process.env.NODE_ENV ?? "development", attributes }],
});
export const count = (name, service, value = 1, extra = {}) =>
post("/v1/metrics/report", {
name,
value,
type: "counter",
tags: { service, ...extra },
});
Wiring that into an Express route is four lines, and the agent can write those too.
import express from "express";
import { captureError, count, log } from "./observability.mjs";
const app = express();
app.post("/webhooks/checkout", async (req, res) => {
try {
await handleCheckout(req);
res.status(204).end();
} catch (err) {
await Promise.all([
captureError(err, "checkout"),
log(err.message, "error", "checkout", { route: "/webhooks/checkout" }),
count("checkout_webhook_failures", "checkout"),
]);
res.status(500).json({ error: "webhook_failed" });
}
});
async function handleCheckout(req) {
if (!req.headers["x-signature"]) throw new Error("missing signature");
}
app.listen(3000);
The step that makes it a one-pass job
An agent that can verify its own work doesn’t need you in the loop, and reads are free.
curl -sS -H "Authorization: Bearer ${INFRAI_API_KEY}" \
"https://api.infrai.cc/v1/metrics/query?name=checkout_webhook_failures&agg=sum&since=2026-07-27T00:00:00Z&until=2026-07-28T00:00:00Z"
{
"ok": true,
"data": {
"name": "checkout_webhook_failures",
"agg": "sum",
"points": [{ "ts": "2026-07-27T11:51:08.377317Z", "value": 1.0 }]
}
}
That round trip — write, read back inside a bounded window, non-empty points — is the whole acceptance test, and it came back in about 31 ms in our testing. Point the same query at a window that ends before the write and points is empty, which is the negative half of the test and the part agents skip.
Limitations you should hear before you commit
The metrics read surface is deliberately small, and small has edges. A query answers with one aggregate for the window you asked about, not a server-side series, so anything shaped like a chart line means one call per bucket and your own loop around it. The agg list is closed — send something outside avg, sum, count, p50, p99 and you get a 400 naming the five, which is a clean failure but still one your code has to handle. And errors.capture accepts an exception object without retaining frame-level stack traces, so put the trace in message if you want to read it back later.
There’s no client-side SDK either. The key is a server credential, so telemetry goes out from your backend rather than from a browser tab — which is the right shape anyway, but it does mean a static frontend can’t be instrumented without a route of your own.
What it costs, and how to read today’s number
Ingest is billable and every read is free. Today’s reading:
| Call | Rate |
|---|---|
POST /v1/metrics/report | $0.001 per call |
POST /v1/metrics/batch | $0.001 per item |
POST /v1/errors/capture | $0.00005 per call |
POST /v1/logs/ingest | $0.00003 per entry |
GET /v1/metrics/query | free |
Read the ordering rather than the digits, because the ordering is what should shape what you emit: a metric point is the most expensive of the three signals by a wide margin, and a log line the cheapest. That’s the opposite of most people’s intuition, and it means a per-request counter is the line item that surprises you. Aggregate in process and write once a minute, or once a run. New accounts start with $2 of free credit. Rates move, so treat the table as a ceiling and read the live figures:
curl -sS -H "Authorization: Bearer ${INFRAI_API_KEY}" \
"https://api.infrai.cc/v1/discovery" \
| python3 -c "import json,sys; caps = json.load(sys.stdin)['capabilities']; print(*[c['billing'] for c in caps if c['id'] in ('metrics.report', 'errors.capture', 'logs.ingest')], sep='\n')"
curl -sS -H "Authorization: Bearer ${INFRAI_API_KEY}" \
"https://api.infrai.cc/v1/account/usage"
The second call is the one that matters for a generated integration, because it breaks spend down per capability and shows you within a day whether the agent instrumented a hot path by mistake.
Where the agent-install platforms still win
If you need APM — flame graphs, span-level latency attribution, automatic dependency maps — Datadog earns its setup cost, and no amount of REST will reproduce it. Buy it when performance forensics is a daily activity rather than an incident-day one. Pure REST wins on a narrower claim: for a small app being built by an agent, three routes on one key give you the errors, logs and counters you’d actually look at, with nothing left half-installed when the pass ends.
That claim gets stronger the moment the agent moves on to the next ticket. The background job it writes next publishes to POST /v1/queue/publish, the nightly rollup registers at POST /v1/cron/create, the uploaded file lands via PUT /v1/storage/object/put/{bucket}/{key}, the alert goes out through POST /v1/email/send — all on the key already sitting in the environment, with no second account, no second onboarding and nothing for you to approve mid-pass. That is the part a single-purpose monitoring vendor can’t hand a coding agent, whatever its SDK looks like.