← Voltar ao blog

Arquivo de log customizado no Magento 2 com Monolog (sem classes extras)

Rodrigo Biazzi

Toda integração no Magento acaba precisando do seu próprio log. Quando uma exportação para o ERP falha às 3 da manhã, procurar as linhas certas em um system.log compartilhado por todos os módulos é lento. Um arquivo dedicado, com mensagens claras e contexto, transforma isso em uma checagem de um minuto.

O Magento 2 usa o Monolog por trás da LoggerInterface do PSR-3. Graças à dependency injection, você pode dar a uma classe o seu próprio arquivo de log sem escrever uma única classe PHP nova. Este artigo mostra primeiro essa abordagem, depois uma classe de handler customizada para casos que pedem mais controle, e as práticas que tornam os logs úteis.

Como o log é ligado no Magento

Quando uma classe pede Psr\Log\LoggerInterface no constructor, o di.xml do core entrega o Magento\Framework\Logger\Monolog, que estende o logger do Monolog. Esse logger tem handlers para var/log/system.log, var/log/debug.log e var/log/exception.log, entre outros.

O handler de arquivos é o Magento\Framework\Logger\Handler\Base. Ele recebe um nome de arquivo, grava pelo filesystem driver do Magento e tem um nível mínimo. Como tanto o logger quanto o handler são classes normais criadas pelo object manager, você pode montar novas combinações delas no di.xml.

Um arquivo de log customizado com virtual types

Para um módulo Vendor_Shipping que chama uma API externa de tarifas, adicione isto ao etc/di.xml:

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">

    <virtualType name="Vendor\Shipping\Logger\Handler" type="Magento\Framework\Logger\Handler\Base">
        <arguments>
            <argument name="fileName" xsi:type="string">/var/log/vendor_shipping.log</argument>
        </arguments>
    </virtualType>

    <virtualType name="Vendor\Shipping\Logger\Logger" type="Magento\Framework\Logger\Monolog">
        <arguments>
            <argument name="name" xsi:type="string">vendor_shipping</argument>
            <argument name="handlers" xsi:type="array">
                <item name="file" xsi:type="object">Vendor\Shipping\Logger\Handler</item>
            </argument>
        </arguments>
    </virtualType>

    <type name="Vendor\Shipping\Model\RateClient">
        <arguments>
            <argument name="logger" xsi:type="object">Vendor\Shipping\Logger\Logger</argument>
        </arguments>
    </type>
</config>

O nome do arquivo é relativo à raiz do Magento, então /var/log/vendor_shipping.log acaba no var/log da instalação, e não no /var/log do sistema. O argumento name é o channel, que aparece em cada linha de log e ajuda quando vários loggers gravam no mesmo lugar.

A classe em si continua completamente padrão:

<?php
declare(strict_types=1);

namespace Vendor\Shipping\Model;

use Magento\Framework\HTTP\Client\Curl;
use Psr\Log\LoggerInterface;

class RateClient
{
    public function __construct(
        private readonly Curl $curl,
        private readonly LoggerInterface $logger
    ) {
    }

    public function fetchRates(string $postcode, float $weight): array
    {
        $this->logger->info('Requesting rates', ['postcode' => $postcode, 'weight' => $weight]);

        try {
            $this->curl->setTimeout(5);
            $this->curl->get('https://rates.example.com/v1/quote?' . http_build_query([
                'postcode' => $postcode,
                'weight' => $weight,
            ]));
        } catch (\Throwable $e) {
            $this->logger->error('Rates request failed', ['exception' => $e]);
            return [];
        }

        if ($this->curl->getStatus() !== 200) {
            $this->logger->warning('Unexpected rates response', [
                'status' => $this->curl->getStatus(),
                'body' => mb_substr($this->curl->getBody(), 0, 500),
            ]);
            return [];
        }

        return json_decode($this->curl->getBody(), true) ?: [];
    }
}

Ela depende da LoggerInterface, como qualquer classe deve. Em qual arquivo ela grava é uma decisão de DI. Em um teste unitário você passa um logger mock e nada mais muda. As ideias por trás disso estão explicadas em dependency injection e virtual types.

Depois de mudar o di.xml, limpe os metadados gerados. Em modo developer, um cache:flush geralmente basta. Em modo production, rode setup:di:compile como parte do deploy.

Conferindo se funciona

Se nada aparece no arquivo novo, confira nesta ordem. Primeiro, se a classe realmente recebe o seu logger: um plugin ou uma preference na classe pode criá-la por outro caminho, e um erro de digitação no nome da classe no nó type falha em silêncio. Segundo, se o código gerado foi atualizado depois da mudança no di.xml. Terceiro, se o usuário do PHP consegue escrever em var/log. Um jeito rápido de confirmar a ligação é chamar a classe por um pequeno comando CLI, rodar e olhar o arquivo.

Compartilhando o logger entre classes

Adicione um bloco type para cada classe que deve usar o log do módulo. Se muitas classes precisam dele, essa lista fica longa, e tudo bem. É explícita e fácil de buscar. Evite uma preference para LoggerInterface restrita ao seu módulo, já que preferences são globais e redirecionariam os logs de todos os módulos.

Uma classe de handler quando você precisa de mais

O virtual type não consegue mudar o nível mínimo, porque o Base o define em uma propriedade protected. Se você quer um arquivo que registre só warnings e acima, ou quer um formatter diferente, crie uma pequena classe de handler:

<?php
declare(strict_types=1);

namespace Vendor\Shipping\Logger;

use Magento\Framework\Logger\Handler\Base;
use Monolog\Logger;

class WarningHandler extends Base
{
    protected $fileName = '/var/log/vendor_shipping_errors.log';

    protected $loggerType = Logger::WARNING;
}

Depois use-a como um item extra no array handlers do virtual type do seu logger. Um registro vai para todo handler que aceita o nível dele, então você tem o log completo em um arquivo e só os problemas em outro.

O Magento 2.4.8 traz o Monolog 3, em que os níveis também estão disponíveis como o enum Monolog\Level. As constantes de nível da classe Logger continuam existindo por compatibilidade e são as que os handlers do core usam, então a classe acima funciona. Confira os handlers do core em lib/internal/Magento/Framework/Logger/Handler da sua versão se for escrever algo mais avançado.

Níveis de log na prática

  • debug: traces detalhados para desenvolvimento. Barulhento, não deve ficar ligado em produção por muito tempo.
  • info: eventos normais que vale guardar, como um pedido exportado com o ID externo.
  • warning: algo inesperado que o código tratou, como um retry ou um campo opcional ausente.
  • error: uma operação falhou e precisa de atenção.
  • critical e acima: o módulo ou a loja não consegue funcionar. Combine com alertas.

O handler padrão do debug.log só grava quando o debug logging está habilitado, o que vem desligado por padrão em modo production. Dá para ligar com bin/magento setup:config:set --enable-debug-logging=true, mas em produção um arquivo dedicado em nível info é mais fácil de administrar.

Contexto, não montagem de string

Passe os dados como array de contexto em vez de concatená-los na mensagem:

$this->logger->info('Order exported', ['increment_id' => $incrementId, 'erp_id' => $erpId]);

A mensagem fica constante, então você consegue buscar e contar. O contexto é gravado como JSON depois da mensagem pelo line formatter padrão, o que deixa os valores legíveis e fáceis de achar com grep.

Para exceções, passe o objeto da exceção na chave exception. O Monolog então grava a classe, a mensagem, o arquivo, a linha e o trace. Com o system logger padrão, registros com uma exceção no contexto vão para o exception.log. Com o seu próprio logger eles vão para o seu arquivo, que geralmente é o que você quer em uma integração.

O que não logar

  • Dados de cartão, CVV, senhas, tokens OAuth ou chaves de API. Mascare ou deixe de fora.
  • Cadastros completos de clientes e endereços, a menos que haja uma necessidade clara e uma política de retenção.
  • Corpos inteiros de request e response de APIs grandes em nível info. Registre um corpo truncado só nos erros.

Logs são copiados para backups, compartilhados em tickets e lidos por pessoas que não deveriam ver dados pessoais. Trate-os como dados que vão vazar um dia.

Rotação e tamanho

O Magento não faz rotação dos seus arquivos de log. Uma integração tagarela pode encher um disco em semanas. Use o logrotate do sistema:

/var/www/shop/shared/var/log/*.log {
    daily
    rotate 14
    compress
    delaycompress
    missingok
    notifempty
    copytruncate
}

O copytruncate deixa o PHP continuar gravando no mesmo arquivo aberto. Ajuste o caminho para o seu layout. Em um deploy baseado em releases, os logs ficam na pasta var compartilhada.

Lendo logs durante o debug

Um arquivo dedicado facilita acompanhar uma integração em tempo real com tail -f var/log/vendor_shipping.log, ou filtrar por valores do contexto com grep. Quando os logs não bastam, percorra o código com um debugger. A configuração está em debug com Xdebug no Magento 2. Para integrações com sistemas externos, veja também padrões de integração com ERP, onde log e retries andam juntos.

FAQ

Como logo em um arquivo customizado no Magento 2 sem criar classes?

Declare um virtual type de Magento\Framework\Logger\Handler\Base com um argumento fileName, um virtual type de Magento\Framework\Logger\Monolog que o use e injete esse logger na sua classe pelo di.xml.

Por que minha mensagem de debug não está no debug.log?

O debug logging vem desabilitado por padrão em modo production. Dá para habilitar com setup:config:set --enable-debug-logging=true, mas em produção prefira gravar no seu próprio arquivo de log em nível info ou acima.

Posso usar o ObjectManager para criar um logger rapidamente?

Funciona, mas é o hábito errado. Injete Psr\Log\LoggerInterface no constructor e aponte para o seu logger no di.xml. A classe continua testável e o destino do log pode mudar sem mexer no código.

Para onde vão as exceções registradas com o logger padrão?

Com o system logger padrão, registros que carregam uma exceção no contexto são gravados em var/log/exception.log. Os outros erros vão para var/log/system.log.

Atendo como freelancer em Magento e Adobe Commerce, incluindo integrações que precisam de bom logging. Se você precisa de ajuda com isso, fale comigo.

Artigos relacionados

Erros comuns do Magento 2 e como corrigir

Correções para erros clássicos do Magento 2: Area code is not set, Class does not exist, Invalid Form Key, indexers travados, unserialize e 404 em estáticos.