Harbor
Monitoring

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

URLhttps://api.harbor.ai/v1/traces
MethodPOST
AuthorizationBearer <your-api-key>
Content-Typeapplication/json or application/x-protobuf
Content-Encodinggzip, or omitted
Success200 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-agent

Note 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:

AttributePurpose
gen_ai.operation.namechat, execute_tool, invoke_agent, embeddings, retrieval
gen_ai.provider.nameopenai, anthropic, gcp.gemini, aws.bedrock, …
gen_ai.request.model / gen_ai.response.modelRequested and served model
gen_ai.input.messages / gen_ai.output.messagesPrompt and completion
gen_ai.usage.input_tokens / gen_ai.usage.output_tokensToken counts
gen_ai.tool.name / gen_ai.tool.call.arguments / gen_ai.tool.call.resultTool spans
gen_ai.agent.nameNames 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.

AttributeBecomes
gen_ai.conversation.idThe session. Reuse it across turns of one conversation
user.idThe end user
harbor.tagsTags, 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

On this page