A Go walkthrough of integrating OpenTelemetry with Ju Jing OBSERVE: prerequisites, SDK setup, automatic and manual instrumentation, OTLP export, plus sampling, clock-drift, and span-explosion gotchas.
Logs alone can't show you how a request flows through your services — for that you need traces. OpenTelemetry (OTel) is the de facto instrumentation standard, and wiring it into Ju Jing OBSERVE takes three steps: install the SDK, configure OTLP export, and confirm data lands in the console. Here's the full walkthrough with a Go service.
Before writing any code, confirm three things:
service.name convention, like {domain}-{service}, so services are easy to find in trace search later.Start with the OTel SDK and the gRPC exporter in Go:
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc"
"go.opentelemetry.io/otel/sdk/trace"
"go.opentelemetry.io/otel/sdk/resource"
semconv "go.opentelemetry.io/otel/semconv/v1.17.0"
)
func initTracer(ctx context.Context) (*trace.TracerProvider, error) {
exporter, err := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("ob.jjhub.cn:4317"),
otlptracegrpc.WithInsecure(),
)
if err != nil {
return nil, err
}
res := resource.NewWithAttributes(semconv.SchemaURL,
semconv.ServiceName("order-service"),
)
tp := trace.NewTracerProvider(
trace.WithBatcher(exporter),
trace.WithResource(res),
)
otel.SetTracerProvider(tp)
return tp, nil
}
Two things matter here. ServiceName is the dimension Ju Jing uses to separate services, so set it accurately per service. And WithBatcher batches spans before export, cutting network overhead roughly in half compared to sending each span individually.
For HTTP services, prefer automatic instrumentation — it's a one-line route wrapper:
import "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
mux.Handle("/api/order", otelhttp.NewHandler(orderHandler, "place-order"))
Auto-instrumentation captures the "request received → response returned" span, but time spent inside the business logic — DB queries, downstream calls, computation — needs manual spans:
ctx, span := otel.Tracer("order-service").Start(ctx, "query-inventory")
defer span.End()
rows, err := db.QueryContext(ctx, "SELECT stock FROM inventory WHERE sku = ?", sku)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
}
Always call span.RecordError and set codes.Error on the error branch. Otherwise error spans won't be marked red in the trace view, and you'll silently miss real failures.
Adding business attributes to spans is what makes traces searchable — don't just give a span a name. Put order IDs, SKUs, and regions in via span.SetAttributes so you can filter by business dimension later. But avoid high-cardinality attributes: they're persisted with the span and will slow queries if every span carries a unique value.
In the Ju Jing console, open "Integrations → OpenTelemetry" and generate an access token. Pass it as a header to distinguish tenants:
exporter, _ := otlptracegrpc.New(ctx,
otlptracegrpc.WithEndpoint("ob.jjhub.cn:4317"),
otlptracegrpc.WithHeaders(map[string]string{
"Authorization": "Bearer " + token,
}),
)
Once the service is up, send a few requests, then go to "Traces → Search" and query by service name or trace ID. You should see the full parent-child waterfall, the time share of each span, and the associated logs and alerts in the side panel. If nothing shows up after five minutes, check the exporter's error logs first — it's usually a refused connection or an expired token.
trace.WithSampler(trace.ParentBased(trace.TraceIDRatioBased(0.1))) for 10%, then raise it once volume stabilizes.span.SetAttributes(attribute.String("user_id", uid)) onto every span indiscriminately. User IDs belong on key transaction spans only, or they'll bloat the index and slow trace search.Traces answer "where is this call slow"; metrics answer "is anything wrong overall." Use the same OTel SDK to export metrics too: initialize a MeterProvider in Go with go.opentelemetry.io/otel/sdk/metric, and add the runtime collector to report GC, goroutine count, and memory. Your service then has a performance baseline from day one, and when a trace shows a slow query you can check the metric dashboard to tell whether it's a one-off or a systemic degradation.
Finally, print the trace ID into your logs (log.Info("...", "trace_id", span.SpanContext().TraceID())) so logs and traces link together. From any log line you can then jump straight to the corresponding trace. Once logs, metrics, and traces are all connected, you're actually using observability.