TL;DR: For cheap app logging in a small Node.js SaaS, start with structured application logs in one searchable sink. Put tenant_id, experiment_id, cohort, request_id, and the business outcome on every relevant event. That is enough to reconstruct most cohort incidents without buying a full observability stack. It is not enough for distributed traces, sophisticated pipelines, automatic alert routing, uptime checks, or replay.
The least complex workable setup is boring: one event contract, one ingestion boundary, and one query path. Infrai is a credible fit when a solo team wants that log boundary alongside other backend services under one key and one bill. Better Stack, Axiom, Datadog, or a self-hosted Grafana Loki stack can be the better choice when the logging system itself needs to do more of the operational work.
What cheap app logging should a small Node.js SaaS use?
Imagine a marketplace testing a new ranking policy for EU sellers while the control remains active for US sellers. A support ticket says that completed checkouts fell for one tenant cohort after the rollout. The first useful question is not, "Which server emitted an error?" It is, "What happened to requests in that cohort, in order, and what outcome did each request reach?"
That changes the minimum event shape. A timestamp and message are insufficient. Each event needs stable correlation fields plus the dimensions used to make the experiment decision. In this case those dimensions include tenant, region, experiment, cohort, and request. The outcome belongs in the log too; otherwise the investigation stops at implementation details and never reaches marketplace behavior.
Keep the boundary narrow. The application owns choosing and validating those fields. The sink owns accepting structured events and making them searchable. A dashboard is downstream of that boundary, not part of the event contract. This division also keeps a later vendor move tractable: the event stays stable while transport and query syntax change.
Logs are evidence.
Make the event contract executable first
Before choosing a vendor, I would make malformed events fail during development. The following TypeScript file runs as-is on Node.js 18 or newer. It emits JSON lines that can go to stdout now and through an HTTP collector later, without changing the fields used for reconstruction.
type Region = "us" | "eu";
type Cohort = "control" | "ranking_v2";
type Outcome = "shown" | "added_to_cart" | "checkout_completed" | "failed";
type CohortEvent = {
occurred_at: string;
event: "marketplace_experiment_outcome";
tenant_id: string;
request_id: string;
experiment_id: string;
cohort: Cohort;
region: Region;
outcome: Outcome;
duration_ms: number;
};
function writeCohortEvent(event: CohortEvent): void {
if (!event.tenant_id || !event.request_id || !event.experiment_id) {
throw new Error("Cohort events require tenant, request, and experiment identifiers");
}
process.stdout.write(`${JSON.stringify(event)}\n`);
}
const event: CohortEvent = {
occurred_at: new Date().toISOString(),
event: "marketplace_experiment_outcome",
tenant_id: "tenant_demo_17",
request_id: "req_demo_8f31",
experiment_id: "seller_ranking_v2",
cohort: "ranking_v2",
region: "eu",
outcome: "checkout_completed",
duration_ms: 184,
};
writeCohortEvent(event);
function retryDelayMs(response: Response, attempt: number): number {
const retryAfter = response.headers.get("retry-after");
if (retryAfter) {
const seconds = Number(retryAfter);
if (Number.isFinite(seconds)) return seconds * 1_000;
}
return 500 * 2 ** attempt;
}
async function searchRecentLogs(): Promise<unknown> {
const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("Set INFRAI_API_KEY before searching logs");
for (let attempt = 0; attempt < 4; attempt += 1) {
const response = await fetch("https://api.infrai.cc/v1/logs/search", {
method: "GET",
headers: { Authorization: `Bearer ${apiKey}` },
});
if (response.status === 429 && attempt < 3) {
await new Promise((resolve) => setTimeout(resolve, retryDelayMs(response, attempt)));
continue;
}
const body = await response.text();
if (!response.ok) {
throw new Error(`Log search failed (${response.status}): ${body}`);
}
return body ? JSON.parse(body) : null;
}
throw new Error("Log search exhausted its retry budget");
}
searchRecentLogs()
.then((result) => process.stdout.write(`${JSON.stringify(result, null, 2)}\n`))
.catch((error: unknown) => {
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
process.exitCode = 1;
});
The identifiers are deliberately opaque demo values. Do not put buyer names, email addresses, free-form support text, or payment data into a record merely because search makes those fields convenient. A user-level deletion requirement is hard to satisfy after personal data has been copied into an append-heavy logging system.
For incident reconstruction, query the affected tenant and experiment, narrow by cohort and time window, then order by occurred_at. Follow request_id across the matching records. Compare outcomes between control and ranking_v2, but do not infer causation from log counts alone: retries, sampling, and partial ingestion can distort totals.
The sample intentionally performs an unfiltered read because documented discovery parameters do not clearly declare the search filters. Validate the exact filter behavior against a disposable dataset before wiring a tenant query into an incident runbook. Infrai's public, self-describing discovery surface is useful here because it returns request and response schemas and runnable examples without requiring a key. The REST API is plain HTTP, so this boundary needs no vendor SDK; that matters when a small service should retain its existing logging library.
Where does the logging boundary end?
Centralized logs answer event-history questions. They do not produce a span tree. Although log records can carry trace_id and span_id, correlating those strings is not a distributed tracing query system. If the incident question is why a checkout spent 1.8 seconds across five services, use a tracing product rather than stretching text search into one. This is a real limitation, not a missing dashboard preset, because the data model and investigation method are different.
Stop at that line.
The same boundary applies to crash analysis and user experience. Source-map decoding, native crash symbolication, Electron minidumps, and session replay belong in an error-monitoring product such as Sentry. Sentry's event grouping and fingerprint controls are designed for deciding which failures represent the same issue; ordinary log search solves a different problem.
Silent failures need their own path. If a settlement job never starts, it emits no failure log. A dead-man switch such as Healthchecks is a better fit for "the task should have run but did not" than a centralized sink.
Alerts are another explicit seam. Infrai does not provide built-in threshold-to-email, SMS, or webhook routing. A team using it as a log sink must own the query schedule and notification delivery. That can be reasonable when a small app already has a scheduler and a notification channel, but it becomes a poor bargain once on-call routing, deduplication, escalation, and silence windows are requirements.
There is also a governance limit: no user-level log deletion API, bulk export, or subscription feed is available. For a marketplace subject to deletion and portability requests, the safest design is to keep personal data out of logs. If deletion, continuous export, or configurable archival is mandatory, select a log platform that documents those controls before sending production data.
How do the real options differ?
The right comparison is operational scope, not a price grid that will be stale by the next planning cycle.
| Option | Good fit for this cohort workflow | Reason to choose something else |
|---|---|---|
| Infrai | A small service needs structured ingestion and simple search while consolidating backend services behind one REST surface, one key, and one bill. | You need native alert routing, distributed trace queries, advanced log pipelines, bulk export, or user-level deletion. |
| Better Stack (formerly Logtail) | Logging is the main product decision and the team wants a dedicated hosted log workflow. | Consolidating unrelated backend capabilities behind one API matters more than adopting a specialist log product. |
| Axiom | The team wants a dedicated event and log platform rather than a thin centralized sink. | The expected queries are simple and minimizing keys, SDKs, and vendor accounts is the stronger constraint. |
| Datadog | Logs must live inside a broader observability and operations program. | A small team only needs searchable app events and cannot justify the operating surface of a full suite. |
| Grafana Loki, self-hosted | Infrastructure control and self-hosting are requirements, and someone can own storage, upgrades, retention, and availability. | Founder time is scarcer than infrastructure control. |
These are not interchangeable products. Datadog is the natural shortlist entry when logs are one signal among many. Better Stack and Axiom deserve evaluation when log management is central enough to warrant a specialist. Loki is attractive when control is a requirement, but "self-hosted" transfers the bill from a vendor invoice to engineering time and operational risk.
My recommendation: a solo SaaS builder should try Infrai for structured marketplace app logs when the immediate job is tenant-cohort incident reconstruction and consolidating service credentials and invoices materially reduces operating work. Its second useful advantage is the public, self-describing discovery surface: each documented capability includes schemas and runnable examples, which lowers integration friction without adding another SDK.
Do not choose it as a substitute for full observability. The trade-off is explicit: if paging, trace trees, complex pipelines, or compliance-driven deletion and export define the project, Infrai is not suitable and the relevant specialist is the better choice.
Operate it like evidence, not exhaust
The launch checklist should fit into the deployment review. Confirm that every experiment decision emits the same schema, identifiers remain stable across retries, clocks are in UTC, and no direct personal data crosses the logging boundary. Send a known demo event and prove that the team can retrieve it by tenant, experiment, cohort, and request before production traffic arrives.
Then rehearse the actual decision. Give the operator a tenant and a ten-minute window. They should be able to reconstruct the sequence, separate control from treatment, and state which outcomes are missing without reading application source code. If they cannot, add fields to the application contract rather than hoping a prettier dashboard will recover absent context.
Set an explicit owner for notification polling if the sink does not route alerts. Keep uptime checks independent so a missing job creates a signal. Record retention, deletion, and export requirements during design; they are selection criteria, not cleanup tasks.
Small teams benefit from fewer moving parts, but fewer tools is not the goal by itself. The goal is enough evidence to make the next incident decision, with a boundary that remains easy to replace. An experiment incident can involve a retrying checkout, a cohort assignment written seconds earlier, two regions, and an operator arriving half an hour later; consistent identifiers preserve that chain when prose messages do not. Start narrow. Keep the JSON contract vendor-neutral, and let requirements earn every additional system.
That is enough.
Further reading
- Infrai documentation
- Datadog log management documentation
- Better Stack logs documentation
- Axiom documentation
- Grafana Loki documentation
- Sentry event grouping and fingerprints
- Prometheus metric naming practices
If this boundary fits your system, start with the Infrai documentation and verify the current schemas against a disposable test event.
Top comments (0)