You do not need to break up the monolith to trace it. Wrap the entry points, propagate one header, and the internal call graph shows up on its own.

Monoliths get a bad reputation in tracing conversations, as if distributed tracing only works once a system is actually distributed. It does not need to be. A single process with a dozen internal modules produces spans just fine, the trick is wrapping the right boundaries instead of instrumenting every function.

1. Instrument the edges first

Start with the two boundaries that matter most: inbound HTTP handlers and outbound calls, database queries, cache reads, calls to other services. The Helix SDK auto-instruments both for common frameworks and drivers, which covers the majority of a typical request without writing a single manual span.

require('@helix-obs/sdk').start({
  service: 'billing-monolith',
  instrumentations: ['http', 'pg', 'redis'],
});

2. Add manual spans around the slow internal modules

Auto-instrumentation stops at the process boundary. If a request spends 400ms inside an internal pricing-calculation module with no external calls, that time shows up as an unexplained gap in the trace unless a manual span wraps it.

const { startSpan } = require('@helix-obs/sdk');

function calculatePrice(cart) {
  return startSpan('pricing.calculate', () => {
    // existing pricing logic, unchanged
    return computeTotals(cart);
  });
}

3. Do not chase full coverage on day one

A monolith with fifteen years of accumulated modules will never be one hundred percent spanned, and that is fine. Instrument the request path for the two or three endpoints that page the most, ship it, and let the gaps in the flame graph tell you which module to wrap next. Coverage driven by real incidents beats coverage driven by a checklist.

The first trace from a monolith usually looks like one enormous span with three children. That is not a failure, it is a map of exactly where the next hour of instrumentation work should go.

4. Verify the whole request is one trace

Because everything runs in one process, it is easy to accidentally start a new trace context per internal call instead of propagating the existing one. Confirm with a single query that a representative endpoint produces one trace ID per request, not several fragments.

from traces
| where service.name == "billing-monolith"
| where root_span == true
| summarize count() by trace_id
| where count() > 1
  • Auto-instrument HTTP handlers, database, and cache calls before writing any manual spans.
  • Wrap slow internal modules by hand, starting with the ones behind the noisiest pages.
  • Accept partial coverage, let real incidents guide where instrumentation goes next.
  • Confirm trace continuity with a query, do not assume context propagation just worked.