Utilisation de la journalisation dans plusieurs modules

257

J'ai un petit projet python qui a la structure suivante -

Project 
 -- pkg01
   -- test01.py
 -- pkg02
   -- test02.py
 -- logging.conf

J'ai l'intention d'utiliser le module de journalisation par défaut pour imprimer des messages sur stdout et un fichier journal. Pour utiliser le module de journalisation, une certaine initialisation est requise -

import logging.config

logging.config.fileConfig('logging.conf')
logger = logging.getLogger('pyApp')

logger.info('testing')

À l'heure actuelle, j'effectue cette initialisation dans chaque module avant de commencer la journalisation des messages. Est-il possible d'effectuer cette initialisation une seule fois au même endroit de sorte que les mêmes paramètres soient réutilisés en se connectant sur tout le projet?

Quest Monger
la source
3
En réponse à votre commentaire sur ma réponse: vous n'avez pas à appeler fileConfigtous les modules qui se connectent, sauf si vous avez de la if __name__ == '__main__'logique dans chacun d'eux. La réponse de prost n'est pas une bonne pratique si le package est une bibliothèque, bien que cela puisse fonctionner pour vous - il ne faut pas configurer la journalisation dans les packages de bibliothèque, sauf pour ajouter un NullHandler.
Vinay Sajip
1
prost impliquait que nous devions appeler les stmts d'importation et de journalisation dans chaque module, et appeler uniquement le stmt fileconfig dans le module principal. n'est-ce pas semblable à ce que vous dites?
Quest Monger
6
prost dit que vous devez mettre le code de configuration de la journalisation package/__init__.py. Ce n'est normalement pas l'endroit où vous mettez du if __name__ == '__main__'code. En outre, l'exemple de prost semble appeler le code de configuration sans condition lors de l'importation, ce qui ne me semble pas correct. En règle générale, la journalisation du code de configuration doit être effectuée au même endroit et ne doit pas se produire comme effet secondaire de l'importation, sauf lorsque vous importez __main__.
Vinay Sajip
vous avez raison, j'ai totalement raté la ligne '# package / __ init__.py' dans son exemple de code. merci de le souligner et de votre patience.
Quest Monger
1
Alors, que se passe-t-il si vous en avez plusieurs if __name__ == '__main__'? (il n'est pas mentionné explicitement en question si tel est le cas)
kon psych

Réponses:

294

La meilleure pratique consiste, dans chaque module, à avoir un enregistreur défini comme ceci:

import logging
logger = logging.getLogger(__name__)

près du haut du module, puis dans un autre code du module, par exemple

logger.debug('My message with %s', 'variable data')

Si vous devez subdiviser l'activité de journalisation à l'intérieur d'un module, utilisez par exemple

loggerA = logging.getLogger(__name__ + '.A')
loggerB = logging.getLogger(__name__ + '.B')

et connectez-vous loggerAet loggerBselon le cas.

Dans votre ou vos programmes principaux, faites par exemple:

def main():
    "your program code"

if __name__ == '__main__':
    import logging.config
    logging.config.fileConfig('/path/to/logging.conf')
    main()

ou

def main():
    import logging.config
    logging.config.fileConfig('/path/to/logging.conf')
    # your program code

if __name__ == '__main__':
    main()

Voir ici pour la journalisation à partir de plusieurs modules, et ici pour la configuration de la journalisation du code qui sera utilisé comme module de bibliothèque par un autre code.

Mise à jour: lors de l'appel fileConfig(), vous souhaiterez peut-être spécifier disable_existing_loggers=Falsesi vous utilisez Python 2.6 ou version ultérieure (consultez la documentation pour plus d'informations). La valeur par défaut est Truepour la compatibilité descendante, ce qui entraîne la désactivation de tous les enregistreurs existants à fileConfig()moins qu'eux-mêmes ou leur ancêtre ne soient explicitement nommés dans la configuration. Avec la valeur définie sur False, les enregistreurs existants sont laissés seuls. Si vous utilisez Python 2.7 / Python 3.2 ou version ultérieure, vous souhaiterez peut-être envisager l' dictConfig()API qui est meilleure que fileConfig()car elle donne plus de contrôle sur la configuration.

Vinay Sajip
la source
21
si vous regardez mon exemple, je fais déjà ce que vous proposez ci-dessus. ma question était de savoir comment centraliser cette initialisation de journalisation de sorte que je n'ai pas à répéter ces 3 instructions. aussi, dans votre exemple, vous avez manqué le stmt 'logging.config.fileConfig (' logging.conf ')'. ce stmt est en fait la cause première de ma préoccupation. vous voyez, si j'ai lancé l'enregistreur dans chaque module, je devrais taper ce stmt dans chaque module. cela signifierait suivre le chemin du fichier conf dans chaque module, ce qui ne me semble pas une meilleure pratique (imaginez les ravages lors du changement d'emplacement de module / package).
Quest Monger
4
Si vous appelez fileConfig après avoir créé l'enregistreur, que ce soit dans le même ou dans un autre module (par exemple lorsque vous créez l'enregistreur en haut du fichier) ne fonctionne pas. La configuration de journalisation s'applique uniquement aux enregistreurs créés après. Cette approche ne fonctionne donc pas ou n'est pas une option viable pour plusieurs modules. @Quest Monger: Vous pouvez toujours créer un autre fichier qui contient l'emplacement du fichier de configuration ..;)
Vincent Ketelaars
2
@Oxidator: Pas nécessairement - voir l' disable_existing_loggersindicateur qui est Truepar défaut mais peut être défini sur False.
Vinay Sajip
1
@Vinay Sajip, merci. Avez-vous des recommandations pour les enregistreurs qui fonctionnent dans des modules mais également en dehors des classes? Étant donné que les importations sont effectuées avant l'appel de la fonction principale, ces journaux auront déjà été enregistrés. Je suppose que la configuration de votre enregistreur avant toutes les importations dans le module principal est le seul moyen? Cet enregistreur pourrait alors être écrasé en principal, si vous le souhaitez.
Vincent Ketelaars
1
Si je veux que tous les enregistreurs spécifiques à mon module aient un niveau d'enregistrement différent de l'AVERTISSEMENT par défaut, dois-je effectuer ce réglage sur chaque module? Dis, je veux que tous mes modules se connectent à INFO.
Raj
128

En fait, chaque enregistreur est un enfant de l'enregistreur de packages du parent (c'est-à-dire qu'il package.subpackage.modulehérite de la configuration package.subpackage), donc tout ce que vous devez faire est simplement de configurer l'enregistreur racine. Ceci peut être réalisé par logging.config.fileConfig(votre propre configuration pour les enregistreurs) ou logging.basicConfig(définit l'enregistreur racine) . Configuration de la journalisation dans votre module d'entrée ( __main__.pyou tout ce que vous voulez exécuter, par exemple main_script.py. __init__.pyFonctionne également)

en utilisant basicConfig:

# package/__main__.py
import logging
import sys

logging.basicConfig(stream=sys.stdout, level=logging.INFO)

en utilisant fileConfig:

# package/__main__.py
import logging
import logging.config

logging.config.fileConfig('logging.conf')

puis créez chaque enregistreur en utilisant:

# package/submodule.py
# or
# package/subpackage/submodule.py
import logging
log = logging.getLogger(__name__)

log.info("Hello logging!")

Pour plus d'informations, voir Tutoriel de journalisation avancée .

Stan Prokop
la source
15
c'est, de loin, la solution la plus simple au problème, sans parler qu'elle expose et exploite la relation parent-enfant entre les modules, quelque chose que i en tant que noob ignorait. danke.
Quest Monger
vous avez raison. et comme vinay l'a souligné dans son article, votre solution est bonne tant qu'elle n'est pas dans le module init .py. votre solution a fonctionné lorsque je l'ai appliquée au module principal (point d'entrée).
Quest Monger
2
réponse beaucoup plus pertinente puisque la question concerne des modules séparés.
Jan Sila
1
Question stupide peut-être: s'il n'y a pas d'enregistreur __main__.py(par exemple, si je veux utiliser le module dans un script qui n'a pas d'enregistreur), fera-t- logging.getLogger(__name__)il quand même une sorte d'enregistrement dans le module ou déclenchera-t-il une exception?
Bill
1
Finalement. J'avais un enregistreur qui fonctionnait, mais il a échoué dans Windows pour les exécutions parallèles avec joblib. Je suppose que c'est un ajustement manuel du système - quelque chose d'autre ne va pas avec Parallel. Mais ça marche sûrement! Merci
B Furtado
17

Je le fais toujours comme ci-dessous.

Utilisez un seul fichier python pour configurer mon journal en tant que modèle singleton qui a nommé ' log_conf.py'

#-*-coding:utf-8-*-

import logging.config

def singleton(cls):
    instances = {}
    def get_instance():
        if cls not in instances:
            instances[cls] = cls()
        return instances[cls]
    return get_instance()

@singleton
class Logger():
    def __init__(self):
        logging.config.fileConfig('logging.conf')
        self.logr = logging.getLogger('root')

Dans un autre module, importez simplement la configuration.

from log_conf import Logger

Logger.logr.info("Hello World")

Il s'agit d'un modèle singleton à enregistrer, simplement et efficacement.

Yarkee
la source
1
merci d'avoir détaillé le motif singleton. je comptais l'implémenter, mais la solution @prost est beaucoup plus simple et convient parfaitement à mes besoins. Je vois cependant que votre solution est utile pour les projets plus importants qui ont plusieurs points d'entrée (autres que principaux). danke.
Quest Monger
46
C'est inutile. L'enregistreur racine est déjà un singleton. Utilisez simplement logging.info au lieu de Logger.logr.info.
Pod
9

Plusieurs de ces réponses suggèrent qu'en haut d'un module, vous

import logging
logger = logging.getLogger(__name__)

Je crois comprendre que cela est considéré comme une très mauvaise pratique . La raison en est que la configuration du fichier désactivera tous les enregistreurs existants par défaut. Par exemple

#my_module
import logging

logger = logging.getLogger(__name__)

def foo():
    logger.info('Hi, foo')

class Bar(object):
    def bar(self):
        logger.info('Hi, bar')

Et dans votre module principal:

#main
import logging

# load my module - this now configures the logger
import my_module

# This will now disable the logger in my module by default, [see the docs][1] 
logging.config.fileConfig('logging.ini')

my_module.foo()
bar = my_module.Bar()
bar.bar()

Maintenant, le journal spécifié dans logging.ini sera vide, car l'enregistreur existant a été désactivé par l'appel fileconfig.

Bien qu'il soit certainement possible de contourner ce problème (disable_existing_Loggers = False), de manière réaliste, de nombreux clients de votre bibliothèque ne seront pas au courant de ce comportement et ne recevront pas vos journaux. Facilitez la tâche de vos clients en appelant toujours logging.getLogger localement. Astuce: j'ai appris ce comportement sur le site Web de Victor Lin .

La bonne pratique consiste donc à toujours appeler localement logging.getLogger. Par exemple

#my_module
import logging

logger = logging.getLogger(__name__)

def foo():
    logging.getLogger(__name__).info('Hi, foo')

class Bar(object):
    def bar(self):
        logging.getLogger(__name__).info('Hi, bar')    

De plus, si vous utilisez fileconfig dans votre main, définissez disable_existing_loggers = False, juste au cas où vos concepteurs de bibliothèque utiliseraient des instances de journalisation au niveau du module.

phil_20686
la source
Tu ne peux pas courir logging.config.fileConfig('logging.ini')avant import my_module? Comme suggéré dans cette réponse .
lucid_dreamer
Pas sûr - mais il serait certainement également considéré comme une mauvaise pratique de mélanger les importations et le code exécutable de cette manière. Vous ne voulez pas non plus que vos clients aient à vérifier s'ils doivent configurer la journalisation avant d'importer, surtout lorsqu'il existe une alternative triviale! Imaginez si une bibliothèque largement utilisée comme les requêtes avait fait ça ....!
phil_20686
"Je ne sais pas - mais il serait certainement également considéré comme une mauvaise pratique de mélanger les importations et le code exécutable de cette manière." - Pourquoi?
lucid_dreamer
Je ne sais pas trop pourquoi c'est mauvais. Et je ne comprends pas bien votre exemple. Pouvez-vous publier votre configuration pour cet exemple et montrer une certaine utilisation?
lucid_dreamer
1
Vous semblez contredire les documents officiels : `` Une bonne convention à utiliser pour nommer les enregistreurs est d'utiliser un enregistreur au niveau du module, dans chaque module qui utilise la journalisation, nommé comme suit: logger = logging.getLogger(__name__)''
iron9
9

Une façon simple d'utiliser une instance de bibliothèque de journalisation dans plusieurs modules pour moi était la solution suivante:

base_logger.py

import logging

logger = logging
logger.basicConfig(format='%(asctime)s - %(message)s', level=logging.INFO)

Autres fichiers

from base_logger import logger

if __name__ == '__main__':
    logger.info("This is an info message")
Alex Jolig
la source
7

Jeter une autre solution.

Dans l' initialisation de mon module, j'ai quelque chose comme:

# mymodule/__init__.py
import logging

def get_module_logger(mod_name):
  logger = logging.getLogger(mod_name)
  handler = logging.StreamHandler()
  formatter = logging.Formatter(
        '%(asctime)s %(name)-12s %(levelname)-8s %(message)s')
  handler.setFormatter(formatter)
  logger.addHandler(handler)
  logger.setLevel(logging.DEBUG)
  return logger

Ensuite, dans chaque module, j'ai besoin d'un enregistreur, je fais:

# mymodule/foo.py
from [modname] import get_module_logger
logger = get_module_logger(__name__)

Lorsque les journaux sont manqués, vous pouvez différencier leur source par le module dont ils sont issus.

Tommy
la source
Que signifie "l'init principal de mon module"? Et "Ensuite, dans chaque classe, j'ai besoin d'un enregistreur, je fais:"? Pouvez-vous fournir un exemple appelé_module.py et un exemple de son utilisation en tant qu'importation à partir du module caller_module.py? Voir cette réponse pour une inspiration du format que je demande. N'essayant pas d'être condescendant. J'essaie de comprendre votre réponse et je sais que je le ferais si vous l'écriviez de cette façon.
lucid_dreamer
1
@lucid_dreamer j'ai clarifié.
Tommy
4

Vous pouvez également proposer quelque chose comme ça!

def get_logger(name=None):
    default = "__app__"
    formatter = logging.Formatter('%(levelname)s: %(asctime)s %(funcName)s(%(lineno)d) -- %(message)s',
                              datefmt='%Y-%m-%d %H:%M:%S')
    log_map = {"__app__": "app.log", "__basic_log__": "file1.log", "__advance_log__": "file2.log"}
    if name:
        logger = logging.getLogger(name)
    else:
        logger = logging.getLogger(default)
    fh = logging.FileHandler(log_map[name])
    fh.setFormatter(formatter)
    logger.addHandler(fh)
    logger.setLevel(logging.DEBUG)
    return logger

Vous pouvez maintenant utiliser plusieurs enregistreurs dans le même module et sur l'ensemble du projet si ce qui précède est défini dans un module distinct et importé dans d'autres modules si la journalisation est requise.

a=get_logger("__app___")
b=get_logger("__basic_log__")
a.info("Starting logging!")
b.debug("Debug Mode")
deeshank
la source
4

@ La solution de Yarkee semblait meilleure. Je voudrais y ajouter encore plus -

class Singleton(type):
    _instances = {}

    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances.keys():
            cls._instances[cls] = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class LoggerManager(object):
    __metaclass__ = Singleton

    _loggers = {}

    def __init__(self, *args, **kwargs):
        pass

    @staticmethod
    def getLogger(name=None):
        if not name:
            logging.basicConfig()
            return logging.getLogger()
        elif name not in LoggerManager._loggers.keys():
            logging.basicConfig()
            LoggerManager._loggers[name] = logging.getLogger(str(name))
        return LoggerManager._loggers[name]    


log=LoggerManager().getLogger("Hello")
log.setLevel(level=logging.DEBUG)

LoggerManager peut donc être enfichable à l'ensemble de l'application. J'espère que cela a du sens et de la valeur.

deeshank
la source
11
Le module de journalisation traite déjà des singletons. logging.getLogger ("Bonjour") obtiendra le même enregistreur sur tous vos modules.
Pod
2

Il y a plusieurs réponses. je me suis retrouvé avec une solution similaire mais différente qui a du sens pour moi, peut-être qu'elle vous plaira aussi. Mon objectif principal était de pouvoir transmettre des logs aux gestionnaires par leur niveau (debug level logs à la console, avertissements et surtout aux fichiers):

from flask import Flask
import logging
from logging.handlers import RotatingFileHandler

app = Flask(__name__)

# make default logger output everything to the console
logging.basicConfig(level=logging.DEBUG)

rotating_file_handler = RotatingFileHandler(filename="logs.log")
rotating_file_handler.setLevel(logging.INFO)

app.logger.addHandler(rotating_file_handler)

créé un joli fichier util nommé logger.py:

import logging

def get_logger(name):
    return logging.getLogger("flask.app." + name)

flask.app est une valeur codée en dur dans flask. l'enregistreur d'applications commence toujours par flask.app comme nom du module.

maintenant, dans chaque module, je peux l'utiliser dans le mode suivant:

from logger import get_logger
logger = get_logger(__name__)

logger.info("new log")

Cela va créer un nouveau journal pour "app.flask.MODULE_NAME" avec un minimum d'effort.

Ben Yitzhaki
la source
2

La meilleure pratique serait de créer un module séparément qui n'a qu'une seule méthode dont la tâche consiste à donner un gestionnaire d'enregistreur à la méthode appelante. Enregistrez ce fichier sous m_logger.py

import logger, logging

def getlogger():
    # logger
    logger = logging.getLogger(__name__)
    logger.setLevel(logging.DEBUG)
    # create console handler and set level to debug
    #ch = logging.StreamHandler()
    ch = logging.FileHandler(r'log.txt')
    ch.setLevel(logging.DEBUG)
    # create formatter
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    # add formatter to ch
    ch.setFormatter(formatter)
    # add ch to logger
    logger.addHandler(ch)
    return logger

Appelez maintenant la méthode getlogger () chaque fois que le gestionnaire de l'enregistreur est nécessaire.

from m_logger import getlogger
logger = getlogger()
logger.info('My mssg')
Mousam Singh
la source
1
C'est bien si vous n'avez pas de paramètres supplémentaires. Mais si, disons, vous avez l' --debugoption dans l'application et que vous souhaitez définir le niveau de journalisation dans tous les enregistreurs de votre application en fonction de ce paramètre ...
The Godfather
@TheGodfather Oui, cette méthode est difficile à réaliser. Ce que nous pouvons faire dans cette situation est de créer une classe pour laquelle le formateur prendrait le paramètre au moment de la création de l'objet et aurait la fonction similaire pour renvoyer le gestionnaire de l'enregistreur. Que pensez-vous de cela?
Mousam Singh
Oui, j'ai fait une chose similaire, faite get_logger(level=logging.INFO)pour retourner une sorte de singleton, donc quand il a appelé pour la première fois depuis l'application principale, il initialise l'enregistreur et les gestionnaires avec le niveau approprié, puis renvoie le même loggerobjet à toutes les autres méthodes.
The Godfather
0

Nouveau sur python, donc je ne sais pas si cela est conseillé, mais cela fonctionne très bien pour ne pas réécrire le passe-partout.

Votre projet doit avoir un init .py pour pouvoir être chargé en tant que module

# Put this in your module's __init__.py
import logging.config
import sys

# I used this dictionary test, you would put:
# logging.config.fileConfig('logging.conf')
# The "" entry in loggers is the root logger, tutorials always 
# use "root" but I can't get that to work
logging.config.dictConfig({
    "version": 1,
    "formatters": {
        "default": {
            "format": "%(asctime)s %(levelname)s %(name)s %(message)s"
        },
    },
    "handlers": {
        "console": {
            "level": 'DEBUG',
            "class": "logging.StreamHandler",
            "stream": "ext://sys.stdout"
        }
    },
    "loggers": {
        "": {
            "level": "DEBUG",
            "handlers": ["console"]
        }
    }
})

def logger():
    # Get the name from the caller of this function
    return logging.getLogger(sys._getframe(1).f_globals['__name__'])

sys._getframe(1)la suggestion vient d' ici

Ensuite, pour utiliser votre enregistreur dans tout autre fichier:

from [your module name here] import logger

logger().debug("FOOOOOOOOO!!!")

Avertissements:

  1. Vous devez exécuter vos fichiers en tant que modules, sinon import [your module]cela ne fonctionnera pas:
    • python -m [your module name].[your filename without .py]
  2. Le nom de l'enregistreur pour le point d'entrée de votre programme sera __main__, mais toute solution utilisant __name__aura ce problème.
npjohns
la source