← Back to list

Polyglot Application Distributed Tracing With OpenTelemetry

A hands-on observability demo that showcases end-to-end distributed tracing across four microservices built with different technology…

Suneel Kandali · 2026-06-14 16:05 · 1 claps · 12.3 min read
#opentelemetry #distributed-tracing #jaeger #microservices-tracing
Open on Medium ↗

Polyglot Application Distributed Tracing With OpenTelemetry and Jaeger

A hands-on observability demo that showcases end-to-end distributed tracing across four microservices built with different technology stacks — React, Spring Boot (Java), FastAPI (Python), and Express (Node.js) — all wired together using OpenTelemetry and visualized in Jaeger.

When you click the “Fire Microservice Cascade” button in the React UI, a single request cascades through all four services, and a correlated distributed trace appears in Jaeger — proving that context propagation works across language and layer boundaries.

📋 Table of Contents

🔧 Required Tools & Software

1. Homebrew

Homebrew is the package manager for macOS. Install it first as other tools depend on it.

/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)"

After installation, verify:

brew --version

2. Docker Desktop for Mac

Docker Desktop is required to build and run all containerized services.

Install via Homebrew:

brew install --cask docker

Then open Docker Desktop from your Applications folder. Ensure it is running before proceeding. You should see the Docker icon in your menu bar.

Verify installation:

docker --version
docker compose version

Note for Apple Silicon (M1/M2/M3): Docker Desktop natively supports Apple Silicon. Make sure you are running Docker Desktop version 4.x or later for best compatibility.

3. Git

brew install git

Verify:

git --version

4. cURL (Pre-installed on macOS)

cURL is pre-installed on macOS. Verify it is available:

curl --version

🏗️ Application Architecture

The application consists of 6 services orchestrated via Docker Compose. Four are application microservices that form a trace cascade, and two are observability backends.

Logical Flow Diagram

┌──────────────────────────────────────────────────────────────────────────────────┐
│                          OBSERVABILITY PLAYGROUND                                │
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐    │
│   │                        TRACE CASCADE FLOW                               │    │
│   │                                                                          │    │
│   │   ┌──────────────┐     ┌──────────────┐     ┌──────────────┐            │    │
│   │   │              │     │              │     │              │            │    │
│   │   │    React     │────▶│  Spring Boot │────▶│    Python    │            │    │
│   │   │   Frontend   │     │  API Gateway │     │  FastAPI     │            │    │
│   │   │   (Port 3001)│     │  (Port 8080) │     │  (Port 8000) │            │    │
│   │   │              │     │              │     │              │            │    │
│   │   └──────────────┘     └──────────────┘     └──────┬───────┘            │    │
│   │                                                     │                    │    │
│   │                                                     ▼                    │    │
│   │                                              ┌──────────────┐            │    │
│   │                                              │              │            │    │
│   │                                              │   Node.js    │            │    │
│   │                                              │   Express    │            │    │
│   │                                              │  (Port 5002) │            │    │
│   │                                              │              │            │    │
│   │                                              └──────────────┘            │    │
│   └──────────────────────────────────────────────────────────────────────────┘    │
│                                                                                  │
│   ┌──────────────────────────────────────────────────────────────────────────┐    │
│   │                    OBSERVABILITY PIPELINE                                │    │
│   │                                                                          │    │
│   │   All services ──────▶ OTel Collector ──────▶ Jaeger UI                 │    │
│   │                    (OTLP gRPC/HTTP)          (Port 16686)               │    │
│   └──────────────────────────────────────────────────────────────────────────┘    │
│                                                                                  │
└──────────────────────────────────────────────────────────────────────────────────┘

Service Communication Flow (Text Line Diagram)

Browser (Mac) ──HTTP──▶ React Frontend (3001)
                            │
                            │ fetch('http://localhost:8080/api/start')
                            ▼
                        Spring Boot API Gateway (8080)
                            │
                            │ HTTP call to http://python-service:8000/process
                            ▼
                        Python FastAPI Service (8000)
                            │
                            │ HTTP call to http://node-service:5000/finalize
                            ▼
                        Node.js Express Service (5000)
  All 4 services ──OTLP──▶ OpenTelemetry Collector (4317/4318) ──▶ Jaeger (16686)

📁 Project Structure

opentelemetry-playground/
├── docker-compose.yml                  # Orchestrates all 6 services
├── otel-collector-config.yaml          # OpenTelemetry Collector configuration
├── .gitignore                          # Git ignore rules
├── README.md                           # This file
│
├── react-frontend/                     # React UI (triggers the cascade)
│   ├── Dockerfile
│   ├── package.json
│   ├── public/
│   │   └── index.html
│   └── src/
│       ├── index.js                    # Entry point (loads tracing first)
│       ├── App.js                      # UI component with "Fire Cascade" button
│       └── tracing.js                  # OpenTelemetry web tracing setup
│
├── spring-boot-api/                    # Java Spring Boot API Gateway
│   ├── Dockerfile                      # Multi-stage build with OTel Java agent
│   ├── pom.xml                         # Maven dependencies (Spring Boot 3.2.3)
│   └── src/main/java/com/example/demo/
│       └── DemoApplication.java        # Gateway controller, calls Python service
│
├── python-service/                     # Python FastAPI microservice
│   ├── Dockerfile
│   ├── requirements.txt
│   └── app.py                          # FastAPI app, calls Node.js service
│
└── node-service/                       # Node.js Express terminal service
    ├── Dockerfile
    ├── package.json
    ├── server.js                       # Express app, returns final response
    └── tracing.js                      # OpenTelemetry Node SDK setup

📄 File Contents

docker-compose.yml

version: '3.8'
services:
  jaeger:
    image: jaegertracing/all-in-one:latest
    ports:
      - "16686:16686" # Jaeger UI on your Mac browser
    networks:
      - otel-network
  otel-collector:
    image: otel/opentelemetry-collector-contrib:latest
    volumes:
      - ./otel-collector-config.yaml:/etc/otelcol/config.yaml
    command: ["--config=/etc/otelcol/config.yaml"]
    ports:
      - "4317:4317" # OTLP gRPC (Internal network backend tracking)
      - "4318:4318" # OTLP HTTP (Exposed to your Mac for React Frontend)
    depends_on:
      - jaeger
    networks:
      - otel-network
  react-frontend:
    build: ./react-frontend
    ports:
      - "3001:3000" # React UI on your Mac browser
    networks:
      - otel-network
  spring-boot-api:
    build: ./spring-boot-api
    ports:
      - "8080:8080"
    environment:
      - OTEL_SERVICE_NAME=spring-boot-api
      - OTEL_EXPORTER_OTLP_ENDPOINT=http://otel-collector:4318
      - OTEL_TRACES_EXPORTER=otlp
      - OTEL_METRICS_EXPORTER=none
      - OTEL_LOGS_EXPORTER=none
    depends_on:
      - otel-collector
    networks:
      - otel-network
  python-service:
    build: ./python-service
    ports:
      - "8000:8000"
    environment:
      - OTEL_SERVICE_NAME=python-service
    depends_on:
      - otel-collector
    networks:
      - otel-network
  node-service:
    build: ./node-service
    ports:
      - "5002:5000" # Maps port 5002 on your Mac to 5000 inside the container
    depends_on:
      - otel-collector
    networks:
      - otel-network
networks:
  otel-network:
    driver: bridge

otel-collector-config.yaml

receivers:
  otlp:
    protocols:
      grpc:
        endpoint: 0.0.0.0:4317
      http:
        endpoint: 0.0.0.0:4318
        cors:
          allowed_origins: ["http://localhost:3001"]
exporters:
  otlp/jaeger:
    endpoint: "jaeger:4317"
    tls:
      insecure: true
processors:
  batch:
service:
  pipelines:
    traces:
      receivers: [otlp]
      processors: [batch]
      exporters: [otlp/jaeger]

react-frontend/package.json

{
  "name": "react-frontend",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/context-zone": "^1.30.0",
    "@opentelemetry/exporter-trace-otlp-http": "^0.57.0",
    "@opentelemetry/instrumentation-fetch": "^0.57.0",
    "@opentelemetry/sdk-trace-web": "^1.30.0",
    "@opentelemetry/resources": "^1.30.0",
    "@opentelemetry/semantic-conventions": "^1.28.0",
    "react": "^18.2.0",
    "react-dom": "^18.2.0",
    "react-scripts": "5.0.1"
  },
  "scripts": {
    "start": "BROWSER=none react-scripts start"
  }
}

react-frontend/Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 3000
CMD ["npm", "start"]

react-frontend/public/index.html

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Observability Playground</title>
  </head>
  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
  </body>
</html>

react-frontend/src/index.js

import './tracing'; // Must be the absolute first import!
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>
);

react-frontend/src/tracing.js

import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { BatchSpanProcessor } from '@opentelemetry/sdk-trace-base';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { registerInstrumentations } from '@opentelemetry/instrumentation';
import { FetchInstrumentation } from '@opentelemetry/instrumentation-fetch';
import { ZoneContextManager } from '@opentelemetry/context-zone';
import { Resource } from '@opentelemetry/resources';
import { ATTR_SERVICE_NAME } from '@opentelemetry/semantic-conventions';
const provider = new WebTracerProvider({
  resource: new Resource({
    [ATTR_SERVICE_NAME]: 'react-frontend',
  }),
});
const exporter = new OTLPTraceExporter({
  url: 'http://localhost:4318/v1/traces',
});
provider.addSpanProcessor(new BatchSpanProcessor(exporter));
provider.register({
  contextManager: new ZoneContextManager(),
});
registerInstrumentations({
  instrumentations: [
    new FetchInstrumentation({
      propagateTraceHeaderCorsUrls: [ /localhost:8080/ ],
    }),
  ],
});

react-frontend/src/App.js

import React, { useState } from 'react';
function App() {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);
  const fireCascade = async () => {
    setLoading(true);
    setError(null);
    setData(null);
    try {
      const response = await fetch('http://localhost:8080/api/start');
      if (!response.ok) {
        throw new Error(`Server responded with status ${response.status}`);
      }
      const json = await response.json();
      setData(json);
    } catch (err) {
      console.error("Cascade execution failed:", err);
      setError(err.message || "An unexpected error occurred in the network chain.");
    } finally {
      setLoading(false);
    }
  };
  return (
    <div style={styles.container}>
      <header style={styles.header}>
        <h1>⚡ Observability Playground ⚡</h1>
        <p>Trigger an end-to-end distributed trace across React, Java, Python, and Node.js.</p>
      </header>
      <main style={styles.main}>
        <button
          onClick={fireCascade}
          disabled={loading}
          style={{
            ...styles.button,
            ...(loading ? styles.buttonDisabled : {})
          }}
        >
          {loading ? 'Dropping Telemetry Spans...' : 'Fire Microservice Cascade 🚀'}
        </button>
        <div style={styles.outputContainer}>
          {loading && (
            <div style={styles.loadingBox}>
              <div style={styles.spinner}></div>
              <p>Traversing network layers: React ➔ Spring Boot ➔ FastAPI ➔ Express...</p>
            </div>
          )}
          {error && (
            <div style={styles.errorBox}>
              <h3>❌ Cascade Blocked</h3>
              <p>{error}</p>
            </div>
          )}
          {data && (
            <div style={styles.successBox}>
              <div style={styles.successHeader}>
                <span>✅ Request Chain Successful!</span>
                <span style={styles.badge}>200 OK</span>
              </div>
              <p style={styles.successText}>
                The downstream microservices processed the token chain successfully.
                Check your Jaeger dashboard to see the matching distributed trace!
              </p>
              <pre style={styles.jsonBlock}>
                {JSON.stringify(data, null, 2)}
              </pre>
            </div>
          )}
        </div>
      </main>
    </div>
  );
}
const styles = {
  container: {
    fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
    backgroundColor: '#0f172a',
    color: '#f8fafc',
    minHeight: '100vh',
    padding: '2rem',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
  },
  header: {
    textAlign: 'center',
    marginBottom: '2rem',
  },
  main: {
    width: '100%',
    maxWidth: '650px',
    display: 'flex',
    flexDirection: 'column',
    alignItems: 'center',
  },
  button: {
    backgroundColor: '#3b82f6',
    color: '#ffffff',
    border: 'none',
    padding: '0.75rem 1.5rem',
    fontSize: '1.1rem',
    fontWeight: 'bold',
    borderRadius: '6px',
    cursor: 'pointer',
    transition: 'background-color 0.2s',
    boxShadow: '0 4px 6px -1px rgba(59, 130, 246, 0.3)',
  },
  buttonDisabled: {
    backgroundColor: '#475569',
    cursor: 'not-allowed',
    boxShadow: 'none',
  },
  outputContainer: {
    marginTop: '2rem',
    width: '100%',
  },
  loadingBox: {
    textAlign: 'center',
    color: '#94a3b8',
    padding: '1.5rem',
  },
  errorBox: {
    backgroundColor: '#451a03',
    border: '1px solid #7f1d1d',
    borderRadius: '8px',
    padding: '1.25rem',
    color: '#fca5a5',
  },
  successBox: {
    backgroundColor: '#022c22',
    border: '1px solid #064e3b',
    borderRadius: '8px',
    padding: '1.5rem',
    boxShadow: '0 10px 15px -3px rgba(0, 0, 0, 0.3)',
  },
  successHeader: {
    display: 'flex',
    justifyContent: 'space-between',
    alignItems: 'center',
    fontWeight: 'bold',
    color: '#34d399',
    fontSize: '1.1rem',
  },
  badge: {
    backgroundColor: '#047857',
    color: '#fff',
    padding: '0.2rem 0.5rem',
    borderRadius: '4px',
    fontSize: '0.8rem',
  },
  successText: {
    color: '#a7f3d0',
    fontSize: '0.95rem',
    margin: '0.75rem 0 1.2rem 0',
    lineHeight: '1.4',
  },
  jsonBlock: {
    backgroundColor: '#090d16',
    padding: '1rem',
    borderRadius: '6px',
    overflowX: 'auto',
    color: '#38bdf8',
    fontSize: '0.85rem',
    border: '1px solid #1e293b',
  }
};
export default App;

spring-boot-api/Dockerfile

# Stage 1: Build the App and Fetch Agent
FROM maven:3.9-eclipse-temurin-17 AS build
WORKDIR /app
COPY pom.xml .
RUN mvn dependency:go-offline
COPY src ./src
RUN mvn clean package -DskipTests
RUN curl -L -o opentelemetry-javaagent.jar \
  https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
# Stage 2: Runtime Environment
FROM eclipse-temurin:17-jre-jammy
WORKDIR /app
COPY --from=build /app/target/demo-0.0.1-SNAPSHOT.jar app.jar
COPY --from=build /app/opentelemetry-javaagent.jar opentelemetry-javaagent.jar
EXPOSE 8080
ENTRYPOINT ["java", "-javaagent:opentelemetry-javaagent.jar", "-jar", "app.jar"]

spring-boot-api/pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
         https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.3</version>
    </parent>
    <groupId>com.example</groupId>
    <artifactId>demo</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

spring-boot-api/src/main/java/com/example/demo/DemoApplication.java

package com.example.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Map;
@SpringBootApplication
public class DemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemoApplication.class, args);
    }
}
@RestController
@CrossOrigin(origins = "*")
class GatewayController {
    private final RestTemplate restTemplate = new RestTemplate();
    @GetMapping("/api/start")
    public Map<String, Object> startPipeline() {
        String pythonUrl = "http://python-service:8000/process";
        Map<String, Object> pythonResponse = restTemplate.getForObject(pythonUrl, Map.class);
        return Map.of(
            "layer", "Spring Boot API Gateway",
            "downstream", pythonResponse
        );
    }
}

python-service/Dockerfile

FROM python:3.11-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY app.py .
EXPOSE 8000
CMD ["python", "app.py"]

python-service/requirements.txt

fastapi
uvicorn
requests
opentelemetry-api
opentelemetry-sdk
opentelemetry-exporter-otlp
opentelemetry-instrumentation-fastapi
opentelemetry-instrumentation-requests

python-service/app.py

import uvicorn
from fastapi import FastAPI
import requests
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
resource = Resource(attributes={"service.name": "python-service"})
provider = TracerProvider(resource=resource)
processor = BatchSpanProcessor(OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True))
provider.add_span_processor(processor)
trace.set_tracer_provider(provider)
app = FastAPI()
from opentelemetry.instrumentation.fastapi import FastAPIInstrumentor
from opentelemetry.instrumentation.requests import RequestsInstrumentor
FastAPIInstrumentor.instrument_app(app)
RequestsInstrumentor().instrument()
@app.get("/process")
def process():
    node_response = requests.get("http://node-service:5000/finalize").json()
    return {
        "layer": "Python Microservice",
        "downstream": node_response
    }
if __name__ == "__main__":
    uvicorn.run(app, host="0.0.0.0", port=8000)

node-service/Dockerfile

FROM node:18-alpine
WORKDIR /app
COPY package.json ./
RUN npm install
COPY . .
EXPOSE 5000
CMD ["npm", "start"]

node-service/package.json

{
  "name": "node-service",
  "version": "1.0.0",
  "main": "server.js",
  "dependencies": {
    "@opentelemetry/api": "^1.9.0",
    "@opentelemetry/sdk-node": "^0.57.0",
    "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0",
    "@opentelemetry/auto-instrumentations-node": "^0.56.0",
    "express": "^4.19.2"
  },
  "scripts": {
    "start": "node -r ./tracing.js server.js"
  }
}

node-service/server.js

const express = require('express');
const app = express();
const PORT = 5000;
app.get('/finalize', (req, res) => {
  res.json({
    layer: "Node.js Database Core",
    status: "Pipeline successfully finished executing!"
  });
});
app.listen(PORT, () => {
  console.log(`Node engine running on port ${PORT}`);
});

node-service/tracing.js

const opentelemetry = require("@opentelemetry/sdk-node");
const { getNodeAutoInstrumentations } = require("@opentelemetry/auto-instrumentations-node");
const { OTLPTraceExporter } = require("@opentelemetry/exporter-trace-otlp-grpc");
const sdk = new opentelemetry.NodeSDK({
  resource: new (require("@opentelemetry/resources").Resource)({
    "service.name": "node-service",
  }),
  traceExporter: new OTLPTraceExporter({
    url: "http://otel-collector:4317",
  }),
  instrumentations: [getNodeAutoInstrumentations()],
});
sdk.start();

🚀 Running the Application

⚠️ This guide assumes you are running on a Mac with Apple Silicon (M1/M2/M3).

Step 1: Clone the Repository

git clone https://github.com/suneelkandali/multilayerapp-distributedtracing-playground
cd multilayerapp-distributedtracing-playground

Step 2: Ensure Docker Desktop is Running

Open Docker Desktop from your Applications folder. Wait for the Docker engine to start (the Docker icon in the menu bar should turn green).

docker info

Step 3: Free Up Required Ports

Before starting the application, ensure the following ports are not in use by other processes. These ports are required by the demo services:

PortService3001React Frontend8080Spring Boot API8000Python FastAPI5002Node.js Express16686Jaeger UI4317OTel Collector gRPC4318OTel Collector HTTP

Check all required ports at once:

for port in 3001 8080 8000 5002 16686 4317 4318; do
  pid=$(lsof -ti :$port 2>/dev/null)
  if [ -n "$pid" ]; then
    echo "⚠️  Port $port is in use by PID $pid"
  else
    echo "✅ Port $port is available"
  fi
done

Free up a specific port (if occupied):

# Find the process using the port
lsof -i :3001
# Kill the process (replace PID with the actual process ID from the output above)
kill -9 <PID>

Free up all required ports at once:

for port in 3001 8080 8000 5002 16686 4317 4318; do
  pid=$(lsof -ti :$port 2>/dev/null)
  if [ -n "$pid" ]; then
    echo "Killing process on port $port (PID: $pid)"
    kill -9 $pid
  fi
done

Step 4: Build and Start All Services

From the project root directory, run:

docker compose up --build

This command will:

  1. Pull the Jaeger and OpenTelemetry Collector images from Docker Hub.
  2. Build Docker images for all four application services (React, Spring Boot, Python, Node.js).
  3. Start all 6 containers and connect them via a shared Docker bridge network (otel-network).

Step 5: Wait for Services to Start

On first run, the Spring Boot Maven build may take 2–5 minutes to download dependencies. Watch the terminal output for these readiness messages:

spring-boot-api    | Started DemoApplication in X seconds
python-service     | Uvicorn running on http://0.0.0.0:8000
node-service       | Node engine running on port 5000
react-frontend     | Compiled successfully!

Step 6: Access the Application

Open the following URLs in your Mac browser:

ServiceURL. Purpose

React Frontend http://localhost:3001 UI to trigger the cascade

Jaeger UI http://localhost:16686 View distributed traces

Spring Boot http://localhost:8080/api/start Direct API endpoint

Python Service http://localhost:8000/process Direct API endpoint

🧪 Testing the Application

Test 1: Trigger a Full Distributed Trace via the UI

  1. Open **http://localhost:3001** in your browser.
  2. Click the “Fire Microservice Cascade 🚀” button.
  3. Wait for the response — you should see a green success box with a JSON response:
  • { "layer": "Spring Boot API Gateway", "downstream": { "layer": "Python Microservice", "downstream": { "layer": "Node.js Database Core", "status": "Pipeline successfully finished executing!" } } }

Test 2: View the Distributed Trace in Jaeger

  1. Open **http://localhost:16686** in your browser.
  2. In the left sidebar, select Service → react-frontend (or any of the four services).
  3. Click “Find Traces”.
  4. Click on the most recent trace to view the full waterfall:
  • You should see 4 spans in a waterfall pattern:
  • react-frontend (HTTP fetch)
  • spring-boot-api (HTTP GET /api/start)
  • python-service (HTTP GET /process)
  • node-service (HTTP GET /finalize)
  • All spans should share the same Trace ID, confirming successful context propagation.

Test 3: Verify Individual Service Endpoints

Test each service independently using curl:

# React Frontend (requires browser)
open http://localhost:3001
# Spring Boot API Gateway
curl http://localhost:8080/api/start
# Python FastAPI Service
curl http://localhost:8000/process
# Node.js Express Service
curl http://localhost:5002/finalize

🔍 Debugging Potential Issues

Issue 1: Port Already in Use

Symptom: Bind for 0.0.0.0:XXXX failed: port is already allocated

Solution:

# Find the process using the port
lsof -i :XXXX
# Kill the process (replace PID with the actual process ID)
kill -9 <PID>
# Or stop all Docker containers and retry
docker compose down
docker compose up --build

Issue 2: Spring Boot Container Fails to Start

Symptom: spring-boot-api exits immediately or keeps restarting.

Solution:

# Check the Spring Boot logs
docker compose logs spring-boot-api
# Common cause: Maven dependency download failed. Rebuild:
docker compose down
docker compose up --build spring-boot-api

Issue 3: React Frontend Cannot Reach Spring Boot

Symptom: “Cascade Blocked” error in the UI, or fetch failed in the browser console.

Solution:

  • Ensure Spring Boot is fully started before clicking the button (watch the logs).
  • The React frontend uses http://localhost:8080 to call Spring Boot. Verify Spring Boot is running:
  • curl [http://localhost:8080/api/start](http://localhost:8080/api/start)

Issue 4: No Traces Appearing in Jaeger

Symptom: Jaeger UI shows no traces after triggering the cascade.

Solution:

  1. Verify the OTel Collector is running:
  • docker compose ps otel-collector
  1. Check the collector logs:
  • docker compose logs otel-collector
  1. Ensure the react-frontend tracing.js exporter URL points to http://localhost:4318 (the HTTP port exposed to the Mac host).

Issue 5: Docker Build Fails on Apple Silicon

Symptom: exec format error or image architecture mismatch errors.

Solution:

  • Ensure you are using the latest version of Docker Desktop for Mac, which natively supports ARM64.
  • Try clearing Docker build cache:
  • docker compose down docker builder prune -a docker compose up --build

Issue 6: Python Service Import Errors

Symptom: ModuleNotFoundError in Python service logs.

Solution:

# Rebuild the Python service image from scratch
docker compose build --no-cache python-service
docker compose up python-service

Viewing Logs for Any Service

# Real-time logs for a specific service
docker compose logs -f <service-name>
# Examples:
docker compose logs -f react-frontend
docker compose logs -f spring-boot-api
docker compose logs -f python-service
docker compose logs -f node-service
docker compose logs -f otel-collector
docker compose logs -f jaeger

🧹 Clean Up Resources

Stop All Services (Keep Containers)

docker compose stop

Stop and Remove All Containers and Networks

docker compose down

Full Cleanup (Remove Containers, Networks, and Build Images)

docker compose down --rmi all

Remove All Docker Build Cache

docker builder prune -a

Verify Cleanup

docker compose ps
docker images | grep -E "opentelemetry|react|spring|python|node"

📌 Note

These steps are specifically written for running the application on a Mac with Apple Silicon (M1, M2, or M3) chip using Docker Desktop for Mac. The Docker images used in this project (node:18-alpine, python:3.11-slim, eclipse-temurin:17, maven:3.9-eclipse-temurin-17) all have multi-architecture builds that support linux/arm64, so no special platform flags are needed on Apple Silicon. If you are running on an Intel-based Mac or a Linux x86_64 machine, the same instructions apply — Docker will automatically pull the correct architecture images.


메타데이터
post_id
c7b572e000a4
slug
opentelemetry-playground-distributed-tracing-demo-c7b572e000a4
url
https://medium.com/@suneelr.kandali/opentelemetry-playground-distributed-tracing-demo-c7b572e000a4
canonical_url
https://medium.com/@suneelr.kandali/opentelemetry-playground-distributed-tracing-demo-c7b572e000a4
author_url
https://medium.com/@suneelr.kandali
status
ok
fetched_at
2026-07-10 11:40:45