diff --git a/changelog.d/+add-environment-info.added.md b/changelog.d/+add-environment-info.added.md new file mode 100644 index 000000000..062b6e979 --- /dev/null +++ b/changelog.d/+add-environment-info.added.md @@ -0,0 +1,2 @@ +Add a new setting "ENVIRONMENT" usable for enriching logs. Includes a logging +filter and formatter that adds the environment to each log record. diff --git a/docs/production.rst b/docs/production.rst index 8fc5bc10a..c647e75d7 100644 --- a/docs/production.rst +++ b/docs/production.rst @@ -6,3 +6,4 @@ Running in production .. toctree:: production/task-queue + production/logging diff --git a/docs/production/logging.rst b/docs/production/logging.rst new file mode 100644 index 000000000..8e1a1bf3c --- /dev/null +++ b/docs/production/logging.rst @@ -0,0 +1,68 @@ +===================== +Logging in production +===================== + +Logging a deployment-specific environment +========================================= + +Utilizing the setting :setting:`ENVIRONMENT` and either the logging filter +``argus.logging.filters.EnvironmentFilter`` or the formatter +``argus.logging.formatters.EnvironmentFormatter`` it is possible to add +whatever is set in ``ENVIRONMENT`` to every line logged. + +``ENVIRONMENT`` can be set via the environment variable ``ARGUS_ENVIRONMENT``. + +Set it to for instance a hostname or ip address or pod name on deployment, to +make it easy to filter out which logs come from which deployment if all logs +end up at the same place. + +Configure the filter, for instance via the dict method, like so:: + + { + .. + "filters": { + .. + "environment": ["argus.logging.filters.EnvironmentFilter"], + .. + }, + .. + } + +Configure the formatter, for instance via the dict method, like so:: + + { + .. + "formatters": { + .. + "environment": { + "()": "argus.logging.formatters.EnvironmentFormatter", + "format": "{environment} {levelname} {message}", + "style": "{", + }, + .. + }, + .. + } + +Pick one of the above. + +Structured logging with JSON +============================ + +By installing ``python-json-logger`` (for instance via ``pip install +python-json-logger`` or ``pip install ".[jsonlogging]"`` and setting up logging formatters like this: + +:: + + "formatters": { + .. + "json": { + "()": "pythonjsonlogger.json.JsonFormatter", + "format": "asctime,name,funcName,levelname,message", + "style": ",", + }, + .. + }, + +With ``python-json-logger`` installed you can also use the formatter +``argus.logging.formatters.JSONEnvironmentFormatter``. diff --git a/docs/reference/site-specific-settings.rst b/docs/reference/site-specific-settings.rst index 92f38c948..28794a2bd 100644 --- a/docs/reference/site-specific-settings.rst +++ b/docs/reference/site-specific-settings.rst @@ -418,6 +418,14 @@ Special environment settings opacity as a percentage, default ``25%``). See :ref:`themes-and-styling` for how to customize themes. +.. setting:: ENVIRONMENT + +* :setting:`ENVIRONMENT` is a plain text string used to enrich log messages. + Examples: "dev", "production", "new feature test". + +See :doc:`../production/logging` + + Debugging settings ------------------ diff --git a/src/argus/logging/__init__.py b/src/argus/logging/__init__.py new file mode 100644 index 000000000..bc7b412bc --- /dev/null +++ b/src/argus/logging/__init__.py @@ -0,0 +1,8 @@ +# Inspiration: +# +# * https://nhairs.github.io/python-json-logger/latest/cookbook/ +# * https://morethanmonkeys.medium.com/structured-logging-with-python-and-django-from-log-soup-to-useful-events-a8de3003ac87 +# * https://medium.com/@joseph4jubilant/logging-system-in-django-e4c55f624861 +# * https://blog.allegro.tech/2021/06/python-logging.html +# +# Not really a fan of https://docs.python.org/3/howto/logging-cookbook.html#implementing-structured-logging diff --git a/src/argus/logging/filters.py b/src/argus/logging/filters.py new file mode 100644 index 000000000..e6b0163c8 --- /dev/null +++ b/src/argus/logging/filters.py @@ -0,0 +1,25 @@ +import logging + + +__all__ = [ + "EnvironmentFilter", +] + + +class EnvironmentFilter(logging.Filter): + "Attach deployment-specific context to log records" + + def __init__(self, name=""): + super().__init__(name) + + from django import settings + + self.ENVIRONMENT = getattr(settings, "ENVIRONMENT", None) + + def filter(self, record: logging.LogRecord) -> bool: + """ + Modify the LogRecord in place, then return True so the record is logged. + """ + if not hasattr(record, "environment"): + record.environment = self.ENVIRONMENT + return True diff --git a/src/argus/logging/formatters.py b/src/argus/logging/formatters.py new file mode 100644 index 000000000..a440a8f7a --- /dev/null +++ b/src/argus/logging/formatters.py @@ -0,0 +1,34 @@ +from logging import Formatter + +__all__ = [ + "EnvironmentFormatter", + "JSONEnvironmentFormatter", +] + + +class EnvironmentMixin: + def __init__(self, **kwargs): + super().__init__(**kwargs) + from django import settings + + self.ENVIRONMENT = getattr(settings, "ENVIRONMENT", None) + + +class EnvironmentFormatter(EnvironmentMixin, Formatter): + def format(self, record): + if not hasattr(record, "environment"): + record.environment = self.ENVIRONMENT + super().format(record) + + +try: + from pythonjsonlogger.json import JsonFormatter +except ImportError: + JSONEnvironmentFormatter = EnvironmentFormatter + +else: + + class JSONEnvironmentFormatter(EnvironmentMixin, JsonFormatter): + def process_log_record(self, log_data): + if not hasattr(log_data, "environment"): + log_data.environment = self.ENVIRONMENT diff --git a/src/argus/site/settings/base.py b/src/argus/site/settings/base.py index 8cb24fe72..d7b18391c 100644 --- a/src/argus/site/settings/base.py +++ b/src/argus/site/settings/base.py @@ -322,6 +322,13 @@ # App settings: override themes, urls, context processors +BANNER_MESSAGE = get_str_env("ARGUS_BANNER_MESSAGE", default=None) + +# Used for looking up the latest version of Argus on PyPI +PYPI_URL = get_str_env("ARGUS_PYPI_URL", "https://pypi-proxy.sokrates.edupaas.no") + +ENVIRONMENT = get_str_env("ARGUS_ENVIRONMENT", "environment-unset") + # add apps that may override other apps _overriding_apps_env = get_json_env("ARGUS_OVERRIDING_APPS", [], quiet=False) OVERRIDING_APPS = validate_app_setting(_overriding_apps_env) @@ -334,7 +341,4 @@ del _extra_apps_env update_settings(globals(), EXTRA_APPS) -BANNER_MESSAGE = get_str_env("ARGUS_BANNER_MESSAGE", default=None) - -# Used for looking up the latest version of Argus on PyPI -PYPI_URL = get_str_env("ARGUS_PYPI_URL", "https://pypi-proxy.sokrates.edupaas.no") +#### DO NOT DEFINE ANY SETTINGS BELOW OVERRIDING_APPS and EXTRA_APPS!