Merge branch 'devel-2.4'
[ganeti-github.git] / lib / utils / log.py
1 #
2 #
3
4 # Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
5 #
6 # This program is free software; you can redistribute it and/or modify
7 # it under the terms of the GNU General Public License as published by
8 # the Free Software Foundation; either version 2 of the License, or
9 # (at your option) any later version.
10 #
11 # This program is distributed in the hope that it will be useful, but
12 # WITHOUT ANY WARRANTY; without even the implied warranty of
13 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
14 # General Public License for more details.
15 #
16 # You should have received a copy of the GNU General Public License
17 # along with this program; if not, write to the Free Software
18 # Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
19 # 02110-1301, USA.
20
21 """Utility functions for logging.
22
23 """
24
25 import os.path
26 import logging
27 import logging.handlers
28
29 from ganeti import constants
30 from ganeti import compat
31
32
33 class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
34 """Log handler with ability to reopen log file on request.
35
36 In combination with a SIGHUP handler this class can reopen the log file on
37 user request.
38
39 """
40 def __init__(self, filename):
41 """Initializes this class.
42
43 @type filename: string
44 @param filename: Path to logfile
45
46 """
47 logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
48
49 assert self.encoding is None, "Encoding not supported for logging"
50 assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
51
52 self._reopen = False
53
54 def shouldRollover(self, _): # pylint: disable-msg=C0103
55 """Determine whether log file should be reopened.
56
57 """
58 return self._reopen or not self.stream
59
60 def doRollover(self): # pylint: disable-msg=C0103
61 """Reopens the log file.
62
63 """
64 if self.stream:
65 self.stream.flush()
66 self.stream.close()
67 self.stream = None
68
69 # Reopen file
70 # TODO: Handle errors?
71 self.stream = open(self.baseFilename, "a")
72
73 def RequestReopen(self):
74 """Register a request to reopen the file.
75
76 The file will be reopened before writing the next log record.
77
78 """
79 self._reopen = True
80
81
82 def _LogErrorsToConsole(base):
83 """Create wrapper class writing errors to console.
84
85 This needs to be in a function for unittesting.
86
87 """
88 class wrapped(base): # pylint: disable-msg=C0103
89 """Log handler that doesn't fallback to stderr.
90
91 When an error occurs while writing on the logfile, logging.FileHandler
92 tries to log on stderr. This doesn't work in Ganeti since stderr is
93 redirected to a logfile. This class avoids failures by reporting errors to
94 /dev/console.
95
96 """
97 def __init__(self, console, *args, **kwargs):
98 """Initializes this class.
99
100 @type console: file-like object or None
101 @param console: Open file-like object for console
102
103 """
104 base.__init__(self, *args, **kwargs)
105 assert not hasattr(self, "_console")
106 self._console = console
107
108 def handleError(self, record): # pylint: disable-msg=C0103
109 """Handle errors which occur during an emit() call.
110
111 Try to handle errors with FileHandler method, if it fails write to
112 /dev/console.
113
114 """
115 try:
116 base.handleError(record)
117 except Exception: # pylint: disable-msg=W0703
118 if self._console:
119 try:
120 # Ignore warning about "self.format", pylint: disable-msg=E1101
121 self._console.write("Cannot log message:\n%s\n" %
122 self.format(record))
123 except Exception: # pylint: disable-msg=W0703
124 # Log handler tried everything it could, now just give up
125 pass
126
127 return wrapped
128
129
130 #: Custom log handler for writing to console with a reopenable handler
131 _LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
132
133
134 def _GetLogFormatter(program, multithreaded, debug, syslog):
135 """Build log formatter.
136
137 @param program: Program name
138 @param multithreaded: Whether to add thread name to log messages
139 @param debug: Whether to enable debug messages
140 @param syslog: Whether the formatter will be used for syslog
141
142 """
143 parts = []
144
145 if syslog:
146 parts.append(program + "[%(process)d]:")
147 else:
148 parts.append("%(asctime)s: " + program + " pid=%(process)d")
149
150 if multithreaded:
151 if syslog:
152 parts.append(" (%(threadName)s)")
153 else:
154 parts.append("/%(threadName)s")
155
156 # Add debug info for non-syslog loggers
157 if debug and not syslog:
158 parts.append(" %(module)s:%(lineno)s")
159
160 # Ses, we do want the textual level, as remote syslog will probably lose the
161 # error level, and it's easier to grep for it.
162 parts.append(" %(levelname)s %(message)s")
163
164 return logging.Formatter("".join(parts))
165
166
167 def _ReopenLogFiles(handlers):
168 """Wrapper for reopening all log handler's files in a sequence.
169
170 """
171 for handler in handlers:
172 handler.RequestReopen()
173 logging.info("Received request to reopen log files")
174
175
176 def SetupLogging(logfile, program, debug=0, stderr_logging=False,
177 multithreaded=False, syslog=constants.SYSLOG_USAGE,
178 console_logging=False, root_logger=None):
179 """Configures the logging module.
180
181 @type logfile: str
182 @param logfile: the filename to which we should log
183 @type program: str
184 @param program: the name under which we should log messages
185 @type debug: integer
186 @param debug: if greater than zero, enable debug messages, otherwise
187 only those at C{INFO} and above level
188 @type stderr_logging: boolean
189 @param stderr_logging: whether we should also log to the standard error
190 @type multithreaded: boolean
191 @param multithreaded: if True, will add the thread name to the log file
192 @type syslog: string
193 @param syslog: one of 'no', 'yes', 'only':
194 - if no, syslog is not used
195 - if yes, syslog is used (in addition to file-logging)
196 - if only, only syslog is used
197 @type console_logging: boolean
198 @param console_logging: if True, will use a FileHandler which falls back to
199 the system console if logging fails
200 @type root_logger: logging.Logger
201 @param root_logger: Root logger to use (for unittests)
202 @raise EnvironmentError: if we can't open the log file and
203 syslog/stderr logging is disabled
204 @rtype: callable
205 @return: Function reopening all open log files when called
206
207 """
208 progname = os.path.basename(program)
209
210 formatter = _GetLogFormatter(progname, multithreaded, debug, False)
211 syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
212
213 reopen_handlers = []
214
215 if root_logger is None:
216 root_logger = logging.getLogger("")
217 root_logger.setLevel(logging.NOTSET)
218
219 # Remove all previously setup handlers
220 for handler in root_logger.handlers:
221 handler.close()
222 root_logger.removeHandler(handler)
223
224 if stderr_logging:
225 stderr_handler = logging.StreamHandler()
226 stderr_handler.setFormatter(formatter)
227 if debug:
228 stderr_handler.setLevel(logging.NOTSET)
229 else:
230 stderr_handler.setLevel(logging.CRITICAL)
231 root_logger.addHandler(stderr_handler)
232
233 if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
234 facility = logging.handlers.SysLogHandler.LOG_DAEMON
235 syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
236 facility)
237 syslog_handler.setFormatter(syslog_fmt)
238 # Never enable debug over syslog
239 syslog_handler.setLevel(logging.INFO)
240 root_logger.addHandler(syslog_handler)
241
242 if syslog != constants.SYSLOG_ONLY:
243 # this can fail, if the logging directories are not setup or we have
244 # a permisssion problem; in this case, it's best to log but ignore
245 # the error if stderr_logging is True, and if false we re-raise the
246 # exception since otherwise we could run but without any logs at all
247 try:
248 if console_logging:
249 logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
250 logfile)
251 else:
252 logfile_handler = _ReopenableLogHandler(logfile)
253
254 logfile_handler.setFormatter(formatter)
255 if debug:
256 logfile_handler.setLevel(logging.DEBUG)
257 else:
258 logfile_handler.setLevel(logging.INFO)
259 root_logger.addHandler(logfile_handler)
260 reopen_handlers.append(logfile_handler)
261 except EnvironmentError:
262 if stderr_logging or syslog == constants.SYSLOG_YES:
263 logging.exception("Failed to enable logging to file '%s'", logfile)
264 else:
265 # we need to re-raise the exception
266 raise
267
268 return compat.partial(_ReopenLogFiles, reopen_handlers)