Introduce re-openable log record handler
[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 logging
26 import logging.handlers
27
28 from ganeti import constants
29
30
31 class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
32 """Log handler with ability to reopen log file on request.
33
34 In combination with a SIGHUP handler this class can reopen the log file on
35 user request.
36
37 """
38 def __init__(self, filename):
39 """Initializes this class.
40
41 @type filename: string
42 @param filename: Path to logfile
43
44 """
45 logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
46
47 assert self.encoding is None, "Encoding not supported for logging"
48 assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
49
50 self._reopen = False
51
52 def shouldRollover(self, _): # pylint: disable-msg=C0103
53 """Determine whether log file should be reopened.
54
55 """
56 return self._reopen or not self.stream
57
58 def doRollover(self): # pylint: disable-msg=C0103
59 """Reopens the log file.
60
61 """
62 if self.stream:
63 self.stream.flush()
64 self.stream.close()
65 self.stream = None
66
67 # Reopen file
68 # TODO: Handle errors?
69 self.stream = open(self.baseFilename, "a")
70
71 def RequestReopen(self):
72 """Register a request to reopen the file.
73
74 The file will be reopened before writing the next log record.
75
76 """
77 self._reopen = True
78
79
80 def _LogErrorsToConsole(base):
81 """Create wrapper class writing errors to console.
82
83 This needs to be in a function for unittesting.
84
85 """
86 class wrapped(base): # pylint: disable-msg=C0103
87 """Log handler that doesn't fallback to stderr.
88
89 When an error occurs while writing on the logfile, logging.FileHandler
90 tries to log on stderr. This doesn't work in Ganeti since stderr is
91 redirected to a logfile. This class avoids failures by reporting errors to
92 /dev/console.
93
94 """
95 def __init__(self, console, *args, **kwargs):
96 """Initializes this class.
97
98 @type console: file-like object or None
99 @param console: Open file-like object for console
100
101 """
102 base.__init__(self, *args, **kwargs)
103 assert not hasattr(self, "_console")
104 self._console = console
105
106 def handleError(self, record): # pylint: disable-msg=C0103
107 """Handle errors which occur during an emit() call.
108
109 Try to handle errors with FileHandler method, if it fails write to
110 /dev/console.
111
112 """
113 try:
114 base.handleError(record)
115 except Exception: # pylint: disable-msg=W0703
116 if self._console:
117 try:
118 # Ignore warning about "self.format", pylint: disable-msg=E1101
119 self._console.write("Cannot log message:\n%s\n" %
120 self.format(record))
121 except Exception: # pylint: disable-msg=W0703
122 # Log handler tried everything it could, now just give up
123 pass
124
125 return wrapped
126
127
128 #: Custom log handler for writing to console with a reopenable handler
129 _LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
130
131
132 def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
133 multithreaded=False, syslog=constants.SYSLOG_USAGE,
134 console_logging=False):
135 """Configures the logging module.
136
137 @type logfile: str
138 @param logfile: the filename to which we should log
139 @type debug: integer
140 @param debug: if greater than zero, enable debug messages, otherwise
141 only those at C{INFO} and above level
142 @type stderr_logging: boolean
143 @param stderr_logging: whether we should also log to the standard error
144 @type program: str
145 @param program: the name under which we should log messages
146 @type multithreaded: boolean
147 @param multithreaded: if True, will add the thread name to the log file
148 @type syslog: string
149 @param syslog: one of 'no', 'yes', 'only':
150 - if no, syslog is not used
151 - if yes, syslog is used (in addition to file-logging)
152 - if only, only syslog is used
153 @type console_logging: boolean
154 @param console_logging: if True, will use a FileHandler which falls back to
155 the system console if logging fails
156 @raise EnvironmentError: if we can't open the log file and
157 syslog/stderr logging is disabled
158
159 """
160 fmt = "%(asctime)s: " + program + " pid=%(process)d"
161 sft = program + "[%(process)d]:"
162 if multithreaded:
163 fmt += "/%(threadName)s"
164 sft += " (%(threadName)s)"
165 if debug:
166 fmt += " %(module)s:%(lineno)s"
167 # no debug info for syslog loggers
168 fmt += " %(levelname)s %(message)s"
169 # yes, we do want the textual level, as remote syslog will probably
170 # lose the error level, and it's easier to grep for it
171 sft += " %(levelname)s %(message)s"
172 formatter = logging.Formatter(fmt)
173 sys_fmt = logging.Formatter(sft)
174
175 root_logger = logging.getLogger("")
176 root_logger.setLevel(logging.NOTSET)
177
178 # Remove all previously setup handlers
179 for handler in root_logger.handlers:
180 handler.close()
181 root_logger.removeHandler(handler)
182
183 if stderr_logging:
184 stderr_handler = logging.StreamHandler()
185 stderr_handler.setFormatter(formatter)
186 if debug:
187 stderr_handler.setLevel(logging.NOTSET)
188 else:
189 stderr_handler.setLevel(logging.CRITICAL)
190 root_logger.addHandler(stderr_handler)
191
192 if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
193 facility = logging.handlers.SysLogHandler.LOG_DAEMON
194 syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
195 facility)
196 syslog_handler.setFormatter(sys_fmt)
197 # Never enable debug over syslog
198 syslog_handler.setLevel(logging.INFO)
199 root_logger.addHandler(syslog_handler)
200
201 if syslog != constants.SYSLOG_ONLY:
202 # this can fail, if the logging directories are not setup or we have
203 # a permisssion problem; in this case, it's best to log but ignore
204 # the error if stderr_logging is True, and if false we re-raise the
205 # exception since otherwise we could run but without any logs at all
206 try:
207 if console_logging:
208 logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"), logfile)
209 else:
210 logfile_handler = _ReopenableLogHandler(logfile)
211
212 logfile_handler.setFormatter(formatter)
213 if debug:
214 logfile_handler.setLevel(logging.DEBUG)
215 else:
216 logfile_handler.setLevel(logging.INFO)
217 root_logger.addHandler(logfile_handler)
218 except EnvironmentError:
219 if stderr_logging or syslog == constants.SYSLOG_YES:
220 logging.exception("Failed to enable logging to file '%s'", logfile)
221 else:
222 # we need to re-raise the exception
223 raise