Fix typo in LUGroupAssignNodes
[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-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
174
175 def SetupLogging(logfile, program, debug=0, stderr_logging=False,
176                  multithreaded=False, syslog=constants.SYSLOG_USAGE,
177                  console_logging=False, root_logger=None):
178   """Configures the logging module.
179
180   @type logfile: str
181   @param logfile: the filename to which we should log
182   @type program: str
183   @param program: the name under which we should log messages
184   @type debug: integer
185   @param debug: if greater than zero, enable debug messages, otherwise
186       only those at C{INFO} and above level
187   @type stderr_logging: boolean
188   @param stderr_logging: whether we should also log to the standard error
189   @type multithreaded: boolean
190   @param multithreaded: if True, will add the thread name to the log file
191   @type syslog: string
192   @param syslog: one of 'no', 'yes', 'only':
193       - if no, syslog is not used
194       - if yes, syslog is used (in addition to file-logging)
195       - if only, only syslog is used
196   @type console_logging: boolean
197   @param console_logging: if True, will use a FileHandler which falls back to
198       the system console if logging fails
199   @type root_logger: logging.Logger
200   @param root_logger: Root logger to use (for unittests)
201   @raise EnvironmentError: if we can't open the log file and
202       syslog/stderr logging is disabled
203   @rtype: callable
204   @return: Function reopening all open log files when called
205
206   """
207   progname = os.path.basename(program)
208
209   formatter = _GetLogFormatter(progname, multithreaded, debug, False)
210   syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
211
212   reopen_handlers = []
213
214   if root_logger is None:
215     root_logger = logging.getLogger("")
216   root_logger.setLevel(logging.NOTSET)
217
218   # Remove all previously setup handlers
219   for handler in root_logger.handlers:
220     handler.close()
221     root_logger.removeHandler(handler)
222
223   if stderr_logging:
224     stderr_handler = logging.StreamHandler()
225     stderr_handler.setFormatter(formatter)
226     if debug:
227       stderr_handler.setLevel(logging.NOTSET)
228     else:
229       stderr_handler.setLevel(logging.CRITICAL)
230     root_logger.addHandler(stderr_handler)
231
232   if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
233     facility = logging.handlers.SysLogHandler.LOG_DAEMON
234     syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
235                                                     facility)
236     syslog_handler.setFormatter(syslog_fmt)
237     # Never enable debug over syslog
238     syslog_handler.setLevel(logging.INFO)
239     root_logger.addHandler(syslog_handler)
240
241   if syslog != constants.SYSLOG_ONLY:
242     # this can fail, if the logging directories are not setup or we have
243     # a permisssion problem; in this case, it's best to log but ignore
244     # the error if stderr_logging is True, and if false we re-raise the
245     # exception since otherwise we could run but without any logs at all
246     try:
247       if console_logging:
248         logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"),
249                                       logfile)
250       else:
251         logfile_handler = _ReopenableLogHandler(logfile)
252
253       logfile_handler.setFormatter(formatter)
254       if debug:
255         logfile_handler.setLevel(logging.DEBUG)
256       else:
257         logfile_handler.setLevel(logging.INFO)
258       root_logger.addHandler(logfile_handler)
259       reopen_handlers.append(logfile_handler)
260     except EnvironmentError:
261       if stderr_logging or syslog == constants.SYSLOG_YES:
262         logging.exception("Failed to enable logging to file '%s'", logfile)
263       else:
264         # we need to re-raise the exception
265         raise
266
267   return compat.partial(_ReopenLogFiles, reopen_handlers)