A minimal-change walkthrough of auto-instrumenting a Java service with the OpenTelemetry SDK and configuring the Collector to push traces and logs into JJHub OBSERVE, with key config and common pitfalls.
Make sure you have three things: a server with outbound network access, a JJHub OBSERVE account (grab the ingest endpoint and your token), and a running Java service. This post uses Spring Boot as the example; other languages follow the same steps, just swap the SDK and startup flags.
Why a local Collector instead of sending straight from the agent? Two reasons. First, the Collector gives you a single place to buffer, retry, and batch data, so a network blip doesn't lose spans. Second, it's where you add processors — sampling, redaction, environment tags — without touching every service.
OpenTelemetry ships a Java Agent that injects instrumentation at startup via a -javaagent flag, covering HTTP, databases, and message queues out of the box — no business code changes needed. This is the fastest way to get value: drop a jar, add a flag, restart. Download it:
curl -LO https://github.com/open-telemetry/opentelemetry-java-instrumentation/releases/latest/download/opentelemetry-javaagent.jar
Then start your app with flags that send data to a local Collector:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=none \
-Dotel.logs.exporter=none \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-jar app.jar
Set service.name to something stable and meaningful. Every search and alert depends on it later, so don't leave the default unknown_service. Pick a name that survives refactors and renames — order-service, not order-service-v2-fix. If you run multiple instances, the agent adds the instance ID automatically; you don't need to encode it in the name.
The agent only collects; the Collector is what lands data in the platform. Install a Collector (the contrib distribution has the most receivers) and write this config:
receivers:
otlp:
protocols: { grpc: { endpoint: 0.0.0.0:4317 } }
processors:
batch:
resource:
attributes:
- key: deployment.environment
value: production
action: upsert
exporters:
otlphttp:
endpoint: https://ob.jjhub.cn/otel
headers:
authorization: Bearer <your-ingest-token>
service:
pipelines:
traces:
receivers: [otlp]
processors: [resource, batch]
exporters: [otlphttp]
The resource processor stamps a consistent environment label on everything, so each service doesn't reinvent its own. This matters more than it looks: once deployment.environment is uniform, you can filter prod vs staging everywhere. The batch processor groups spans to cut network overhead; without it, every span is its own HTTP request.
The authorization header carries your ingest token. Treat it like a secret: put it in an environment variable or a mounted secret, not plaintext committed to git.
Start the service and the Collector, fire a request, then open the Traces page in the platform and filter by service.name. You should see traces within tens of seconds; click one to inspect the span waterfall and latency breakdown. If nothing appears, check three places in order: whether the agent actually loaded (check startup logs for the OpenTelemetry Javaagent config summary), whether the Collector logs report exporter errors, and whether your token is correct.
A useful smoke test is to hit an endpoint that touches a database. If the trace shows an HTTP span but no DB span, the agent loaded but the database instrumentation didn't attach — usually a version mismatch between the agent and your driver.
Once the smoke test passes, you have the basics in place: every request is traced end to end, logs carry the trace ID, and you can jump from a slow span straight to the logs it produced. From here the natural next steps are enabling metrics export, turning on tail-based sampling for production traffic, and tagging business attributes like customer tier or region on the resource. Each is a small, independent change on top of the pipeline you just built.