Skip to content

Instantly share code, notes, and snippets.

@JohnL4
Created September 10, 2020 21:38
Show Gist options
  • Save JohnL4/e8bf5a253003aa0458471f448af9b302 to your computer and use it in GitHub Desktop.
Save JohnL4/e8bf5a253003aa0458471f448af9b302 to your computer and use it in GitHub Desktop.
Complex logging configuration in python, using the "logging" module and yaml configuration
version: 1
formatters:
# Note that the following formats may use the 'hostname' attribute, which is non-standard. Be sure to use an adapter
# or a filter to get that info into the LogRecord context or ugliness will result.
# See:
# - mem_cleaner2.HostnameLoggerAdapter
# - https://docs.python.org/3/howto/logging-cookbook.html#adding-contextual-information-to-your-logging-output
#
oneLine:
format: "%(asctime)s %(levelname)-8s -- %(message)s -- %(module)s.%(funcName)s:%(lineno)d -- %(pathname)s"
multiLine:
format: "%(asctime)s -- %(levelname)s\n\n%(message)s\n\n%(hostname)s -- %(module)s.%(funcName)s:%(lineno)d -- %(pathname)s"
filters:
smtpWorthy:
(): __main__.SmtpWorthy # Some magic filter w/code I write to sent "restart" messages unconditionally (by
# matching regex, I guess) and critical-failure exceptions every two hours
# (between x:00 and x:10, where x is an even number).
handlers:
console:
class: logging.StreamHandler
stream: ext://sys.stdout
level: NOTSET # Let logger set level
formatter: oneLine
file:
class: logging.handlers.RotatingFileHandler
filename: "Logs/mem_cleaner2.log" # Directory must exist ahead of time or logging will fail (i.e., not auto-created).
maxBytes: 10485760 # 1048576 is a megabyte
backupCount: 10
level: NOTSET # Let logger set level
formatter: oneLine
smtp:
class: logging.handlers.SMTPHandler
mailhost: smtp.ur-company.com
fromaddr: [email protected]
toaddrs:
# - [email protected]
# - [email protected]
- [email protected]
subject: "mem_cleaner2.py restarted a cube (or saw a critical error)"
timeout: 15.0 # In seconds, I assume
level: NOTSET # Let logger set level
formatter: multiLine
filters: [smtpWorthy]
# Unless there are other loggers declared (probably not necessary for a script this simple), there's only the root logger.
root:
level: DEBUG
handlers:
- console
- file
- smtp
# Excerpted from my running production code.
# Probably don't need all this garbage, but oh well.
import sys
import os
import socket
import io
import traceback
import logging, logging.config
import re
import time
from ruamel.yaml import YAML # Not built-in
# ... blah blah blah ...
def configure():
# Get settings
if getattr(sys, 'frozen', False):
# PyInstaller .exe
application_path = os.path.dirname(sys.executable)
elif __file__:
# "Normal" Python script
application_path = sys.path[0]
# 'safe' loader as opposed to 'rt' (round-trip; but we won't be writing any YAML, so no need);
# pure Python yaml-parsing code as opposed to whatever C/C++ parser the module finds
yamlReader = YAML(typ='safe', pure=True)
print( 'Attempt logging config')
try:
logconfig = yamlReader.load( open( application_path + r'\log-config.yaml', 'r'))
logging.config.dictConfig( logconfig)
except:
logging.exception( 'Exception configuring logging')
logger = HostnameLoggerAdapter( logging.getLogger('__main__'), {'hostname': socket.gethostname()})
logger.debug( 'Logging configured')
class SmtpWorthy(logging.Filter):
"""Decides whether a particular log record is worthy of an SMTP message."""
def filter(self, aLogRecord):
if re.match( 'MAI-WORKSTATION-NAME', socket.gethostname()):
# Don't be spamming people from your dev workstation.
retval = False
elif aLogRecord.levelno == logging.CRITICAL:
# Top priority
retval = True
elif re.search('Full cube info:', aLogRecord.message):
# Filter this out because it produces a false match on the regex below.
# It's a huge blob of text that just means the cube isn't running (it might be just sleeping).
retval = False
else:
retval = re.search('Restarting|Detaching', aLogRecord.message)
return retval
class HostnameLoggerAdapter( logging.LoggerAdapter):
"""
Looks like a logger; adds current hostname (as 'hostname') to LogRecord context info.
NOTE: Inherited constructor seems to set 'extra' attribute, so we just need to override processing.
"""
def process( self, msg, kwargs):
"""Less-destructive handling of 'extra' keyword."""
if 'extra' in kwargs:
kwargs['extra'].update( self.extra)
else:
kwargs['extra'] = self.extra
return msg, kwargs
@JohnL4
Copy link
Author

JohnL4 commented Sep 10, 2020

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment