blob: 5cf8ed6a13021b5adf7b02ddb57f71630af0832c [file] [log] [blame]
Matthew Treinish0db53772013-07-26 10:39:35 -04001# vim: tabstop=4 shiftwidth=4 softtabstop=4
2
3# Copyright 2011 OpenStack Foundation.
4# Copyright 2010 United States Government as represented by the
5# Administrator of the National Aeronautics and Space Administration.
6# All Rights Reserved.
7#
8# Licensed under the Apache License, Version 2.0 (the "License"); you may
9# not use this file except in compliance with the License. You may obtain
10# a copy of the License at
11#
12# http://www.apache.org/licenses/LICENSE-2.0
13#
14# Unless required by applicable law or agreed to in writing, software
15# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
16# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
17# License for the specific language governing permissions and limitations
18# under the License.
19
20"""Openstack logging handler.
21
22This module adds to logging functionality by adding the option to specify
23a context object when calling the various log methods. If the context object
24is not specified, default formatting is used. Additionally, an instance uuid
25may be passed as part of the log message, which is intended to make it easier
26for admins to find messages related to a specific instance.
27
28It also allows setting of formatting information through conf.
29
30"""
31
32import inspect
33import itertools
34import logging
35import logging.config
36import logging.handlers
37import os
38import sys
39import traceback
40
41from oslo.config import cfg
Matthew Treinishf45528a2013-10-24 20:12:28 +000042import six
Matthew Treinish0db53772013-07-26 10:39:35 -040043from six import moves
44
45from tempest.openstack.common.gettextutils import _ # noqa
46from tempest.openstack.common import importutils
47from tempest.openstack.common import jsonutils
48from tempest.openstack.common import local
49
50
51_DEFAULT_LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S"
52
53common_cli_opts = [
54 cfg.BoolOpt('debug',
55 short='d',
56 default=False,
57 help='Print debugging output (set logging level to '
58 'DEBUG instead of default WARNING level).'),
59 cfg.BoolOpt('verbose',
60 short='v',
61 default=False,
62 help='Print more verbose output (set logging level to '
63 'INFO instead of default WARNING level).'),
64]
65
66logging_cli_opts = [
67 cfg.StrOpt('log-config',
68 metavar='PATH',
69 help='If this option is specified, the logging configuration '
70 'file specified is used and overrides any other logging '
71 'options specified. Please see the Python logging module '
72 'documentation for details on logging configuration '
73 'files.'),
74 cfg.StrOpt('log-format',
75 default=None,
76 metavar='FORMAT',
77 help='DEPRECATED. '
78 'A logging.Formatter log message format string which may '
79 'use any of the available logging.LogRecord attributes. '
80 'This option is deprecated. Please use '
81 'logging_context_format_string and '
82 'logging_default_format_string instead.'),
83 cfg.StrOpt('log-date-format',
84 default=_DEFAULT_LOG_DATE_FORMAT,
85 metavar='DATE_FORMAT',
86 help='Format string for %%(asctime)s in log records. '
87 'Default: %(default)s'),
88 cfg.StrOpt('log-file',
89 metavar='PATH',
90 deprecated_name='logfile',
91 help='(Optional) Name of log file to output to. '
92 'If no default is set, logging will go to stdout.'),
93 cfg.StrOpt('log-dir',
94 deprecated_name='logdir',
95 help='(Optional) The base directory used for relative '
96 '--log-file paths'),
97 cfg.BoolOpt('use-syslog',
98 default=False,
99 help='Use syslog for logging.'),
100 cfg.StrOpt('syslog-log-facility',
101 default='LOG_USER',
102 help='syslog facility to receive log lines')
103]
104
105generic_log_opts = [
106 cfg.BoolOpt('use_stderr',
107 default=True,
108 help='Log output to standard error')
109]
110
111log_opts = [
112 cfg.StrOpt('logging_context_format_string',
113 default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
114 '%(name)s [%(request_id)s %(user)s %(tenant)s] '
115 '%(instance)s%(message)s',
116 help='format string to use for log messages with context'),
117 cfg.StrOpt('logging_default_format_string',
118 default='%(asctime)s.%(msecs)03d %(process)d %(levelname)s '
119 '%(name)s [-] %(instance)s%(message)s',
120 help='format string to use for log messages without context'),
121 cfg.StrOpt('logging_debug_format_suffix',
122 default='%(funcName)s %(pathname)s:%(lineno)d',
123 help='data to append to log format when level is DEBUG'),
124 cfg.StrOpt('logging_exception_prefix',
125 default='%(asctime)s.%(msecs)03d %(process)d TRACE %(name)s '
126 '%(instance)s',
127 help='prefix each line of exception output with this format'),
128 cfg.ListOpt('default_log_levels',
129 default=[
130 'amqplib=WARN',
131 'sqlalchemy=WARN',
132 'boto=WARN',
133 'suds=INFO',
134 'keystone=INFO',
Matthew Treinish0db53772013-07-26 10:39:35 -0400135 ],
136 help='list of logger=LEVEL pairs'),
137 cfg.BoolOpt('publish_errors',
138 default=False,
139 help='publish error events'),
140 cfg.BoolOpt('fatal_deprecations',
141 default=False,
142 help='make deprecations fatal'),
143
144 # NOTE(mikal): there are two options here because sometimes we are handed
145 # a full instance (and could include more information), and other times we
146 # are just handed a UUID for the instance.
147 cfg.StrOpt('instance_format',
148 default='[instance: %(uuid)s] ',
149 help='If an instance is passed with the log message, format '
150 'it like this'),
151 cfg.StrOpt('instance_uuid_format',
152 default='[instance: %(uuid)s] ',
153 help='If an instance UUID is passed with the log message, '
154 'format it like this'),
155]
156
157CONF = cfg.CONF
158CONF.register_cli_opts(common_cli_opts)
159CONF.register_cli_opts(logging_cli_opts)
160CONF.register_opts(generic_log_opts)
161CONF.register_opts(log_opts)
162
163# our new audit level
164# NOTE(jkoelker) Since we synthesized an audit level, make the logging
165# module aware of it so it acts like other levels.
166logging.AUDIT = logging.INFO + 1
167logging.addLevelName(logging.AUDIT, 'AUDIT')
168
169
170try:
171 NullHandler = logging.NullHandler
172except AttributeError: # NOTE(jkoelker) NullHandler added in Python 2.7
173 class NullHandler(logging.Handler):
174 def handle(self, record):
175 pass
176
177 def emit(self, record):
178 pass
179
180 def createLock(self):
181 self.lock = None
182
183
184def _dictify_context(context):
185 if context is None:
186 return None
187 if not isinstance(context, dict) and getattr(context, 'to_dict', None):
188 context = context.to_dict()
189 return context
190
191
192def _get_binary_name():
193 return os.path.basename(inspect.stack()[-1][1])
194
195
196def _get_log_file_path(binary=None):
197 logfile = CONF.log_file
198 logdir = CONF.log_dir
199
200 if logfile and not logdir:
201 return logfile
202
203 if logfile and logdir:
204 return os.path.join(logdir, logfile)
205
206 if logdir:
207 binary = binary or _get_binary_name()
208 return '%s.log' % (os.path.join(logdir, binary),)
209
Matthew Treinishf45528a2013-10-24 20:12:28 +0000210 return None
211
Matthew Treinish0db53772013-07-26 10:39:35 -0400212
213class BaseLoggerAdapter(logging.LoggerAdapter):
214
215 def audit(self, msg, *args, **kwargs):
216 self.log(logging.AUDIT, msg, *args, **kwargs)
217
218
219class LazyAdapter(BaseLoggerAdapter):
220 def __init__(self, name='unknown', version='unknown'):
221 self._logger = None
222 self.extra = {}
223 self.name = name
224 self.version = version
225
226 @property
227 def logger(self):
228 if not self._logger:
229 self._logger = getLogger(self.name, self.version)
230 return self._logger
231
232
233class ContextAdapter(BaseLoggerAdapter):
234 warn = logging.LoggerAdapter.warning
235
236 def __init__(self, logger, project_name, version_string):
237 self.logger = logger
238 self.project = project_name
239 self.version = version_string
240
241 @property
242 def handlers(self):
243 return self.logger.handlers
244
245 def deprecated(self, msg, *args, **kwargs):
246 stdmsg = _("Deprecated: %s") % msg
247 if CONF.fatal_deprecations:
248 self.critical(stdmsg, *args, **kwargs)
249 raise DeprecatedConfig(msg=stdmsg)
250 else:
251 self.warn(stdmsg, *args, **kwargs)
252
253 def process(self, msg, kwargs):
Matthew Treinishf45528a2013-10-24 20:12:28 +0000254 # NOTE(mrodden): catch any Message/other object and
255 # coerce to unicode before they can get
256 # to the python logging and possibly
257 # cause string encoding trouble
258 if not isinstance(msg, six.string_types):
259 msg = six.text_type(msg)
260
Matthew Treinish0db53772013-07-26 10:39:35 -0400261 if 'extra' not in kwargs:
262 kwargs['extra'] = {}
263 extra = kwargs['extra']
264
265 context = kwargs.pop('context', None)
266 if not context:
267 context = getattr(local.store, 'context', None)
268 if context:
269 extra.update(_dictify_context(context))
270
271 instance = kwargs.pop('instance', None)
Matthew Treinishf45528a2013-10-24 20:12:28 +0000272 instance_uuid = (extra.get('instance_uuid', None) or
273 kwargs.pop('instance_uuid', None))
Matthew Treinish0db53772013-07-26 10:39:35 -0400274 instance_extra = ''
275 if instance:
276 instance_extra = CONF.instance_format % instance
Matthew Treinishf45528a2013-10-24 20:12:28 +0000277 elif instance_uuid:
278 instance_extra = (CONF.instance_uuid_format
279 % {'uuid': instance_uuid})
Matthew Treinish0db53772013-07-26 10:39:35 -0400280 extra.update({'instance': instance_extra})
281
282 extra.update({"project": self.project})
283 extra.update({"version": self.version})
284 extra['extra'] = extra.copy()
285 return msg, kwargs
286
287
288class JSONFormatter(logging.Formatter):
289 def __init__(self, fmt=None, datefmt=None):
290 # NOTE(jkoelker) we ignore the fmt argument, but its still there
291 # since logging.config.fileConfig passes it.
292 self.datefmt = datefmt
293
294 def formatException(self, ei, strip_newlines=True):
295 lines = traceback.format_exception(*ei)
296 if strip_newlines:
297 lines = [itertools.ifilter(
298 lambda x: x,
299 line.rstrip().splitlines()) for line in lines]
300 lines = list(itertools.chain(*lines))
301 return lines
302
303 def format(self, record):
304 message = {'message': record.getMessage(),
305 'asctime': self.formatTime(record, self.datefmt),
306 'name': record.name,
307 'msg': record.msg,
308 'args': record.args,
309 'levelname': record.levelname,
310 'levelno': record.levelno,
311 'pathname': record.pathname,
312 'filename': record.filename,
313 'module': record.module,
314 'lineno': record.lineno,
315 'funcname': record.funcName,
316 'created': record.created,
317 'msecs': record.msecs,
318 'relative_created': record.relativeCreated,
319 'thread': record.thread,
320 'thread_name': record.threadName,
321 'process_name': record.processName,
322 'process': record.process,
323 'traceback': None}
324
325 if hasattr(record, 'extra'):
326 message['extra'] = record.extra
327
328 if record.exc_info:
329 message['traceback'] = self.formatException(record.exc_info)
330
331 return jsonutils.dumps(message)
332
333
334def _create_logging_excepthook(product_name):
335 def logging_excepthook(type, value, tb):
336 extra = {}
337 if CONF.verbose:
338 extra['exc_info'] = (type, value, tb)
339 getLogger(product_name).critical(str(value), **extra)
340 return logging_excepthook
341
342
343class LogConfigError(Exception):
344
345 message = _('Error loading logging config %(log_config)s: %(err_msg)s')
346
347 def __init__(self, log_config, err_msg):
348 self.log_config = log_config
349 self.err_msg = err_msg
350
351 def __str__(self):
352 return self.message % dict(log_config=self.log_config,
353 err_msg=self.err_msg)
354
355
356def _load_log_config(log_config):
357 try:
358 logging.config.fileConfig(log_config)
359 except moves.configparser.Error as exc:
360 raise LogConfigError(log_config, str(exc))
361
362
363def setup(product_name):
364 """Setup logging."""
365 if CONF.log_config:
366 _load_log_config(CONF.log_config)
367 else:
368 _setup_logging_from_conf()
369 sys.excepthook = _create_logging_excepthook(product_name)
370
371
372def set_defaults(logging_context_format_string):
373 cfg.set_defaults(log_opts,
374 logging_context_format_string=
375 logging_context_format_string)
376
377
378def _find_facility_from_conf():
379 facility_names = logging.handlers.SysLogHandler.facility_names
380 facility = getattr(logging.handlers.SysLogHandler,
381 CONF.syslog_log_facility,
382 None)
383
384 if facility is None and CONF.syslog_log_facility in facility_names:
385 facility = facility_names.get(CONF.syslog_log_facility)
386
387 if facility is None:
388 valid_facilities = facility_names.keys()
389 consts = ['LOG_AUTH', 'LOG_AUTHPRIV', 'LOG_CRON', 'LOG_DAEMON',
390 'LOG_FTP', 'LOG_KERN', 'LOG_LPR', 'LOG_MAIL', 'LOG_NEWS',
391 'LOG_AUTH', 'LOG_SYSLOG', 'LOG_USER', 'LOG_UUCP',
392 'LOG_LOCAL0', 'LOG_LOCAL1', 'LOG_LOCAL2', 'LOG_LOCAL3',
393 'LOG_LOCAL4', 'LOG_LOCAL5', 'LOG_LOCAL6', 'LOG_LOCAL7']
394 valid_facilities.extend(consts)
395 raise TypeError(_('syslog facility must be one of: %s') %
396 ', '.join("'%s'" % fac
397 for fac in valid_facilities))
398
399 return facility
400
401
402def _setup_logging_from_conf():
403 log_root = getLogger(None).logger
404 for handler in log_root.handlers:
405 log_root.removeHandler(handler)
406
407 if CONF.use_syslog:
408 facility = _find_facility_from_conf()
409 syslog = logging.handlers.SysLogHandler(address='/dev/log',
410 facility=facility)
411 log_root.addHandler(syslog)
412
413 logpath = _get_log_file_path()
414 if logpath:
415 filelog = logging.handlers.WatchedFileHandler(logpath)
416 log_root.addHandler(filelog)
417
418 if CONF.use_stderr:
419 streamlog = ColorHandler()
420 log_root.addHandler(streamlog)
421
422 elif not CONF.log_file:
423 # pass sys.stdout as a positional argument
424 # python2.6 calls the argument strm, in 2.7 it's stream
425 streamlog = logging.StreamHandler(sys.stdout)
426 log_root.addHandler(streamlog)
427
428 if CONF.publish_errors:
429 handler = importutils.import_object(
430 "tempest.openstack.common.log_handler.PublishErrorsHandler",
431 logging.ERROR)
432 log_root.addHandler(handler)
433
434 datefmt = CONF.log_date_format
435 for handler in log_root.handlers:
436 # NOTE(alaski): CONF.log_format overrides everything currently. This
437 # should be deprecated in favor of context aware formatting.
438 if CONF.log_format:
439 handler.setFormatter(logging.Formatter(fmt=CONF.log_format,
440 datefmt=datefmt))
441 log_root.info('Deprecated: log_format is now deprecated and will '
442 'be removed in the next release')
443 else:
444 handler.setFormatter(ContextFormatter(datefmt=datefmt))
445
446 if CONF.debug:
447 log_root.setLevel(logging.DEBUG)
448 elif CONF.verbose:
449 log_root.setLevel(logging.INFO)
450 else:
451 log_root.setLevel(logging.WARNING)
452
453 for pair in CONF.default_log_levels:
454 mod, _sep, level_name = pair.partition('=')
455 level = logging.getLevelName(level_name)
456 logger = logging.getLogger(mod)
457 logger.setLevel(level)
458
459_loggers = {}
460
461
462def getLogger(name='unknown', version='unknown'):
463 if name not in _loggers:
464 _loggers[name] = ContextAdapter(logging.getLogger(name),
465 name,
466 version)
467 return _loggers[name]
468
469
470def getLazyLogger(name='unknown', version='unknown'):
471 """Returns lazy logger.
472
473 Creates a pass-through logger that does not create the real logger
474 until it is really needed and delegates all calls to the real logger
475 once it is created.
476 """
477 return LazyAdapter(name, version)
478
479
480class WritableLogger(object):
481 """A thin wrapper that responds to `write` and logs."""
482
483 def __init__(self, logger, level=logging.INFO):
484 self.logger = logger
485 self.level = level
486
487 def write(self, msg):
488 self.logger.log(self.level, msg)
489
490
491class ContextFormatter(logging.Formatter):
492 """A context.RequestContext aware formatter configured through flags.
493
494 The flags used to set format strings are: logging_context_format_string
495 and logging_default_format_string. You can also specify
496 logging_debug_format_suffix to append extra formatting if the log level is
497 debug.
498
499 For information about what variables are available for the formatter see:
500 http://docs.python.org/library/logging.html#formatter
501
502 """
503
504 def format(self, record):
505 """Uses contextstring if request_id is set, otherwise default."""
506 # NOTE(sdague): default the fancier formating params
507 # to an empty string so we don't throw an exception if
508 # they get used
509 for key in ('instance', 'color'):
510 if key not in record.__dict__:
511 record.__dict__[key] = ''
512
513 if record.__dict__.get('request_id', None):
514 self._fmt = CONF.logging_context_format_string
515 else:
516 self._fmt = CONF.logging_default_format_string
517
518 if (record.levelno == logging.DEBUG and
519 CONF.logging_debug_format_suffix):
520 self._fmt += " " + CONF.logging_debug_format_suffix
521
522 # Cache this on the record, Logger will respect our formated copy
523 if record.exc_info:
524 record.exc_text = self.formatException(record.exc_info, record)
525 return logging.Formatter.format(self, record)
526
527 def formatException(self, exc_info, record=None):
528 """Format exception output with CONF.logging_exception_prefix."""
529 if not record:
530 return logging.Formatter.formatException(self, exc_info)
531
532 stringbuffer = moves.StringIO()
533 traceback.print_exception(exc_info[0], exc_info[1], exc_info[2],
534 None, stringbuffer)
535 lines = stringbuffer.getvalue().split('\n')
536 stringbuffer.close()
537
538 if CONF.logging_exception_prefix.find('%(asctime)') != -1:
539 record.asctime = self.formatTime(record, self.datefmt)
540
541 formatted_lines = []
542 for line in lines:
543 pl = CONF.logging_exception_prefix % record.__dict__
544 fl = '%s%s' % (pl, line)
545 formatted_lines.append(fl)
546 return '\n'.join(formatted_lines)
547
548
549class ColorHandler(logging.StreamHandler):
550 LEVEL_COLORS = {
551 logging.DEBUG: '\033[00;32m', # GREEN
552 logging.INFO: '\033[00;36m', # CYAN
553 logging.AUDIT: '\033[01;36m', # BOLD CYAN
554 logging.WARN: '\033[01;33m', # BOLD YELLOW
555 logging.ERROR: '\033[01;31m', # BOLD RED
556 logging.CRITICAL: '\033[01;31m', # BOLD RED
557 }
558
559 def format(self, record):
560 record.color = self.LEVEL_COLORS[record.levelno]
561 return logging.StreamHandler.format(self, record)
562
563
564class DeprecatedConfig(Exception):
565 message = _("Fatal call to deprecated config: %(msg)s")
566
567 def __init__(self, msg):
568 super(Exception, self).__init__(self.message % dict(msg=msg))