Connecting with OpenTelemetry

Send AI inference telemetry to CloudZero from an application or proxy you built yourself: anything that can emit OpenTelemetry traces. Point your OTLP exporter at CloudZero and your model usage and token counts flow into a unified view of all your cloud and AI spend, organized by the Dimensions that matter to your business.

This is the generic, bring-your-own-OpenTelemetry path. If you run LiteLLM or Bifrost, use the dedicated guide for that gateway instead.

CloudZero reads the standard OpenTelemetry GenAI semantic conventions (gen_ai.*). There is no CloudZero-specific span format to learn. If your code already emits GenAI spans through an OpenTelemetry SDK, you point the exporter at CloudZero and you are done. If you hand-instrument, you set a small set of gen_ai.* attributes described below.

ℹ️

This integration does not use the Create Connection flow in Settings > Cloud Connections. You connect it by creating a CloudZero API key and pointing telemetry at CloudZero, as described below. Confirm it is working in AI Signals once data flows.

What you need

  • A CloudZero user with permission to create API keys
  • An application or proxy that makes LLM calls and can emit OpenTelemetry traces
  • An OpenTelemetry SDK, or the OpenTelemetry Collector, in your stack. Most GenAI instrumentation libraries (OpenLLMetry, OpenInference, vendor OpenTelemetry SDKs) set the required gen_ai.* attributes automatically.
  • Outbound HTTPS connectivity from your host to telemetry.cloudzero.com on port 443
ℹ️

Point this integration at a workload with real, recurring LLM traffic. A production service is ideal. Sparse or ad hoc activity makes it harder to confirm data is flowing and harder to interpret totals. If you have several candidate workloads, start with the highest-volume one.

How it works

Your application or Collector exports OpenTelemetry traces over OTLP/HTTP to CloudZero's ingest endpoint. CloudZero turns each qualifying GenAI span into one cost-and-usage event by reading the standard gen_ai.* attributes.

This route displays exactly the OpenTelemetry metadata your spans send. Nothing is added, inferred, or reshaped. Whatever appears in CloudZero is exactly what your telemetry carried, including any business context you set (see Step 4).

Spans that do not qualify are silently ignored. You still receive an HTTP 202 response, because the ingest is fail-open and never blocks your application.

Step 1: Create a CloudZero API key

  1. Sign in to CloudZero and go to Settings > API Keys.
  2. Create a new API key.
  3. Under Scopes, open the Ai-Telemetry-Ingest category and select ingest-otel-genai-v1. You can type otel in the scope search box to find it.
  4. Copy the key value. You set it as an environment variable in Step 2. Store it securely and treat it like any other secret.
ℹ️

Scope enforcement is per route. The ai-telemetry-ingest:ingest-otel-genai-v1 scope authorizes the generic OpenTelemetry GenAI ingest endpoint only. If a request later returns 403, the key is missing this scope.

Step 2: Point your OTLP exporter at CloudZero

Configure CloudZero as your OTLP traces destination. Set these three variables on the process that exports your spans, which is either your application or your OpenTelemetry Collector.

SettingEnvironment variableValue
OTLP traces endpointOTEL_EXPORTER_OTLP_TRACES_ENDPOINThttps://telemetry.cloudzero.com/v2/telemetry/otel-genai
Authorization headerOTEL_EXPORTER_OTLP_TRACES_HEADERSAuthorization=<your-cloudzero-api-key> (the raw key from Step 1, with no Bearer prefix)
OTLP protocolOTEL_EXPORTER_OTLP_TRACES_PROTOCOLhttp/protobuf
OTEL_EXPORTER_OTLP_TRACES_ENDPOINT=https://telemetry.cloudzero.com/v2/telemetry/otel-genai
OTEL_EXPORTER_OTLP_TRACES_HEADERS=Authorization=<your-cloudzero-api-key>
OTEL_EXPORTER_OTLP_TRACES_PROTOCOL=http/protobuf
⚠️

Use the traces-specific variables (..._TRACES_...) set to the complete /v2/telemetry/otel-genai route exactly as shown. Do not use the generic OTEL_EXPORTER_OTLP_ENDPOINT. The OpenTelemetry SDK appends /v1/traces to that value, which produces .../v2/telemetry/otel-genai/v1/traces and fails with a 403 or 404.

⚠️

The protocol must be http/protobuf. CloudZero's ingest does not accept gRPC. A gRPC exporter fails silently.

If you export through an OpenTelemetry Collector, point an otlphttp exporter at the same endpoint:

exporters:
  otlphttp/cloudzero:
    # Use traces_endpoint (the full route, sent verbatim), NOT endpoint.
    # The otlphttp exporter appends /v1/traces to `endpoint`, which would
    # produce .../v2/telemetry/otel-genai/v1/traces and fail with 403 or 404.
    traces_endpoint: https://telemetry.cloudzero.com/v2/telemetry/otel-genai
    headers:
      Authorization: <your-cloudzero-api-key>

service:
  pipelines:
    traces:
      exporters: [otlphttp/cloudzero]
⚠️

Use the otlphttp exporter's traces_endpoint (not endpoint). traces_endpoint sends the route verbatim, whereas endpoint appends the signal path (/v1/traces) and misses the v2 route, returning a 403 or 404. If you route GenAI spans to CloudZero through a shared Collector, give that pipeline its own exporter and confirm the final URL it builds.

Step 3: Emit the right span attributes

If you use a standard OpenTelemetry GenAI instrumentation library, it sets most of these for you, and you can skip to Step 4. Read this step if you hand-instrument or post-process spans.

CloudZero reads the standard OpenTelemetry GenAI attributes. At minimum, a span must carry gen_ai.request.model plus either gen_ai.operation.name or token usage to be counted. A span missing the rest is still accepted, but it does not give you usable cost or attribution, so send the full set below.

Required

AttributeWhy
gen_ai.request.modelThe model invoked. Without a model, the span is dropped entirely.
gen_ai.operation.nameMarks the span as a real inference call (for example, chat).
gen_ai.provider.nameProvider attribution (anthropic, openai, and so on). If missing, the cost is attributed as unknown, which weakens cost breakdowns.
gen_ai.response.modelThe model that actually served the request, accounting for aliases and routing.
gen_ai.usage.input_tokens / gen_ai.usage.output_tokensThe token counts cost is computed from.
gen_ai.usage.costThe request's cost in USD. CloudZero uses it directly when present.
gen_ai.client.appA clean, human-readable label for the calling application (for example, internal-copilot).
gen_ai.client.user_agentThe calling client's user-agent string (for example, internal-copilot/1.4 (python/3.12)).

Highly recommended

AttributeWhat it gives you
user.idThe end user or tenant behind the call.
user.emailHuman-readable per-user attribution.
user.nameDisplay name for the user.
gen_ai.usage.cache_read.input_tokens / gen_ai.usage.cache_creation.input_tokensCache-token breakdown.
gen_ai.response.finish_reasonsWhy the response ended (for example, ["stop"]).
gen_ai.response.idThe provider's response ID, to trace back to your own logs.
gen_ai.conversation.idGroups multi-turn calls into one conversation.
gen_ai.request.max_tokens / gen_ai.request.temperatureRequest parameters, for usage analysis.

Also read when present

Any standard gen_ai.* attribute your sender emits is understood. CloudZero maps the attributes relevant to cost and usage, including gen_ai.request.top_p and top_k, the gen_ai.agent.* and gen_ai.tool.* subtrees, error.type, and http.response.status_code, and ignores the rest.

Deprecated names still accepted

Senders are inconsistent, so older attribute names are aliased to the current ones. If both are present, the current name wins.

DeprecatedCurrent
gen_ai.systemgen_ai.provider.name
gen_ai.usage.prompt_tokensgen_ai.usage.input_tokens
gen_ai.usage.completion_tokensgen_ai.usage.output_tokens
enduser.iduser.id
⚠️

Your telemetry must follow the OpenTelemetry GenAI conventions. Spans that do not set the gen_ai.* attributes are not ingested. Some tools emit their own telemetry namespace rather than the GenAI conventions and cannot be ingested on this route as-is. Confirm your emitter sets gen_ai.* on a span before you onboard it. If it does not, re-emit a compliant gen_ai.* span yourself.

Step 4: Add business context

gen_ai.* attributes give you per-model, per-provider cost. Adding your own business context turns that into business-aligned dimensions: what kind of work the call did, for which team and project, against which repository and ticket. There are two places to set context.

Resource attributes (set once, at startup)

Set these on the tracer provider so they ride every span:

AttributeWhat it is for
service.nameHow CloudZero distinguishes your services. Two processes that share this value report as one service. Set it via OTEL_SERVICE_NAME or the SDK Resource.
deployment.environmentEnvironment qualifier (prod, staging).
cloud.account.idJoins your LLM telemetry to the matching cloud billing account.

Any other resource attribute you set (for example, team or cost_center) is carried through as context, so the resource is the right place for stable, process-wide dimensions.

Business enrichment (set per span)

These are the dimensions you slice cost by in CloudZero. Set them on each inference span, alongside the gen_ai.* attributes.

AttributeExample
cz.request.activityCode Comprehension & Docs
cz.context.projectbilling-service
cz.context.departmentengineering
cz.context.topiccode review
cz.context.task_summaryexplain payment retry flow
cz.context.task_idENG-4521
cz.context.vcs.repository.url.fullhttps://github.com/acme/billing-service
cz.context.vcs.ref.head.namefeat/ENG-4521-auth-refactor

cz.request.activity classifies the request itself, using a fixed CloudZero taxonomy. cz.context.* are your own business dimensions, free text you control. Note that topic is your free-form subject label for the call and is not the same as activity, which is a fixed category.

task_id is the ticket or work-item key the call relates to. project and the vcs.* fields are different things and can diverge: project is a logical grouping you define, while vcs.repository.url.full and vcs.ref.head.name are the actual git repository and branch. One project can span several repositories, and one repository can hold several projects.

ℹ️

Keep dimension values consistent. project, department, and topic become aggregation dimensions, so inconsistent casing or phrasing fragments your breakdowns. Standardize them in your code. task_summary is the exception: it is for human inspection, so free-form text is fine.

Step 5: Send traffic and verify

  1. Deploy the configuration so your application or Collector exports with the new endpoint, header, and protocol.
  2. Confirm real LLM traffic is flowing through the instrumented code path.
  3. In CloudZero, open AI Signals and watch for live usage from your service.name. Live usage appears within a few minutes of activity.

Smoke-test auth and routing

Before you wait on real traffic, confirm your key and route work. An empty POST returns HTTP 202:

curl -i -X POST \
  "https://telemetry.cloudzero.com/v2/telemetry/otel-genai" \
  -H "Authorization: <your-cloudzero-api-key>" \
  -H "Content-Type: application/x-protobuf" \
  --data-binary $'\x00'

Expected response:

HTTP/2 202
content-type: application/json

{"status":"accepted"}

A 202 confirms auth, scope, and routing are good. A 403 means the key is missing the ai-telemetry-ingest:ingest-otel-genai-v1 scope. Re-check Step 1.

⚠️

A 202 does not confirm your spans landed. The ingest is fail-open: it returns 202 even for an empty body or a span that does not qualify. The smoke test proves your key and route work. AI Signals proves your real spans are being ingested. If the smoke test passes but nothing appears, your spans are likely missing a required attribute. Re-check Step 3.

What to expect

Once configured, model usage and token counts appear in AI Signals within a few minutes of activity, showing model, provider, token counts, and timing. Reconciled costs, matched against your provider billing, settle within about 24 hours.

From there, your AI costs work like any other cost data in CloudZero. You can explore them in the Explorer, organize them by team or product using Dimensions, track trends in Dashboards, set Budgets, or ask questions in the AI Hub.

Data privacy

CloudZero never reads or stores your prompt or response content on this route. The ingest reads only metadata: model, provider, token counts, and timing. The content attributes (gen_ai.input.messages, gen_ai.output.messages, gen_ai.system_instructions) are never read, and there is no place for message bodies in CloudZero's schema, so content cannot flow through even if your spans carry it.

We recommend turning content off at the source. No CloudZero feature on this route uses message content, so there is no reason to send it. Most GenAI instrumentation libraries have a one-line setting to omit message bodies from spans. You do not have to do this to onboard: if your spans still carry content, CloudZero discards it and your cost and usage reporting is unaffected.

The one identifying field this guide recommends is user.email, for per-user attribution. You choose whether to send it. When you do, CloudZero captures it for user-level breakdowns but treats it as identifying and does not surface it as a general-purpose grouping dimension. To attribute by an opaque key instead, send user.id.

Troubleshooting

If data is not appearing in AI Signals a few minutes after confirmed traffic, work through these in order.

  1. Wrong endpoint variable (appends /v1/traces). The most common cause of 403 or 404. Use OTEL_EXPORTER_OTLP_TRACES_ENDPOINT set to .../v2/telemetry/otel-genai. Do not use the generic OTEL_EXPORTER_OTLP_ENDPOINT.
  2. Wrong protocol. OTEL_EXPORTER_OTLP_TRACES_PROTOCOL must be http/protobuf. gRPC fails silently.
  3. Bearer prefix on the API key. The Authorization header value must be the raw key (for example, Authorization=<your-cloudzero-api-key>), not Authorization=Bearer <your-cloudzero-api-key>.
  4. Missing scope. A 403 on the smoke test means the key lacks ai-telemetry-ingest:ingest-otel-genai-v1. Re-check Step 1.
  5. Spans missing a required attribute. The smoke test passes (202) but nothing appears in AI Signals. Your spans are being accepted and dropped as non-qualifying. Confirm each inference span carries gen_ai.request.model plus either gen_ai.operation.name or token usage (see Step 3).
  6. Configuration not redeployed. Exporter settings are read at process startup. Restart the application or Collector after any change.

If the configuration looks correct and data still is not flowing, tail the logs of whatever exports your spans (your application or your OpenTelemetry Collector). OTLP exporter failures such as network errors, auth rejections, and bad endpoints surface there.

# Application or Collector container
docker logs -f <container>

# Kubernetes
kubectl logs -f <pod>

# Systemd or bare process
journalctl -u <service> -f

Look for messages from the OTLP exporter around the time you sent traffic.

ℹ️

Have questions or feedback? Reach out to your account manager.


Did this page help you?