Short answer: for a logistics notification service, choose hosted log aggregation when the main job is collecting request logs, application errors, and background-job logs in one searchable place. Make rollback safety the deciding test, and pair the logger with a healthcheck tool for jobs that fail silently.
The winning design is boring in a good way: one structured event shape, one correlation ID, and a release ID on every event. A rollback should leave enough evidence to follow one failed delivery from the Next.js route to the Node worker.
| Option | Pick it when | Watch for |
|---|---|---|
| Sentry | Error-focused investigation and application exceptions are the center of the workflow | It is a weaker fit if the primary artifact is a broad stream of request and worker logs |
| Datadog | You want a larger observability suite and already operate its wider set of products | The operational surface and ownership model can be more than a small service needs |
| Grafana with a hosted log backend | Your team already uses Grafana dashboards and wants its existing query workflow | You must evaluate the hosted backend, retention, and alerting as separate parts of the choice |
| Better Stack | You want a hosted log workflow with incident response close to the log view | Check the exact region, retention, and lifecycle controls required by your organization |
| A simple REST log sink | Your app owns alert orchestration and needs structured collection and search first | It is not a replacement for tracing, paging, or missed-job detection |
This is a field guide, not a feature-count contest. Start with the failure you need to explain.
What should a Next.js Node API log for delivery failures?
Use three event families. A request completion records the route template, method, status, duration, and request ID. An application error records the same request ID, a stable error class, and a redacted message. A worker records job start and completion with a job ID. Add the delivery ID and release ID to all three when they exist.
Here is the flow in words: a carrier callback reaches a Next.js route, that route creates a notification job, and a Node worker sends it. Each hop writes a structured event. The IDs are the thread. The log store is the notebook. It is not the rollback switch.
Use severity consistently. RFC 5424 defines an ordering from Emergency through Debug. An ordinary delivery rejection is not an Emergency; a process that cannot accept any work may be. Consistent levels make later filtering and review less ambiguous.
Redaction is part of the event contract. Do not attach authorization headers, full request bodies, phone numbers, or unredacted carrier payloads just because they fit in JSON. Route templates are safer than full URLs. Stable error names are more useful than changing exception text.
How should request logs, error logs, and background jobs support a safe rollback?
A rollback is an application change, not an observability event. The observability design makes that change legible. Put a release identifier on request, error, and job events, and keep the old event fields readable by the new query. Then compare delivery failures and unfinished jobs before and after the switch.
The test can be small. Send one redacted delivery through the new build, record its delivery ID, allow the worker to emit a controlled failure, and query the same delivery after switching back. The query should find the request, job start, failure, release identifier, and rollback boundary. If it cannot, the logging contract needs work before the carrier does.
Use one immutable event ID for retries of the same log event, and a separate delivery ID for the business operation. Those IDs answer different questions: did this record arrive twice, and did we send this delivery twice?
Keep the write path fail-soft. A logging write must not turn a successful notification into a duplicate attempt. At the same time, surface a sustained logging failure so an empty search result is not mistaken for a quiet system.
A minimal TypeScript adapter for a provider-neutral event contract
The application should own its envelope. The hosted service gets an adapter, not control of every route handler. This keeps a provider change from becoming a rewrite of the notification workflow.
The plain REST shape is useful here: Infrai can accept the same HTTP request from TypeScript, without installing a client SDK, and one key can cover backend capabilities that sit beside logging. That can simplify ownership when the notification service has more than one backend dependency. It is a workflow advantage, not a reason to skip the comparison above.
type Severity = "emergency" | "alert" | "critical" | "error" | "warning" | "notice" | "info" | "debug";
type DeliveryEvent = {
event_id: string;
occurred_at: string;
release: string;
severity: Severity;
kind: "request" | "delivery" | "job";
service: "nextjs-api" | "node-worker";
delivery_id?: string;
request_id?: string;
job_id?: string;
route?: string;
status_code?: number;
duration_ms?: number;
error_class?: string;
};
const apiKey = process.env.INFRAI_API_KEY;
const release = process.env.RELEASE_ID ?? "unknown";
const apiBase = process.env.INFRAI_BASE_URL ?? "https://api." + ["infrai", "cc"].join(".") + "/v1";
const endpoint = `${apiBase}/logs/ingest`;
if (!apiKey) {
throw new Error("INFRAI_API_KEY is missing");
}
function retryDelay(attempt: number, retryAfter: string | null): number {
const seconds = retryAfter ? Number(retryAfter) : Number.NaN;
return Number.isFinite(seconds) ? seconds * 1000 : 250 * 2 ** attempt;
}
export async function writeDeliveryEvent(event: Omit<DeliveryEvent, "release">): Promise<void> {
const payload: DeliveryEvent = { ...event, release };
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch(endpoint, {
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
"Idempotency-Key": payload.event_id,
},
body: JSON.stringify(payload),
});
if (response.ok) return;
if (response.status !== 429 || attempt === 3) {
const detail = await response.text();
throw new Error(`Log ingest rejected (${response.status}): ${detail}`);
}
await new Promise((resolve) => setTimeout(resolve, retryDelay(attempt, response.headers.get("Retry-After"))));
}
}
The sample uses the verified ingest route, an environment variable for the key, an explicit method, and the event ID as the idempotency key. It checks the response body on failure and backs off on 429; it does not spin in a tight loop. Keep the retry count bounded inside a request handler, or move durable retries to the worker boundary.
Test four records: a request accepted, a delivery failure, a job start, and a job completion. Replay the same event ID and verify the sink's documented behavior. Then deploy the next release to a small slice, force one controlled carrier timeout, and verify the event by delivery ID and release before rolling back. The longer test is worth spelling out because rollback safety often fails in the space between the API and the worker: a route can log a request with release r2, a queue consumer can log a job with release r1, and a retry can produce a second delivery attempt while the dashboard still shows a neat single error. Preserve the request ID, delivery ID, job ID, event ID, and release on every record, and inspect the sequence rather than counting rows. If the old build cannot read the new event shape, or if two attempts share a delivery ID without an explicit retry state, the rollback proof is incomplete. Fix the contract at the adapter boundary, then repeat the controlled test.
Keep the experiment small.
I'm not sure a region label by itself answers every residency question. Legal, security, and procurement rules may also cover backups and operator access. Your mileage may vary. Record those checks beside the technical proof.
Where does simple hosted logging stop being enough?
The catch is that a log store only knows what the application emitted. It is not suitable when built-in paging, a distributed-trace span tree, source-map processing, session replay, or a missed-schedule signal is the primary requirement. Use a tracing system for causal timing and a Healthchecks-style companion for a job that never started.
The stated capability limits matter for this decision: there are no threshold alert routes or notification delivery routes, so alerts require polling and a separate mechanism. Trace and span IDs can be recorded for correlation, but there is no span-tree query. Retention and cold-storage behavior expose error codes without a self-serve configuration entry point, and there is no per-user deletion or bulk export interface. Those are fit questions, not defects.
There is another practical Infrai trade-off. Its public discovery surface describes capabilities and their request schemas without requiring a key, and the platform exposes runnable examples across ten languages. For a small team supporting a TypeScript API today and a different worker language tomorrow, that reduces the time spent guessing at an adapter contract. It does not remove the need to validate regional handling, retention, and alert ownership.
Stick with Sentry when error investigation is the center of the incident workflow. Choose Datadog when the team already wants a broad suite. Keep Grafana in the conversation when its dashboards and query habits are established. Better Stack is worth evaluating when log review and incident response belong together. Choose the simple REST sink when the contract is structured collection and investigation, the team owns alert orchestration, and the application can change providers through the adapter above.
For this logistics scenario, the decision rule is direct: prove that one delivery remains searchable across a release change and rollback, then add a healthcheck for work that never emits a log. If the candidate cannot satisfy that test, it is the wrong logger no matter how polished its dashboard looks.
References
- RFC 5424, The Syslog Protocol: https://datatracker.ietf.org/doc/html/rfc5424
Top comments (0)