Course contentsShow
← Node.js
Lesson 1707 of 1,98015. Logging, Observability, and MetricsFree lesson

Instrumenting Manual Span Creation

Manually create and close spans to trace complex logic that automatic instrumentation misses.

In this lesson, you will learn how to capture custom spans to trace complex logic that automatic instrumentation libraries—which typically only hook into HTTP or database drivers—often overlook.

Why Manual Spans?

Automatic instrumentation is excellent for tracking the start and end of an incoming request. However, if your request handler triggers a series of intensive background tasks, complex data transformations, or sequential steps in a long-running process, automatic tools often treat that entire sequence as one monolithic blob. By creating manual spans, you gain visibility into the precise internal timing and status of these isolated operations, allowing you to see exactly where your code spends its cycles.

Creating a Custom Span

To manually instrument a block of code, you interact with the Tracer instance provided by the OpenTelemetry SDK. The key is to start a span, execute your logic, and ensure the span is closed properly—even if an error occurs.

Consider this example of a CPU-intensive data processing block:

const { trace } = require('@opentelemetry/api');

async function processLargePayload(data) {
  const tracer = trace.getTracer('my-application-service');
  // Start the span
  const span = tracer.startSpan('process-large-payload');

  try {
    // Perform complex operations
    const transformed = await transformData(data);
    span.setAttribute('data.size', data.length);
    return transformed;
  } catch (err) {
    span.setStatus({ code: 1, message: err.message }); // Set to Error status
    throw err;
  } finally {
    // Crucial: always end the span to avoid leaking resources
    span.end();
  }
}

Best Practices for Manual Spans

  1. Scope Management: Always use try...finally blocks to ensure span.end() is invoked. If a span is never closed, it remains open in memory, eventually leading to memory leaks and incomplete trace data.
  2. Context Propagation: If your manual operation involves asynchronous calls that move through different event loop ticks, ensure you are utilizing context.with() or the active context manager to keep the trace correlation intact.
  3. Attributes: Don't just track time; add meaningful metadata via span.setAttribute() to help filter your traces in your observability platform later.

Key Takeaway: Manually instrumenting spans allows you to break down internal bottlenecks by wrapping specific logic blocks in their own timed units, provided you strictly guarantee the closing of every span in a finally block.