Monitoring microservices is hard.
When a user request fans out across multiple services, each with its own database, logs, and failure modes, traditional monitoring tools often give you a fragmented picture. You can tell something is slow, but not exactly where or why.
Distributed tracing solves this.
In this tutorial, we'll implement distributed tracing for a Java Spring Boot microservices application using two open-source tools: OpenTelemetry and OpenObserve.
If your stack includes other languages, check out these guides too:
What you'll build
By the end of this guide, you'll have:
- A working Spring Boot microservices setup with cross-service HTTP calls
- Zero-code instrumentation using the OpenTelemetry Java Agent
- End-to-end traces in OpenObserve with flamegraph and Gantt chart views
What is distributed tracing?
In microservices, one user action can trigger a chain of calls across many services. If a request takes 3 seconds, tracing helps answer:
- Which service caused the delay?
- Which operation failed?
- Where exactly time was spent?
Distributed tracing works by attaching context (trace_id, span_id) at request entry and propagating it across service boundaries (usually with traceparent headers). This gives you one complete request journey.
A trace is made up of spans. Each span records:
- Service + operation
- Start time + duration
- HTTP details (method, URL, status)
- DB query metadata
- Errors/exceptions
- Parent-child relationships
For deeper fundamentals: Distributed Tracing Basics to Beyond
Why OpenTelemetry + OpenObserve?
OpenTelemetry
OpenTelemetry is a CNCF standard for traces, metrics, and logs.
For Java, the OpenTelemetry Java Agent can auto-instrument Spring Boot, JDBC, and HTTP clients with no code changes.
OpenObserve
OpenObserve is an open-source backend for logs, metrics, and traces.
- OTLP-native ingest
- SQL-powered analytics
- Unified observability in one interface
- Lightweight and storage-efficient
Architecture used in this tutorial
We'll run four services:
| Service | Port | Responsibility |
|---|---|---|
discovery-service |
8761 | Eureka registry |
user-service |
8081 | User CRUD (MySQL) |
order-service |
8082 | Order management; calls user-service
|
payment-service |
8083 | Payment processing; calls order-service
|
The key trace path is:
payment-service -> order-service -> user-service -> MySQL
Prerequisites
- Java 17+
- Maven 3.8+
- Docker + Docker Compose
- MySQL 8 (or use Dockerized MySQL from compose)
Step 1: Clone the project
git clone https://github.com/openobserve/java-distributed-tracing.git
cd java-distributed-tracing
Step 2: Start OpenObserve and MySQL
docker-compose up -d
This starts:
- OpenObserve:
http://localhost:5080 - MySQL:
localhost:3306(tracingdb)
Login to OpenObserve with:
- Email:
admin@example.com - Password:
Admin123!
Step 3: Download OpenTelemetry Java Agent
mkdir agents
curl -L https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar \
-o agents/opentelemetry-javaagent.jar
Step 4: Configure agent export to OpenObserve
Example from user-service/scripts/start.sh:
export OTEL_SERVICE_NAME=user-service
export OTEL_RESOURCE_ATTRIBUTES=service.name=user-service,deployment.environment=dev
export OTEL_TRACES_EXPORTER=otlp
export OTEL_METRICS_EXPORTER=none
export OTEL_LOGS_EXPORTER=none
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=http://localhost:5080/api/default/traces
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Basic {token}"
java \
-Xms256m \
-Xmx512m \
-javaagent:../agents/opentelemetry-javaagent.jar \
-jar target/user-service-0.0.1-SNAPSHOT.jar
Get {token} from OpenObserve UI:
Step 5: Start discovery-service
cd discovery-service
mvn clean install -Dmaven.test.skip
sh scripts/start.sh
Open: http://localhost:8761
Step 6: Start user/order/payment services
Run each in a separate terminal.
cd user-service
mvn clean install -Dmaven.test.skip
sh scripts/start.sh
cd order-service
mvn clean install -Dmaven.test.skip
sh scripts/start.sh
cd payment-service
mvn clean install -Dmaven.test.skip
sh scripts/start.sh
Verify registration in Eureka:
Step 7: Generate traces
1) Create user
curl -X POST http://localhost:8081/api/users \
-H "Content-Type: application/json" \
-d '{
"name": "Priya Sharma",
"email": "priya@example.com",
"phone": "+91-9876543210"
}'
2) Create order
curl -X POST http://localhost:8082/api/orders \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"productName": "Mechanical Keyboard",
"quantity": 1,
"totalAmount": 4999.00
}'
3) Process payment (full distributed trace)
curl -X POST http://localhost:8083/api/payments/process \
-H "Content-Type: application/json" \
-d '{
"userId": 1,
"orderId": 1,
"amount": 4999.00,
"currency": "INR",
"paymentMethod": "UPI"
}'
4) Trigger an error trace
curl -X POST http://localhost:8082/api/orders \
-H "Content-Type: application/json" \
-d '{
"userId": 9999,
"productName": "Test Product",
"quantity": 1,
"totalAmount": 100.00
}'
Expected: 400 Bad Request
Visualize in OpenObserve
Go to http://localhost:5080 -> Traces
Trace Explorer
You'll see:
- Trace ID
- Root span
- Service
- Duration
- Span count
- Status
Filter examples
service_name = payment-servicestatus = ERROR- Duration range
-
operation_namefor specific endpoints
Flamegraph + Gantt chart
Click a POST /api/payments/process trace.
- Flamegraph: nested span timing hierarchy
- Gantt: timeline-aligned span bars
Query traces with SQL
OpenObserve supports SQL over trace data.
Slowest payment traces
SELECT trace_id, duration, service_name, operation_name
FROM "default"
WHERE service_name = 'payment-service'
AND operation_name LIKE '%payments/process%'
ORDER BY duration DESC
LIMIT 10;
Error count by service
SELECT service_name, COUNT(*) as error_count
FROM "default"
WHERE span_status = 'ERROR'
GROUP BY service_name
ORDER BY error_count DESC;
Avg/max latency by service
SELECT service_name,
AVG(duration) as avg_duration_us,
MAX(duration) as max_duration_us,
COUNT(*) as request_count
FROM "default"
GROUP BY service_name;
What the Java agent captured automatically
Without adding tracing code, the OpenTelemetry Java Agent instrumented:
- Spring Web incoming HTTP requests
-
RestTemplateoutbound calls (traceparentinjected) - JDBC/MySQL queries
- Context propagation across service boundaries
See supported libraries: OpenTelemetry Java Instrumentation
Final takeaway
You now have end-to-end distributed tracing for a Java microservices app with:
- Zero-code instrumentation
- Full request path visibility
- Visual root-cause analysis (flamegraph/Gantt)
- SQL-based troubleshooting in OpenObserve
- A path to production scaling without vendor lock-in




















Top comments (0)