The best logging backend for a property-management SaaS is the one that can answer two questions together: which tenant cohort saw the behavior, and what happened before the incident. For a small Node.js team, that usually means choosing a backend around structured events, a stable correlation key, and a query path that can reconstruct one request across services. Storage brand comes later.
The schema is the product.
TL;DR: start with an OpenTelemetry-shaped event model and a backend that can filter exact fields, retain the required incident window, and export data in a portable format. A label-oriented system is a good fit for bounded dimensions such as environment and service. A document or inverted-index system is better when incident reconstruction depends on searching many changing fields. Test that decision against a tenant experiment before committing.
| Backend shape | Strong fit | Main risk |
|---|---|---|
| Label-oriented log store | Fast filtering on a small set of known dimensions | High-cardinality tenant or request labels can become expensive or awkward |
| Document search store | Many fields, ad hoc incident queries, long-lived evidence | More indexing and retention decisions to operate |
| Object storage plus query engine | Cheap archive and replayable evidence | Slower interactive investigation and more pipeline work |
My recommendation is to keep the event schema independent from the backend, put tenant_cohort in the event body rather than turning every tenant into an index dimension, and evaluate query time against a real incident reconstruction. That keeps weekly shipping possible while preserving an exit path.
That is the whole decision.
What does a property incident need to prove?
A cohort experiment can look healthy in aggregate while one group of tenants receives stale lease data. A dashboard showing error counts will not reconstruct that event. The investigation needs a chain: request accepted, feature assignment selected, database read completed, response sent, and any retry or timeout that changed the result.
This is why a plain text line such as lease lookup failed is weak evidence. It names a symptom but loses the identifiers that connect the symptom to a tenant, an experiment, and a deployment. The log record should be one event with typed fields. OpenTelemetry treats logs as a signal that can be correlated with traces and metrics through shared context; the useful part here is the shared context, not a particular collector or storage product.
For a property-management workflow, I would make these fields mandatory for application events:
-
timestampand severity -
service.nameand deployment version -
tenant_idandtenant_cohort -
experiment_idand assignment -
request_idor trace context - operation name and outcome
Tenant identifiers need care. Hash or tokenize them if the investigation does not require the raw identifier. Keep the join key stable enough to connect application events with audit records, while making the retention policy explicit. Logs are operational evidence, not a second database for every customer attribute.
How should a logging backend handle capturing and searching application logs?
The decision turns on query shape more than ingestion volume. Write down the first five questions an on-call person must answer. Mine would be: “Did cohort B receive the new lease-calculation path?” “Which deployment first produced the wrong result?” and “Did the same request retry after the database timeout?” Those questions use exact fields first, then occasional text or range searches.
A label-oriented store works well when the filter set is small and predictable. Environment, service, severity, and perhaps experiment ID are reasonable dimensions. A tenant ID or request ID is different. Those values have high cardinality and are often unique, so making them primary labels can create index pressure or make retention behavior harder to reason about. The event can still contain them as searchable fields where the backend supports that model.
A document search store is a stronger match when the team must pivot through changing fields during an incident: nested error details, database duration, queue metadata, or a provider response code. Elasticsearch and OpenSearch both provide document-oriented search systems, but their operational details, licensing, managed offerings, and upgrade paths are separate decisions. They are examples of a backend class, not an endorsement.
A third option is an append-only archive in object storage queried in batches. It is useful for long retention, audit review, and replaying a narrow time range into a temporary index. The trade-off is obvious: the investigator accepts slower interactive queries and owns more pipeline mechanics. For a solo SaaS founder, that can be the right runner-up when incident reconstruction is rare and the archive is a compliance requirement.
Loki is another example of the label-oriented model: its documentation describes indexing labels while storing log lines for later querying. That boundary is useful to understand before putting request or tenant identifiers into labels. The question is not which system has the longest feature list. It is which query contract remains predictable at 2 a.m.
A Node.js event that survives the first incident
The logger should accept a typed event, attach request context, and serialize one record. It should not force every caller to know the backend's indexing vocabulary. This small boundary is worth outsourcing to a shared module because repeated logging code is undifferentiated work and drifts quickly.
type LogEvent = { timestamp: string; severity: "info" | "warn" | "error"; service: string; version: string; operation: string; requestId?: string; traceId?: string; tenantIdHash?: string; tenantCohort?: string; experimentId?: string; assignment?: string; outcome: "ok" | "error" | "timeout"; durationMs?: number; errorCode?: string; }; export function writeLog(event: LogEvent): void { process.stdout.write(`${JSON.stringify(event)}\n`); } writeLog({ timestamp: new Date().toISOString(), severity: "error", service: "lease-api", version: "2026.11.0", operation: "lease_summary", requestId: "req-7f2", traceId: "trace-91a", tenantIdHash: "sha256:8d2...", tenantCohort: "cohort-b", experimentId: "lease-calc-v2", assignment: "treatment", outcome: "timeout", durationMs: 1200, errorCode: "LEASE_DB_TIMEOUT" }); The version is part of the event because a deployment boundary matters during reconstruction. The cohort is also explicit. Deriving it later from a current experiment table can rewrite history after an assignment rule changes.
Do not log the whole request object by default. It creates accidental retention of names, addresses, tokens, and other data that an incident query rarely needs. Define an allowlist for fields, cap error payload sizes, and sample only events that are genuinely redundant. Sampling the one timeout that explains a tenant-visible failure defeats the purpose.
How should the experiment be tested before rollout?
Use a replayable fixture with two cohorts and a known failure sequence. For example, make cohort A use the existing lease calculation and cohort B use the candidate path. Inject a database timeout after the assignment event, then assert that the resulting records share the same request or trace context and preserve the assignment. The test is about evidence, not just whether an exception is thrown.
A compact assertion can catch the most damaging schema regression:
type ParsedEvent = { operation: string; requestId?: string; tenantCohort?: string; experimentId?: string; outcome: string; }; export function assertReconstructable(events: ParsedEvent[]): void { const failed = events.find((event) => event.outcome === "timeout"); if (!failed) throw new Error("expected a timeout event"); const related = events.filter((event) => event.requestId === failed.requestId); if (related.length < 2) throw new Error("missing request context"); if (!failed.tenantCohort || !failed.experimentId) { throw new Error("missing experiment context"); } } Run this fixture across deployments. One version might serialize tenantCohort, another might emit cohort; both look readable in isolation and break the query in production. A schema check in CI is cheap insurance.
What should the decision review measure?
Measure the time to answer a fixed reconstruction script, not a synthetic ingestion contest. Start with a 30-minute window containing one deployment, two cohorts, a retry, and a timeout. Ask an investigator to identify the affected cohort, the first failing operation, and the version that served it. Record query latency, false joins, missing context, and the manual steps required to move from logs to traces. Repeat the exercise after changing one field name and after removing one correlation field; these are small tests, but they expose whether the search contract is documented or merely familiar to its author.
Then test retention and deletion behavior. Tenant evidence may need removal or redaction, and the backend must make that operationally possible. Check export formats too. A JSON event written to standard output is portable; a query built entirely around proprietary parsing rules is not.
Keep the review small. I care about revenue per engineering hour, so a backend that requires a week of tuning before the first useful incident query has already failed this evaluation, even if it wins a benchmark. Ship the minimum event contract first. Add fields when an incident or experiment proves they are needed.
The runner-up is better when the primary workload is archival analysis rather than live response. An object-storage archive plus a batch query layer can preserve a long evidence trail with fewer interactive indexes. A document search backend is better when the business has many unpredictable investigation pivots. A label-oriented backend is better when the dimensions are few, stable, and known ahead of time.
The decision is therefore a contract: preserve causal context in the event, keep cardinality deliberate, and test the exact tenant-cohort reconstruction that your service must explain. Pick the backend shape that makes that contract boring to operate.
Sources
- OpenTelemetry, “Logs signal concepts”: https://opentelemetry.io/docs/concepts/signals/logs/
- OpenTelemetry, “Logs data model”: https://opentelemetry.io/docs/specs/otel/logs/data-model/
- Grafana Loki documentation, “Label best practices”: https://grafana.com/docs/loki/latest/get-started/labels/bp-labels/
- Elasticsearch documentation, “Inverted index”: https://www.elastic.co/guide/en/elasticsearch/reference/current/index-modules.html
- OpenSearch documentation, “Index data”: https://docs.opensearch.org/latest/im-plugin/index-settings/
- MDN Web Docs, “JSON.stringify()”: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify