Skip to main content

No project description provided

Project description

Dan's Log Formatter

Extensible, reusable, awesome log formatter for Python

Test Coverage Package version Supported Python versions


You too are tiered of rewriting log handling for each project? Here's my simple, extensible formatter, designed so we never have to write it again.

This log formatter ships with commonly used features, like JSON serialization, attribute injection, error handling and more.

Adding log attributes beside the message is made simple, like contextual data, runtime information, request information, basicly whatever you may need. Those attribute providers can easily be shared between your services, streamlining the development experince between your services.

Features

  • Extensible - Add attributes to logs with ease, including simple error handling
  • Reusable - Share your attribute providers across projects
  • Contextual - Automatically adds useful context to logs
  • Out-of-the-box - Include common providers for HTTP data, runtime, and more

My log record's default attributes are mostly compatible with DataDog's Standard Attributes.

Integrations

  • Django - Automatically adds request context
  • FastAPI - Automatically adds request context (including Starlette support)
  • Flask - Automatically adds request context
  • Celery - Automatically adds task context
  • orjson - Uses orSON for serialization
  • ujson - Uses uJSON for serialization

Usage

Install my package using pip:

pip install dans-log-formatter

Then set up your logging configuration:

import logging.config

from dans_log_formatter.providers.context import ContextProvider

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "providers": [
        ContextProvider(),
      ],  # optional
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "formatter": "json",
    }
  },
  "root": {
    "handlers": ["console"],
    "level": "INFO",
  },
})

Then, use it in your project:

import logging

logger = logging.getLogger(__name__)


def main():
  logger.info("Hello, world!")


if __name__ == "__main__":
  main()

# STDOUT: {'timestamp': 1704060000.0, 'status': 'INFO', 'message': 'hello world!', 'location': 'my_module-main#4', 'file': '/Users/danyi1212/projects/my-project/my_module.py'}

Providers

Providers add attributes to logs. You can use the built-in providers or create your own.

Context Provider

Inject context into logs using decorator or context manager.

from dans_log_formatter import JsonLogFormatter
from dans_log_formatter.providers.context import ContextProvider

formatter = JsonLogFormatter(providers=[ContextProvider()])

Then use the inject_log_context() as a context manager

import logging
from dans_log_formatter.providers.context import inject_log_context

logger = logging.getLogger(__name__)

with inject_log_context({"user_id": 123}):
  logger.info("Hello, world!")

# STDOUT: {'timestamp': 1704060000.0, 'status': 'INFO', 'message': 'hello world!', 'user_id': 123, ...}

Alternatively, use it as @inject_log_context() decorator

import logging
from dans_log_formatter.providers.context import inject_log_context

logger = logging.getLogger(__name__)


@inject_log_context({"custom_context": "value"})
def my_function():
  logger.info("Hello, world!")

# STDOUT: {'timestamp': 1704060000.0, 'status': 'INFO', 'message': 'hello world!', 'custom_context': 'value', ...}

Extra Provider

Add ExtraProvider() from dans_log_formatter.providers.extra, then use the extra={} argument in your log calls

import logging

logger = logging.getLogger(__name__)
logger.info("Hello, world!", extra={"user_id": 123})
# STDOUT: {'timestamp': 1704060000.0, 'status': 'INFO', 'message': 'hello world!', 'user_id': 123, ...}

Runtime Provider

Add RuntimeProvider() from dans_log_formatter.providers.runtime to add runtime information to logs.

Attributes

  • process - Current process name and ID (e.g. main (12345))
  • thread - Current thread name and ID (e.g. MainThread (12345))
  • task - Current asyncio task name (e.g. my_corrutine)

Create your own provider

from logging import LogRecord
from typing import Any

from dans_log_formatter.providers.abstract import AbstractProvider


class MyProvider(AbstractProvider):
  """Add 'my_attribute' to all logs"""

  def get_attributes(self, record: LogRecord) -> dict[str, Any]:
    return {"my_attribute": "some value"}

You can also use the abstract context provider to add data from contextvars

from contextvars import ContextVar
import logging
from typing import Any
from dataclasses import dataclass

from dans_log_formatter.providers.abstract_context import AbstractContextProvider


@dataclass
class User:
  id: int
  name: str


current_user_context: ContextVar[User | None] = ContextVar("current_user_context", default=None)


class MyContextProvider(AbstractContextProvider):
  """Add user.id and user.name context to logs"""

  def __init__(self):
    super().__init__(current_user_context)  # Pass the context

  def get_context_attributes(self, record: logging.LogRecord, current_user: User) -> dict[str, Any]:
    return {"user.id": current_user.id, "user.name": current_user.name}


logger = logging.getLogger(__name__)

token = current_user_context.set(User(id=123, name="John Doe"))
logger.info("Hello, world!")
current_user_context.reset(token)
# STDOUT: {'timestamp': 1704060000.0, 'status': 'INFO', 'message': 'Hello, world!', 'user.id': 123, 'user.name': 'John Doe', ...}

Integrations

Django Request Provider

Install using 'pip install dans-log-formatter[django]'

Add the 'LogContextMiddleware' to your Django middlewares at the very beginning.

# settings.py
MIDDLEWARE = [
  "dans_log_formatter.contrib.django.middleware.LogContextMiddleware",
  ...
]

Then, add DjangoRequestProvider() to your formatter.

# settings.py
from dans_log_formatter.contrib.django.provider import DjangoRequestProvider

LOGGING = {
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "providers": [
        DjangoRequestProvider(),
      ],
    }
  },
  # ...
}

Attributes

  • resource - View route (e.g. POST /api/users/<int:user_id>/delete)
  • http.url - Full URL (e.g. https://example.com/api/users/123/delete)
  • http.method - HTTP method (e.g. POST)
  • http.referrer - Referrer header (e.g. https://example.com/previous-page)
  • http.user_agent - useragent header
  • http.remote_addr - X-Forwarded-For or REMOTE_ADDR header
  • user.id - User ID
  • user.name - User's username
  • user.email - User email

Note: The user attributes available only inside the django.contrib.auth.middleware.AuthenticationMiddleware middleware.

FastAPI Request Provider

Install using 'pip install dans-log-formatter[fastapi]'

Add the 'LogContextMiddleware' to your FastAPI app.

from fastapi import FastAPI
from dans_log_formatter.contrib.fastapi.middleware import LogContextMiddleware

app = FastAPI()
app.add_middleware(LogContextMiddleware)

Then, add FastAPIRequestProvider() to your formatter.

import logging.config
from dans_log_formatter.contrib.fastapi.provider import FastAPIRequestProvider

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "providers": [
        FastAPIRequestProvider(),
      ],
    }
  },
  # ...
})

Attributes

  • resource - Route path (e.g. POST /api/users/{user_id}/delete)
  • http.url - Full URL (e.g. https://example.com/api/users/123/delete)
  • http.method - HTTP method (e.g. POST)
  • http.referrer - Referrer header (e.g. https://example.com/previous-page)
  • http.user_agent - useragent header
  • http.remote_addr - X-Forwarded-For header or the request.client.host attribute

Flask Request Provider

Install using 'pip install dans-log-formatter[flask]'

Add the 'FlastRequestProvider' to your formatter, and its magic!

import logging.config
from dans_log_formatter.contrib.flask.provider import FlaskRequestProvider

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "providers": [
        FlaskRequestProvider(),
      ],
    }
  },
  # ...
})

Attributes

  • resource - URL path (e.g. POST /api/users/123/delete)
  • http.url - Full URL (e.g. https://example.com/api/users/123/delete)
  • http.method - HTTP method (e.g. POST)
  • http.referrer - Referrer header (e.g. https://example.com/previous-page)
  • http.user_agent - useragent header
  • http.remote_addr - request.remote_addr attribute

Celery Task Provider

Install using 'pip install dans-log-formatter[celery]'

Add the 'CeleryTaskProvider' to your formatter, and its magic!

import logging.config
from dans_log_formatter.contrib.celery.provider import CeleryTaskProvider

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "providers": [
        CeleryTaskProvider(),  # optional include_args=True
      ],
    }
  },
  # ...
})

Attributes

  • resource - Task name (e.g. my_project.tasks.my_task)
  • task.id - Task ID
  • task.retries - Number of retries
  • task.root_id - Root task ID
  • task.parent_id - Parent task ID
  • task.origin - Producer host name
  • task.delivery_info - Delivery info ( e.g. {"exchange": "my_exchange", "routing_key": "my_routing_key", "queue": "my_queue"})
  • task.worker - Worker hostname
  • task.args - Task arguments (if include_args=True)
  • task.kwargs - Task keyword arguments (if include_args=True)

Warning: Including task arguments can expose sensitive information, and may result in very large logs.

ujson Formatter

Install using 'pip install dans-log-formatter[ujson]'

Uses ujson for JSON serialization of the log records.

import logging.config

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.contrib.ujson.UJsonLogFormatter",
      "providers": [],  # optional
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "formatter": "json",
    }
  },
  "root": {
    "handlers": ["console"],
    "level": "INFO",
  },
})

orjson Serializer Formatter

Install using 'pip install dans-log-formatter[orjson]'

Uses orjson for JSON serialization of the log records.

import logging.config

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.contrib.orjson.OrJsonLogFormatter",
      "providers": [],  # optional
    }
  },
  "handlers": {
    "console": {
      "class": "logging.StreamHandler",
      "formatter": "json",
    }
  },
  "root": {
    "handlers": ["console"],
    "level": "INFO",
  },
})

Available Formatters

By default, all formatter includes the following attributes:

  • timestamp - Unix timestamp (same as the record.created attribute, or the value returned by time.time(). See the docs)
  • status - Log level name (e.g. INFO, ERROR, CRITICAL)
  • message - Log message
  • location - Location of the log call (e.g. my_module-my_func#4)
  • file - File path of the log call (e.g. /Users/danyi1212/projects/my-project/my_module.py)
  • error - Exception message and traceback (when exec_info=True)
  • stack_info - Stack trace (when stack_info=True)
  • formatter_errors - Errors from the formatter or providers (when an error occurs)

By default, the message value is truncated to 64k characters, and the error, 'stack_info', and formatter_errors values are truncated to 128k characters.

You can override the default truncation using:

import logging.config

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "json": {
      "()": "dans_log_formatter.JsonLogFormatter",
      "message_size_limit": 1024,  # Set None to unlimited
      "stack_size_limit": 1024,  # Set None to unlimited
    }
  },
  # ...
})

JsonLogFormatter

Format log records as JSON using json.dumps().

TextLogFormatter

Format log records as human-readable text using logging.Formatter (See the docs).

All attributes are available to use in the format string.

The timestamp attribute is formatted using the datefmt like in the logging.Formatter.

import logging.config

from dans_log_formatter.providers.context import ContextProvider, inject_log_context

logging.config.dictConfig({
  "version": 1,
  "formatters": {
    "text": {
      "()": "dans_log_formatter.TextLogFormatter",
      "providers": [ContextProvider()],
      "fmt": "{timestamp} {status} | {user_id} - {message}",
      "datefmt": "%H:%M:%S",
      "style": "{"
    }
  },
  # ...
})

logger = logging.getLogger(__name__)

with inject_log_context({"user_id": 123}):
  logger.info("Hello, world!")

# STDOUT: 12:00:42 INFO | 123 - Hello, world!

Extending your own formatter

You can extend the JsonLogFormatter to modify the default attributes, add new ones, use other log record serializer or anything else.

import socket
from logging import LogRecord
import xml.etree.ElementTree as ET

from dans_log_formatter import JsonLogFormatter


class MyCustomFormatter(JsonLogFormatter):
  root_tag = "log"

  def format(self, record: LogRecord) -> str:
    # Serialize to XML instead of JSON
    return self.attributes_to_xml(self.get_attributes(record))

  def attributes_to_xml(self, attributes: dict[str, str]) -> str:
    root = ET.Element(self.root_tag)
    for key, value in attributes.items():
      element = ET.SubElement(root, key)
      element.text = value
    return ET.tostring(root, encoding="unicode")

  def format_status(self, record: LogRecord) -> int:
    return record.levelno  # Use the level number instead of the level name

  def format_location(self, record: LogRecord) -> str:
    return f"{record.module}-{record.funcName}"  # Use only the module and function name, without the line number

  def format_exception(self, record: LogRecord) -> str:
    return f"{record.exc_info[0].__name__}: {record.exc_info[1]}"  # Use only the exception name and message

  def get_attributes(self, record: LogRecord) -> dict:
    attributes = super().get_attributes(record)
    attributes["hostname"] = socket.gethostname()  # Add an extra hostname default attribute
    return attributes

Note: Creating a custom HostnameProvider is a better way to add the hostname attribute.

Error handling

When an error occurs in the formatter or providers, the formatter_errors attribute is added to the log record.

Silent errors can be added to the formatter_errors attribute using the record_error() method.

from dans_log_formatter.providers.abstract import AbstractProvider


class MyProvider(AbstractProvider):
  def get_attributes(self, record: LogRecord) -> dict[str, Any]:
    self.record_error("Something went wrong")  # Add an error to the formatter_errors attribute
    return {'my_attribute': 'some value'}

Exception traceback context is automatically added to the recorded error or caught exceptions described in the formatter_errors attribute.

Contributing

Before contributing, please read the contributing guidelines for guidance on how to get started.

License

This project is licensed under the MIT License.

Happy logging!

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dans_log_formatter-0.1.1.tar.gz (15.3 kB view details)

Uploaded Source

Built Distribution

dans_log_formatter-0.1.1-py3-none-any.whl (17.4 kB view details)

Uploaded Python 3

File details

Details for the file dans_log_formatter-0.1.1.tar.gz.

File metadata

  • Download URL: dans_log_formatter-0.1.1.tar.gz
  • Upload date:
  • Size: 15.3 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.2 Linux/6.5.0-1016-azure

File hashes

Hashes for dans_log_formatter-0.1.1.tar.gz
Algorithm Hash digest
SHA256 378296fa098d4dee955c9b4f0e9917c0662cdc61f0a2c95c1c798d7a7a991a91
MD5 68fddba49acd8be99169f571a021c857
BLAKE2b-256 6c5c08d2e08f0b3ae91664a106c4f4e0cbceddfe5938acc3fe0e82ceacabc718

See more details on using hashes here.

File details

Details for the file dans_log_formatter-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: dans_log_formatter-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 17.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: poetry/1.8.2 CPython/3.12.2 Linux/6.5.0-1016-azure

File hashes

Hashes for dans_log_formatter-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 574b1081ac105bfdc3f35460cd4be5e15262db6e189e1c492506773fb31f1b94
MD5 5c553f3ac0d0dec3118644b8fee80397
BLAKE2b-256 430bd43986d845d857d63b439af5bcef59099dc6bec8e24eb5f738d73619891d

See more details on using hashes here.

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page