Skip to content
Kubit Guide home
Kubit Guide home

Manual OTel Integration

This cookbook shows how to send OpenTelemetry spans to Kubit's OTLP endpoint using the stock OTel SDKs — no kubit-otel (Python) or @kubit-ai/otel (Node) required. Reach for it when you already have an OTel pipeline and just want to add Kubit as another destination, or when your runtime isn't covered by our SDKs (Java, Kotlin, Go, Ruby, and so on).

The Java recipe applies to Kotlin as well — the OTel Java SDK is the same library in both languages, and the Kotlin translation is mechanical (`val` for locals, trailing-lambda syntax, .use { } instead of try-with-resources).

Connection details

Endpoint

https://otel.kubit.ai/v1/traces

Method

POST

Auth header

x-api-key: <KUBIT_API_KEY>

Protocol

OTLP/HTTP — protobuf preferred (`Content-Type: application/x-protobuf`); JSON also accepted

Required resource attributes

service.name at minimum;

service.version and deployment.environment are recommended

A few things to keep in mind:

  • The endpoint is the full path, including /v1/traces. Don't strip the suffix the way OTel's "base endpoint" env vars expect.

  • The header is x-api-key, not Authorization.

  • Kubit collector normalizes spans server-side, so you send raw OTel — no rewriting or token-exchange step is needed.

Python

Install the SDK and the HTTP/protobuf exporter:

pip install opentelemetry-api opentelemetry-sdk opentelemetry-exporter-otlp-proto-http

Wire up a TracerProvider that exports to Kubit:

import os from opentelemetry import trace from opentelemetry.sdk.resources import Resource from opentelemetry.sdk.trace import TracerProvider from opentelemetry.sdk.trace.export import BatchSpanProcessor from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter KUBIT_API_KEY = os.environ["KUBIT_API_KEY"] exporter = OTLPSpanExporter( endpoint="https://otel.kubit.ai/v1/traces", headers={"x-api-key": KUBIT_API_KEY}, ) provider = TracerProvider( resource=Resource.create({ "service.name": "my-python-service", "service.version": "1.0.0", "deployment.environment": "production", }) ) provider.add_span_processor(BatchSpanProcessor(exporter)) trace.set_tracer_provider(provider) tracer = trace.get_tracer(__name__) with tracer.start_as_current_span("chat.completion") as span: span.set_attribute("gen_ai.system", "openai") span.set_attribute("gen_ai.request.model", "gpt-4o") # ... your LLM call ...

Node.js / TypeScript

Install the SDK packages and the protobuf trace exporter:

npm install @opentelemetry/api \ @opentelemetry/sdk-trace-node \ @opentelemetry/sdk-trace-base \ @opentelemetry/resources \ @opentelemetry/exporter-trace-otlp-proto

This recipe requires @opentelemetry/sdk-trace-node v2+, where span processors are passed at construction time rather than added afterwards.

import { trace } from "@opentelemetry/api"; import { NodeTracerProvider } from "@opentelemetry/sdk-trace-node"; import { BatchSpanProcessor } from "@opentelemetry/sdk-trace-base"; import { resourceFromAttributes } from "@opentelemetry/resources"; import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-proto"; const KUBIT_API_KEY = process.env.KUBIT_API_KEY!; const exporter = new OTLPTraceExporter({ url: "https://otel.kubit.ai/v1/traces", headers: { "x-api-key": KUBIT_API_KEY }, }); const provider = new NodeTracerProvider({ resource: resourceFromAttributes({ "service.name": "my-node-service", "service.version": "1.0.0", "deployment.environment": "production", }), spanProcessors: [new BatchSpanProcessor(exporter)], }); provider.register(); const tracer = trace.getTracer("my-node-service"); await tracer.startActiveSpan("chat.completion", async (span) => { span.setAttribute("gen_ai.system", "openai"); span.setAttribute("gen_ai.request.model", "gpt-4o"); // ... your LLM call ... span.end(); });

Java (and Kotlin)

The same OTel Java SDK powers both Java and Kotlin services — pick this recipe for either. Add the dependencies to your Gradle build:

implementation("io.opentelemetry:opentelemetry-api:1.49.0") implementation("io.opentelemetry:opentelemetry-sdk:1.49.0") implementation("io.opentelemetry:opentelemetry-exporter-otlp:1.49.0")

Initialize the SDK on startup and register a shutdown hook so buffered spans get flushed on exit:

import io.opentelemetry.api.common.Attributes; import io.opentelemetry.api.trace.Tracer; import io.opentelemetry.api.trace.Span; import io.opentelemetry.exporter.otlp.http.trace.OtlpHttpSpanExporter; import io.opentelemetry.sdk.OpenTelemetrySdk; import io.opentelemetry.sdk.resources.Resource; import io.opentelemetry.sdk.trace.SdkTracerProvider; import io.opentelemetry.sdk.trace.export.BatchSpanProcessor; import java.time.Duration; public class KubitTracing { public static OpenTelemetrySdk init() { String apiKey = System.getenv("KUBIT_API_KEY"); OtlpHttpSpanExporter exporter = OtlpHttpSpanExporter.builder() .setEndpoint("https://otel.kubit.ai/v1/traces") .addHeader("x-api-key", apiKey) .setTimeout(Duration.ofSeconds(10)) .build(); Resource resource = Resource.getDefault().merge(Resource.create( Attributes.builder() .put("service.name", "my-java-service") .put("service.version", "1.0.0") .put("deployment.environment", "production") .build() )); SdkTracerProvider tracerProvider = SdkTracerProvider.builder() .setResource(resource) .addSpanProcessor(BatchSpanProcessor.builder(exporter).build()) .build(); OpenTelemetrySdk sdk = OpenTelemetrySdk.builder() .setTracerProvider(tracerProvider) .build(); Runtime.getRuntime().addShutdownHook(new Thread(sdk::close)); return sdk; } public static void main(String[] args) { OpenTelemetrySdk sdk = init(); Tracer tracer = sdk.getTracer("my-java-service"); Span span = tracer.spanBuilder("chat.completion").startSpan(); try (var ignored = span.makeCurrent()) { span.setAttribute("gen_ai.system", "openai"); span.setAttribute("gen_ai.request.model", "gpt-4o"); // ... your LLM call ... } finally { span.end(); } } }

In Kotlin, the same code translates idiomatically: replace Span locals with val, swap try-with-resources for span.makeCurrent().use { … }, and wrap the shutdown hook with Runtime.getRuntime().addShutdownHook(Thread { sdk.close() }).

Spring Boot 3 + Micrometer Tracing

If you're using micrometer-tracing-bridge-otel, you don't need to build a TracerProvider yourself. Register the Kubit exporter as a SpanExporters bean and Spring's auto-config will wrap it in a BatchSpanProcessor and attach it to the autoconfigured SdkTracerProvider:

@Bean fun kubitSpanExporter(@Value("\${kubit.api-key}") apiKey: String): SpanExporters = SpanExporters.of(listOf( OtlpHttpSpanExporter.builder() .setEndpoint("https://otel.kubit.ai/v1/traces") .addHeader("x-api-key", apiKey) .build() ))

Sanity-check with curl

The smallest OTLP/HTTP JSON payload that will exercise credentials and connectivity end-to-end (a real client should send protobuf):

A successful response is 200 OK with {"partialSuccess":{}}.

curl -i -X POST https://otel.kubit.ai/v1/traces \ -H "Content-Type: application/json" \ -H "x-api-key: $KUBIT_API_KEY" \ -d '{ "resourceSpans": [{ "resource": { "attributes": [ {"key":"service.name","value":{"stringValue":"curl-smoke-test"}} ]}, "scopeSpans": [{ "scope": {"name":"manual"}, "spans": [{ "traceId":"5b8aa5a2d2c872e8321cf37308d69df2", "spanId":"051581bf3cb55c13", "name":"chat.completion", "kind":2, "startTimeUnixNano":"1700000000000000000", "endTimeUnixNano":"1700000001000000000", "attributes":[ {"key":"gen_ai.system","value":{"stringValue":"openai"}}, {"key":"gen_ai.request.model","value":{"stringValue":"gpt-4o"}} ] }] }] }]

Things to watch out for

  • Span filtering is server-side. The kubit-otel SDKs drop non-LLM and coordination spans before they ship. When sending directly, every span you create reaches the collector. If you're using a generic tracer across an entire app, scope what you forward to Kubit — a dedicated tracer, a span attribute filter, or a tail sampler will all work.

  • Watch the endpoint env vars. OTEL_EXPORTER_OTLP_TRACES_ENDPOINT and OTEL_EXPORTER_OTLP_ENDPOINT are process-wide — they'll redirect your Kubit exporter and any other OTel-based SDK in the same process. When you have more than one destination, always pass endpoint / setEndpoint(...) explicitly on the exporter.

  • Co-existing with another OTel pipeline. Add Kubit as an additional BatchSpanProcessor on the same TracerProvider — don't register two providers.