The page is slow, APIs time out randomly, and you've already checked Java logs and slow SQL — still nothing. The gap is often the Nginx in front: it proxy-passes the request, but classic access logs won't tell you whether the hop succeeded, how long it took, or where it stalled.
OpenTelemetry turns that into "load a module, write a few lines of config." This post walks a Demo we actually ran: instrument Nginx and Java with OpenTelemetry, then open one Trace in DataBuff that spans Nginx → Java → Redis.
What OpenTelemetry is doing
OpenTelemetry stitches each service hop into one call chain. A full request is a Trace; each processing segment is a Span. Nginx uses the official module, Java uses the official agent — each records its hop, exports with the same TraceID, and the platform merges the fragments.
curl / browser
│ GET /hello
▼
[ Nginx nginx:1.27-alpine-otel ] ngx_otel_module → OTLP gRPC 4317
│ reverse proxy proxy_pass
▼
[ Java JDK HttpServer + Jedis ] opentelemetry-javaagent → OTLP HTTP 4318
│ set + get
▼
[ Redis 192.168.50.120:16379 ] Jedis auto-instrumented
▼
[ DataBuff ] ← gRPC 4317 + HTTP 4318, merged under one TraceID
Hop 1 · Nginx — two install paths
The official OTel module needs a recent Nginx (1.21+). Our host still runs 1.20.1, so path A uses the official image that already ships the module; path B installs the module package on an existing Nginx and adds one load_module line. OTel directives are the same either way.
Path A — official image:
docker run -d --name nginx-otel \
-p 8090:80 \
--add-host host.docker.internal:host-gateway \
-v /path/to/nginx.conf:/etc/nginx/nginx.conf:ro \
nginx:1.27-alpine-otel
Path B — Alpine example: apk add --repository https://nginx.org/packages/mainline/alpine/v3.21/main nginx-module-otel, then load_module /usr/lib/nginx/modules/ngx_otel_module.so; and nginx -s reload.
Config points that matter:
- Export over gRPC
host.docker.internal:4317(HTTP 4318 fails withOTel export failure ... Socket closed) otel_service_name nginx-otel-demootel_trace on
The line people miss: otel_trace_context propagate;. By default the module receives upstream context but does not inject it downstream. Without this line Nginx reports its own Span, but Java never sees the TraceID — you get two unrelated Traces. After adding propagate, they joined immediately.
otel_exporter {
endpoint host.docker.internal:4317; # gRPC
}
otel_service_name nginx-otel-demo;
otel_trace_context propagate; # required: inject traceparent
Hop 2 · Java agent, zero code changes
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=java-redis-demo \
-Dotel.exporter.otlp.endpoint=http://127.0.0.1:4318 \
-cp "libs/*:classes" OtelDemoServer
Java uses HTTP OTLP (4318), Nginx uses gRPC (4317) — two protocols, one DataBuff, one Trace later.
Hop 3 · Two exporters, one Trace
Both nginx-otel-demo and java-redis-demo show up with volume, error rate, and latency.
Topology draws nginx-otel-demo → java-redis-demo → Redis.
https://dev-to-uploads.s3.us-east-2.amazonaws.com/uploads/articles/epov100r6v9hxqllxlv9.png
Waterfall: /hello (nginx) → GET /hello (java) → AUTH/SET/GET (redis), total 975ms, TraceID 7dff87e865b7fe975877a506db26a2ab. The Demo sleeps about one second in Java on purpose — nginx Span (975ms) and Java Span (974.75ms) rise together, so Nginx duration really covers the whole request. Redis even records AUTH as its own Span when the connection uses a password.
Don't trust the UI alone — check Doris
SELECT service, name, span_id, parent_id, is_parent, duration
FROM trace_dc_span
WHERE trace_id='7dff87e865b7fe975877a506db26a2ab'
ORDER BY startTime;
nginx-otel-demo /hello (empty, root) 975000000
java-redis-demo GET /hello <nginx span> 974745285
[redis]…:16379 AUTH <java span> 638593
[redis]…:16379 SET <java span> 361212
[redis]…:16379 GET <java span> 164941
Filter the trace list by nginx / java when you're hunting edge issues.
Try it tonight — three steps
-
docker runnginx:1.27-alpine-otelwithotel_trace_context propagate;, exposehttp://<host>:8090/hello. - Start Java with
-javaagent:opentelemetry-javaagent.jarpointing athttp://127.0.0.1:4318. -
curl http://192.168.50.140:8090/helloa few times, wait ~30s for batch export, open DataBuff Traces, filternginx-otel-demo.
Self-check: nginx root Span present; Java under nginx (not a sibling Trace); Redis SET/GET under Java. If you see two Traces, you almost certainly forgot propagate.
Fifteen minutes on the Nginx module and the Java agent turns the first hop from a blind spot into a span you can scroll.
DataBuff
Open-source AI-native OpenTelemetry APM — metrics, traces, logs and AI troubleshooting in one platform.
GitHub: https://github.com/databufflabs/databuff
Live Demo: https://demo.databuff.ai




Top comments (0)