A step-by-step OpenTelemetry onboarding for a Java service: dependencies, SDK init, trace and metric collection, OBSERVE backend config, and verification — with copy-paste snippets you can follow from scratch.
For many teams, the first hurdle to observability is "getting started is too much work." In practice, going from zero to seeing your first trace with OpenTelemetry (OTel) takes only a few minutes. This article walks through the full flow with a Java service; the snippets are copy-paste ready.
Add the OTel SDK and exporter to pom.xml, then write an initialization class. The essentials are sampling rate, resource attributes (service.name), and the export endpoint:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk</artifactId>
<version>1.38.0</version>
</dependency>
Resource resource = Resource.getDefault()
.merge(Resource.create(Attributes.of(
ResourceAttributes.SERVICE_NAME, "order-svc")));
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(BatchSpanProcessor.builder(
OtlpHttpSpanExporter.builder()
.setEndpoint("https://ob.jjhub.cn/v1/traces")
.build()).build())
.setSampler(Sampler.parentBased(Sampler.traceIdRatioBased(0.1)))
.build();
service.name is the key field for later search and alert grouping. Make it unique and stable — don't use a version-stamped name.
Framework auto-instrumentation covers HTTP requests, but business semantics need manual spans. Here's order payment as an example:
Span span = tracer.spanBuilder("payment.execute")
.setAttribute("order.id", orderId)
.setAttribute("payment.channel", channel)
.startSpan();
try (Scope scope = span.makeCurrent()) {
// business logic
span.setStatus(StatusCode.OK);
} catch (Exception e) {
span.setStatus(StatusCode.ERROR, "payment failed");
span.recordException(e);
throw e;
} finally {
span.end();
}
Keep attribute names in a lowercase dotted convention (payment.channel); it pays off later when you aggregate metrics or filter traces.
Metrics use the Meter API and don't depend on traces. This counter tallies successful payments:
LongCounter successCounter = meter
.counterBuilder("payment.success.total")
.setDescription("total successful payments")
.setUnit("1")
.build();
successCounter.add(1, Attributes.of(
AttributeKey.stringKey("channel"), channel));
Counters fit cumulative values; for instantaneous distributions like latency or queue depth, use a Histogram instead.
Create a data source in the OBSERVE console and fill in the OTLP endpoint and token. After the service starts, hit a few endpoints, then open the tracing page and filter by service.name = order-svc — you should see the full span waterfall. If not, check three things in order: endpoint reachability, sampling rate too low, and a wrong service.name.
Sampling is the lever that keeps trace volume and cost under control, but the wrong strategy hides exactly the traces you need. For most teams, head-based probabilistic sampling plus a rule that always keeps requests with an error status is the right balance: sample 10% of everything, then force-keep any span whose status is 5xx or whose latency exceeds its service's P99.
Set the ratio per service rather than globally. A high-traffic gateway can afford 1%, while a low-traffic payment service should keep 100%. Whichever you choose, make the sampling decision at the very first hop so the whole trace follows one consistent choice — mixing ratios mid-path produces broken, one-span traces that are worse than no trace at all. Tail-based sampling, which decides after the trace completes, keeps every interesting trace but needs a centralized sampler and adds export latency; reach for it only after you've outgrown head-based rules.
If spans still don't appear, check the exporter isn't dropping batches. The BatchSpanProcessor buffers spans and flushes on a schedule; under load, raise maxQueueSize and maxExportBatchSize so bursts aren't dropped, and confirm the endpoint returns HTTP 200 instead of silently swallowing errors.
Decide naming rules before you instrument, not after. Span names should be stable, low-cardinality verbs — payment.execute, payment.refund — while the variable parts (order id, channel, amount) go into attributes, never into the span name; otherwise you explode metric cardinality and make aggregation useless. Keep attribute keys lowercase and dotted, and maintain a short team list of the attributes every service must set (service.name, env, region, txn.id). Consistency here is what turns thousands of individual traces into a queryable map of your system.
One tip: start with a high sampling rate (e.g. 0.5) to verify the trace is complete, then lower it to what production can afford. Don't set 0.01 from day one and then wonder why onboarding failed.