Advanced Logging of the Magento 2 Services

API Magento 2

Everything you need to know about the advanced logging of the Magento 2 services. Modify a file that handles all M2 API calls. Also, learn how to log the requests in a custom log file.

What is Web API?

According to Wikipedia, an API (Application Programming Interface) is a set of definitions of routines, protocols, and tools for building software and applications. In other words, it is a kind of interface with a set of features that allow developers to access certain data of an application, operating system, or other services.

So, the Web API, as the name implies, is an API over the Internet that can be accessed by an HTTP. It’s a concept, not a technology. We can build a Web API using different technologies such as Java, .NET, etc. For example, Twitter’s REST APIs provide a programmatic approach to reading and writing data. These APIs can integrate Twitter’s capabilities into our own application.

Web Service Types

The main protocol for transporting data between a web service and a client is HTTP. Of course, you can use other protocols as well. The data format is XML or JSON.

SOAP

The first standard for publishing and consuming web services was the XML-based Simple Object Access Protocol (SOAP). Web clients would form HTTP requests and receive responses using the SOAP syntax. We will not deal with soap services today, but if you want to read more about them, just click here.

RESTful API

The data is most commonly transferred in JSON format, although XML and YAML are also available. Based on the REST architecture, not only is it very flexible but also easy to understand. It can be executed on any client or server that has HTTP/HTTPS support.

RESTful services should have the following features:

  • Stateless
  • Cacheable
  • Uniform interface URI
  • Explicit use of HTTP methods
  • XML and / or JSON transfer

With this type of service, resources (e.g., static pages, files, databases) have their own URL or URI that identifies them. The HTTP protocol defines access to resources. Each call performs one action, i.e., creates, reads, modifies, or deletes data). All operations use the same URL, but the HTTP method that defines the type of operation changes. REST uses CRUD-like HTTP methods, i.e., GET, POST, PUT, DELETE, and OPTIONS.

Magento 2 API

The Magento Web API Framework provides integrators and developers with the means of using Web services that communicate with the Magento system. Magento supports both REST (representative state transfer) and SOAP (Simple Object Access Protocol). In Magento 2, the web API coverage is the same for REST and SOAP.

Magento 2 REST API

Is there some way to log all the requested REST APIs with details (i.e., requested URLs, parameters, methods, timing, etc.) in Magento 2? It is a question that pesters many.

The answer to this question is “There is no setting that can enable/disable logging API calls in Magento”. But, you can do so by modifying a file that handles all API calls and by logging the requests in a custom log file. 

This is useful when you know that there are many API call requests by third-party applications to your website. Especially in case you are not sure what is actually being called, how many times, and at what time.

Module Folder

So, let’s start. The first part is to create the module folder. It should look like this:

Module Folder

In order for the Module to successfully register, it is necessary to contain the minimum of these three files: modul.xml, registration.php, and composer.json.

<?xml version="1.0" ?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="urn:magento:framework:Module/etc/module.xsd">
    <module name="SyncIt_ApiRestLog" setup_version="1.0.0" schema_version="1.0.0" />
</config>

SyncIt/ApiRestLog/etc/modul.xml

<?php
use Magento\Framework\Component\ComponentRegistrar;

ComponentRegistrar::register(
    ComponentRegistrar::MODULE,
    'SyncIt_ApiRestLog',
    __DIR__
);

SyncIt/ApiRestLog/registration.php

{
    "name": "syncit/module-apirestlog",
    "description": "SyncIt Api Rest Logging Module",
    "type": "magento2-module",
    "authors": [
        {
            "email": "[email protected]",
            "name": "Vladimir Djurovic"
        }
    ],
    "minimum-stability": "dev",
    "require": {},
    "autoload": {
        "files": [
            "registration.php"
        ],
        "psr-4": {
            "SyncIt\\ApiRestLog\\": ""
        }
    }
}

SyncIt/ApiRestLog/composer.json

Handler.php

We will now hold onto the part that allows us to create logs. You don’t want your API requests to log in *.log files not intended for this purpose. So, you need to create a Handler.php file in which you will specify the path of the file logging.

<?php
namespace SyncIt\ApiRestLog\Model\Logger;

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

/**
 * Class Handler
 * @package SyncIt\ApiRestLog\Model\Logger
 */
class Handler extends Base
{
    /**
     * Logging level
     * @var int
     */
    protected $loggerType = Logger::DEBUG;

    /**
     * File name
     * @var string
     */
    protected $fileName = '/var/log/syncit_rest_api.log';
}

SyncIt/ApiRestLog/Model/Logger/Handler.php

This will sound strange to many – an empty class. We use it as a virtual type in di.xml. If you want to know more about virtual types, check out the official Magento 2 documentation.

<?php
namespace SyncIt\ApiRestLog\Model\Logger;

/**
 * Class Logger
 * @package SyncIt\ApiRestLog\Model\Logger
 */
class Logger extends \Monolog\Logger
{
    #Code
}

SyncIt/ApiRestLog/Model/Logger/Logger.php

system.xml file

The system.xml file is to create the settings that can be set from the admin panel. In this case, you will have to create two <field> fields. The first one will enable and disable the logic of your module while the second one will tell you whether to log the request and response header or not.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Config:etc/system_file.xsd">
    <system>
        <tab id="syncit" translate="label" sortOrder="2000" class="syncit-tab">
            <label>SyncIt</label>
        </tab>
        <section id="syncit_api_rest_logger" translate="label" type="text" sortOrder="10" showInDefault="1" showInStore="1" showInWebsite="1" >
            <label>API Rest Logger</label>
            <tab>syncit</tab>
            <class>syncit-settings</class>
            <resource>Magento_Backend::content</resource>
            <group id="general" translate="label" type="text" sortOrder="10" showInDefault="1" showInStore="1" showInWebsite="1" >
                <label>General</label>
                <field id="enabled" translate="label" type="select" sortOrder="11" showInDefault="1" showInStore="1" showInWebsite="1" >
                    <label>Enable</label>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
                <field id="allowed_log_headers" translate="label" type="select" sortOrder="12" showInDefault="1" showInStore="1" showInWebsite="1" >
                    <label>Allowed Log Headers</label>
                    <comment/>
                    <source_model>Magento\Config\Model\Config\Source\Yesno</source_model>
                </field>
            </group>
        </section>
    </system>
</config>

SyncIt/ApiRestLog/etc/adminhtml/system.xml

config.xml file

In the following config.xml file, you will define a default value for your settings. For example, the logic will be disabled after a module installation, while the header logging will be allowed.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="urn:magento:module:Magento_Store:etc/config.xsd">
    <default>
        <syncit_api_rest_logger>
            <general>
                <enabled>0</enabled>
                <allowed_log_headers>1</allowed_log_headers>
            </general>
        </syncit_api_rest_logger>
    </default>
</config>

SyncIt/ApiRestLog/etc/config.xml

You have now reached the heart of your code. Inject Logger through constructors of the SyncIt\ApiRestLog\Plugin\RestApiLog class.

RequestInterface

Also, you can safely use RequestInterface methods. Magento is able to actually inject the class Magento\Framework\App\Request\Http when you use the following method:
Magento\Framework\HTTP\PhpEnvironment\Request. This object contains the information you need when logging in.

beforeDispatch

To begin with, you need to check if the status of your module is “enabled”. If it’s not, you can cancel the function execution. The example in the beforeDispatch function retrieves the following info: Store ID, Path, HTTP Method, Client Ip, Date, Requested Data, and header, if allowed.

Finally, you need to convert the resulting data array to JSON and start writing the data onto a file.

    /**
    * @param Rest $subject
    * @param RequestInterface $request
    */
    public function beforeDispatch(Rest $subject, RequestInterface $request)
    {
        try {
            // If Enabled Api Rest Log
            if (!$this->_scopeConfig->getValue(self::API_LOGGER_ENABLED, ScopeInterface::SCOPE_STORE)) {
                return;
            }

            // Prepare Data For Log
            $requestedLogData = [
                'storeId' => $this->_storeManager->getStore()->getId(),
                'path' => $request->getPathInfo(),
                'httpMethod' => $request->getMethod(),
                'requestData' => $request->getContent(),
                'clientIp' => $request->getClientIp(),
                'date' => $this->_date->date()->format('Y-m-d H:i:s')
            ];

            // Log Headers
            if ($this->_scopeConfig->getValue(
self::API_LOGGER_ALLOWED_LOG_HEADERS, ScopeInterface::SCOPE_STORE)) {
                $requestedLogData['header'] = $this->getHeadersData(
$request->getHeaders());
            }

            // Logging Data
            $this->_logger->debug('Request = ' . $this->_serializer->serialize($requestedLogData));
        } catch (\Exception $exception) {
            $this->_logger->critical($exception->getMessage(), ['exception' => $exception]);
        }
    }

SyncIt/ApiRestLog/Plugin/RestApiLog.php (Part I)

afterSendResponse

Ok, the request is logged. But, something is missing. Of course – a response. In the same class, you can add one more function – afterSendResponse. You call it after sending a reply, as the name suggests.

As in the example above, where you log the request, you are also logging the response with the data you want. In our example, we are logging status string, status code, and body data. Of course, if header logging is enabled, it will be logged in your log file.

    /**
    * @param Response $response
    * @param $result
    * @return mixed
    */
    public function afterSendResponse(Response $response, $result)
    {
        try {
            // If Enabled Api Rest Log
            if (!$this->_scopeConfig->getValue(self::API_LOGGER_ENABLED, ScopeInterface::SCOPE_STORE)) {
                return;
            }

            // Prepare Data For Log
            $requestedLogData = [
                'responseStatus' => $response->getReasonPhrase(),
                'responseStatusCode' => $response->getStatusCode(),
                'responseBody' => $response->getBody()
            ];

            // Log Headers
            if ($this->_scopeConfig->getValue(self::API_LOGGER_ALLOWED_LOG_HEADERS, ScopeInterface::SCOPE_STORE)) {
                $requestedLogData['header'] = $this->getHeadersData($response->getHeaders());
            }

            // Logging Data
            $this->_logger->debug('Response = ' . $this->_serializer->serialize($requestedLogData));
        } catch (\Exception $exception) {
            $this->_logger->critical($exception->getMessage(), ['exception' => $exception]);
        }
        return $result;
    }

SyncIt/ApiRestLog/Plugin/RestApiLog.php (Part II)

<?php
namespace SyncIt\ApiRestLog\Plugin;

use \Magento\Framework\App\Config\ScopeConfigInterface;
use \Magento\Framework\App\RequestInterface;
use \Magento\Framework\Stdlib\DateTime\TimezoneInterface;
use \Magento\Framework\Webapi\Rest\Response;
use \Magento\Framework\Serialize\SerializerInterface;
use \Magento\Store\Model\ScopeInterface;
use \Magento\Store\Model\StoreManagerInterface;
use \Magento\Webapi\Controller\Rest;
use SyncIt\ApiRestLog\Model\Logger\Logger;

/**
 * Class RestApiLog
 * @package SyncIt\ApiRestLog\Plugin
 */
class RestApiLog
{
    /**
     * Store Config Ids
     */
    const API_LOGGER_ENABLED = 'syncit_api_rest_logger/general/enabled';
    const API_LOGGER_ALLOWED_LOG_HEADERS = 'syncit_api_rest_logger/general/allowed_log_headers';

    /**
     * @var Logger
     */
    protected $_logger;

    /**
     * @var TimezoneInterface
     */
    protected $_date;

    /**
     * @var StoreManagerInterface
     */
    protected $_storeManager;

    /**
     * @var ScopeConfigInterface
     */
    protected $_scopeConfig;

    /**
     * @var SerializerInterface
     */
    private $_serializer;

    /**
     * RestApiLog constructor.
     * @param Logger $logger
     * @param TimezoneInterface $date
     * @param StoreManagerInterface $storeManager
     * @param ScopeConfigInterface $scopeConfig
     * @param SerializerInterface $serializer
     */
    public function __construct(
        Logger $logger,
        TimezoneInterface $date,
        StoreManagerInterface $storeManager,
        ScopeConfigInterface $scopeConfig,
        SerializerInterface $serializer
    )
    {
        $this->_logger = $logger;
        $this->_date = $date;
        $this->_storeManager = $storeManager;
        $this->_scopeConfig = $scopeConfig;
        $this->_serializer = $serializer;
    }

    /**
     * @param Rest $subject
     * @param RequestInterface $request
     */
    public function beforeDispatch(Rest $subject, RequestInterface $request)
    {
        try {
            // If Enabled Api Rest Log
            if (!$this->_scopeConfig->getValue(self::API_LOGGER_ENABLED, ScopeInterface::SCOPE_STORE)) {
                return;
            }

            // Prepare Data For Log
            $requestedLogData = [
                'storeId' => $this->_storeManager->getStore()->getId(),
                'path' => $request->getPathInfo(),
                'httpMethod' => $request->getMethod(),
                'requestData' => $request->getContent(),
                'clientIp' => $request->getClientIp(),
                'date' => $this->_date->date()->format('Y-m-d H:i:s')
            ];

            // Log Headers
            if ($this->_scopeConfig->getValue(self::API_LOGGER_ALLOWED_LOG_HEADERS, ScopeInterface::SCOPE_STORE)) {
                $requestedLogData['header'] = $this->getHeadersData($request->getHeaders());
            }

            // Logging Data
            $this->_logger->debug('Request = ' . $this->_serializer->serialize($requestedLogData));
        } catch (\Exception $exception) {
            $this->_logger->critical($exception->getMessage(), ['exception' => $exception]);
        }
    }

    /**
     * @param Response $response
     * @param $result
     * @return mixed
     */
    public function afterSendResponse(Response $response, $result)
    {
        try {
            // If Enabled Api Rest Log
            if (!$this->_scopeConfig->getValue(self::API_LOGGER_ENABLED, ScopeInterface::SCOPE_STORE)) {
                return;
            }

            // Prepare Data For Log
            $requestedLogData = [
                'responseStatus' => $response->getReasonPhrase(),
                'responseStatusCode' => $response->getStatusCode(),
                'responseBody' => $response->getBody()
            ];

            // Log Headers
            if ($this->_scopeConfig->getValue(self::API_LOGGER_ALLOWED_LOG_HEADERS, ScopeInterface::SCOPE_STORE)) {
                $requestedLogData['header'] = $this->getHeadersData($response->getHeaders());
            }

            // Logging Data
            $this->_logger->debug('Response = ' . $this->_serializer->serialize($requestedLogData));
        } catch (\Exception $exception) {
            $this->_logger->critical($exception->getMessage(), ['exception' => $exception]);
        }
        return $result;
    }

    /**
     * Method for getting all available data in header and convert them to array
     *
     * @param $headers
     * @return array
     */
    private function getHeadersData($headers): array
    {
        $headerLogData = [];
        foreach ($headers as $header) {
            $headerLogData[$header->getFieldName()] = $header->getFieldValue();
        }
        return $headerLogData;
    }
}

SyncIt/ApiRestLog/Plugin/RestApiLog.php (Full Class)

di.xml file

Of course, your module is not finished yet. You must remember the empty class we have created before. Now you are actually going to use it. It is necessary to develop the di.xml file. In it, you need to create the type of virtual class mentioned earlier.

<?xml version="1.0"?>
<config xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:noNamespaceSchemaLocation="urn:magento:framework:ObjectManager/etc/config.xsd">
    <type name="Magento\Webapi\Controller\Rest">
        <plugin name="rest-api-log" type="SyncIt\ApiRestLog\Plugin\RestApiLog"/>
    </type>
    <type name="Magento\Framework\Webapi\Rest\Response">
        <plugin name="rest-api-log" type="SyncIt\ApiRestLog\Plugin\RestApiLog" />
    </type>
    <type name="SyncIt\ApiRestLog\Model\Logger\Handler">
        <arguments>
            <argument name="filesystem" xsi:type="object">Magento\Framework\Filesystem\Driver\File</argument>
        </arguments>
    </type>
    <type name="SyncIt\ApiRestLog\Model\Logger\Logger">
        <arguments>
            <argument name="name" xsi:type="string">SyncIt_ApiRestLog</argument>
            <argument name="handlers"  xsi:type="array">
                <item name="system" xsi:type="object">SyncIt\ApiRestLog\Model\Logger\Handler</item>
            </argument>
        </arguments>
    </type>
</config>

SyncIt/ApiRestLog/etc/di.xmlTag <plugin> tells Magento 2 to call the beforeDispatch method before calling the dispatch method. The dispatch method is the main entry point for REST API calls. If you want to know more about Prioritizing plugins, take a look at Magento 2 DevDocs.

Testing the Module

Then, you come to the part where the magic is made. What is left is to enable the module and delete the cache. Finally, after these few commands, you can test the module. 

bin/magento module:enable SyncIt_ApiRestLog
bin/magento setup:upgrade
bin/magento cache:clean

After a couple of API calls, the log file should look like this:

Advanced Logging of M2 Services - Log FIle

Wrap Up

To sum up, you have learnt how to:

  • modify a file in order for it to handle all the API calls
  • log the requests in a custom log file

Of course, you can always contact us at [email protected] for M2 development services.

0 0 votes
Article Rating
Subscribe
Notify of
guest
0 Comments
Inline Feedbacks
View all comments

Innovation in your inbox!

Stay in the know on the latest tech news.

Maybe later…