Set up distributed tracing for Node.js and Python microservices with OpenTelemetry and Jaeger

Intermediate 45 min Apr 11, 2026 670 views
Ubuntu 24.04 Debian 12 AlmaLinux 9 Rocky Linux 9

Configure end-to-end distributed tracing across Node.js and Python microservices using OpenTelemetry instrumentation and Jaeger backend with Elasticsearch for centralized trace collection and analysis.

Prerequisites

  • Root or sudo access
  • 4GB+ RAM available
  • Docker support
  • Node.js and Python development knowledge

What this solves

Distributed tracing helps you monitor request flows across multiple microservices, identify performance bottlenecks, and debug complex service interactions. This tutorial sets up OpenTelemetry instrumentation for Node.js and Python applications with Jaeger as the tracing backend, enabling you to visualize request paths, measure latencies, and troubleshoot issues in your microservice architecture.

Step-by-step installation

Update system packages

Start by updating your package manager to ensure you get the latest versions of dependencies.

sudo apt update && sudo apt upgrade -y
sudo dnf update -y

Install Docker and Docker Compose

Install Docker to run Jaeger and Elasticsearch containers for the tracing backend infrastructure.

sudo apt install -y apt-transport-https ca-certificates curl gnupg lsb-release
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
sudo apt update
sudo apt install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin
sudo dnf install -y yum-utils
sudo yum-config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo
sudo dnf install -y docker-ce docker-ce-cli containerd.io docker-compose-plugin

Start Docker service

Enable and start the Docker service, then add your user to the docker group.

sudo systemctl enable --now docker
sudo usermod -aG docker $USER
newgrp docker

Create Jaeger with Elasticsearch configuration

Set up a Docker Compose file to run Jaeger with Elasticsearch as the storage backend for better scalability and data retention.

version: '3.8'
services:
  elasticsearch:
    image: docker.elastic.co/elasticsearch/elasticsearch:8.11.0
    container_name: jaeger-elasticsearch
    environment:
      - discovery.type=single-node
      - xpack.security.enabled=false
      - "ES_JAVA_OPTS=-Xms512m -Xmx512m"
    ports:
      - "9200:9200"
    volumes:
      - elasticsearch_data:/usr/share/elasticsearch/data
    networks:
      - jaeger-net

  jaeger-collector:
    image: jaegertracing/jaeger-collector:1.51
    container_name: jaeger-collector
    environment:
      - SPAN_STORAGE_TYPE=elasticsearch
      - ES_SERVER_URLS=http://elasticsearch:9200
      - ES_NUM_SHARDS=1
      - ES_NUM_REPLICAS=0
    ports:
      - "14269:14269"
      - "14268:14268"
      - "14250:14250"
      - "4317:4317"
      - "4318:4318"
    depends_on:
      - elasticsearch
    networks:
      - jaeger-net

  jaeger-query:
    image: jaegertracing/jaeger-query:1.51
    container_name: jaeger-query
    environment:
      - SPAN_STORAGE_TYPE=elasticsearch
      - ES_SERVER_URLS=http://elasticsearch:9200
    ports:
      - "16686:16686"
      - "16687:16687"
    depends_on:
      - elasticsearch
    networks:
      - jaeger-net

  jaeger-agent:
    image: jaegertracing/jaeger-agent:1.51
    container_name: jaeger-agent
    command: [
      "--reporter.grpc.host-port=jaeger-collector:14250"
    ]
    ports:
      - "5775:5775/udp"
      - "6831:6831/udp"
      - "6832:6832/udp"
      - "5778:5778"
    depends_on:
      - jaeger-collector
    networks:
      - jaeger-net

volumes:
  elasticsearch_data:

networks:
  jaeger-net:
    driver: bridge

Create the tracing directory and start services

Create the directory structure and start the Jaeger infrastructure with Elasticsearch backend.

sudo mkdir -p /opt/tracing
sudo chown $USER:$USER /opt/tracing
cd /opt/tracing
docker compose up -d

Install Node.js and npm

Install Node.js runtime and npm package manager for the Node.js microservice development.

curl -fsSL https://deb.nodesource.com/setup_20.x | sudo -E bash -
sudo apt install -y nodejs
curl -fsSL https://rpm.nodesource.com/setup_20.x | sudo bash -
sudo dnf install -y nodejs npm

Install Python and pip

Install Python runtime and pip package manager for the Python microservice development.

sudo apt install -y python3 python3-pip python3-venv
sudo dnf install -y python3 python3-pip python3-virtualenv

Create Node.js microservice with OpenTelemetry

Set up a sample Node.js Express application with OpenTelemetry auto-instrumentation for distributed tracing.

mkdir -p ~/microservices/nodejs-service
cd ~/microservices/nodejs-service
npm init -y

Install Node.js OpenTelemetry dependencies

Install the required OpenTelemetry packages for automatic instrumentation and OTLP export.

npm install express axios
npm install @opentelemetry/api @opentelemetry/sdk-node @opentelemetry/auto-instrumentations-node @opentelemetry/exporter-jaeger @opentelemetry/exporter-otlp-http

Create OpenTelemetry configuration for Node.js

Configure OpenTelemetry initialization with automatic instrumentation and Jaeger exporter.

const { NodeSDK } = require('@opentelemetry/sdk-node');
const { getNodeAutoInstrumentations } = require('@opentelemetry/auto-instrumentations-node');
const { JaegerExporter } = require('@opentelemetry/exporter-jaeger');
const { OTLPTraceExporter } = require('@opentelemetry/exporter-otlp-http');
const { Resource } = require('@opentelemetry/resources');
const { SemanticResourceAttributes } = require('@opentelemetry/semantic-conventions');

// Configure OTLP exporter for Jaeger
const traceExporter = new OTLPTraceExporter({
  url: 'http://localhost:4318/v1/traces',
});

// Initialize the SDK
const sdk = new NodeSDK({
  resource: new Resource({
    [SemanticResourceAttributes.SERVICE_NAME]: 'nodejs-microservice',
    [SemanticResourceAttributes.SERVICE_VERSION]: '1.0.0',
  }),
  traceExporter: traceExporter,
  instrumentations: [getNodeAutoInstrumentations()],
});

// Start the SDK
sdk.start();

console.log('OpenTelemetry started successfully');

// Graceful shutdown
process.on('SIGTERM', () => {
  sdk.shutdown()
    .then(() => console.log('Tracing terminated'))
    .catch((error) => console.log('Error terminating tracing', error))
    .finally(() => process.exit(0));
});

Create Node.js application

Build an Express application that demonstrates distributed tracing with HTTP requests to other services.

require('./tracing'); // Initialize tracing first

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

const app = express();
const PORT = process.env.PORT || 3000;

// Get tracer
const tracer = trace.getTracer('nodejs-microservice');

app.use(express.json());

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'healthy', service: 'nodejs-microservice' });
});

// Main endpoint that calls Python service
app.get('/process/:id', async (req, res) => {
  const span = tracer.startSpan('process-request');
  
  try {
    const { id } = req.params;
    span.setAttributes({
      'request.id': id,
      'service.operation': 'process-request'
    });

    // Simulate some processing
    await new Promise(resolve => setTimeout(resolve, Math.random() * 100));

    // Call Python microservice
    const pythonResponse = await axios.get(`http://localhost:8000/analyze/${id}`, {
      headers: {
        'x-trace-id': span.spanContext().traceId
      }
    });

    const result = {
      id: id,
      timestamp: new Date().toISOString(),
      nodeData: {
        processed: true,
        processingTime: Math.random() * 100
      },
      pythonData: pythonResponse.data
    };

    span.setAttributes({
      'response.status': 'success',
      'response.size': JSON.stringify(result).length
    });

    res.json(result);
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    
    res.status(500).json({ 
      error: 'Processing failed', 
      message: error.message 
    });
  } finally {
    span.end();
  }
});

// Batch processing endpoint
app.post('/batch', async (req, res) => {
  const span = tracer.startSpan('batch-process');
  
  try {
    const { items } = req.body;
    span.setAttributes({
      'batch.size': items.length,
      'service.operation': 'batch-process'
    });

    const results = [];
    for (const item of items) {
      const childSpan = tracer.startSpan('process-item', { parent: span });
      childSpan.setAttributes({ 'item.id': item.id });
      
      // Simulate processing
      await new Promise(resolve => setTimeout(resolve, Math.random() * 50));
      
      results.push({
        id: item.id,
        processed: true,
        timestamp: new Date().toISOString()
      });
      
      childSpan.end();
    }

    res.json({ results, total: results.length });
  } catch (error) {
    span.recordException(error);
    span.setStatus({ code: 2, message: error.message });
    res.status(500).json({ error: 'Batch processing failed' });
  } finally {
    span.end();
  }
});

app.listen(PORT, () => {
  console.log(`Node.js microservice running on port ${PORT}`);
});

Create Python virtual environment

Set up a Python virtual environment for the Python microservice with isolated dependencies.

mkdir -p ~/microservices/python-service
cd ~/microservices/python-service
python3 -m venv venv
source venv/bin/activate

Install Python OpenTelemetry dependencies

Install the required OpenTelemetry packages for Python along with FastAPI for the web framework.

pip install fastapi uvicorn requests
pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-jaeger opentelemetry-exporter-otlp
pip install opentelemetry-instrumentation-fastapi opentelemetry-instrumentation-requests opentelemetry-instrumentation-logging

Create Python OpenTelemetry configuration

Configure OpenTelemetry initialization for the Python application with automatic instrumentation.

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.sdk.resources import Resource
from opentelemetry.semconv.resource import ResourceAttributes
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
from opentelemetry.instrumentation.logging import LoggingInstrumentor
import logging

def init_tracing():
    # Create resource
    resource = Resource(attributes={
        ResourceAttributes.SERVICE_NAME: "python-microservice",
        ResourceAttributes.SERVICE_VERSION: "1.0.0",
    })
    
    # Set tracer provider
    trace.set_tracer_provider(TracerProvider(resource=resource))
    
    # Create OTLP exporter
    otlp_exporter = OTLPSpanExporter(
        endpoint="http://localhost:4318/v1/traces",
    )
    
    # Create span processor
    span_processor = BatchSpanProcessor(otlp_exporter)
    
    # Add span processor to tracer provider
    trace.get_tracer_provider().add_span_processor(span_processor)
    
    # Auto-instrument libraries
    RequestsInstrumentor().instrument()
    LoggingInstrumentor().instrument(set_logging_format=True)
    
    # Configure logging
    logging.basicConfig(level=logging.INFO)
    
    print("OpenTelemetry initialized for Python service")

def get_tracer():
    return trace.get_tracer(__name__)

Create Python FastAPI application

Build a FastAPI application with OpenTelemetry instrumentation that processes requests and demonstrates trace correlation.

from fastapi import FastAPI, HTTPException, Request
from pydantic import BaseModel
import asyncio
import random
import time
import logging
from typing import List, Optional
from datetime import datetime

# Import tracing configuration
from tracing import init_tracing, get_tracer
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry import trace
from opentelemetry.trace import Status, StatusCode

# Initialize tracing
init_tracing()

# Create FastAPI app
app = FastAPI(title="Python Microservice", version="1.0.0")

# Instrument FastAPI
FastAPIInstrumentor.instrument_app(app)

# Get tracer
tracer = get_tracer()

logger = logging.getLogger(__name__)

class AnalysisResult(BaseModel):
    id: str
    analysis_type: str
    score: float
    metadata: dict
    processing_time_ms: float
    timestamp: str

@app.get("/health")
async def health_check():
    return {"status": "healthy", "service": "python-microservice"}

@app.get("/analyze/{item_id}", response_model=AnalysisResult)
async def analyze_item(item_id: str, request: Request):
    with tracer.start_as_current_span("analyze-item") as span:
        try:
            start_time = time.time()
            
            # Extract trace context from headers if available
            trace_id = request.headers.get('x-trace-id')
            if trace_id:
                span.set_attribute("parent.trace_id", trace_id)
            
            span.set_attributes({
                "item.id": item_id,
                "service.operation": "analyze-item",
                "analysis.type": "sentiment"
            })
            
            logger.info(f"Starting analysis for item {item_id}")
            
            # Simulate different types of analysis
            analysis_types = ["sentiment", "classification", "similarity", "anomaly"]
            analysis_type = random.choice(analysis_types)
            
            # Simulate processing time
            processing_delay = random.uniform(0.1, 0.5)
            await asyncio.sleep(processing_delay)
            
            # Simulate analysis score
            score = random.uniform(0.1, 0.99)
            
            # Create child span for detailed analysis
            with tracer.start_as_current_span("detailed-analysis") as detail_span:
                detail_span.set_attributes({
                    "analysis.algorithm": f"{analysis_type}-v2",
                    "analysis.score": score,
                    "analysis.confidence": random.uniform(0.7, 0.95)
                })
                
                # Simulate detailed processing
                await asyncio.sleep(random.uniform(0.05, 0.15))
            
            end_time = time.time()
            processing_time = (end_time - start_time) * 1000
            
            result = AnalysisResult(
                id=item_id,
                analysis_type=analysis_type,
                score=score,
                metadata={
                    "algorithm_version": "2.1",
                    "confidence": random.uniform(0.7, 0.95),
                    "features_extracted": random.randint(10, 50)
                },
                processing_time_ms=processing_time,
                timestamp=datetime.utcnow().isoformat()
            )
            
            span.set_attributes({
                "response.status": "success",
                "processing.time_ms": processing_time,
                "analysis.score": score
            })
            
            span.set_status(Status(StatusCode.OK))
            logger.info(f"Completed analysis for item {item_id}")
            
            return result
            
        except Exception as e:
            span.record_exception(e)
            span.set_status(Status(StatusCode.ERROR, str(e)))
            logger.error(f"Error analyzing item {item_id}: {str(e)}")
            raise HTTPException(status_code=500, detail=f"Analysis failed: {str(e)}")

@app.post("/batch-analyze")
async def batch_analyze(items: List[str]):
    with tracer.start_as_current_span("batch-analyze") as span:
        try:
            span.set_attributes({
                "batch.size": len(items),
                "service.operation": "batch-analyze"
            })
            
            results = []
            
            for item_id in items:
                with tracer.start_as_current_span("batch-item-analysis") as item_span:
                    item_span.set_attribute("item.id", item_id)
                    
                    # Simulate processing
                    await asyncio.sleep(random.uniform(0.05, 0.2))
                    
                    result = {
                        "id": item_id,
                        "status": "processed",
                        "score": random.uniform(0.1, 0.99),
                        "timestamp": datetime.utcnow().isoformat()
                    }
                    
                    results.append(result)
            
            span.set_attributes({
                "response.status": "success",
                "batch.processed_count": len(results)
            })
            
            return {
                "results": results,
                "total_processed": len(results),
                "processing_timestamp": datetime.utcn

Automated install script

Run this to automate the entire setup

Wil je dit niet zelf beheren?

Wij beheren infrastructuur voor bedrijven die afhankelijk zijn van uptime. Volledig beheerd, met één vast aanspreekpunt dat je omgeving kent.

U krijgt één vast aanspreekpunt dat uw omgeving kent

Op kantoor in Rotterdam 14:07 · bereikbaar in een bericht, geen ticketformulier