Sending data with OpenTelemetry

ObserveKit accepts traces, metrics, and logs over the OpenTelemetry Protocol (OTLP). Any app — running on Kubernetes, Docker, an EC2 / cloud VM, an Azure WebApp, on-prem, anywhere with outbound HTTP — can push data without an ObserveKit agent.

You don't construct request bodies by hand

A common first question: *"what JSON do I POST to /v1/traces?"*

You don't. Your app's OpenTelemetry SDK handles the body for you. You import the SDK in your code, configure the endpoint and API-key header (env vars below), and call tracer.startSpan(...) / meter.add(...) / logger.info(...) like normal. The SDK batches those calls, serializes them into the standard OTLP wire format (protobuf-over-HTTP, or JSON-over-HTTP per the OTel spec), and POSTs them to ObserveKit on a background timer.

Every language SDK (Java, Node, Python, .NET, Go, Ruby, PHP, Rust) implements the same OTLP spec, so the body shape is identical across languages — you just pick the SDK that matches your stack.

If you really want to see the wire shape (for debugging, or to write your own exporter from scratch), the OTLP spec is at opentelemetry.io/docs/specs/otlp. Almost no one needs to read it.

OTLP endpoints

All three signal types use the same base URL with different paths:

SignalHTTP endpoint
Traceshttps://observekit-api.expeed.com/v1/traces
Metricshttps://observekit-api.expeed.com/v1/metrics
Logshttps://observekit-api.expeed.com/v1/logs

If your app runs in a Kubernetes cluster that already has the ObserveKit agent installed, you can also point at the agent on localhost:4317 (gRPC) or localhost:4318 (HTTP) and let the agent forward to the server. Direct-to-server is fine for any environment; agent-relay is just a convenience for in-cluster apps that want to avoid setting outbound DNS.

Compression & body size

Enable gzip (or zstd) on your exporter — ObserveKit decompresses both, and it meaningfully cuts egress. Each request body is capped at 10 MiB (compressed *and* decompressed); if you batch aggressively and get a 400, lower the SDK's max export batch size.

Authentication

Every OTLP request must carry an ObserveKit API key. Pass it as either:

X-API-Key: <your-key>

or

Authorization: Bearer <your-key>

API keys are issued per source. Each app or service typically gets its own source so you can revoke / rotate / scope access independently. See "Provisioning a service source" below.

Per-language SDK setup

The OpenTelemetry SDK config is identical in shape across languages — set the OTLP endpoint, the API-key header, and the service name. The official OpenTelemetry SDK docs cover each language in detail.

Java / Spring Boot (auto-instrumentation agent)

-javaagent:/path/to/opentelemetry-javaagent.jar
-Dotel.exporter.otlp.endpoint=https://observekit-api.expeed.com
-Dotel.exporter.otlp.headers=X-API-Key=<your-key>
-Dotel.service.name=cargospot-api
-Dotel.resource.attributes=deployment.environment=prod

Java / Micronaut (compile-time modules, no javaagent)

Micronaut apps usually use the first-party micronaut-tracing-opentelemetry-* modules rather than the javaagent. Two things differ from the agent flow and silently break telemetry if missed:

  • Config uses the OpenTelemetry autoconfigure schema. In application.yml, you must *select* the exporter and pass the key as a single header string — a nested headers: map, or a missing otel.traces.exporter, ships nothing (no error):

```yaml

otel:

traces:

exporter: otlp

logs:

exporter: otlp

exporter:

otlp:

endpoint: https://observekit-api.expeed.com

headers: "X-API-Key=${OBSERVEKIT_API_KEY}"

compression: gzip

```

  • Logs need the appender installed. Without the javaagent, the Logback OpenTelemetryAppender has no SDK to emit through unless you install it once at startup — otherwise records buffer and drop (numLogsCapturedBeforeOtelInstall … too small):

```java

// in an ApplicationEventListener<ServerStartupEvent>, injecting the OpenTelemetry bean

OpenTelemetryAppender.install(openTelemetry);

```

The full Micronaut walkthrough (dependencies, sampling, log correlation) lives in the observekit Claude Code plugin's Micronaut reference.

Node.js / TypeScript

OTEL_EXPORTER_OTLP_ENDPOINT=https://observekit-api.expeed.com
OTEL_EXPORTER_OTLP_HEADERS=X-API-Key=<your-key>
OTEL_SERVICE_NAME=billing-api
node --require @opentelemetry/auto-instrumentations-node/register app.js

Python

OTEL_EXPORTER_OTLP_ENDPOINT=https://observekit-api.expeed.com
OTEL_EXPORTER_OTLP_HEADERS=X-API-Key=<your-key>
OTEL_SERVICE_NAME=ingest-worker
opentelemetry-instrument python app.py

.NET

OTEL_EXPORTER_OTLP_ENDPOINT=https://observekit-api.expeed.com
OTEL_EXPORTER_OTLP_HEADERS=X-API-Key=<your-key>
OTEL_SERVICE_NAME=accounting-service
dotnet AccountingService.dll

Go

Go has no auto-instrumentation agent — wire the SDK explicitly. A minimal HTTP exporter setup:

exporter, _ := otlptracehttp.New(ctx,
    otlptracehttp.WithEndpoint("observekit-api.expeed.com"),
    otlptracehttp.WithHeaders(map[string]string{"X-API-Key": "<your-key>"}),
)
provider := sdktrace.NewTracerProvider(
    sdktrace.WithBatcher(exporter),
    sdktrace.WithResource(resource.NewSchemaless(
        attribute.String("service.name", "search-service"),
    )),
)
otel.SetTracerProvider(provider)

Provisioning a service source

For an OTel-only app, the operator creates a service-type source in ObserveKit's Sources page. Service sources don't run an agent — they exist to issue an API key and group OTLP data by source.

  1. Open Sources → Add Source → pick type service.
  2. Enter a name (typically the service name, e.g. cargospot-api).
  3. Save. The install dialog returns the API key + the OTLP endpoints the app should target. Hand both to the developer.
  4. Once the app starts pushing OTLP data, the source's badge on the Sources list will flip from no data yetOTLP receiving within a few minutes.

If the operator doesn't want to create a per-app source, an existing kubernetes / docker source's API key also works for OTLP push. Filtering then happens by service.name in the Traces, Metrics, and Logs views instead of by source.

What you DO see for a service source

  • Traces — every span, filterable by service.name.
  • Services — auto-derived service list from incoming traces.
  • Service map — auto-derived dependency topology.
  • Metrics (Explorer) — every metric the app pushes, plottable ad-hoc or as the basis for an alert rule.
  • Logs — every log line the app pushes via OTLP.
  • Cost Analytics — ingestion bytes per source, including service sources.
  • Alerts (user-input templates)composite, simple_anomaly, metric_change_percent, log_pattern_match, high_error_rate all fire against pushed data when their conditions match.

What you DO NOT see for a service source

These features are built around agent-collected data and are explicitly NOT collected for service sources:

  • Infrastructure topology (the Infrastructure page + the Infrastructure card on the source detail page) — no pods, containers, or nodes are collected because no agent runs.
  • Metrics Analytics tab (KPI tiles, Node Health, Pod / Container Analytics) — built from agent-emitted metrics like kube_pod_status_phase and container_cpu_usage_percent.
  • Certificates card (the Phase C TLS scanner) — runs as part of the K8s agent.
  • K8s-specific alert templates (high_cpu, pod_pending, pod_crashloop, kube_deployment_replicas, kube_pod_not_ready, node_not_ready, deployment_zero_available, low_disk, high_memory, certificate_expiring) — these reference metric names only the agent emits. The rule-creation picker hides them when the active source is a service source; selecting them while the picker shows "All sources" surfaces a ⚠ requires kubernetes warning.

Pages where these features would normally render show a "Not collected for this source type" banner so it's clear ObserveKit is honest about the gap rather than presenting an empty dashboard.

Environment-specific guidance

Azure WebApps

Azure App Service auto-enables Application Insights. To send the same data to ObserveKit, use the Azure Monitor OpenTelemetry Distro (Microsoft's OTel-spec-compatible distribution) or the plain OTel SDK. Set the OTLP env vars in App Service → Configuration → Application settings, then restart. No App Service redeploy needed.

EC2, GCP Compute, on-prem Linux VMs

Same as any other environment — instrument the app with OTel and point at ObserveKit's /v1/* endpoints. Host-level metrics (CPU, memory, disk usage of the VM itself) are NOT collected on bare VMs without Kubernetes or Docker — instrument the app with OTel for its own signals.

Docker (without K8s)

If the app runs in Docker on a host that already has the ObserveKit Docker agent, both paths work:

  • Agent collects host + container metrics; ObserveKit aggregates them as the existing Docker source.
  • The app's own request metrics / business counters / traces are NOT collected by the Docker agent — instrument with OTel as above to surface them.

Kubernetes

The cleanest pattern is a per-cluster K8s source for infra signals (pods, nodes, certificates) plus a per-app service source for app signals. Or instrument the app with OTel pointing at the in-cluster agent and let the agent forward to the server using the cluster's existing API key.

Troubleshooting

"My OTLP push returns 401 Unauthorized" — the API key isn't being sent. Check OTEL_EXPORTER_OTLP_HEADERS (case matters; some SDKs use OTEL_EXPORTER_OTLP_TRACES_HEADERS per-signal-type). Also verify the key matches a source on this server — keys are env-specific.

"Source shows 'no data yet' even though my app started" — the SDK is buffering. Most OTel SDKs batch + flush every 5 seconds. Generate some traffic and wait one batch interval.

"Metrics arrive but `service.name` shows up as `unknown_service`" — set OTEL_SERVICE_NAME (or service.name resource attribute). Many SDKs default to unknown_service when neither is configured.

"Service source's badge stuck on 'OTLP idle'" — the SDK has stopped pushing. Check the app logs for OTLP exporter errors; check the agent / network path can reach observekit-api.expeed.com.

"OTLP push returns 400 Bad Request / unable to parse" — the body couldn't be decoded. Point the exporter at the base URL (not a hard-coded /v1/... path), use http/protobuf for the HTTPS endpoint (port 4318; 4317 is gRPC), keep batches under the 10 MiB cap, and note gzip/zstd are both accepted.

"Traces work but logs don't (Micronaut / no javaagent)" — the Logback appender was never handed the SDK. Call OpenTelemetryAppender.install(openTelemetry) at startup and set otel.logs.exporter: otlp.