Skip to main content

config.yaml

A fully annotated example ships in the repository as config.yaml.example. This page walks through the fields by topic, with their defaults. The config schema is generated from the code and lists every field, including the few this page only links to.

Location: $HERMES_HOME/hermes_otel.yaml (recommended), or $HERMES_OTEL_CONFIG, or the legacy $HERMES_HOME/plugins/hermes_otel/config.yaml; see Where does config.yaml live?. The file is parsed with PyYAML, which the plugin installs as a dependency. ${VAR} in a string value is replaced with that environment variable.

Project / resource attributes​

# Shown in Phoenix's "Project" dropdown. Set as `openinference.project.name`
# on the Resource, so every span carries it. Env: HERMES_OTEL_PROJECT_NAME;
# when unset, OTEL_PROJECT_NAME is used.
project_name: hermes-agent

# Report each named profile as its own service (service.name=<profile>,
# service.namespace=hermes-agent); the default profile stays hermes-agent.
# See /configuration/profiles#one-service-per-profile.
# Env: HERMES_OTEL_SERVICE_NAME_FROM_PROFILE. Default: false.
service_name_from_profile: false

# Free-form key/value pairs merged into the OTel Resource (shared by every
# span, metric and log record). Good for deployment tags. The defaults
# service.name=hermes-agent, service.instance.id, service.version,
# process.pid and hermes.profile are set for you.
resource_attributes:
team: platform
env: prod
region: us-east-1

# Deprecated spelling of resource_attributes: still accepted, merged into
# resource_attributes at load (which wins on a key conflict), with a warning.
# global_tags:
# team: platform

# Stamped on every span, every log record and every metric point as a plain
# attribute, for backends that do not index resource attributes per signal
# (Loki labels, Mimir series, OpenObserve columns). Scalars only; keys of at
# most 64 characters; `hermes.`, `gen_ai.` and `service.` are reserved and
# dropped with a warning. See "Which attributes reach which signal" below.
common_attributes:
team: otel
cost_center: research

# Extra HTTP headers added to every OTLP request across ALL backends.
# Per-backend `headers:` are merged on top (per-backend wins on conflict).
# A per-backend `headers_helper:` command mints headers that expire; see
# /configuration/short-lived-credentials.
# This file is the only source of headers: the SDK's OTEL_EXPORTER_OTLP_*HEADERS
# variables are ignored so one backend's token never reaches another (#472).
headers:
X-Scope-OrgID: tenant-a

Which attributes reach which signal​

resource_attributes describe the process and travel on the OTLP Resource, which every signal shares but which not every backend indexes per signal: Loki keeps a fixed set of resource attributes as labels, Mimir's OTLP write promotes only the standard ones to series labels, OpenObserve stores them in columns that differ per stream type. common_attributes (#480) are stamped on the data points themselves, so team=otel filters traces, logs and metrics alike on any backend.

KeySpansLog records and eventsMetric pointsResource
resource_attributes (and global_tags)via the Resourcevia the Resourcevia the Resourceyes
common_attributesevery span on the plugin's provider, third-party (MCP, httpx) spans includedevery record and event, before redaction and the length capevery point of the plugin's instruments (the Views admit the keys); not the host/GPU observables or the SDK's own otel.sdk.* seriesno
project_namevia the Resource (openinference.project.name)via the Resourcevia the Resourceyes
profilehermes.profile on the root span—profile label on every pointhermes.profile
session idsession.id / hermes.session.id on the roothermes.session_id / gen_ai.conversation.idnever (compat instruments only, with metric_name_compat_session_id)no

Rules: values are scalars (string, number, bool); a key is at most 64 characters and never under hermes., gen_ai. or service. (the plugin's vocabularies and the service identity); a rejected entry is dropped with a warning at load and the rest apply. A key the plugin already sets on a span, record or point is left alone: the plugin's attribute wins. ${VAR} references expand like everywhere else in the file. Because the values are constant for the process they do not fan metric series out, but they are part of each series' identity: changing team: otel to team: platform and restarting starts new series (the old ones go stale), exactly as a change to service.instance.id would.

Master switch​

# Master kill switch. false = no pipeline, no hooks registered, no spans.
# Env: HERMES_OTEL_ENABLED
enabled: true

Sampling​

# ParentBased(TraceIdRatioBased). null = AlwaysOn (100%).
# 0.25 = sample 25% of traces. Parent-based: if the root is sampled,
# descendants are too — you always see whole traces, never partial ones.
# Env: HERMES_OTEL_SAMPLE_RATE
sample_rate: null

See Sampling for how to pick a rate.

Content capture and previews​

# How much conversation content is recorded:
# full (default) the complete request and response on every api.* span,
# unclipped; clipped previews on the other spans
# preview clipped previews only
# off no content at all; names, timings, tokens and status only
# Quote it: a bare `off` is YAML for false (accepted, and read as off).
# Env: HERMES_OTEL_CONTENT_CAPTURE
content_capture: "full"

# Cap on preview strings (tool args and results, user message, assistant
# response). Longer values are clipped with a trailing "...".
# Env: HERMES_OTEL_PREVIEW_MAX_CHARS
preview_max_chars: 1200

# Per-category caps; null = preview_max_chars.
# Env: HERMES_OTEL_TOOL_INPUT_PREVIEW_MAX_CHARS, and so on.
tool_input_preview_max_chars: null
tool_output_preview_max_chars: null
llm_input_preview_max_chars: null
llm_output_preview_max_chars: null

# Also attach the conversation Hermes held at the start of the turn to the
# llm.* span's input.value, as JSON. Mostly redundant with content_capture:
# full, which already puts the complete request on each api.* span.
# Env: HERMES_OTEL_CAPTURE_CONVERSATION_HISTORY
capture_conversation_history: false

# Cap on that JSON; clipped with "..." if bigger.
conversation_history_max_chars: 20000

# Gateway sessions add hermes.sender.id and user.id (`platform:sender`).
# Can identify people, so it is opt-in. Env: HERMES_OTEL_CAPTURE_SENDER_ID
capture_sender_id: false

capture_previews, capture_full_prompts and capture_full_responses are the pre-1.11 spellings of content_capture. They still work (capture_previews: false means off) and log a deprecation warning. See Conversation capture for what each mode writes, and Privacy mode for off.

Logs (opt-in)​

logs:
# Forward Python logging records (logger.info(...)) to every log-capable
# backend and the dashboard's Logs tab, stamped with the turn's trace and
# span ids. Off by default: attaching to the root logger also exports the
# logs of every library Hermes imports.
# Env: HERMES_OTEL_CAPTURE_LOGS
capture: false

# Minimum severity: DEBUG, INFO, WARNING (or WARN), ERROR, CRITICAL.
# Env: HERMES_OTEL_LOG_LEVEL
level: INFO

# Which Python logger to attach to. null = root (everything);
# "hermes_otel" = the plugin's own lines only.
# Env: HERMES_OTEL_LOG_ATTACH_LOGGER
attach_logger: null

# Structured hermes.* / GenAI events from the hooks. Off by default.
# Env: HERMES_OTEL_LOG_EVENTS
events:
enabled: false

The flat spellings (capture_logs, log_level, log_attach_logger, …) still work. See Logs and events for every key, the loop guard for HTTP client libraries, and which backends accept logs.

Metrics​

# Emit OTel GenAI semantic-convention metrics (gen_ai.client.*, gen_ai.agent.*)
# alongside the custom hermes.* metrics, so generic OTel-GenAI dashboards and
# alerts work without per-user wiring. Set false for hermes.* only.
# Env: HERMES_OTEL_EMIT_GENAI_METRICS. Default: true.
emit_genai_metrics: true

# Temporality: cumulative (Prometheus family) or delta (Datadog, New Relic,
# Logfire). Unset = the SDK default (cumulative, or
# OTEL_EXPORTER_OTLP_METRICS_TEMPORALITY_PREFERENCE). SigNoz, Uptrace and
# Elastic entries default to delta; a backend entry's own
# metrics_temporality wins. Env: HERMES_OTEL_METRICS_TEMPORALITY
metrics_temporality: null

# Histogram aggregation: explicit (fixed bucket boundaries) or exponential
# (base-2, for backends that accept it). Env: HERMES_OTEL_METRICS_HISTOGRAM
metrics_histogram: explicit

# Distinct values a label such as `model` may take per process before the
# rest fold into `other`; 0 = no cap. Env: HERMES_OTEL_METRICS_LABEL_LIMIT
metrics_label_limit: 100

# Export session count, tokens, cost and active time under Claude Code's
# metric names: off | claude_code (instead of hermes.*) | both.
# See /guides/claude-code-dashboards. Env: HERMES_OTEL_METRIC_NAME_COMPAT
metric_name_compat: "off"

flush_interval_ms (below) sets how often metrics are exported. See the Metrics reference for the full instrument list, units, and labels.

Disabling metrics and span families​

Metrics are otherwise all-or-nothing per backend (metrics: false) and spans can only be sampled (sample_rate) or switched off per backend (traces: false). These two lists (#479) switch off individual instruments and whole span families while everything else keeps flowing.

# Instruments never created, recorded or exported. Write the OTLP name, the
# hook key (tool_duration) or the key with dots (tool.duration); `all` disables
# every instrument. An unknown name logs a warning listing the valid ones.
# Env: HERMES_OTEL_DISABLE_METRICS=tool.duration,token_usage (comma-separated).
disable_metrics:
- tool.duration
- hermes.approval.duration

# Span families never recorded. A disabled span is replaced by a non-recording
# stand-in that carries its parent's context, so whatever would have nested
# under it parents to the nearest recording ancestor: with `llm` disabled the
# api.* spans sit directly under the turn root; with `api` disabled the tool.*
# spans sit under llm.*. The hooks still run, so the metrics of a disabled
# family (hermes.tool.duration for `tool`, hermes.approval.count for
# `approval`) are still recorded: use disable_metrics for those. `all` is
# refused (the turn root always records; a backend's `traces: false` is the
# switch for whole traces). Env: HERMES_OTEL_DISABLE_SPANS=approval,skill.
disable_spans:
- approval

A disabled instrument does not exist for the process: it is not created on the meter, record_metric records nothing for it (the dashboard's live store included), and a DropAggregation View on its name keeps anything else recorded under that name out of the export. The Settings tab shows both lists, and the plugin logs them at init.

Valid disable_metrics entries, one row per instrument (the first column is what the config stores and the Settings tab shows):

InstrumentAlso accepted
hermes.span.swept.countspan.swept, span.swept.count, span_swept
hermes.session.countsession.count, session_count
hermes.session.turnssession.turns, session_turns
hermes.session.durationsession.duration, session_duration
hermes.active_time.totalactive.time, active_time, active_time.total
hermes.token.usagetoken.usage, token_usage
hermes.prompt_cache.tokensprompt.cache.tokens, prompt_cache.tokens, prompt_cache_tokens
hermes.prompt_cache.observationsprompt.cache.observations, prompt_cache.observations, prompt_cache_observations
hermes.cost.usagecost.usage, cost_usage
hermes.tool.durationtool.duration, tool_duration
hermes.message.countmessage.count, message_count
hermes.model.usagemodel.usage, model_usage
hermes.auxiliary.countauxiliary.count, auxiliary_count
hermes.skill.inferredskill.inferred, skill_inferred
hermes.subagent.countsubagent.count, subagent_count
hermes.subagent.durationsubagent.duration, subagent_duration
hermes.api.error.countapi.error.count, api_error_count
hermes.retry.countretry.count, retry_count
hermes.kanban.task.countkanban.task.count, kanban_task_count
hermes.api.time_to_first_chunkapi.time.to.first.chunk, api.time_to_first_chunk, api_time_to_first_chunk
hermes.command.countcommand.count, command_count
hermes.commit.countcommit.count, commit_count
hermes.human_input.counthuman.input.count, human_input.count, human_input_count
hermes.skill.lifecycle.countskill.lifecycle.count, skill_lifecycle_count
hermes.transcription.counttranscription.count, transcription_count
hermes.kanban.dispatch.countkanban.dispatch.count, kanban_dispatch_count
hermes.kanban.dispatch.resultkanban.dispatch.result, kanban_dispatch_result
hermes.kanban.task.updatedkanban.task.updated, kanban_task_updated
hermes.approval.countapproval.count, approval_count
hermes.approval.durationapproval.duration, approval_duration
gen_ai.execute_tool.durationtool.duration.s, tool_duration_s
hermes.approval.wait.durationapproval.wait.duration, approval.wait.s, approval_wait_s
hermes.subagent.run.durationsubagent.run.duration, subagent.run.s, subagent_run_s
gen_ai.client.token.usage—
gen_ai.client.operation.duration—
gen_ai.agent.token.usage—
claude_code.session.count—
claude_code.token.usage—
claude_code.cost.usage—
claude_code.active_time.total—

Valid disable_spans families, by the first segment of the span name. auxiliary is the auxiliary api.* span (titling, compression, MoA; see Auxiliary calls), which api leaves alone:

tool, api, llm, approval, skill, subagent, kanban, auxiliary, human_input

Skills​

# Open a skill.<name> span when the agent loads a skill (via skill_view or a
# /skills/<name>/ read), spanning from load to the turn boundary. Skills
# overlap freely. The hermes.skill.name attribute and skill_inferred counter
# are emitted regardless of this flag.
# Env: HERMES_OTEL_SKILL_SPANS. Default: true.
skill_spans: true

See Span hierarchy → skill.* for the span shape and attributes.

MCP keepalive pings​

# MCP Python SDK 2.x (Hermes v0.21.0+) emits an OTel span for every MCP
# request, including the `ping` Hermes sends on each idle keepalive interval
# per connection. Each one lands as a standalone one-span "MCP send ping"
# trace. True drops the successful pings before any exporter or the live
# store sees them; failed pings (error status) are always kept.
# Env: HERMES_OTEL_SUPPRESS_MCP_PING_SPANS. Default: true.
suppress_mcp_ping_spans: true

Joining an outer trace and the startup probe​

# When the process that launched Hermes exported HERMES_OTEL_TRACEPARENT
# (a W3C traceparent, plus optional HERMES_OTEL_TRACESTATE), every turn
# root becomes a child of that span. false ignores the variables. The
# kanban worker hand-off (plain TRACEPARENT) is separate.
# Env: HERMES_OTEL_REMOTE_PARENT. Default: true.
remote_parent: true

# At startup, open one TCP connection to each backend endpoint on a
# background thread and log "reachable" or "unreachable" per backend.
# Runs once, sends no bytes, and never delays an export.
# Env: HERMES_OTEL_PROBE_BACKENDS. Default: true.
probe_backends: true

# Add a short system-prompt section telling the model that telemetry and
# the hermes_otel:observability skill exist. Changes what the model sees
# on every turn, so it is opt-in. Env: HERMES_OTEL_DISCOVERY_PROMPT.
discovery_prompt: false

Live store and host metrics​

# Keep recent spans, metrics and logs in $HERMES_HOME/hermes_otel_live.db
# (override the path with HERMES_OTEL_LIVE_DB) for the dashboard's Live mode.
# Env: HERMES_OTEL_DASHBOARD_LIVE. Default: true.
dashboard_live: true
dashboard_live_max_spans: 1000 # rows kept per kind (spans, metrics, logs)
dashboard_live_retention_hours: 168 # older rows are dropped; 0 = row cap only

# CPU / GPU sampling. See /configuration/host-metrics.
host_metrics: false
host_metrics_gpu: auto # auto | amd | nvidia | off
host_metrics_interval_ms: 1000 # floor 50

Lifecycle / cleanup​

# Orphan-turn sweep. A turn root still open after this long is closed,
# with `hermes.turn.final_status=timed_out` (or `interrupted` when the
# gateway stopped the turn).
# Env: HERMES_OTEL_ROOT_SPAN_TTL_MS. Default: 10 minutes.
root_span_ttl_ms: 600000

# A subagent.* span stays open across the parent's turns (Hermes runs
# delegations in the background). One whose subagent_stop never arrives
# is closed after this. Env: HERMES_OTEL_SUBAGENT_SPAN_TTL_MS. Default: 24 h.
subagent_span_ttl_ms: 86400000

# How often metrics are exported (PeriodicExportingMetricReader).
# Env: HERMES_OTEL_FLUSH_INTERVAL_MS. Default: 60s.
flush_interval_ms: 60000

# At the end of each turn, flush every backend's spans, then metrics and
# logs, from a background thread (500 ms per backend; a second request
# while one is queued is dropped). The trace shows up in the UI right away.
# false = rely on the batch worker's schedule.
# Env: HERMES_OTEL_FORCE_FLUSH_ON_SESSION_END. Default: true.
force_flush_on_session_end: true

# How long the turn waits for that flush before returning to Hermes.
# A one-shot run (`hermes -z`) exits right after the turn, so this bounds
# what it exports. 0 = do not wait.
# Env: HERMES_OTEL_FORCE_FLUSH_WAIT_MS. Default: 1500.
force_flush_wait_ms: 1500

See Orphan-span sweep.

BatchSpanProcessor tunables​

Each backend gets its own processor with these settings. Defaults are fine for almost every workload — tune only if you're losing spans under sustained high throughput.

# Max spans buffered per backend; when full, the oldest are dropped.
# Env: HERMES_OTEL_SPAN_BATCH_MAX_QUEUE_SIZE
span_batch_max_queue_size: 2048

# How often the background worker wakes up to drain the queue.
# Env: HERMES_OTEL_SPAN_BATCH_SCHEDULE_DELAY_MS
span_batch_schedule_delay_ms: 1000

# Max spans per OTLP request. null = 512, or 64 when content_capture is
# full (the default), so a batch of large spans stays under receiver limits.
# Env: HERMES_OTEL_SPAN_BATCH_MAX_EXPORT_BATCH_SIZE
span_batch_max_export_batch_size: null

# Per-export HTTP timeout.
# Env: HERMES_OTEL_SPAN_BATCH_EXPORT_TIMEOUT_MS
span_batch_export_timeout_ms: 30000

See Batch export tuning.

Multi-backend fan-out​

See the dedicated Multi-backend page — full grammar, per-backend overrides, env-var interpolation, and debugging tips.

backends:
- type: phoenix
endpoint: http://localhost:6006/v1/traces
# ...

When backends: is absent or empty, the plugin falls back to single-backend env-var detection.

Per-entry keys you may need beyond type and endpoint: headers, headers_helper for tokens that expire (Short-lived credentials), traces / metrics / logs to switch a signal per backend, certificate_file (a private CA) and client_certificate_file + client_key_file (mTLS) for an HTTPS or gRPC endpoint, and, on a type: otlp entry only, protocol: grpc with insecure (gRPC and TLS). The config schema lists every entry key.

Full example​

enabled: true
sample_rate: 0.25
project_name: hermes-prod

resource_attributes:
team: platform
env: prod
region: us-east-1

common_attributes:
team: platform

headers:
X-Scope-OrgID: tenant-a

content_capture: "full"
preview_max_chars: 1200
capture_conversation_history: true
conversation_history_max_chars: 40000

logs:
capture: true
level: INFO

root_span_ttl_ms: 600000
flush_interval_ms: 60000
force_flush_on_session_end: true

span_batch_max_queue_size: 4096
span_batch_schedule_delay_ms: 500

backends:
- type: lgtm
endpoint: http://localhost:4318/v1/traces

- type: phoenix
endpoint: http://localhost:6006/v1/traces

- type: langfuse
public_key_env: LANGFUSE_PUBLIC_KEY
secret_key_env: LANGFUSE_SECRET_KEY
base_url: https://cloud.langfuse.com

Credentials resolve the same way for the exporter and the dashboard's query adapters: an inline value (secret_key: ...) wins over the env var a *_env key names, which wins over the type's vendor fallbacks (LANGFUSE_SECRET_KEY and friends), and ${VAR} references in an entry are expanded for both. An entry that sets both therefore exports and queries with one key. Prefer the *_env spelling so secrets stay out of the file.