Observabilidade, monitoramento e telemetria para PHP 8.1+ e Laravel — 100% local, em SQLite.
Sem banco externo, sem agente, sem SaaS. Um único arquivo, um dashboard completo.
Demo · Instalação · Uso · Laravel · Roadmap · Contribuição
Overview do PulsePHP rodando localmente — veja mais prints ↓
Prints reais do dashboard com o tráfego de exemplo (
php examples/traffic.php).
Sinais vitais do período comparados com o período anterior, gráfico de requisições e exceções sobre papel de eletrocardiograma com marcadores de deploy, rotas que precisam de atenção e erros abertos. O tema segue o sistema ou é escolhido no botão do topo.
![]() |
![]() |
| Tema claro | Tema escuro |
Cada requisição ou job vira um trace: spans, queries e chamadas externas posicionados na linha do tempo, mais as exceções e os logs daquela execução. No print, o N+1 (a mesma query em sequência) aparece a olho nu.
Exceções agrupadas por classe e local. Marque como resolvida ou ignorada; se voltar a acontecer depois de resolvida, reaparece como regressed. O detalhe mostra a frequência, as rotas afetadas, os eventos recentes (cada um abre o trace) e o stack trace.
![]() |
![]() |
A tela Storage & settings mostra o tamanho do banco, as linhas por tabela, a retenção e os deploys marcados.
Terminal — instalação via Composer, execução e tráfego de exemplo.
PulsePHP é uma biblioteca de observabilidade local para PHP 8.1+ e Laravel 9+. Ela captura requisições, exceções, queries SQL e chamadas externas, e mostra tudo em um dashboard ao vivo — usando apenas um arquivo SQLite, sem nenhuma infraestrutura externa (sem Redis, sem banco de métricas, sem serviço na nuvem).
- 🎯 Local por padrão — seus dados ficam no seu servidor, em um SQLite.
- ⚡ Leve — buffer em memória com gravação em lote, sem overhead por request.
- 🧩 Integrável — funciona standalone em PHP puro ou plugado no Laravel 9+.
- 🔐 Seguro — dashboard negado por padrão (Basic Auth + IPs + Gate).
- Buffer em memória com gravação em lote, seguro para filas, Octane e Horizon.
- Normalização e catalogação de SQL para agrupar consultas equivalentes.
- Timers para medir duração e variação de memória de operações.
- Captura automática de requisições web, exceções e jobs de fila.
- Traces: request, queries, spans, chamadas externas e exceções correlacionados, com página de detalhe.
- Detecção de N+1 e exceções agrupadas por classe e local.
- Latência p95/p99 no resumo e por rota.
- Captura de chamadas do cliente
Httpdo Laravel e do Guzzle; queries em PHP puro viaTracedPDO. - Retenção configurável, sampling e lista de caminhos ignorados.
- Alertas por webhook (JSON/Slack) e e-mail.
- Dashboard com 8 telas, busca Ctrl+K, tema claro/escuro e atualização ao vivo — assets e fontes embutidos, sem CDN.
- Erros com ciclo de vida (aberto, resolvido, ignorado, regressão), logs correlacionados e marcadores de deploy.
- Segurança por padrão: dashboard negado até configurar uma política de acesso.
- Integração opcional com Laravel 9+ via auto-discovery.
- Dados sempre locais — nada sai da sua máquina (exceto os alertas que você configurar).
| Camada | Tecnologia | Descrição |
|---|---|---|
| Core | PHP 8.1+ | Núcleo standalone, sem dependência de framework. |
| Integração | Laravel 9+ | Service provider com auto-discovery (opcional). |
| Dados | SQLite (pdo_sqlite) |
Armazenamento local em arquivo único. |
| Dashboard | PHP templates + Alpine.js + Chart.js | Telas renderizadas no servidor, CSS próprio e fontes IBM Plex embutidas; nenhum CDN. |
| Testes | PHPUnit + Testbench | Suíte automatizada em PHP 8.1 a 8.4, incluindo integração com Laravel. |
php --version # 8.1 ou superior
php -m | grep -i sqlite # extensões pdo e pdo_sqlitecomposer require jgcansi/pulse-phpRequisitos: PHP 8.1+ com as extensões pdo, pdo_sqlite e json (esta última já vem habilitada por padrão no PHP 8+).
Inicialize o Pulse uma vez no ponto de entrada da aplicação. Os helpers pulse(), pulse_metric(), pulse_start() e pulse_end() são carregados automaticamente pelo Composer:
<?php
require __DIR__ . '/vendor/autoload.php';
use PulsePHP\Pulse;
Pulse::init(__DIR__ . '/storage/pulse.sqlite', options: [
'sample_rate' => 1.0, // fração de requisições gravadas
'ignore_paths' => ['/health'], // nunca gravados (* é curinga)
'retention_days' => 7, // limpeza automática de dados antigos
]);
pulse('pedido.criado');
pulse_metric('valor_venda', 250.00, ['categoria' => 'eletronicos']);
pulse_start('relatorio.gerar');
// Gere o relatório.
pulse_end('relatorio.gerar');O repositório inclui public/dashboard.php. Configure credenciais fortes e rode o servidor embutido apenas em loopback:
# Linux / macOS
export PULSE_DASHBOARD_USER=pulse-admin
export PULSE_DASHBOARD_PASSWORD='use-um-segredo-forte'
php -S 127.0.0.1:8000 -t public
# Windows (PowerShell)
$env:PULSE_DASHBOARD_USER="pulse-admin"
$env:PULSE_DASHBOARD_PASSWORD="use-um-segredo-forte"
php -S 127.0.0.1:8000 -t publicAcesse http://127.0.0.1:8000/dashboard.php
O exemplo restringe o acesso a 127.0.0.1 e ::1; quando as variáveis Basic Auth estão configuradas, as credenciais também são exigidas. As políticas são cumulativas. Não exponha o endpoint publicamente sem autenticação, restrições de rede e HTTPS.
Em outra janela do terminal, simule requisições para ver o dashboard ganhar vida (a partir da raiz do repositório):
# Contínuo (Ctrl+C para parar)
php examples/traffic.php
# Execução controlada — útil para demos e GIFs
php examples/traffic.php --iterations=10O script grava cada request imediatamente em
storage/database.sqlite.
Com o tráfego rodando, o dashboard ganha vida em segundos: o script também marca um deploy, grava logs de requisições lentas e simula um N+1 na rota /api/v1/orders.
Em PHP puro, requisições web e exceções são capturadas automaticamente pelos coletores do Pulse::init(). Cada requisição vira um trace: tudo o que ela gerar fica correlacionado e aparece na página de detalhe do dashboard.
Queries SQL — troque o PDO por TracedPDO (mesma API). Só o SQL normalizado e a duração são gravados; os valores nunca saem da aplicação:
use PulsePHP\Database\TracedPDO;
$pdo = new TracedPDO('mysql:host=localhost;dbname=app', $user, $password);Ou registre manualmente:
Pulse::getInstance()->recordQuery('SELECT * FROM users WHERE id = 42', 1.25);Chamadas HTTP com Guzzle — adicione o middleware ao HandlerStack:
use GuzzleHttp\Client;
use GuzzleHttp\HandlerStack;
use PulsePHP\Http\GuzzleMiddleware;
$stack = HandlerStack::create();
$stack->push(GuzzleMiddleware::create(), 'pulse');
$client = new Client(['handler' => $stack]);Credenciais e query string são removidas da URL antes de gravar.
Workers de longa duração (loops de fila próprios, ReactPHP etc.) — marque cada unidade de trabalho e grave o buffer ao final:
$pulse = Pulse::getInstance();
$pulse->beginTrace('job:enviar-email', 'JOB');
// ... trabalho ...
$pulse->writeBuffer();Logs — grave linhas ligadas ao trace atual, ou plugue o handler no Monolog 3:
Pulse::getInstance()->recordLog('warning', 'Estoque baixo', ['sku' => 'A-1']);
use Monolog\Level;
use PulsePHP\Logging\PulseLogHandler;
$logger->pushHandler(new PulseLogHandler(Level::Warning));Deploys — marque cada release; ela aparece como uma linha vertical nos gráficos:
vendor/bin/pulse deploy --db=storage/pulse.sqlite --version=v1.4.0O pacote instala o binário vendor/bin/pulse. Agende-o no cron:
# Apaga telemetria com mais de 7 dias (diariamente)
vendor/bin/pulse prune --db=storage/pulse.sqlite --days=7
# Avalia alertas e notifica um webhook (a cada minuto)
vendor/bin/pulse check --db=storage/pulse.sqlite \
--webhook=https://hooks.slack.com/services/... --format=slack \
--error-rate=5 --exceptions=10 --p95=2000Regras: taxa de 5xx, número de exceções e latência p95 numa janela (--window, padrão 5 min), com cooldown (--cooldown, padrão 30 min). Use off para desligar uma regra e --dry-run para testar sem notificar.
Aplicações que instanciam o Dashboard diretamente devem configurar pelo menos uma política:
use PulsePHP\Dashboard\Dashboard;
$dashboard = new Dashboard($databasePath);
$dashboard->authorize(static fn (): bool => $currentUser->isAdmin());
$dashboard->render();O service provider é descoberto automaticamente pelo Composer. Nenhum passo extra de registro.
php artisan pulse:installO comando publica config/pulse.php e mostra os próximos passos.
Por padrão, o Dashboard fica em /pulse e o banco em storage/pulse.sqlite (ajustável via database_path em config/pulse.php).
O acesso permanece negado até você definir o Gate viewPulse:
use Illuminate\Support\Facades\Gate;
Gate::define('viewPulse', static fn (User $user): bool => $user->is_admin);Opcionalmente, combine Basic Auth e whitelist de IPs no .env:
PULSE_DASHBOARD_USER=pulse-admin
PULSE_DASHBOARD_PASSWORD=use-um-segredo-forte
PULSE_DASHBOARD_IPS=127.0.0.1,::1
PULSE_SERVICE_NAME=api-pagamentosQuando configuradas, as credenciais e a whitelist são verificadas além do Gate. O arquivo publicado também permite ajustar enabled, rota, middleware e coletores.
// routes/console.php (Laravel 11+; no 9/10 use o schedule() de app/Console/Kernel.php)
use Illuminate\Support\Facades\Schedule;
Schedule::command('pulse:prune')->daily();
Schedule::command('pulse:check')->everyMinute(); // com PULSE_ALERTS_ENABLED=trueAlertas usam PULSE_ALERT_WEBHOOK (JSON ou Slack) e/ou PULSE_ALERT_MAIL_TO (mailer do Laravel).
- Requisições dos grupos
webeapi, com nome da rota, sem query string. - Exceções reportadas pelo Laravel (HTTP, jobs e console). O que está em
dontReport(404, validação etc.) é ignorado. - Queries via
DB::listen, correlacionadas ao request ou job. - Jobs de fila viram traces próprios (
job:App\Jobs\...) com span de duração. - O buffer é gravado ao fim de cada request e de cada job, então Octane, Horizon e
queue:worknão acumulam memória. - Logs a partir de
warning(ajustável emPULSE_LOG_LEVEL), ligados ao request ou job. Logs que carregam uma exceção são pulados, porque a exceção já é registrada.
Marque deploys no seu script de deploy para vê-los nos gráficos:
php artisan pulse:deploy "$(git rev-parse --short HEAD)"As chamadas feitas com Illuminate\Support\Facades\Http aparecem como span http.outbound:{host} e na tabela de chamadas externas, com método, status e duração. Credenciais e query string são removidas da URL persistida por segurança:
use Illuminate\Support\Facades\Http;
$response = Http::withToken(config('services.stripe.secret'))
->get('https://api.stripe.com/v1/balance');Sem
pulse_start()manual ao redor das chamadas — o middleware e os eventos do cliente HTTP cuidam de tudo. A captura cobre o clienteHttpdo Laravel; para Guzzle usado diretamente, adicione oGuzzleMiddleware(veja Como instrumentar).
As rotas web e API recebem contexto com o nome da rota resolvida ou seu URI. O serviço usa PULSE_SERVICE_NAME, com fallback para APP_NAME e depois default. Bancos SQLite v1 existentes recebem as novas colunas na inicialização, sem apagar os registros anteriores.
| Variável / chave | Padrão | Descrição |
|---|---|---|
pulse.enabled |
true |
Liga/desliga toda a coleta. |
pulse.database_path |
storage/pulse.sqlite |
Caminho do banco SQLite (PULSE_DB_PATH). |
pulse.service_name |
default |
Nome do serviço exibido no dashboard. |
pulse.dashboard_path |
pulse |
Rota do dashboard no Laravel (PULSE_PATH). O endpoint ao vivo fica em {path}/data. |
pulse.dashboard_middleware |
['web'] |
Middleware aplicado à rota do dashboard. |
pulse.dashboard_user |
null |
Usuário do Basic Auth do dashboard (.env: PULSE_DASHBOARD_USER). |
pulse.dashboard_password |
null |
Senha do Basic Auth do dashboard (.env: PULSE_DASHBOARD_PASSWORD). |
pulse.dashboard_allowed_ips |
[] |
Whitelist de IPs do dashboard (.env: PULSE_DASHBOARD_IPS). |
pulse.collect_requests |
true |
Captura de requisições web. |
pulse.collect_exceptions |
true |
Captura de exceções. |
pulse.collect_queries |
true |
Captura de consultas SQL. |
pulse.collect_outbound_requests |
true |
Captura de chamadas HTTP externas (cliente Http do Laravel). |
pulse.collect_jobs |
true |
Traces e spans para jobs de fila. |
pulse.collect_logs |
true |
Grava logs do Laravel (PULSE_COLLECT_LOGS). |
pulse.log_level |
warning |
Nível mínimo de log gravado (PULSE_LOG_LEVEL). |
pulse.sample_rate |
1.0 |
Fração de requisições/jobs gravados (PULSE_SAMPLE_RATE). Exceções sempre são gravadas. |
pulse.ignore_paths |
/up,/health,... |
Caminhos nunca gravados, * é curinga (PULSE_IGNORE_PATHS). O dashboard é sempre ignorado. |
pulse.max_buffer_size |
1000 |
Registros em memória que forçam uma gravação antecipada. |
pulse.n_plus_one_threshold |
5 |
Repetições da mesma query num trace para sinalizar N+1. |
pulse.retention_days |
7 |
Dias de telemetria mantidos (PULSE_RETENTION_DAYS). |
pulse.prune_lottery |
[1, 200] |
Chance de limpar dados antigos durante uma gravação. |
pulse.alerts.* |
desligado | Regras, janela, cooldown, webhook e e-mail dos alertas (PULSE_ALERTS_ENABLED, PULSE_ALERT_*). |
composer install
vendor/bin/phpunitA suíte roda no GitHub Actions em PHP 8.1 a 8.4. Os testes de integração com Laravel (--testsuite Laravel) usam orchestra/testbench e rodam a partir do PHP 8.2.
O dashboard não tem etapa de build: o CSS e o JS ficam em src/Dashboard/resources/assets/ e são servidos pelo próprio PHP com cache.
...................................................................... 70 / 70 (100%)
OK (70 tests, 420 assertions)
- Núcleo standalone com buffer e flush em lote
- Normalização e catalogação de SQL
- Dashboard ao vivo com filtros e widgets
- Integração opcional com Laravel 9+
- Retenção configurável e limpeza do banco
- Alertas por e-mail / webhook
- Traces por request/job, detecção de N+1 e percentis
- Exportação de traces em formato OpenTelemetry
- Captura de comandos Artisan agendados
Veja os issues abertos para o plano completo.
Contribuições são muito bem-vindas! Abra uma issue para discutir ideias ou envie um pull request.
# Fork -> branch -> commit -> pull request
git clone https://github.com/Khyarus/pulse-php.git
cd pulse-php
composer install
git checkout -b feat/minha-feature
git commit -m "feat: adiciona minha feature"
git push origin feat/minha-feature- Faça um fork do projeto.
- Crie uma branch (
git checkout -b feat/minha-feature). - Rode a suíte (
vendor/bin/phpunit) antes de abrir o PR. - Abra um pull request descrevendo a mudança.
Distribuído sob a Licença MIT. Veja LICENSE para detalhes.
Criado e mantido por João Gabriel Cansi Silveira.









