/gelf-formatter

GELF formatter for the Python standard library logging module.

Primary LanguagePythonMIT LicenseMIT

GELF Formatter

Graylog Extended Log Format (GELF) formatter for the
Python standard library logging module

Release PyPI Python versions Travis Codecov Software License SemVer Conventional Commits Code style Downloads Contributors SayThanks.io


Motivation

There are several packages available providing handlers for the standard library logging module that can send application logs to Graylog by TCP/UDP/HTTP (py-gelf is a good example). Although these can be useful, it's not ideal to make an application performance dependent on network requests just for the purpose of delivering logs.

Alternatively, one can simply log to a file or stdout and have a collector (like Fluentd) processing and sending those logs asynchronously to a remote server (and not just to Graylog, as GELF can be used as a generic log format), which is a common pattern for containerized applications. In a scenario like this all we need is a GELF logging formatter.

Features

  • Support for arbitrary additional fields;
  • Support for including reserved logging.LogRecord attributes as additional fields;
  • Exceptions detection with traceback formatting;
  • Zero dependencies and tiny footprint.

Installation

With pip

$ pip install gelf-formatter

From source

$ python setup.py install

Usage

Simply create a gelfformatter.GelfFormatter instance and pass it as argument to logging.Handler.setFormatter:

import sys
import logging

from gelfformatter import GelfFormatter

formatter = GelfFormatter()

handler = logging.StreamHandler(sys.stdout)
handler.setFormatter(formatter)

Apply it globally with logging.basicConfig to automatically format log records from third-party packages as well:

logging.basicConfig(level=logging.DEBUG, handlers=[handler])

Alternatively, you can configure a local logging.Logger instance through logging.Logger.addHandler:

logger = logging.getLogger('my-app')
logger.addHandler(handler)

That's it. You can now use the logging module as usual, all records will be formatted as GELF messages.

Standard Fields

The formatter will output all (non-deprecated) fields described in the GELF Payload Specification (version 1.1):

  • version: String, always set to 1.1;

  • host: String, the output of socket.gethostname at initialization;

  • short_message: String, log record message;

  • full_message (optional): String, formatted exception traceback (if any);

  • timestamp: Number, time in seconds since the epoch as a floating point;

  • level: Integer, syslog severity level.

None of these fields can be ignored, renamed or overridden.

Example

logging.info("Some message")
{"version":"1.1","host":"my-server","short_message":"Some message","timestamp":1557342545.1067393,"level":6}

Exceptions

The full_message field is used to store the traceback of exceptions. You just need to log them with logging.exception.

Example
import urllib.request

req = urllib.request.Request('http://www.pythonnn.org')
try:
    urllib.request.urlopen(req)
except urllib.error.URLError as e:
    logging.exception(e.reason)
{"version": "1.1", "short_message": "[Errno -2] Name or service not known", "timestamp": 1557342714.0695107, "level": 3, "host": "my-server", "full_message": "Traceback (most recent call last):\n  ...(truncated)... raise URLError(err)\nurllib.error.URLError: <urlopen error [Errno -2] Name or service not known>"}

Additional Fields

The GELF specification allows arbitrary additional fields, with keys prefixed with an underscore.

To include additional fields use the standard logging extra keyword. Keys will be automatically prefixed with an underscore (if not already).

Example

logging.info("request received", extra={"path": "/orders/1", "method": "GET"})
{"version": "1.1", "short_message": "request received", "timestamp": 1557343604.5892842, "level": 6, "host": "my-server", "_path": "/orders/1", "_method": "GET"}

Reserved Fields

By default the formatter ignores all logging.LogRecord attributes. You can however opt to include them as additional fields. This can be used to display useful information like the current module, filename, line number, etc.

To do so, simply pass a list of LogRecord attribute names as value of the allowed_reserved_attrs keyword when initializing a GelfFormatter. You can also modify the allowed_reserved_attrs instance variable of an already initialized formatter.

Example
attrs = ["lineno", "module", "filename"]

formatter = GelfFormatter(allowed_reserved_attrs=attrs)
# or
formatter.allowed_reserved_attrs = attrs

logging.debug("starting application...")
{"version": "1.1", "short_message": "starting application...", "timestamp": 1557346554.989846, "level": 6, "host": "my-server", "_lineno": 175, "_module": "myapp", "_filename": "app.py"}

You can optionally customize the name of these additional fields using a logging.Filter (see below).

Similarily, you can choose to ignore additional attributes passed via the extra keyword argument. This can be usefull to e.g. not log keywords named secret or password.

To do so, pass a list of names to the ignored_attrs keyword when initializing a GelfFormatter. You can also modify the ignored_attrs instance variable of an already initialized formatter.

Example

But be aware: nested fields will be printed! Only the root level of keywords is filtered by the ignored_attrs.

attrs = ["secret", "password"]

formatter = GelfFormatter(ignored_attrs=attrs)
# or
formatter.ignored_attrs = attrs

logging.debug("app config", extra={"connection": "local", "secret": "verySecret!", "mysql": {"user": "test", "password": "will_be_logged"}})
{"version": "1.1", "short_message": "app config", "timestamp": 1557346554.989846, "level": 6, "host": "my-server", "_connection": "local", "_mysql": {"user": "test", "password": "will_be_logged"}}

Context Fields

Having the ability to define a set of additional fields once and have them included in all log messages can be useful to avoid repetitive extra key/value pairs and enable contextual logging.

Python's logging module provides several options to add context to a logger, among which we highlight the logging.LoggerAdapter and logging.Filter.

Between these we recommend a logging.Filter, which is simpler and can be attached directly to a logging.Handler. A logging.Filter can therefore be used locally (on a logging.Logger) or globally (through logging.basicConfig). If you opt for a LoggerAdapter you'll need a logging.Logger to wrap.

You can also use a logging.Filter to reuse/rename any of the reserved logging.LogRecord attributes.

Example
class ContextFilter(logging.Filter):
    def filter(self, record):
        # Add any number of arbitrary additional fields
        record.app = "my-app"
        record.app_version = "1.2.3"
        record.environment = os.environ.get("APP_ENV")

        # Reuse any reserved `logging.LogRecord` attributes
        record.file = record.filename
        record.line = record.lineno
        return True


formatter = GelfFormatter()

handler = logging.StreamHandler(sys.stdout)

handler.setFormatter(formatter)
handler.addFilter(ContextFilter())

logging.basicConfig(level=logging.DEBUG, handlers=[handler])

logging.info("hi", extra=dict(foo="bar"))
{"version": "1.1", "short_message": "hi", "timestamp": 1557431642.189755, "level": 6, "host": "my-server", "_foo": "bar", "_app": "my-app", "_app_version": "1.2.3", "_environment": "development", "_file": "app.py", "_line": 159}

Pretty-Print

Looking for a GELF log pretty-printer? If so, have a look at gelf-pretty 🔥

Contributions

This project adheres to the Contributor Covenant code of conduct. By participating, you are expected to uphold this code. Please refer to our contributing guide for further information.