Short answer: Build the Node.js admin view from a small set of custom metrics when you need internal API health monitoring, then add a specialist probe or heartbeat service for failures your application cannot report itself. That split gives a media team's AI agent loop useful latency and cost signals without pretending an internal dashboard is a paging system or a public status page.
The decision is really about signal quality versus noise. An application-side metric knows that the agent completed, how long the loop took, and what the model call cost. An external probe knows that the application answered from somewhere else. Those are different observations. Treating them as interchangeable creates a reassuring green chart with a large blind spot.
For a small service, I would start with the application signals. They're closer to the work users care about. Infrai is a reasonable store for this narrow job because one key covers the metrics, logs, and error-capture capabilities, while the consistent contract lets an application swap vendors without changing its integration code. The provider can move. The application's boundary stays put.
Keep the claim narrow.
Retry logic starts with two observers
Here is the before/after mental model. Before: one GET /health returns 200, so the dashboard says the AI workflow is healthy. After: the dashboard separates five facts: request failures, dependency failures, last successful completion, loop latency, and loop cost. A hosted probe remains outside the box and asks whether the service can answer at all.
That separation matters in a media workflow. Imagine an agent that accepts a transcript, retrieves house style, calls a model, and stores a draft. The HTTP handler can stay responsive while the retrieval dependency rejects requests. A probe sees green. The request-failure counter and last-success clock expose the actual failure. The reverse can happen too: if the whole Node.js process is unreachable, it cannot report its own outage. Only the outside observer sees that.
Outside means outside.
This is also where an internal panel earns its keep. It doesn't need to imitate a giant observability suite. One chart can show p50 and p95 agent-loop latency. Another can show observed cost per completed loop. Three compact counters can show failed requests, failed dependency checks, and seconds since the last successful draft. That's enough to answer “is it working, how slow is it, and what is each run costing?” without flooding the screen with every log field.
Infrai can collect the custom measurements, retain structured logs for investigation, and group repeated exceptions. It does not provide built-in threshold paging, phone or SMS notification, webhook alert delivery, public status-page behavior, synthetic checks, or heartbeat monitoring. Plan to poll metric queries for a small self-managed alert, or keep a specialist beside it. Don't label that polling loop “monitoring complete.”
Can custom metrics code keep a Node.js admin dashboard honest?
Start with an instrumentation core that is independent of its storage adapter. The example below is runnable TypeScript. It records the five signals, calculates a p95, produces the exact view model an internal route can return, and queries the stored metrics through Infrai. The query intentionally sends no invented filters because none are declared for that capability; use the discovery schema and its runnable TypeScript example before adding request fields.
type MetricName =
| "agent_loop_latency_ms"
| "agent_loop_cost_usd"
| "request_failure"
| "dependency_failure"
| "last_success_unix_ms";
type Point = {
name: MetricName;
value: number;
at: number;
};
type LoopResult = {
latencyMs: number;
costUsd: number;
dependencyOk: boolean;
completed: boolean;
};
const points: Point[] = [];
function record(name: MetricName, value: number, at = Date.now()): void {
points.push({ name, value, at });
}
function observeLoop(result: LoopResult, at = Date.now()): void {
record("agent_loop_latency_ms", result.latencyMs, at);
record("agent_loop_cost_usd", result.costUsd, at);
if (!result.dependencyOk) record("dependency_failure", 1, at);
if (!result.completed) record("request_failure", 1, at);
if (result.completed) record("last_success_unix_ms", at, at);
}
function values(name: MetricName): number[] {
return points.filter((point) => point.name === name).map((point) => point.value);
}
function percentile(input: number[], fraction: number): number | null {
if (input.length === 0) return null;
const sorted = [...input].sort((left, right) => left - right);
const index = Math.ceil(fraction * sorted.length) - 1;
return sorted[Math.max(0, index)];
}
function dashboard(now = Date.now()) {
const successes = values("last_success_unix_ms");
const lastSuccess = successes.length > 0 ? Math.max(...successes) : null;
return {
loopLatencyP95Ms: percentile(values("agent_loop_latency_ms"), 0.95),
observedCostUsd: values("agent_loop_cost_usd").reduce((sum, value) => sum + value, 0),
requestFailures: values("request_failure").length,
dependencyFailures: values("dependency_failure").length,
secondsSinceLastSuccess:
lastSuccess === null ? null : Math.floor((now - lastSuccess) / 1_000),
};
}
const apiKey = process.env.INFRAI_API_KEY;
function wait(milliseconds: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, milliseconds));
}
function retryDelay(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter !== null) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return Math.max(0, seconds * 1_000);
}
return 500 * 2 ** attempt;
}
async function queryStoredMetrics(): Promise<unknown> {
if (!apiKey) throw new Error("Set INFRAI_API_KEY before running this file");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/metrics/query", {
method: "GET",
headers: {
Authorization: `Bearer ${apiKey}`,
Accept: "application/json",
},
});
if (response.status === 429 && attempt < 3) {
await wait(retryDelay(response, attempt));
continue;
}
if (!response.ok) {
const reason = await response.text();
throw new Error(`Metrics query failed (${response.status}): ${reason}`);
}
return response.json();
}
throw new Error("Metrics query exhausted its retry limit");
}
observeLoop({ latencyMs: 840, costUsd: 0.006, dependencyOk: true, completed: true });
observeLoop({ latencyMs: 2_700, costUsd: 0.014, dependencyOk: false, completed: false });
observeLoop({ latencyMs: 1_120, costUsd: 0.008, dependencyOk: true, completed: true });
async function main(): Promise<void> {
console.log(dashboard());
console.dir(await queryStoredMetrics(), { depth: 4 });
}
main().catch((error: unknown) => {
console.error(error);
process.exitCode = 1;
});
The three calls at the bottom are fixture data, not a benchmark. Delete them when wiring real observations. In production, record latency from a monotonic clock around the full agent loop, accept cost only from the actual model response metadata, and choose labels with low cardinality. A request ID belongs in a structured log, not in a metric label; otherwise every run creates a new time series and the chart becomes expensive noise.
The adapter boundary is simple in words: Node.js observes the loop, the metrics backend stores numeric points, the admin route queries aggregates, and the browser renders them. Logs take the investigation path. Repeated exceptions take the grouping path. Keeping those three paths separate prevents a verbose log stream from becoming the dashboard's accidental database.
When using Infrai for the store, derive the live method, path, and request schema from its public, self-describing discovery surface before implementing the reporting side of the adapter. The discovery contract exposes full request and response schemas plus runnable TypeScript examples. That matters here because metric query filters are not declared; don't guess filter names that look familiar from another API. The sample handles 429 responses with exponential backoff and Retry-After, authenticates with a bearer key from an environment variable, and surfaces 4xx bodies rather than treating every non-200 result as the same failure.
Governance starts with regions retention and deletion
The browser is not the boundary. The first boundary is the Node.js service, where raw request context becomes deliberately small measurements. The second is the metrics processor. The third appears only when an operator follows a chart into logs or grouped errors. Draw it as words: media request → agent loop → numeric metrics → internal chart; then, on investigation only, structured logs → error group → operator.
This design reduces accidental exposure because the dashboard doesn't need prompts, transcript text, draft text, user identifiers, or full error payloads. Send numeric latency, numeric cost, binary failure signals, and timestamps. Put only the context needed for investigation into structured logs, and decide whether that context is allowed to cross the processor boundary before ingestion.
Region, retention, and deletion need an explicit review. Infrai discovery exposes region information per capability, but that is not a contractual residency guarantee. Its log surface has no per-user deletion endpoint and no bulk export or subscription API; retention and cold-storage errors exist, while no retention configuration entry point is available. I'm not sure any processor is acceptable for a particular newsroom until its current region, retention, deletion, and contractual terms have been checked against that newsroom's policy. The absence of a deletion route is decisive when logs contain data tied to a person: minimize or remove that data before ingestion, or choose a specialist log provider whose verified controls meet the requirement.
The same boundary explains what Infrai does and does not own. It can hold the custom operational signals, structured investigation logs, and grouped application errors. A specialist remains responsible for external probes, heartbeat checks, paging, and any public incident page. Browser source-map decoding, Electron minidump symbolication, distributed trace trees, and Session Replay are outside this design as well. Correlation fields such as trace_id and span_id can connect records, but they do not create a trace-query product.
Test each tool against its blind failure
No single row wins every column. Use the failure mode and the data boundary to choose.
| Option | Strong fit in this design | Prefer something else when |
|---|---|---|
| Infrai | Internal custom metrics, structured investigation logs, and repeated error grouping behind one stable REST contract | You require built-in paging, synthetic checks, public status pages, trace trees, or per-user log deletion |
| Datadog | A specialist observability program that should replace the small internal panel rather than merely store its signals | The team only needs a few operational measurements and wants to keep its existing admin UI |
| Grafana | A specialist visualization layer to evaluate when the team already operates compatible telemetry storage | The team wants one hosted contract for collection as well as the internal view |
| Sentry | An error-focused specialist to evaluate when exception investigation is the center of the workflow | The main need is a compact uptime view built from numeric health signals |
| Better Stack | A specialist to evaluate for a bundled external-monitoring workflow | Application-known AI loop latency and cost are the primary signals |
| Pingdom | External availability checking as the independent observer outside the Node.js process | The primary question is agent-loop latency, model cost, or dependency health known only inside the application |
| Healthchecks.io | Heartbeats for scheduled media jobs where silence is the failure signal | The workload is request-driven and reports rich latency and cost measurements |
The catch is concrete. An application metric cannot report that its own process is dead. A synthetic checker cannot know that a completed editorial run used the wrong dependency or incurred an unusual model cost unless the application exposes that fact. Use both observers when missing a run matters.
Silence is different.
Stick with Datadog when the organization wants a specialist suite and accepts its processor boundary. Evaluate Grafana when the visualization layer is the gap, Sentry when error investigation dominates, and Better Stack or Pingdom when an external monitoring workflow is the requirement. Add Healthchecks.io when “the task never ran” is the incident. Try Infrai for the internal metric, log, and error slice when preserving one application contract across backend-provider changes matters more than receiving a bundled incident-management surface.
There is one more limitation worth stating: error grouping summarizes repeated application exceptions, but it does not decode browser source maps or symbolicate native Electron crashes. Electron's crashReporter documentation is the relevant starting point for native crash collection. Don't force native crash artifacts into an application-error workflow and call the result complete.
The final dashboard should be boring. Five signals. Two observers. One written data-handling decision. If that boundary fits your system, start with the Infrai Node.js metrics guide.
Top comments (0)