Skip to content
skyl

Spans and metrics

The GenAI semantic convention names skyl emits.

The instrumentation implements the OpenTelemetry GenAI semantic conventions, so model traffic looks the same whichever provider served it — which is the point of the library, extended to your observability stack.

Metrics#

NameMeaning
gen_ai.client.token.usageTokens consumed, split by gen_ai.token.type (input / output)
gen_ai.client.operation.durationHow long an operation took

Attributes#

AttributeValue
gen_ai.operation.namecomplete, stream, stream_end, or models
gen_ai.provider.nameThe adapter that was called
gen_ai.request.modelThe model that was asked for
gen_ai.response.modelThe model that actually answered
gen_ai.response.idThe provider's response identifier
gen_ai.response.finish_reasonsWhy generation ended
gen_ai.usage.input_tokensInput tokens, cache included
gen_ai.usage.output_tokensOutput tokens
gen_ai.request.temperatureWhen set
gen_ai.request.top_pWhen set
gen_ai.request.max_tokensWhen set
gen_ai.token.typeinput or output, on the usage metric
error.typeOn a failed operation

Caveats

  • Group by gen_ai.response.model, not gen_ai.request.model. Aliases resolve to dated snapshots, so grouping by the request merges two models with different pricing into one line.
  • No prompt or completion content is recorded, deliberately.
  • Names are spelled out in the package rather than taken from a semconv module: the GenAI conventions are still in development, and pinning a semconv module would tie skyl's release cadence to theirs.
  • ScopeName is github.com/BAGOMBEKA-JOB-DEV/skyl/otel.

Usage#

Token spend by model

sum by (gen_ai_response_model, gen_ai_token_type) (
  rate(gen_ai_client_token_usage_sum[5m])
) * 60
sum by (gen_ai_response_model, gen_ai_token_type) (
  rate(gen_ai_client_token_usage_sum[5m])
) * 60

Error rate by provider

sum by (gen_ai_provider_name, error_type) (
  rate(gen_ai_client_operation_duration_count{error_type!=""}[5m])
)
sum by (gen_ai_provider_name, error_type) (
  rate(gen_ai_client_operation_duration_count{error_type!=""}[5m])
)

Latency percentiles

histogram_quantile(0.95,
  sum by (le, gen_ai_provider_name) (
    rate(gen_ai_client_operation_duration_bucket{gen_ai_operation_name="complete"}[5m])
  )
)
histogram_quantile(0.95,
  sum by (le, gen_ai_provider_name) (
    rate(gen_ai_client_operation_duration_bucket{gen_ai_operation_name="complete"}[5m])
  )
)

Filtering to complete matters: stream measures only the handshake, so mixing them makes the percentile meaningless.

Detecting abandoned streams

# stream_end operations that carry no output tokens — a client hung up.
sum by (gen_ai_provider_name) (
  rate(gen_ai_client_operation_duration_count{gen_ai_operation_name="stream_end"}[5m])
)
# stream_end operations that carry no output tokens — a client hung up.
sum by (gen_ai_provider_name) (
  rate(gen_ai_client_operation_duration_count{gen_ai_operation_name="stream_end"}[5m])
)

Troubleshooting#

gen_ai.response.model is missing

Best-effort — a provider that does not report the serving model leaves it empty. Fall back to gen_ai.request.model in your query.

Token metrics are absent for streaming

They arrive on stream_end, and require the upstream host to report usage. On OpenAI-family hosts that means stream_options.include_usage must be honoured.

The attribute names changed under me

The GenAI conventions are still in development. skyl spells the keys out rather than pinning a semconv module, precisely so a convention change is a small, visible correction here rather than a forced dependency bump.

Edit this page on GitHub