Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions changelog.d/+add-environment-info.added.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions docs/production.rst
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ Running in production
.. toctree::

production/task-queue
production/logging
68 changes: 68 additions & 0 deletions docs/production/logging.rst
Original file line number Diff line number Diff line change
@@ -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``.
8 changes: 8 additions & 0 deletions docs/reference/site-specific-settings.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
------------------

Expand Down
8 changes: 8 additions & 0 deletions src/argus/logging/__init__.py
Original file line number Diff line number Diff line change
@@ -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
25 changes: 25 additions & 0 deletions src/argus/logging/filters.py
Original file line number Diff line number Diff line change
@@ -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
34 changes: 34 additions & 0 deletions src/argus/logging/formatters.py
Original file line number Diff line number Diff line change
@@ -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
12 changes: 8 additions & 4 deletions src/argus/site/settings/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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!
Loading