OpenTelemetry tracing follows one user action from the app through every service it touches. When the user taps the checkout button, the trace shows the tap, the requests the app made, what the backend did with each one and which database query was slow, as one timeline. Codename One produces these traces in the standard OTLP format, so any OpenTelemetry collector or tracing backend can display them.

Turning it on

Put @OpenTelemetry on the main class:

@OpenTelemetry(relay = "https://api.example.com",
        serviceName = "shop-app")
public static class ShopApp extends Lifecycle {
}

The build generates a bootstrap that installs com.codename1.telemetry.Telemetry before the app starts. From then on every ConnectionRequest is a span, and so is every call through a REST, gRPC-Web or GraphQL client the build generates, because they all go through ConnectionRequest. Each request also sends the W3C traceparent header, so a server that understands trace context continues the app’s trace instead of starting a new one. The Codename One backend understands it (see the backend chapter), and so does any service instrumented with OpenTelemetry.

An app without the annotation doesn’t reference the telemetry classes, so they aren’t part of its build.

Where the spans go

There are two ways to reach a collector, and the annotation takes one of them.

relay sends the spans to the app’s own Codename One backend, started with cn1.otel.relay=true. The backend adds the collector’s credentials and forwards the spans. This is the recommended setup: nothing secret ships in the app, and a web build never has to reach a collector on another origin.

endpoint sends the spans straight to an OTLP/HTTP collector:

@OpenTelemetry(
        endpoint = "https://abc12345.live.dynatrace.com/api/v2/otlp",
        headers = "Authorization: Api-Token dt0c01.XXXX")
public static class ShopApp extends Lifecycle {
}

The header is compiled into the app, and anyone who installs the app can read it. Use a token that can ingest traces and do nothing else.

Direct exports are binary protobuf by default, which every collector accepts and some, Dynatrace among them, require. Set protobuf = false for JSON. Exports to a relay are always JSON, and the backend re-encodes them for its collector.

Timing what the user did

A request made on its own is a trace of its own. To group the requests that one user action causes, run the action inside a span:

Telemetry.run("checkout", () -> cart.submit());

Requests queued while checkout is running become its children, and so does the backend work they cause. Telemetry.startSpan returns a span you end yourself, for work that starts in one place and finishes in another.

Sampling and privacy

sampleRatio records a fraction of new traces, from 0 to 1. A trace the app decides not to record still sends traceparent, flagged as not sampled, and the backend’s default sampler follows the app’s decision. Each trace is therefore recorded completely or not at all.

A span records the request’s method, host, path, and status. It never records the query string, request bodies or response bodies.

Whether trace data needs the user’s consent depends on what the app records and where it sends it, so the app decides. With requireAnalyticsConsent = true on the annotation, or requireAnalyticsConsent(true) on TelemetryConfig, tracing runs only while the user has granted analytics consent through the Analytics API (see Analytics). Without that consent, requests are sent exactly as they would be with telemetry off: no span and no traceparent header. Spans that were buffered before the user withdrew consent are discarded, not exported. Until the user makes a choice, the analytics consent mode decides, and the default OPT_IN mode means no tracing.

The web build

traceparent isn’t one of the request headers a browser sends across origins without asking. When a web app adds it to a request for another origin, the browser first sends a CORS preflight, and a server that doesn’t allow the header fails the request. The web build therefore sends trace context only to the relay’s host and to the hosts listed in propagateTo. The other platforms send it to every host.

Configuring it in code

The annotation is a shortcut for this call, which an app can also make itself, for example to choose the endpoint at run time:

Telemetry.install(new TelemetryConfig()
        .relay("https://api.example.com")
        .serviceName("shop-app")
        .sampleRatio(0.2));

Spans are exported in batches, when 32 have ended or every ten seconds. Call Telemetry.flush() when the app is paused, because spans still in memory are lost if the process is reclaimed.