Skip to main content

python logfmter

pre-commit test python-3.10-3.11-3.12-3.13-3.14

Add logfmt structured logging using the stdlib logging module and without changing a single log call.

> logging.warn("user created", extra=user)

at=WARNING msg="user created" first_name=John last_name=Doe age=25

Table of Contents

  1. Why
  2. Install
  3. Usage
    1. Integration
    2. Configuration
    3. Extension
    4. Guides
    5. Gotchas
  4. Development
    1. Required Software
    2. Getting Started
    3. Contributing
    4. Publishing

Why

  • enables both human and computer readable logs, recommended as a "best practice" by Splunk
  • formats all first and third party logs, you never have to worry about a library using a different logging format
  • simple to integrate into any existing application, requires no changes to existing log statements i.e. structlog

Install

$ pip install logfmter

Usage

This package exposes a single Logfmter class that can be integrated into the standard library logging system like any logging.Formatter.

Integration

Simply use the standard logger's basicConfig or dictConfig initialization systems to get started. Examples are provided below.

basicConfig

import logging
from logfmter import Logfmter

handler = logging.StreamHandler()
handler.setFormatter(Logfmter())

logging.basicConfig(handlers=[handler])

logging.error("hello", extra={"alpha": 1}) # at=ERROR msg=hello alpha=1
logging.error({"token": "Hello, World!"}) # at=ERROR token="Hello, World!"

dictConfig

If you are using dictConfig, you need to consider your setting of disable_existing_loggers. It is enabled by default, and causes any third party module loggers to be disabled.

import logging.config

logging.config.dictConfig(
    {
        "version": 1,
        "formatters": {
            "logfmt": {
                "()": "logfmter.Logfmter",
            }
        },
        "handlers": {
            "console": {"class": "logging.StreamHandler", "formatter": "logfmt"}
        },
        "loggers": {"": {"handlers": ["console"], "level": "INFO"}},
    }
)

logging.info("hello", extra={"alpha": 1}) # at=INFO msg=hello alpha=1

Notice, you can configure the Logfmter by providing keyword arguments as dictionary items after "()":

...

    "logfmt": {
        "()": "logfmter.Logfmter",
        "keys": [...],
        "mapping": {...}
    }

...

fileConfig

Using logfmter via fileConfig is not supported, because fileConfig does not support custom formatter initialization. There may be some hacks to make this work in the future. Let me know if you have ideas or really need this.

Configuration

There is no additional configuration necessary to get started using Logfmter. However, if desired, you can modify the functionality using the following initialization parameters.

keys

By default, the at=<levelname> key/value will be included in all log messages. These default keys can be overridden using the keys parameter. If the key you want to include in your output is represented by a different attribute on the log record, then you can use the mapping parameter to provide that key/attribute mapping.

Reference the Python logging.LogRecord Documentation for a list of available attributes.

import logging
from logfmter import Logfmter

formatter = Logfmter(keys=["at", "processName"])

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

logging.basicConfig(handlers=[handler])

logging.error("hello") # at=ERROR processName=MainProceess msg=hello

mapping

By default, a mapping of {"at": "levelname"} is used to allow the at key to reference the log record's levelname attribute. You can override this parameter to provide your own mappings.

import logging
from logfmter import Logfmter

formatter = Logfmter(
    keys=["at", "process"],
    mapping={"at": "levelname", "process": "processName"}
)

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

logging.basicConfig(handlers=[handler])

logging.error("hello") # at=ERROR process=MainProceess msg=hello

datefmt

If you request the asctime attribute (directly or through a mapping), then the date format can be overridden through the datefmt parameter.

import logging
from logfmter import Logfmter

formatter = Logfmter(
    keys=["at", "when"],
    mapping={"at": "levelname", "when": "asctime"},
    datefmt="%Y-%m-%d"
)

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

logging.basicConfig(handlers=[handler])

logging.error("hello") # at=ERROR when=2022-04-20 msg=hello

defaults

Instead of providing key/value pairs at each log call, you can provide defaults:

import logging
from logfmter import Logfmter

formatter = Logfmter(
    keys=["at", "when", "trace_id"],
    mapping={"at": "levelname", "when": "asctime"},
    datefmt="%Y-%m-%d",
    defaults={"trace_id": "123"},
)

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

logging.basicConfig(handlers=[handler])

logging.error("hello") # at=ERROR when=2022-04-20 trace_id=123 msg=hello

This will cause all logs to have the trace_id=123 pair regardless of including trace_id in keys or manually adding trace_id to the extra parameter or the msg object.

Note, the defaults object uses format strings as values. This allows for variables templating. See "Aliases" guide for more information.

ignored_keys

Sometimes log records include fields that you don't want in your output. This often happens when other libraries or frameworks add extra keys to the LogRecord that are not relevant to your log format.

You can explicitly exclude unwanted keys by using the ignored_keys parameter.

import logging
from logfmter import Logfmter

formatter = Logfmter(
    keys=["at"],
    mapping={"at": "levelname"},
    datefmt="%Y-%m-%d",
    ignored_keys=["color_message"],
)

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

logging.basicConfig(handlers=[handler])

logging.info("Started server process [%s]", 97819, extra={"color_message": "Started server process [%d]"})
# at=INFO msg="Started server process [97819]"

Extension

You can subclass the formatter to change its behavior.

import logging
from logfmter import Logfmter


class CustomLogfmter(Logfmter):
    """
    Provide a custom logfmt formatter which formats
    booleans as "yes" or "no" strings.
    """

    @classmethod
    def format_value(cls, value):
        if isinstance(value, bool):
            return "yes" if value else "no"

	return super().format_value(value)

handler = logging.StreamHandler()
handler.setFormatter(CustomLogfmter())

logging.basicConfig(handlers=[handler])

logging.error({"example": True}) # at=ERROR example=yes

Guides

Aliases

Providing a format string as a default's key/value allows the realization of aliases:

import logging
from logfmter import Logfmter

formatter = Logfmter(
    keys=["at", "when", "func"],
    mapping={"at": "levelname", "when": "asctime"},
    datefmt="%Y-%m-%d",
    defaults={"func": "{module}.{funcName}:{lineno}"},
)

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

logging.basicConfig(handlers=[handler])

logging.error("hello") # at=ERROR when=2022-04-20 func="mymodule.__main__:12" msg=hello

Gotchas

Reserved Keys

The standard library logging system restricts the ability to pass internal log record attributes via the log call's extra parameter.

> logging.error("invalid", extra={"filename": "alpha.txt"})
Traceback (most recent call last):
  ...

This can be circumvented by utilizing logfmter's ability to pass extras via the log call's msg argument.

> logging.error({"msg": "valid", "filename": "alpha.txt"})
at=ERROR msg=valid filename=alpha.txt

Development

Required Software

If you are using nix & direnv, then your dev environment will be managed automatically. Otherwise, you will need to manually install the following software:

Additionally, if you aren't using nix, then you will need to manually build the "external" tools found in external. These are used during testing to verify compatibility with libraries from different ecosystems. Alternatively, you can exclude those tests with pytest -m "not external", but this is not recommended.

Getting Started

Setup

If you are using pyenv, you will need to install the correct versions of python using <runtimes.txt xargs -n 1 pyenv install -s.

$ direnv allow
$ pip install -r requirements/dev.txt
$ pre-commit install
$ pip install -e .

Tests

Run the test suite against the active python environment.

$ pytest

Run the test suite against the active python environment and watch the codebase for any changes.

$ ptw

Run the test suite against all supported python versions.

$ tox

Contributing

  1. Create an issue with all necessary details.
  2. Create a branch off from main.
  3. Make changes.
  4. Verify tests pass in all supported python versions: tox.
  5. Verify code conventions are maintained: git add --all && pre-commit run -a.
  6. Create your commit following the conventionalcommits.
  7. Create a pull request with all necessary details: description, testing notes, resolved issues.

Publishing

Create

  1. Update the version number in logfmter/__init__.py.

  2. Add an entry in HISTORY.md.

  3. Commit the changes, tag the commit, and push the tags:

    $ git commit -am "v<major>.<minor>.<patch>"
    $ git tag v<major>.<minor>.<patch>
    $ git push origin main --tags
    
  4. Convert the tag to a release in GitHub with the history entry as the description.

Build

$ python -m build

Upload

$ twine upload dist/*

Metadata

Release files for logfmter 0.0.12

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for logfmter 0.0.12
File Size Uploaded
logfmter-0.0.12.tar.gz 12.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for logfmter 0.0.12
File Interpreter ABI Platform
logfmter-0.0.12-py3-none-any.whl Python 3 none any Details

Total release size: 21.3 kB

Release files / logfmter-0.0.12.tar.gz

Download URL logfmter-0.0.12.tar.gz
Size 12.4 kB
Tags Source
SHA-256 checksum
How to use checksums
958d0e1be215621451845f95f8e32660d8da760a4bcd8ecc5564a0c79570c679
BLAKE2b-256 checksum
How to use checksums
2ec07383fe1005aad176f1448e785646177a6e2c7f3260d8705bf4531be60395
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release files / logfmter-0.0.12-py3-none-any.whl

Download URL logfmter-0.0.12-py3-none-any.whl
Size 9.0 kB
Tags Python 3
SHA-256 checksum
How to use checksums
56e67c40e5772951873ed4b39dda9b6dcfebea3be949409803f71b99207a6f3f
BLAKE2b-256 checksum
How to use checksums
6483295154b0f12b3c448e6db84d63b47e7e8ca47fa8fa03b0be65d39866ab97
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/6.2.0 CPython/3.14.3

Release history Release notifications | RSS feed

This release

0.0.12 This release

2 release files

0.0.11

2 release files

0.0.10

2 release files

0.0.9

2 release files

0.0.8

2 release files

0.0.7

2 release files

0.0.6

2 release files

0.0.5

2 release files

0.0.4

2 release files

0.0.3

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page