Observability
The service exposes three complementary ways to see what it’s doing: cumulative Prometheus metrics for trends, per-run OpenTelemetry traces for drill-down, and rotating log files for the raw event stream. This page covers all three.
Prometheus metrics
Section titled “Prometheus metrics”GET /metrics exposes Prometheus text-format metrics, computed from
pipeline_runs / pipeline_steps at scrape time. All counters are cumulative
all-time totals — use rate()/ratios in PromQL for escalation rate, per-agent
success rate, and verifier veto frequency rather than relying on pre-baked
percentages.
Every metric below is scoped to stage=production — a stage=testing
pipeline (the default, see Pipeline stages)
contributes to none of them, including the metrics that query
pipeline_steps without otherwise touching pipeline_runs.
| Metric | Type | Labels | Description |
|---|---|---|---|
vectorstep_pipeline_runs_total |
counter | pipeline, status |
Total runs by pipeline and terminal status |
vectorstep_pipeline_runs_in_progress |
gauge | — | Runs currently in status=running |
vectorstep_pipeline_steps_total |
counter | pipeline, step_name, executor, agent, model, provider, status |
Total steps by pipeline, step, executor, agent, model, provider, and status. pipeline/step_name/model/provider are what let a Grafana dashboard reconstruct the per-step and per-model success-rate breakdowns the Pipelines/Steps/Agents Insights UI pages compute directly from the DB — the UI is for a quick look, this metric is for a real dashboard or alert. NULL agent/model/provider (non-gateway executors) are bucketed as "". |
vectorstep_pipeline_step_duration_seconds |
histogram | pipeline, step_name, executor, agent, model, provider |
Step execution duration (buckets: 1, 2, 5, 10, 30, 60, 120, 300, 600, 1200, +Inf seconds) |
vectorstep_verifier_runs_total |
counter | agent |
Steps where a verifier ran, by primary agent |
vectorstep_verifier_overrides_total |
counter | agent |
Verifier runs where the verifier lowered the primary’s effective confidence |
vectorstep_pipeline_tokens_total |
counter | team, pipeline, step_name, executor, agent, model, provider, direction |
Cumulative input/output tokens consumed, broken down by owning team (Team attribution) for cost attribution. direction is input/output. NULL team/model/provider are bucketed as "" rather than dropped, so unattributed spend stays visible. Steps from executors that don’t report tokens (openclaw, human, webhook) are excluded rather than padded as zero. |
vectorstep_pipeline_cache_tokens_total |
counter | team, pipeline, step_name, executor, agent, model, provider, direction |
Cumulative Anthropic prompt-cache tokens. direction is read/write. Billed at their own rates and not included in vectorstep_pipeline_tokens_total, which keeps counting only input and output. Only steps that reported cache tokens contribute. See Prompt-cache tokens. |
vectorstep_human_approval_decisions_total |
counter | team, pipeline, decision |
Cumulative human step approve/reject decisions. decision is approved/rejected, derived from primary_confidence (1.0/0.0 — see the executor’s contract). Timeouts leave primary_confidence NULL and are excluded rather than miscounted as either outcome. NULL team is bucketed as "". |
vectorstep_pipeline_feedback_total |
counter | pipeline, outcome |
Cumulative human accuracy feedback. outcome is correct/partial/incorrect. |
vectorstep_step_feedback_total |
counter | pipeline, step_name, agent, model, provider, outcome |
Cumulative per-step human accuracy feedback, production-scoped. outcome is correct/partial/incorrect. |
vectorstep_step_grounding_score |
histogram | pipeline, step_name, agent, model, provider |
Shadow-mode grounding score (G) distribution per step, production-scoped (buckets: 0.1, 0.2, …, 1.0, +Inf). Only steps with a grounding: block that produced a score contribute — NULL/not-computed steps are excluded, not padded as zero. |
vectorstep_step_deterministic_check_total |
counter | pipeline, step_name, outcome |
Cumulative whole-step deterministic-check outcomes, production-scoped. outcome is passed/failed — passed only when every declared check for that step run passed. Steps with no deterministic_checks: declared are excluded. |
vectorstep_human_approvals_pending |
gauge | team |
Currently pending human step approvals, awaiting a response on whichever channel (Telegram/Slack/Teams) that team is routed to. Unlike every other metric here this isn’t derived from the database — pending approvals are in-memory only — so it reflects only this process’s current state, not a historical/cumulative total. NULL team is bucketed as "". Always emits at least a zero-valued series so the metric doesn’t disappear from dashboards when nothing’s pending. Excludes stage=testing pending approvals, same as every other series on this page. |
vectorstep_pipeline_cost_total |
counter | pipeline, team, model, provider, source |
Cumulative step cost in pricing.currency’s units, split by pricing tier (source is manual|live_exact|live_cross_provider). Unpriced steps (no tier match, or no token data) are excluded rather than padded as zero, same treatment as the tokens metric. One honest series rather than a separate approximate metric — use sum without(source) for the total, or filter on source for just the tiers you trust. See Cost accounting. |
vectorstep_team_budget_ratio |
gauge | team |
Month-to-date spend / pricing.team_budgets[team], UTC calendar month. Only teams with a configured budget get a series. Advisory only — never gates a run. |
vectorstep_runs_resumed_total |
counter | pipeline |
Total runs resumed after a restart. See Durability & resume. |
vectorstep_replay_batches_total |
counter | step_name |
Total replay batches launched via POST /steps/{name}/replay. See Replay / shadow evaluation. |
vectorstep_replay_steps_total |
counter | step_name, grade |
Total replay sample outcomes. grade is completed|deterministic_failed|execution_failed|unreplayable. See Replay / shadow evaluation. |
Standard python_* / process_* / python_gc_* process-health metrics are
included automatically via prometheus_client’s default collectors.
OpenTelemetry tracing
Section titled “OpenTelemetry tracing”While /metrics gives aggregate, all-time counters, distributed tracing
gives a per-run drill-down — one trace per pipeline run, with a span for
every step, branch, verifier, and underlying LLM call. Use /metrics to spot
trends (“escalation rate is up this week”); use traces to answer “why was
this run slow / why did this step escalate”.
Tracing is disabled by default and adds zero overhead until enabled — the
OTel SDK’s default TracerProvider is a no-op.
Enabling tracing — add to config.yaml:
observability: otel: enabled: true exporter: otlp # otlp | console endpoint: http://localhost:4318/v1/traces # OTLP/HTTP collector endpoint service_name: vectorstep-serviceexporter: console prints spans to stdout — useful for local verification
without a collector. exporter: otlp (default) sends spans via OTLP/HTTP to a
collector (e.g. Grafana Alloy/Tempo).
Span hierarchy — each pipeline run is its own trace root:
pipeline.run: <team>/<pipeline> (vectorstep.pipeline.name, vectorstep.run.id, vectorstep.source, vectorstep.team, vectorstep.run.status)├── <step name> (vectorstep.span.kind=step, vectorstep.executor, vectorstep.agent, confidences, vectorstep.model, vectorstep.provider, vectorstep.prompt_hash, vectorstep.agent_version)│ ├── gen_ai.<executor> (vectorstep.span.kind=gen_ai — the LLM call itself)│ └── <step>:verifier|:independent (vectorstep.span.kind=verifier, vectorstep.verifier.mode, vectorstep.confidence)│ └── gen_ai.<executor>├── <group name> (vectorstep.span.kind=parallel_group, vectorstep.join_strategy, vectorstep.branch_count, vectorstep.agent_version)│ ├── <branch name> (vectorstep.span.kind=branch, vectorstep.executor, vectorstep.agent, vectorstep.confidence, vectorstep.model, vectorstep.provider)│ │ └── gen_ai.<executor>│ └── ... (concurrent siblings)├── <fan_out name> (vectorstep.span.kind=fan_out, vectorstep.join_strategy, vectorstep.prompt_hash, vectorstep.agent_version)│ ├── <fan_out name>/0 (vectorstep.span.kind=branch, vectorstep.executor, vectorstep.agent, vectorstep.confidence)│ │ └── gen_ai.<executor>│ └── ... (one branch per runtime list item)└── ...vectorstep.prompt_hash/vectorstep.agent_version are set on the <step name> span
(from that step’s own prompt_template) and the <fan_out name> group span
(all branches of a fan-out share one template, so a single hash is meaningful
there) — but deliberately not on individual branch spans or the <group name> parallel_group span’s prompt_hash, since each parallel branch has its
own distinct template and no single hash would be meaningful at the group
level. vectorstep.agent_version is set wherever a single agent’s output is
available, including the parallel group span. Neither is added as a
Prometheus metric label — unbounded cardinality, since every prompt/agent
edit mints a new, permanently-retained label value.
The root span name is pipeline.run: <team>/<pipeline-name> when a team is
attributed (e.g. pipeline.run: payments/alert-triage-critical), or
pipeline.run: <pipeline-name> for unattributed runs. This makes the Name
column in Grafana Tempo immediately useful without needing to expand
attributes. vectorstep.team is also set as a span attribute when present, so
traces can be filtered/grouped by team in PromQL or Tempo query expressions.
Each run’s structured log event (step_started, verifier_ran,
branch_completed, etc. — see Run storage) is also
recorded as a span event on the current span, so the structured run log
and the trace timeline tell the same story from two angles.
gen_ai.* span attributes:
| Attribute | Description |
|---|---|
gen_ai.system |
Executor name (openclaw, openclaw_ws, gateway) |
gen_ai.request.model |
Model override requested, if any |
gen_ai.response.model |
Actual model used, from executor metadata |
vectorstep.gateway.duration_ms |
Backend-reported call duration |
vectorstep.agent |
Agent name |
Forward compatibility with the VectorStep Gateway: every gen_ai.* span
injects the current traceparent/tracestate (W3C Trace Context) into the
outbound agent request sent to the OpenClaw/VectorStep Gateway WebSocket APIs.
Today’s gateways ignore these extra params. Once the VectorStep Gateway adds its
own OTel instrumentation, its LLM/tool-call spans will automatically nest
under the corresponding gen_ai.* span — giving one connected trace from
webhook → pipeline → step → individual LLM/tool calls, with no further
changes needed on the VectorStep side.
Sending this to a backend
Section titled “Sending this to a backend”Both signals are standard, vendor-neutral output — /metrics is plain
Prometheus text format, and traces are OTLP/HTTP. VectorStep has no
Grafana-specific (or any other backend-specific) integration beyond the
config above: point any Prometheus-compatible scraper at /metrics, and
point observability.otel.endpoint at any OTLP-compatible collector —
Grafana Alloy, the OpenTelemetry
Collector, or a vendor’s own
ingest endpoint all work the same way from VectorStep’s side. See
OpenTelemetry’s docs for the collector
and backend side of this setup — this page only covers what VectorStep
itself emits.
Logging
Section titled “Logging”The service writes rotating log files under the directory set by
logging.dir (/data/logs by default in the container, auto-created) —
omit logging.dir entirely to log to stdout only, which is the usual choice
inside a container or Kubernetes pod. Two files are produced:
service.log— application logs (rotating, 10 MB × 5 files).access.log— HTTP access logs, kept separate fromservice.logso a busy webhook endpoint’s request/response lines don’t drown out actual application events when you’re reading through them.
Both files rotate at 10 MB, keeping 5 generations. logging.level controls
verbosity (INFO by default). logging.format chooses between the default text
line and json (one object per line); see Correlating logs with
traces below. See
Deployment for the full logging: block in
context, and Configuration reference for every
field.
Correlating logs with traces
Section titled “Correlating logs with traces”Every log line from the service and the Gateway carries four fields, so you can go from a line to its trace and from a run to its lines:
2026-11-06T10:15:30 INFO src.pipeline.runner [trace_id=0af7651916cd43dd8448eb211c80319c span_id=b7ad6b7169203331 run=3f2a… step=triage] Step 'triage' verifier: …trace_id/span_id: the active OpenTelemetry span.-when tracing is off.run/step: the pipeline run and step.-outside a run. They are set per run and per branch, so lines from concurrent runs and parallel branches stay apart. The service sends the run and step to the Gateway with each agent call, so the Gateway’s lines carry them even when tracing is off. Older Gateways ignore them and show-.- Pipeline name: also set for the whole run and sent to the Gateway, but only in the JSON
format (
pipeline) and on exported records (vectorstep.pipeline.name), so you can filter a log backend by pipeline. The text line leaves it out to stay short.
A sub-pipeline is its own trace and its own run, with its own IDs.
Set logging.format: json (in both the service and the Gateway) for one JSON object per line.
Fields that have no value are left out:
{"ts":"2026-11-06T10:15:30.123Z","level":"INFO","logger":"src.pipeline.runner","message":"…","trace_id":"0af7…","span_id":"b7ad…","run_id":"3f2a…","step":"triage","pipeline":"alert-triage"}An exception field holds the traceback when there is one. The JSON format is scrubbed for
credentials exactly like the text format. The text format changed in this release: it now
has the bracketed field block before the message, so anything that parses the old line needs
updating, or switch to json. Access-log lines are unchanged.
The run’s trace ID is stored with it and shown on the run page. Set
observability.otel.trace_url_template to turn it into a link to your tracing backend:
observability: otel: enabled: true trace_url_template: "https://tracing.example.com/trace/{trace_id}" # whatever URL opens a trace by ID in your backendThe template must be an http(s) URL containing {trace_id}; anything else is ignored
with a warning. Runs made before this release, and runs made with tracing off, have no trace ID.
The ID is also in the run API as trace_id, so it reaches the MCP get_run tool.
To find a run’s log lines, filter your log backend on the trace_id (or run_id) field. How you
write that filter depends on the backend and on how you ship the logs; with logging.format: json
the fields are already structured.
Exporting logs over OTLP
Section titled “Exporting logs over OTLP”Logs can also be sent to your collector alongside stdout, the same way traces are:
observability: otel: enabled: true endpoint: http://collector:4318/v1/traces logs: enabled: false # default endpoint: null # default: the traces endpoint with /v1/traces -> /v1/logs level: INFO # may be stricter than logging.level capture_content: none # none | output | output_and_prompt (service only) capture_content_max_chars: 4000- It is additional to stdout, not a replacement. When the collector is down the exporter drops records, so stdout (and the optional log files) remain the record during exactly that kind of incident. Keep them on.
- Records carry the trace and span IDs,
vectorstep.run.id,vectorstep.stepandvectorstep.pipeline.name, and use the sameservice.nameas traces, so your backend can link a trace to its logs and back. Loki shows these attributes with underscores (vectorstep_step,vectorstep_pipeline_name). - Credentials are scrubbed from the message, its arguments, attributes and the exception text before export (same best-effort backstop as above).
- A dead collector does not slow or fail a run: export happens on a background thread with a bounded queue.
- If
logs.endpointis unset andobservability.otel.endpointdoes not end in/v1/traces, the service refuses to start rather than send logs to a guessed URL. - Auth uses the standard
OTEL_EXPORTER_OTLP_HEADERS(orOTEL_EXPORTER_OTLP_LOGS_HEADERS) environment variable, as for traces. Access logs are not exported.
Step output capture is off by default. With capture_content: output, each finished step
(and each parallel or fan-out branch) sends its parsed output to the backend as a log record named
vectorstep.step.output; output_and_prompt also sends the rendered prompt as
vectorstep.step.prompt. Each is cut at capture_content_max_chars, with
vectorstep.content.truncated set when it was.
Secrets in logs
Section titled “Secrets in logs”Every log handler scrubs the final line before it is written, so a credential
that appears in a message, a %s argument or an exception traceback is
replaced with <redacted>. It recognises Telegram bot tokens (which sit in the
Bot API URL path), Slack webhook URLs and tokens, bearer tokens and sk-
provider keys. This is a best-effort backstop for those known shapes, not a
guarantee. A credential in some other format can still reach a log, so keep
secrets out of anything you ask the service to log. The Gateway applies the same
scrubbing. Earlier versions could write a Telegram bot token to the log; if you
ran one of those with Telegram enabled, rotate the token and purge old log files.