Myrmex Hive

Myrmex Hive

[Secure Agent Orchestrator & Gateway]

Observability: Prometheus metrics

The gateway can expose a Prometheus exposition endpoint at /metrics. It is opt-in: with nothing configured the route is not registered and the gateway behaves exactly as before.

Enabling

{
  "metrics_enabled": true,
  "metrics_poll_seconds": 30
}

Scraping

/metrics sits behind the same requireAuth middleware as every other API path — a scraper must present a bearer token. The fleet topology it exposes (agent IDs, counts, tool usage) is not public data, and there is deliberately no unauthenticated bypass.

Any role reaches it; use read-only, the least privilege that works:

{
  "tokens": { "s3cr3t-scrape-token": "read-only" }
}
# prometheus.yml
scrape_configs:
  - job_name: myrmex-gateway
    scheme: https
    authorization:
      credentials: s3cr3t-scrape-token
    tls_config:
      # The gateway generates a self-signed cert in-memory unless tls.enabled
      # pins a real one. Drop this once you have a proper certificate.
      insecure_skip_verify: true
    static_configs:
      - targets: ['gateway.internal:8080']

Verify by hand:

curl -sk -H 'Authorization: Bearer s3cr3t-scrape-token' https://localhost:8080/metrics

Metrics

Metric Type Labels Meaning
myrmex_gateway_info gauge version, gateway_id Always 1; carries build/identity as labels.
myrmex_agents_connected gauge Agents with an open SSH tunnel to this gateway.
myrmex_agent_online gauge agent_id 1 if last-seen is within the liveness window, else 0.
myrmex_agent_cpu_usage_percent gauge agent_id Newest polled CPU sample. Requires metrics_poll_seconds > 0.
myrmex_agent_mem_used_percent gauge agent_id Newest polled memory sample. Requires metrics_poll_seconds > 0.
myrmex_agent_disk_used_percent gauge agent_id Newest polled disk sample. Requires metrics_poll_seconds > 0.
myrmex_upstream_up gauge server 1 if the upstream MCP server reports connected.
myrmex_agent_alert_breached gauge agent_id, dimension 1 if the last polled sample breached the configured threshold. Requires alert_thresholds.
myrmex_tool_calls_total counter agent, tool, status Operator-initiated tool calls.
myrmex_tool_call_duration_seconds histogram agent, tool Gateway-observed latency, dispatch to response.
myrmex_peer_forwards_total counter status Calls forwarded to a peer gateway holding the target agent.
myrmex_alert_deliveries_total counter target, status Outbound alert deliveries (see Alert routing).

Gateway-native tools appear with agent="gateway" (e.g. tool="ask_gemma"), matching the gateway__ namespace.

What the numbers mean (and don’t)

Worth knowing before you build alerts on these:

Grafana dashboard

An example dashboard is shipped at dashboards/myrmex-hive.json: fleet size and connectivity, tool-call rate/error-ratio/latency percentiles, per-agent CPU/memory/disk, upstream health, threshold breaches, alert-delivery failures, and HA peer forwards.

Import it:

  1. Grafana → DashboardsNewImport
  2. Upload dashboard JSON file, pick dashboards/myrmex-hive.json
  3. Select your Prometheus datasource when prompted for DS_PROMETHEUS
  4. Import

Or via the API:

curl -X POST http://grafana:3000/api/dashboards/import \
  -H 'Content-Type: application/json' \
  -H "Authorization: Bearer $GRAFANA_TOKEN" \
  -d "{\"dashboard\": $(cat dashboards/myrmex-hive.json), \"overwrite\": true,
       \"inputs\": [{\"name\": \"DS_PROMETHEUS\", \"type\": \"datasource\",
                     \"pluginId\": \"prometheus\", \"value\": \"Prometheus\"}]}"

Provision it declaratively by mounting the file into Grafana’s dashboard provisioning path (/etc/grafana/provisioning/dashboards/).

Panels that stay empty are telling you something real:

The dashboard is kept in sync with the exported metric names by a test (cmd/gateway/dashboard_test.go), which fails the build if a panel references a metric /metrics does not export, or if an exported metric has no panel. A dashboard drifting out of sync renders empty panels, which reads as “no traffic” rather than “broken dashboard” — hence the check.

Useful queries

# Tool call error rate over 5m
sum(rate(myrmex_tool_calls_total{status="error"}[5m]))
  / sum(rate(myrmex_tool_calls_total[5m]))

# p95 latency per tool
histogram_quantile(0.95,
  sum by (le, tool) (rate(myrmex_tool_call_duration_seconds_bucket[5m])))

# Agents that dropped off
myrmex_agent_online == 0

Alert routing (webhook / Alertmanager)

The gateway’s threshold alerts (alert_thresholds) go to the log and the signed audit trail by default. They can additionally be routed to on-call systems. Both targets are opt-in; with neither set, nothing changes.

{
  "metrics_poll_seconds": 30,
  "alert_thresholds": { "cpu_percent": 90, "mem_percent": 90, "disk_percent": 85 },
  "alert_webhook_url": "https://hooks.example.com/myrmex",
  "alertmanager_url": "http://alertmanager:9093",
  "alert_delivery_retries": 3
}

Set either, or both — each configured target receives every alert. Delivery is asynchronous, so a slow or dead receiver never stalls the metrics poller.

Generic webhook

POSTs application/json:

{
  "agent_id": "web-1",
  "dimension": "cpu",
  "status": "firing",
  "value": 91.5,
  "threshold": 90,
  "timestamp": "2026-07-14T17:26:14Z",
  "gateway_id": "gw-1"
}

status is firing on breach onset and resolved on recovery.

Alertmanager

POSTs to <alertmanager_url>/api/v2/alerts (give the base URL; the path is appended). Alerts carry alertname=MyrmexThresholdBreach plus agent_id, dimension, gateway_id and severity labels.

Alertmanager has no “resolved” field — an alert resolves via endsAt. Firing alerts deliberately omit endsAt so Alertmanager applies its own resolve_timeout; resolved alerts set it. Route on the labels:

route:
  routes:
    - matchers: [ alertname="MyrmexThresholdBreach" ]
      receiver: myrmex-oncall

Delivery, retries and failure

Authenticating to the receiver

Most on-call systems want a token. Send arbitrary headers with each delivery:

{
  "alert_webhook_url": "https://events.pagerduty.com/v2/enqueue",
  "alert_webhook_headers": {
    "Authorization": "env:PAGERDUTY_TOKEN",
    "X-Routing-Key": "team-sre"
  },
  "alertmanager_url": "https://alertmanager.internal",
  "alertmanager_headers": { "Authorization": "file:/run/secrets/am-token" }
}

Header values resolve through the same secret indirection as llm_api_keyenv: / file: / agenix: / vault: — so the token never has to sit in the config file. Header names are not secrets and are used as-is.

Notes:

TLS: the system trust store is used, with no custom-CA or skip-verify option. A receiver with a private certificate is not supported yet.

Distributed tracing (OpenTelemetry)

Tool calls, Gemma orchestration, upstream proxying and peer forwarding can be traced and exported over OTLP/HTTP. Opt-in; with tracing_enabled unset no tracer provider is installed, every span call site is a no-op, and no exporter goroutine or network call exists.

{
  "tracing_enabled": true,
  "otlp_endpoint": "otel-collector:4318",
  "otlp_insecure": true,
  "trace_service_name": "myrmex-gateway",
  "trace_sample_ratio": 1.0,
  "otlp_headers": { "authorization": "env:OTLP_TOKEN" }
}

Spans

Span Parent Covers
mcp.tool_call root (or the forwarding peer) The whole operator tool call, any transport. Attributes: myrmex.agent_id, myrmex.tool.
mcp.agent_call mcp.tool_call The SSH-tunnel hop — separates time on the agent from gateway overhead.
mcp.upstream_call mcp.tool_call Proxying to an upstream MCP server.
mcp.peer_forward mcp.tool_call Forwarding to the peer gateway holding the agent. Injects W3C traceparent.
gemma.orchestration mcp.tool_call The whole multi-step LLM loop.
gemma.step gemma.orchestration One step — shows which tool call was slow, not just a slow total.

Resource attributes carry service.name, service.version and myrmex.gateway_id, so a trace can be attributed to one gateway in an HA mesh.

A failing call sets the span status to error with the JSON-RPC error, using the same definition of “error” as myrmex_tool_calls_total — traces and metrics agree. (Which means the same caveat applies: an allowlist rejection is a well-formed result and is not an error here.)

Propagation

Verifying

docker run --rm --network host \
  -v ./collector.yaml:/etc/otelcol/config.yaml \
  otel/opentelemetry-collector:latest --config /etc/otelcol/config.yaml

with a debug exporter at verbosity: detailed; a tool call then prints its spans. A dead collector is never fatal — the exporter retries and logs [TRACE] exporter error: ... while the gateway keeps serving.

Implementation note

The Prometheus exposition is written by hand (cmd/gateway/metrics.go) rather than via prometheus/client_golang: the text format is a handful of Fprintf calls, so a client library would drag a transitive tree through vendor/ for no functional gain. If the metric surface grows substantially, revisit.

Tracing went the other way and uses the real OTel SDK, which is most of the ~21MB vendor/. That was a deliberate trade: unlike a text exposition, tracing has genuinely subtle parts — W3C context propagation, parent-based sampling, batching, retry — where reimplementing the spec buys bugs we own rather than saving bytes.