Published: 2026-06-01 โ€ข Updated: 2026-07-05

Instrumenting Applications with OpenTelemetry (OTel)

An Operational Guide to Automatic and Manual Instrumentation, Context Propagation, and Structuring Telemetry Exports to Grafana Tempo.


Executive Summary & Core Concepts

Distributed tracing backend platforms like Grafana Tempo are only as useful as the data they receive. To gain deep visibility into a microservices network, your application code must be instrumented to actively measure its own internal execution steps, record performance metadata, and forward these tracing records to your monitoring infrastructure. OpenTelemetry (OTel) is the industry-standard, vendor-neutral framework designed to handle this data generation layer.

OpenTelemetry separates telemetry collection into an API and an SDK. The API provides a lightweight, abstract interface that developers use to write code instrumentation without binding their application to a specific backend vendor. The SDK implements that API, managing the heavy lifting under the hood: handling memory allocations, sampling traffic, batching spans, and managing connection pools to ship telemetry out to an OpenTelemetry Collector or an ingestion target like Tempo.

  • Tracer Provider: The core stateful factory object within the OpenTelemetry SDK that manages the lifecycle of your application's tracing pipelines and configurations.
  • Sampler: The configuration component that determines which requests are recorded as active traces and which are safely discarded to save storage space and network bandwidth.
  • Span Processor: The internal pipeline stage (such as the BatchSpanProcessor) that intercepts completed spans, buffers them in memory, and handles background exports asynchronously to protect application performance.
  • Automatic Instrumentation: Dynamic runtime code modification that hooks into standard framework libraries (like Express, HTTP, or gRPC) to extract tracing data automatically without altering your source code.

Instrumentation Strategies: Automatic vs. Manual

Modern software engineering balances two different approaches to code instrumentation depending on the level of depth and customization required.

1. Automatic Instrumentation (Zero-Code Modification)

Automatic instrumentation hooks into your application runtime (using Java agents, Node.js monkey-patching, or Python wrappers) to intercept standard framework libraries. It automatically captures inbound HTTP requests, injects W3C context headers, measures outbound database queries, and tracks basic error codes. This allows development teams to gain immediate end-to-end visibility across their entire architecture without writing any custom monitoring code.

2. Manual Instrumentation (Deep Programmatic Control)

Manual instrumentation uses the native OpenTelemetry API directly inside your business logic to measure highly specific application behavior. This approach is necessary when you need to track code loops, capture internal data states, or inject custom domain metadata (like an organization_id or cart_value) into your span attributes for targeted troubleshooting.

Enterprise SDK Initialization (Node.js & TypeScript)

To implement tracing in a production microservice, you must initialize the OpenTelemetry SDK before your application loads any other module, ensuring the framework hooks can capture downstream operations accurately.

Production Instrumentation Bootstrapper

Create a dedicated file named instrumentation.ts to configure an export pipeline that streams structured data out to an OpenTelemetry Collector using the efficient gRPC protocol:


// instrumentation.ts
import { NodeSDK } from '@opentelemetry/sdk-node';
import { Resource } from '@opentelemetry/resources';
import { SemanticResourceAttributes } from '@opentelemetry/semantic-conventions';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { ParentBasedSampler, TraceIdRatioBasedSampler } from '@opentelemetry/sdk-trace-node';
import { HttpInstrumentation } from '@opentelemetry/instrumentation-http';
import { ExpressInstrumentation } from '@opentelemetry/instrumentation-express';

// Define the structural identity of this application instance
const resource = new Resource({
  [SemanticResourceAttributes.SERVICE_NAME]: 'checkout-api-service',
  [SemanticResourceAttributes.SERVICE_VERSION]: '1.4.2',
  [SemanticResourceAttributes.DEPLOYMENT_ENVIRONMENT]: 'production',
});

// Configure an optimized OTLP gRPC telemetry exporter
const traceExporter = new OTLPTraceExporter({
  url: 'http://otel-collector.internal.net:4317', // Points to central collector ingress
});

// Initialize the core OpenTelemetry NodeSDK pipeline
const sdk = new NodeSDK({
  resource: resource,
  // Optimization: Sample exactly 10% of total production traffic to manage storage costs
  sampler: new ParentBasedSampler({
    root: new TraceIdRatioBasedSampler(0.10),
  }),
  // Asynchronously batch spans to prevent monitoring code from adding request latency
  spanProcessor: new BatchSpanProcessor(traceExporter, {
    maxQueueSize: 2048,
    maxExportBatchSize: 512,
    scheduledDelayMillis: 5000,
  }),
  // Register automatic instrumentation plugins for standard frameworks
  instrumentation: [
    new HttpInstrumentation(),
    new ExpressInstrumentation(),
  ],
});

// Start the SDK pipeline and handle clean resource teardown on system exit
sdk.start();

process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('OpenTelemetry SDK cleanly terminated.'))
    .catch((error) => console.error('Error terminating OpenTelemetry SDK:', error))
    .finally(() => process.exit(0));
});
    
Production Bootstrapping Rule: When starting your application, you must require this instrumentation file before loading your main entry point (e.g., node -r ./instrumentation.js dist/index.js). Loading your main application logic before the OpenTelemetry SDK initializes can cause the automatic framework hooks to miss incoming requests.

Writing Manual Tracer Logic and Context Capture

Once the global SDK is initialized, you can use the OpenTelemetry API anywhere in your codebase to create custom spans around critical business operations and attach high-value metadata attributes.


import { trace, SpanStatusCode, context } from '@opentelemetry/api';

async function processPaymentTransaction(orderId: string, amount: number): Promise<void> {
  // Access the globally registered tracer instance
  const tracer = trace.getTracer('checkout-api-service', '1.4.2');
  
  // Start a new span bounded to this payment operation
  const span = tracer.startSpan('process_payment_transaction');
  
  // Inject domain-specific business metadata into the span attributes
  span.setAttribute('payment.order_id', orderId);
  span.setAttribute('payment.transaction_amount', amount);
  span.setAttribute('payment.processor', 'stripe');

  try {
    // Execute core payment processing business logic
    await executeStripeCharge(orderId, amount);
    
    // Explicitly mark the span status as successful
    span.setStatus({ code: SpanStatusCode.OK });
  } catch (error: any) {
    // Record anomalies and exception details automatically
    span.recordException(error);
    span.setStatus({
      code: SpanStatusCode.ERROR,
      message: error.message || 'Stripe connection timeout event',
    });
    throw error;
  } finally {
    // Always close the span to record its final duration and queue it for export
    span.end();
  }
}
    

Technical Interview Questions & Detailed Answers

Q1: What is the mechanical difference between the OpenTelemetry API and the OpenTelemetry SDK? Why is this decoupling critical for enterprise library maintainers?

Answer: The separation of the API and the SDK is a fundamental architectural pattern in OpenTelemetry:

  • The API: Contains only the abstract interfaces, tracer hooks, and empty data references used to write instrumentation. It carries zero external dependencies and does not perform any processing logic or network transfers.
  • The SDK: Implements the API interfaces, managing the concrete application behaviors like memory buffers, sampling rules, context propagation protocols, and network connections to forward data to exporters.

This decoupling is critical for enterprise software and open-source library maintainers because it allows them to embed standard OpenTelemetry API instrumentation directly into their shared libraries without forcing downstream users to adopt a specific monitoring platform or carry heavy SDK operational footprints. If an application imports an instrumented library but does not initialize an OpenTelemetry SDK, the API calls safely fall back to an internal no-op layer, ensuring zero performance overhead.

Q2: Why is the BatchSpanProcessor highly preferred over the SimpleSpanProcessor in production container environments? Explain the underlying resource mechanics.

Answer: The choice of span processor directly impacts application performance and network resource management under heavy traffic:

  • SimpleSpanProcessor: Evaluates and exports each span synchronously to the network target as soon as span.end() is called. In a busy production environment, this creates massive connection overhead, blocks application threads during network latency spikes, and can quickly saturate your network interface card.
  • BatchSpanProcessor: Intercepts completed spans and queues them instantly into a highly efficient in-memory ring buffer. A dedicated background routine wakes up at configurable intervals (e.g., every 5 seconds) or when the batch size fills up, bundles the queued spans into a single, compressed network payload, and ships them out asynchronously. This batching strategy isolates your application's request loop from monitoring network traffic, ensuring that logging and tracing overhead never degrades user performance.

Summary

Instrumenting Applications with OpenTelemetry establishes the foundational telemetry generation layer required for full system visibility. By leveraging automatic instrumentation for standard web framework hooks alongside manual instrumentation for custom business attributes, platform teams can capture rich telemetry data across their services. Configuring optimized SDK pipelines with robust sampling and async batch span processors ensures high-performance telemetry collection that scales reliably across enterprise networks.

About the Author

Naresh Kumar

Naresh Kumar

Senior Java Backend Engineer experienced in Banking, Payments, ISO 20022, Spring Boot, Microservices, Kafka, Docker, Kubernetes, AWS and Cloud Native Systems.

Built enterprise payment solutions, transaction processing systems, API platforms and scalable microservices used in production.

LinkedIn Profile