A from-scratch OpenTelemetry onboarding guide: the three nodes of the pipeline, Go and Java SDK setup with config examples, a collector snippet, verification steps, and a troubleshooting checklist.
This walkthrough covers the full path from zero: install the collector, enable the SDK in your service, and verify that traces and logs line up. It's aimed at backend engineers wiring up OpenTelemetry for the first time, and every step below is something you can run today.
An OTel setup that feeds OBSERVE has three moving parts: the SDK inside your service (emitting spans and logs), an OTel Collector (aggregation and sampling), and OBSERVE's OTLP endpoint. For a small deployment you can skip the collector and point the SDK straight at the OTLP endpoint; in production keep the collector so sampling, redaction, and egress all happen in one place, and you can change the sampling rate without touching every service. By the end of this guide you'll have a service emitting spans, a collector forwarding them, and trace-linked logs you can search — and metrics arrive for free, since the SDK exports them over the same OTLP channel.
Import the SDK and initialize a tracer provider in main:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
sdktrace "go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)
func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
exp, _ := otlptracehttp.New(ctx,
otlptracehttp.WithEndpoint("collector:4318"),
otlptracehttp.WithInsecure(),
)
tp := sdktrace.NewTracerProvider(
sdktrace.WithBatcher(exp),
sdktrace.WithResource(resource.NewWithAttributes(
semconv.ServiceName("order-svc"),
)),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Three things matter: keep ServiceName consistent everywhere (it becomes the service dimension in OBSERVE); export with a Batcher rather than per-request; and route through a collector in production so you can tune sampling and redaction centrally.
Import the BOM to pin versions in pom.xml:
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.40.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
The javaagent path is the least work: add -javaagent:opentelemetry-javaagent.jar to startup flags, then set the endpoint via environment variables:
OTEL_EXPORTER_OTLP_ENDPOINT=http://collector:4318
OTEL_SERVICE_NAME=order-svc
OTEL_METRICS_EXPORTER=otlp
Auto-instrumentation covers HTTP, JDBC, Redis, and other common calls without hand-written tracing code. Add business-level spans with @WithSpan only where you need them, and avoid blanket instrumentation that explodes span counts.
The collector is where auth, sampling, and redaction live in one place. A minimal OTLP receiver-to-exporter config looks like this:
receivers:
otlp:
protocols:
http:
endpoint: 0.0.0.0:4318
exporters:
otlphttp:
endpoint: https://ob.jjhub.cn/otlp
headers:
Authorization: Bearer <token>
service:
pipelines:
traces:
receivers: [otlp]
exporters: [otlphttp]
Point the SDKs at collector:4318 and let the collector hold the token and any sampling policy. When you need to redact a field or raise the trace sampling rate, you change one file instead of redeploying every service. The Authorization header carries the OBSERVE token, and for anything beyond a local test you should terminate TLS at the collector rather than shipping spans in the clear.
For a log line to appear under a trace, it must carry the trace_id. Use the OTel log bridge to write trace context into a structured field named trace_id, and OBSERVE will join logs to spans on that field. A bare-text log line with no trace_id won't show up from the trace detail page, no matter how well tracing itself is configured. This one convention — always emitting trace_id in logs — is what makes the whole "jump from trace to log" workflow possible.
When something doesn't line up, work through the list in order. First check endpoint and port — the OTLP HTTP port is 4318, and the gRPC port is 4317, so make sure your firewall egress allows whichever one you're using. Second, confirm ServiceName matches everywhere; a typo between services is the most common silent failure. Third, check whether sampling dropped the request: tail sampling always keeps errored spans but can drop healthy ones, so during debugging temporarily set the sampling rate to 100%. Finally, if spans show up but look broken, check clock sync across hosts — skewed timestamps make a trace read as garbage even when it's complete.