fd1cabfef88170a8e11acf46e9e4ea807f7b4d4b
[sfa.git] / sfa / util / sfalogging.py
1 #!/usr/bin/env python3
2
3 """
4 A reroutable logger that can handle deep tracebacks
5
6 Requirements:
7
8 * for legacy, we want all our code to just do:
9
10     from sfa.util.sfalogging import logger
11     ...
12     logger.info('blabla')
13
14 * depending on whether the code runs (a) inside the server,
15   (b) as part of sfa-import, or (c) as part of the sfi CLI,
16   we want these messages to be directed in different places
17
18 * also because troubleshooting is very painful, we need a better way
19   to report stacks when an exception occurs.
20
21 Implementation:
22
23 * we use a single unique logger name 'sfa' (wrt getLogger()),
24   and provide an auxiliary function `init_logger()` that
25   accepts for its `context` parameter one of :
26   `server`, `import` `sfi` or `console`
27   It will then reconfigure the 'sfa' logger to do the right thing
28
29 * also we create our own subclass of loggers, and install it
30   with logging.setLoggerClass(), so we can add our own customized
31   `log_exc()` method
32
33 """
34
35 # pylint: disable=c0111, c0103, w1201
36
37
38
39 import os
40 import os.path
41 import sys
42 import traceback
43 import logging
44 import logging.handlers
45 import logging.config
46
47 # so that users of this module don't need to import logging
48 from logging import (CRITICAL, ERROR, WARNING, INFO, DEBUG)
49
50
51 class SfaLogger(logging.getLoggerClass()):
52     """
53     a rewrite of  old _SfaLogger class that was way too cumbersome
54     keep this as much as possible though
55     """
56
57     # shorthand to avoid having to import logging all over the place
58     def setLevelDebug(self):
59         self.setLevel(DEBUG)
60
61     def debugEnabled(self):
62         return self.getEffectiveLevel() == logging.DEBUG
63
64     # define a verbose option with s/t like
65     # parser.add_option("-v", "--verbose", action="count",
66     #                   dest="verbose", default=0)
67     # and pass the coresponding options.verbose to this method to adjust level
68     def setLevelFromOptVerbose(self, verbose):
69         if verbose == 0:
70             self.setLevel(logging.WARNING)
71         elif verbose == 1:
72             self.setLevel(logging.INFO)
73         elif verbose >= 2:
74             self.setLevel(logging.DEBUG)
75
76     # in case some other code needs a boolean
77     @staticmethod
78     def getBoolVerboseFromOpt(verbose):
79         return verbose >= 1
80
81     @staticmethod
82     def getBoolDebugFromOpt(verbose):
83         return verbose >= 2
84
85     def log_exc(self, message, limit=100):
86         """
87         standard logger has an exception() method but this will
88         dump the stack only between the frames
89         (1) that does `raise` and (2) the one that does `except`
90
91         log_exc() has a limit argument that allows to see deeper than that
92
93         use limit=None to get the same behaviour as exception()
94         """
95         self.error("%s BEG TRACEBACK" % message + "\n" +
96                    traceback.format_exc(limit=limit).strip("\n"))
97         self.error("%s END TRACEBACK" % message)
98
99     # for investigation purposes, can be placed anywhere
100     def log_stack(self, message, limit=100):
101         to_log = "".join(traceback.format_stack(limit=limit))
102         self.info("%s BEG STACK" % message + "\n" + to_log)
103         self.info("%s END STACK" % message)
104
105     def enable_console(self):
106         formatter = logging.Formatter("%(message)s")
107         handler = logging.StreamHandler(sys.stdout)
108         handler.setFormatter(formatter)
109         self.addHandler(handler)
110
111
112 # install our class as the default
113 logging.setLoggerClass(SfaLogger)
114
115
116 # configure
117 # this is *NOT* passed to dictConfig as-is
118 # instead we filter 'handlers' and 'loggers'
119 # to contain just one entry
120 # so make sure that 'handlers' and 'loggers'
121 # have the same set of keys
122 def logging_config(context):
123     if context == 'server':
124         handlername = 'file'
125         filename = '/var/log/sfa.log'
126         level = 'DEBUG'
127     elif context == 'import':
128         handlername = 'file'
129         filename = '/var/log/sfa-import.log'
130         level = 'INFO'
131     elif context == 'cli':
132         handlername = 'file'
133         filename = os.path.expanduser("~/.sfi.log")
134         level = 'DEBUG'
135     elif context == 'console':
136         handlername = 'stdout'
137         filename = 'ignored'
138         level = 'INFO'
139     else:
140         print("Cannot configure logging - exiting")
141         exit(1)
142
143     config = {
144         'version': 1,
145         # IMPORTANT: we may be imported by something else, so:
146         'disable_existing_loggers': False,
147         'formatters': {
148             'standard': {
149                 'datefmt': '%m-%d %H:%M:%S',
150                 'format': ('%(asctime)s %(levelname)s '
151                            '%(filename)s:%(lineno)d %(message)s'),
152             },
153         },
154         # fill in later with just the one needed
155         # otherwise a dummy 'ignored' file gets created
156         'handlers': {
157         },
158         'loggers': {
159             'sfa': {
160                 'handlers': [handlername],
161                 'level': level,
162                 'propagate': False,
163             },
164         },
165     }
166     if handlername == 'stdout':
167         config['handlers']['stdout'] = {
168             'level': level,
169             'formatter': 'standard',
170             'class': 'logging.StreamHandler',
171         }
172     else:
173         config['handlers']['file'] = {
174             'filename': filename,
175             'level': level,
176             'formatter': 'standard',
177             'class': 'logging.handlers.TimedRotatingFileHandler',
178             # every monday and during 3 months
179             'when': 'w0',
180             'interval': 1,
181             'backupCount': 12,
182         }
183     return config
184
185
186 logger = logging.getLogger('sfa')
187
188
189 def init_logger(context):
190     logging.config.dictConfig(logging_config(context))
191
192
193 # if the user process does not do anything
194 # like for the miscell testers and other certificate
195 # probing/dumping utilities
196 init_logger('console')