Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can structured logging be done with Pythons standard library?

I recently read about structured logging (here). The idea seems to be to log not by appending simple strings as a line to a logfile, but instead JSON objects. This makes it possible to analyze the logfile by automatic tools.

Can Pythons logging library do structured logging? If not, is there a "mainstream" solution for it (e.g. like numpy/scipy is the mainstream solution for scientific calculations)? I found structlog, but I'm not sure how widespread it is.

like image 310
Martin Thoma Avatar asked Jan 09 '18 14:01

Martin Thoma


2 Answers

Have you looked at python docs site section describing Implementing structured logging that explain how python built-in logger can be utilized for structured logging?

Below is a simple example as listed on above site .

import json
import logging

class StructuredMessage(object):
    def __init__(self, message, **kwargs):
        self.message = message
        self.kwargs = kwargs

    def __str__(self):
        return '%s >>> %s' % (self.message, json.dumps(self.kwargs))

m = StructuredMessage   # optional, to improve readability

logging.basicConfig(level=logging.INFO, format='%(message)s')
logging.info(m('message 1', foo='bar', bar='baz', num=123, fnum=123.456))

Which results in following log.

message 1 >>> {"fnum": 123.456, "num": 123, "bar": "baz", "foo": "bar"}

Hope this helps.

like image 56
Anil_M Avatar answered Sep 19 '22 17:09

Anil_M


As of py3.2, it's possible to do this with the standard library, no external dependencies required:

from datetime import datetime
import json
import logging
import traceback


APP_NAME = 'hello world json logging'
APP_VERSION = 'git rev-parse HEAD'
LOG_LEVEL = logging._nameToLevel['INFO']


class JsonEncoderStrFallback(json.JSONEncoder):
  def default(self, obj):
    try:
      return super().default(obj)
    except TypeError as exc:
      if 'not JSON serializable' in str(exc):
        return str(obj)
      raise


class JsonEncoderDatetime(JsonEncoderStrFallback):
  def default(self, obj):
    if isinstance(obj, datetime):
      return obj.strftime('%Y-%m-%dT%H:%M:%S%z')
    else:
      return super().default(obj)


logging.basicConfig(
  format='%(json_formatted)s',
  level=LOG_LEVEL,
  handlers=[
    # if you wish to also log to a file -- logging.FileHandler(log_file_path, 'a'),
    logging.StreamHandler(sys.stdout),
  ],
)


_record_factory_bak = logging.getLogRecordFactory()
def record_factory(*args, **kwargs) -> logging.LogRecord:
  record = _record_factory_bak(*args, **kwargs)
  
  record.json_formatted = json.dumps(
    {
      'level': record.levelname,
      'unixtime': record.created,
      'thread': record.thread,
      'location': '{}:{}:{}'.format(
        record.pathname or record.filename,
        record.funcName,
        record.lineno,
      ),
      'exception': record.exc_info,
      'traceback': traceback.format_exception(*record.exc_info) if record.exc_info else None,
      'app': {
        'name': APP_NAME,
        'releaseId': APP_VERSION,
        'message': record.getMessage(),
      },
    },
    cls=JsonEncoderDatetime,
  )
  return record
logging.setLogRecordFactory(record_factory)

Calling logging.info('HELLO %s', 'WORLD') ...

... results in {"level": "INFO", "unixtime": 1623532882.421775, "thread": 4660305408, "location": "<ipython-input-3-abe3276ceab4>:<module>:1", "exception": null, "traceback": null, "app": {"name": "hello world json logging", "releaseId": "git rev-parse HEAD", "message": "HELLO WORLD"}}

like image 32
user2426679 Avatar answered Sep 17 '22 17:09

user2426679