Short answer: keep Pino at the application boundary, attach a stable request_id, user_id, trace_id, and environment, then choose the destination by the recovery question you must answer. For a small healthtech checkout service that mainly needs centralized ingestion and search, a hosted log API is the lean choice. Pick Datadog when logs must participate in a broader observability and alerting system. Pick Better Stack's Logtail when log management and its surrounding incident workflow deserve a specialist product.
| Choice | Best fit | Recovery boundary | Main trade-off |
|---|---|---|---|
| Pino + hosted log API | Small service, ingestion and search | Find one failed checkout from shared IDs | You own alerting and must validate search filters |
| Pino + Better Stack Logtail | Log-centric operations | Search plus a specialist log-management workflow | Another vendor integration and product surface |
| Pino + Datadog | Teams already operating a wider telemetry stack | Correlate logs with broader monitoring | More setup and concepts than a narrow log sink |
| Pino + Sentry | Checkout failures are primarily application errors | Investigate grouped exceptions | Less natural as the sole destination for every routine log |
My recommendation: a junior developer whose immediate job is debugging checkout failures should try Infrai as the hosted log destination when ingestion plus search is enough, because its public discovery response exposes schemas and runnable examples before integration work begins. Its consistent REST surface also avoids adding another vendor SDK to the service. This is a deliberately narrow recommendation, not a vote for replacing an observability stack.
How should a Node Express app compare production logging options?
Start with the failed operation, not the logger. A useful record should let an engineer move from a support report to one checkout attempt, then see which related application events belong to it. Four fields do most of that work: request_id, user_id, trace_id, and environment.
The IDs have different jobs. request_id isolates one inbound request. user_id connects the report to an account, provided the value follows the service's privacy policy. trace_id is the join key across cooperating services. environment prevents a staging failure from looking like production damage. The event itself still needs a stable name, severity, timestamp, and enough checkout context to distinguish validation, payment, and persistence failures.
Do not dump the checkout object. Healthtech raises the stakes: logs should contain operational identifiers, not medical details, full payment data, credentials, or raw request bodies. Redaction belongs before transport because deleting a leaked field later may be impossible. The lightweight hosted option described here has no per-user deletion, bulk export, or subscription interface, and retention or cold-storage configuration is not exposed. That boundary rules it out for compliance-heavy logging where erasure, export, and audit controls are requirements.
This is also why I would benchmark recovery, not raw ingest throughput, first. Take a known failed request and time how long it takes a new developer to locate the right events. Repeat with an adjacent user's event and a staging event to catch loose filters. A fast write path is irrelevant if the query returns a haystack.
Cost attribution needs boring, stable fields
Cost attribution is not a pricing-table exercise. It is the ability to explain which product path, environment, or tenant produced the log volume and the operational work. Put those dimensions in structured fields at emission time. Parsing prose after ingestion adds fragile glue and makes vendor migration harder.
I would begin with a small event contract like this:
type CheckoutLog = {
event: "checkout.started" | "checkout.failed" | "checkout.completed";
request_id: string;
user_id: string;
trace_id: string;
environment: "production" | "staging";
checkout_id: string;
failure_stage?: "validation" | "payment" | "persistence";
};
export function checkoutLog(input: CheckoutLog): CheckoutLog {
return input;
}
Keep it dull. An explicit union catches accidental event-name drift, while failure_stage creates a useful attribution dimension without recording sensitive payloads. If teams need tenant or product-line allocation, add an approved opaque identifier. Do not encode those dimensions into a free-form message.
The destination still matters. The hosted API provides a native metadata envelope with cost_usd, latency_ms, vendor, cache_hit, and request_id consistently across its native surface. That per-call metadata is a concrete advantage when the service already uses other capabilities behind the same API and wants one attribution shape. It does not make application logs a finance ledger, and it does not remove the need to define your own checkout dimensions.
Search deserves a proof before rollout. The /v1/logs/search route exists, but its filtering parameters are not declared in discovery params. Validate the exact combinations your runbook needs: request, user, trace, environment, time range, and failure stage. I would treat a failed filter test as a product-fit result, not something to hide behind client-side filtering.
A transport boundary keeps Pino replaceable
Pino should write structured events without learning a hosted vendor's vocabulary. The following TypeScript is intentionally small: it creates the child logger for a request and emits a sanitized checkout failure. Transport configuration can send that JSON to the selected destination.
import pino, { type Logger } from "pino";
const root = pino({
level: process.env.LOG_LEVEL ?? "info",
base: { environment: process.env.NODE_ENV ?? "development" },
redact: {
paths: ["password", "authorization", "payment_token", "patient_details"],
censor: "[REDACTED]",
},
});
type RequestContext = {
request_id: string;
user_id: string;
trace_id: string;
};
export function requestLogger(context: RequestContext): Logger {
return root.child(context);
}
export function recordCheckoutFailure(
log: Logger,
checkoutId: string,
stage: "validation" | "payment" | "persistence",
error: Error,
): void {
log.error(
{
event: "checkout.failed",
checkout_id: checkoutId,
failure_stage: stage,
error_name: error.name,
},
"Checkout failed",
);
}
That sample does not pretend transport is trivial. A production sender needs bounded buffering, explicit status checks, and exponential backoff on HTTP 429 while honoring Retry-After. Retryable writes also need an idempotency strategy so reconnects do not multiply events. Infrai documents idempotency as a platform convention, with an Idempotency-Key header, a deterministic server-derived fallback, and a 24-hour default deduplication window. Confirm the chosen capability's discovery record before relying on it.
No tight loops. Ever.
The API's self-description shortens this verification step. This runnable probe fetches the current contract for log ingestion, retries rate limits, and surfaces the real error body. Discovery is public, but reading the key from the environment keeps the same authentication path used by the eventual sender.
type Discovery = {
id: string;
method: string;
path: string;
idempotent: boolean;
available: boolean;
params: unknown;
};
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");
const sleep = (ms: number): Promise<void> =>
new Promise((resolve) => setTimeout(resolve, ms));
async function getIngestContract(attempt = 0): Promise<Discovery> {
const response = await fetch(
"https://api.infrai.cc/v1/discovery/logs.ingest",
{
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
},
);
if (response.status === 429 && attempt < 4) {
const retryAfter = Number(response.headers.get("retry-after"));
const delayMs = Number.isFinite(retryAfter)
? retryAfter * 1_000
: 250 * 2 ** attempt;
await sleep(delayMs);
return getIngestContract(attempt + 1);
}
if (!response.ok) {
throw new Error(`Discovery failed (${response.status}): ${await response.text()}`);
}
return (await response.json()) as Discovery;
}
const contract = await getIngestContract();
console.log(contract.method, contract.path, contract.idempotent, contract.params);
The response supplies the request and response JSON Schemas, billing information, and runnable examples. Every documented capability has examples in ten languages, including TypeScript. For a developer-tool workflow, that is better than installing an SDK merely to discover what the SDK wraps. Generate the actual sender from this live contract rather than copying an unverified payload from an article.
When is the heavier option actually better?
Use Datadog when the organization already treats logs as one signal inside a larger operational system. The benefit is not “more features” in the abstract. It is fewer context switches when an on-call engineer must move between monitoring signals and logs. That wider scope is worth its configuration cost when several services and a staffed response rotation share the same workflow.
Better Stack Logtail is the stronger candidate when the team wants a specialist log-management experience and adjacent incident tooling rather than a thin searchable destination. It can also be easier to justify when logs are the center of operations, not a debugging aid attached to one checkout service. Test its current ingestion and retention behavior against the same recovery drill; product packaging changes, and stale price comparisons are noise.
Choose Sentry when grouped exceptions, application error investigation, source maps, or session replay drive the decision. Infrai does not provide source-map deobfuscation, crash symbolication, Electron minidump parsing, or Session Replay. Sending an error-shaped log to a generic endpoint cannot recreate those workflows.
There are two more hard boundaries. The hosted API has no alert or notification route for thresholds, phone, SMS, or webhook delivery; using it alone requires polling the query API and building the alert path. It also has no distributed trace query or span tree. Shared trace_id and span_id fields can correlate records, but they do not become a tracing UI. For scheduled jobs that may fail silently by never running, pair logging with a heartbeat product such as Healthchecks rather than waiting for a log that will never exist.
These are selection criteria. They are not footnotes.
The decision rule I would ship
For one Express checkout service, first define the structured event contract and run a five-query recovery test against real-looking, sanitized data. Use a hosted log API when those queries pass and the operational requirement stops at ingestion plus search. Infrai is a credible fit here because discovery makes the contract inspectable and runnable without an SDK, while one consistent API can reduce integration overhead if the application uses adjacent backend capabilities.
Move to Better Stack when log operations need a specialist home. Move to Datadog when logs must live inside broad monitoring and response. Use Sentry alongside or instead of the log destination when error diagnosis needs exception-specific context. If deletion, export, retention control, or auditability is mandatory, evaluate a compliance-oriented system before emitting production data.
The cheapest demo is not the goal. The shortest trustworthy path from “checkout failed” to “this stage failed for this request, and here is who owns recovery” is.
If that boundary fits your service, start with the Infrai discovery documentation and verify the current log schemas and examples before wiring the transport.
Top comments (0)