server instances also log onto stdout for journalctl to handle
[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         # use stdout and let journalctl do the heavy lifting
125         handlername = 'stdout'
126         #filename = '/var/log/sfa.log'
127         level = 'DEBUG'
128     elif context == 'import':
129         handlername = 'file'
130         filename = '/var/log/sfa-import.log'
131         level = 'INFO'
132     elif context == 'cli':
133         handlername = 'file'
134         filename = os.path.expanduser("~/.sfi.log")
135         level = 'DEBUG'
136     elif context == 'console':
137         handlername = 'stdout'
138         #filename = 'ignored'
139         level = 'INFO'
140     else:
141         print("Cannot configure logging - exiting")
142         exit(1)
143
144     config = {
145         'version': 1,
146         # IMPORTANT: we may be imported by something else, so:
147         'disable_existing_loggers': False,
148         'formatters': {
149             'standard': {
150                 'datefmt': '%m-%d %H:%M:%S',
151                 'format': ('%(asctime)s %(levelname)s '
152                            '%(filename)s:%(lineno)d %(message)s'),
153             },
154         },
155         # fill in later with just the one needed
156         # otherwise a dummy 'ignored' file gets created
157         'handlers': {
158         },
159         'loggers': {
160             'sfa': {
161                 'handlers': [handlername],
162                 'level': level,
163                 'propagate': False,
164             },
165         },
166     }
167     if handlername == 'stdout':
168         config['handlers']['stdout'] = {
169             'level': level,
170             'formatter': 'standard',
171             'class': 'logging.StreamHandler',
172         }
173     else:
174         config['handlers']['file'] = {
175             'filename': filename,
176             'level': level,
177             'formatter': 'standard',
178             'class': 'logging.handlers.TimedRotatingFileHandler',
179             # every monday and during 3 months
180             'when': 'w0',
181             'interval': 1,
182             'backupCount': 12,
183         }
184     return config
185
186
187 logger = logging.getLogger('sfa')
188
189
190 def init_logger(context):
191     logging.config.dictConfig(logging_config(context))
192
193
194 # if the user process does not do anything
195 # like for the miscell testers and other certificate
196 # probing/dumping utilities
197 init_logger('console')