Docs›Segurança & Observabilidade›Tracing (MadTrace)
Segurança & Observabilidade

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 em dsn; quem recebe, armazena e exibe esses eventos (dashboards de erro/performance) é um serviço à parte, fora deste repositório. Sem um dsn apontando para um receptor compatível, ligar MADTRACE_ENABLED só gera eventos que caem no error_log local — 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 com throttle: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 do mad:trace-flush. Ao apontar MADTRACE_DSN para 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():

  1. 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 de sql_slow_ms (default 100ms).
  2. 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.
  3. Se mad.trace.sql_log=true, a mesma query também vira uma linha em mad_log_sql (model App\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_ENABLED sem dsn só te dá entradas no error_log — útil para depuração local, não para um dashboard de produção. E o receptor de referência exige Redis (throttle:redis na ingestão): sem ele a ingestão responde 500 e o spool nunca drena.
  • job_ingest por 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 por mad:trace-infra.
  • traces_sample_rate é por request, não por evento de erro. Erros (schema 1) têm o próprio sample_rate (default 1.0 = sempre captura, dentro do teto max_per_request); é a amostragem de performance (schema 2) que é fracionada por traces_sample_rate.
  • sql_log (auditoria em mad_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 o sql_log opcional 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-infra no contexto dos demais comandos mad:*.
  • Scheduler — onde os dois comandos são agendados (everyMinute()->withoutOverlapping()).
  • Hardening de filtros e ordenação (grid) — o SQL que o DB::listen observa já passou pelas allowlists da grid.