Revert "utils.log: Write error messages to stderr"
[ganeti-local] / 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=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=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     # Don't reopen on the next message
74     self._reopen = False
75
76   def RequestReopen(self):
77     """Register a request to reopen the file.
78
79     The file will be reopened before writing the next log record.
80
81     """
82     self._reopen = True
83
84
85 def _LogErrorsToConsole(base):
86   """Create wrapper class writing errors to console.
87
88   This needs to be in a function for unittesting.
89
90   """
91   class wrapped(base): # pylint: disable=C0103
92     """Log handler that doesn't fallback to stderr.
93
94     When an error occurs while writing on the logfile, logging.FileHandler
95     tries to log on stderr. This doesn't work in Ganeti since stderr is
96     redirected to a logfile. This class avoids failures by reporting errors to
97     /dev/console.
98
99     """
100     def __init__(self, console, *args, **kwargs):
101       """Initializes this class.
102
103       @type console: file-like object or None
104       @param console: Open file-like object for console
105
106       """
107       base.__init__(self, *args, **kwargs)
108       assert not hasattr(self, "_console")
109       self._console = console
110
111     def handleError(self, record): # pylint: disable=C0103
112       """Handle errors which occur during an emit() call.
113
114       Try to handle errors with FileHandler method, if it fails write to
115       /dev/console.
116
117       """
118       try:
119         base.handleError(record)
120       except Exception: # pylint: disable=W0703
121         if self._console:
122           try:
123             # Ignore warning about "self.format", pylint: disable=E1101
124             self._console.write("Cannot log message:\n%s\n" %
125                                 self.format(record))
126           except Exception: # pylint: disable=W0703
127             # Log handler tried everything it could, now just give up
128             pass
129
130   return wrapped
131
132
133 #: Custom log handler for writing to console with a reopenable handler
134 _LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
135
136
137 def _GetLogFormatter(program, multithreaded, debug, syslog):
138   """Build log formatter.
139
140   @param program: Program name
141   @param multithreaded: Whether to add thread name to log messages
142   @param debug: Whether to enable debug messages
143   @param syslog: Whether the formatter will be used for syslog
144
145   """
146   parts = []
147
148   if syslog:
149     parts.append(program + "[%(process)d]:")
150   else:
151     parts.append("%(asctime)s: " + program + " pid=%(process)d")
152
153   if multithreaded:
154     if syslog:
155       parts.append(" (%(threadName)s)")
156     else:
157       parts.append("/%(threadName)s")
158
159   # Add debug info for non-syslog loggers
160   if debug and not syslog:
161     parts.append(" %(module)s:%(lineno)s")
162
163   # Ses, we do want the textual level, as remote syslog will probably lose the
164   # error level, and it's easier to grep for it.
165   parts.append(" %(levelname)s %(message)s")
166
167   return logging.Formatter("".join(parts))
168
169
170 def _ReopenLogFiles(handlers):
171   """Wrapper for reopening all log handler's files in a sequence.
172
173   """
174   for handler in handlers:
175     handler.RequestReopen()
176   logging.info("Received request to reopen log files")
177
178
179 def SetupLogging(logfile, program, debug=0, stderr_logging=False,
180                  multithreaded=False, syslog=constants.SYSLOG_USAGE,
181                  console_logging=False, root_logger=None):
182   """Configures the logging module.
183
184   @type logfile: str
185   @param logfile: the filename to which we should log
186   @type program: str
187   @param program: the name under which we should log messages
188   @type debug: integer
189   @param debug: if greater than zero, enable debug messages, otherwise
190       only those at C{INFO} and above level
191   @type stderr_logging: boolean
192   @param stderr_logging: whether we should also log to the standard error
193   @type multithreaded: boolean
194   @param multithreaded: if True, will add the thread name to the log file
195   @type syslog: string
196   @param syslog: one of 'no', 'yes', 'only':
197       - if no, syslog is not used
198       - if yes, syslog is used (in addition to file-logging)
199       - if only, only syslog is used
200   @type console_logging: boolean
201   @param console_logging: if True, will use a FileHandler which falls back to
202       the system console if logging fails
203   @type root_logger: logging.Logger
204   @param root_logger: Root logger to use (for unittests)
205   @raise EnvironmentError: if we can't open the log file and
206       syslog/stderr logging is disabled
207   @rtype: callable
208   @return: Function reopening all open log files when called
209
210   """
211   progname = os.path.basename(program)
212
213   formatter = _GetLogFormatter(progname, multithreaded, debug, False)
214   syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
215
216   reopen_handlers = []
217
218   if root_logger is None:
219     root_logger = logging.getLogger("")
220   root_logger.setLevel(logging.NOTSET)
221
222   # Remove all previously setup handlers
223   for handler in root_logger.handlers:
224     handler.close()
225     root_logger.removeHandler(handler)
226
227   if stderr_logging:
228     stderr_handler = logging.StreamHandler()
229     stderr_handler.setFormatter(formatter)
230     if debug:
231       stderr_handler.setLevel(logging.NOTSET)
232     else:
233       stderr_handler.setLevel(logging.CRITICAL)
234     root_logger.addHandler(stderr_handler)
235
236   if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
237     facility = logging.handlers.SysLogHandler.LOG_DAEMON
238     syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
239                                                     facility)
240     syslog_handler.setFormatter(syslog_fmt)
241     # Never enable debug over syslog
242     syslog_handler.setLevel(logging.INFO)
243     root_logger.addHandler(syslog_handler)
244
245   if syslog != constants.SYSLOG_ONLY:
246     # this can fail, if the logging directories are not setup or we have
247     # a permisssion problem; in this case, it's best to log but ignore
248     # the error if stderr_logging is True, and if false we re-raise the
249     # exception since otherwise we could run but without any logs at all
250     try:
251       if console_logging:
252         logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
253                                       logfile)
254       else:
255         logfile_handler = _ReopenableLogHandler(logfile)
256
257       logfile_handler.setFormatter(formatter)
258       if debug:
259         logfile_handler.setLevel(logging.DEBUG)
260       else:
261         logfile_handler.setLevel(logging.INFO)
262       root_logger.addHandler(logfile_handler)
263       reopen_handlers.append(logfile_handler)
264     except EnvironmentError:
265       if stderr_logging or syslog == constants.SYSLOG_YES:
266         logging.exception("Failed to enable logging to file '%s'", logfile)
267       else:
268         # we need to re-raise the exception
269         raise
270
271   return compat.partial(_ReopenLogFiles, reopen_handlers)