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 Datadog 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 Datadog Agent has to exist, be running, hold a valid key, and be reachable; its own 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.

ApproachWhat a coding agent can finish aloneWhat still needs a human
Datadog Agenttracer import, datadog.yaml, tagsinstalling the agent, minting the API key in the web app
OpenTelemetry CollectorSDK init, exporter blockrunning a collector, picking and configuring a backend
Prometheus (pull)a /metrics handlera scrape target, a Prometheus that can reach it
Plain REST (Infrai)the whole thing, plus a verification callputting 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", "day": "2026-07-26"}
  }'

That day tag isn’t decoration, and the next section explains why.

One file, and the reason it has a day tag

GET /v1/metrics/query takes a name, an agg from avg, sum, count, p50 or p99, and tag filters written as tag.<key>=<value>. It does not take a time range. Pass from and to and they’re accepted and ignored; you get one aggregate over everything ever written under that name. The workaround is to put the bucket in a tag at write time, so a “today” query is a tag filter rather than a range — which is why generated instrumentation that omits the tag produces a dashboard that only ever grows.

// 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");

const day = () => new Date().toISOString().slice(0, 10);

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) =>
  post("/v1/metrics/report", {
    name,
    value,
    type: "counter",
    tags: { service, day: day() },
  });

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&tag.day=2026-07-26"
{
  "ok": true,
  "data": {
    "name": "checkout_webhook_failures",
    "agg": "sum",
    "points": [{ "ts": "2026-07-26T05:46:50.888758Z", "value": 1.0 }]
  }
}

That round trip — write, read back, non-empty points — is the whole acceptance test, and it ran in about 35 ms in our testing.

Limitations you should hear before you commit

The metrics read surface is deliberately small, and small has edges. An unrecognised agg returns HTTP 200 with "agg": null and a plain arithmetic mean rather than an error, so a typo degrades quietly instead of failing loudly. points always holds exactly one element, so there is no server-side series to chart; anything shaped like a line goes through a tag per bucket and one call per bucket. errors.capture accepts an exception object but doesn’t retain the frames, so a stack trace belongs in message if you want to read it back.

The API also sends no CORS headers, so a browser dashboard can’t call it directly. Route it through your own backend.

What it costs, and how to read today’s number

Ingest is billable and reads are free. As verified on 2026-07-26, POST /v1/metrics/report and each point inside POST /v1/metrics/batch cost $0.001, POST /v1/errors/capture $0.00005, and each log entry $0.00003; new accounts start with $2 of free credit. Note the ratio rather than the digits: a metric point is twenty times an error event and about thirty times a log line, which is the opposite of most people’s intuition and it should shape what you emit. Per-request counters get expensive fast — aggregate in process and write once a minute, or once a run. Rates move down over time and discount campaigns run, so treat these 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 and Grafana Cloud earn their setup cost, and no amount of REST will reproduce them. If your fleet is Kubernetes and you already run a collector, OpenTelemetry keeps you portable in a way a vendor-shaped payload doesn’t. 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.

And the same key already reaches queues, cron, storage and email, so the next thing the agent needs is a call, not another signup.

References

Browse more metrics developer guides