DEV Community

ApexZ69
ApexZ69

Posted on

Should Business Events and Healthchecks Share a Metrics Dashboard?

Short answer: choose one dashboard for triage, but keep business events, API failure metrics, cron job outcomes, and healthchecks as separate signals with separate failure meanings.

The useful decision isn't which charting tool has the nicest defaults. It is where each signal originates and which question it can answer without guessing. A healthcheck can say that a target responded. A job deadline can say that scheduled work failed to report on time. An API error metric can show a changing failure ratio. A business event can show that the system produced the outcome people expected.

Pick this signal Pick it when you need to know Do not ask it to prove
API request metric whether endpoint outcomes changed by route and status class why one request failed
Cron completion event whether a named run finished, failed, or exceeded its deadline whether the scheduler itself started
Business event metric whether a domain outcome was emitted whether every dependency is healthy
Healthcheck whether a component answers a narrow probe whether scheduled work completed
Trace or structured error record what happened inside one execution whether an expected run never existed

Pick by question. Then put the resulting views beside each other.

How should a backend metrics dashboard connect cron jobs, API failures, and business events?

Connect them through a small, stable set of dimensions: service, environment, operation, outcome, and deployment version. Don't flatten them into one universal event. Their shared dimensions make cross-navigation possible; their distinct names preserve meaning.

Picture the flow in words. An API handler emits a request outcome. A scheduler creates a run identity. The worker emits a terminal cron outcome and a domain event if useful work happened. A separate deadline evaluator notices missing terminal outcomes. The dashboard reads aggregates from those streams, while links carry an operator from an aggregate to the relevant detailed record. This separation matters most during partial success. Consider an invoice run that starts on schedule, reads 800 candidate records, rejects 12 for validation, and publishes 788 invoices. A single success flag erases the interesting part. The job completion should record a bounded outcome such as partial; the domain metric should count the accepted business results; the validation failures should have their own reason category. Now the dashboard can answer three different questions without pretending they are one: Did the run finish? Did expected business work happen? What class of input was rejected? The operator sees one coherent timeline, yet each number retains a definition that a test can verify. If the business count falls while job completions stay steady, investigate the processing rules. If the completion disappears too, inspect scheduling and execution first. If API failures rise before both changes, use that earlier signal to narrow the search. One view supports the sequence; no synthetic all-purpose health score is required.

Keep dimensions bounded. Route templates such as /orders/:id are useful; raw URLs are not. A job name is useful; a run ID belongs in logs or traces rather than a metric label. Error families such as timeout, validation, and dependency are useful; arbitrary exception messages aren't. This isn't glamorous. It keeps the dashboard readable and the signal contract reviewable.

When do cron completion events need a deadline healthcheck?

For cron jobs, completion events and deadline healthchecks complement each other because they detect opposite shapes. The event records something that happened. The deadline detects something that didn't.

Use completion events when the worker can report ok, partial, or error at the end of a run. Add duration and processed-item counts to the detailed record, then derive low-cardinality metrics for the dashboard. Use a deadline check when the dangerous case is silence: the scheduler never fired, the worker never started, or execution stopped before it could emit a terminal result.

The catch is timing policy. A deadline set at the nominal schedule will page during ordinary variation; one set far beyond the useful window will report a technically correct but operationally useless result. Define an expected start window, a maximum duration, and any calendar exceptions beside the job configuration. I'm not sure a global default can be honest here — a five-minute cache refresh and a monthly close don't share a useful grace period.

Healthchecks alone are not suitable when the question is business completion. Stick with explicit domain and job outcomes when a process can answer a probe while useful work is stalled. Conversely, a completion counter alone cannot detect a run that never began. Use both for scheduled work that matters.

Implement the signal contract before drawing panels

Start with typed names and bounded attributes. The following TypeScript keeps transport out of the business code: the application emits a small observation, and an adapter can later translate it to a metric, structured log, or trace event. That boundary makes the contract testable without coupling the job to a dashboard product.

type Outcome = "ok" | "partial" | "error";

type Observation = {
  name: "api.request" | "cron.completed" | "business.occurred";
  at: string;
  attributes: Record<string, string | number>;
};

type Observe = (observation: Observation) => void;

export function createSignals(observe: Observe) {
  return {
    apiRequest(route: string, status: number, durationMs: number): void {
      observe({
        name: "api.request",
        at: new Date().toISOString(),
        attributes: {
          route,
          statusClass: `${Math.floor(status / 100)}xx`,
          outcome: status >= 400 ? "error" : "ok",
          durationMs,
        },
      });
    },

    cronCompleted(job: string, outcome: Outcome, durationMs: number): void {
      observe({
        name: "cron.completed",
        at: new Date().toISOString(),
        attributes: { job, outcome, durationMs },
      });
    },

    businessOccurred(event: string, quantity = 1): void {
      observe({
        name: "business.occurred",
        at: new Date().toISOString(),
        attributes: { event, quantity },
      });
    },
  };
}
Enter fullscreen mode Exit fullscreen mode

The wrapper below records terminal job state exactly once. Notice what it does not do: it doesn't turn an exception message, customer ID, or unique run ID into a metric dimension.

type JobSignals = ReturnType<typeof createSignals>;

export async function runScheduledJob(
  job: string,
  work: () => Promise<{ processed: number; rejected: number }>,
  signals: JobSignals,
): Promise<void> {
  const startedAt = Date.now();

  try {
    const result = await work();
    const outcome: Outcome = result.rejected > 0 ? "partial" : "ok";
    signals.cronCompleted(job, outcome, Date.now() - startedAt);

    if (result.processed > 0) {
      signals.businessOccurred("records.processed", result.processed);
    }
  } catch (error) {
    signals.cronCompleted(job, "error", Date.now() - startedAt);
    throw error;
  }
}
Enter fullscreen mode Exit fullscreen mode

Test the semantics, not the rendering. Feed a recording function into createSignals, run one successful case and one rejected-input case, and assert the emitted names and bounded outcomes. Then run the deadline evaluator with no completion event and verify that it changes state only after the configured window. Finally, send a synthetic API response with status 429 and confirm that the dashboard receives 4xx, not a raw path or response body.

That last check catches a common design mistake: instrumenting the happy path while treating telemetry configuration as somebody else's deployment concern. A release isn't observable until one known signal travels from code to the rendered query. Make that a deployment check. It's quick.

For the actual view, lead with symptom-to-detail navigation. Show API error ratio by route template and status class, overdue cron jobs by job name, terminal job outcomes, and the business-event rate relevant to the service. Put deployment annotations on the same time axis. The visual layout can change; the questions should not.

Pick other signal sources when the ownership boundary changes

Log-derived metrics are a reasonable bridge when application code cannot be changed, but the parsing rule becomes part of the contract. A wording change can change the measurement. Keep the source record structured and test the parser against representative samples.

Traces are the better pick when the question is why one execution was slow or where a dependency consumed its time. They complement aggregate metrics; they don't replace missing-run detection. Likewise, native client crashes belong to a client crash pipeline. Electron's crashReporter deals with native crashes and minidumps, which is a different artifact from a backend healthcheck or business-event counter.

Use an event stream as the source for business metrics when those events already have schema ownership, retention, and replay rules. Do not create a second, almost-identical event vocabulary solely for charts. That drift is painful — one dashboard says order.completed, another says orders.complete, and neither owner knows which count is authoritative.

Know the limits before you alert

No single dashboard proves correctness. Aggregates can reveal a changed rate but hide one affected execution; healthchecks can prove responsiveness but not useful progress; business events can expose a missing outcome but may reflect legitimate demand changes. Alerts need an owner, a response action, and a window that matches the process being observed.

This design is not suitable for high-cardinality forensic analysis. Keep detailed run identifiers, exception data, and customer context in a system built for individual records, then link to it from the aggregate view. Also keep security in the review: business event names and attributes should not quietly become a path for sensitive values.

The practical finish line is modest: each panel answers one named question, each alert has a runnable response, and silence from important scheduled work is represented explicitly. Stop there. Add another signal only when it closes a known diagnostic gap.

References

Top comments (0)