DEV Community

TateFletcher6754
TateFletcher6754

Posted on

Node.js Multi-Tenant B2B SaaS App Logs: Choose Managed Backend — Self-Host for Cost

Short answer: choose a managed log-search backend for most multi-tenant B2B SaaS teams; self-host only when measured ingest and retention costs justify owning the operational load, or when a deployment boundary requires it.

Start with the decision, not a vendor shortlist. A marketplace needs enough evidence to reconstruct a customer incident, fast lookup by request ID and user ID, explicit US/EU routing, and a tenant-level cost trail. The backend matters. The event contract matters more.

Pick Pick it when The catch
Managed search The team values low operational load, predictable on-call ownership, and a quick path to searchable fields Cost attribution still depends on fields and measurements that you design; residency and deletion behavior need contract review
Self-hosted search A team can operate storage, indexing, upgrades, capacity, and recovery, and measured volume makes that work worthwhile The apparent infrastructure control comes with another production system to test and page on

This is a conditional choice, not a tie: start managed, keep the event schema portable, and revisit self-hosting with real measurements. Don't move because a projected spreadsheet looks dramatic. Move when the operational and compliance boundary is clear.

What logging backend should a multi-tenant B2B SaaS use for searchable Node.js logs?

Use a backend that can filter exact structured fields, enforce tenant-scoped access, route data by region, express retention and deletion policies, and report usage at a granularity that can be mapped back to a tenant. Those are acceptance criteria. A feature grid is secondary.

For the query path, require exact matches on request_id, user_id, and tenant_id, plus a bounded time range. Full-text search is useful for the human-readable message, but it shouldn't be the only way to reconstruct an incident. If an engineer has to search for fragments such as “checkout maybe failed,” the event model has already thrown away useful joins.

The word “audit-ish” deserves caution. Application logs can show that a request reached a service and that a domain action was attempted. They aren't automatically a formal audit ledger. If the business needs tamper evidence, prescribed retention, legal holds, or proof about administrator actions, define that evidence stream separately and have the requirement reviewed. Mixing every diagnostic detail and every compliance record into one index makes deletion, access, and retention harder to reason about.

US and EU are also policy boundaries, not decorative region labels. Decide where an event is created, where it may be processed, where replicas and backups may exist, and which staff roles may search it. Then test those decisions. I'm not sure which retention period fits your customer contracts, and a generic article can't settle that; the answer has to come from the actual contracts, legal obligations, and incident-response window.

The Twelve-Factor logging guidance provides a useful application boundary: treat logs as event streams and let the execution environment handle routing. For Node.js, that means emitting structured events to stdout rather than teaching application code the details of one storage product. It doesn't solve tenancy or privacy by itself. Good. It gives those concerns a clean place to live: the shared event schema and the collection layer.

Pick managed search when operations are the scarce resource

A managed backend is the default when the team wants search without owning the index lifecycle. That default is strongest for a small platform team, uneven marketplace traffic, or an incident process that needs to work before anyone can justify a dedicated logging operator. Keep procurement grounded in a proof, though. Load representative structured events, run the real request-ID and user-ID queries, test role boundaries with two tenants, and exercise deletion against the provider's documented behavior.

Cost attribution must be designed before the first invoice. Record tenant_id, region, service, and an event size at collection time; aggregate accepted bytes and retention class by tenant outside the search backend. A backend's account-level bill cannot explain which marketplace customer caused a burst unless the pipeline preserved that dimension. Sampling can reduce diagnostic volume, but never silently sample a record class that the incident or audit policy says must be complete.

This option is not suitable when policy forbids the required processing arrangement, when the necessary regional boundary cannot be demonstrated, or when the backend cannot delete or isolate the relevant subject data as required. Stick with a deployment model you can verify in those cases. Also walk away if tenant isolation depends on every engineer remembering to paste a filter into every query; access control belongs in the query boundary, not in team folklore.

Keep the evaluation concrete. Can an on-call engineer enter a request ID and get the complete cross-service sequence? Can a support role search one tenant without seeing another? Can the platform team calculate accepted bytes for one tenant and one retention class? Can a deletion job identify the affected diagnostic records without scanning arbitrary message text? Four questions. Plenty of signal.

Pick self-hosting only with an ownership plan

Self-hosting becomes the better choice when control of placement and indexing is a hard requirement, or when observed workload economics outweigh the people and reliability cost of operating search. The word “observed” is doing work there. Capture event volume, peak rate, indexed-field cardinality, query concurrency, retention tiers, and recovery targets before making the case.

Then name the owners. Someone has to handle capacity, failed ingestion, index changes, upgrades, backups, restore tests, access reviews, and alerts for the logging system itself. During a customer incident, the evidence system must remain usable while the application is under stress. A design that shares the same failure boundary, saturated storage, or unrestricted administrator role with the production service weakens that goal.

Self-hosting is a poor fit when the only argument is that object storage looks inexpensive. Search needs compute, indexes, metadata, replication choices, and operational time; the storage line item isn't the system cost. It is also a poor fit when no team owns recovery. A backup checkbox is not a restore test.

There is a clean boundary rule: application services emit one portable event shape; a regional collector validates, redacts, measures, and routes it; the search system indexes only the approved fields; a separate usage stream aggregates accepted bytes by tenant. In words, the path is Node.js service → regional collector → policy gate → search plus usage ledger. That layout works with either backend choice and keeps vendor-specific transport out of business code.

Tiny distinction. Big payoff.

Implement the evidence contract before shopping

Begin with a narrow event type. IDs are strings because identifiers are labels, not quantities. The region and retention_class fields are closed sets so an accidental spelling can't create a shadow policy. The message stays useful to a person, while search and attribution use dedicated fields.

import { AsyncLocalStorage } from "node:async_hooks";

type Region = "us" | "eu";
type RetentionClass = "diagnostic" | "security";
type Outcome = "started" | "succeeded" | "rejected";

type RequestContext = {
  request_id: string;
  tenant_id: string;
  user_id?: string;
  region: Region;
};

type AppEvent = RequestContext & {
  timestamp: string;
  service: "marketplace-api";
  event_name: string;
  outcome: Outcome;
  retention_class: RetentionClass;
  message: string;
  status_code?: number;
  duration_ms?: number;
};

const requestContext = new AsyncLocalStorage<RequestContext>();

export function writeEvent(
  fields: Omit<AppEvent, keyof RequestContext | "timestamp" | "service">,
): void {
  const context = requestContext.getStore();
  if (!context) throw new Error("request context is required");

  const event: AppEvent = {
    ...context,
    ...fields,
    timestamp: new Date().toISOString(),
    service: "marketplace-api",
  };

  process.stdout.write(`${JSON.stringify(event)}\n`);
}
Enter fullscreen mode Exit fullscreen mode

The request boundary should validate trusted tenant and region values after authentication, create the context once, and carry it through asynchronous work. Don't accept tenant_id from an arbitrary request body and call that isolation. The authoritative value should come from the authenticated principal and the application's tenant mapping.

A marketplace action can now emit a small, searchable sequence. Notice what is absent: email addresses, access tokens, request bodies, and free-form exception dumps. Add data only when it has a named incident use, an access policy, and a deletion story.

writeEvent({
  event_name: "listing.purchase",
  outcome: "succeeded",
  retention_class: "diagnostic",
  message: "Marketplace purchase accepted",
  status_code: 201,
  duration_ms: 84,
});
Enter fullscreen mode Exit fullscreen mode

At the collector, validate required fields and reject malformed events into a controlled diagnostic path. Redact forbidden keys before storage. Compute accepted UTF-8 bytes after redaction, because those are the bytes the downstream system actually receives, then increment a usage record keyed by tenant_id, region, day, and retention class. This produces a defensible allocation input without stuffing billing math into every service. Search access needs the same discipline: put tenant scope into the server-side authorization layer and require bounded time ranges. Support staff may receive a tenant-scoped view; incident responders may receive broader access through an approved role with its own access records. Test a negative case: a principal for tenant A searching a known request ID from tenant B must receive no event data. Test region routing too by emitting synthetic US and EU events and checking only the intended stores and usage partitions. Deletion is where casual logging designs crack. GDPR Article 17 defines a right to erasure along with stated conditions and exceptions, so don't promise that every record must always be deleted or always retained. Instead, classify fields and record types, obtain legal review for the applicable rule, keep a subject-to-event lookup where required, and test that the chosen backend and its backup process can carry out the resulting policy. Search by user_id is operationally handy; it also makes the identifier part of the data lifecycle.

Deploy the schema as a versioned contract. During a change, collectors should accept the old and new versions long enough for a controlled rollout, while dashboards and incident queries are tested against both. Alert on rejected-event count, ingestion lag, missing required dimensions, and a mismatch between emitted and accepted volume. The logging path shouldn't crash a customer request, but losing evidence silently is not acceptable either; expose loss through a metric and a bounded local failure policy.

Finally, run one reconstruction drill. Give an engineer only a tenant ID, user ID, approximate time, and request ID. Ask for the action sequence, outcome, involved service, region, and gaps. The exercise reveals more than a catalog demo because it tests the whole chain: instrumentation, context propagation, collection, indexing, authorization, retention, and the human query workflow.

Limits and the final decision

Managed search wins the initial decision for most teams because it removes a substantial operations surface while the event contract is still evolving. Self-hosting wins when regional control or measured economics is decisive and a named team can prove recovery and ongoing ownership. Neither choice repairs unstructured messages, missing tenant context, or an access model built from optional query filters.

Keep the exit path boring: structured stdout, a regional collection boundary, a small versioned schema, and exportable events. Re-evaluate with measurements from accepted bytes, retention classes, query load, and operator time. No theater.

The durable choice is the contract. The backend can change.

References

Top comments (0)