Blog

Logging su Magento 2

Lettura 4 minuti

Un buon utilizzo dei log, nelle integrazioni o nelle funzionalità di una certa consistenza, può fare la differenza nell’individuazione e risoluzione di problemi sia in fase di sviluppo che in produzione.

Magento 2 include di default Monolog, uno strumento molto interessante che, rispetto a Magento 1, permette di indirizzare l’output a destinazioni diverse da file di testo (socket, inbox, database e web services). Ciò semplifica tutti gli scenari nei quali i server in cui l’applicazione viene eseguita siano effimeri, e i log debbano essere raccolti in maniera centralizzata ad esempio su Elasticache. Si evita quindi l’inclusione manuale di dipendenze come aleron75/magemonolog o l’aggiunta di un layer intermedio come logstash.

Utilizzo

Purtroppo, rispetto al buon vecchio Mage::log, su Magento 2 è necessario scrivere qualche riga in più di codice. Come ogni cosa, va incluso come dipendenza della classe. Dal momento che Monolog\Logger estende l’interfaccia Psr\Log\LoggerInterface, utilizzeremo quest’ultima per l’inclusione.

/**
 * @var Psr\Log\LoggerInterface
 */
protected $logger;
 
public function __construct(
    Psr\Log\LoggerInterface $logger
) {
    $this->logger = $logger;
}

A questo punto si possono usare i metodi debug, info, notice, warning, error, critical, alert e emergency all’occorrenza.

$this->logger->info('Test message');

Troveremo il nostro messaggio nel file var/log/system.log, il che è perfetto per provare che il tutto funzioni, ma non necessariamente ciò che desideriamo. Il più delle volte, infatti, loggare su file separati è preferibile, se non necessario. La domanda è quindi: come si fa?

File separati

Specificare un file diverso da system.log si può fare; rispetto a come si faceva su Magento 1 è più prolissa ma pur sempre semplice.

Il readme di Monolog suggerisce la seguente modalità:

$logger->pushHandler(new StreamHandler('path/to/your.log', Logger::WARNING));

Per rimanere fedeli al DI pattern di Magento 2, dobbiamo però fare qualche modifica. Dichiariamo nel costruttore la dipendenza dalla classe factory di StreamHandler per poterne creare un’istanza.

/**
 * @var Monolog\Logger
 */
protected $logger;
 
public function __construct(
    Monolog\Logger $logger,
    Monolog\Handler\StreamHandlerFactory $streamHandlerFactory
) {
    $this->logger = $logger;
 
    $this->logger->pushHandler($streamHandlerFactory->create(
        ['stream' => 'var/log/custom.log']
    ));
}

Ora abbiamo un oggetto logger il quale dirigerà l’output su un file specifico. Siamo riusciti a fare ciò che facevamo anche su Magento 1, ma manca ancora qualcosa. Se vogliamo utilizzare il logger da più classi all’interno dello stesso modulo, dobbiamo di volta in volta aggiungere due oggetti nel costruttore ed assegnare un handler con il percorso corretto al logger. C’è un modo per essere più sintetici?

Centralizzare l’assegnazione dell’handler

Al fine di effettuare in un punto soltanto l’assegnazione dell’handler al logger, implementeremo una classe che chiameremo LoggerProvider. Questa avrà per parametro il nome del file verso cui verranno direzionati i log e permetterà in ogni momento di ottenere un’istanza del logger.

namespace Bitbull\LoggerExample\Helper;
 
class LoggerProvider
{
    /**
     * @var Monolog\Logger
     */
    protected $logger;
 
    public function __construct(
        Monolog\Logger $logger,
        Magento\Framework\Filesystem $filesystem,
        Monolog\Handler\StreamHandlerFactory $streamHandlerFactory,
        $filename = 'system'
    ) {
        $this->logger = $logger;
 
        $vardir = $filesystem
            ->getDirectoryRead(Magento\Framework\App\Filesystem\DirectoryList::VAR_DIR)
            ->getAbsolutePath();
 
        $this->logger->pushHandler($this->streamHandlerFactory->create(
            ['stream' => $vardir . 'log' . DIRECTORY_SEPARATOR . basename($filename) . '.log']
        ));
    }
 
    public function getLogger()
    {
        return $this->logger;
    }
}

A parte una piccola aggiunta per ottenere in maniera più sicura il percorso della directory var, il codice è lo stesso di cui sopra. In aggiunta abbiamo il parametro filename, che permetterà di utilizzare un filename specifico ma avrà come default system.

Ora possiamo richiedere un’istanza di LoggerProvider ogni volta in cui ci serve il logger, e conservare solamente quest’ultimo nella classe.

/**
 * @var Monolog\Logger
 */
protected $logger;

public function __construct(
    Bitbull\LoggerExample\Helper\LoggerProvier $loggerProvider
) {
    $this->logger = $loggerProvider->getLogger();
}

La modalità di utilizzo del logger quindi non varia.

Ora dobbiamo soltanto creare un LoggerProvider per ogni filename di cui necessitiamo, e poi iniettarlo nelle classi corrette. Questo viene fatto tramite il file di.xml, posizionato nella cartella etc del modulo. Per chi non conoscesse la struttura base di un modulo Magento 2, consiglio la lettura dell’articolo di Mirko “Creiamo il nostro primo modulo su Magento 2”.

Dunque, per ogni filename, creiamo un virtual type:

<virtualType name="IntegrationALoggerProvider" type="Bitbull\LoggerExample\Helper\LoggerProvider">
    <arguments>
        <argument name="filename" xsi:type="string">integration_a</argument>
    </arguments>
</virtualType>

Infine specifichiamo, per ogni classe che necessiterà di includere un logger, quale tra i virtualType che estendono LoggerProvider vogliamo venga iniettato.

<type name="Bitbull\LoggerExample\Model\IntegrationA">
    <arguments>
        <argument name="loggerProvider" xsi:type="object">IntegrationALoggerProvider</argument>
    </arguments>
</type>

Conclusione

Abbiamo visto le differenze tra Magento 1 e 2 per ciò che riguarda il logging. Il livello di complessità è leggermente maggiore, ma ha vantaggi importanti. Il logger provider è uno spunto interessante e come strumento potrebbe essere inserito in un modulo core da utilizzare in diversi progetti. Questo perché risulta particolarmente interessante nel momento in cui abbiamo diversi moduli o diverse classi che direzionano l’output in file diversi.

La parte più interessante è probabilmente quella relativa alla centralizzazione dell’inizializzazione del LoggingProvider. Questo ci permette potenzialmente di direzionare il logging a piattaforme diverse (elasticsearch, amqp, ecc) in base a configurazioni di sistema o variabili d’ambiente. C’è inoltre la possibilità di agire in base al livello di errore, ad esempio inviando email di notifica su eventi di tipo critical, SMS sugli alert, e via dicendo.

Renato Cason