c39e96a8e987dbd76a0bdfcdbe80f55b48139054
[ganeti-github.git] / lib / utils / log.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
5 # All rights reserved.
6 #
7 # Redistribution and use in source and binary forms, with or without
8 # modification, are permitted provided that the following conditions are
9 # met:
10 #
11 # 1. Redistributions of source code must retain the above copyright notice,
12 # this list of conditions and the following disclaimer.
13 #
14 # 2. Redistributions in binary form must reproduce the above copyright
15 # notice, this list of conditions and the following disclaimer in the
16 # documentation and/or other materials provided with the distribution.
17 #
18 # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS
19 # IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED
20 # TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
21 # PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR
22 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
23 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
24 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
25 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
26 # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
27 # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
28 # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
29
30 """Utility functions for logging.
31
32 """
33
34 import os.path
35 import logging
36 import logging.handlers
37
38 from ganeti import constants
39 from ganeti import compat
40 from ganeti import pathutils
41
42
43 class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
44 """Log handler with ability to reopen log file on request.
45
46 In combination with a SIGHUP handler this class can reopen the log file on
47 user request.
48
49 """
50 def __init__(self, filename):
51 """Initializes this class.
52
53 @type filename: string
54 @param filename: Path to logfile
55
56 """
57 logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
58
59 assert self.encoding is None, "Encoding not supported for logging"
60 assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
61
62 self._reopen = False
63
64 def shouldRollover(self, _): # pylint: disable=C0103
65 """Determine whether log file should be reopened.
66
67 """
68 return self._reopen or not self.stream
69
70 def doRollover(self): # pylint: disable=C0103
71 """Reopens the log file.
72
73 """
74 if self.stream:
75 self.stream.flush()
76 self.stream.close()
77 self.stream = None
78
79 # Reopen file
80 # TODO: Handle errors?
81 self.stream = open(self.baseFilename, "a")
82
83 # Don't reopen on the next message
84 self._reopen = False
85
86 def RequestReopen(self):
87 """Register a request to reopen the file.
88
89 The file will be reopened before writing the next log record.
90
91 """
92 self._reopen = True
93
94
95 def _LogErrorsToConsole(base):
96 """Create wrapper class writing errors to console.
97
98 This needs to be in a function for unittesting.
99
100 """
101 class wrapped(base): # pylint: disable=C0103
102 """Log handler that doesn't fallback to stderr.
103
104 When an error occurs while writing on the logfile, logging.FileHandler
105 tries to log on stderr. This doesn't work in Ganeti since stderr is
106 redirected to a logfile. This class avoids failures by reporting errors to
107 /dev/console.
108
109 """
110 def __init__(self, console, *args, **kwargs):
111 """Initializes this class.
112
113 @type console: file-like object or None
114 @param console: Open file-like object for console
115
116 """
117 base.__init__(self, *args, **kwargs)
118 assert not hasattr(self, "_console")
119 self._console = console
120
121 def handleError(self, record): # pylint: disable=C0103
122 """Handle errors which occur during an emit() call.
123
124 Try to handle errors with FileHandler method, if it fails write to
125 /dev/console.
126
127 """
128 try:
129 base.handleError(record)
130 except Exception: # pylint: disable=W0703
131 if self._console:
132 try:
133 # Ignore warning about "self.format", pylint: disable=E1101
134 self._console.write("Cannot log message:\n%s\n" %
135 self.format(record))
136 except Exception: # pylint: disable=W0703
137 # Log handler tried everything it could, now just give up
138 pass
139
140 return wrapped
141
142
143 #: Custom log handler for writing to console with a reopenable handler
144 _LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
145
146
147 def _GetLogFormatter(program, multithreaded, debug, syslog):
148 """Build log formatter.
149
150 @param program: Program name
151 @param multithreaded: Whether to add thread name to log messages
152 @param debug: Whether to enable debug messages
153 @param syslog: Whether the formatter will be used for syslog
154
155 """
156 parts = []
157
158 if syslog:
159 parts.append(program + "[%(process)d]:")
160 else:
161 parts.append("%(asctime)s: " + program + " pid=%(process)d")
162
163 if multithreaded:
164 if syslog:
165 parts.append(" (%(threadName)s)")
166 else:
167 parts.append("/%(threadName)s")
168
169 # Add debug info for non-syslog loggers
170 if debug and not syslog:
171 parts.append(" %(module)s:%(lineno)s")
172
173 # Ses, we do want the textual level, as remote syslog will probably lose the
174 # error level, and it's easier to grep for it.
175 parts.append(" %(levelname)s %(message)s")
176
177 return logging.Formatter("".join(parts))
178
179
180 def _ReopenLogFiles(handlers):
181 """Wrapper for reopening all log handler's files in a sequence.
182
183 """
184 for handler in handlers:
185 handler.RequestReopen()
186 logging.info("Received request to reopen log files")
187
188
189 def SetupLogging(logfile, program, debug=0, stderr_logging=False,
190 multithreaded=False, syslog=constants.SYSLOG_USAGE,
191 console_logging=False, root_logger=None,
192 verbose=True):
193 """Configures the logging module.
194
195 @type logfile: str
196 @param logfile: the filename to which we should log
197 @type program: str
198 @param program: the name under which we should log messages
199 @type debug: integer
200 @param debug: if greater than zero, enable debug messages, otherwise
201 only those at C{INFO} and above level
202 @type stderr_logging: boolean
203 @param stderr_logging: whether we should also log to the standard error
204 @type multithreaded: boolean
205 @param multithreaded: if True, will add the thread name to the log file
206 @type syslog: string
207 @param syslog: one of 'no', 'yes', 'only':
208 - if no, syslog is not used
209 - if yes, syslog is used (in addition to file-logging)
210 - if only, only syslog is used
211 @type console_logging: boolean
212 @param console_logging: if True, will use a FileHandler which falls back to
213 the system console if logging fails
214 @type root_logger: logging.Logger
215 @param root_logger: Root logger to use (for unittests)
216 @type verbose: boolean
217 @param verbose: whether to log at 'info' level already (logfile logging only)
218 @raise EnvironmentError: if we can't open the log file and
219 syslog/stderr logging is disabled
220 @rtype: callable
221 @return: Function reopening all open log files when called
222
223 """
224 progname = os.path.basename(program)
225
226 formatter = _GetLogFormatter(progname, multithreaded, debug, False)
227 syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
228
229 reopen_handlers = []
230
231 if root_logger is None:
232 root_logger = logging.getLogger("")
233 root_logger.setLevel(logging.NOTSET)
234
235 # Remove all previously setup handlers
236 for handler in root_logger.handlers:
237 handler.close()
238 root_logger.removeHandler(handler)
239
240 if stderr_logging:
241 stderr_handler = logging.StreamHandler()
242 stderr_handler.setFormatter(formatter)
243 if debug:
244 stderr_handler.setLevel(logging.NOTSET)
245 else:
246 stderr_handler.setLevel(logging.CRITICAL)
247 root_logger.addHandler(stderr_handler)
248
249 if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
250 facility = logging.handlers.SysLogHandler.LOG_DAEMON
251 syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
252 facility)
253 syslog_handler.setFormatter(syslog_fmt)
254 # Never enable debug over syslog
255 syslog_handler.setLevel(logging.INFO)
256 root_logger.addHandler(syslog_handler)
257
258 if syslog != constants.SYSLOG_ONLY:
259 # this can fail, if the logging directories are not setup or we have
260 # a permisssion problem; in this case, it's best to log but ignore
261 # the error if stderr_logging is True, and if false we re-raise the
262 # exception since otherwise we could run but without any logs at all
263 try:
264 if console_logging:
265 logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
266 logfile)
267 else:
268 logfile_handler = _ReopenableLogHandler(logfile)
269
270 logfile_handler.setFormatter(formatter)
271 if debug:
272 logfile_handler.setLevel(logging.DEBUG)
273 elif verbose:
274 logfile_handler.setLevel(logging.INFO)
275 else:
276 logfile_handler.setLevel(logging.WARN)
277 root_logger.addHandler(logfile_handler)
278 reopen_handlers.append(logfile_handler)
279 except EnvironmentError:
280 if stderr_logging or syslog == constants.SYSLOG_YES:
281 logging.exception("Failed to enable logging to file '%s'", logfile)
282 else:
283 # we need to re-raise the exception
284 raise
285
286 return compat.partial(_ReopenLogFiles, reopen_handlers)
287
288
289 def SetupToolLogging(debug, verbose, threadname=False,
290 toolname=None):
291 """Configures the logging module for tools.
292
293 All log messages are sent to the tools.log logfile.
294
295 @type toolname: string
296 @param toolname: name of the tool that's logging
297 @type debug: boolean
298 @param debug: Disable log message filtering
299 @type verbose: boolean
300 @param verbose: Enable verbose log messages
301 @type threadname: boolean
302 @param threadname: Whether to include thread name in output
303
304 """
305 if not toolname:
306 toolname = "unspecified_tool"
307
308 # 'SetupLogging' takes a quite unintuitive 'debug' option that
309 # is '0' for 'log higher than debug level' and '1' for
310 # 'log at NOSET' level. Hence this conversion.
311 debug_int = 0
312 if debug:
313 debug_int = 1
314
315 SetupLogging(pathutils.LOG_TOOLS,
316 program=toolname,
317 debug=debug_int,
318 multithreaded=threadname,
319 verbose=verbose)