Skip to main content

Metrics reference

Every instrument the plugin creates, exactly as named in tracer._create_metric_instruments. Metrics reach every backend with metrics: true through one shared MeterProvider (flush_interval_ms controls the export cadence). Backends that speak only traces (Phoenix, Jaeger, Tempo, Langfuse, Weave) ignore them.

Label policy. Labels are bounded: model, provider, platform, tool name, skill name, outcome, choice, error class. Never a session id, task id, tool-call id or free text — per-session analysis belongs on spans (session.id). Each process also carries its own service.instance.id resource attribute, so two Hermes processes exporting to one backend never write the same series (see Config schema → resource_attributes).

hermes.* metrics

MetricKindUnitLabelsRecorded by
hermes.session.countCounterplatformsession_start
hermes.message.countCountermodel, providerpost_llm_call (one per completed assistant message)
hermes.model.usageCountermodel, providerpost_api_request (one per API call)
hermes.token.usageCountermodel, provider, token_type = input · output · cacheRead · cacheCreation · reasoningpost_api_request
hermes.prompt_cache.tokensCounter{token}model, provider, api_mode, cache_result = hit · misspost_api_request (only when the provider reported cache accounting)
hermes.prompt_cache.observationsCounter{request}model, provider, api_mode, cache_resultpost_api_request
hermes.cost.usageCounterUSDmodel, providerpost_api_request (when Hermes reports usage.cost)
hermes.tool.durationHistogrammstool_name, gen_ai.tool.namepost_tool_call
hermes.skill.inferredCounterskill_name, source = skill_view · path_matchpre_tool_call
hermes.approval.countCounterchoicepost_approval_response
hermes.approval.durationHistogrammschoicepost_approval_response (human / guardian decision wait)
hermes.api.error.countCountererror_type, status_class = 2xx5xx · network · other, retryable, model, providerapi_request_error
hermes.retry.countCountermodel, providerapi_request_error (once per retryable failure)
hermes.subagent.countCounterrole, statussubagent_stop
hermes.subagent.durationHistogrammsrolesubagent_stop

token_type semantics: input is the whole prompt (uncached + cache reads + cache writes, Hermes' prompt_tokens); cacheRead / cacheCreation are subsets of it; reasoning is a subset of output. So the prompt-cache hit rate from this counter alone is rate(cacheRead) / rate(input).

Prompt-cache hit rate

hermes.prompt_cache.tokens splits the whole prompt into hit (cache reads) and miss (uncached input + cache writes), so hit + miss is the prompt:

sum(rate(hermes_prompt_cache_tokens_total{cache_result="hit"}[5m]))
/
sum(rate(hermes_prompt_cache_tokens_total[5m]))

Divide counter rates; never average per-request percentages. A request is counted only when the provider reported any cache accounting, so unknown support never shows up as a 0 % hit rate.

OTel GenAI semantic-convention metrics

Emitted alongside the hermes.* instruments so generic GenAI dashboards work unchanged. Set emit_genai_metrics: false (HERMES_OTEL_EMIT_GENAI_METRICS=false) to turn them off.

MetricKindUnitLabelsRecorded by
gen_ai.client.token.usageHistogram{token}gen_ai.token.type = input · output, gen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.response.modelpost_api_request
gen_ai.client.operation.durationHistogramsgen_ai.operation.name, gen_ai.provider.name, gen_ai.request.model, gen_ai.response.model; error.type on failurespost_api_request, api_request_error
gen_ai.agent.token.usageHistogram{token}gen_ai.token.type, gen_ai.operation.name = invoke_agent, gen_ai.provider.name, gen_ai.request.modelsession_end (per-turn rollup)
  • Durations follow the spec unit (seconds); the hermes.* histograms stay in ms.
  • gen_ai.token.type is limited to the spec's input / output; cache and reasoning breakdowns live on hermes.token.usage.
  • gen_ai.agent.request.duration is deliberately not emitted yet — there is no reliable per-turn duration signal until true session-lifecycle timing lands.

Host & GPU metrics (opt-in)

Only with host_metrics: true. Observable instruments read the in-process sampler's latest value on each collection; names follow the OTel system / hardware conventions. Details and PromQL: Host & GPU metrics.

MetricKindUnitLabels
process.cpu.utilizationGauge1cpu.mode = user · system
system.cpu.utilizationGauge1cpu.mode
hw.gpu.utilizationGauge1hw.id, hw.vendor
hw.gpu.memory.usageUpDownCounterByhw.id, hw.vendor
hw.powerGaugeWhw.id, hw.vendor, hw.type=gpu

Names as your backend shows them

OTLP names use dots; Prometheus-style stores mangle them. Rules of thumb:

OTLP namePrometheus / LGTM / SigNozOpenObserve
hermes.token.usage (counter)hermes_token_usage_totalhermes_token_usage
hermes.tool.duration (histogram, ms)hermes_tool_duration_milliseconds_bucket / _sum / _counthermes_tool_duration_*
gen_ai.client.operation.duration (histogram, s)gen_ai_client_operation_duration_seconds_*gen_ai_client_operation_duration_*
hermes.prompt_cache.tokenshermes_prompt_cache_tokens_totalhermes_prompt_cache_tokens

Query histograms by suffix (_sum, _count, _bucket); the bare name has no series. Prometheus instant queries go stale ~5 minutes after the producing process exits — widen the range.