Cara menambahkan loglevel kustom ke fasilitas logging Python


116

Saya ingin memiliki loglevel TRACE (5) untuk aplikasi saya, karena menurut saya itu tidak debug()cukup. Selain log(5, msg)itu bukan yang saya inginkan. Bagaimana cara menambahkan loglevel khusus ke logger Python?

Saya punya mylogger.pydengan konten berikut:

import logging

@property
def log(obj):
    myLogger = logging.getLogger(obj.__class__.__name__)
    return myLogger

Dalam kode saya, saya menggunakannya dengan cara berikut:

class ExampleClass(object):
    from mylogger import log

    def __init__(self):
        '''The constructor with the logger'''
        self.log.debug("Init runs")

Sekarang saya ingin menelepon self.log.trace("foo bar")

Terima kasih sebelumnya atas bantuan Anda.

Sunting (8 Des 2016): Saya mengubah jawaban yang diterima menjadi pfa yang, IMHO, solusi luar biasa berdasarkan proposal yang sangat bagus dari Eric S.

Jawaban:


171

@Bayu_joo

Jawaban Eric S. sangat bagus, tetapi saya belajar melalui eksperimen bahwa ini akan selalu menyebabkan pesan yang dicatat di tingkat debug baru untuk dicetak - terlepas dari apa tingkat log yang disetel. Jadi jika Anda membuat nomor tingkat baru 9, jika Anda menelepon setLevel(50), pesan tingkat yang lebih rendah akan dicetak secara keliru.

Untuk mencegah hal itu terjadi, Anda memerlukan baris lain di dalam fungsi "debugv" untuk memeriksa apakah tingkat logging yang dimaksud benar-benar diaktifkan.

Memperbaiki contoh yang memeriksa apakah level logging diaktifkan:

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    if self.isEnabledFor(DEBUG_LEVELV_NUM):
        # Yes, logger takes its '*args' as 'args'.
        self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

Jika Anda melihat kode untuk class Loggerdi logging.__init__.pyPython 2.7, inilah yang dilakukan semua fungsi log standar (.critical, .debug, dll.).

Sepertinya saya tidak dapat memposting balasan ke jawaban orang lain karena kurangnya reputasi ... semoga Eric akan memperbarui postingannya jika dia melihat ini. =)


7
Ini adalah jawaban yang lebih baik karena memeriksa level log dengan benar.
Kolonel Panic

2
Tentunya jauh lebih informatif daripada jawaban saat ini.
Fisikawan Gila

4
@pfa Bagaimana dengan menambahkan logging.DEBUG_LEVEL_NUM = 9sehingga Anda dapat mengakses tingkat debug di mana pun Anda mengimpor logger dalam kode Anda?
edgarstack

4
Tentu saja DEBUG_LEVEL_NUM = 9Anda harus mendefinisikan logging.DEBUG_LEVEL_NUM = 9. Dengan cara ini Anda akan dapat menggunakan log_instance.setLevel(logging.DEBUG_LEVEL_NUM)cara yang sama seperti Anda menggunakan pengetahuan yang benar logging.DEBUGataulogging.INFO
maQ

Jawaban ini sangat membantu. Terima kasih pfa dan EricS. Saya ingin menyarankan bahwa untuk kelengkapan dua pernyataan lagi disertakan: logging.DEBUGV = DEBUG_LEVELV_NUMdan logging.__all__ += ['DEBUGV'] Yang kedua tidak terlalu penting tetapi yang pertama diperlukan jika Anda memiliki kode yang secara dinamis menyesuaikan tingkat pencatatan dan Anda ingin dapat melakukan sesuatu seperti if verbose: logger.setLevel(logging.DEBUGV)`
Keith Hanlan

63

Saya mengambil jawaban "hindari melihat lambda" dan harus memodifikasi di mana log_at_my_log_level ditambahkan. Saya juga melihat masalah yang dilakukan Paul, "Menurut saya ini tidak berhasil. Apakah Anda tidak perlu logger sebagai argumen pertama di log_at_my_log_level?" Ini berhasil untuk saya

import logging
DEBUG_LEVELV_NUM = 9 
logging.addLevelName(DEBUG_LEVELV_NUM, "DEBUGV")
def debugv(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(DEBUG_LEVELV_NUM, message, args, **kws) 
logging.Logger.debugv = debugv

7
+1 juga. Pendekatan yang elegan, dan berhasil dengan sempurna. Catatan penting: Anda hanya perlu melakukan ini sekali, dalam satu modul, dan ini akan berfungsi untuk semua modul . Anda bahkan tidak perlu mengimpor modul "penyiapan". Jadi, lemparkan ini ke dalam satu paket __init__.pydan berbahagialah: D
MestreLion

4
@Eric S. Anda harus melihat jawaban ini: stackoverflow.com/a/13638084/600110
Sam Mussmann

1
Saya setuju dengan @SamMussmann. Saya melewatkan jawaban itu karena ini adalah jawaban pilihan teratas.
Kolonel Panic

@ Eric S. Mengapa Anda perlu args tanpa *? Jika saya melakukan itu, saya mengerti TypeError: not all arguments converted during string formattingtetapi berfungsi dengan baik dengan *. (Python 3.4.3). Apakah ini masalah versi python, atau sesuatu yang saya lewatkan?
Peter

Jawaban ini tidak berhasil untuk saya. Mencoba melakukan 'logging.debugv' memberikan kesalahanAttributeError: module 'logging' has no attribute 'debugv'
Alex

51

Menggabungkan semua jawaban yang ada dengan sekumpulan pengalaman penggunaan, saya pikir saya telah membuat daftar semua hal yang perlu dilakukan untuk memastikan penggunaan level baru yang sepenuhnya mulus. Langkah-langkah di bawah ini mengasumsikan bahwa Anda menambahkan level baru TRACEdengan nilai logging.DEBUG - 5 == 5:

  1. logging.addLevelName(logging.DEBUG - 5, 'TRACE') perlu dipanggil untuk mendapatkan level baru yang terdaftar secara internal sehingga bisa direferensikan dengan nama.
  2. Tingkat baru perlu ditambahkan sebagai atribut untuk loggingdirinya sendiri untuk konsistensi: logging.TRACE = logging.DEBUG - 5.
  3. Metode yang dipanggil traceperlu ditambahkan ke loggingmodul. Ini harus bersikap seperti debug, info, dll
  4. Metode yang dipanggil traceperlu ditambahkan ke kelas logger yang saat ini dikonfigurasi. Karena ini tidak dijamin 100% logging.Logger, gunakan logging.getLoggerClass()saja.

Semua langkah diilustrasikan dalam metode di bawah ini:

def addLoggingLevel(levelName, levelNum, methodName=None):
    """
    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):
       raise AttributeError('{} already defined in logging module'.format(levelName))
    if hasattr(logging, methodName):
       raise AttributeError('{} already defined in logging module'.format(methodName))
    if hasattr(logging.getLoggerClass(), methodName):
       raise AttributeError('{} already defined in logger class'.format(methodName))

    # 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)

Urutkan jawaban berdasarkan Oldest, dan Anda akan menghargai bahwa ini adalah jawaban terbaik dari semuanya!
Serge Stroobandt

Terima kasih. Saya telah melakukan cukup banyak pekerjaan untuk membuat sesuatu seperti ini bersama-sama dan QA ini sangat membantu, jadi saya mencoba menambahkan sesuatu.
Fisikawan Gila

1
@Tokopedia. Beri tahu saya jika Anda mengalami masalah dengan ini. Di kotak peralatan pribadi saya, saya memiliki versi tambahan yang memungkinkan Anda mengkonfigurasi cara menangani definisi level yang bertentangan. Itu muncul sekali untuk saya karena saya suka menambahkan level TRACE, dan begitu juga salah satu komponen sphinx.
Fisikawan Gila

1
Apakah kurangnya tanda bintang di depan argsdalam logForLevelimplementasi disengaja / dibutuhkan?
Chris L. Barnes

1
@Tisia. Itu tidak disengaja. Terima kasih untuk tangkapannya.
Mad Physicist

40

Pertanyaan ini agak lama, tetapi saya hanya membahas topik yang sama dan menemukan cara yang mirip dengan yang telah disebutkan yang tampak sedikit lebih bersih bagi saya. Ini telah diuji pada 3.4, jadi saya tidak yakin apakah metode yang digunakan ada di versi yang lebih lama:

from logging import getLoggerClass, addLevelName, setLoggerClass, NOTSET

VERBOSE = 5

class MyLogger(getLoggerClass()):
    def __init__(self, name, level=NOTSET):
        super().__init__(name, level)

        addLevelName(VERBOSE, "VERBOSE")

    def verbose(self, msg, *args, **kwargs):
        if self.isEnabledFor(VERBOSE):
            self._log(VERBOSE, msg, args, **kwargs)

setLoggerClass(MyLogger)

1
Ini IMHO jawaban terbaik, karena menghindari penambalan monyet. Apa getdan setLoggerClasstepatnya lakukan dan mengapa mereka dibutuhkan?
Marco Sulla

3
@MarcoSulla Mereka didokumentasikan sebagai bagian dari modul logging Python. Sub-classing dinamis, saya asumsikan, digunakan jika seseorang menginginkan llogger mereka sendiri saat menggunakan library ini. MyLogger ini kemudian akan menjadi sub-kelas dari kelas saya, menggabungkan keduanya.
CrackerJack9

Ini sangat mirip dengan solusi yang disajikan dalam diskusi ini, apakah menambahkan TRACElevel ke pustaka logging default. +1
IMP1

18

Siapa yang memulai praktik buruk menggunakan metode internal ( self._log) dan mengapa setiap jawaban didasarkan pada itu ?! Solusi pythonic akan digunakan self.logsehingga Anda tidak perlu mengacaukan hal-hal internal apa pun:

import logging

SUBDEBUG = 5
logging.addLevelName(SUBDEBUG, 'SUBDEBUG')

def subdebug(self, message, *args, **kws):
    self.log(SUBDEBUG, message, *args, **kws) 
logging.Logger.subdebug = subdebug

logging.basicConfig()
l = logging.getLogger()
l.setLevel(SUBDEBUG)
l.subdebug('test')
l.setLevel(logging.DEBUG)
l.subdebug('test')

18
Penggunaan _log () sebagai ganti log () diperlukan untuk menghindari pengenalan level tambahan dalam tumpukan panggilan. Jika log () digunakan, pengenalan bingkai tumpukan tambahan menyebabkan beberapa atribut LogRecord (funcName, lineno, nama file, nama jalur, ...) mengarah ke fungsi debug daripada pemanggil yang sebenarnya. Ini sepertinya bukan hasil yang diinginkan.
rivy

5
Sejak kapan pemanggilan metode internal kelas sendiri tidak diizinkan? Hanya karena fungsi didefinisikan di luar kelas tidak berarti bahwa itu adalah metode eksternal.
OozeMeister

3
Metode ini tidak hanya mengubah pelacakan tumpukan secara tidak perlu, tetapi juga tidak memeriksa apakah tingkat yang benar sedang dicatat.
Fisikawan Gila

Saya merasa, apa yang dikatakan @schlamar benar, tetapi alasan tandingan mendapat jumlah suara yang sama. Jadi apa yang harus digunakan?
Sumit Murari

1
Mengapa suatu metode tidak menggunakan metode internal?
Gringo Suave

9

Saya merasa lebih mudah untuk membuat atribut baru untuk objek logger yang melewati fungsi log (). Saya pikir modul logger menyediakan addLevelName () dan log () karena alasan ini. Jadi tidak diperlukan subclass atau metode baru.

import logging

@property
def log(obj):
    logging.addLevelName(5, 'TRACE')
    myLogger = logging.getLogger(obj.__class__.__name__)
    setattr(myLogger, 'trace', lambda *args: myLogger.log(5, *args))
    return myLogger

sekarang

mylogger.trace('This is a trace message')

harus bekerja seperti yang diharapkan.


Bukankah ini memiliki kinerja yang kecil dibandingkan subclassing? Dengan pendekatan ini, setiap kali beberapa meminta logger, mereka harus melakukan panggilan setattr. Anda mungkin akan membungkus ini bersama-sama dalam kelas khusus tetapi meskipun demikian, setattr itu harus dipanggil pada setiap logger yang dibuat, bukan?
Matthew Lund

@Zbigniew di bawah menunjukkan ini tidak berfungsi, yang menurut saya karena logger Anda perlu melakukan panggilan ke _log, bukan log.
Marqueed

9

Meskipun kami sudah memiliki banyak jawaban yang benar, berikut ini menurut saya lebih pythonic:

import logging

from functools import partial, partialmethod

logging.TRACE = 5
logging.addLevelName(logging.TRACE, 'TRACE')
logging.Logger.trace = partialmethod(logging.Logger.log, logging.TRACE)
logging.trace = partial(logging.log, logging.TRACE)

Jika Anda ingin menggunakan mypykode Anda, disarankan untuk menambahkan # type: ignoreuntuk menyembunyikan peringatan dari menambahkan atribut.


1
Ini terlihat bagus, tetapi baris terakhir membingungkan. Bukankah seharusnya begitu logging.trace = partial(logging.log, logging.TRACE) # type: ignore?
Sergey Nudnov

@SergeyNudnov terima kasih telah menunjukkan, saya memperbaikinya. Adalah kesalahan dari pihak saya, saya hanya menyalin dari kode saya dan tampaknya mengacaukan pembersihan.
DerWeh

8

Saya pikir Anda harus membuat subclass Loggerkelas dan menambahkan metode traceyang pada dasarnya memanggil Logger.logdengan level yang lebih rendah dari DEBUG. Saya belum mencoba ini tetapi ini yang ditunjukkan oleh dokumen .


3
Dan Anda mungkin ingin mengganti logging.getLoggeruntuk mengembalikan subclass Anda, bukan kelas bawaan.
S. Lotot

4
@ S.Lott - Sebenarnya (setidaknya dengan versi Python saat ini, mungkin itu tidak terjadi di tahun 2010) Anda harus menggunakan setLoggerClass(MyClass)dan kemudian menelepon getLogger()seperti biasa ...
mac

IMO, sejauh ini ini adalah jawaban terbaik (dan paling Pythonic), dan jika saya bisa memberikannya beberapa +1, saya akan melakukannya. Ini mudah untuk dieksekusi, namun kode contoh akan menyenangkan. :-D
Doug R.22

@ DougR. Terima kasih tetapi seperti yang saya katakan, saya belum mencobanya. :)
Noufal Ibrahim

6

Kiat untuk membuat logger ubahsuaian:

  1. Jangan gunakan _log, gunakan log(Anda tidak perlu memeriksa isEnabledFor)
  2. modul logging harus menjadi salah satu instance pembuatan logger kustom karena ia melakukan beberapa keajaiban getLogger, jadi Anda perlu mengatur kelas melaluisetLoggerClass
  3. Anda tidak perlu mendefinisikan __init__untuk logger, kelas jika Anda tidak menyimpan apapun
# Lower than debug which is 10
TRACE = 5
class MyLogger(logging.Logger):
    def trace(self, msg, *args, **kwargs):
        self.log(TRACE, msg, *args, **kwargs)

Saat memanggil logger ini gunakan setLoggerClass(MyLogger)untuk menjadikan ini logger default darigetLogger

logging.setLoggerClass(MyLogger)
log = logging.getLogger(__name__)
# ...
log.trace("something specific")

Anda akan perlu setFormatter, setHandlerdan setLevel(TRACE)pada handlerdan pada logdirinya sendiri untuk benar-benar se jejak ini tingkat rendah


3

Ini berhasil untuk saya:

import logging
logging.basicConfig(
    format='  %(levelname)-8.8s %(funcName)s: %(message)s',
)
logging.NOTE = 32  # positive yet important
logging.addLevelName(logging.NOTE, 'NOTE')      # new level
logging.addLevelName(logging.CRITICAL, 'FATAL') # rename existing

log = logging.getLogger(__name__)
log.note = lambda msg, *args: log._log(logging.NOTE, msg, args)
log.note('school\'s out for summer! %s', 'dude')
log.fatal('file not found.')

Masalah lambda / funcName telah diperbaiki dengan logger._log seperti yang ditunjukkan @marqueed. Saya pikir menggunakan lambda terlihat sedikit lebih bersih, tetapi kekurangannya adalah tidak dapat mengambil argumen kata kunci. Aku sendiri belum pernah menggunakannya, jadi bukan masalah besar.

  CATATAN pengaturan: sekolah keluar untuk musim panas! bung
  Setup FATAL: file tidak ditemukan.

2

Dalam pengalaman saya, ini adalah solusi lengkap dari masalah op ... untuk menghindari melihat "lambda" sebagai fungsi di mana pesan tersebut dipancarkan, masuk lebih dalam:

MY_LEVEL_NUM = 25
logging.addLevelName(MY_LEVEL_NUM, "MY_LEVEL_NAME")
def log_at_my_log_level(self, message, *args, **kws):
    # Yes, logger takes its '*args' as 'args'.
    self._log(MY_LEVEL_NUM, message, args, **kws)
logger.log_at_my_log_level = log_at_my_log_level

Saya belum pernah mencoba bekerja dengan kelas logger mandiri, tapi menurut saya ide dasarnya sama (gunakan _log).


Saya tidak berpikir ini berhasil. Apakah Anda tidak perlu loggersebagai argumen pertama log_at_my_log_level?
Paul

Ya, saya pikir Anda mungkin akan melakukannya. Jawaban ini diadaptasi dari kode yang memecahkan masalah yang sedikit berbeda.
Marqueed

2

Penambahan contoh Mad Physicists untuk mendapatkan nama file dan nomor baris yang benar:

def logToRoot(message, *args, **kwargs):
    if logging.root.isEnabledFor(levelNum):
        logging.root._log(levelNum, message, args, **kwargs)

1

Berdasarkan jawaban yang disematkan, saya menulis sedikit metode yang secara otomatis membuat tingkat logging baru

def set_custom_logging_levels(config={}):
    """
        Assign custom levels for logging
            config: is a dict, like
            {
                'EVENT_NAME': EVENT_LEVEL_NUM,
            }
        EVENT_LEVEL_NUM can't be like already has logging module
        logging.DEBUG       = 10
        logging.INFO        = 20
        logging.WARNING     = 30
        logging.ERROR       = 40
        logging.CRITICAL    = 50
    """
    assert isinstance(config, dict), "Configuration must be a dict"

    def get_level_func(level_name, level_num):
        def _blank(self, message, *args, **kws):
            if self.isEnabledFor(level_num):
                # Yes, logger takes its '*args' as 'args'.
                self._log(level_num, message, args, **kws) 
        _blank.__name__ = level_name.lower()
        return _blank

    for level_name, level_num in config.items():
        logging.addLevelName(level_num, level_name.upper())
        setattr(logging.Logger, level_name.lower(), get_level_func(level_name, level_num))

config mungkin seperti itu:

new_log_levels = {
    # level_num is in logging.INFO section, that's why it 21, 22, etc..
    "FOO":      21,
    "BAR":      22,
}

0

Sebagai alternatif untuk menambahkan metode tambahan ke kelas Logger, saya akan merekomendasikan menggunakan Logger.log(level, msg)metode tersebut.

import logging

TRACE = 5
logging.addLevelName(TRACE, 'TRACE')
FORMAT = '%(levelname)s:%(name)s:%(lineno)d:%(message)s'


logging.basicConfig(format=FORMAT)
l = logging.getLogger()
l.setLevel(TRACE)
l.log(TRACE, 'trace message')
l.setLevel(logging.DEBUG)
l.log(TRACE, 'disabled trace message')

0

Saya bingung; dengan python 3.5, setidaknya, ini berfungsi:

import logging


TRACE = 5
"""more detail than debug"""

logging.basicConfig()
logging.addLevelName(TRACE,"TRACE")
logger = logging.getLogger('')
logger.debug("n")
logger.setLevel(logging.DEBUG)
logger.debug("y1")
logger.log(TRACE,"n")
logger.setLevel(TRACE)
logger.log(TRACE,"y2")
    

keluaran:

DEBUG: root: y1

JEJAK: root: y2


1
Ini tidak memungkinkan Anda melakukan logger.trace('hi')yang saya yakini sebagai tujuan utama
Ultimasi

-3

Jika ada yang menginginkan cara otomatis untuk menambahkan level logging baru ke modul logging (atau salinannya) secara dinamis, saya telah membuat fungsi ini, memperluas jawaban @ pfa:

def add_level(log_name,custom_log_module=None,log_num=None,
                log_call=None,
                   lower_than=None, higher_than=None, same_as=None,
              verbose=True):
    '''
    Function to dynamically add a new log level to a given custom logging module.
    <custom_log_module>: the logging module. If not provided, then a copy of
        <logging> module is used
    <log_name>: the logging level name
    <log_num>: the logging level num. If not provided, then function checks
        <lower_than>,<higher_than> and <same_as>, at the order mentioned.
        One of those three parameters must hold a string of an already existent
        logging level name.
    In case a level is overwritten and <verbose> is True, then a message in WARNING
        level of the custom logging module is established.
    '''
    if custom_log_module is None:
        import imp
        custom_log_module = imp.load_module('custom_log_module',
                                            *imp.find_module('logging'))
    log_name = log_name.upper()
    def cust_log(par, message, *args, **kws):
        # Yes, logger takes its '*args' as 'args'.
        if par.isEnabledFor(log_num):
            par._log(log_num, message, args, **kws)
    available_level_nums = [key for key in custom_log_module._levelNames
                            if isinstance(key,int)]

    available_levels = {key:custom_log_module._levelNames[key]
                             for key in custom_log_module._levelNames
                            if isinstance(key,str)}
    if log_num is None:
        try:
            if lower_than is not None:
                log_num = available_levels[lower_than]-1
            elif higher_than is not None:
                log_num = available_levels[higher_than]+1
            elif same_as is not None:
                log_num = available_levels[higher_than]
            else:
                raise Exception('Infomation about the '+
                                'log_num should be provided')
        except KeyError:
            raise Exception('Non existent logging level name')
    if log_num in available_level_nums and verbose:
        custom_log_module.warn('Changing ' +
                                  custom_log_module._levelNames[log_num] +
                                  ' to '+log_name)
    custom_log_module.addLevelName(log_num, log_name)

    if log_call is None:
        log_call = log_name.lower()

    setattr(custom_log_module.Logger, log_call, cust_log)
    return custom_log_module

1
Evaluasi di dalam exec. Wow.
Fisikawan Gila

2
..... tidak tahu apa yang membuat saya melakukan ini .... setelah berbulan-bulan saya akan dengan senang hati mengganti pernyataan ini dengan setattr...
Vasilis Lemonidis
Dengan menggunakan situs kami, Anda mengakui telah membaca dan memahami Kebijakan Cookie dan Kebijakan Privasi kami.
Licensed under cc by-sa 3.0 with attribution required.