Skip to content

Monitoring & Dashboards

SynthOrg exposes runtime telemetry via a Prometheus /metrics endpoint plus structured JSON logs. This guide walks through every metric the application emits, a ready-to-import Grafana dashboard, and suggested alert rules. The canonical metric registration lives in src/synthorg/observability/prometheus_collector.py (pull-refreshed families) and src/synthorg/observability/prometheus_push_metrics.py (push-updated families); bounded label allowlists live in src/synthorg/observability/prometheus_labels.py.

Scraping

Point any Prometheus-compatible scraper at the running app:

scrape_configs:
  - job_name: synthorg
    scrape_interval: 30s
    static_configs:
      - targets: ['synthorg:8000']

The endpoint is unauthenticated by default; put it behind your normal scrape-ACL (firewall, sidecar proxy, Kubernetes NetworkPolicy). All metric names are prefixed with synthorg_.

Metric inventory

The Dashboard column maps each metric to the row it appears under in the default Grafana overview dashboard (monitoring/grafana/synthorg-overview.json). Rows are collapsible; the only row expanded by default is Health & SLO. A metric marked n/a (scrape-only) is exposed on /metrics and queryable in Prometheus but has no dedicated panel in the bundled dashboard yet. The dashboard exposes one filter variable ($workflow_definition_id) that drills panels down per-entity. No per-agent label is exposed: an unbounded agent_id / agent label is a cardinality bomb, so per-agent cost and task breakdowns live in the structured logs and the REST cost / task APIs rather than in metrics. Default queries aggregate across the full set so the unfiltered view is always meaningful.

Bounded-label values are enforced at record time in src/synthorg/observability/prometheus_labels.py; PromQL filters that reference values outside those allowlists will never match data. The five registry-bound push-time label names (agent_id, workflow_definition_id, tool_name, provider, model_id on the metrics noted below) are validated against a registry-bound snapshot rebuilt on every Prometheus scrape; unknown values drop that one sample with a metrics.scrape.failed WARN log per unknown label per scrape. The log repeats on the next scrape if the value is still unknown. The agent_id carried on synthorg_agent_identity_version_changes_total is an OpenMetrics exemplar (validated against the same snapshot), not a label, so per-agent attribution survives without per-agent series.

Info

Metric Type Labels Description Dashboard
synthorg_app_info Info version Application build info. prometheus_client appends the _info suffix, so the scraped series is synthorg_app_info (not synthorg_app). Client Health

Coordination (push-updated per multi-agent run)

Metric Type Labels Description Dashboard
synthorg_coordination_efficiency Gauge - 0.0-1.0 efficiency ratio. Health & SLO
synthorg_coordination_overhead_percent Gauge - % of wall time spent coordinating. Health & SLO

Cost & budget (pull-refreshed at scrape)

Metric Type Labels Description Dashboard
synthorg_cost_total Gauge - Total accumulated cost. Cost & Budget
synthorg_budget_used_percent Gauge - Monthly budget utilisation. Meaningful only when the period's spend is fully metered; a period mixing metered and flat-rate (subscription) connections, or one where nothing is metered, reads 0.0 here, with the verdict carried on synthorg_budget_spend_measurability instead of folded into this number. Health & SLO
synthorg_budget_monthly_cost Gauge - Monthly budget in configured currency. Cost & Budget
synthorg_budget_spend_measurability Gauge state What the monthly percentage above actually measures: exactly one of measured / mixed / unmeasurable reads 1, the other two 0 (state bounded to SpendMeasurability). n/a (scrape-only)
synthorg_budget_daily_used_percent Gauge - Daily utilisation (prorated). The same caveat as synthorg_budget_used_percent applies, against the day's spend rather than the period's. Cost & Budget
synthorg_budget_daily_spend_measurability Gauge state The daily counterpart of synthorg_budget_spend_measurability, against the current day's spend. n/a (scrape-only)
synthorg_budget_query_duration_seconds Histogram query_type Budget read-path query duration (query_type bounded to balance / available_spend / burn_rate / daily_spend / cost_summary / total_cost / agent_cost / project_cost; buckets 1ms-1s). Audit & Performance

Agents & tasks

Metric Type Labels Description Dashboard
synthorg_active_agents_total Gauge status, trust_level Active agent count by status and trust level (both bounded to their enums; an out-of-vocabulary value folds to other). Health & SLO
synthorg_tasks_total Gauge status Task count per status (no per-agent label; per-agent breakdowns are served by the REST task API). Tasks
synthorg_task_runs_total Counter outcome Emitted task outcomes by bounded outcome (succeeded / failed / cancelled / rejected). One increment per terminal-status hop on a task; a task that transitions through failed and is later retried therefore counts as one failed and one succeeded (or another terminal value) -- the counter records emitted outcomes, not unique task ids. Tasks
synthorg_task_duration_seconds Histogram outcome Task lifetime in seconds (creation to outcome, so it includes queued, blocked and failed-attempt time, not just active execution), partitioned by the same outcome values as synthorg_task_runs_total (buckets 0.1s-600s). Observed on every emitted outcome, so the counter and the histogram always describe the same population. The duration is measured from the task row's own persisted created_at, which means a restart cannot lose it and a retried task still measures from its original creation rather than from the retry. Tasks
synthorg_task_transitions_total Counter from_status, to_status Every persisted task status hop (not just terminal outcomes); both labels bounded to TaskStatus (out-of-vocabulary folds to the sentinel). Charts intermediate-state flow that the terminal-only synthorg_task_runs_total cannot show. n/a (scrape-only)

Providers

Metric Type Labels Description Dashboard
synthorg_provider_tokens_total Counter provider, model, direction Input/output tokens by model (direction bounded to input/output). Tools & Providers
synthorg_provider_tokens_per_call Histogram provider, model, direction Per-call token distribution (token-count buckets 128-131072, not seconds); charts per-request prompt / completion size rather than the running total. n/a (scrape-only)
synthorg_provider_cost_total Counter provider, model Cost per provider call. Tools & Providers
synthorg_provider_errors_total Counter provider, model, error_class Provider-call failures classified by rate_limit / timeout / connection / internal / invalid_request / auth / content_filter / not_found / other. Tools & Providers
synthorg_provider_call_duration_seconds Histogram provider, model, call_type Provider call wall-clock duration per provider, model, and call type (buckets 0.05s-120s). The auto-emitted _count series is the per-label call counter. Tools & Providers

Tools

Metric Type Labels Description Dashboard
synthorg_tool_invocations_total Counter tool_name, outcome Tool invocations by bounded outcome (success / error / timeout). Tools & Providers
synthorg_tool_duration_seconds Histogram tool_name, outcome Tool invocation duration (buckets 5ms-120s). Tools & Providers

API

Metric Type Labels Description Dashboard
synthorg_api_request_duration_seconds Histogram method, route, status_class HTTP request handler duration (buckets 5ms-10s). The auto-emitted _count series is the per-label request counter; use it for request-rate PromQL. Client Health
synthorg_api_error_classification_total Counter category, status_class 4xx/5xx response counter partitioned by RFC 9457 category (auth / validation / not_found / conflict / rate_limit / budget_exhausted / provider_error / internal) and status class. Audit & Security

Caches

Metric Type Labels Description Dashboard
synthorg_cache_operations_total Counter cache_name, outcome In-process cache operations (cache_name bounded to mcp_result / reranker; outcome bounded to hit / miss / evict). Client Health

Security

Metric Type Labels Description Dashboard
synthorg_security_evaluations_total Counter verdict Pre-tool security verdicts (verdict bounded to allow / deny / escalate / output_scan). Audit & Security
synthorg_auth_failures_total Counter reason Authentication rejections; reason bounded to the VALID_AUTH_FAILURE_REASONS set (invalid_password, hash_verification_error, jwt_secret_missing, token_expired, token_invalid, refresh_rejected, account_locked, unauthenticated), out-of-vocabulary folds to __other__. Sustained invalid_password / refresh_rejected is a brute-force signal. n/a (scrape-only)
synthorg_auth_lockouts_total Counter (none) Account lockouts triggered by repeated failed logins. Any sustained rate is alertable. n/a (scrape-only)

Audit chain

Metric Type Labels Description Dashboard
synthorg_audit_chain_appends_total Counter status Audit chain append operations (status bounded to signed / fallback / error). Audit & Security
synthorg_audit_chain_verifications_total Counter outcome Audit chain integrity verifications (outcome bounded to valid / broken); a non-zero broken rate is the chain-tampering alert signal. Audit & Performance
synthorg_audit_chain_depth Gauge - Current hash chain length. Audit & Security
synthorg_audit_chain_last_append_timestamp_seconds Gauge - Unix timestamp of the most recent append. Audit & Security
synthorg_security_audit_log_fill_ratio Gauge - Security audit log occupancy as a fraction of max_entries (0.0 empty, 1.0 full). Alert at 0.9: increase retention or archive older entries before the ring buffer wraps and overwrites unread evidence. Audit & Security

OTLP export health

Metric Type Labels Description Dashboard
synthorg_otlp_export_batches_total Counter kind, outcome Export batches by kind (logs / traces) and outcome (success / failure). Client Health
synthorg_otlp_export_dropped_records_total Counter kind Records dropped because the queue was full or the retry budget exhausted. Client Health

Client transport

Metric Type Labels Description Dashboard
synthorg_client_disconnects_total Counter transport, reason Client transport disconnections (transport bounded to sse / websocket / mcp_stdio / mcp_http; reason bounded to client_initiated / transport_error / cancelled / timeout). Client Health

WebSocket transport

Metric Type Labels Description Dashboard
synthorg_ws_active_connections Gauge (none) Open WebSocket connections. WebSocket Transport
synthorg_ws_connection_lifetime_seconds Histogram transport WebSocket connection lifetime by transport (transport bounded to websocket / sse; buckets 1s-4h). A collapsing p95 flags clients dropping shortly after auth. WebSocket Transport
synthorg_ws_revalidation_total Counter outcome Per-frame WS revalidation outcomes (outcome bounded to pass / fail / budget_exhausted). A sustained budget_exhausted rate is the revalidation-saturation signal (saturated peers close with 4011). WebSocket Transport

Database connection pool

Metric Type Labels Description Dashboard
synthorg_pg_pool_size Gauge backend Configured Postgres pool size (backend bounded to primary / replica). Database Connection Pool
synthorg_pg_pool_active_connections Gauge backend Connections checked out of the pool. Approaching synthorg_pg_pool_size is the saturation precursor. Database Connection Pool
synthorg_pg_pool_acquire_duration_seconds Histogram backend Wall time spent waiting for a connection (buckets 1ms-5s). Rising acquire latency precedes exhaustion. Database Connection Pool
synthorg_pg_pool_exhausted_total Counter backend Pool-acquisition timeouts (no connection available). Any non-zero rate is alertable. Database Connection Pool

Push queue + log shipping

Metric Type Labels Description Dashboard
synthorg_push_queue_events_total Counter outcome Workspace merge+push queue events (outcome bounded to enqueued / merged). A growing gap between enqueued and merged means the queue is backing up. Log Shipping & Queues
synthorg_log_sink_events_total Counter sink, outcome HTTP / syslog log-shipping sink export outcomes (sink bounded to http / syslog; outcome bounded to success / failure). A sustained failure rate means a misconfigured or unreachable shipping endpoint is dropping records. Log Shipping & Queues

Identity + workflow

Metric Type Labels Description Dashboard
synthorg_agent_identity_version_changes_total Counter change_type Identity-version lifecycle events (change_type bounded to created / updated / rolled_back / archived). The agent_id rides as an OpenMetrics exemplar (registry-bound), not a label. Audit & Security
synthorg_workflow_execution_seconds Histogram workflow_definition_id (registry-bound), status Workflow execution duration (status bounded to completed / failed / cancelled / timeout; buckets 0.5s-3600s). Workflows

Decisions

Metric Type Labels Description Dashboard
synthorg_approval_decisions_total Counter outcome Approval-gate terminal decisions (outcome bounded to approved / rejected / expired). Decisions
synthorg_blueprint_instantiations_total Counter outcome Workflow blueprint instantiation attempts (outcome bounded to success / validation_error / not_found / unknown_error). Decisions
synthorg_autonomy_promotion_decisions_total Counter outcome Autonomy-promotion workflow terminal decisions by bounded outcome (granted / denied). Decisions

Configuration & MCP

Metric Type Labels Description Dashboard
synthorg_settings_mutations_total Counter namespace Settings mutations by namespace (namespace bounded to the settings-namespace allowlist, one entry per settings/definitions/ file). Configuration & MCP
synthorg_mcp_handler_outcomes_total Counter tool, outcome MCP handler invocations by tool (outcome bounded to success / error / validation_error / guardrail_violated / not_found / capability_unsupported). Configuration & MCP
synthorg_mcp_handler_duration_seconds Histogram tool, outcome MCP handler invocation duration by tool and outcome (buckets 1ms-10s). Configuration & MCP

Suggested PromQL queries

Saturation / backlog

# Workflow p95 latency exceeds 60s
histogram_quantile(0.95, sum by (le) (rate(synthorg_workflow_execution_seconds_bucket[5m]))) > 60

Cost / budget

# Burned 80% of the monthly budget (gate on full measurability -- see below)
synthorg_budget_used_percent > 80 and ignoring(state) synthorg_budget_spend_measurability{state="measured"} == 1

# Total accumulated cost (per-agent breakdown lives in the REST cost API / logs)
synthorg_cost_total

synthorg_budget_used_percent reads 0 whenever the period's spend is not fully metered (any flat-rate connection in the mix, or nothing metered at all), so an unqualified > N alert can silently never fire on such a deployment. Gate it on synthorg_budget_spend_measurability{state="measured"} == 1 as above, or watch that gauge directly to catch a deployment where the percentage has gone structurally unmeasurable. The ignoring(state) is load-bearing, and on() is the wrong tool here: the two metrics differ only by the state label, so plain and matches nothing and the alert never fires however far over budget the deployment runs. on() would fix that by matching on the empty label set, which also discards job and instance, so one target reporting measured would keep every other target's series. ignoring(state) drops the one label that differs and keeps target identity, so each target is gated on its own synthorg_budget_spend_measurability series.

Coordination health

# Coordination overhead sustained above 40% for 10 minutes
avg_over_time(synthorg_coordination_overhead_percent[10m]) > 40

# Coordination efficiency dropped below 0.5 (half of runs wasted)
avg_over_time(synthorg_coordination_efficiency[15m]) < 0.5

Identity lifecycle

# Rollback rate over the last hour (audit-relevant spike check)
sum(rate(synthorg_agent_identity_version_changes_total{change_type="rolled_back"}[1h]))

# Churn rate -- identity updates per minute
sum by (change_type) (rate(synthorg_agent_identity_version_changes_total[5m]))

API health

# 5xx rate as a fraction of total (clamp_min avoids NaN/Inf in idle windows)
sum(rate(synthorg_api_request_duration_seconds_count{status_class="5xx"}[5m]))
  / clamp_min(sum(rate(synthorg_api_request_duration_seconds_count[5m])), 1)

# Request rate by status class (histogram's auto-emitted _count series)
sum by (status_class) (rate(synthorg_api_request_duration_seconds_count[1m]))

# Error rate by RFC 9457 category
sum by (category) (rate(synthorg_api_error_classification_total[5m]))

# 5xx rate by category (internal vs rate_limit vs provider_error, etc.)
sum by (category) (rate(synthorg_api_error_classification_total{status_class="5xx"}[5m]))

Provider health

# Provider error rate per class (hot loop: rate_limit + timeout + connection)
sum by (provider, error_class) (rate(synthorg_provider_errors_total[5m]))

# Token-normalised provider error rate (error events per token volume)
sum by (provider) (rate(synthorg_provider_errors_total[5m]))
  / clamp_min(sum by (provider) (rate(synthorg_provider_tokens_total[5m])), 1)

# Provider call latency p95 by provider + call type
histogram_quantile(
  0.95,
  sum by (le, provider, call_type) (
    rate(synthorg_provider_call_duration_seconds_bucket[5m])
  )
)

# Provider call rate (the histogram's auto-emitted _count series)
sum by (provider, call_type) (rate(synthorg_provider_call_duration_seconds_count[5m]))

Cache hit rate

# Hit rate per cache (0.0-1.0)
sum by (cache_name) (rate(synthorg_cache_operations_total{outcome="hit"}[5m]))
  / clamp_min(sum by (cache_name) (rate(synthorg_cache_operations_total[5m])), 1)

# Eviction spike (may indicate undersized cache)
sum by (cache_name) (rate(synthorg_cache_operations_total{outcome="evict"}[5m]))

Autonomy promotion

# Promotion grant vs deny rate (governance throughput by outcome)
sum by (outcome) (rate(synthorg_autonomy_promotion_decisions_total[1h]))

# Denial fraction of all promotion decisions over the last day
sum(rate(synthorg_autonomy_promotion_decisions_total{outcome="denied"}[1d]))
  / clamp_min(sum(rate(synthorg_autonomy_promotion_decisions_total[1d])), 1)

Security posture

# Denial rate (should be low; spike indicates policy tightening or attack)
rate(synthorg_security_evaluations_total{verdict="deny"}[5m])

# Escalation rate per minute
rate(synthorg_security_evaluations_total{verdict="escalate"}[1m])

Audit chain health

# Append-error rate (non-zero = signing pipeline is broken)
rate(synthorg_audit_chain_appends_total{status="error"}[5m])

# Seconds since last append (flat line for > 5m is suspicious)
time() - synthorg_audit_chain_last_append_timestamp_seconds

# Audit log fill ratio (alert when the ring buffer is near capacity).
# At >0.9 the next bursts of activity overwrite the oldest entries
# before an operator can read them; rotate retention or archive.
synthorg_security_audit_log_fill_ratio

Audit log fill ratio

The synthorg_security_audit_log_fill_ratio gauge reports the occupancy of the in-memory security audit log as a fraction of its configured max_entries capacity. The log is a ring buffer: once full, the oldest entries are overwritten as new audit events land. A sustained value above 0.9 means the buffer is about to wrap; any unread evidence beyond that point is permanently lost.

Recommended alert rule:

- alert: SynthorgSecurityAuditLogNearCapacity
  expr: synthorg_security_audit_log_fill_ratio > 0.9
  for: 10m
  labels: {severity: warning}
  annotations:
    summary: "Security audit log is {{ $value | humanizePercentage }} full"
    runbook: "increase max_entries, archive entries to long-term storage, or shorten retention"

Grafana panel definition (drop into the Audit & Security row of monitoring/grafana/synthorg-overview.json):

{
  "title": "Security audit log fill ratio",
  "type": "gauge",
  "datasource": "${DS_PROMETHEUS}",
  "fieldConfig": {
    "defaults": {
      "min": 0,
      "max": 1,
      "unit": "percentunit",
      "thresholds": {
        "mode": "absolute",
        "steps": [
          {"color": "green", "value": null},
          {"color": "yellow", "value": 0.75},
          {"color": "red", "value": 0.9}
        ]
      }
    }
  },
  "targets": [
    {"expr": "synthorg_security_audit_log_fill_ratio", "refId": "A"}
  ]
}

OTLP export health

# Export-failure rate per kind
sum by (kind) (rate(synthorg_otlp_export_batches_total{outcome="failure"}[5m]))

# Dropped records per kind (queue overflow or retries exhausted)
sum by (kind) (rate(synthorg_otlp_export_dropped_records_total[5m]))

Client transport health

# Disconnect rate by transport (alert on transport_error spikes)
sum by (transport, reason) (rate(synthorg_client_disconnects_total[5m]))

# Transport-error rate as a fraction of all disconnects
sum(rate(synthorg_client_disconnects_total{reason="transport_error"}[5m]))
  / clamp_min(sum(rate(synthorg_client_disconnects_total[5m])), 1)

Grafana dashboard

Import monitoring/grafana/synthorg-overview.json into any Grafana v10+ instance. The file is Grafana v10-compatible dashboard JSON (authored against the v11 editor, which emits a schema readable by v10) with a single ${DS_PROMETHEUS} template variable bound to your Prometheus data source plus one filter variable: $workflow_definition_id. There are no per-agent filter variables: per-agent metrics labels were removed for cardinality safety, so per-agent cost / task drill-downs come from the REST APIs and structured logs instead.

The dashboard organises over fifty panels into thirteen rows. Only Health & SLO is expanded by default; expand the others as needed to keep the unfiltered view scannable.

Row Default Panels
Health & SLO expanded Coordination efficiency, coordination overhead, budget utilisation, active agents
Tasks collapsed Task completion rate, task duration p50/p95, tasks-by-status, task-runs-by-outcome
Workflows collapsed Workflow duration p50/p95, workflow execution rate by status, top-N workflow definitions
Tools & Providers collapsed Tool invocation rate, tool duration p95 by tool_name, provider tokens, provider cost, provider errors by class, provider call latency p95 (synthorg_provider_call_duration_seconds)
Cost & Budget collapsed synthorg_cost_total, monthly cost, daily used %
Audit & Security collapsed Audit chain append rate, depth, last-append age, audit-log fill-ratio gauge, security verdicts, agent identity version changes, active agents by trust level, API error categories
Client Health collapsed Client disconnects by transport+reason, API request rate by status class, OTLP export batches, OTLP dropped records, cache hit rate, app info
Decisions collapsed Approval decisions/sec (synthorg_approval_decisions_total), blueprint instantiations/sec (synthorg_blueprint_instantiations_total), autonomy promotion decisions/sec (synthorg_autonomy_promotion_decisions_total)
Configuration & MCP collapsed Settings mutations/sec by namespace (synthorg_settings_mutations_total), MCP handler success rate (synthorg_mcp_handler_outcomes_total), MCP handler p95 latency by tool (synthorg_mcp_handler_duration_seconds)
Audit & Performance collapsed Audit chain signing-error rate (synthorg_audit_chain_appends_total{status="error"}), audit chain integrity over the last hour (synthorg_audit_chain_verifications_total), budget query p95 latency by query type (synthorg_budget_query_duration_seconds)
WebSocket Transport collapsed Active connections (synthorg_ws_active_connections), revalidation outcomes (synthorg_ws_revalidation_total), connection lifetime p95 by transport (synthorg_ws_connection_lifetime_seconds)
Database Connection Pool collapsed Pool size + active connections by backend (synthorg_pg_pool_size / synthorg_pg_pool_active_connections), acquire p95 (synthorg_pg_pool_acquire_duration_seconds), exhaustion rate (synthorg_pg_pool_exhausted_total)
Log Shipping & Queues collapsed Workspace push-queue events (synthorg_push_queue_events_total), HTTP / syslog sink outcomes (synthorg_log_sink_events_total)

To install via the Grafana UI: Dashboards → New → Import → Upload JSON file. Via the provisioning API: POST /api/dashboards/db with {"dashboard": <file>, "overwrite": true, "inputs": [...]}.

Alerts

The file does not ship alert rules because thresholds are deployment-specific. The suggested PromQL above is ready to drop into Prometheus' rules.yml; pair each query with a labels: severity: warning|critical and a for: duration. Example:

groups:
  - name: synthorg
    rules:
      - alert: SynthorgCoordinationOverheadHigh
        expr: avg_over_time(synthorg_coordination_overhead_percent[10m]) > 40
        for: 10m
        labels: {severity: warning}
        annotations:
          summary: "Coordination overhead is {{ $value }}%"
          runbook: "https://synthorg.io/docs/runbooks/coordination-overhead"

Logfire

Logfire's Prometheus integration can scrape the same /metrics endpoint directly; no additional wiring is required on the SynthOrg side. Follow the Logfire documentation for the Prometheus setup and point it at http://synthorg:8000/metrics. All metrics documented above will appear under the same names in Logfire dashboards.

Per-prompt-purpose alerts (operational)

These are operator-side operational alerts over your own structured-log / observability backend, in the same vein as the suggested PromQL rules above. They are NOT part of SynthOrg's opt-in product telemetry (which carries only anonymous aggregate signal and never per-entity cost), and they are NOT Prometheus rules: cost and latency sliced by prompt purpose (prompt_class_id, a PromptPurposeId) are deliberately not Prometheus labels, since a per-purpose metric label is the same cardinality hazard as a per-agent one.

The per-purpose data instead lives in the structured cost-recording log/span stream (each CostRecord carries prompt_class_id, latency_ms, cache_read_input_tokens, cache_write_input_tokens, retry_count) and the read-role GET /budget/prompt-class-breakdown REST endpoint that backs the dashboard's "Cost by prompt purpose" panel. Configure the two regression alerts as SQL queries (one row per prompt_class_id) in whatever log/observability backend you ship those structured logs to; run them on a rolling window and alert when a purpose's recent window diverges from its trailing baseline:

  • Cost regression: a purpose's spend in the last hour exceeds its trailing 7-day hourly mean by the deployment's factor (start at 3x). Group by prompt_class_id; the alert payload names the drifting purpose so operators open the breakdown panel filtered to it.
  • p95-latency regression: a purpose's p95 latency_ms in the last hour exceeds its trailing 7-day p95 by the deployment's factor (start at 2x). Group by prompt_class_id.

Thresholds are deployment-specific (low-traffic purposes are noisy on ratio alerts), so pin a minimum call-count floor per window before the ratio fires. Point both alerts at the same notification channel as the Prometheus rules above.

Further reading

  • Observability design: sink layout, correlation IDs, per-domain routing
  • Reference: errors: RFC 9457 error categories
  • src/synthorg/observability/prometheus_collector.py: canonical metric registration
  • src/synthorg/observability/prometheus_push_metrics.py: push-updated metric families
  • src/synthorg/observability/prometheus_labels.py: bounded label value sets