📊 Os 3 pilares — Logs, métricas, traces
Observabilidade moderna se apoia em três sinais: logs (eventos discretos com contexto), métricas (séries temporais agregadas) e traces (caminho de uma request por N serviços). Hermes Agent precisa dos três porque cada turno do agente envolve LLM call + tool execution + sandbox spawn — qualquer um pode falhar de jeito diferente.
💡 Quando usar cada um
- •Logs: "o que aconteceu numa request específica" — debug pós-incidente, auditoria.
- •Métricas: "como o sistema está agora vs ontem" — dashboards, alertas, SLOs.
- •Traces: "onde está o gargalo desta request lenta" — Hermes chama LLM, depois sandbox, depois LLM de novo.
# Stack canônica para Hermes em produção
┌──────────────────┐
│ Hermes Agent │
│ (Python + OTel) │
└────────┬─────────┘
│ OTLP gRPC :4317
▼
┌──────────────────┐
│ OTel Collector │ (sidecar/daemon)
│ receivers/proc │
└───┬────┬─────┬───┘
│ │ │
logs ─────┘ │ └───── traces
┌───────┐ │ ┌────────┐
│ Loki │ │ │ Tempo │
└───┬───┘ │ └───┬────┘
│ │ │
│ ┌──▼──┐ │
└──►│Graf │◄──┘
│ana │
┌──────►│ │
│ └─────┘
┌───┴────┐
│ Prom │ ◄── métricas
└────────┘
📊 Dados — RED vs USE
- RED (serviços): Rate, Errors, Duration — perfeito para o endpoint do Hermes.
- USE (recursos): Utilization, Saturation, Errors — para CPU/MEM/GPU do sandbox.
- Times maduros instrumentam ambos: RED por feature, USE por nó.
🔭 OpenTelemetry — Instrumentação no Hermes
OpenTelemetry (OTel) é o padrão CNCF para emitir telemetria. Você instrumenta o Hermes uma vez e troca o backend (Tempo, Jaeger, Datadog, Honeycomb) sem mudar código. Use o SDK Python + as instrumentações automáticas para requests, httpx, openai.
# requirements adicionais
opentelemetry-api==1.27.0
opentelemetry-sdk==1.27.0
opentelemetry-exporter-otlp==1.27.0
opentelemetry-instrumentation-httpx==0.48b0
opentelemetry-instrumentation-openai==0.30.0 # community
# hermes/telemetry.py
from opentelemetry import trace, metrics
from opentelemetry.sdk.trace import TracerProvider
from opentelemetry.sdk.trace.export import BatchSpanProcessor
from opentelemetry.exporter.otlp.proto.grpc.trace_exporter import OTLPSpanExporter
from opentelemetry.sdk.resources import Resource
resource = Resource.create({
"service.name": "hermes-agent",
"service.version": "0.7.2",
"deployment.environment": os.getenv("ENV", "prod"),
})
provider = TracerProvider(resource=resource)
provider.add_span_processor(BatchSpanProcessor(
OTLPSpanExporter(endpoint="http://otel-collector:4317", insecure=True)
))
trace.set_tracer_provider(provider)
tracer = trace.get_tracer("hermes.agent")
# uso dentro do loop do agente
with tracer.start_as_current_span("agent.turn") as span:
span.set_attribute("user.id", user_id)
span.set_attribute("model.name", model)
response = llm.complete(...)
span.set_attribute("llm.tokens.in", response.usage.prompt_tokens)
span.set_attribute("llm.tokens.out", response.usage.completion_tokens)
# otel-collector-config.yaml
receivers:
otlp:
protocols:
grpc:
endpoint: 0.0.0.0:4317
http:
endpoint: 0.0.0.0:4318
processors:
batch:
timeout: 5s
send_batch_size: 1024
memory_limiter:
check_interval: 1s
limit_mib: 512
resource:
attributes:
- key: cluster
value: prod-us-east-1
action: upsert
exporters:
otlp/tempo:
endpoint: tempo:4317
tls: { insecure: true }
prometheusremotewrite:
endpoint: http://mimir:9009/api/v1/push
loki:
endpoint: http://loki:3100/loki/api/v1/push
service:
pipelines:
traces: { receivers: [otlp], processors: [memory_limiter, batch, resource], exporters: [otlp/tempo] }
metrics: { receivers: [otlp], processors: [memory_limiter, batch, resource], exporters: [prometheusremotewrite] }
logs: { receivers: [otlp], processors: [memory_limiter, batch, resource], exporters: [loki] }
💡 Dica — Sentry para erros
OTel é excelente para traces/metrics, mas Sentry (ou GlitchTip self-hosted) ainda ganha em UX de exceções: stack trace agrupado, release tracking, source maps. Rode os dois lado a lado: OTel para latência, Sentry para erros. Custo: free tier do Sentry cobre 5k events/mês.
📈 Grafana dashboards — Templates prontos para Hermes
Você não precisa começar do zero. Use o Grafana com 3 datasources (Prometheus, Loki, Tempo) e organize em 3 dashboards: Overview (saúde geral), Per-User (debug individual), Cost (tokens × $).
# Queries PromQL essenciais para Hermes
# 1. Request rate (RED-R) por canal
sum(rate(hermes_agent_turns_total[5m])) by (channel)
# 2. Error rate (RED-E) como % do total
sum(rate(hermes_agent_turns_total{status="error"}[5m]))
/ sum(rate(hermes_agent_turns_total[5m]))
# 3. Latência P95 (RED-D) do turno completo
histogram_quantile(0.95,
sum(rate(hermes_agent_turn_duration_seconds_bucket[5m])) by (le))
# 4. Tokens/segundo por modelo
sum(rate(hermes_llm_tokens_total[5m])) by (model, direction)
# 5. Custo aproximado por minuto ($/1M tokens hardcoded)
sum(rate(hermes_llm_tokens_total{model="hermes-4-405b",direction="output"}[1m]))
* 60 * 3.0 / 1e6
# 6. Sandbox failures (alvo: <0.5%)
sum(rate(hermes_sandbox_spawn_total{status="failed"}[5m]))
/ sum(rate(hermes_sandbox_spawn_total[5m]))
🧩 Layout sugerido — Dashboard Overview
- •Row 1: 4 stat panels — turns/min, error %, P95, $/h.
- •Row 2: timeseries de RED (rate, errors, duration) com janela de 6h.
- •Row 3: heatmap de latência por modelo + breakdown de tools chamadas.
- •Row 4: tabela top-10 usuários por custo + link drill-down para Tempo.
📥 Atalho
Importe o dashboard "LLM Observability" ID 20100 do grafana.com como base e adapte os labels para os do Hermes. Economiza ~6h de design.
🎯 SLOs práticos — Latência P95, error rate, custo
SLO (Service Level Objective) é uma promessa numérica que você se compromete a cumprir. Sem SLO, alertas viram ruído. Comece com 3 SLOs por canal e ajuste depois de 2 semanas observando o real.
| SLI | SLO sugerido | Janela | Error budget/mês | Por quê |
|---|---|---|---|---|
| Latência P95 turn | < 3.0 s | 28d rolling | 5% (36 h) | Chat humano tolera 3s; acima vira "lento". |
| Error rate (5xx + tool fail) | < 1.0 % | 28d rolling | 1% (7.2 h) | 99% de respostas úteis = baseline mínimo. |
| Custo médio/request | < $0.02 | 7d rolling | 10% das requests acima | Acima disso, prompt está inflado ou modelo é caro demais. |
| Sandbox spawn success | > 99.5 % | 7d rolling | 0.5% (36 min) | Sandbox down = agente inútil para qualquer task de código. |
| Disponibilidade endpoint | 99.9 % | 30d | 43 min | Padrão de SaaS; "três noves" é alcançável em single-region. |
# Recording rule para burn rate do SLO de latência
# (Sloth ou Pyrra geram isto automaticamente a partir de YAML simples)
groups:
- name: slo-hermes-latency
interval: 30s
rules:
- record: slo:hermes_latency_p95:burnrate_5m
expr: |
(
sum(rate(hermes_agent_turn_duration_seconds_count{le="3.0"}[5m]))
/
sum(rate(hermes_agent_turn_duration_seconds_count[5m]))
)
- alert: HermesLatencySLOBurnFast
expr: slo:hermes_latency_p95:burnrate_5m < 0.95
for: 2m
labels: { severity: page }
annotations:
summary: "Queimando 14.4x do budget — <2h até exaurir"
runbook: "https://wiki/runbooks/hermes-latency"
🚨 Alerting — PagerDuty, ntfy, Discord webhook
Alerta sem ação é ruído. Cada alerta precisa de 3 coisas: severidade clara (page vs ticket), runbook linkado, owner identificável. Hermes em produção raramente justifica PagerDuty pago — comece com ntfy.sh (FOSS, $0) e escale depois.
✓ O que FAZER
- ✓Alertar em burn-rate de SLO, não em valor instantâneo.
- ✓2 níveis: page (acorda alguém) e ticket (vê de manhã).
- ✓Cada alerta com runbook em URL no annotation.
- ✓Rotear por canal: severo → ntfy/PagerDuty; warn → Discord.
✗ O que NÃO fazer
- ✗"Tudo verde" sem alerta — significa que você não tem alertas, não que está tudo bem.
- ✗Alertar em CPU > 80% sem contexto — ruído puro.
- ✗Threshold mágico ("<100ms!") sem janela e sem error budget.
- ✗PagerDuty pago para um agente com 50 usuários — overkill.
# alertmanager.yml — ntfy + Discord
global:
resolve_timeout: 5m
route:
receiver: ntfy-warn
group_by: [alertname, severity]
group_wait: 30s
group_interval: 5m
repeat_interval: 4h
routes:
- matchers: [severity="page"]
receiver: ntfy-page
repeat_interval: 30m
- matchers: [severity="ticket"]
receiver: discord-ops
receivers:
- name: ntfy-page
webhook_configs:
- url: https://ntfy.sh/hermes-prod-pages
send_resolved: true
http_config:
authorization:
type: Bearer
credentials: tk_xxxxxxxxxxx
- name: ntfy-warn
webhook_configs:
- url: https://ntfy.sh/hermes-prod-warn
- name: discord-ops
webhook_configs:
- url: https://discord.com/api/webhooks/.../...
send_resolved: true
⚠️ Cardinality bomb
NUNCA use user_id ou session_id como label de métrica Prometheus. Cada valor único cria uma nova série temporal — 10k usuários × 5 métricas = 50k séries só dessa label. Use user_id em traces e logs, nunca em métricas. Para top-N por usuário, agregue por hora num job offline.
🔍 Distributed tracing — Seguir request multi-hop
Um turno do Hermes pode envolver: webhook → router → agent loop (3 LLM calls) → sandbox spawn → tool exec → S3 read. Trace distribuído amarra tudo numa árvore com timing exato, então você vê o gargalo em 5 segundos no Grafana Tempo.
00:00:00 — Alerta dispara
P95 burn-rate > 14x
ntfy buzina no celular. Annotation linka direto para Grafana com janela já filtrada.
00:00:45 — Olho no Overview
Latência subiu, error rate normal
Heatmap mostra que só requests com tool web_search estão lentas. Hipótese: API externa degradou.
00:01:30 — Drill-down no Tempo
Trace de exemplo, span web_search = 8.4s
Child span http GET serpapi.com sozinho consome 8.1s. Confirmado: SerpAPI está lenta.
00:03:00 — Mitigar
Feature flag desabilita web_search
Agente passa a responder sem search. P95 volta a <2s em 1 ciclo de scrape. Abre ticket pós-mortem.
💡 Dica — Trace ID no log
Configure o logger para incluir trace_id e span_id em todo log estruturado (Python: opentelemetry-instrumentation-logging). No Grafana, clicar num log abre o trace correspondente em outra aba. Esse "logs ↔ traces correlation" é o maior multiplicador de produtividade de incident triage.
✅ Resumo do Módulo
Próximo módulo:
5.4 — 🔁 CI/CD para Hermes Agent: GitHub Actions buildando, testando skills e fazendo deploy automático com canary e rollback.