log.py 8.26 KB
Newer Older
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#
#

# Copyright (C) 2006, 2007, 2010, 2011 Google Inc.
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful, but
# WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
# General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
# 02110-1301, USA.

"""Utility functions for logging.

"""

25
import os.path
26
27
28
29
import logging
import logging.handlers

from ganeti import constants
30
from ganeti import compat
31
32


33
34
class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
  """Log handler with ability to reopen log file on request.
35

36
37
  In combination with a SIGHUP handler this class can reopen the log file on
  user request.
38
39

  """
40
41
  def __init__(self, filename):
    """Initializes this class.
42

43
44
    @type filename: string
    @param filename: Path to logfile
45
46

    """
47
    logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
48

49
50
    assert self.encoding is None, "Encoding not supported for logging"
    assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
51

52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
    self._reopen = False

  def shouldRollover(self, _): # pylint: disable-msg=C0103
    """Determine whether log file should be reopened.

    """
    return self._reopen or not self.stream

  def doRollover(self): # pylint: disable-msg=C0103
    """Reopens the log file.

    """
    if self.stream:
      self.stream.flush()
      self.stream.close()
      self.stream = None

    # Reopen file
    # TODO: Handle errors?
    self.stream = open(self.baseFilename, "a")

  def RequestReopen(self):
    """Register a request to reopen the file.

    The file will be reopened before writing the next log record.

    """
    self._reopen = True


def _LogErrorsToConsole(base):
  """Create wrapper class writing errors to console.

  This needs to be in a function for unittesting.

  """
  class wrapped(base): # pylint: disable-msg=C0103
    """Log handler that doesn't fallback to stderr.

    When an error occurs while writing on the logfile, logging.FileHandler
    tries to log on stderr. This doesn't work in Ganeti since stderr is
    redirected to a logfile. This class avoids failures by reporting errors to
94
95
96
    /dev/console.

    """
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
    def __init__(self, console, *args, **kwargs):
      """Initializes this class.

      @type console: file-like object or None
      @param console: Open file-like object for console

      """
      base.__init__(self, *args, **kwargs)
      assert not hasattr(self, "_console")
      self._console = console

    def handleError(self, record): # pylint: disable-msg=C0103
      """Handle errors which occur during an emit() call.

      Try to handle errors with FileHandler method, if it fails write to
      /dev/console.

      """
115
      try:
116
        base.handleError(record)
117
      except Exception: # pylint: disable-msg=W0703
118
119
120
121
122
123
124
125
126
127
128
129
130
131
        if self._console:
          try:
            # Ignore warning about "self.format", pylint: disable-msg=E1101
            self._console.write("Cannot log message:\n%s\n" %
                                self.format(record))
          except Exception: # pylint: disable-msg=W0703
            # Log handler tried everything it could, now just give up
            pass

  return wrapped


#: Custom log handler for writing to console with a reopenable handler
_LogHandler = _LogErrorsToConsole(_ReopenableLogHandler)
132
133


134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
def _GetLogFormatter(program, multithreaded, debug, syslog):
  """Build log formatter.

  @param program: Program name
  @param multithreaded: Whether to add thread name to log messages
  @param debug: Whether to enable debug messages
  @param syslog: Whether the formatter will be used for syslog

  """
  parts = []

  if syslog:
    parts.append(program + "[%(process)d]:")
  else:
    parts.append("%(asctime)s: " + program + " pid=%(process)d")

  if multithreaded:
    if syslog:
      parts.append(" (%(threadName)s)")
    else:
      parts.append("/%(threadName)s")

  # Add debug info for non-syslog loggers
  if debug and not syslog:
    parts.append(" %(module)s:%(lineno)s")

  # Ses, we do want the textual level, as remote syslog will probably lose the
  # error level, and it's easier to grep for it.
  parts.append(" %(levelname)s %(message)s")

  return logging.Formatter("".join(parts))


167
168
169
170
171
172
173
174
def _ReopenLogFiles(handlers):
  """Wrapper for reopening all log handler's files in a sequence.

  """
  for handler in handlers:
    handler.RequestReopen()


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
179
180
181
  """Configures the logging module.

  @type logfile: str
  @param logfile: the filename to which we should log
182
183
  @type program: str
  @param program: the name under which we should log messages
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
  @type debug: integer
  @param debug: if greater than zero, enable debug messages, otherwise
      only those at C{INFO} and above level
  @type stderr_logging: boolean
  @param stderr_logging: whether we should also log to the standard error
  @type multithreaded: boolean
  @param multithreaded: if True, will add the thread name to the log file
  @type syslog: string
  @param syslog: one of 'no', 'yes', 'only':
      - if no, syslog is not used
      - if yes, syslog is used (in addition to file-logging)
      - if only, only syslog is used
  @type console_logging: boolean
  @param console_logging: if True, will use a FileHandler which falls back to
      the system console if logging fails
199
200
  @type root_logger: logging.Logger
  @param root_logger: Root logger to use (for unittests)
201
202
  @raise EnvironmentError: if we can't open the log file and
      syslog/stderr logging is disabled
203
204
  @rtype: callable
  @return: Function reopening all open log files when called
205
206

  """
207
208
209
210
  progname = os.path.basename(program)

  formatter = _GetLogFormatter(progname, multithreaded, debug, False)
  syslog_fmt = _GetLogFormatter(progname, multithreaded, debug, True)
211

212
213
214
215
  reopen_handlers = []

  if root_logger is None:
    root_logger = logging.getLogger("")
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
  root_logger.setLevel(logging.NOTSET)

  # Remove all previously setup handlers
  for handler in root_logger.handlers:
    handler.close()
    root_logger.removeHandler(handler)

  if stderr_logging:
    stderr_handler = logging.StreamHandler()
    stderr_handler.setFormatter(formatter)
    if debug:
      stderr_handler.setLevel(logging.NOTSET)
    else:
      stderr_handler.setLevel(logging.CRITICAL)
    root_logger.addHandler(stderr_handler)

  if syslog in (constants.SYSLOG_YES, constants.SYSLOG_ONLY):
    facility = logging.handlers.SysLogHandler.LOG_DAEMON
    syslog_handler = logging.handlers.SysLogHandler(constants.SYSLOG_SOCKET,
                                                    facility)
236
    syslog_handler.setFormatter(syslog_fmt)
237
238
239
240
241
242
243
244
245
246
247
    # Never enable debug over syslog
    syslog_handler.setLevel(logging.INFO)
    root_logger.addHandler(syslog_handler)

  if syslog != constants.SYSLOG_ONLY:
    # this can fail, if the logging directories are not setup or we have
    # a permisssion problem; in this case, it's best to log but ignore
    # the error if stderr_logging is True, and if false we re-raise the
    # exception since otherwise we could run but without any logs at all
    try:
      if console_logging:
248
        logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"), logfile)
249
      else:
250
        logfile_handler = _ReopenableLogHandler(logfile)
251
252
253
254
255
256
    except EnvironmentError:
      if stderr_logging or syslog == constants.SYSLOG_YES:
        logging.exception("Failed to enable logging to file '%s'", logfile)
      else:
        # we need to re-raise the exception
        raise
257
258
259
260
261
262
263

    logfile_handler.setFormatter(formatter)
    if debug:
      logfile_handler.setLevel(logging.DEBUG)
    else:
      logfile_handler.setLevel(logging.INFO)
    root_logger.addHandler(logfile_handler)
264
265
266
267

    reopen_handlers.append(logfile_handler)

  return compat.partial(_ReopenLogFiles, reopen_handlers)