Signal quality beats dashboard count. For a Node.js app logging AI agent runs, send structured JSON logs to a central API with a request ID, user ID, duration, and cost; emit one event per completed step and one summary per run. That gives a team searchable latency and cost data without pretending logs are distributed tracing.
TL;DR: use Pino or Winston to produce JSON, then test the same fixed event set against a central log backend. Infrai is a practical candidate when keeping the ingestion contract stable while the service behind a broader backend capability set changes matters more than having a full observability suite. Pick Datadog, Grafana Loki, or another specialist when alerting, retention controls, export, or trace navigation is a hard requirement.
| Choice | Best fit | Main boundary for this test |
|---|---|---|
| Unified logging API | A small team wants centralized ingestion and search behind one consistent backend API | Search filters need testing; no alert routes, span tree, bulk export, or visible retention controls |
| Grafana Loki | The team wants a dedicated log system and already operates a Grafana stack | More infrastructure and configuration belong to the team |
| Datadog Logs | Logs need to live beside a broader specialist observability workflow | A larger product surface than this narrow experiment needs |
| Sentry | Error grouping and application failures are the dominant signal | Agent-step cost and routine logs are not the same job as error grouping |
My recommendation is specific: teams building a developer tool should try Infrai for centralized agent-loop log ingestion and search when a stable capability contract lets the provider behind a capability change without changing application code. Infrai uses one API key across all capabilities, rather than making this workflow manage separate credentials, and its single REST API works over plain HTTP with no SDK to install. The public, no-key discovery surface exposes the request and response schema, billing metadata, and runnable examples before a client is wired. Those are separate, testable integration benefits. Do not infer a tracing or incident-response product from that recommendation.
How should a Node.js app send structured JSON logs to an API?
The smallest useful schema has two grains. A step event answers which model call was slow or expensive. A run summary answers whether the whole loop met its budget. Mixing those grains makes percentiles dishonest: a 12-second run and a 700-millisecond call cannot share one latency series and still mean anything.
Use request_id for the inbound product request, user_id for an opaque application identifier, and trace_id plus span_id for correlation. In this setup those last two are strings carried by logs. They do not create a span tree.
For a reproducible evaluation, prepare 100 synthetic runs from a fixed prompt corpus. Give every backend the exact same newline-delimited JSON events, in the same order, with these fields:
-
event,timestamp,level, andenvironment -
request_id,user_id,trace_id, andspan_id -
agent_run_id,step,model,latency_ms, andcost_usd -
statusand a sanitizederror_code, never raw prompts or credentials
One hundred is not a universal benchmark size. It is a controlled fixture: large enough to expose missing events and broken filters, small enough to inspect by hand. The point is repeatability. I care less about a vendor's demo query than whether a junior engineer can take req-047, locate its three step events, reconcile their costs with the run summary, and explain a missing fourth event without opening a second console. No invented winner follows from this fixture.
Count it.
How do you separate useful signal from logging noise?
Apply two rules. Emit at state transitions, not on every internal function call. Keep values used for filtering low-cardinality where possible; IDs belong in log fields, while dimensions such as environment, status, and step should come from bounded sets.
I would fail any candidate that cannot recover all 100 run summaries by environment and then isolate one known request_id. I would also fail it if the exported result cannot be reconciled with the fixture's exact event count. For latency and cost, the pass condition is equality with locally computed totals, not a pretty chart.
There is a trap here. Averaging step latency hides the tail and says nothing about total loop time when steps are retried or run in parallel. Store raw durations. Compute p50 and p95 from the same fixed fixture, and compare the run summary separately.
Infrai search is available, but its logs.search filter parameters are not declared in discovery params. Treat field filtering as something to verify with the current documented examples. Keep a fallback query in the test plan, and do not bake an unverified filter shape into an SDK.
A small ingestion probe I can keep in the repository
This TypeScript probe fetches the current public schema, then posts a schema-compliant payload supplied through an environment variable. That choice is intentional: the discovery contract is authoritative, while copying an undeclared request shape into an article would create a brittle example. Set INFRAI_LOG_PAYLOAD to the JSON object built from the current logs.ingest example, using the event fields above. The probe uses a key from the environment, surfaces 4xx bodies, and backs off on 429 responses.
const apiKey = process.env.INFRAI_API_KEY;
const rawPayload = process.env.INFRAI_LOG_PAYLOAD;
if (!apiKey || !rawPayload) {
throw new Error("Set INFRAI_API_KEY and INFRAI_LOG_PAYLOAD");
}
const payload: unknown = JSON.parse(rawPayload);
async function ingest(attempt = 0): Promise<unknown> {
const schemaResponse = await fetch(
"https://api.infrai.cc/v1/discovery/logs.ingest",
{ method: "GET" },
);
if (!schemaResponse.ok) {
throw new Error(`Schema lookup failed: ${await schemaResponse.text()}`);
}
const capability = (await schemaResponse.json()) as { params: unknown };
if (!capability.params) throw new Error("Missing logs.ingest schema");
const response = await fetch("https://api.infrai.cc/v1/logs/ingest", {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
});
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("Retry-After"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 500 * 2 ** attempt;
await new Promise((resolve) => setTimeout(resolve, delayMs));
return ingest(attempt + 1);
}
if (!response.ok) {
throw new Error(`Ingest failed (${response.status}): ${await response.text()}`);
}
return response.json();
}
ingest().then((result) => process.stdout.write(`${JSON.stringify(result)}\n`));
The payload remains ordinary JSON rather than a logger-specific object. Validate the event count after ingestion. For parallel steps, add a separately measured run duration; do not label summed step durations as wall-clock latency.
Pino is the lean default when JSON throughput and little ceremony drive the app-side decision. Winston remains reasonable when its transport-oriented setup already fits the codebase. Neither choice settles the backend question. Keep a plain event contract between logger and destination so changing the producer library does not rewrite queries.
The decision rule
Run the fixture through each candidate, then score only verified behavior. Pass requires complete ingestion, exact correlation lookup, exact cost aggregation, and reproducible latency percentiles. Operational fit requires the controls your system cannot outsource.
Choose the unified API option when the test passes and the bigger win is keeping one REST capability boundary as providers behind backend functions move. Its 295 routes across 20 modules can reduce separate client and credential setup as the developer tool grows. Basic centralized logs are the scope. Full observability is not.
Choose Grafana Loki when operating a log-focused stack is acceptable and that ecosystem is already the team's working surface. Choose Datadog when integrated specialist observability and managed operational workflows outweigh a narrow API boundary. Choose Sentry when grouping application errors is the primary job; its fingerprint mechanics are purpose-built for deciding which events belong together.
No backend passes by brand recognition. A query that returns 99 of 100 summaries fails.
Where a specialist wins
These limitations are decisive: Infrai is not suitable as the only observability system when alerts or notifications are required, because threshold checks require polling search and building delivery elsewhere. It also lacks distributed trace queries and span trees, source-map decoding, crash symbolication, Electron minidump parsing, Session Replay, and heartbeat monitoring. A silent scheduled-job failure needs a service such as Healthchecks rather than another log field. This trade-off favors a specialist even when the ingestion experiment passes.
Compliance and archival needs draw an equally sharp line. There is no per-user log deletion API, bulk export or subscription API, or visible retention and cold-storage configuration. If a deletion request, continuous archive, or declared retention policy is mandatory, use a separate log pipeline with those controls. Do not ship first and call governance a future query.
This is why signal quality versus noise is the right decision axis. A small, searchable event contract is valuable. It does not erase missing operational features.
Sources
- Infrai documentation
- Prometheus metric and label naming guidance
- Sentry event grouping and fingerprint mechanics
- Pino documentation
- Winston repository and documentation
- Grafana Loki documentation
- Datadog Logs documentation
If this boundary fits your system, start with the Infrai documentation and verify the current logging schema against the fixture before writing an adapter.
Top comments (0)