This walkthrough shows how to add observability to your Java application using the manual instrumention libraries and tools provided by OpenTelemetry Java.
For details on how to assemble the base OTLP endpoint URL, see Export with OTLP.
The URL should end in /api/v2/otlp
.
The access token for ingesting traces, logs, and metrics can be generated under Access Tokens.
Export with OTLP has more details on the format and the necessary access scopes.
To create new spans, we first need a tracer object.
Tracer tracer = GlobalOpenTelemetry.getTracerProvider().tracerBuilder("my-tracer") //TODO Replace with the name of your tracer.build();
With tracer
, we can now use a span builder to create and start new spans.
// Obtain and name new span from tracerSpan span = tracer.spanBuilder("Call to /myendpoint").setSpanKind(SpanKind.CLIENT).startSpan();// Set demo span attributes using semantic namingspan.setAttribute("http.method", "GET");span.setAttribute("net.protocol.version", "1.1");// Set the span as current span and parent for future child spanstry (Scope scope = span.makeCurrent()){// TODO your code goes here}finally{// Completing the spanspan.end();}
In the code above, we:
makeCurrent()
method to mark it as active span and parent of future spans (until the span finished)end()
method to complete the span (in a finally
block to ensure the method is called)To instantiate new metrics instruments, we first need a meter object.
Meter meter = GlobalOpenTelemetry.getMeterProvider().meterBuilder("my-meter") //TODO Replace with the name of your meter.build();
With meter
, we can now create individual instruments, such as a counter.
LongCounter counter = meter.counterBuilder("request_counter").setDescription("The number of requests we received").setUnit().build();
We can now invoke the add()
method of counter
to record new values with our counter and save additional attributes (for example, action.type
).
Attributes attrs = Attributes.of(stringKey("action.type"), "create");counter.add(1, attrs);
You can also create an asynchronous gauge, which requires a callback function that will be invoked by OpenTelemetry upon data collection.
The following example records on each invocation the available memory, along with an attribute on the number of active users obtained from a fictitious getUserCount()
method.
meter.gaugeBuilder("free_memory").setDescription("Available memory in bytes").setUnit("bytes").buildWithCallback(measurement -> {measurement.record(Runtime.getRuntime().freeMemory(),Attributes.of(stringKey("user_count"), getUserCount()));});
You first need to adjust your Log4j 2 configuration file log4j.xml
, to include the OpenTelemetry appender.
<?xml version="1.0" encoding="UTF-8"?><Configuration status="WARN" packages="io.opentelemetry.instrumentation.log4j.appender.v2_17"><Appenders><Console name="Console" target="SYSTEM_OUT"><PatternLayoutpattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{36} trace_id: %X{trace_id} span_id: %X{span_id} trace_flags: %X{trace_flags} - %msg%n"/></Console><OpenTelemetry name="OpenTelemetryAppender"/></Appenders><Loggers><Root><AppenderRef ref="OpenTelemetryAppender" level="All"/><AppenderRef ref="Console" level="All"/></Root></Loggers></Configuration>
In this configuration, we added a new <OpenTelemetry>
entry under <Appenders>
, as well as an <AppenderRef>
entry under <Loggers>
.
With the call to GlobalLoggerProvider
, which we previously performed under Setup, this appender is configured for the Dynatrace backend.
Context propagation is particularly important when network calls (for example, REST) are involved.
If you are using automatic instrumentation and your networking libraries are covered by automatic instrumentation, this will be automatically taken care of by the instrumentation libraries. Otherwise, your code needs to take this into account.
In the following example, we assume that we have received a network call via com.sun.net.httpserver.HttpExchange
and we define a TextMapGetter
instance to fetch the context information from the HTTP headers. We then pass that instance to extract()
, returning the context object, which allows us to continue the previous trace with our spans.
//The getter will be used for incoming requestsTextMapGetter<HttpExchange> getter =new TextMapGetter<>() {@Overridepublic String get(HttpExchange carrier, String key) {if (carrier.getRequestHeaders().containsKey(key)) {return carrier.getRequestHeaders().get(key).get(0);}return null;}@Overridepublic Iterable<String> keys(HttpExchange carrier) {return carrier.getRequestHeaders().keySet();}};@Overridepublic void handle(HttpExchange httpExchange) {//Extract the SpanContext and other elements from the requestContext extractedContext = GlobalOpenTelemetry.getPropagators().getTextMapPropagator().extract(Context.current(), httpExchange, getter);try (Scope scope = extractedContext.makeCurrent()) {//This will automatically propagate context by creating child spans within the extracted contextSpan serverSpan = tracer.spanBuilder("my-server-span") //TODO Replace with the name of your span.setSpanKind(SpanKind.SERVER) //TODO Set the kind of your span.startSpan();serverSpan.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); //TODO Add attributesserverSpan.end();}}
In the following example, we send a REST request to another service and provide our existing context as part of the HTTP headers of our request.
To do so, we define a TextMapSetter
instance, which adds the respective information with setRequestProperty()
. Once we have instantiated our REST object, we pass it, along with the context and the setter instance, to inject()
, which will add the necessary headers to the request.
//The setter will be used for outgoing requestsTextMapSetter<HttpURLConnection> setter =(carrier, key, value) -> {assert carrier != null;// Insert the context as Headercarrier.setRequestProperty(key, value);};URL url = new URL("<URL>"); //TODO Replace with the URL of the service to be calledSpan outGoing = tracer.spanBuilder("my-client-span") //TODO Replace with the name of your span.setSpanKind(SpanKind.CLIENT) //TODO Set the kind of your span.startSpan();try (Scope scope = outGoing.makeCurrent()) {outGoing.setAttribute(SemanticAttributes.HTTP_METHOD, "GET"); //TODO Add attributesHttpURLConnection transportLayer = (HttpURLConnection) url.openConnection();// Inject the request with the *current* Context, which contains our current spanGlobalOpenTelemetry.getPropagators().getTextMapPropagator().inject(Context.current(), transportLayer, setter);// Make outgoing call} finally {outGoing.end();}
While Dynatrace automatically captures all OpenTelemetry attributes, only attribute values specified in the allowlist are stored and displayed in the Dynatrace web UI. This prevents accidental storage of personal data, so you can meet your privacy requirements and control the amount of monitoring data stored.
To view your custom attributes, you need to allow them in the Dynatrace web UI first. To learn how to configure attribute storage and masking, see Attribute redaction.
Once you have finished the instrumentation of your application, perform a couple of test actions to create and send demo traces, metrics, and logs and verify that they were correctly ingested into Dynatrace.
To do that for traces, go to Distributed Traces or Distributed Traces Classic (latest Dynatrace) and select the Ingested traces tab. If you use OneAgent, select PurePaths instead.
For metrics and logs, go to Metrics or Logs or Logs & Events (latest Dynatrace).