Skip to content
Judgment Labs
Esc
↑↓navigate↵open⌘Jpreview
On this page

Send OTLP directly

Export OpenTelemetry traces to Judgment over OTLP/HTTP without the judgeval SDK.

This fits a service in Go or Java, an OpenTelemetry Collector you already run, or a tool like OpenClaw that ships its own exporter. Any OTLP/HTTP exporter works.

Connection details

Setting Value
Endpoint https://api.judgmentlabs.ai/otel/v1/traces (POST)
Authorization Bearer <JUDGMENT_API_KEY>
X-Organization-Id your organization ID. Needed for a personal API key; ignored for an ingest API key
X-Project-Id the project’s ID, not its name
Body OTLP protobuf by default. Send Content-Type: application/json for OTLP/JSON
Compression Content-Encoding: gzip is supported

Use an ingest API key for JUDGMENT_API_KEY: it can only send traces, so it is safer than a personal API key, which also works. An ingest API key already belongs to one organization, so the server ignores X-Organization-Id for it. See Authentication and projects.

The main difference from the SDK is the project ID. The SDK turns project_name into an ID and creates the project if it’s missing. Raw OTLP does neither. Create the project first (running the SDK once with the name works), then copy the ID from the project’s URL in the app, or press Ctrl-K and search for the project ID. That ID goes in X-Project-Id.

The examples below read the ID from JUDGMENT_PROJECT_ID. That’s your own variable for your exporter config; the SDK doesn’t read it.

pip install opentelemetry-sdk opentelemetry-exporter-otlp-proto-http
import os

from opentelemetry import trace
from opentelemetry.exporter.otlp.proto.http.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor

resource = Resource.create(
    {"service.name": "lunch-order-agent", "deployment.environment.name": "production"}
)
provider = TracerProvider(resource=resource)
provider.add_span_processor(
    BatchSpanProcessor(
        OTLPSpanExporter(
            endpoint="https://api.judgmentlabs.ai/otel/v1/traces",
            headers={
                "Authorization": f"Bearer {os.environ['JUDGMENT_API_KEY']}",
                "X-Organization-Id": os.environ["JUDGMENT_ORG_ID"],
                "X-Project-Id": os.environ["JUDGMENT_PROJECT_ID"],
            },
        )
    )
)
trace.set_tracer_provider(provider)

tracer = trace.get_tracer("lunch-order-agent")
with tracer.start_as_current_span("handle_request"):
    pass

provider.force_flush()
npm install @opentelemetry/api @opentelemetry/exporter-trace-otlp-http @opentelemetry/resources @opentelemetry/sdk-trace-node
import { trace } from "@opentelemetry/api";
import { OTLPTraceExporter } from "@opentelemetry/exporter-trace-otlp-http";
import { resourceFromAttributes } from "@opentelemetry/resources";
import { BatchSpanProcessor, NodeTracerProvider } from "@opentelemetry/sdk-trace-node";

const exporter = new OTLPTraceExporter({
  url: "https://api.judgmentlabs.ai/otel/v1/traces",
  headers: {
    Authorization: `Bearer ${process.env.JUDGMENT_API_KEY}`,
    "X-Organization-Id": process.env.JUDGMENT_ORG_ID ?? "",
    "X-Project-Id": process.env.JUDGMENT_PROJECT_ID ?? "",
  },
});

const provider = new NodeTracerProvider({
  resource: resourceFromAttributes({
    "service.name": "lunch-order-agent",
    "deployment.environment.name": "production",
  }),
  spanProcessors: [new BatchSpanProcessor(exporter)],
});
provider.register();

const span = trace.getTracer("lunch-order-agent").startSpan("handle_request");
span.end();
await provider.forceFlush();
go get go.opentelemetry.io/otel go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp go.opentelemetry.io/otel/sdk
package main

import (
	"context"
	"os"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/attribute"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
)

func main() {
	ctx := context.Background()

	exporter, err := otlptracehttp.New(
		ctx,
		otlptracehttp.WithEndpointURL("https://api.judgmentlabs.ai/otel/v1/traces"),
		otlptracehttp.WithHeaders(map[string]string{
			"Authorization":     "Bearer " + os.Getenv("JUDGMENT_API_KEY"),
			"X-Organization-Id": os.Getenv("JUDGMENT_ORG_ID"),
			"X-Project-Id":      os.Getenv("JUDGMENT_PROJECT_ID"),
		}),
	)
	if err != nil {
		panic(err)
	}

	provider := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(resource.NewWithAttributes(
			"",
			attribute.String("service.name", "lunch-order-agent"),
			attribute.String("deployment.environment.name", "production"),
		)),
	)
	defer func() {
		if err := provider.Shutdown(ctx); err != nil {
			panic(err)
		}
	}()

	otel.SetTracerProvider(provider)
	tracer := otel.Tracer("lunch-order-agent")

	_, span := tracer.Start(ctx, "handle_request")
	span.End()
}

deployment.environment.name on the resource sets the environment the trace lands in. Leave it out and the trace shows as No environment.

From an OpenTelemetry Collector

If your services already send to a Collector, add Judgment as one more exporter instead of changing application code:

receivers:
  otlp:
    protocols: { http: {}, grpc: {} }

exporters:
  otlphttp/judgment:
    traces_endpoint: https://api.judgmentlabs.ai/otel/v1/traces
    headers:
      Authorization: "Bearer ${env:JUDGMENT_API_KEY}"
      X-Organization-Id: "${env:JUDGMENT_ORG_ID}"
      X-Project-Id: "${env:JUDGMENT_PROJECT_ID}"

service:
  pipelines:
    traces:
      receivers: [otlp]
      exporters: [otlphttp/judgment]

What Judgment expects in the spans

  • A trace is evaluated once its root span (no parent) arrives with a nonzero duration. Send child spans before or with the root.
  • The session comes from judgment.session_id on the root span only.
  • A span whose attributes exceed 8 MiB fails the whole request with 413. Request bodies are capped at 50 MB, and a gzip body over 20 MB after decompression fails with 400. Once your organization is over its usage limit, requests fail with 402.

The full contract, including every status code, is in Ingest contract. Attribute names Judgment reads are in Attribute keys and mappers.

OpenClaw

OpenClaw’s diagnostics-otel plugin exports OpenClaw Gateway traces over OTLP/HTTP. Install it with openclaw plugins install clawhub:@openclaw/diagnostics-otel, then point it at Judgment in your OpenClaw config:

{
  plugins: { allow: ["diagnostics-otel"], entries: { "diagnostics-otel": { enabled: true } } },
  diagnostics: {
    enabled: true,
    otel: {
      enabled: true,
      endpoint: "https://api.judgmentlabs.ai/otel",
      protocol: "http/protobuf",
      serviceName: "openclaw-gateway",
      headers: {
        Authorization: "Bearer <JUDGMENT_API_KEY>",
        "X-Organization-Id": "<JUDGMENT_ORG_ID>",
        "X-Project-Id": "<JUDGMENT_PROJECT_ID>",
      },
      traces: true, metrics: false, logs: false,
      captureContent: true,
    },
  },
}
  • Judgment ingests traces only, so leave metrics and logs off.
  • Without captureContent, OpenClaw exports model, token counts, and timing but no prompt or response text, and judges have nothing to read. Turn it on only if your data policy allows sending that text.
  • If you configure OpenClaw through OTEL_EXPORTER_OTLP_ENDPOINT instead of the config file, also set OTEL_EXPORTER_OTLP_HEADERS with the three headers. The endpoint alone gets a 401.

Was this page helpful?