Code-based Instrumentation of Java using OpenTelemetry
5 minute read
Overview
Code-based instrumentation in Java with OpenTelemetry involves manually writing code to generate telemetry data using the OpenTelemetry API - a set of libraries provided by OpenTelemetry and added to your project. The API provides functions and classes you can use to manually instrument your code to capture telemetry data. You write additional code using the OpenTelemetry API to specify what parts of your program should be monitored. This typically involves creating “spans” to track the execution timing of certain blocks of code or using “meters” to capture metrics like the number of requests processed.
Prerequisites
Ensure that you have:
- Java 8 or later
- OpenTelemetry API and SDK dependencies added to your project
- An Edge Delta account and a fleet with an OTLP input node.
Setup OpenTelemetry
1. Import Required Classes
First, import the necessary OpenTelemetry classes:
import io.opentelemetry.api.trace.*;
import io.opentelemetry.api.metrics.*;
import io.opentelemetry.api.logs.*;
import io.opentelemetry.api.OpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
api.trace.*
for creating and managing spans (tracing).api.metrics.*
for defining and recording metrics.api.logs.*
for logging support.api.OpenTelemetry
the main interface for acquiring OpenTelemetry instances.sdk.OpenTelemetrySdk
provides the SDK implementation, required for exporting telemetry data.
2. Set Up Resource Attributes
Define resource attributes, such as service name, to associate telemetry data with your application:
Resource resource = Resource.create(Attributes.of(ResourceAttributes.SERVICE_NAME, "my-service"));
3. Configure Traces
Create a TracerProvider to enable distributed tracing:
SdkTracerProvider tracerProvider = SdkTracerProvider.builder()
.setResource(resource)
.addSpanProcessor(BatchSpanProcessor.builder(OtlpHttpSpanExporter.builder()
.setEndpoint("http://localhost:4318/v1/traces")
.build()).build())
.build();
The SdkTracerProvider
is a critical SDK component responsible for creating and managing traces. You can customize its behavior by configuring the associated resources, samplers, span processors, and span exporters.
The endpoint configuration depends on your environment. For example, in a Kubernetes Environment, you need a service open, for example ed-data-supply-svc
, for the port that the exporters will send logs, metrics, and traces to. For non-kubernetes environments with exporters running in the same environment as the Edge Delta agent, such as on a Linux VM, the exporters will be able to communicate directly with the Edge Delta agent on localhost
. For HTTP (4318
), include the path /v1/traces
, /v1/metrics
, or /v1/logs
. For gRPC (4317
), do not include the path. See Ingest Data from an OTLP Source.
4. Configure Metrics
Set up a MeterProvider to track application metrics:
SdkMeterProvider meterProvider = SdkMeterProvider.builder()
.setResource(Resource.getDefault().toBuilder()
.put(ResourceAttributes.SERVICE_NAME, "my-service").build())
.registerMetricReader(PeriodicMetricReader.builder(
OtlpGrpcMetricExporter.builder()
.setEndpoint("http://localhost:4318/v1/metrics")
.build()).build())
.build();
As with the Traces configuration, the endpoint configuration depends on your environment.
5. Configure Logs
OpenTelemetry supports logging via OpenTelemetry Logging SDK (since v1.22+).
SdkLoggerProvider loggerProvider = SdkLoggerProvider.builder()
.setResource(Resource.getDefault().toBuilder()
.put(ResourceAttributes.SERVICE_NAME, "my-service").build())
.addLogRecordProcessor(BatchLogRecordProcessor.builder(
OtlpGrpcLogExporter.builder()
.setEndpoint("http://localhost:4318/v1/logs")
.build()).build())
.build();
As with the Traces configuration, the endpoint configuration depends on your environment.
6. Debugging and Console Export (Optional):
To view traces, metrics, and logs locally, configure console exporters:
ConsoleSpanExporter spanExporter = ConsoleSpanExporter.create();
ConsoleMetricExporter metricExporter = ConsoleMetricExporter.create();
Instrumentation
1. Acquire a Tracer and Start a Span
Obtain a Tracer and use it to create spans for monitoring execution timing:
Tracer tracer = GlobalOpenTelemetry.getTracer("example.Tracer");
Span span = tracer.spanBuilder("process_request").startSpan();
try (Scope scope = span.makeCurrent()) {
// Business logic being traced
} finally {
span.end();
}
Add attributes and events for richer trace data:
span.setAttribute("http.method", "GET");
span.addEvent("User authenticated", Attributes.of(AttributeKey.stringKey("user.id"), "123"));
2. Measure Metrics
Use Meter to track application metrics:
Meter meter = GlobalOpenTelemetry.getMeter("example.Meter");
LongCounter requestCounter = meter.counterBuilder("http_requests").setDescription("Counts HTTP requests").build();
requestCounter.add(1);
Utilize different types of instruments such as Counter, Histogram, etc., to capture various types of metric data. To track fluctuating values, use gauges:
ObservableDoubleGauge gauge = meter.gaugeBuilder("cpu_usage")
.setDescription("CPU Usage")
.setUnit("%")
.buildWithCallback(observableMeasurement -> {
observableMeasurement.record(getCpuUsage());
});
3. Capture Logs
Capture logs with trace context for better observability:
Logger logger = GlobalOpenTelemetry.getLogger("example.Logger");
Span currentSpan = Span.current();
logger.logRecordBuilder()
.setBody("User login event")
.setSeverity(Severity.INFO)
.setAttribute("trace_id", currentSpan.getSpanContext().getTraceId())
.emit();
Testing and Validation
- Verify Logs: Use Edge Delta to check logs for trace and span IDs.
- Check Metrics: Use Edge Delta.
- Trace Requests: Use Edge Delta.
Advanced
Context Propagation
Ensure trace continuity across services by using ContextPropagators. In distributed systems, context propagation maintains trace continuity as requests traverse different services and components. OpenTelemetry provides APIs to manage and propagate context across service boundaries. The Context in OpenTelemetry serves as an immutable bundle of key-value pairs that travels along the execution path within and across application boundaries. It implicitly maintains and provides access to the current active Span, among other contextual data. The ContextPropagators API is central to context propagation. It enables the injection of context into outgoing requests and extraction from incoming requests, allowing trace contexts and baggage to move across process boundaries. OpenTelemetry auto-instrumentation often handles propagation automatically, but manual configuration may be necessary for custom instrumentation.
To configure context propagation, create a ContextPropagators
instance using various TextMapPropagators
that define the serialization of context over wire protocols like HTTP headers.
ContextPropagators propagators = ContextPropagators.create(
TextMapPropagator.composite(W3CTraceContextPropagator.getInstance(), W3CBaggagePropagator.getInstance())
);
This setup, including W3CTraceContextPropagator
and W3CBaggagePropagator
, ensures trace context and baggage are serialized and deserialized according to W3C specs.
Inject and extract context in HTTP requests:
propagators.getTextMapPropagator().inject(Context.current(), request, (carrier, key, value) -> request.setHeader(key, value));
Context extractedContext = propagators.getTextMapPropagator().extract(Context.current(), exchange, TEXT_MAP_GETTER);
Advanced Customizations
In advanced scenarios, the default telemetry processing provided by OpenTelemetry might not fully meet all application requirements. You can extend the SDK functionality by implementing custom components, such as custom Samplers or SpanProcessors, to fine-tune telemetry data capture and processing.
Custom Sampler:
A Sampler in OpenTelemetry determines which traces are sampled and recorded. By implementing a custom Sampler, you can define complex logic to control trace sampling based on specific criteria.
public class CustomSampler implements Sampler {
@Override
public SamplingResult shouldSample(Context parentContext, String traceId, String name, SpanKind spanKind, Attributes attributes, List<LinkData> parentLinks) {
return SpanKind.SERVER == spanKind ? SamplingResult.recordAndSample() : SamplingResult.drop();
}
}
Custom Span Processor: SpanProcessors process the lifecycle of spans from when they start until they end. Creating a custom SpanProcessor allows additional span enrichment or processing at the end of a span’s lifecycle.
public class CustomSpanProcessor implements SpanProcessor {
@Override
public void onStart(Context parentContext, ReadWriteSpan span) {
span.setAttribute("custom.attribute", "custom_value");
}
}
Custom Metric Exporter: This example demonstrates how to create a custom MetricExporter in OpenTelemetry Java that processes and logs exported metric data:
public class CustomMetricExporter implements MetricExporter {
@Override
public CompletableResultCode export(Collection<MetricData> metrics) {
for (MetricData metric : metrics) {
System.out.println("Exporting metric: " + metric.getName() + " Value: " + metric.getData());
}
return CompletableResultCode.ofSuccess();
}
}
Fine Tuning Performance
Use batch processing instead of immediate export.
Adjust sampling rates to control data volume.
Optimize context propagation for efficiency in distributed systems.
Semantic Conventions
Ensure trace and metric standardization:
Attributes attributes = Attributes.builder()
.put(HttpAttributes.HTTP_REQUEST_METHOD, "GET")
.put(ServerAttributes.SERVER_ADDRESS, "example.com")
.build();