OpenTelemetry Java SDK¶
Author: Liu Rui
By introducing the OpenTelemetry SDK, you can instrument core business logic, such as creating a span for critical business operations to measure, trace, and analyze their actual behavior, or setting business attribute metrics. This approach has a certain degree of invasiveness.
Startup Command¶
java -javaagent:../opentelemetry-javaagent/opentelemetry-javaagent.jar \
-Dotel.traces.exporter=otlp \
-Dotel.exporter.otlp.endpoint=http://192.168.91.11:4317 \
-Dotel.resource.attributes=service.name=demo,version=dev \
-Dotel.metrics.exporter=otlp \
-jar springboot-opentelemetry-otlp-server.jar --client=true
Important Note
Depending on the exporter specified in the startup parameters, the SDK approach also requires importing the corresponding exporter dependencies; otherwise, startup will fail. For example, if the startup parameters use both otlp and logging exporters, you need to add both corresponding exporter dependencies in the pom.
If you are not using the SDK-related dependencies, no corresponding adjustments are needed.
Add Dependencies¶
<dependencies>
...
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-exporter-otlp</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-extension-annotations</artifactId>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-semconv</artifactId>
<version>1.21.0-alpha</version>
</dependency>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-sdk-extension-autoconfigure</artifactId>
<version>1.21.0-alpha</version>
</dependency>
...
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.opentelemetry</groupId>
<artifactId>opentelemetry-bom</artifactId>
<version>1.21.0</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Create SDK¶
Not recommended
@Bean
public OpenTelemetry openTelemetry() {
return AutoConfiguredOpenTelemetrySdk.builder()
.setResultAsGlobal(false)
.build()
.getOpenTelemetrySdk();
}
The above approach causes AutoConfiguredOpenTelemetrySdk to reload. The SDK provides a global object GlobalOpenTelemetry to obtain the OpenTelemetry object.
The following usage primarily employs GlobalOpenTelemetry.get() to obtain the OpenTelemetry object.
Trace¶
Create a Tracer¶
Tracer is mainly used to obtain and create span objects. Note: Tracer is not typically responsible for configuration; that is the responsibility of TracerProvider. The OpenTelemetry interface provides a default TracerProvider implementation.
Use openTelemetry() to get the Tracer object:
Create a Span¶
Info
No other API is allowed to create a Span except the Tracer.
Get the Current Span Object¶
Get the current span object to set attributes, events, etc., on the current span.
Create an Attribute¶
An attribute is a property of a span, serving as a tag for the current span.
Create a Link Span¶
A span can be linked to one or more causally related spans. Links can be used to represent batch operations, where the initialization of one span is composed of the initialization of multiple spans, each representing a single input item in the batch.
Span child = tracer.spanBuilder(spanName)
.addLink(span(1))
.addLink(span(2))
.addLink(span(3))
.startSpan();
Create an Event¶
A span can be annotated with zero or more named events that carry span attributes. Each event is a key:value pair and automatically carries the corresponding timestamp.
Note
recordException is a special variant of addEvent, used to record exception events.
Create a Nested Span¶
Use setParent(parentSpan) to set the parent span.
void parent() {
Span parentSpan = tracer.spanBuilder("parent")
.startSpan();
childSpan(parentSpan);
parentSpan.end();
}
void childSpan(Span parentSpan) {
Span childSpan = tracer.spanBuilder("childSpan")
.setParent(parentSpan)
.startSpan();
// do stuff
childSpan.end();
}
Baggage Usage¶
Baggage can be propagated throughout the trace. It is suitable for global instrumentation, such as instrumenting a user ID or user name to track business data.
Gateway method sets Baggage:
// Baggage usage, set here
Baggage.current().toBuilder().put("app.username", "gateway").build().makeCurrent();
logger.info("gateway set baggage[app.username] value: gateway");
Resource method gets Baggage:
// Baggage usage, get here
String baggage = Baggage.current().getEntryValue("app.username");
logger.info("resource get baggage[app.username] value: {}", baggage);
Construct a New Span Using a Known TraceId and SpanId¶
The Tracer provides the setParent(context) method for constructing a span, making it easy to set a parent span for a custom span.
setParent requires a Context parameter, so a context must be constructed.
The OpenTelemetry SDK provides only one create method for creating a SpanContext, which allows you to customize the traceId and spanId.
SpanContext create(String traceIdHex, String spanIdHex, TraceFlags traceFlags, TraceState traceState)
SpanContext, as part of representing a Span, must be serializable and propagatable along the distributed context. SpanContext is immutable.
OpenTelemetry SpanContext conforms to the W3C TraceContext specification. It contains two identifiers - TraceId and SpanId - a set of common TraceFlags, and system-specific TraceState.
-
TraceIdA validTraceIdis a 16-byte array with at least one non-zero byte. -
SpanIdA validSpanIdis an 8-byte array with at least one non-zero byte. -
TraceFlagsContains details about the trace. UnlikeTraceFlags,TraceFlagsaffects all traces. The only flag defined in the current version issampled. -
TraceStateCarries vendor-specific trace identification data, identified by an array of key-value pairs.TraceStateallows multiple tracing systems to participate in the same trace. For a full definition, refer to the W3C Trace Context specification.
The API must implement methods to create SpanContext. These methods should be the only way to create a SpanContext. This functionality must be fully implemented in the API and should not be overridable.
However, SpanContext is not a Context, so an additional conversion is needed.
private Context withSpanContext(SpanContext spanContext, Context context) {
return context.with(Span.wrap(spanContext));
}
Full code:
/***
* @Description Construct a new span using a known traceId and spanId.
* @Param [spanName, traceId, spanId]
* @return java.lang.String
**/
@GetMapping("/customSpanByTraceIdAndSpanId")
@ResponseBody
public String customSpanByTraceIdAndSpanId(String spanName,String traceId,String spanId){
assert StringUtils.isEmpty(spanName):"spanName cannot be empty";
assert StringUtils.isEmpty(traceId):"traceId cannot be empty";
assert StringUtils.isEmpty(spanId):"spanId cannot be empty";
Context context =
withSpanContext(
SpanContext.create(
traceId, spanId, TraceFlags.getSampled(), TraceState.getDefault()),
Context.current());
Span span = tracer.spanBuilder(spanName)
.setParent(context)
.startSpan();
span.setAttribute("attribute.a2", "some value");
span.setAttribute("func","attr");
span.setAttribute("app","otel3");
span.end();
return buildTraceUrl(span.getSpanContext().getTraceId());
}
private Context withSpanContext(SpanContext spanContext, Context context) {
return context.with(Span.wrap(spanContext));
}
Note
It is important to note that, based on the current test implementation, the request itself generates a new trace. The newly constructed span is built based on the passed parameters.
You can observe the result by visiting the following link:
http://localhost:8080/customSpanByTraceIdAndSpanId?spanName=tSpan&traceId=24baeeddfbb35fceaf4c18e7cae58fe1&spanId=ff1955b4f0eacc4f
Metrics¶
OpenTelemetry also provides APIs for metric-related operations.
Spans provide detailed information about the application, but the data generated is proportional to the system load. In contrast, metrics aggregate individual measurements into aggregates and generate constant data as a function of system load. Aggregates lack the detail needed to diagnose low-level issues, but they complement spans by helping identify trends and providing application runtime telemetry.
The Metrics API defines various instruments. Instruments record measurements, which are aggregated by the Metrics SDK and eventually exported out of process. Instruments can be synchronous or asynchronous. Synchronous instruments record measurements. Asynchronous instruments register a callback, which is invoked once per collection, and record the measurement at that point in time. The following instruments are available:
-
LongCounter/DoubleCounter: Records only positive values, with synchronous and asynchronous options. Useful for counting things, such as bytes sent over the network. By default, counter measurements are aggregated as an always-increasing monotonic sum.
-
LongUpDownCounter/DoubleUpDownCounter: Records positive and negative values, with synchronous and asynchronous options. Useful for counting things that go up and down, such as queue size. By default, up-down counter measurements are aggregated as a non-monotonic sum.
-
LongGauge/DoubleGauge: Measures instantaneous values with an asynchronous callback. Useful for recording values that cannot be merged across attributes, such as CPU usage percentage. By default, gauge measurements are aggregated as a gauge.
-
LongHistogram/DoubleHistogram: Records measurements that are most useful for analyzing histogram distributions. No asynchronous option is available. Useful for recording the time taken to process a request by an HTTP server. By default, histogram measurements are aggregated as an explicit bucket histogram.
Get the Meter Object¶
The API defines a Meter interface. This interface consists of a set of instrument constructors and a tool for atomically batch-collecting measurements. A new Meter instance can be created via the MeterProvider.getMeter(name) method. MeterProvider is typically expected to be used as a singleton. Its implementation should serve as the global, unique implementation of MeterProvider. Different types of metrics can be constructed through the Meter object.
The details of MeterProvider are skipped here, mainly because the OpenTelemetry interface provides a default noop implementation of MeterProvider.
Build a Gauge-Type Metric¶
meter.gaugeBuilder("connections")
.setDescription("Current number of Socket.io connections")
.setUnit("1")
.buildWithCallback(
result -> {
System.out.println("metrics");
for (int i = 1; i < 4; i++) {
result.record(
i,
Attributes.of(
AttributeKey.stringKey("id"),
"a" + i));
}
});
buildWithCallback is a callback function that supports the asynchronous API and collects metric data on demand. Data is collected at intervals, defaulting to once every 1 minute.
Related Documentation¶
OpenTelemetry Trace Data Ingestion