DEV Community

WadeSterling3125
WadeSterling3125

Posted on

Error Tracking Library Practices: Execution Logs over Exception Counts for Nightly Imports

Approach What it answers Decision boundary
Structured execution logs plus exception capture Which property import failed, and which attempts consumed work? Prefer when attribution across nightly runs matters.
Exception counts alone Which failures need investigation? Use when triage is sufficient and execution accounting already exists elsewhere.

TL;DR: Choose structured execution logs as the attribution record for a property management pipeline; integrate an error tracking library to preserve exception details alongside that record. Exception counts alone are the weaker choice when the job is to search nightly imports and explain where processing effort went.

The distinction matters for a solo SaaS operator. A stack trace can explain a failed import without explaining the work surrounding it. To ship weekly, keep the integration small: one execution boundary, a stable correlation key, and explicit fields you can search. Outsource the undifferentiated storage and search work when maintaining it would consume time better spent on the product.

Which property import consumed the work?

Start with attribution.

For this design, treat a property import attempt as the searchable unit. Give it a run identifier, an opaque property identifier, an operation name, and an attempt number. Those fields let an investigation move from a nightly run to one property, then distinguish its first attempt from a retry. Keep the exception linked to the same identifiers. A failure total without that context cannot answer the question the pipeline owner actually needs to ask.

OpenTelemetry's logs concepts describe structured logs and correlation with execution context. That provides the architectural basis here: preserve structured context around an error instead of asking the error message to carry every dimension. Trace context can add another correlation path when tracing is present; the example below uses application identifiers so the basic investigation does not depend on a trace being available.

Consider an illustrative run with 12 property imports. One fails and is retried twice. Counting three failed attempts as three affected properties gives the wrong operational picture. Counting only the final successful result loses the retry work. Neither number is inherently useless, but each answers a different question. Store the attempt and outcome explicitly so the query can choose the right denominator. These numbers describe an example, not measured production results.

The same discipline applies to cost attribution. Elapsed time is a useful workload signal, but it is not an invoice. Shared workers, overlapping tasks, and resources outside the timed operation prevent a duration field from establishing a property's actual billed cost. Use the logs to identify attempts; join them to a separately defined resource allocation method before reporting money. If that allocation does not exist, label the result as attributed processing time.

Names matter here.

A field called cost invites a financial interpretation. A field called elapsed_ms makes a narrower claim that the implementation can support.

Keep the integration small enough to ship weekly

The second criterion is maintenance effort. For a one-person business, the useful revenue-per-hour question is how much recurring investigation time the integration removes relative to the time spent maintaining it. A larger telemetry setup needs a concrete investigation it enables. Collecting another dimension merely because a library accepts it adds work without establishing a benefit.

Put exception capture at the boundary that owns the import attempt. That boundary knows the property, run, retry number, and outcome. Lower-level parsing code should throw an error to its caller; recording the same exception at every layer would make an event count harder to interpret. If separate boundaries report separate events, give those events distinct meanings rather than silently treating them as additional failed imports.

Use an allowlist for context. A property identifier is enough for this example; resident names, lease documents, access tokens, and raw source rows have no place in the search record. Apply the same review to exception capture. Error messages can contain input values, so sanitizing only the structured log is insufficient. The library adapter needs an explicit policy for what exception content it sends.

Keep that policy testable.

How should you integrate an error tracking library into nightly imports?

This TypeScript example shows the application boundary for a Node.js job. ErrorTracker is an application-owned adapter around the selected library, not a claim that libraries share one API. Its implementation must map context and exception handling to the actual SDK. LogSink accepts application JSON records; these objects are not an OpenTelemetry wire-format payload.

type Attempt = {
  run_id: string;
  property_id: string;
  operation: "property_import";
  attempt: number;
};

type RecordEntry = Attempt & {
  timestamp: string;
  outcome: "success" | "failure";
  elapsed_ms: number;
};

interface LogSink {
  write(record: RecordEntry): Promise<void>;
}

interface ErrorTracker {
  // The adapter applies the exception redaction policy.
  captureException(error: unknown, context: Attempt): Promise<void>;
}

async function runAttempt<T>(
  context: Attempt,
  work: () => Promise<T>,
  logs: LogSink,
  tracker: ErrorTracker,
): Promise<T> {
  const started = performance.now();
  const record = (outcome: RecordEntry["outcome"]): RecordEntry => ({
    ...context,
    timestamp: new Date().toISOString(),
    outcome,
    elapsed_ms: performance.now() - started,
  });

  let result: T;
  try {
    result = await work();
  } catch (error) {
    await Promise.allSettled([
      Promise.resolve().then(() => logs.write(record("failure"))),
      Promise.resolve().then(() => tracker.captureException(error, context)),
    ]);
    throw error;
  }

  await Promise.allSettled([
    Promise.resolve().then(() => logs.write(record("success"))),
  ]);
  return result;
}
Enter fullscreen mode Exit fullscreen mode

Both outcomes produce an execution record. That gives a search query a denominator beyond captured exceptions. The runner supplies a fresh attempt number for each retry while retaining the run and property identifiers; it also owns the decision to retry. A telemetry wrapper should not quietly rerun an import that may already have changed application data.

The code preserves the work result when telemetry rejects. On failure, it rethrows the original value so the job runner can apply its failure policy. The deferred calls also turn a synchronous adapter throw into a rejected promise. Keeping the success log outside the work's try block prevents a logging failure from being classified as a failed import.

There is a trade-off: waiting for adapters can delay completion. Require bounded delivery behavior in those adapters, and use the chosen library's documented flush behavior before a short-lived worker exits. This example deliberately provides no durable delivery guarantee. If execution records are required for financial reconciliation, use a durable accounting mechanism and reconcile telemetry against it. Best-effort logs are useful investigation evidence, but missing records must remain visible as a limitation of any totals derived from them.

Before deployment, exercise a successful import, a rejected import, a retry, and a rejecting telemetry adapter with fake dependencies. Check that the original error reaches the runner, successful work stays successful, and retries remain distinct in search. Include a redaction check with a deliberately sensitive input value. During rollout, query a known test run across both destinations to verify the context mapping; compiling an adapter does not prove its attributes are searchable.

When exception counts alone are enough

The runner-up fits a narrower job. If a durable execution ledger already records every attempt and its allocation dimensions, exception capture can serve triage while the ledger answers attribution questions. Duplicating the entire ledger into logs may add maintenance without improving the investigation. Preserve a correlation key so the two records can be joined.

It can also be enough when the only operational question is which error to fix next. Accept that boundary explicitly. Once the question becomes which property's nightly processing consumed work, a failure count needs execution context and an accounting denominator.

For the property management pipeline, keep the decision attached to that question. Prefer structured attempt records for search and attribution, attach exception details for diagnosis, and report financial cost only when an allocation method supports it. The practical goal is a shorter path from a failed night to a defensible explanation, leaving time to ship the next week's feature.

References

Top comments (0)