Tracing
Overview
Distributed tracing is a critical feature of microservice based applications, since it traces workflow both within a service and across multiple services. This provides insight to sequence and timing data for specific blocks of work, which helps you identify performance and operational issues. Helidon includes support for distributed tracing through its own API, backed by OpenTelemetry. Tracing is integrated with WebServer and Security.
Maven Coordinates
To enable Helidon Tracing, add the following dependency to your project’s
pom.xml (see Managing Dependencies).
Usage
This section explains a few concepts that you need to understand before you get started with tracing.
- In the context of this document, a service is synonymous with an application.
- A span is the basic unit of work done within a single service, on a single host. Every span has a name, starting timestamp, and duration. For example, the work done by a REST endpoint is a span. A span is associated to a single service, but its descendants can belong to different services and hosts.
- A trace contains a collection of spans from one or more services, running on one or more hosts. For example, if you trace a service endpoint that calls another service, then the trace would contain spans from both services. Within a trace, spans are organized as a directed acyclic graph (DAG) and can belong to multiple services, running on multiple hosts.
- Baggage is a collection of key-value pairs associated with a span.
- Span context captures data about a span not related to its duration, such as the tracer ID, the span ID, and baggage.
Support for specific tracers is abstracted. Your application can depend on the
Helidon abstraction layer and provide a specific tracer implementation as a
Java ServiceLoader service. Helidon provides OpenTelemetry tracers configured
from Helidon tracing settings.
Migrating from Global Tracer APIs
Helidon 27 removes the deprecated static APIs for assigning or retrieving the
Helidon global tracer. The OpenTelemetry helper methods which assigned a
Helidon global tracer, such as HelidonOpenTelemetry.global(...) and
OpenTelemetryTracerProvider.globalTracer(...), are also removed. Application
code should obtain the application Tracer from the Helidon service registry,
using either declarative injection or Services.get(Tracer.class).
If an application prepares its own tracing runtime, register the manually
prepared OpenTelemetry and Tracer instances with Services.set(...) before
any application or Helidon component requests either contract from the service
registry. Once a registry singleton is resolved, replacing it is not supported.
Some OpenTelemetry instrumentation reads GlobalOpenTelemetry directly. For
that interop case, configure telemetry.global: true or tracing.global: true
so Helidon publishes the selected application OpenTelemetry instance to the
OpenTelemetry global during service-registry ownership startup. This must happen
before any other code initializes GlobalOpenTelemetry, because OpenTelemetry
allows the JVM-wide global to be assigned only once. If an OpenTelemetry global
already exists, Helidon leaves it unchanged and uses the existing global as the
application OpenTelemetry instance in the service registry.
WebServer Setup
Configuring Tracer:
Custom Spans
To create a custom span from tracer:
Handling Baggage
Your application can set and read baggage associated with a Span. The
Span.baggage() method returns a WritableBaggage instance.
Further, Helidon also provides read-only access to baggage linked to a
SpanContext. For example, HTTP headers can convey trace ID,
span ID, and baggage information and Helidon puts such information into a
SpanContext. Your code can create a SpanContext from other sources as well.
The SpanContext.baggage() method returns a read-only Baggage
instance.
The Javadoc for the types describes how to get and set baggage entries, get all the baggage keys, and check whether a baggage key exists in the baggage.
Span Lifecycle
Events
Applications and libraries can register listeners to be notified at several moments during the lifecycle of every Helidon span:
- Before a new span starts
- After a new span has started
- After a span ends
- After a span is activated (creating a new scope)
- After a scope is closed
The next sections explain how you can write and add a listener and what it can
do. See the SpanListener Javadoc for more information.
Listeners
A listener cannot affect the lifecycle of a span or scope it is notified about, but it can add tags and events and update the baggage associated with a span. Often a listener does additional work that does not change the span or scope such as logging a message.
When Helidon invokes the listener’s methods it passes proxies for the
Span.Builder, Span, and Scope arguments. These proxies limit the access
the listener has to the span builder, span, or scope, as summarized in the
following table. If a listener method tries to invoke a forbidden operation, the
proxy throws a SpanListener.ForbiddenOperationException
and Helidon then logs a WARNING message describing the invalid operation
invocation.
| Tracing type | Changes allowed |
|---|---|
Span.Builder | Add tags |
Span | Retrieve and update baggage, add events, add tags |
Scope | none |
Summary of Permitted Operations on Proxies Passed to Listeners
The following tables list specifically what operations the proxies permit.
| Method | Purpose | OK? |
|---|---|---|
build() | Starts the span. | - |
end methods | Ends the span. | - |
get() | Starts the span. | - |
kind(Kind) | Sets the "kind" of span (server, client, internal, etc.) | - |
parent(SpanContext) | Sets the parent of the span to be created from the builder. | - |
start() | Starts the span. | - |
start(Instant) | Starts the span. | - |
tag methods | Add a tag to the builder before the span is built. | ✓ |
unwrap(Class) | Cast the builder to the specified implementation type. | ✓ |
io.helidon.tracing.Span.Builder Operations
| Method | Purpose | OK? |
|---|---|---|
activate() | Makes the span "current", returning a Scope. | - |
addEvent methods | Associate a string (and optionally other info) with a span. | ✓ |
baggage() | Returns the Baggage instance associated with the span. | ✓ |
context() | Returns the SpanContext associated with the span. | ✓ |
status(Status) | Sets the status of the span. | - |
any tag method | Add a tag to the span. | ✓ |
unwrap(Class) | Cast the span to the specified implementation type. | ✓ |
io.helidon.tracing.Span Operations
| Method | Purpose | OK? |
|---|---|---|
close() | Close the scope. | - |
isClosed() | Reports whether the scope is closed. | ✓ |
io.helidon.tracing.Scope Operations
| Method | Purpose | OK? |
|---|---|---|
asParent(Span.Builder) | Sets this context as the parent of a new span builder. | ✓ |
baggage() | Returns Baggage instance associated with the span context. | ✓ |
sampled() | Reports whether the associated span is sampled. | ✓ |
spanId() | Returns the span ID. | ✓ |
traceId() | Returns the trace ID. | ✓ |
io.helidon.tracing.SpanContext Operations
Adding a Listener
Explicitly Registering a Listener on a Tracer
Create a SpanListener instance and invoke the Tracer#register(SpanListener)
method to make the listener known to that tracer.
Automatically Registering a Listener on all Tracer Instances
Helidon also uses Java service loading to locate listeners and register them
automatically on all Tracer objects. Follow these steps to add a listener
service provider.
- Implement the
SpanListenerinterface. - Declare your implementation as a service provider:
- Create the file
META-INF/services/io.helidon.tracing.SpanListenercontaining a line with the fully-qualified name of your class which implementsSpanListener. - If your service has a
module-info.javafile add the following line to it:provides io.helidon.tracing.SpanListener with <your-implementation-class>;
- Create the file
The SpanListener interface declares default no-op implementations for all the
methods, so your listener can implement only the methods it needs to.
Helidon invokes each listener’s methods in the following order:
| Method | When invoked |
|---|---|
starting(Span.Builder<?> spanBuilder) | Just before a span is started from its builder. |
started(Span span) | Just after a span has started. |
activated(Span span, Scope scope) | After a span has been activated, creating a new scope. A given span might never be activated; it depends on the code. |
closed(Span span, Scope scope) | After a scope has been closed. |
ended(Span span) | After a span has ended successfully. |
ended(Span span, Throwable t) | After a span has ended unsuccessfully. |
Order in which Helidon Invokes Listener Methods
OpenTelemetry Callbacks
To use lifecycle callbacks, applications should normally work with the Helidon
Tracer, Span.Builder, Span, and Scope types which automatically call
back to each registered SpanListener.
In some cases application code might want to use a reference to an OpenTelemetry
Tracer or Span rather than a reference to the Helidon counterpart but
still want to respond to lifecycle events as the OpenTelemetry object goes
through its lifecycle.
The HelidonOpenTelemetry type provides several methods
which enable callbacks for OpenTelemetry objects, as summarized in the following
table.
| Method | Return Value |
|---|---|
Tracer callback |
Callback-enabled OpenTelemetry Tracer corresponding to the
specified Helidon Tracer |
Tracer callback |
Callback-enabled OpenTelemetry Tracer for the specified
OpenTelemetry Tracer |
Span callback |
Callback-enabled OpenTelemetry Span corresponding to the specified
Helidon Span |
Span callback |
Callback-enabled OpenTelemetry Span for the specified OpenTelemetry
Span |
Enabling OpenTelemetry Objects for SpanListener Support
An OpenTelemetry object returned from a method on a callback-enabled object is itself callback-enabled automatically. Specifically:
SpanBuilderreturned fromTracer#spanBuilder(String).Spanreturned fromSpanBuilder#startSpan.Scopereturned fromSpan#makeCurrent.
Each callback-enabled object is a new instance of a Helidon object which
implements both the indicated OpenTelemetry interface and the Helidon
Wrapper interface. These Helidon objects do not themselves
implement other OpenTelemetry interfaces. To do type checks and casts on
callback-enabled objects, invoke the unwrap(Class<?>) on a callback-enabled
object as shown in the following example.
// Note that callbackEnabledSpan implements OpenTelemetry Span.
io.opentelemetry.api.trace.Span nativeOtelSpan = callbackEnabledSpan.unwrap(io.opentelemetry.api.trace.Span.class);
if (nativeOtelSpan instanceof ReadableSpan readableSpan) {
// Work with the span as a ReadableSpan
}
Remember that operations on the nativeOtelSpan variable do not notify span
listeners of lifecycle changes.
Configuration options
| Key | Type | Default | Description |
|---|---|---|---|
path | String | Path on the collector host to use when sending data to tracing collector | |
protocol | String | Protocol to use (such as http or https) to connect to tracing collector | |
boolean- | Map< | Tracer level tags that get added to all reported spans | |
port | Integer | Port to use to connect to tracing collector | |
service | String | Service name of the traced service | |
host | String | Host to use to connect to tracing collector | |
global | Boolean | true | Whether the OpenTelemetry instance created by this tracer builder should be published to Global |
int- | Map< | Tracer level tags that get added to all reported spans | |
enabled | Boolean | true | When enabled, tracing will be sent |
tags | Map< | Tracer level tags that get added to all reported spans |
Helidon Spans
The following table lists all spans traced by Helidon components:
| component | span name | description |
|---|---|---|
web-server | HTTP Request | The overall span of the Web Server from request initiation until response. |
web-server | content-read | Span for reading the request entity |
web-server | content-write | Span for writing the response entity |
security | security | Processing of request security |
security | security:atn | Span for request authentication |
security | security:atz | Span for request authorization |
security | security:response | Processing of response security |
security | security:outbound | Processing of outbound security |
Some of these spans log to the span. These log events can be (in most cases)
configured:
| span name | log name | configurable | enabled by default | description |
|---|---|---|---|---|
HTTP Request | handler.class | YES | YES | Each handler has its class and event logged |
security | status | YES | YES | Logs either "status: PROCEED" or "status: DENY" |
security:atn | security.user | YES | NO | The username of the user if logged in |
security:atn | security.service | YES | NO | The name of the service if logged in |
security:atn | status | YES | YES | Logs the status of security response (such as SUCCESS) |
security:atz | status | YES | YES | Logs the status of security response (such as SUCCESS) |
security:outbound | status | YES | YES | Logs the status of security response (such as SUCCESS) |
There are also tags that are set by Helidon components. These are not configurable.
| span name | tag name | description |
|---|---|---|
HTTP Request | component | name of the component, helidon-webserver |
HTTP Request | http.method | HTTP method of the request, such as GET, POST |
HTTP Request | http.status_code | HTTP status code of the response |
HTTP Request | http.url | The path of the request (for Helidon without protocol, host and port) |
HTTP Request | error | If the request ends in error, this tag is set to true, usually accompanied by logs with details |
security | security.id | ID of the security context created for this request (if security is used) |
Configuration
Each component and its spans can be configured using Config. The traced configuration has the following layers:
TracingConfig- the overall configuration of traced components of HelidonComponentTracingConfig- a component of Helidon that traces spans (such asweb-serverandsecurity)SpanTracingConfig- a single traced span within a component (such assecurity:atn)SpanLogTracingConfig- a single log event on a span (such assecurity.userin spansecurity:atn)
The components using tracing configuration use the TracingConfigUtil. This
uses the io.helidon.common.Context to retrieve current configuration.
Configuration Using Builder
Builder approach, example that disables a single span log event:
Configure tracing using a builder:
TracingConfig.builder()
.addComponent(ComponentTracingConfig.builder("web-server")
.addSpan(SpanTracingConfig.builder("HTTP Request")
.addSpanLog(SpanLogTracingConfig.builder("content-write")
.enabled(false)
.build())
.build())
.build())
.build();
Configuration using Helidon Config
Tracing configuration can be defined in a config file.
Tracing configuration:
tracing:
components:
web-server:
spans:
- name: "HTTP Request"
logs:
- name: "content-write"
enabled: false
Use the configuration in web server:
Path-based Configuration in Helidon WebServer
For Web Server we have path-based support for configuring tracing, in addition to the configuration described above.
Configuration of path can use any path string supported by the WebServer. The configuration itself has the same possibilities as traced configuration described above. The path-specific configuration will be merged with global configuration (path is the "newer" configuration, global is the "older")
Configuration in YAML:
tracing:
paths:
- path: "/favicon.ico"
enabled: false
- path: "/metrics"
enabled: false
- path: "/health"
enabled: false
- path: "/greet"
methods: ["GET"]
components:
web-server:
spans:
- name: "content-read"
new-name: "read"
enabled: false
Configured method names are matched exactly.
Renaming top level span using request properties
To have a nicer overview in search pane of a tracer, you can customize the top-level span name using configuration.
Example:
Configuration in YAML:
tracing:
components:
web-server:
spans:
- name: "HTTP Request"
new-name: "HTTP %1$s %2$s"
This is supported ONLY for the span named "HTTP Request" on component "web-server".
Parameters provided:
- Method - HTTP method
- Path - path of the request (such as
/greet) - Query - query of the request (maybe null)
WebClient Propagation
Span propagation is supported with Helidon WebClient. Tracing propagation is automatic as long as the current span context is available in Helidon Context (which is automatic when running within a WebServer request).
<dependencies>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient</artifactId>
</dependency>
<dependency>
<groupId>io.helidon.webclient</groupId>
<artifactId>helidon-webclient-tracing</artifactId>
</dependency>
</dependencies>
Tracing propagation with Helidon WebClient:
WebClient client = WebClient.builder()
.addService(WebClientTracing.create())
.build();
String response = client.get()
.uri(uri)
.requestEntity(String.class);
OpenTelemetry Tracing
Helidon supports configuration of OpenTelemetry and OpenTelemetry tracing in two
primary ways: using tracing or using telemetry. This page describes support for
controlling OpenTelemetry tracing using the tracing config section and
OpenTelemetryTracerConfig builder. Users typically adopt
this approach to ease migration from older tracing configuration to
OpenTelemetry.
That said, Helidon’s support for OpenTelemetry using tracing does not afford
as much control as do the Helidon telemetry settings. For example, using
OpenTelemetry tracing config you can choose either the OTLP gRPC span exporter
or the OTLP HTTP one; additional span exporters are available only using the
telemetry settings.
The telemetry doc page describes how to use the Helidon
telemetry config section and the related builder to exert more control over
OpenTelemetry and OpenTelemetry tracing behavior.
telemetry and tracing, Helidon uses the
telemetry settings. Specifying both does not confuse Helidon, but it might
confuse users. Set registered: false under tracing to make explicit that
the tracing section should not contribute the application-wide
OpenTelemetry and Tracer.Dependency for OpenTelemetry support using tracing:
<dependency>
<groupId>io.helidon.tracing.providers</groupId>
<artifactId>helidon-tracing-providers-opentelemetry</artifactId>
</dependency>
Configuration options
| Key | Type | Default | Description |
|---|---|---|---|
export- | Duration | PT10S | Maximum time a transmission can be in progress before being cancelled |
sampler- | Double | 1. | Parameter value used by the selected sampler; interpretation depends on the sampler type |
boolean- | Map< | Tracer-level tags with boolean values added to all reported spans | |
span- | Span | BATCH | Type of span processor for accumulating spans before transmission to the tracing collector |
trusted- | Resource | Trusted certificates for connecting to the tracing collector | |
sampler- | Sampler | constant | Type of sampler for collecting spans |
registered | Boolean | true | Whether this tracer configuration should be treated as an application-wide ownership candidate when it is resolved from the Helidon service registry |
global | Boolean | true | Whether the OpenTelemetry instance created from this configuration should be published as the OpenTelemetry global; if an OpenTelemetry global already exists, Helidon leaves it unchanged and uses the existing global as the application OpenTelemetry instance |
exporter- | Otlp | GRPC | Type of OTLP exporter to use for pushing span data |
propagators | List< | Context propagators | |
client- | Resource | Client certificate for connecting securely to the tracing collector | |
uri | URI | URI for the collector to which to send tracing data | |
enabled | Boolean | true | Whether to enable tracing |
tags | Map< | Tracer-level tags with String values added to all reported spans | |
path | String | Path at the collector host and port used when sending trace data to the collector | |
protocol | String | Protocol (such as http or https) used in connecting to the tracing collector | |
port | Integer | Port used in connecting to the tracing collector | |
schedule- | Duration | PT5S | Delay between consecutive transmissions to the tracing collector (batch processing) |
max- | Integer | 512 | Maximum number of spans grouped for transmission together; typically does not exceed #max (batch processing) |
service | String | Service name of the traced service | |
host | String | Host used in connecting to the tracing collector | |
private- | Resource | Private key for connecting securely to the tracing collector | |
int- | Map< | Tracer level tags with integer values added to all reported spans | |
max- | Integer | 2048 | Maximum number of spans retained before discarding any not sent to the tracing collector (batch processing) |
Example Helidon configuration for OpenTelemetry tracing:
Set tracing.registered to false only when the tracing section should not
contribute the application-wide OpenTelemetry and Tracer from the Helidon
service registry. OpenTelemetry globals are JVM-wide and can be assigned only
once, so any tracing.global: true publication must happen before any Java
agent, OpenTelemetry autoconfiguration, or application code initializes
GlobalOpenTelemetry. If an OpenTelemetry global already exists, Helidon leaves
it unchanged and uses the existing global as the application OpenTelemetry
instance in the service registry.
By default, Helidon tracing support for OpenTelemetry uses OpenTelemetry’s OTLP
gRPC exporter. Alternatively, you can choose to use OpenTelemetry’s HTTP
exporter using protobuf by setting exporter-type to http/proto. To use other
exporters OpenTelemetry offers, use the Helidon telemetry configuration
instead of tracing.