Logging

The trigger task’s Task Context provides a number of objects and methods that are important to help a trigger task do its job properly.

One of its most critical features is creating a logger, via its get_logger_context() method.

Typically, get_logger_context() is the first statement in the task body:

from triggers.task import TaskContext, TriggerTask

class ImportSubject(TriggerTask):
  def _run(self, context):
    # type: (TaskContext) -> dict
    with context.get_logger_context() as logger:
      ...

The resulting logger instance acts like a regular logging.Logger object, with a couple of notable differences:

  • You can attach “context” variables to the logger and log messages.
  • The max log level emitted by this logger is recorded by the trigger manager for later reference.

Context Variables

Oftentimes, it is difficult to convey all of the desired information in a log message. Developers often have to resort to workarounds such as tacking reprs of critical state values onto the end of the log message.

However, this results in long, unformatted text dumps that are a pain to sift through and contribute significantly to warning fatigue.

get_logger_context() tackles this problem in a different way.

When emitting a log level, your task may optionally attach a “context” object to the log message, like this:

from my_app.models import Subject
from triggers.task import TaskContext, TriggerTask

class ImportSubject(TriggerTask):
  def _run(self, context):
    # type: (TaskContext) -> dict
    with context.get_logger_context() as logger:
      page_data =\
        context.trigger_kwargs['firstPageReceived']['responses']

      given_names = page_data.get('givenNames')
      if not given_names:
        logger.warning(
          'Missing givenNames in response data.',

          # Attach the ``page_data`` to the log message via its context dict.
          extras={'context': {
            'page_data': page_data,
          }},
        )

In the above example, a missing or empty givenNames value in the response data is notable enough to warrant a warning message, but not an exception.

When troubleshooting this issue, it may be useful for a developer to have a full readout of the page data. Rather than try to include this (potentially massive) value in the log message itself, the code attaches it to the log’s context dict.

Note

Depending on how your application processes log messages, you may need to configure your log formatter(s) specifically to take advantage of this feature.

Review the logging module documentation for more information.

Tip

You can also provide a dict directly to get_logger_context(). These context values will be attached automatically to every log message:

from my_app import __version__

class ImportSubject(TriggerTask):
  def _run(self, context):
    # type: (TaskContext) -> dict

    extra_context = {
      "app_version": __version__,
    }

    with context.get_logger_context(extra_context) as logger:
      # The application version number will be attached to every
      # log emitted by ``logger``.
      ...

Exception Context

As with log messages, you can also attach context values to exceptions that your task raises.

To use this feature, pass the exception to triggers.exceptions.with_context() before raising it.

As an example, suppose we wanted to add some kind of a spam filter to our ImportSubject trigger task:

from triggers.exceptions import with_context
from triggers.task import TaskContext, TriggerTask

class ImportSubject(TriggerTask):
  def _run(self, context):
    # type: (TaskContext) -> dict
    with context.get_logger_context() as logger:
      ...
      spam_score = ...
      if spam_score < threshold:
        raise with_context(
          exc = ValueError("Response data failed spam check."),

          context = {
            'spam_score': spam_score,
            'threshold': threshold,
          },
        )

The actual spam score and threshold are interesting information, but it might not be that helpful to include them in the exception message itself (how often do you check those values when your email application flags an email as spam)?

Still, it’s useful to attach them to the exception to assist with any troubleshooting efforts.

with_context() facilitates this.

Important

The exception will only get logged if it is raised inside of the get_logger_context() block!

Tracking Log Levels

The logger returned by get_logger_context() also keeps track of the max log level emitted inside of that context.

This enables your application to track task instance failure/success with a finer degree of granularity.

For example, if you integrate a custom trigger manager with logic to “finalize” a session, you may opt to have it only finalize the session only if none of the task instances emitted log messages with WARNING or higher level.

Task Instance Log Level

Once a task instance has finished running (successfully or otherwise), the max log level emitted is stored in its log_level property:

task_instance = trigger_manager.storage['t_importSubject#0']

task_instance.log_level       # e.g.: logging.INFO
task_instance.log_level_name  # e.g.: 'INFO'

Note

If the task instance hasn’t finished running yet, its log_level will be NOTSET.

Resolving Logs

In some cases, it may be necessary to mark a task instance’s logs as “resolved”.

For example, a task instance may emit a WARNING or ERROR log, but the application determines that these logs are no longer relevant (e.g., a user reviewed them and addressed any issues manually).

To resolve an instance’s logs use the mark_instance_logs_resolved() method:

trigger_manager.mark_instance_logs_resolved('t_importSubject#0')