Logging#

An application should use (but not over-use) logging to make it easier to debug and monitor it in production. The logging module is preferred.

See also

Logging section of the String formatting style guide

Name#

In most cases, use logger = logging.getLogger(__name__), as recommended.

If a file is run directly, __name__ will be "__main__", which is less informative. In such cases, either use the name of the module, like "workers.check.dataset" in Pelican backend, or include the name of the sub-command, like "ocdskingfisher.summarize.add" in Kingfisher Summarize.

If a command-line tool logs messages to give user feedback, we typically use the name of the command, like "oc4ids", "ocdskit", "ocdsextensionregistry" and "spoonbill".

Format#

In the context of web servers and worker daemons, the formatter’s format string should be set to:

"%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(message)s"

In most cases, the other placeholders are unnecessary:

  • If the logger’s name is __name__, then %(name)s covers %(module)s (identical), %(pathname)s (too long) and %(filename)s (too short).

  • %(funcName)s, unless %(name)s, %(lineno)s and %(message)s are insufficient to locate the relevant code.

  • %(process)s and %(thread)s, unless the log messages from different processes/threads are written to the same location.

To configure the format in Django:

# https://docs.djangoproject.com/en/4.2/topics/logging/#django-security
LOGGING = {
    "version": 1,
    "disable_existing_loggers": False,
    "formatters": {
        "console": {
            "format": "%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(message)s",
        },
    },
    "handlers": {
        "console": {
            "class": "logging.StreamHandler",
            "formatter": "console",
        },
        "null": {
            "class": "logging.NullHandler",
        },
    },
    "loggers": {
        "": {
            "handlers": ["console"],
            "level": "INFO",
        },
        "django.security.DisallowedHost": {
            "handlers": ["null"],
            "propagate": False,
        },
        "mymodule": {
            "handlers": ["console"],
            "level": "DEBUG",
            "propagate": False,
        },
    },
}

Note

Django’s default logging configuration configures the django and django.server loggers. To change those (for example, to set the level to DEBUG in development to view database queries), add them to the above template.

To configure the format in general, replacing MYMODULE:

import logging
import logging.config

logging.config.dictConfig(
    {
        "version": 1,
        "disable_existing_loggers": False,
        "formatters": {
            "console": {
                "format": "%(asctime)s %(levelname)s [%(name)s:%(lineno)s] %(message)s",
            },
        },
        "handlers": {
            "console": {
                "class": "logging.StreamHandler",
                "formatter": "console",
            },
        },
        "loggers": {
            "MYMODULE": {
                "handlers": ["console"],
                "level": "INFO",
                "propagate": False,
            },
        },
    }
)

Reference: Python’s warnings.py format string

Methods#

Use the corresponding method for the appropriate logging level.

When logging messages inside an except clause, if the error is unexpected and isn’t re-raised, use logger.exception(msg) to log the message at the ERROR level along with exception information. Do not bother with the traceback module.

Maintainers can review loggers inside except branches with this regular expression:

except (?!RecoverableException).+\n( +)(\S.*\n(\1.*\n)*\1)?.*log.*\.(?!exception\(|format\()

Configuration#

Loggers are organized into a hierarchy. As such, you can configure only the root logger (its name is '' in Django, or None in general), or only the loggers for top-level modules (like only a, instead of both a.b and a.c).