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.comon 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
- Sign in to CloudZero and go to Settings > API Keys.
- Create a new API key.
- Under Scopes, open the Ai-Telemetry-Ingest category and select
ingest-otel-genai-v1. You can typeotelin the scope search box to find it. - 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-v1scope 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.
| Setting | Environment variable | Value |
|---|---|---|
| OTLP traces endpoint | OTEL_EXPORTER_OTLP_TRACES_ENDPOINT | https://telemetry.cloudzero.com/v2/telemetry/otel-genai |
| Authorization header | OTEL_EXPORTER_OTLP_TRACES_HEADERS | Authorization=<your-cloudzero-api-key> (the raw key from Step 1, with no Bearer prefix) |
| OTLP protocol | OTEL_EXPORTER_OTLP_TRACES_PROTOCOL | http/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/protobufUse the traces-specific variables (
..._TRACES_...) set to the complete/v2/telemetry/otel-genairoute exactly as shown. Do not use the genericOTEL_EXPORTER_OTLP_ENDPOINT. The OpenTelemetry SDK appends/v1/tracesto that value, which produces.../v2/telemetry/otel-genai/v1/tracesand 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
otlphttpexporter'straces_endpoint(notendpoint).traces_endpointsends the route verbatim, whereasendpointappends 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
| Attribute | Why |
|---|---|
gen_ai.request.model | The model invoked. Without a model, the span is dropped entirely. |
gen_ai.operation.name | Marks the span as a real inference call (for example, chat). |
gen_ai.provider.name | Provider attribution (anthropic, openai, and so on). If missing, the cost is attributed as unknown, which weakens cost breakdowns. |
gen_ai.response.model | The model that actually served the request, accounting for aliases and routing. |
gen_ai.usage.input_tokens / gen_ai.usage.output_tokens | The token counts cost is computed from. |
gen_ai.usage.cost | The request's cost in USD. CloudZero uses it directly when present. |
gen_ai.client.app | A clean, human-readable label for the calling application (for example, internal-copilot). |
gen_ai.client.user_agent | The calling client's user-agent string (for example, internal-copilot/1.4 (python/3.12)). |
Highly recommended
| Attribute | What it gives you |
|---|---|
user.id | The end user or tenant behind the call. |
user.email | Human-readable per-user attribution. |
user.name | Display name for the user. |
gen_ai.usage.cache_read.input_tokens / gen_ai.usage.cache_creation.input_tokens | Cache-token breakdown. |
gen_ai.response.finish_reasons | Why the response ended (for example, ["stop"]). |
gen_ai.response.id | The provider's response ID, to trace back to your own logs. |
gen_ai.conversation.id | Groups multi-turn calls into one conversation. |
gen_ai.request.max_tokens / gen_ai.request.temperature | Request 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.
| Deprecated | Current |
|---|---|
gen_ai.system | gen_ai.provider.name |
gen_ai.usage.prompt_tokens | gen_ai.usage.input_tokens |
gen_ai.usage.completion_tokens | gen_ai.usage.output_tokens |
enduser.id | user.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 setsgen_ai.*on a span before you onboard it. If it does not, re-emit a compliantgen_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:
| Attribute | What it is for |
|---|---|
service.name | How 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.environment | Environment qualifier (prod, staging). |
cloud.account.id | Joins 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.
| Attribute | Example |
|---|---|
cz.request.activity | Code Comprehension & Docs |
cz.context.project | billing-service |
cz.context.department | engineering |
cz.context.topic | code review |
cz.context.task_summary | explain payment retry flow |
cz.context.task_id | ENG-4521 |
cz.context.vcs.repository.url.full | https://github.com/acme/billing-service |
cz.context.vcs.ref.head.name | feat/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, andtopicbecome aggregation dimensions, so inconsistent casing or phrasing fragments your breakdowns. Standardize them in your code.task_summaryis the exception: it is for human inspection, so free-form text is fine.
Step 5: Send traffic and verify
- Deploy the configuration so your application or Collector exports with the new endpoint, header, and protocol.
- Confirm real LLM traffic is flowing through the instrumented code path.
- 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.
- Wrong endpoint variable (appends
/v1/traces). The most common cause of 403 or 404. UseOTEL_EXPORTER_OTLP_TRACES_ENDPOINTset to.../v2/telemetry/otel-genai. Do not use the genericOTEL_EXPORTER_OTLP_ENDPOINT. - Wrong protocol.
OTEL_EXPORTER_OTLP_TRACES_PROTOCOLmust behttp/protobuf. gRPC fails silently. Bearerprefix on the API key. The Authorization header value must be the raw key (for example,Authorization=<your-cloudzero-api-key>), notAuthorization=Bearer <your-cloudzero-api-key>.- Missing scope. A 403 on the smoke test means the key lacks
ai-telemetry-ingest:ingest-otel-genai-v1. Re-check Step 1. - 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.modelplus eithergen_ai.operation.nameor token usage (see Step 3). - 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> -fLook for messages from the OTLP exporter around the time you sent traffic.
Have questions or feedback? Reach out to your account manager.
Updated 15 days ago

