#!/usr/local/bin/python
# encoding: utf-8
"""
*Logger setup for python projects*
Author
: David Young
"""
from builtins import object
import os
import sys
import logging
from logging import handlers
[docs]
def console_logger(level="WARNING"):
"""
*Setup and return a console logger*
**Key Arguments**
- ``level`` -- the level of logging required
**Return**
- ``logger`` -- the console logger
**Usage**
```python
from fundamentals import logs
log = logs.console_logger(
level="DEBUG"
)
log.debug("Testing console logger")
```
"""
## STANDARD LIB ##
import logging
import logging.config
## THIRD PARTY ##
import yaml
try:
yaml.warnings({"YAMLLoadWarning": False})
except:
pass
## LOCAL APPLICATION ##
# SETUP LOGGING
loggerConfig = (
"""
version: 1
formatters:
console_style:
format: '* %(asctime)s - %(levelname)s: %(pathname)s:%(funcName)s:%(lineno)d > %(message)s'
datefmt: '%H:%M:%S'
handlers:
console:
class: logging.StreamHandler
level: """
+ level
+ """
formatter: console_style
stream: ext://sys.stdout
root:
level: """
+ level
+ """
handlers: [console]"""
)
logging.config.dictConfig(yaml.safe_load(loggerConfig))
logger = logging.getLogger(__name__)
return logger
[docs]
def setup_dryx_logging(yaml_file):
"""
*setup dryx style python logging*
**Key Arguments**
- ``level`` -- the level of logging required
**Return**
- ``logger`` -- the console logger
**Usage**
```python
from fundamentals import logs
log = logs.setup_dryx_logging(
yaml_file="/Users/Dave/.config/fundamentals/fundamentals.yaml"
)
log.error("test error")
```
Here is an example of the settings in the yaml file:
```yaml
version: 1
logging settings:
formatters:
file_style:
format: '* %(asctime)s - %(name)s - %(levelname)s (%(pathname)s > %(funcName)s > %(lineno)d) - %(message)s '
datefmt: '%Y/%m/%d %H:%M:%S'
console_style:
format: '* %(asctime)s - %(levelname)s: %(pathname)s:%(funcName)s:%(lineno)d > %(message)s'
datefmt: '%H:%M:%S'
html_style:
format: '<div id="row" class="%(levelname)s"><span class="date">%(asctime)s</span> <span class="label">file:</span><span class="filename">%(filename)s</span> <span class="label">method:</span><span class="funcName">%(funcName)s</span> <span class="label">line#:</span><span class="lineno">%(lineno)d</span> <span class="pathname">%(pathname)s</span> <div class="right"><span class="message">%(message)s</span><span class="levelname">%(levelname)s</span></div></div>'
datefmt: '%Y-%m-%d <span class= "time">%H:%M <span class= "seconds">%Ss</span></span>'
handlers:
console:
class: logging.StreamHandler
level: DEBUG
formatter: console_style
stream: ext://sys.stdout
file:
class: logging.handlers.GroupWriteRotatingFileHandler
level: WARNING
formatter: file_style
filename: /Users/Dave/.config/fundamentals/fundamentals.log
mode: w+
maxBytes: 102400
backupCount: 1
root:
level: WARNING
handlers: [file,console]
```
"""
import logging
import logging.config
import yaml
try:
from StringIO import StringIO
except ImportError:
from io import StringIO
from os.path import expanduser
# IMPORT CUSTOM HANDLER THAT ALLOWS GROUP WRITING
handlers.GroupWriteRotatingFileHandler = GroupWriteRotatingFileHandler
# GET CONTENT OF YAML FILE AND REPLACE ~ WITH HOME DIRECTORY PATH
with open(yaml_file) as f:
content = f.read()
home = expanduser("~")
content = content.replace("~/", home + "/")
stream = StringIO(content)
yamlContent = yaml.safe_load(stream)
stream.close()
# ADD log.print() LEVEL
exists = addLoggingLevel("PRINT", logging.INFO + 5)
if exists:
# SET THE ROOT LOGGER
logger = logging.getLogger(__name__)
return logger
# USE THE LOGGING SETTINGS SECTION OF THE SETTINGS DICTIONARY FILE IF THERE IS ONE
# OTHERWISE ASSUME THE FILE CONTAINS ONLY LOGGING SETTINGS
if "logging settings" in yamlContent:
yamlContent = yamlContent["logging settings"]
yamlContent["version"] = 1
if (
"handlers" in yamlContent
and "file" in yamlContent["handlers"]
and "filename" in yamlContent["handlers"]["file"]
):
loggingDir = os.path.dirname(yamlContent["handlers"]["file"]["filename"])
# Recursively create missing directories
if not os.path.exists(loggingDir):
os.makedirs(loggingDir)
if "root" in yamlContent and "level" in yamlContent["root"]:
level = yamlContent["root"]["level"]
if (
"handlers" in yamlContent
and "console" in yamlContent["handlers"]
and "level" in yamlContent["handlers"]["console"]
):
level = logging.getLevelName(yamlContent["handlers"]["console"]["level"]) - 6
if "formatters" in yamlContent and "console_style" in yamlContent["formatters"]:
format = yamlContent["formatters"]["console_style"]["format"]
dateFmt = yamlContent["formatters"]["console_style"]["datefmt"]
else:
format = '%(levelname)s: "%(pathname)s", line %(lineno)d, in %(funcName)s > %(message)s'
dateFmt = "%H:%M:%S"
addConsole = False
if "root" in yamlContent and "handlers" in yamlContent["root"]:
if "console" in yamlContent["root"]["handlers"]:
addConsole = True
yamlContent["root"]["handlers"].remove("console")
if len(yamlContent["root"]["handlers"]):
logging.config.dictConfig(yamlContent)
# SET THE ROOT LOGGER
logger = logging.getLogger(__name__)
if addConsole:
consoleLog = logging.StreamHandler()
consoleLog.set_name("consoleLog")
consoleLog.setLevel(level)
cf = ColorFormatter()
cf.setFormat(format, dateFmt)
consoleLog.setFormatter(cf)
logger.addHandler(consoleLog)
logging.captureWarnings(True)
return logger
[docs]
def addLoggingLevel(levelName, levelNum, methodName=None):
"""
FROM https://stackoverflow.com/questions/2183233/how-to-add-a-custom-loglevel-to-pythons-logging-facility/35804945#35804945
Comprehensively adds a new logging level to the `logging` module and the
currently configured logging class.
`levelName` becomes an attribute of the `logging` module with the value
`levelNum`. `methodName` becomes a convenience method for both `logging`
itself and the class returned by `logging.getLoggerClass()` (usually just
`logging.Logger`). If `methodName` is not specified, `levelName.lower()` is
used.
To avoid accidental clobberings of existing attributes, this method will
raise an `AttributeError` if the level name is already an attribute of the
`logging` module or if the method name is already present
Example
-------
>>> addLoggingLevel('TRACE', logging.DEBUG - 5)
>>> logging.getLogger(__name__).setLevel("TRACE")
>>> logging.getLogger(__name__).trace('that worked')
>>> logging.trace('so did this')
>>> logging.TRACE
5
"""
if not methodName:
methodName = levelName.lower()
if hasattr(logging, levelName):
return 1
if hasattr(logging, methodName):
return 1
if hasattr(logging.getLoggerClass(), methodName):
return 1
# This method was inspired by the answers to Stack Overflow post
# http://stackoverflow.com/q/2183233/2988730, especially
# http://stackoverflow.com/a/13638084/2988730
def logForLevel(self, message, *args, **kwargs):
if self.isEnabledFor(levelNum):
self._log(levelNum, message, args, **kwargs)
def logToRoot(message, *args, **kwargs):
logging.log(levelNum, message, *args, **kwargs)
logging.addLevelName(levelNum, levelName)
setattr(logging, levelName, levelNum)
setattr(logging.getLoggerClass(), methodName, logForLevel)
setattr(logging, methodName, logToRoot)
return 0
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
"""
*rotating file handler for logging*
"""
def doRollover(self):
"""
*Override base class method to make the new log file group writable.*
"""
# Rotate the file first.
handlers.RotatingFileHandler.doRollover(self)
# Add group write to the current permissions.
currMode = os.stat(self.baseFilename).st_mode
os.chmod(
self.baseFilename,
currMode | stat.S_IWGRP | stat.S_IRGRP | stat.S_IWOTH | stat.S_IROTH,
)
[docs]
class GroupWriteRotatingFileHandler(handlers.RotatingFileHandler):
"""
*rotating file handler for logging*
"""
def _open(self):
prevumask = os.umask(0)
rtv = logging.handlers.RotatingFileHandler._open(self)
os.umask(prevumask)
return rtv
[docs]
class emptyLogger(object):
"""
*A fake logger object so user can set ``log=False`` if required*
**Usage**
```python
if log == False:
from fundamentals.logs import emptyLogger
log = emptyLogger()
```
"""
[docs]
def info(self, argu):
pass
[docs]
def error(self, argu):
pass
[docs]
def debug(self, argu):
pass
[docs]
def critical(self, argu):
pass
[docs]
def warning(self, argu):
pass
[docs]
def print(self, argu):
pass