OpenTelemetry Observability in Practice: Unified Traces, Metrics, Logs Collection
The Three Pillars of Observability: Traces, Metrics, Logs
In microservice and cloud-native architectures, systems consist of dozens or even hundreds of services. Traditional log-based debugging is no longer sufficient. Observability has become essential for understanding system behavior.
Observability ≠ Monitoring. Monitoring detects known problems; observability explores unknown ones.
| Pillar | Purpose | Typical Scenario | Example |
|---|---|---|---|
| Traces | Full request lifecycle | Cross-service call chain | User order → inventory → payment → notification |
| Metrics | Quantified system indicators | Performance monitoring, capacity planning | QPS=1200, P99 latency=230ms |
| Logs | Discrete event records | Error investigation, auditing | NullPointerException at OrderService:142 |
OpenTelemetry is a CNCF incubating project providing vendor-neutral APIs, SDKs, and tools to uniformly collect all three signals.
OpenTelemetry Architecture: SDK, Collector, Exporter
OpenTelemetry uses a three-layer architecture, decoupling collection from export:
┌─────────────────────────────────────────────────┐
│ Application │
│ ┌──────┐ ┌──────┐ ┌──────┐ │
│ │ SDK │ │ SDK │ │ SDK │ ← Auto/Manual │
│ │(Java)│ │(Go) │ │(Node)│ │
│ └──┬───┘ └──┬───┘ └──┬───┘ │
│ └─────────┼─────────┘ │
│ │ OTLP(gRPC/HTTP) │
│ ┌────────────▼────────────┐ │
│ │ OTel Collector │ ← Data Hub │
│ │ Recv → Proc → Export │ │
│ └──────┬───────┬─────────┘ │
│ ┌────▼──┐ ┌──▼─────┐ │
│ │Jaeger │ │Prometheus│ ← Backends │
│ └───────┘ └─────────┘ │
└─────────────────────────────────────────────────┘
| Layer | Component | Responsibility | Deployment |
|---|---|---|---|
| SDK | OpenTelemetry SDK | Auto/manual instrumentation | In-process |
| Collector | OTel Collector | Receive, process, route telemetry | Standalone |
| Exporter | OTLP Exporter | Export data to backends | Inside Collector |
Java Agent Auto-Instrumentation: Zero Code Intrusion
The OpenTelemetry Java Agent's greatest appeal is zero code intrusion — just add a JVM argument to automatically trace Spring Boot, HTTP clients, JDBC, Redis, and more.
Launch with Java Agent
java -javaagent:opentelemetry-javaagent.jar \
-Dotel.service.name=order-service \
-Dotel.traces.exporter=otlp \
-Dotel.metrics.exporter=otlp \
-Dotel.logs.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://collector:4317 \
-jar my-app.jar
Key Configuration
| Config | Default | Description |
|---|---|---|
otel.service.name |
unknown | Service name in trace |
otel.traces.exporter |
otlp | Trace exporter: otlp/zipkin/none |
otel.traces.sampler |
parentBased_alwaysOn | Sampling strategy |
otel.exporter.otlp.endpoint |
http://localhost:4317 | Collector address |
Auto-Instrumentation Coverage
| Component | Auto-Trace | Auto-Metrics |
|---|---|---|
| Spring Web MVC/WebFlux | ✅ | ✅ |
| RestTemplate/WebClient | ✅ | ✅ |
| JDBC / R2DBC | ✅ | ✅ |
| Redis (Lettuce/Jedis) | ✅ | ✅ |
| Kafka | ✅ | ✅ |
| gRPC | ✅ | ✅ |
Distributed Tracing: Cross-Service Call Chain Visualization
Example Call Chain
[OrderService] POST /api/orders
├── [InventoryService] POST /api/inventory/deduct
│ └── [Redis] GET inventory:product-123
├── [PaymentService] POST /api/payments/charge
│ ├── [DB] INSERT INTO payments
│ └── [Kafka] PRODUCE payment-success
└── [NotificationService] CONSUME payment-success
W3C Trace Context Propagation
GET /api/inventory/deduct HTTP/1.1
traceparent: 00-4bf92f3577b34da6a3ce929d0e0e4736-00f067bf0bc902b7-01
| Field | Format | Description |
|---|---|---|
| version | 00 |
W3C version |
| trace-id | 32-hex | Globally unique trace identifier |
| parent-id | 16-hex | Parent span identifier |
| trace-flags | 2-hex | 01=sampled, 00=not sampled |
Custom Spans and Attributes
Auto-instrumentation covers technical aspects; business semantics require manual custom spans:
@Service
public class OrderService {
private final Tracer tracer;
public OrderService(Tracer tracer) {
this.tracer = tracer;
}
public OrderResult createOrder(CreateOrderRequest request) {
Span span = tracer.spanBuilder("order.create")
.setAttribute("order.userId", request.getUserId())
.setAttribute("order.productCount", request.getItems().size())
.setAttribute("order.totalAmount", request.getTotalAmount())
.startSpan();
try (Scope scope = span.makeCurrent()) {
OrderResult result = doCreateOrder(request);
span.setAttribute("order.id", result.getOrderId());
span.setAttribute("order.status", result.getStatus().name());
return result;
} catch (Exception e) {
span.recordException(e);
span.setStatus(StatusCode.ERROR, e.getMessage());
throw e;
} finally {
span.end();
}
}
}
Attribute Naming Conventions
| Prefix | Semantics | Example |
|---|---|---|
order. |
Order business | order.id, order.status |
user. |
User business | user.id, user.tier |
db. |
Database | db.operation, db.table |
http. |
HTTP | http.method, http.status_code |
Metrics Collection: Counter, Histogram, Gauge
| Type | Semantics | Monotonic | Aggregation | Use Case |
|---|---|---|---|---|
| Counter | Ever-increasing count | Yes | Sum | Request count, error count |
| Histogram | Value distribution | - | Histogram | Request latency, response size |
| Gauge | Current instantaneous value | No | Last Value | Active connections, queue depth |
@Service
public class PaymentService {
private final LongCounter requestCounter;
private final LongHistogram latencyHistogram;
public PaymentService(MeterProvider meterProvider) {
Meter meter = meterProvider.meterBuilder("payment-service").build();
this.requestCounter = meter.counterBuilder("payment.requests.total")
.setDescription("Total payment requests")
.build();
this.latencyHistogram = meter.histogramBuilder("payment.request.duration")
.setUnit("ms")
.ofLongs()
.build();
}
public PaymentResult processPayment(PaymentRequest request) {
long startTime = System.currentTimeMillis();
requestCounter.add(1, Attributes.builder()
.put("payment.method", request.getMethod().name())
.build());
try {
PaymentResult result = doProcessPayment(request);
latencyHistogram.record(System.currentTimeMillis() - startTime);
return result;
} catch (PaymentException e) {
latencyHistogram.record(System.currentTimeMillis() - startTime);
throw e;
}
}
}
Collector Deployment Modes
| Mode | Architecture | Pros | Cons | Use Case |
|---|---|---|---|---|
| Sidecar | One per Pod | Isolation, flexible config | High resource cost | Large K8s clusters |
| DaemonSet | One per Node | Good resource utilization | Single point risk | General recommended |
| Gateway | Standalone cluster | Centralized, scalable | Extra network hops | Multi-cluster |
Collector Config
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
send_batch_size: 1024
timeout: 5s
memory_limiter:
check_interval: 1s
limit_mib: 512
exporters:
otlp/jaeger:
endpoint: jaeger:4317
prometheusremotewrite:
endpoint: http://prometheus:9090/api/v1/write
service:
pipelines:
traces:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [otlp/jaeger]
metrics:
receivers: [otlp]
processors: [memory_limiter, batch]
exporters: [prometheusremotewrite]
Sampling Strategies: Head-Based vs Tail-Based
Head-Based Sampling
otel.traces.sampler=parentbased_traceidratio
otel.traces.sampler.arg=0.1
Simple and efficient, but may miss error requests.
Tail-Based Sampling
processors:
tail_sampling:
decision_wait: 10s
num_traces: 100000
sampling_policies:
- name: errors-policy
type: status_code
status_code:
status_codes: [ERROR]
- name: slow-policy
type: latency
latency:
threshold_ms: 1000
- name: fallback-policy
type: probabilistic
probabilistic:
sampling_percentage: 10
Recommendation: Use tail-based sampling in production to ensure 100% visibility of errors and slow requests.
Frontend RUM Integration
import { WebTracerProvider } from '@opentelemetry/sdk-trace-web';
import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http';
import { SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base';
const exporter = new OTLPTraceExporter({
url: 'https://collector.example.com/v1/traces',
});
const provider = new WebTracerProvider();
provider.addSpanProcessor(new SimpleSpanProcessor(exporter));
provider.register();
// Auto-instrument fetch/XHR requests
registerInstrumentations({
instrumentations: [
new FetchInstrumentation({
propagateTraceHeaderCorsUrls: [/api\.example\.com/],
}),
],
});
Result: Frontend HTTP requests carry
traceparentheaders, enabling end-to-end tracing from browser to backend.
Prometheus + Grafana Classic Stack
OpenTelemetry SDK → OTel Collector → Prometheus → Grafana
→ Jaeger/Tempo → Grafana
→ Loki → Grafana
Key Grafana Dashboard Metrics
| Panel | Metric | PromQL | Alert Threshold |
|---|---|---|---|
| Request Rate | QPS | rate(http_server_request_total[5m]) |
>10000 |
| P99 Latency | Latency | histogram_quantile(0.99, rate(http_server_duration_bucket[5m])) |
>500ms |
| Error Rate | Errors | rate(http_server_request_total{status=~"5.."}[5m]) / rate(http_server_request_total[5m]) |
>1% |
RED Principle: Rate, Errors, Duration — the three golden signals for service monitoring.
Summary
OpenTelemetry's core value in 2026:
- Unified Collection: One SDK for Traces, Metrics, Logs
- Vendor Neutral: Not locked into any backend
- Zero Intrusion: Java Agent auto-instrumentation
- End-to-End: From frontend RUM to backend microservices
- Mature Ecosystem: CNCF project, SDKs for all major languages
Observability is not a luxury — it's a necessity for microservice architectures. Without it, microservices are black boxes; with OpenTelemetry, system behavior is fully visible.
Try these browser-local tools — no sign-up required →