DEV Community

MirageB18
MirageB18

Posted on

A Simple Node.js Production Setup for Logs, Errors, and Metrics

Short answer: a beginner SaaS should use app logging to reconstruct one request, error tracking to organize repeated failures, and metrics to see rates and trends; start with structured logs, then add the other two when those unanswered questions become operationally important.

The limiting resource is usually attention, not data. A solo developer can collect every signal and still have no quick way to decide what to inspect after a customer reports a failed action. The useful setup is the smallest one that answers three different questions without making one event look like three unrelated incidents.

This is a design experiment, not a vendor comparison. The evaluation constraint is simple: given one failed operation, can the setup show what happened, whether it is recurring, and whether the overall service changed? A log-only design answers the first question well but makes the other two expensive. Sending everything everywhere creates a different failure: duplicated noise, duplicated fields, and more plumbing to maintain. The chosen design records one shared event shape, then gives each signal a narrow job.

How should a beginner SaaS use app logging, error tracking, and metrics?

App logging is for evidence about a particular execution. A useful record carries a timestamp, event name, request correlation ID, outcome, duration, and a small set of fields needed to explain that operation. It should tell an engineer which path ran and what the program knew at the decision point. It shouldn't become a transcript of every local variable.

Error tracking is an index over failures. Its value appears when identical or related exceptions recur: instead of reading a stream line by line, the team needs a work queue organized around the failure, its context, and its recurrence. An error record should still preserve the correlation ID so the surrounding application logs remain reachable. Otherwise the tracker says what broke while hiding what led there.

Metrics answer aggregate questions. How many operations completed? What fraction failed? How did latency move across a deployment? They trade event detail for a compact time-oriented view. This is exactly why a customer ID or request ID is a poor metric dimension: a value that changes for almost every request defeats aggregation and makes the signal harder to operate.

Keep the distinction mechanical:

Signal Ask it this Keep out of it
App logs What happened during this operation? Secrets and unbounded debug chatter
Error tracking Which failures recur and need attention? Routine successful events
Metrics Are volume, failures, or duration changing? Per-request and per-customer identifiers

One event may feed all three, but the records shouldn't be identical. A failed checkout can produce a contextual log, an exception for grouping, and one increment to a bounded outcome counter. That isn't duplication for its own sake. Each representation supports a different retrieval pattern.

Each signal earns its place.

The smallest architecture that keeps the signals connected

Put a thin instrumentation boundary inside the application. Business code reports an event once through that boundary; adapters decide whether the event becomes a JSON log, a captured exception, a counter increment, or some combination. This keeps field names and redaction rules in one place, while leaving storage and dashboards outside the domain logic.

The boundary needs a stable vocabulary more than a large schema. Pick one spelling for request_id, one bounded set of outcomes, and one duration unit. Pass an actual Error object on failure rather than flattening it early. The logging adapter can serialize it, while an error-tracking adapter can retain the error type and stack information it needs for organization.

This split also makes replacement less painful. The application depends on a small local interface, not on calls scattered across every route and worker. Logback's appender model illustrates the same architectural idea in another ecosystem: an appender is responsible for delivering logging events to a destination. The transferable lesson is the boundary, not the Java API.

There is a catch. A local abstraction can become an internal observability framework, complete with options nobody uses. Don't build that. Keep the event contract boring, make adapters small, and let the backend-specific configuration stay at the edge.

A focused Node.js production monitoring example

The following TypeScript sketch uses no vendor SDK. It shows the contract and one operation, including the asymmetry that matters: request_id belongs in the log and error context, but the metric receives only the bounded operation and outcome values.

type Outcome = "ok" | "failed";

type OperationEvent = {
  name: string;
  request_id: string;
  outcome: Outcome;
  duration_ms: number;
  error?: Error;
};

type Instruments = {
  writeLog(event: OperationEvent): void;
  captureError(error: Error, context: Omit<OperationEvent, "error">): void;
  increment(name: string, labels: Record<string, string>): void;
};

function recordOperation(event: OperationEvent, tools: Instruments): void {
  tools.writeLog(event);

  if (event.error) {
    const { error, ...context } = event;
    tools.captureError(error, context);
  }

  tools.increment("operations_total", {
    operation: event.name,
    outcome: event.outcome,
  });
}
Enter fullscreen mode Exit fullscreen mode

The deliberately simple approach would call console.log, an error client, and a metric client directly from every handler. It ships quickly. It also lets the three copies drift: one route says requestId, another says request_id, and a worker forgets duration altogether. A tiny shared function prevents that class of schema mismatch without pretending to solve transport, storage, or alerting.

The adapters should enforce the less visible rules. The logging adapter serializes a consistent JSON object and removes sensitive fields. The error adapter receives failures only. The metric adapter rejects unexpected label names rather than quietly creating a new dimension. Tests can then pass fake adapters and assert that one failed operation emits the expected three representations. That test should also prove that a successful operation doesn't call the error adapter, and that the metric labels never inherit request_id from the richer event. These checks matter because the instrumentation will otherwise look correct in a code review while producing an aggregate split into thousands of tiny groups. Don't turn the shared function into an in-process monitoring backend, either: process memory is the wrong place to promise durable history, and application code shouldn't own retention. The example ends at the emission boundary on purpose.

What should you measure before copying this simple setup?

Measure the questions the team cannot answer today. If the recurring task is reconstructing one customer's operation, improve structured logs and correlation first. If repeated exceptions are consuming triage time, add error grouping. If nobody can tell whether failures or duration are moving, add the smallest bounded set of metrics that answers those questions.

Then test the workflow with a controlled application failure in a non-production environment. Start a timer, trigger the failure, and ask someone to locate the relevant event, connect it to its exception group, and determine whether the aggregate counter changed. Record time-to-first-useful-evidence, missing fields, and duplicate notifications. I'm not sure a universal time threshold would help here; the baseline from the team's current workflow is the honest comparison.

Check operating cost as usage grows -- especially log volume, retention, metric dimensions, and the human cost of noisy notifications -- but don't make price the architecture. A cheaper signal that nobody can query under pressure is still waste.

This setup is not suitable when one operation crosses several independently deployed services and the team needs to follow causality across them. Add distributed tracing and context propagation in that case. Stick with platform-provided stdout collection alone when the application is early enough that a separate tracker and metrics store would add more maintenance than decision value. For workloads with strict data-handling requirements, define redaction and retention before emitting customer context anywhere.

Small first. Connected always.

Sources

Top comments (0)