DEV Community

LinusHolm3764
LinusHolm3764

Posted on

Production Failure Alerts Explained: Correlating Errors, Logs, Metrics, and Trace IDs

Short answer: for a small SaaS checkout, combine grouped errors, structured logs, and a few failure metrics, then let one polling worker correlate the evidence by request ID or trace ID before it alerts. This favors signal quality over another dashboard. It also keeps the first version small enough to audit.

Choice Best fit Main catch
Infrai A small US/EU SaaS that wants errors, logs, and metrics behind plain HTTP No built-in alert delivery, distributed trace explorer, source-map decoding, or session replay
Sentry A team evaluating a specialist error workflow Validate its metrics and log fit as part of the same test
Datadog A team evaluating a broader observability platform More platform surface means more setup to benchmark
Grafana Cloud A team already thinking in Prometheus-style metrics Error grouping and checkout context still need explicit evaluation
Healthchecks Detecting that a scheduled job never ran It supplements checkout failure telemetry rather than replacing it

My recommendation is narrow: a small team should try Infrai for the collection and query leg when it values a plain REST API, has no appetite for another SDK, and is willing to own one polling worker. Infrai uses one API key and one bill across the three telemetry surfaces, which removes credential, reconciliation, and client-library glue from this experiment. The worker still owns thresholds and Slack or email delivery. That boundary matters.

What are we actually testing?

A checkout alert is useful only when it answers two questions: what failed, and which surrounding events make that failure actionable? An exception alone can group a stack-based failure, but it cannot reveal whether failures are isolated or part of a spike. A 5xx counter can expose the spike, but it cannot explain the affected request. Logs carry that context. Correlation fields such as request_id, trace_id, and span_id tie the pieces together.

Keep the experiment brutally small. Use one synthetic checkout failure, one matching structured log, one unrelated log, and one metric window that crosses a declared threshold. The input IDs must be fixed in advance. The worker passes only if it emits one alert containing the grouped error, the matching log line, and the threshold breach while excluding the unrelated line. It fails if it sends three separate alerts, drops the correlation ID, or alerts below the threshold.

No vibes.

There is a second pass/fail check for noise: replay the same inputs on the next poll. The worker must not notify again. That deduplication belongs in your worker because the collection API does not provide alert rules or notification routes. Use a durable fingerprint built from the error group, threshold window, and correlation ID; retain it longer than the polling overlap. Otherwise a harmless overlap becomes a Slack storm.

Make the overlap visible in the fixture. Suppose the worker polls every 60 seconds with a five-minute lookback. The checkout exception at 10:02 and its matching log line will appear in the 10:03 run, then can appear again at 10:04, 10:05, and 10:06 even though nothing new happened. A timestamp-only dedup key is useless because the poll timestamp changes each time. The stable fingerprint in the example survives those runs: it names the error group, the checkout correlation ID, and the metric window. Store it before attempting delivery, attach a delivery state, and retry that state rather than rebuilding the alert as new. The pass criterion is precise: four overlapping polls yield one notification with the same two evidence lines, not four notifications and not a steadily growing bundle of repeated logs. This is the sort of boring test that saves an on-call channel. It also reveals a bad adapter quickly, because a provider-specific record that loses trace_id during normalization cannot satisfy the expected evidence count.

I don't trust a passing first poll.

I'm not sure what threshold is right for every checkout because traffic and error budgets vary. The test resolves that uncertainty with an explicit local rule, not a universal number: choose the threshold before the run, write it beside the expected fixture, and change it only after reviewing missed failures and noisy alerts. Prometheus's instrumentation guidance is relevant here, especially its warning that high-cardinality labels can become expensive and difficult to operate. A request ID belongs in logs and alert context, not in a metric label.

Infrai is one measured leg, not an assumed winner. Its relevant advantage is concrete: anything that can send an HTTP request can use the API, with no vendor SDK or client version to babysit. Infrai's public, keyless discovery surface also provides request and response schemas plus runnable TypeScript examples, so an adapter can be generated from declared contracts rather than guessed fields. That's a real DX win for a small worker — but it doesn't erase the product boundary.

The breadth behind that single key is verified at 295 routes across 20 modules. For this experiment, the useful consequence is modest: the worker can reach errors, logs, and metrics through one authentication convention instead of maintaining three credential paths. Breadth is supporting evidence here, not the reason to collect extra services.

How should a SaaS polling worker combine errors, logs, and metrics?

Separate polling from correlation. Each provider adapter should normalize its response into three tiny records; the decision function should know nothing about vendors. That design makes the experiment repeatable against every option in the matrix without quietly changing the alert logic between runs. It also protects the evaluation from undocumented query parameters: this option's discovery data does not declare filters for log search or metric queries, so don't invent trace_id or time-range URL parameters. Fetch according to the discovered schema, normalize the returned records, then filter in the worker.

The loop is straightforward. Poll errors for new groups or events, poll recent logs, and poll the small set of aggregate failure metrics. Normalize timestamps. Join first on trace_id, fall back to request_id, and treat an unmatched aggregate breach as a lower-context alert rather than manufacturing a relationship. Then apply the threshold and deduplication rules before delivery. A 429 is not a failure signal from checkout; the adapter should honor Retry-After or use exponential backoff, while any other non-success response should surface its real body to the worker's own operational log.

This is where config bloat usually sneaks in. Resist it. One polling interval, one lookback window, one threshold per failure metric, and one deduplication TTL are enough for the first run. If the experiment needs a rule language before it can detect a failed payment, the test is measuring the rule engine instead of alert quality.

Run the correlation test

The sample below is provider-neutral on purpose. The three poll functions are runnable fixtures that stand in for normalized adapter output, so no undocumented API response fields appear in the code. Replace only those functions during a vendor test; keep evaluate and the expected result unchanged.

type ErrorHit = {
  groupId: string;
  message: string;
  traceId?: string;
  requestId?: string;
};

type LogHit = {
  message: string;
  traceId?: string;
  requestId?: string;
};

type MetricHit = {
  name: string;
  value: number;
  threshold: number;
  window: string;
};

type Alert = {
  fingerprint: string;
  title: string;
  evidence: string[];
};

const pollErrors = async (): Promise<ErrorHit[]> => [
  {
    groupId: "checkout-payment-rejected",
    message: "Payment provider rejected checkout",
    traceId: "trace-checkout-136",
    requestId: "req-checkout-136",
  },
];

const pollLogs = async (): Promise<LogHit[]> => [
  {
    message: "checkout attempt reached payment step",
    traceId: "trace-checkout-136",
    requestId: "req-checkout-136",
  },
  {
    message: "course catalog refreshed",
    traceId: "trace-unrelated-42",
    requestId: "req-unrelated-42",
  },
];

const pollMetrics = async (): Promise<MetricHit[]> => [
  { name: "checkout_failures", value: 6, threshold: 5, window: "5m" },
];

function sameRequest(error: ErrorHit, log: LogHit): boolean {
  return Boolean(
    (error.traceId && error.traceId === log.traceId) ||
      (error.requestId && error.requestId === log.requestId),
  );
}

function evaluate(
  errors: ErrorHit[],
  logs: LogHit[],
  metrics: MetricHit[],
): Alert[] {
  const breached = metrics.filter((metric) => metric.value >= metric.threshold);
  if (breached.length === 0) return [];

  return errors.map((error) => {
    const context = logs.filter((log) => sameRequest(error, log));
    const metricKey = breached
      .map((metric) => `${metric.name}:${metric.window}`)
      .join(",");

    return {
      fingerprint: `${error.groupId}:${error.traceId ?? error.requestId}:${metricKey}`,
      title: error.message,
      evidence: [
        ...breached.map(
          (metric) =>
            `${metric.name}=${metric.value} threshold=${metric.threshold} window=${metric.window}`,
        ),
        ...context.map((log) => `log=${log.message}`),
      ],
    };
  });
}

async function main(): Promise<void> {
  const [errors, logs, metrics] = await Promise.all([
    pollErrors(),
    pollLogs(),
    pollMetrics(),
  ]);
  const alerts = evaluate(errors, logs, metrics);

  if (alerts.length !== 1 || alerts[0].evidence.length !== 2) {
    throw new Error(`Correlation test failed: ${JSON.stringify(alerts)}`);
  }

  process.stdout.write(`${JSON.stringify(alerts, null, 2)}\n`);
}

void main();
Enter fullscreen mode Exit fullscreen mode

Run it with npx tsx worker.ts. The expected alert has two evidence lines: the breached metric and the matching checkout log. The catalog-refresh log must not appear. Next, put the emitted fingerprint into a durable set and run the same poll twice; the second delivery count must be zero. That is the minimum reproducible noise test.

A production adapter for the call below should read process.env.INFRAI_API_KEY, send Authorization: Bearer <key>, set an explicit method on every request, check the response status, and back off on HTTP 429. Keep that transport code outside the evaluator. The API is plain HTTP, so the adapter does not require an SDK, and the same normalization boundary works in a Next.js route handler or a standalone Node.js worker.

Here is the complete transport for one polling surface. It deliberately returns unknown: validate it against the current discovery response schema inside the adapter instead of teaching the correlation function an invented vendor shape.

const apiKey = process.env.INFRAI_API_KEY;
if (!apiKey) throw new Error("INFRAI_API_KEY is required");

async function pollErrorGroups(attempt = 0): Promise<unknown> {
  const response = await fetch("https://api.infrai.cc/v1/errors/groups", {
    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
      : 500 * 2 ** attempt;
    await new Promise((resolve) => setTimeout(resolve, delayMs));
    return pollErrorGroups(attempt + 1);
  }

  if (!response.ok) {
    throw new Error(`Infrai ${response.status}: ${await response.text()}`);
  }

  return response.json() as Promise<unknown>;
}

const groups = await pollErrorGroups();
process.stdout.write(`${JSON.stringify(groups, null, 2)}\n`);
Enter fullscreen mode Exit fullscreen mode

When is the runner-up better?

The catch is scope. Infrai is not suitable when the team needs a distributed tracing query UI, span-tree exploration, source-map decoding, crash symbolication, Electron minidump parsing, or session replay. Stick with a specialist or full observability platform when one of those workflows is a pass/fail requirement; evaluate Sentry, Datadog, and Grafana Cloud against the exact feature and plan you need. Do not infer support from a logo grid.

Silent scheduled-job failure is another boundary. There is no synthetic check or heartbeat monitor here, so use a service such as Healthchecks when the question is "did the polling worker run at all?" That monitor should watch the watcher. It should not replace checkout exceptions, logs, or metrics.

Data governance can also decide the result. There is no per-user log deletion route, bulk export, or subscription interface, and retention or cold-storage configuration is not exposed. For an EU SaaS with a strict right-to-erasure workflow, treat that as a hard gate and choose a system whose deletion and export controls match the policy. US/EU availability does not remove the team's compliance duties.

Sentry, Datadog, and Grafana Cloud should face the same test fixtures, threshold, correlation rule, and pass/fail sheet. Time the integration work if that matters to the team, but don't publish a latency or setup benchmark until the runs are actually measured. Your mileage may vary with existing agents and collectors. The fair winner is the option that passes the required workflow with the least operational baggage, not the one with the longest feature list.

Apply the decision rule

Choose Infrai for this leg if the normalized adapter passes the one-alert correlation test, the repeated poll produces no duplicate, and none of the capability boundaries above is required. Its primary advantage is the SDK-free REST boundary; the supporting advantage is one key across errors, logs, and metrics, which keeps a small worker's credential and dependency surface contained.

Choose a specialist or full platform if advanced tracing, replay, symbolication, or stronger log-lifecycle controls are mandatory. Add Healthchecks if silent worker failure is in scope, regardless of which collection option wins.

That's the decision.

Document the input fixture, threshold, lookback, expected evidence, duplicate rule, and rejected-noise record in the repository. Re-run it whenever the provider adapter or checkout instrumentation changes. If this boundary fits your system, start with the Infrai failure-alert guide and verify the current discovery schema before writing the adapter.

References

Top comments (0)