.. _logging:

Logging
=======

Context Logging
---------------

Since *pypz* has a :ref:`multi-layered design <models>` and entities on each layer are existing in their own context,
we want to have a logging concept that is consistent and reusable across contexts. For example,
if a plugin wants to log something, then it would be nice to reuse the logger of the operator, but denoting that
the log came from the plugin's context.

.. code-block:: python

   operator.get_logger().info("something")
   plugin.get_logger().info("something")

.. code-block:: console

   INFO | pipeline | operator - something
   INFO | pipeline | operator | plugin - something

.. note::
   Since a pipeline is a virtual organization of operators, it does not have logger interface, but acts as a
   context for the operators.

So we have the following requirements:

- one logger to rule them all
- context information shall be available based on from where the logger is called

The concept involves the following classes:

- :class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>`
- :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>`

The :class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>` shall be used to implement
the actual logging functionality. Notice that this class provides protected methods that shall not be called directly.
The :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>` provides the functionality to call the methods
of :class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>` so that it supplies the context
information in the background. You will then use a :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>`
object to perform logging.

Operator Logging
----------------

Since an operator is a self-contained entity, we need to ensure that there is only one logger within
an operator context, which can be used by the operator and the nested plugins as well. An additional
twist to the story that there might be multiple logger plugins in an operator. Instead of calling
each logger plugins' corresponding methods manually, we want to abstract this, so for the user it
seems to be a single method call.

The following diagram shows the inheritance map of the logging-relevant classes:

.. inheritance-diagram::
   pypz.core.specs.plugin.LoggerPlugin
   pypz.core.commons.loggers.ContextLogger
   pypz.plugins.loggers.default.DefaultLoggerPlugin
   :parts: 1
   :caption: Inheritance diagram

Notice that the :class:`LoggerPlugin <pypz.core.specs.plugin.LoggerPlugin>` interface inherits from the
:class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>`, so if you implement
a logger plugin, then you actually implement the
:class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>`.

Furthermore, there is a :class:`ContextLoggerInterface <pypz.core.commons.loggers.ContextLoggerInterface>`
implementation in the Operator: :class:`pypz.core.specs.operator.Operator.Logger`.

This class implements the functionality to abstract the method call of all
:class:`LoggerPlugins <pypz.core.specs.plugin.LoggerPlugin>` in the operator context.

If you check the Operator's constructor, you will notice the member `self.__logger`.
This is actually a :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>`
object, where the :class:`Operator.Logger <pypz.core.specs.operator.Operator.Logger>`
is provided as logger implementation and the context is the full name of the
operator. This is the one and only logger in the entire operator context. If a plugin is created within this context,
then it creates its own :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>` object, however as the logger
implementation it will take the logger of
the operator, while adding its own context to the context list. Hence, if any logger method will be called inside
the plugin, it will be routed to the operator's :class:`ContextLogger <pypz.core.commons.loggers.ContextLogger>`,
which invokes the implemented method of the Operator.Logger, which will call all the LoggerPlugins' corresponding method.

To access the logger in either the operator or plugin context, you simply need to invoke the ``get_logger()`` method.

.. code-block:: python

   operator.get_logger().info("something")
   plugin.get_logger().info("something")

.. warning::
   Currently there is a guard in Operator.Logger, which will trace back the call stack and will throw an exception
   if a LoggerPlugin tries to invoke any of the logger methods. This will prevent recursion at a cost of some
   CPU load.