Logger, der weiß von welcher Klasse er aufgerufen wurde

Code-Stücke können hier veröffentlicht werden.
Antworten
Igor
User
Beiträge: 8
Registriert: Donnerstag 15. Februar 2007, 13:16

Mittwoch 14. November 2007, 21:03

Hallo,

ich habe die letzten Abende an einer Erweiterung für den standard-Python-Logger gesessen, der mir nicht nur sagt, von welcher Funktion er aufgerufen wurde, sondern auch von welcher Klasse. Ausserdem erlaubt er, das Logging Packet/Modul/Klassen/Methodenweise zu konfigurieren, ohne dabei tausend logger-Instanzen handlen zu müssen. Mehr Infos stehen im __doc__ :-)

Der im Code angegebene FormatString erzeugt Ausgaben im folgenden Format:
[INFO] ClassName.MethodName() =>Was die Methode zu sagen hat!
Der Code ist noch nicht final, tuts aber schon ganz gut. Und entschuldigt die deutschen Kommentare :roll:

Verbesserungsvorschläge, vor allem was die Funktion _analyze_frame() (ganz unten) angeht, sind herzlich willikommen!

Code: Alles auswählen

#!/usr/bin/python
# -*- coding: ISO-8859-1 -*-

import logging as original_logging
import inspect, types

"""
Dieses Modul ist als Erweiterung zum Standard-Python-Logging-Modul
konzipiert.

============================================
Änderungen im Verhalten des Standard-Loggers
============================================
  Wenn eine der Logging-Methoden (debug...critical) aus einer 
  Klassen-Methode herraus aufgerufen wird, wird der String
  'Packagename.Modulename.Klassenname.Methodenname' als Name 
  für die Logging-Klasse verwendet.
  
  Im Format-String für Logging-Ausgaben stehen zusätzlich die 
  Keys
    %(classname)s
    %(functionname)s und
    %(packagename)s 
  zur Verfügung. Wenn keine Klasse ermittelt werden konnte, 
  enthält %(classname)s den String 'NoClass'.
   
  Der Standard-Key %(funcName)s liefert leider immer _do_log 
  aus diesem Modul zurück.

=============================================================  
Vorteile gegenüber dem Standard-Logger ohne diese Erweiterung
=============================================================
  Dies ermöglicht, dass das Logging Package-, Module-, Klassen- 
  und/oder Methodenweise gefiltert werden kann, ohne dass jede 
  Klasse seinen eigenen Logger bereithalten muss.

===================================  
Kompatiblität zum Standard-Logger 
Migration von bestehendem Quellcode
===================================
  Scripte, die über
   
    import logging
    logging.info("Hello World!")
    
  geloggt haben, können sehr einfach angepasst werden, indem der 
  Import auf
  
    import contextlogging as logging
    
  geändert wird. Sollten auch Zugriffe auf das original-Logging-Modul
  nötig sein, kann dies z.B. als 
  
    import logging as original_logging
    
  importiert werden. So können beispielsweise folgende Aufrufe 
  umgeleitet werden:
  
    original_logging.basicConfig(level=original_logging.DEBUG,
                                 format='[%(levelname)s] %(classname)s.%(functionname)s() => %(message)s')

    # Setzt das Logging für das gesammte Package auf Warn
    original_logging.getLogger('testpackage').setLevel(old_logging.WARN)
    
    # Setzt das Logging für diese einzelne Klasse auf Debug
    original_logging.getLogger('testpackage.testpackage.TestClass').
      setLevel(old_logging.DEBUG)  

=====      
TODOs
=====
  Funktion _analyze_frame(frame)  
    Der packagename sollte auch zurückgegeben werden, wenn keine Klasse 
    ermittelt werden konnte.
    
"""

def critical(msg, *args, **kwargs):
  """Wrapper für logging.critical"""
  frame = inspect.currentframe().f_back
  _do_log(frame, original_logging.CRITICAL, msg, *args, **kwargs)
  del frame # see Doku 26.10.4

fatal = critical

def error(msg, *args, **kwargs):
  """Wrapper für logging.error"""
  frame = inspect.currentframe().f_back
  _do_log(frame, original_logging.ERROR, msg, *args, **kwargs)
  del frame # see Doku 26.10.4

def exception(msg, *args):
  """Wrapper für logging.exception"""
  frame = inspect.currentframe().f_back
  logger = _get_logger(frame)
  logger.exception(msg, *args)
  del frame # see Doku 26.10.4

def warning(msg, *args, **kwargs):
  """Wrapper für logging.warning"""
  frame = inspect.currentframe().f_back
  _do_log(frame, original_logging.WARNING, msg, *args, **kwargs)
  del frame # see Doku 26.10.4
      
warn = warning

def info(msg, *args, **kwargs):
  """Wrapper für logging.info"""
  frame = inspect.currentframe().f_back
  _do_log(frame, original_logging.INFO, msg, *args, **kwargs)
  del frame # see Doku 26.10.4

def debug(msg, *args, **kwargs):
  """Wrapper für logging.debug"""
  frame = inspect.currentframe().f_back
  _do_log(frame, original_logging.DEBUG, msg, *args, **kwargs)
  del frame # see Doku 26.10.4

def log(level, msg, *args, **kwargs):
  """Wrapper für logging.log"""
  frame = inspect.currentframe().f_back
  _do_log(frame, level, msg, *args, **kwargs)  
  del frame # see Doku 26.10.4
  
def setLevel(level):
  """Ermittelt anhand des Frames den passenden Logger und setzt
    dessen Logging-Level 
  """
  frame = inspect.currentframe().f_back
  logger = _get_logger(frame)
  logger.setLevel(level)
  del frame # see Doku 26.10.4
  
# ==== PRIVATE FUNKTIONEN ===================================================
def _get_logger(frame):
  """
    Ermittelt anhand des Frames den passenden Logger.
    Als Loggername wird Classname.Functionname verwendet.
    Wenn der Classname None ist, wird der RootLogger zurückgegeben.
  """
  (classname, function, packagename) = _analyze_frame(frame)
  area = ""
  
  if packagename:
    area += packagename
    
  if classname:    
    area = '.'.join((area, classname))
    
  if area != "":
    area = '.'.join((area, function))
    logger = original_logging.getLogger(area)
  else:
    logger = original_logging.root
  return logger
  
def _do_log(frame, level, msg, *args, **kwargs):
  """
    Ermittelt anhand des Frames den passenden Logger und ruft dessen
    Log-Methode auf.    
  """
  logger = _get_logger(frame)
  
  # classname und packagename im Formatstring verfügbar machen
  (classname, function, packagename) = _analyze_frame(frame)  
  if 'extra' not in kwargs.keys():
    kwargs['extra'] = {}
    
  if not classname:
    classname = 'NoClass'
    
  kwargs['extra'].update({'classname': classname,
                          'functionname': function,
                          'packagename': packagename})
  
  logger.log(level, msg, *args, **kwargs )
     
def _analyze_frame(frame):  
  """
    Diese Methode extrahiert Funktionsname, Klassenname und Packagename aus dem 
    übergebenen Frame.
    
    Wenn die Funktion keine Methode oder eine static-Methode ist, ist der Klassenname
    None.
  
    Dieser Hack hat folgende Probleme:
     - Klappt nicht für statische Methoden
     - Klappt nicht, wenn der erste Parameter nicht self heißt
     - Klappt nicht, wenn ich eine einfache Funktion schreibe, deren erster Parameter self heißt.
     - Ist weder sauber noch schön.
  
    ToDo: Der packagename sollte auch zurückgegeben werden, wenn keien
    Klasse ermittelt werden kann.
  """ 

  klass = None
  # Pruefe, ob die aufgerufene Funktion mindestens einen Parameter uebernimmt  
  if len(frame.f_code.co_varnames) > 0:
    # Pruefe, ob der erste Parameter 'self' heisst
    if frame.f_code.co_varnames[0] == 'self':      
      o = frame.f_locals['self']
      # Pruefe, ob self eine Instanz ist
      if type(o) == types.InstanceType:
        klass = o.__class__
        
  if klass:
    classname = klass.__name__
    packagename = inspect.getmodule(klass).__name__
  else:
    classname = None
    packagename = None
  
  info = inspect.getframeinfo(frame)
  function = info[2]  
  return (classname, function, packagename)
Antworten