vlog: Ability to override the default log facility.
[cascardo/ovs.git] / python / ovs / vlog.py
1
2 # Copyright (c) 2011, 2012, 2013 Nicira, Inc.
3 #
4 # Licensed under the Apache License, Version 2.0 (the "License");
5 # you may not use this file except in compliance with the License.
6 # You may obtain a copy of the License at:
7 #
8 #     http://www.apache.org/licenses/LICENSE-2.0
9 #
10 # Unless required by applicable law or agreed to in writing, software
11 # distributed under the License is distributed on an "AS IS" BASIS,
12 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 # See the License for the specific language governing permissions and
14 # limitations under the License.
15
16 import datetime
17 import logging
18 import logging.handlers
19 import os
20 import re
21 import socket
22 import sys
23 import threading
24
25 import ovs.dirs
26 import ovs.unixctl
27 import ovs.util
28
29 DESTINATIONS = {"console": "info", "file": "info", "syslog": "info"}
30 PATTERNS = {
31     "console": "%D{%Y-%m-%dT%H:%M:%SZ}|%05N|%c%T|%p|%m",
32     "file": "%D{%Y-%m-%dT%H:%M:%S.###Z}|%05N|%c%T|%p|%m",
33     "syslog": "ovs|%05N|%c%T|%p|%m",
34 }
35 LEVELS = {
36     "dbg": logging.DEBUG,
37     "info": logging.INFO,
38     "warn": logging.WARNING,
39     "err": logging.ERROR,
40     "emer": logging.CRITICAL,
41     "off": logging.CRITICAL
42 }
43 FACILITIES = ['auth', 'authpriv', 'cron', 'daemon', 'ftp', 'kern', 'lpr',
44               'mail', 'news', 'syslog', 'user', 'uucp', 'local0', 'local1',
45               'local2', 'local3', 'local4', 'local5', 'local6', 'local7']
46 syslog_facility = "daemon"
47 syslog_handler = ''
48
49
50 def get_level(level_str):
51     return LEVELS.get(level_str.lower())
52
53
54 class Vlog:
55     __inited = False
56     __msg_num = 0
57     __start_time = 0
58     __mfl = {}  # Module -> destination -> level
59     __log_file = None
60     __file_handler = None
61     __log_patterns = PATTERNS
62
63     def __init__(self, name):
64         """Creates a new Vlog object representing a module called 'name'.  The
65         created Vlog object will do nothing until the Vlog.init() static method
66         is called.  Once called, no more Vlog objects may be created."""
67
68         assert not Vlog.__inited
69         self.name = name.lower()
70         if name not in Vlog.__mfl:
71             Vlog.__mfl[self.name] = DESTINATIONS.copy()
72
73     def __log(self, level, message, **kwargs):
74         if not Vlog.__inited:
75             return
76
77         level_num = LEVELS.get(level.lower(), logging.DEBUG)
78         msg_num = Vlog.__msg_num
79         Vlog.__msg_num += 1
80
81         for f, f_level in Vlog.__mfl[self.name].iteritems():
82             f_level = LEVELS.get(f_level, logging.CRITICAL)
83             if level_num >= f_level:
84                 msg = self._build_message(message, f, level, msg_num)
85                 logging.getLogger(f).log(level_num, msg, **kwargs)
86
87     def _build_message(self, message, destination, level, msg_num):
88         pattern = self.__log_patterns[destination]
89         tmp = pattern
90
91         tmp = self._format_time(tmp)
92
93         matches = re.findall("(%-?[0]?[0-9]?[AcmNnpPrtT])", tmp)
94         for m in matches:
95             if "A" in m:
96                 tmp = self._format_field(tmp, m, ovs.util.PROGRAM_NAME)
97             elif "c" in m:
98                 tmp = self._format_field(tmp, m, self.name)
99             elif "m" in m:
100                 tmp = self._format_field(tmp, m, message)
101             elif "N" in m:
102                 tmp = self._format_field(tmp, m, str(msg_num))
103             elif "n" in m:
104                 tmp = re.sub(m, "\n", tmp)
105             elif "p" in m:
106                 tmp = self._format_field(tmp, m, level.upper())
107             elif "P" in m:
108                 self._format_field(tmp, m, str(os.getpid()))
109             elif "r" in m:
110                 now = datetime.datetime.utcnow()
111                 delta = now - self.__start_time
112                 ms = delta.microseconds / 1000
113                 tmp = self._format_field(tmp, m, str(ms))
114             elif "t" in m:
115                 subprogram = threading.currentThread().getName()
116                 if subprogram == "MainThread":
117                     subprogram = "main"
118                 tmp = self._format_field(tmp, m, subprogram)
119             elif "T" in m:
120                 subprogram = threading.currentThread().getName()
121                 if not subprogram == "MainThread":
122                     subprogram = "({})".format(subprogram)
123                 else:
124                     subprogram = ""
125                 tmp = self._format_field(tmp, m, subprogram)
126         return tmp.strip()
127
128     def _format_field(self, tmp, match, replace):
129         formatting = re.compile("^%(0)?([1-9])?")
130         matches = formatting.match(match)
131         # Do we need to apply padding?
132         if not matches.group(1) and replace != "":
133             replace = replace.center(len(replace)+2)
134         # Does the field have a minimum width
135         if matches.group(2):
136             min_width = int(matches.group(2))
137             if len(replace) < min_width:
138                 replace = replace.center(min_width)
139         return re.sub(match, replace, tmp)
140
141     def _format_time(self, tmp):
142         date_regex = re.compile('(%(0?[1-9]?[dD])(\{(.*)\})?)')
143         match = date_regex.search(tmp)
144
145         if match is None:
146             return tmp
147
148         # UTC date or Local TZ?
149         if match.group(2) == "d":
150             now = datetime.datetime.now()
151         elif match.group(2) == "D":
152             now = datetime.datetime.utcnow()
153
154         # Custom format or ISO format?
155         if match.group(3):
156             time = datetime.date.strftime(now, match.group(4))
157             try:
158                 i = len(re.search("#+", match.group(4)).group(0))
159                 msec = '{0:0>{i}.{i}}'.format(str(now.microsecond / 1000), i=i)
160                 time = re.sub('#+', msec, time)
161             except AttributeError:
162                 pass
163         else:
164             time = datetime.datetime.isoformat(now.replace(microsecond=0))
165
166         return self._format_field(tmp, match.group(1), time)
167
168     def emer(self, message, **kwargs):
169         self.__log("EMER", message, **kwargs)
170
171     def err(self, message, **kwargs):
172         self.__log("ERR", message, **kwargs)
173
174     def warn(self, message, **kwargs):
175         self.__log("WARN", message, **kwargs)
176
177     def info(self, message, **kwargs):
178         self.__log("INFO", message, **kwargs)
179
180     def dbg(self, message, **kwargs):
181         self.__log("DBG", message, **kwargs)
182
183     def __is_enabled(self, level):
184         level = LEVELS.get(level.lower(), logging.DEBUG)
185         for f, f_level in Vlog.__mfl[self.name].iteritems():
186             f_level = LEVELS.get(f_level, logging.CRITICAL)
187             if level >= f_level:
188                 return True
189         return False
190
191     def emer_is_enabled(self):
192         return self.__is_enabled("EMER")
193
194     def err_is_enabled(self):
195         return self.__is_enabled("ERR")
196
197     def warn_is_enabled(self):
198         return self.__is_enabled("WARN")
199
200     def info_is_enabled(self):
201         return self.__is_enabled("INFO")
202
203     def dbg_is_enabled(self):
204         return self.__is_enabled("DBG")
205
206     def exception(self, message):
207         """Logs 'message' at ERR log level.  Includes a backtrace when in
208         exception context."""
209         self.err(message, exc_info=True)
210
211     @staticmethod
212     def init(log_file=None):
213         """Intializes the Vlog module.  Causes Vlog to write to 'log_file' if
214         not None.  Should be called after all Vlog objects have been created.
215         No logging will occur until this function is called."""
216
217         if Vlog.__inited:
218             return
219
220         Vlog.__inited = True
221         Vlog.__start_time = datetime.datetime.utcnow()
222         logging.raiseExceptions = False
223         Vlog.__log_file = log_file
224         for f in DESTINATIONS:
225             logger = logging.getLogger(f)
226             logger.setLevel(logging.DEBUG)
227
228             try:
229                 if f == "console":
230                     logger.addHandler(logging.StreamHandler(sys.stderr))
231                 elif f == "syslog":
232                     Vlog.add_syslog_handler()
233                 elif f == "file" and Vlog.__log_file:
234                     Vlog.__file_handler = logging.FileHandler(Vlog.__log_file)
235                     logger.addHandler(Vlog.__file_handler)
236             except (IOError, socket.error):
237                 logger.setLevel(logging.CRITICAL)
238
239         ovs.unixctl.command_register("vlog/reopen", "", 0, 0,
240                                      Vlog._unixctl_vlog_reopen, None)
241         ovs.unixctl.command_register("vlog/set", "spec", 1, sys.maxint,
242                                      Vlog._unixctl_vlog_set, None)
243         ovs.unixctl.command_register("vlog/list", "", 0, 0,
244                                      Vlog._unixctl_vlog_list, None)
245
246     @staticmethod
247     def set_level(module, destination, level):
248         """ Sets the log level of the 'module'-'destination' tuple to 'level'.
249         All three arguments are strings which are interpreted the same as
250         arguments to the --verbose flag.  Should be called after all Vlog
251         objects have already been created."""
252
253         module = module.lower()
254         destination = destination.lower()
255         level = level.lower()
256
257         if destination != "any" and destination not in DESTINATIONS:
258             return
259
260         if module != "any" and module not in Vlog.__mfl:
261             return
262
263         if level not in LEVELS:
264             return
265
266         if module == "any":
267             modules = Vlog.__mfl.keys()
268         else:
269             modules = [module]
270
271         if destination == "any":
272             destinations = DESTINATIONS.keys()
273         else:
274             destinations = [destination]
275
276         for m in modules:
277             for f in destinations:
278                 Vlog.__mfl[m][f] = level
279
280     @staticmethod
281     def set_pattern(destination, pattern):
282         """ Sets the log pattern of the 'destination' to 'pattern' """
283         destination = destination.lower()
284         Vlog.__log_patterns[destination] = pattern
285
286     @staticmethod
287     def add_syslog_handler(facility=None):
288         global syslog_facility, syslog_handler
289
290         # If handler is already added and there is no change in 'facility',
291         # there is nothing to do.
292         if (not facility or facility == syslog_facility) and syslog_handler:
293             return
294
295         if facility:
296             syslog_facility = facility
297
298         logger = logging.getLogger('syslog')
299         if syslog_handler:
300             logger.removeHandler(syslog_handler)
301         syslog_handler = logging.handlers.SysLogHandler(address="/dev/log",
302                                                     facility=syslog_facility)
303         logger.addHandler(syslog_handler)
304         return
305
306     @staticmethod
307     def set_levels_from_string(s):
308         module = None
309         level = None
310         destination = None
311
312         words = re.split('[ :]', s)
313         if words[0] == "pattern":
314             try:
315                 if words[1] in DESTINATIONS and words[2]:
316                     segments = [words[i] for i in range(2, len(words))]
317                     pattern = "".join(segments)
318                     Vlog.set_pattern(words[1], pattern)
319                     return
320                 else:
321                     return "Destination %s does not exist" % words[1]
322             except IndexError:
323                 return "Please supply a valid pattern and destination"
324         elif words[0] == "FACILITY":
325             if words[1] in FACILITIES:
326                 Vlog.add_syslog_handler(words[1])
327                 return
328             else:
329                 return "Facility %s is invalid" % words[1]
330
331         for word in [w.lower() for w in words]:
332             if word == "any":
333                 pass
334             elif word in DESTINATIONS:
335                 if destination:
336                     return "cannot specify multiple destinations"
337                 destination = word
338             elif word in LEVELS:
339                 if level:
340                     return "cannot specify multiple levels"
341                 level = word
342             elif word in Vlog.__mfl:
343                 if module:
344                     return "cannot specify multiple modules"
345                 module = word
346             else:
347                 return "no destination, level, or module \"%s\"" % word
348
349         Vlog.set_level(module or "any", destination or "any", level or "any")
350
351     @staticmethod
352     def get_levels():
353         lines = ["                 console    syslog    file\n",
354                  "                 -------    ------    ------\n"]
355         lines.extend(sorted(["%-16s  %4s       %4s       %4s\n"
356                              % (m,
357                                 Vlog.__mfl[m]["console"],
358                                 Vlog.__mfl[m]["syslog"],
359                                 Vlog.__mfl[m]["file"]) for m in Vlog.__mfl]))
360         return ''.join(lines)
361
362     @staticmethod
363     def reopen_log_file():
364         """Closes and then attempts to re-open the current log file.  (This is
365         useful just after log rotation, to ensure that the new log file starts
366         being used.)"""
367
368         if Vlog.__log_file:
369             logger = logging.getLogger("file")
370             logger.removeHandler(Vlog.__file_handler)
371             Vlog.__file_handler = logging.FileHandler(Vlog.__log_file)
372             logger.addHandler(Vlog.__file_handler)
373
374     @staticmethod
375     def _unixctl_vlog_reopen(conn, unused_argv, unused_aux):
376         if Vlog.__log_file:
377             Vlog.reopen_log_file()
378             conn.reply(None)
379         else:
380             conn.reply("Logging to file not configured")
381
382     @staticmethod
383     def _unixctl_vlog_set(conn, argv, unused_aux):
384         for arg in argv:
385             msg = Vlog.set_levels_from_string(arg)
386             if msg:
387                 conn.reply(msg)
388                 return
389         conn.reply(None)
390
391     @staticmethod
392     def _unixctl_vlog_list(conn, unused_argv, unused_aux):
393         conn.reply(Vlog.get_levels())
394
395
396 def add_args(parser):
397     """Adds vlog related options to 'parser', an ArgumentParser object.  The
398     resulting arguments parsed by 'parser' should be passed to handle_args."""
399
400     group = parser.add_argument_group(title="Logging Options")
401     group.add_argument("--log-file", nargs="?", const="default",
402                        help="Enables logging to a file.  Default log file"
403                        " is used if LOG_FILE is omitted.")
404     group.add_argument("-v", "--verbose", nargs="*",
405                        help="Sets logging levels, see ovs-vswitchd(8)."
406                        "  Defaults to dbg.")
407
408
409 def handle_args(args):
410     """ Handles command line arguments ('args') parsed by an ArgumentParser.
411     The ArgumentParser should have been primed by add_args().  Also takes care
412     of initializing the Vlog module."""
413
414     log_file = args.log_file
415     if log_file == "default":
416         log_file = "%s/%s.log" % (ovs.dirs.LOGDIR, ovs.util.PROGRAM_NAME)
417
418     if args.verbose is None:
419         args.verbose = []
420     elif args.verbose == []:
421         args.verbose = ["any:any:dbg"]
422
423     for verbose in args.verbose:
424         msg = Vlog.set_levels_from_string(verbose)
425         if msg:
426             ovs.util.ovs_fatal(0, "processing \"%s\": %s" % (verbose, msg))
427
428     Vlog.init(log_file)