Revision b6fa9a44

b/Makefile.am
504 504
	test/ganeti.utils.filelock_unittest.py \
505 505
	test/ganeti.utils.hash_unittest.py \
506 506
	test/ganeti.utils.io_unittest.py \
507
	test/ganeti.utils.log_unittest.py \
507 508
	test/ganeti.utils.mlock_unittest.py \
508 509
	test/ganeti.utils.nodesetup_unittest.py \
509 510
	test/ganeti.utils.process_unittest.py \
b/lib/utils/log.py
28 28
from ganeti import constants
29 29

  
30 30

  
31
class LogFileHandler(logging.FileHandler):
32
  """Log handler that doesn't fallback to stderr.
31
class _ReopenableLogHandler(logging.handlers.BaseRotatingHandler):
32
  """Log handler with ability to reopen log file on request.
33 33

  
34
  When an error occurs while writing on the logfile, logging.FileHandler tries
35
  to log on stderr. This doesn't work in ganeti since stderr is redirected to
36
  the logfile. This class avoids failures reporting errors to /dev/console.
34
  In combination with a SIGHUP handler this class can reopen the log file on
35
  user request.
37 36

  
38 37
  """
39
  def __init__(self, filename, mode="a", encoding=None):
40
    """Open the specified file and use it as the stream for logging.
38
  def __init__(self, filename):
39
    """Initializes this class.
41 40

  
42
    Also open /dev/console to report errors while logging.
41
    @type filename: string
42
    @param filename: Path to logfile
43 43

  
44 44
    """
45
    logging.FileHandler.__init__(self, filename, mode, encoding)
46
    self.console = open(constants.DEV_CONSOLE, "a")
45
    logging.handlers.BaseRotatingHandler.__init__(self, filename, "a")
47 46

  
48
  def handleError(self, record): # pylint: disable-msg=C0103
49
    """Handle errors which occur during an emit() call.
47
    assert self.encoding is None, "Encoding not supported for logging"
48
    assert not hasattr(self, "_reopen"), "Base class has '_reopen' attribute"
50 49

  
51
    Try to handle errors with FileHandler method, if it fails write to
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
52 92
    /dev/console.
53 93

  
54 94
    """
55
    try:
56
      logging.FileHandler.handleError(self, record)
57
    except Exception: # pylint: disable-msg=W0703
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
      """
58 113
      try:
59
        self.console.write("Cannot log message:\n%s\n" % self.format(record))
114
        base.handleError(record)
60 115
      except Exception: # pylint: disable-msg=W0703
61
        # Log handler tried everything it could, now just give up
62
        pass
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)
63 130

  
64 131

  
65 132
def SetupLogging(logfile, debug=0, stderr_logging=False, program="",
......
138 205
    # exception since otherwise we could run but without any logs at all
139 206
    try:
140 207
      if console_logging:
141
        logfile_handler = LogFileHandler(logfile)
208
        logfile_handler = _LogHandler(open(constants.DEV_CONSOLE, "a"), logfile)
142 209
      else:
143
        logfile_handler = logging.FileHandler(logfile)
210
        logfile_handler = _ReopenableLogHandler(logfile)
211

  
144 212
      logfile_handler.setFormatter(formatter)
145 213
      if debug:
146 214
        logfile_handler.setLevel(logging.DEBUG)
b/test/ganeti.utils.log_unittest.py
1
#!/usr/bin/python
2
#
3

  
4
# Copyright (C) 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

  
22
"""Script for testing ganeti.utils.log"""
23

  
24
import os
25
import unittest
26
import logging
27
import tempfile
28

  
29
from ganeti import constants
30
from ganeti import errors
31
from ganeti import utils
32

  
33
import testutils
34

  
35

  
36
class TestLogHandler(unittest.TestCase):
37
  def test(self):
38
    tmpfile = tempfile.NamedTemporaryFile()
39

  
40
    handler = utils.log._ReopenableLogHandler(tmpfile.name)
41
    handler.setFormatter(logging.Formatter("%(asctime)s: %(message)s"))
42

  
43
    logger = logging.Logger("TestLogger")
44
    logger.addHandler(handler)
45
    self.assertEqual(len(logger.handlers), 1)
46

  
47
    logger.error("Test message ERROR")
48
    logger.info("Test message INFO")
49

  
50
    logger.removeHandler(handler)
51
    self.assertFalse(logger.handlers)
52
    handler.close()
53

  
54
    self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 2)
55

  
56
  def testReopen(self):
57
    tmpfile = tempfile.NamedTemporaryFile()
58
    tmpfile2 = tempfile.NamedTemporaryFile()
59

  
60
    handler = utils.log._ReopenableLogHandler(tmpfile.name)
61

  
62
    self.assertFalse(utils.ReadFile(tmpfile.name))
63
    self.assertFalse(utils.ReadFile(tmpfile2.name))
64

  
65
    logger = logging.Logger("TestLoggerReopen")
66
    logger.addHandler(handler)
67

  
68
    for _ in range(3):
69
      logger.error("Test message ERROR")
70
    handler.flush()
71
    self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 3)
72
    before_id = utils.GetFileID(tmpfile.name)
73

  
74
    handler.RequestReopen()
75
    self.assertTrue(handler._reopen)
76
    self.assertTrue(utils.VerifyFileID(utils.GetFileID(tmpfile.name),
77
                                       before_id))
78

  
79
    # Rename only after requesting reopen
80
    os.rename(tmpfile.name, tmpfile2.name)
81
    assert not os.path.exists(tmpfile.name)
82

  
83
    # Write another message, should reopen
84
    for _ in range(4):
85
      logger.info("Test message INFO")
86
      self.assertFalse(utils.VerifyFileID(utils.GetFileID(tmpfile.name),
87
                                          before_id))
88

  
89
    logger.removeHandler(handler)
90
    self.assertFalse(logger.handlers)
91
    handler.close()
92

  
93
    self.assertEqual(len(utils.ReadFile(tmpfile.name).splitlines()), 4)
94
    self.assertEqual(len(utils.ReadFile(tmpfile2.name).splitlines()), 3)
95

  
96
  def testConsole(self):
97
    for (console, check) in [(None, False),
98
                             (tempfile.NamedTemporaryFile(), True),
99
                             (self._FailingFile(os.devnull), False)]:
100
      # Create a handler which will fail when handling errors
101
      cls = utils.log._LogErrorsToConsole(self._FailingHandler)
102

  
103
      # Instantiate handler with file which will fail when writing,
104
      # provoking a write to the console
105
      handler = cls(console, self._FailingFile(os.devnull))
106

  
107
      logger = logging.Logger("TestLogger")
108
      logger.addHandler(handler)
109
      self.assertEqual(len(logger.handlers), 1)
110

  
111
      # Provoke write
112
      logger.error("Test message ERROR")
113

  
114
      # Take everything apart
115
      logger.removeHandler(handler)
116
      self.assertFalse(logger.handlers)
117
      handler.close()
118

  
119
      if console and check:
120
        console.flush()
121

  
122
        # Check console output
123
        consout = utils.ReadFile(console.name)
124
        self.assertTrue("Cannot log message" in consout)
125
        self.assertTrue("Test message ERROR" in consout)
126

  
127
  class _FailingFile(file):
128
    def write(self, _):
129
      raise Exception
130

  
131
  class _FailingHandler(logging.StreamHandler):
132
    def handleError(self, _):
133
      raise Exception
134

  
135

  
136
if __name__ == "__main__":
137
  testutils.GanetiTestProgram()

Also available in: Unified diff