Observabilidade - Pilot Status
Stack LGTM (Self-Hosted)
O projeto utiliza a stack Grafana LGTM para observabilidade:
- Loki - Agregação de logs
- Grafana - Visualização e dashboards
- Tempo - Distributed tracing
- Mimir/Prometheus - Métricas
- Alloy - Coleta e roteamento de sinais (OTLP receiver)
Subir a Stack
# Subir toda a stack de observabilidade
npm run obs:up
# Derrubar (remove volumes)
npm run obs:down
# Ver logs
npm run obs:logs
Acessos padrão:
- Grafana: http://localhost:3001 (admin/admin)
- Prometheus: http://localhost:9090
- Tempo: http://localhost:3200
- Loki: http://localhost:3100
- Alloy UI: http://localhost:12345
Apontar o App para o Alloy Local
No .env.development ou variáveis de ambiente:
OTEL_EXPORTER_OTLP_ENDPOINT=http://localhost:4318
OTEL_EXPORTER_OTLP_PROTOCOL=http/protobuf
OTEL_TRACES_SAMPLER_ARG=1.0
OTEL_LOGS_ENABLED=true
OTEL_NODE_EXPERIMENTAL_SDK_METRICS=true
Arquitetura de Instrumentação
Pacote Compartilhado: packages/shared/src/observability/
observability/
index.ts # Re-exports
init.ts # initObservability() - NodeSDK bootstrap
tracer.ts # withSpan() helper para Use Cases
metrics.ts # Counters e Histograms de negócio
logger.ts # OTelLogger com correlação trace↔log
bullmq-trace.ts # injectTraceContext/extractTraceContext
pii-span-processor.ts # Redaction de PII em spans
resource.ts # resolveInstanceId, getResourceAttributes
sampling.ts # ParentBasedSampler config
Padrão de Instrumentação por Camada
| Camada | Instrumentação |
|--------|---------------|
| Controllers (route.ts) | Auto-instrumentation HTTP (via @opentelemetry/auto-instrumentations-node) |
| Use Cases | withSpan("UseCase.XYZ", { attrs }, fn) manual |
| Adapters Prisma | @prisma/instrumentation (auto) |
| Adapters HTTP | Instrumentation undici/fetch (auto) |
| BullMQ | injectTraceContext(jobData) no producer + runBullJobWithTrace(queueName, job, fn) no worker |
| RabbitMQ | @opentelemetry/instrumentation-amqplib (auto) |
| Redis/ioredis | Instrumentation ioredis (auto) |
Métricas de Negócio
Prefixo pilot_*:
pilot_messages_sent_total{instance, status}pilot_webhook_events_total{source, channel, outcome}pilot_ai_request_duration_seconds{provider, model}pilot_use_case_duration_seconds{module, use_case}pilot_job_duration_seconds{queue_name}
Spans de Negócio
Nomeados como UseCase.<Name>:
UseCase.SendMessageUseCase.SyncStripePeriodUseCase.RenewFreePlanUseCase.ConnectInstanceUseCase.SyncInstanceStatus
Correlação Trace↔Log
O OTelLogger anexa automaticamente trace_id e span_id em cada log quando há um span ativo.
Variáveis de Ambiente
| Variável | Default | Descrição |
|----------|---------|-----------|
| OTEL_EXPORTER_OTLP_ENDPOINT | (vazio = desabilitado) | URL do OTLP receiver (Alloy) |
| OTEL_EXPORTER_OTLP_PROTOCOL | http/protobuf | Protocolo de export |
| OTEL_SERVICE_NAME | automático | pilot-fullstack ou pilot-worker |
| OTEL_TRACES_SAMPLER | parentbased_traceidratio | always_on, always_off, parentbased_always_on, parentbased_traceidratio |
| OTEL_TRACES_SAMPLER_ARG | 0.1 | Taxa de sampling raiz (0.0-1.0) quando sampler usa ratio |
| OTEL_LOGS_ENABLED | false (dev compose) | Somente true ou 1 habilita LoggerProvider OTLP (além do JSON em stdout) |
| OTEL_NODE_EXPERIMENTAL_SDK_METRICS | — | Defina true em produção para registrar o MeterProvider do NodeSDK (métricas OTLP) |
| OTEL_SERVICE_INSTANCE_ID | HOSTNAME | ID único da instância |
| APP_VERSION | dev | Versão do serviço |
Rodando Múltiplas Réplicas
Cada réplica recebe automaticamente service.instance.id (resolvido de OTEL_SERVICE_INSTANCE_ID > HOSTNAME > UUID).
Para distinguir réplicas no Grafana:
- Logs: filtrar por
{service_instance_id="xxx"} - Métricas: usar
sum by (service_name, instance)para ver desbalanceamento - Traces: cada span carrega
service.instance.id
Política de PII
O PiiRedactingSpanProcessor redactiona automaticamente atributos cujo nome contem palavras sensíveis: token, api_key, secret, password, authorization, cookie, session, phone, email, content, etc.
O OTelLogger aplica a mesma redaction nos campos de metada dos logs.
Dashboards Provisionados
- RED Overview (
pilot-red-overview) - Request rate, duration p99, error rate por rota + métricas de mensagens, webhooks, AI, use cases - Worker & BullMQ (
pilot-worker-bullmq) - Job duration, messages sent por instância, traces recentes, logs do worker
Adicionar Instrumentação em Novos Use Cases
import { withSpan } from "@pilot-status/shared";
export class MyNewUseCase {
async execute(input: MyInput): Promise<MyOutput> {
return withSpan("UseCase.MyNew", {
"my.input_id": input.id,
}, async () => this._execute(input));
}
private async _execute(input: MyInput): Promise<MyOutput> {
// ... lógica de negócio
}
}
Propagar Trace Context em Jobs BullMQ
No producer (enfileirar job):
import { injectTraceContext } from "@pilot-status/shared";
const jobData = injectTraceContext({ myData: "value" });
await queue.add("job-name", jobData);
No consumer (handler), prefira o helper que restaura o contexto e abre o span BullMQ.<queue>.<jobName>:
import { runBullJobWithTrace } from "@pilot-status/shared";
await runBullJobWithTrace(MY_QUEUE_NAME, job, async () => {
// processar job
});
Prometheus e remote write
O Alloy envia métricas OTLP convertidas via prometheus.remote_write para http://prometheus:9090/api/v1/write. O serviço Prometheus na stack local sobe com --web.enable-remote-write-receiver para aceitar esse endpoint (ver docker-compose.observability.yml).