KubeMQ
Client SDKsJavaHow-to guidesObservability

OpenTelemetry Setup

Configure OpenTelemetry tracing and metrics for the KubeMQ Java SDK to observe message flow across your services.

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. 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

  • 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)

Code

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

  • 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.

Was this page helpful?

On this page