# OpenTelemetry Setup (/sdks/java/how-to/observability/opentelemetry-setup)



## Overview [#overview]

**OpenTelemetry integration** wires the SDK's messaging operations into your tracing and metrics pipeline without hand-instrumenting every call site. In a distributed system where a message might be published by one service, queued, and consumed by three others, per-call logging tells you almost nothing — you need spans that correlate across process boundaries and latency/error metrics broken out by channel and operation. Instrumenting that by hand around every `publishEventStore` or `subscribeToEventsStore` call is tedious and easy to get inconsistent; letting the JVM agent do it guarantees uniform coverage with zero SDK code changes.

The SDK ships its own instrumentation — it does **not** rely solely on an external Java agent to intercept gRPC calls. Internally, `KubeMQClient` detects whether the OpenTelemetry API is present on the classpath (`Class.forName("io.opentelemetry.api.trace.Tracer")`); if it is, the SDK creates spans and records metrics directly, under the instrumentation scope name `io.kubemq.sdk`. Both the tracer and the meter default to `GlobalOpenTelemetry` — there's no builder method to hand the client your own `TracerProvider`/`MeterProvider`, so you register your OTel SDK globally (via the `-javaagent:` agent, or `GlobalOpenTelemetry.set(...)` in code) *before* creating the `KubeMQClient`, and the SDK's instrumentation picks it up automatically.

Spans and metrics follow the OTel messaging semantic conventions (`messaging.client.*` / `messaging.system`, semconv v1.27.0), not a custom `kubemq.*` namespace — `messaging.system` is set to the string `"kubemq"` as a span/metric **attribute**, and instruments are named things like `messaging.client.operation.duration`, `messaging.client.sent.messages`, and `messaging.client.consumed.messages`. The one exception is two SDK-specific retry counters — `kubemq.client.retry.attempts` and `kubemq.client.retry.exhausted` — which do carry a `kubemq.*` prefix because they have no equivalent in the messaging semconv. &#x2A;*Gotchas:** without an OTel API on the classpath, the SDK falls back to no-op tracer/meter implementations with near-zero overhead — there's no partial instrumentation; if you use the Java agent, it must be configured with `-Dotel.traces.exporter=...`/`-Dotel.metrics.exporter=...` or spans/metrics have nowhere to go; and the SDK's own spans only cover its gRPC-level operations, so custom business spans still need manual `Span` creation in your own code.

## Prerequisites [#prerequisites]

* KubeMQ server running on `localhost:50000`
* Java SDK installed (`implementation 'io.kubemq.sdk:kubemq-sdk-Java:3.1.1'` (Gradle) or Maven dependency from [Getting Started](/sdks/java))

## Code [#code]

```java title="OpenTelemetrySetupExample.java"
package io.kubemq.example.observability;

import io.kubemq.sdk.client.KubeMQClient;
import io.kubemq.sdk.common.ServerInfo;
import io.kubemq.sdk.pubsub.*;
import io.opentelemetry.api.GlobalOpenTelemetry;
import io.opentelemetry.sdk.OpenTelemetrySdk;
import io.opentelemetry.sdk.metrics.SdkMeterProvider;
import io.opentelemetry.sdk.metrics.data.MetricData;
import io.opentelemetry.sdk.testing.exporter.InMemoryMetricReader;
import java.util.Collection;

/**
 * OpenTelemetry Setup Example
 *
 * Demonstrates how the KubeMQ Java SDK integrates with OpenTelemetry metrics.
 *
 * The SDK auto-detects the OTel API on the classpath and, when present,
 * records metrics under the instrumentation scope "io.kubemq.sdk" using the
 * OTel messaging semantic conventions (messaging.client.*), reading from
 * whatever MeterProvider is registered as GlobalOpenTelemetry. There is no
 * KubeMQClient builder method to inject a custom MeterProvider directly, so
 * this example registers one globally *before* creating the client — the
 * same effect the -javaagent: agent has in production, just done in code
 * with an in-memory reader so the example is self-contained.
 *
 * In production, skip the manual OpenTelemetrySdk.builder() step below and
 * instead attach the OpenTelemetry Java Agent on the JVM command line:
 *   java -javaagent:opentelemetry-javaagent.jar \
 *        -Dotel.service.name=my-kubemq-service \
 *        -Dotel.metrics.exporter=otlp \
 *        -jar myapp.jar
 */
public class OpenTelemetrySetupExample {

    private static final String ADDRESS = "localhost:50000";
    private static final String CLIENT_ID = "java-observability-otel-setup-client";

    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== OpenTelemetry Setup Example ===\n");

        // Register an in-memory OTel meter provider as the global instance
        // BEFORE creating the KubeMQClient -- the SDK reads GlobalOpenTelemetry
        // at client-construction time.
        InMemoryMetricReader reader = InMemoryMetricReader.create();
        SdkMeterProvider meterProvider =
                SdkMeterProvider.builder().registerMetricReader(reader).build();
        OpenTelemetrySdk.builder().setMeterProvider(meterProvider).buildAndRegisterGlobal();

        System.out.println("Registered a global OTel MeterProvider with an in-memory reader.");
        System.out.println("The KubeMQ SDK will record metrics under scope \"io.kubemq.sdk\",");
        System.out.println("using messaging.client.* instrument names (OTel messaging semconv),");
        System.out.println("not a custom \"kubemq.*\" namespace.\n");

        // Create a client (OTel auto-detected because the API is on the classpath
        // and a global MeterProvider is now registered)
        try (PubSubClient client = PubSubClient.builder()
                .address(ADDRESS)
                .clientId(CLIENT_ID)
                .logLevel(KubeMQClient.Level.INFO)
                .build()) {

            ServerInfo info = client.ping();
            System.out.println("Connected to: " + info.getHost() + " v" + info.getVersion());

            // Create channel for send/receive (generates OTel spans + metrics)
            String channel = "java-observability.otel-setup";
            client.createEventsStoreChannel(channel);

            // Send messages (increments messaging.client.sent.messages)
            System.out.println("\nSending messages...");
            for (int i = 1; i <= 3; i++) {
                EventStoreMessage message = EventStoreMessage.builder()
                        .id("otel-" + i)
                        .channel(channel)
                        .body(("Traced message #" + i).getBytes())
                        .metadata("otel-example")
                        .build();

                EventSendResult result = client.publishEventStore(message);
                System.out.println("  Sent #" + i + " (sent=" + result.isSent() + ")");
            }

            // Subscribe and receive (increments messaging.client.consumed.messages)
            System.out.println("\nSubscribing...");
            EventsStoreSubscription sub = EventsStoreSubscription.builder()
                    .channel(channel)
                    .eventsStoreType(EventsStoreType.StartFromFirst)
                    .onReceiveEventCallback(event ->
                        System.out.println("  Received: " + new String(event.getBody())))
                    .onErrorCallback(err ->
                        System.err.println("  Error: " + err.getMessage()))
                    .build();

            client.subscribeToEventsStore(sub);
            Thread.sleep(2000);

            // Clean up resources
            sub.cancel();
            client.deleteEventsStoreChannel(channel);

        } catch (Exception e) {
            System.err.println("Error: " + e.getMessage());
        }

        // Read the metrics back from the in-memory reader to confirm the real
        // instrument names -- this is the reproducible proof that the SDK
        // emits messaging.client.* (and NOT a kubemq.* meter namespace).
        System.out.println("\nRecorded metrics (instrumentation scope \"io.kubemq.sdk\"):");
        Collection<MetricData> metrics = reader.collectAllMetrics();
        for (MetricData metric : metrics) {
            System.out.println("  " + metric.getName() + " (" + metric.getType() + ")");
        }

        meterProvider.close();
        GlobalOpenTelemetry.resetForTest();
        System.out.println("\nOpenTelemetry setup example completed.");
    }
}

```

## How It Works [#how-it-works]

* The SDK detects the OTel API on the classpath at `KubeMQClient` construction time and, when present, wires up its own tracer and meter — reading whichever `TracerProvider`/`MeterProvider` is registered as `GlobalOpenTelemetry` at that moment. There is no builder method to pass a provider directly, so registration must happen first (either the `-javaagent:` agent or, as in this example, `OpenTelemetrySdk.builder().buildAndRegisterGlobal()`).
* Metrics are recorded under instrumentation scope `io.kubemq.sdk` using OTel messaging semantic-convention names — `messaging.client.operation.duration`, `messaging.client.sent.messages`, `messaging.client.consumed.messages`, `messaging.client.connection.count`, `messaging.client.reconnections` — with `messaging.system="kubemq"` as an attribute, not part of the instrument name.
* Two retry counters are the only `kubemq.*`-prefixed instruments: `kubemq.client.retry.attempts` and `kubemq.client.retry.exhausted` (no messaging-semconv equivalent exists for SDK-level retry accounting).
* `reader.collectAllMetrics()` reads directly from the `InMemoryMetricReader` registered above, which is how this example proves the real instrument names without needing an external collector; in production, replace it with a `PeriodicMetricReader` exporting to your backend (Prometheus, OTLP, etc.), or use the Java agent instead of the manual `OpenTelemetrySdk.builder()` step.
* Without any OTel API on the classpath, the SDK falls back to no-op tracer/meter implementations — instrumentation is fully absent, not partially degraded.

## Related [#related]

* [Java SDK Reference](/sdks/java/reference)
