Tracing (MadTrace)
APM opt-in: erros, performance, spans, SQL/N+1, jobs/scheduler e infra — mad:trace-flush, mad:trace-infra.
Tracing (MadTrace)
MadTrace é o cliente APM/observabilidade embutido no framework: captura de
erros (schema 1) e de performance (schema 2 — transações, spans, SQL, jobs,
scheduler, infra de host), com amostragem, scrub de segredos e envio
assíncrono a um receptor HTTP externo. É uma classe global (\MadTrace,
\MadTraceSpan), sem namespace, em
packages/mad-framework/src/mad/trace/MadTrace.php.
Status: disponível, desligado por padrão. A implementação está completa e coberta por testes (
tests/Unit/MadTraceCoreTest.php,tests/Feature/MadTrace{DbListen,Flush,Job,Port}Test.php) — não é uma feature pela metade. O que não vem pronto é o backend receptor: o MadTrace só envia eventos para o endpoint configurado emdsn; quem recebe, armazena e exibe esses eventos (dashboards de erro/performance) é um serviço à parte, fora deste repositório. Sem umdsnapontando para um receptor compatível, ligarMADTRACE_ENABLEDsó gera eventos que caem noerror_loglocal — ainda útil em dev, mas sem o painel agregado.
Ativando
Tudo gira em torno de config('mad.trace'), lido uma vez no boot
(MadServiceProvider::registerTrace()) e passado para MadTrace::install()
— exceto enabled, db_listen e sql_log, que são consumidos separadamente
fora do install() (o primeiro é o próprio kill-switch; os outros dois
controlam o bridge DB::listen, abaixo).
# .env
MADTRACE_ENABLED=true
MADTRACE_DSN=https://seu-receptor.exemplo/ingest
MADTRACE_APP=mad_framework # nome da app no receptor
MADTRACE_ENV=production # default: APP_ENV
MADTRACE_MIN_LEVEL=warning # info < warning < error < fatal
MADTRACE_PERFORMANCE=true # liga o APM (schema 2)
MADTRACE_TRACES_SAMPLE_RATE=0.2 # 20% das requests "ok"; lentas (>slow_threshold_ms) sempre 100%
MADTRACE_SEND=async # shutdown | async | sync
MADTRACE_DB_LISTEN=true # bridge DB::listen -> APM + modal de debug
MADTRACE_SQL_LOG=false # opt-in: também grava DML em mad_log_sql (ver Observabilidade)
Sem dsn, o install() já avisa via error_log('[MadTrace] instalado SEM dsn...')
— modo só-local, sem perda de dados mas sem agregação.
O resto da config não é env-driven — é literal em config/mad.php e só muda
editando o arquivo: slow_threshold_ms (800), capture_sql (true),
sql_slow_ms (100), max_queries (500), spool_dir
(storage_path('madtrace-spool')) e job_ingest (false, hardcoded — ver
abaixo). Sem closures nesse bloco: config:cache quebraria; before_send só
via MadTrace::setBeforeSend().
O receptor precisa de Redis. O receptor de referência (
madbuilder-backend) protege a rota de ingestão comthrottle:redis— sem um Redis alcançável (por padrão:6379) o endpoint responde 500 e todo o spool volta como falha de envio, sem nenhum sintoma do lado do SDK além das tentativas domad:trace-flush. Ao apontarMADTRACE_DSNpara um receptor próprio, garanta o mesmo pré-requisito de infraestrutura antes de culpar o cliente.
O que é capturado
Schema 1 — erros. captureException(), captureMessage() e
captureFromErrorLog() (drop-in para error_log(): troque por
mad_trace_error_log() para os mesmos chamados também virarem eventos
estruturados). O handler de exceptions do Laravel é conectado via
reportable() no boot — qualquer exception não tratada que chega ao
ExceptionHandler é capturada automaticamente, sem precisar instrumentar
cada catch. Por padrão, scrub_keys mascara campos como password,
token, cpf, cnpj, cvv, secret, authorization antes de qualquer
payload sair do processo.
Schema 2 — performance. Uma transação por request, amostrada por
traces_sample_rate (sempre 100% se passar de slow_threshold_ms).
Dentro dela:
// Span manual — abre/fecha em volta de um trecho caro
$span = \MadTrace::startSpan('integration', 'ConsultaCEP::buscar', ['cep' => $cep]);
try {
$resultado = ConsultaCEP::buscar($cep);
$span->annotate('status', 'ok');
return $resultado;
} finally {
$span->finish();
}
// Açúcar — mesma coisa, com try/finally embutido
$resultado = \MadTrace::span('integration', 'ConsultaCEP::buscar', fn () => ConsultaCEP::buscar($cep));
startSpan() é no-op seguro fora de uma request amostrada — devolve um
MadTraceSpan com id -1 que ignora finish()/annotate() sem custo
relevante, então instrumentar código que roda fora do APM (CLI sem
performance, request não amostrada) não exige nenhum if.
Bridge DB::listen — SQL no APM (e, opcionalmente, no log)
Quando mad.trace.db_listen está ligado (default true, mas só importa com
trace.enabled=true), todo QueryExecuted de qualquer conexão (exceto
a própria log, para não auto-instrumentar o log) passa por
MadServiceProvider::bootTrace():
- Se a request está sendo amostrada (
MadTrace::apmActive()), a query e o tempo real de execução ($query->time, medido pelo próprio Laravel) vão para o span atual do APM — incluindo detecção de SQL repetido (N+1) e marca de "lenta" acima desql_slow_ms(default 100ms). - Sempre alimenta
Mad\Util\MadSqlCollector— o buffer usado pela modal de debug (mad_dump_modal) para listar os SQLs da request atual, mesmo sem o APM ligado. - Se
mad.trace.sql_log=true, a mesma query também vira uma linha emmad_log_sql(modelApp\Models\Log\Sql) — é o substituto do antigo "slog". Ver Observabilidade para a tela que lê essa tabela.
Ou seja: APM (schema 2, amostrado, enviado ao receptor) e log SQL
persistente (mad_log_sql, local, auditável via grid) são dois
consumidores independentes do mesmo DB::listen, ligados por flags
separadas (performance/traces_sample_rate vs. sql_log) — dá para ter
um sem o outro.
Jobs e scheduler
JobProcessing/JobProcessed/JobExceptionOccurred (filas) e
ScheduledTaskStarting/Finished/Failed (agendador) viram spans tipo
job, via MadTrace::beginJob($nome, $meta) / endJob($status, $exception).
Cobre tanto php artisan queue:work quanto php artisan schedule:run/
schedule:work — sem instrumentação manual no seu handle(). Note o
caveat: job_ingest no install() fica hardcoded em false no
registerTrace() — o envio por execução individual de job ainda não é
aceito pelo lado receptor; o que existe e funciona hoje é o agregado de
filas via mad:trace-infra (abaixo).
Spool + comandos artisan
Com send=async (o padrão), eventos não são enviados de forma síncrona no
fim do request — vão para um spool NDJSON em disco
(storage/madtrace-spool/spool.ndjson, locking seguro mesmo em filesystem
de rede) e dois comandos artisan, já agendados a cada minuto em
routes/console.php, drenam esse spool e coletam infra:
php artisan mad:trace-flush # envia o spool ao receptor (dsn)
php artisan mad:trace-flush --max-attempts=5 # tentativas antes de descartar 1 evento
php artisan mad:trace-infra # snapshot de host (cpu/mem/disco/load) + filas
php artisan mad:trace-infra --dry # só imprime o payload, não envia
mad:trace-infra é cross-platform (Linux via /proc, macOS via
sysctl/vm_stat, com graceful degradation pra null quando a métrica não
dá pra medir) e nunca derruba o cron — falha de coleta de uma métrica não
aborta as demais. A profundidade de fila só é medida quando a queue connection
ativa usa o driver database (introspecção de jobs/failed_jobs); outros
drivers (redis, sqs) não são instrumentados por essa via hoje.
Ver CLI para a referência completa dessas duas flags.
Caveats
- Receptor é externo — e tem infra própria. MadTrace é um SDK emissor, não
um painel. Se você não tem (ou não vai construir) um endpoint compatível com
o schema de ingestão, ligar
MADTRACE_ENABLEDsemdsnsó te dá entradas noerror_log— útil para depuração local, não para um dashboard de produção. E o receptor de referência exige Redis (throttle:redisna ingestão): sem ele a ingestão responde 500 e o spool nunca drena. job_ingestpor execução está desligado de propósito — não é bug, é decisão de escopo até o lado receptor aceitar esse formato. O que chega hoje é o agregado pormad:trace-infra.traces_sample_rateé por request, não por evento de erro. Erros (schema 1) têm o própriosample_rate(default 1.0 = sempre captura, dentro do tetomax_per_request); é a amostragem de performance (schema 2) que é fracionada portraces_sample_rate.sql_log(auditoria emmad_log_sql) é uma decisão separada de manter ligada — cresce rápido em produção com tráfego real e não tem poda automática embutida (ver os caveats em Observabilidade).
Ver também
- Observabilidade (logs) — telas de
auditoria local (
mad_log_*), incluindo a tabela que osql_logopcional do MadTrace alimenta. - Autenticação — falhas de login não
tratadas também passam pelo
reportable()capturado pelo MadTrace. - CLI —
mad:trace-flush/mad:trace-infrano contexto dos demais comandosmad:*. - Scheduler — onde os dois comandos são
agendados (
everyMinute()->withoutOverlapping()). - Hardening de filtros e ordenação (grid) — o
SQL que o
DB::listenobserva já passou pelas allowlists da grid.