OpenTelemetry
Send raw OTLP traces to Harbor from any language, framework, or collector.
Harbor's ingestion endpoint speaks standard OTLP over HTTP. Anything with an OpenTelemetry SDK can export to it directly — no Harbor library involved.
Use this when there is no Harbor SDK for your language, when a framework ships its own OTLP exporter you would rather point at Harbor, or when a collector already aggregates your traces and you want Harbor as one more destination.
On Node.js, the TypeScript SDK does everything on this page
for you and adds capture for session and user context. For agent frameworks, start with
Vercel AI SDK or Mastra.
Endpoint
| URL | https://api.harbor.ai/v1/traces |
| Method | POST |
| Authorization | Bearer <your-api-key> |
| Content-Type | application/json or application/x-protobuf |
| Content-Encoding | gzip, or omitted |
| Success | 200 with an empty body |
The body is a standard ExportTraceServiceRequest. There is no project header: the API key
identifies both the organization and the project.
Only the traces signal is ingested. Point metrics and logs at your existing observability backend — see running alongside another backend.
Verify with curl
One span, enough to confirm credentials and connectivity:
curl -i -X POST https://api.harbor.ai/v1/traces \
-H "Authorization: Bearer $HARBOR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"resourceSpans": [{
"resource": {
"attributes": [
{ "key": "service.name", "value": { "stringValue": "curl-test" } }
]
},
"scopeSpans": [{
"scope": { "name": "manual" },
"spans": [{
"traceId": "5b8aa5a2d2c872e8321cf37308d69df2",
"spanId": "051581bf3cb55c13",
"name": "chat gpt-5",
"kind": 3,
"startTimeUnixNano": "1760000000000000000",
"endTimeUnixNano": "1760000001000000000",
"attributes": [
{ "key": "gen_ai.operation.name", "value": { "stringValue": "chat" } },
{ "key": "gen_ai.provider.name", "value": { "stringValue": "openai" } },
{ "key": "gen_ai.request.model", "value": { "stringValue": "gpt-5" } }
]
}]
}]
}]
}'A 200 means the batch was accepted for processing. Trace ids must be 32 hex characters and
span ids 16, or the span is dropped server-side.
Configure an exporter
Every OpenTelemetry SDK reads the standard OTLP environment variables, which is the shortest path in any language:
export OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://api.harbor.ai/v1/traces
export OTEL_EXPORTER_OTLP_TRACES_HEADERS="Authorization=Bearer $HARBOR_API_KEY"
export OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
export OTEL_SERVICE_NAME=my-agentNote the endpoint variable is the full path including /v1/traces. The non-signal
OTEL_EXPORTER_OTLP_ENDPOINT takes a base URL instead and appends the path itself, which is
a common source of 404s.
In code:
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
import os
provider = TracerProvider(resource=Resource.create({"service.name": "my-agent"}))
provider.add_span_processor(
BatchSpanProcessor(
OTLPSpanExporter(
endpoint="https://api.harbor.ai/v1/traces",
headers={"Authorization": f"Bearer {os.environ['HARBOR_API_KEY']}"},
)
)
)What Harbor reads
Any span is stored, but the ones Harbor can score follow the GenAI semantic conventions. The attributes that matter most:
| Attribute | Purpose |
|---|---|
gen_ai.operation.name | chat, execute_tool, invoke_agent, embeddings, retrieval |
gen_ai.provider.name | openai, anthropic, gcp.gemini, aws.bedrock, … |
gen_ai.request.model / gen_ai.response.model | Requested and served model |
gen_ai.input.messages / gen_ai.output.messages | Prompt and completion |
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens | Token counts |
gen_ai.tool.name / gen_ai.tool.call.arguments / gen_ai.tool.call.result | Tool spans |
gen_ai.agent.name | Names the agent |
Messages use the GenAI parts format, JSON-encoded because OpenTelemetry attribute values must be scalars or arrays of scalars:
[
{ "role": "user", "parts": [{ "type": "text", "content": "Where is my order?" }] },
{
"role": "assistant",
"parts": [{ "type": "tool_call", "id": "call_1", "name": "lookup_order", "arguments": { "id": "A-1" } }]
}
]Harbor also understands the OpenInference and OpenLLMetry attribute names, so spans from those instrumentation families need no translation either.
Session, user, tags and metadata
These are what turn individual traces into sessions and let you filter them. The
TypeScript SDK sets them with
capture; without it, set them yourself on the root span of each trace.
| Attribute | Becomes |
|---|---|
gen_ai.conversation.id | The session. Reuse it across turns of one conversation |
user.id | The end user |
harbor.tags | Tags, as a string array or a JSON-encoded one |
harbor.metadata.<key> | One metadata entry per attribute |
Setting them on the root span alone is enough for the trace to be attributed; setting them on every span is harmless.
Running alongside another backend
A tracer provider can hold several span processors, so Harbor does not replace your existing observability. Add a second exporter and both receive every span:
service:
pipelines:
traces:
exporters: [otlphttp/harbor, datadog]The same applies in-process — register one batch processor per destination. Harbor ingests traces only, so keep metrics and logs pointed at your existing backend.
Troubleshooting
The header must be exactly Authorization: Bearer <key>. A bare key with no Bearer
prefix reads as missing. Otherwise the key is wrong or revoked — note that OTLP exporters
treat 401 as fatal and drop the batch rather than retrying.
The URL is not /v1/traces. This is usually OTEL_EXPORTER_OTLP_ENDPOINT versus
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT: the signal-specific variable takes the full path,
the generic one takes a base URL and appends /v1/traces itself.
Only gzip and identity are accepted. Any other Content-Encoding is refused rather than
accepted and silently dropped later.
A batch exceeded 32 MiB. Lower your exporter's max batch size — the default of 512 spans can be large when spans carry whole conversations.
Retryable, and exporters retry them for you. A 503 in particular means Harbor could not
verify your key right now, not that it is invalid — the batch is worth resending.
The exporter works and the conventions are missing. Check the raw attributes on the span in Harbor against the table above; instrumentation predating the GenAI conventions often emits vendor-specific names Harbor cannot map.
A batch processor buffers. Scripts, jobs and serverless handlers must force a flush or shut the provider down before exiting, or the last batch dies with the process.