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-httpimport 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-nodeimport { 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/sdkpackage 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_idon 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
metricsandlogsoff. - 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_ENDPOINTinstead of the config file, also setOTEL_EXPORTER_OTLP_HEADERSwith the three headers. The endpoint alone gets a 401.