Manutenção orientada a sinais: por que logs, métricas e rastreamento importam
Em produção, a maior parte dos problemas não é “como implementar”, mas “como entender rapidamente o que aconteceu”. Para isso, use três tipos de sinais: logs (eventos), métricas (tendências/quantidades) e rastreamento (caminho de uma requisição). A ideia aqui é manter o mínimo necessário para diagnosticar falhas e evoluir o projeto com segurança, sem adicionar complexidade desproporcional.
Logging estruturado no Slim: eventos com contexto e consistência
O que é logging estruturado
Logging estruturado é registrar eventos em formato consistente (geralmente JSON), com campos fixos que permitem filtrar e correlacionar ocorrências. Em vez de mensagens soltas, você registra request_id, user_id, route, method, status_code, duration_ms, entre outros.
Campos recomendados (mínimo útil)
timestamp: gerado pelo loggerlevel: debug/info/warn/errormessage: descrição curta do eventorequest_id: id único por requisição (correlação)user_id: quando autenticado (ounull)route: padrão da rota (ex.:GET /v1/orders/{id})method: GET/POST/PUT/DELETEstatus_code: 200/400/500…duration_ms: tempo total da requisiçãoipeuser_agent: úteis para auditoria/abusoerror: objeto comtype,message,stack(somente em ambiente controlado)
Boas práticas de níveis (debug/info/warn/error)
| Nível | Quando usar | Exemplos |
|---|---|---|
debug | Detalhes para diagnóstico local/temporário | payload sanitizado, decisões internas, cache hit/miss |
info | Eventos esperados e relevantes | requisição finalizada, job concluído, login bem-sucedido |
warn | Algo inesperado, mas recuperável | timeout em serviço externo com fallback, tentativa inválida repetida |
error | Falha que impede o fluxo esperado | exceção não tratada, falha de conexão persistente, 5xx |
Evite: (1) logar dados sensíveis (senha, token, cartão), (2) logar o corpo inteiro de requisições/respostas em produção, (3) usar error para validações de cliente (isso é warn ou info, dependendo do caso).
Passo a passo: request-id, contexto e logs por requisição
1) Gerar/propagar um request-id
O request_id deve ser aceito se vier do cliente (ex.: header X-Request-Id) ou gerado no início da requisição. Também é boa prática devolvê-lo na resposta para facilitar suporte.
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
final class RequestIdMiddleware implements MiddlewareInterface
{
public function process(Request $request, Handler $handler): Response
{
$requestId = $request->getHeaderLine('X-Request-Id');
if ($requestId === '') {
$requestId = bin2hex(random_bytes(16));
}
$request = $request->withAttribute('request_id', $requestId);
$response = $handler->handle($request);
return $response->withHeader('X-Request-Id', $requestId);
}
}2) Criar um logger com formato JSON (Monolog) e injetar no container
Um logger JSON facilita filtros e dashboards. Abaixo, um exemplo de factory simples para Monolog com JsonFormatter.
- Ouça o áudio com a tela desligada
- Ganhe Certificado após a conclusão
- + de 5000 cursos para você explorar!
Baixar o aplicativo
<?php
use Monolog\Logger;
use Monolog\Handler\StreamHandler;
use Monolog\Formatter\JsonFormatter;
use Psr\Log\LoggerInterface;
return [
LoggerInterface::class => function () {
$logger = new Logger('app');
$handler = new StreamHandler(__DIR__ . '/../var/log/app.log', Logger::INFO);
$handler->setFormatter(new JsonFormatter());
$logger->pushHandler($handler);
return $logger;
},
];Em desenvolvimento, você pode reduzir o nível para DEBUG. Em produção, comece com INFO e eleve para DEBUG apenas de forma temporária e controlada.
3) Middleware de logging de requisição (com duração e rota)
Registre um log por requisição finalizada. Isso cria uma trilha confiável para auditoria e troubleshooting.
<?php
use Psr\Http\Message\ServerRequestInterface as Request;
use Psr\Http\Server\RequestHandlerInterface as Handler;
use Psr\Http\Server\MiddlewareInterface;
use Psr\Http\Message\ResponseInterface as Response;
use Psr\Log\LoggerInterface;
final class AccessLogMiddleware implements MiddlewareInterface
{
public function __construct(private LoggerInterface $logger) {}
public function process(Request $request, Handler $handler): Response
{
$start = microtime(true);
$response = $handler->handle($request);
$durationMs = (int) round((microtime(true) - $start) * 1000);
$route = $request->getAttribute('__route__');
$routePattern = is_object($route) && method_exists($route, 'getPattern')
? $route->getPattern()
: null;
$this->logger->info('request_completed', [
'request_id' => $request->getAttribute('request_id'),
'user_id' => $request->getAttribute('user_id'),
'method' => $request->getMethod(),
'route' => $routePattern,
'path' => (string) $request->getUri()->getPath(),
'status_code' => $response->getStatusCode(),
'duration_ms' => $durationMs,
'ip' => $request->getServerParams()['REMOTE_ADDR'] ?? null,
'user_agent' => $request->getHeaderLine('User-Agent') ?: null,
]);
return $response;
}
}Observação prática: o atributo __route__ depende de como o Slim expõe a rota no request. Se não estiver disponível, registre path e method. O mais importante é a consistência do campo.
4) Logs de domínio e de integração (sem poluir handlers)
Além do access log, registre eventos relevantes do domínio e integrações externas. Regra simples: um log por decisão importante, não por linha de código. Exemplo em um serviço de aplicação:
<?php
final class PaymentService
{
public function __construct(private Psr\Log\LoggerInterface $logger) {}
public function capture(string $orderId, int $amountCents, string $requestId): void
{
$this->logger->info('payment_capture_started', [
'request_id' => $requestId,
'order_id' => $orderId,
'amount_cents' => $amountCents,
]);
// ... chamada ao gateway
$this->logger->info('payment_capture_succeeded', [
'request_id' => $requestId,
'order_id' => $orderId,
]);
}
}Métricas simples: tempo de resposta por rota (sem stack complexa)
O que medir primeiro
- latência por rota (p50/p95/p99 quando possível)
- taxa de erro (4xx e 5xx por rota)
- volume (requisições por minuto)
Mesmo sem Prometheus/StatsD, você pode começar com uma abordagem pragmática: registrar métricas como logs estruturados (eventos do tipo metric) e agregá-las depois no seu stack de logs.
Passo a passo: emitir métrica como log
<?php
final class MetricsMiddleware implements Psr\Http\Server\MiddlewareInterface
{
public function __construct(private Psr\Log\LoggerInterface $logger) {}
public function process(Psr\Http\Message\ServerRequestInterface $request, Psr\Http\Server\RequestHandlerInterface $handler): Psr\Http\Message\ResponseInterface
{
$start = microtime(true);
$response = $handler->handle($request);
$durationMs = (int) round((microtime(true) - $start) * 1000);
$route = $request->getAttribute('__route__');
$routePattern = is_object($route) && method_exists($route, 'getPattern')
? $route->getPattern()
: 'unknown';
$this->logger->info('metric.http_request', [
'request_id' => $request->getAttribute('request_id'),
'metric_type' => 'timing',
'name' => 'http_request_duration_ms',
'value' => $durationMs,
'tags' => [
'route' => $routePattern,
'method' => $request->getMethod(),
'status' => (string) $response->getStatusCode(),
],
]);
return $response;
}
}Com isso, você consegue filtrar por name e tags.route e calcular médias/p95 no seu agregador de logs. Quando o projeto crescer, você substitui a emissão por um cliente de métricas real mantendo a mesma interface (contrato) de emissão.
Rastreamento básico: correlação ponta a ponta sem “tracing distribuído” completo
Objetivo do rastreamento básico
Sem adotar OpenTelemetry de imediato, você ainda pode obter grande parte do benefício com: request-id + logs consistentes + propagação do id para chamadas externas.
Passo a passo: propagar request-id em chamadas HTTP
Se você usa um cliente HTTP (ex.: Guzzle), inclua o header X-Request-Id em todas as chamadas. Assim, logs do serviço externo (ou do seu próprio serviço downstream) podem ser correlacionados.
<?php
final class ExternalApiClient
{
public function __construct(private GuzzleHttp\Client $client) {}
public function fetchSomething(string $requestId): array
{
$resp = $this->client->request('GET', '/something', [
'headers' => [
'X-Request-Id' => $requestId,
],
'timeout' => 2.0,
]);
return json_decode((string) $resp->getBody(), true);
}
}Span “manual” opcional (medir trechos críticos)
Para trechos críticos (consulta pesada, integração lenta), registre um evento com span e duration_ms. Isso já ajuda a localizar gargalos.
<?php
$start = microtime(true);
try {
$result = $repo->findById($id);
$logger->info('span.db_find_order', [
'request_id' => $requestId,
'duration_ms' => (int) round((microtime(true) - $start) * 1000),
'order_id' => $id,
]);
} catch (Throwable $e) {
$logger->error('span.db_find_order_failed', [
'request_id' => $requestId,
'duration_ms' => (int) round((microtime(true) - $start) * 1000),
'order_id' => $id,
'error' => ['type' => get_class($e), 'message' => $e->getMessage()],
]);
throw $e;
}Refatorar com segurança: manter contratos e reduzir risco
Princípio: refatoração não muda comportamento observável
Quando você refatora, o objetivo é melhorar estrutura interna sem quebrar o que consumidores dependem. Em APIs, “contrato” inclui: rota, método, status codes, formato do JSON, nomes de campos, regras de validação, e semântica (o que significa cada campo).
Checklist prático antes de refatorar
- Defina o contrato atual: exemplos reais de request/response (incluindo erros) e casos de borda.
- Proteja com testes de contrato: snapshots de JSON ou asserts de schema/estrutura.
- Adicione observabilidade antes da mudança: logs e métricas para comparar comportamento.
- Refatore em passos pequenos: extraia funções, introduza interfaces, mova código por camadas gradualmente.
- Evite mudanças “mistas”: não refatore e altere regra de negócio no mesmo PR.
Padrão Strangler (estrangular aos poucos) para trocar implementações
Quando precisa substituir uma implementação (ex.: repositório, integração, regra), mantenha uma interface e troque o “miolo” por trás dela. Exemplo: introduzir uma interface e duas implementações (antiga e nova) com feature flag.
<?php
interface OrderRepository
{
public function getById(string $id): array;
}
final class LegacyOrderRepository implements OrderRepository
{
public function getById(string $id): array { /* ... */ }
}
final class NewOrderRepository implements OrderRepository
{
public function getById(string $id): array { /* ... */ }
}
final class OrderRepositoryFactory
{
public function __construct(private bool $useNew) {}
public function create(): OrderRepository
{
return $this->useNew ? new NewOrderRepository() : new LegacyOrderRepository();
}
}Com isso, você consegue ativar a nova implementação gradualmente, comparar métricas (latência/erros) e reverter rapidamente.
Versionamento de rotas: evoluir API sem quebrar clientes
Estratégias comuns
- Versionamento por path:
/v1,/v2(simples e explícito). - Versionamento por header: mais flexível, mas aumenta complexidade operacional.
Para a maioria dos projetos, path versioning é o melhor custo-benefício.
Passo a passo: organizar grupos de rotas por versão
<?php
$app->group('/v1', function ($group) {
$group->get('/orders/{id}', V1\GetOrderAction::class);
$group->post('/orders', V1\CreateOrderAction::class);
});
$app->group('/v2', function ($group) {
$group->get('/orders/{id}', V2\GetOrderAction::class);
$group->post('/orders', V2\CreateOrderAction::class);
});Compatibilidade: mudanças permitidas vs. mudanças quebradoras
| Tipo | Exemplo | Impacto |
|---|---|---|
| Compatível | Adicionar campo novo opcional no JSON | Clientes antigos ignoram |
| Compatível | Adicionar endpoint novo | Sem impacto |
| Quebradora | Renomear/remover campo | Quebra parsing |
| Quebradora | Mudar semântica de um campo (ex.: unidade) | Erros silenciosos |
| Quebradora | Mudar status code esperado | Fluxos de cliente falham |
Quando precisar de mudança quebradora, publique em /v2 e mantenha /v1 por um período com monitoramento de uso (métrica de volume por versão).
Evitar dívida técnica: padrões de código e revisões de arquitetura em pontos críticos
Padrões de código que reduzem custo de manutenção
- Padronize logs: sempre com
request_id,routee nomes de evento consistentes (ex.:snake_case). - Padronize erros operacionais: logue exceções com
typee contexto; evite mensagens genéricas sem dados. - Padronize limites: timeouts e retries explícitos em integrações externas; logue quando ocorrer retry.
- Padronize nomenclatura: Actions/Services/Repositories com nomes previsíveis e pastas por domínio.
- Padronize formatação: use um formatter (ex.: PHP-CS-Fixer) e um linter (ex.: PHPStan/Psalm) para evitar “discussões de estilo” em PR.
Revisões de arquitetura: quando parar e ajustar
Defina “gatilhos” para uma revisão arquitetural curta (30–60 min) antes de seguir adicionando features:
- A mesma regra de negócio foi duplicada em 2+ lugares.
- Uma rota começou a depender de 4+ serviços/repositórios diretamente.
- Logs não permitem responder: “qual rota está mais lenta?” ou “qual erro mais frequente?”
- Integrações externas sem timeout/retry/circuit-breaker básico.
- Alterações frequentes em endpoints sem versionamento claro.
Checklist de PR para manter evolução saudável
- Inclui/atualiza logs estruturados para o novo fluxo?
- Inclui métrica de duração (ou ao menos aparece no access log)?
- Não loga dados sensíveis?
- Mantém contrato de API (ou cria nova versão)?
- Refatoração e mudança de regra estão separadas?
- Há plano de rollback (feature flag, toggle, deploy reversível)?
Ordem recomendada de adoção (incremental)
- 1º:
RequestIdMiddleware+ access log estruturado por requisição - 2º: logs de eventos de domínio (início/fim de operações críticas)
- 3º: métrica de duração por rota (como log ou backend de métricas)
- 4º: spans manuais em gargalos e propagação do request-id para integrações
- 5º: versionamento de rotas e estratégia de compatibilidade
- 6º: disciplina de PR (padrões, lint, revisão arquitetural por gatilhos)