Boas práticas de manutenção em Slim Framework: logs, observabilidade e evolução do projeto

Capítulo 15

Tempo estimado de leitura: 11 minutos

+ Exercício

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 logger
  • level: debug/info/warn/error
  • message: descrição curta do evento
  • request_id: id único por requisição (correlação)
  • user_id: quando autenticado (ou null)
  • route: padrão da rota (ex.: GET /v1/orders/{id})
  • method: GET/POST/PUT/DELETE
  • status_code: 200/400/500…
  • duration_ms: tempo total da requisição
  • ip e user_agent: úteis para auditoria/abuso
  • error: objeto com type, message, stack (somente em ambiente controlado)

Boas práticas de níveis (debug/info/warn/error)

NívelQuando usarExemplos
debugDetalhes para diagnóstico local/temporáriopayload sanitizado, decisões internas, cache hit/miss
infoEventos esperados e relevantesrequisição finalizada, job concluído, login bem-sucedido
warnAlgo inesperado, mas recuperáveltimeout em serviço externo com fallback, tentativa inválida repetida
errorFalha que impede o fluxo esperadoexceçã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.

Continue em nosso aplicativo e ...
  • Ouça o áudio com a tela desligada
  • Ganhe Certificado após a conclusão
  • + de 5000 cursos para você explorar!
ou continue lendo abaixo...
Download App

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

TipoExemploImpacto
CompatívelAdicionar campo novo opcional no JSONClientes antigos ignoram
CompatívelAdicionar endpoint novoSem impacto
QuebradoraRenomear/remover campoQuebra parsing
QuebradoraMudar semântica de um campo (ex.: unidade)Erros silenciosos
QuebradoraMudar status code esperadoFluxos 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, route e nomes de evento consistentes (ex.: snake_case).
  • Padronize erros operacionais: logue exceções com type e 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)

  • : RequestIdMiddleware + access log estruturado por requisição
  • : logs de eventos de domínio (início/fim de operações críticas)
  • : métrica de duração por rota (como log ou backend de métricas)
  • : spans manuais em gargalos e propagação do request-id para integrações
  • : versionamento de rotas e estratégia de compatibilidade
  • : disciplina de PR (padrões, lint, revisão arquitetural por gatilhos)

Agora responda o exercício sobre o conteúdo:

Ao evoluir uma API em Slim Framework com uma mudança que quebra clientes (por exemplo, renover ou remover um campo do JSON), qual prática é a mais adequada para reduzir risco e manter compatibilidade?

Você acertou! Parabéns, agora siga para a próxima página

Você errou! Tente novamente.

Mudanças quebradoras devem ir para uma nova versão (ex.: /v2), mantendo a antiga por um período. Assim, clientes não quebram e é possível acompanhar adoção e impacto por métricas/volume por versão.

Próximo capitúlo

Projeto final guiado: API com Slim Framework aplicando rotas, middlewares e Arquitetura Limpa

Arrow Right Icon
Capa do Ebook gratuito Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa
94%

Back-end com Slim Framework (PHP): Rotas, Middlewares e Arquitetura Limpa

Novo curso

16 páginas

Baixe o app para ganhar Certificação grátis e ouvir os cursos em background, mesmo com a tela desligada.