A step-by-step guide to shipping distributed traces to Jujing OBSERVE: deploying the Collector, configuring OTLP export, instrumenting your app, then verifying and avoiding common pitfalls — with Java and Python examples and full config.
Sending traces straight from your app to the backend is the least-effort and most fragile option. Backend URLs and auth credentials get hardcoded into every service, so a single change means redeploying everything. In-flight data is lost when the app restarts. And with no buffering, a traffic spike can crush the downstream. The OpenTelemetry Collector keeps all of this out of your business code: it runs as a separate process that receives, buffers, batches, and retries, while your app only sends data to a fixed local port. Backend changes never touch application code.
There is a bonus too. Cross-cutting policy — sampling rate, PII redaction, adding common resource attributes like environment or cluster name — becomes a Collector config change instead of a change to dozens of services. You can start with head-based sampling for cost control, then move to tail-based sampling later, without touching a single line of application code.
Start with a config file, otel-collector.yaml:
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
memory_limiter:
check_interval: 1s
limit_mib: 512
batch:
timeout: 5s
send_batch_size: 512
exporters:
otlphttp:
endpoint: https://ob.jjhub.cn/otel/v1/traces
headers:
Authorization: Bearer ${env:JJHUB_TOKEN}
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlphttp]
Run it with Docker:
docker run -d --name otel-collector \
-p 4317:4317 -p 4318:4318 \
-e JJHUB_TOKEN=your_token \
-v $PWD/otel-collector.yaml:/etc/otelcol/config.yaml \
otel/opentelemetry-collector-contrib:0.115.0
Two notes. First, export over otlphttp (HTTP) rather than gRPC — it passes through gateways and firewalls more easily. Second, inject the token via an environment variable instead of hardcoding it into a file that ends up in version control. For production, run the Collector as a Kubernetes DaemonSet or a systemd service rather than a plain Docker container, and point every app at its local Collector.
For Java (Spring Boot), the lowest-friction path is the Java agent, which requires no code changes:
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://localhost:4317 \
-Dotel.service.name=order-service \
-jar app.jar
For Python, instrument manually and add spans only around the functions that matter:
from opentelemetry import trace
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.resources import SERVICE_NAME, Resource
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.trace.export import BatchSpanProcessor
trace.set_tracer_provider(TracerProvider(
resource=Resource.create({SERVICE_NAME: 'order-service'})))
trace.get_tracer_provider().add_span_processor(
BatchSpanProcessor(OTLPSpanExporter(endpoint='localhost:4317', insecure=True)))
tracer = trace.get_tracer(__name__)
with tracer.start_as_current_span('checkout') as span:
span.set_attribute('order.id', order_id)
# business logic
Whatever the language, three defaults are worth setting everywhere: a stable service.name, propagation enabled so trace_id flows across HTTP calls, and at least a couple of business attributes on the spans that matter (order ID, user ID, tenant). These are what turn a trace from 'which function was slow' into 'whose request was slow'.
The same Collector can ship all three signals to Jujing OBSERVE, not just traces. Add a filelog receiver to tail your application logs and a prometheus receiver to scrape metrics, then route each through the batch processor to its own OTLP exporter:
receivers:
filelog:
include: [/var/log/app/*.log]
operators:
- type: json_parser
prometheus:
config:
scrape_configs:
- job_name: app
static_configs:
- targets: ['localhost:8080']
service:
pipelines:
logs:
receivers: [filelog]
processors: [memory_limiter, batch]
exporters: [otlphttp/logs]
metrics:
receivers: [prometheus]
processors: [memory_limiter, batch]
exporters: [otlphttp/metrics]
The otlphttp/logs and otlphttp/metrics exporters point at the same host with different paths, mirroring the traces exporter. The advantage is one agent per host handling everything, with a single place to manage the token, sampling, and redaction.
Keep one rule in mind: set the service.name resource consistently across all three signals. If traces say order-service but logs say host-03, the trace-to-log correlation will silently break.
Verify three things: your service.name shows up in the Jujing OBSERVE tracing page; opening a trace shows the full span hierarchy and timing; and searching logs by trace_id pulls up every line from that request. If spans appear but logs do not correlate, the usual cause is a missing trace_id in your log lines — many logging frameworks need an explicit MDC entry for it.
Common pitfalls. First, port 4317 is only reachable from the Collector host — containerized apps need the host IP or a service name. Second, do not set the batch processor timeout too high, or you will stare at an empty page while debugging. Third, remember that sampling drops traces; if a specific request is missing, check the sampler before blaming the exporter. Fourth, keep the Collector version aligned with your SDKs to avoid OTLP incompatibilities.
Before you call it done, wire the logging exporter into a pipeline locally for a few minutes and confirm spans are actually being received from your app. When spans show up in the Collector log but not in Jujing, the problem is almost always downstream — the endpoint, the token, or a firewall. When nothing reaches the Collector at all, the problem is the SDK config, usually the endpoint or an exporter typo. Splitting the path in half this way turns a vague 'it does not work' into a precise 'it breaks here'. Also set resource attributes for environment and cluster at the SDK or Collector level; they cost nothing and make filtering staging versus production trivial later.