Comment enregistrer une erreur Python avec des informations de débogage?

470

J'imprime des messages d'exception Python dans un fichier journal avec logging.error:

import logging
try:
    1/0
except ZeroDivisionError as e:
    logging.error(e)  # ERROR:root:division by zero

Est-il possible d'imprimer des informations plus détaillées sur l'exception et le code qui l'a généré que juste la chaîne d'exception? Des choses comme les numéros de ligne ou les traces de pile seraient formidables.

probablement à la plage
la source

Réponses:

735

logger.exception affichera une trace de pile à côté du message d'erreur.

Par exemple:

import logging
try:
    1/0
except ZeroDivisionError as e:
    logging.exception("message")

Production:

ERROR:root:message
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero

@Paulo Check notes, "sachez que dans Python 3, vous devez appeler la logging.exceptionméthode juste à l'intérieur de la exceptpièce. Si vous appelez cette méthode dans un endroit arbitraire, vous pouvez obtenir une exception bizarre. Les documents alertent à ce sujet."

SiggyF
la source
131
La exceptionméthode appelle simplement error(message, exc_info=1). Dès que vous passez exc_infoà l'une des méthodes de journalisation à partir d'un contexte d'exception, vous obtiendrez une traceback.
Helmut Grohne
16
Vous pouvez également définir sys.excepthook(voir ici ) pour éviter d'avoir à encapsuler tout votre code dans try / except.
juillet
23
Vous pouvez simplement écrire except Exception:parce que vous n'utilisez een aucune façon;)
Marco Ferrari
21
Vous pouvez très bien vouloir inspecter elorsque vous tentez de déboguer de manière interactive votre code. :) C'est pourquoi je l'inclus toujours.
Vicki Laidler
4
Corrigez-moi si je me trompe, dans ce cas, il n'y a pas vraiment de gestion de l'exception et il est donc logique d'ajouter raiseà la fin de la exceptportée. Sinon, la course continuera comme si tout allait bien.
Dror
184

Une bonne chose à logging.exceptionce sujet la réponse de SiggyF ne montre pas que vous pouvez passer dans un message arbitraire et l' exploitation forestière montrera toujours la pleine retraçage avec tous les détails d'exception:

import logging
try:
    1/0
except ZeroDivisionError:
    logging.exception("Deliberate divide by zero traceback")

Avec le comportement de journalisation par défaut (dans les versions récentes) des erreurs d'impression uniquement sys.stderr, il ressemble à ceci:

>>> import logging
>>> try:
...     1/0
... except ZeroDivisionError:
...     logging.exception("Deliberate divide by zero traceback")
... 
ERROR:root:Deliberate divide by zero traceback
Traceback (most recent call last):
  File "<stdin>", line 2, in <module>
ZeroDivisionError: integer division or modulo by zero
ncoghlan
la source
Une exception peut-elle être enregistrée sans fournir de message?
Stevoisiak
@StevenVascellaro - Je vous suggère de passer ''si vous ne voulez vraiment pas taper un message ... cependant, la fonction ne peut pas être appelée sans au moins un argument, vous devrez donc lui donner quelque chose.
ArtOfWarfare
147

L'utilisation des exc_infooptions peut être meilleure, pour vous permettre de choisir le niveau d'erreur (si vous utilisez exception, ce sera toujours au errorniveau):

try:
    # do something here
except Exception as e:
    logging.critical(e, exc_info=True)  # log exception info at CRITICAL log level
flycee
la source
@CivFan: Je n'ai pas vraiment regardé les autres modifications ou l'introduction du post; cette introduction a également été ajoutée par un éditeur tiers. Je ne vois nulle part dans les commentaires supprimés que cela ait jamais été l'intention, mais je peux aussi bien annuler ma modification et supprimer les commentaires, il a été trop long pour le vote ici d'avoir été pour autre chose que la version modifiée .
Martijn Pieters
Une logging.fatalméthode est-elle dans la bibliothèque de journalisation? Je vois seulement critical.
Ian
1
@Ian C'est un alias pour critical, tout comme l' warnest warning.
0xc0de
35

Citant

Que faire si votre application se connecte d'une autre manière - sans utiliser le logging module?

Maintenant, tracebackpourrait être utilisé ici.

import traceback

def log_traceback(ex, ex_traceback=None):
    if ex_traceback is None:
        ex_traceback = ex.__traceback__
    tb_lines = [ line.rstrip('\n') for line in
                 traceback.format_exception(ex.__class__, ex, ex_traceback)]
    exception_logger.log(tb_lines)
  • Utilisez-le dans Python 2 :

    try:
        # your function call is here
    except Exception as ex:
        _, _, ex_traceback = sys.exc_info()
        log_traceback(ex, ex_traceback)
  • Utilisez-le dans Python 3 :

    try:
        x = get_number()
    except Exception as ex:
        log_traceback(ex)
zangw
la source
Pourquoi avez-vous placé "_, _, ex_traceback = sys.exc_info ()" en dehors de la fonction log_traceback et l'avez-vous ensuite passé en argument? Pourquoi ne pas l'utiliser directement dans la fonction?
Basil Musa
@BasilMusa, pour répondre à votre question, en bref, pour être compatible avec Python 3, car le ex_tracebackest ex.__traceback__sous Python 3, mais ex_tracebackest sys.exc_info()sous Python 2.
zangw
12

Si vous utilisez des journaux simples - tous vos enregistrements du journal doivent correspondre à cette règle: one record = one line. En suivant cette règle, vous pouvez utilisergrep et d'autres outils pour traiter vos fichiers journaux.

Mais les informations de traceback sont multi-lignes. Donc ma réponse est une version étendue de la solution proposée par zangw ci-dessus dans ce fil. Le problème est que les lignes de trace pourraient avoir à l' \nintérieur, nous devons donc faire un travail supplémentaire pour se débarrasser de ces fins de ligne:

import logging


logger = logging.getLogger('your_logger_here')

def log_app_error(e: BaseException, level=logging.ERROR) -> None:
    e_traceback = traceback.format_exception(e.__class__, e, e.__traceback__)
    traceback_lines = []
    for line in [line.rstrip('\n') for line in e_traceback]:
        traceback_lines.extend(line.splitlines())
    logger.log(level, traceback_lines.__str__())

Après cela (lorsque vous analyserez vos journaux), vous pouvez copier / coller les lignes de trace requises à partir de votre fichier journal et procédez comme suit:

ex_traceback = ['line 1', 'line 2', ...]
for line in ex_traceback:
    print(line)

Profit!

doomatel
la source
9

Cette réponse s'appuie sur les excellentes réponses ci-dessus.

Dans la plupart des applications, vous n'appelerez pas directement logging.exception (e). Vous avez probablement défini un enregistreur personnalisé spécifique à votre application ou module comme celui-ci:

# Set the name of the app or module
my_logger = logging.getLogger('NEM Sequencer')
# Set the log level
my_logger.setLevel(logging.INFO)

# Let's say we want to be fancy and log to a graylog2 log server
graylog_handler = graypy.GELFHandler('some_server_ip', 12201)
graylog_handler.setLevel(logging.INFO)
my_logger.addHandler(graylog_handler)

Dans ce cas, utilisez simplement l'enregistreur pour appeler l'exception (e) comme ceci:

try:
    1/0
except ZeroDivisionError, e:
    my_logger.exception(e)
Volonté
la source
C'est une touche de finition utile en effet si vous voulez un enregistreur dédié juste pour les exceptions.
logicOnAbstractions
7

Vous pouvez enregistrer la trace de pile sans exception.

https://docs.python.org/3/library/logging.html#logging.Logger.debug

Le deuxième argument de mot clé facultatif est stack_info, qui est par défaut False. Si vrai, les informations de pile sont ajoutées au message de journalisation, y compris l'appel de journalisation réel. Notez que ce ne sont pas les mêmes informations de pile que celles affichées en spécifiant exc_info: les premières sont des trames de pile du bas de la pile jusqu'à l'appel de journalisation dans le thread actuel, tandis que la seconde est des informations sur les trames de pile qui ont été déroulées, suite à une exception, lors de la recherche de gestionnaires d'exceptions.

Exemple:

>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> logging.getLogger().info('This prints the stack', stack_info=True)
INFO:root:This prints the stack
Stack (most recent call last):
  File "<stdin>", line 1, in <module>
>>>
Baczek
la source
5

Un peu de traitement de décoration (très lâchement inspiré par la monade peut-être et le levage). Vous pouvez supprimer en toute sécurité les annotations de type Python 3.6 et utiliser un style de formatage de message plus ancien.

fallible.py

from functools import wraps
from typing import Callable, TypeVar, Optional
import logging


A = TypeVar('A')


def fallible(*exceptions, logger=None) \
        -> Callable[[Callable[..., A]], Callable[..., Optional[A]]]:
    """
    :param exceptions: a list of exceptions to catch
    :param logger: pass a custom logger; None means the default logger, 
                   False disables logging altogether.
    """
    def fwrap(f: Callable[..., A]) -> Callable[..., Optional[A]]:

        @wraps(f)
        def wrapped(*args, **kwargs):
            try:
                return f(*args, **kwargs)
            except exceptions:
                message = f'called {f} with *args={args} and **kwargs={kwargs}'
                if logger:
                    logger.exception(message)
                if logger is None:
                    logging.exception(message)
                return None

        return wrapped

    return fwrap

Démo:

In [1] from fallible import fallible

In [2]: @fallible(ArithmeticError)
    ...: def div(a, b):
    ...:     return a / b
    ...: 
    ...: 

In [3]: div(1, 2)
Out[3]: 0.5

In [4]: res = div(1, 0)
ERROR:root:called <function div at 0x10d3c6ae8> with *args=(1, 0) and **kwargs={}
Traceback (most recent call last):
  File "/Users/user/fallible.py", line 17, in wrapped
    return f(*args, **kwargs)
  File "<ipython-input-17-e056bd886b5c>", line 3, in div
    return a / b

In [5]: repr(res)
'None'

Vous pouvez également modifier cette solution pour renvoyer quelque chose d'un peu plus significatif que Nonede la exceptpartie (ou même rendre la solution générique, en spécifiant cette valeur de retour dans fallibleles arguments de).

Eli Korvigo
la source
0

Dans votre module de journalisation (si module personnalisé), activez simplement stack_info.

api_logger.exceptionLog("*Input your Custom error message*",stack_info=True)
Piyush Kumar
la source
-1

Si vous pouvez faire face à la dépendance supplémentaire, puis utilisez twisted.log, vous n'avez pas à consigner explicitement les erreurs et cela renvoie également l'intégralité du suivi et de l'heure au fichier ou au flux.

Jakob Bowyer
la source
8
C'est peut twisted- être une bonne recommandation, mais cette réponse ne contribue pas vraiment beaucoup. Il ne dit pas comment utiliser twisted.log, ni quels avantages il a sur le loggingmodule de la bibliothèque standard, ni expliquer ce que l'on entend par "vous n'avez pas à consigner explicitement les erreurs" .
Mark Amery
-8

Une façon propre de le faire consiste à utiliser format_exc()puis à analyser la sortie pour obtenir la partie appropriée:

from traceback import format_exc

try:
    1/0
except Exception:
    print 'the relevant part is: '+format_exc().split('\n')[-2]

Cordialement

caraconan
la source
4
Hein? Pourquoi est-ce "la partie pertinente" ? Tout le .split('\n')[-2]fait est jeter le numéro de ligne et retraçage du résultat de format_exc()- informations utiles que vous voulez normalement! De plus, il ne fait même pas un bon travail de ce ; si votre message d'exception contient une nouvelle ligne, cette approche n'imprimera que la dernière ligne du message d'exception, ce qui signifie que vous perdez la classe d'exception et la plupart du message d'exception en plus de perdre la trace. -1.
Mark Amery