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