summaryrefslogtreecommitdiffstats
path: root/ipapython/log_manager.py
diff options
context:
space:
mode:
authorJohn Dennis <jdennis@redhat.com>2011-11-15 14:12:53 -0500
committerMartin Kosek <mkosek@redhat.com>2011-11-23 09:35:44 +0100
commit730f1228a91ec9c6e575181807da2ab994a38071 (patch)
tree8d1a279d6dc4a30f78650b0445ed3381fd11d2dc /ipapython/log_manager.py
parente7a6d1055574d2dd892f414dbe993ee5782ab488 (diff)
downloadfreeipa-730f1228a91ec9c6e575181807da2ab994a38071.tar.gz
freeipa-730f1228a91ec9c6e575181807da2ab994a38071.tar.xz
freeipa-730f1228a91ec9c6e575181807da2ab994a38071.zip
ticket 2022 - Add log manager module
Diffstat (limited to 'ipapython/log_manager.py')
-rw-r--r--ipapython/log_manager.py1748
1 files changed, 1748 insertions, 0 deletions
diff --git a/ipapython/log_manager.py b/ipapython/log_manager.py
new file mode 100644
index 000000000..6fa5ec5a1
--- /dev/null
+++ b/ipapython/log_manager.py
@@ -0,0 +1,1748 @@
+# Authors: John Dennis <jdennis@redhat.com>
+#
+# Copyright (C) 2011 Red Hat
+# see file 'COPYING' for use and warranty information
+#
+# 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 3 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, see <http://www.gnu.org/licenses/>.
+
+'''
+
+Quick Start Guide For Using This Module
+=======================================
+
+This module implements a Log Manager class which wraps the Python
+logging module and provides some utility functions for use with
+logging. All logging operations should be done through the
+`LogManager` where available. *DO NOT create objects using the
+Python logging module, the log manager will be unaware of them.*
+
+This module was designed for ease of use while preserving advanced
+functionality and performance. You must perform the following steps.
+
+1. Import the log_manger module and instantiate *one* `LogManager`
+ instance for your application or library. The `LogManager` is
+ configured via `LogManager.configure()` whose values are
+ easily populated from command line options or a config file. You
+ can modify the configuration again at any point.
+
+2. Create one or more output handlers via
+ `LogManager.create_log_handlers()` an easy to use yet powerful
+ interface.
+
+3. In your code create loggers via `LogManager.get_logger()`. Since
+ loggers are normally bound to a class this method is optimized for
+ that case, all you need to do in the call ``__init__()`` is::
+
+ log_mgr.get_logger(self, True)
+
+ Then emitting messages is as simple as ``self.debug()`` or ``self.error()``
+
+Example:
+--------
+
+::
+
+ # Step 1, Create log manager and configure it
+ prog_name = 'my_app'
+ log_mgr = LogManager(prog_name)
+ log_mgr.configure(dict(verbose=True))
+
+ # Step 2, Create handlers
+ log_mgr.create_log_handlers([dict(name='my_app stdout',
+ stream=sys.stdout,
+ level=logging.INFO),
+ dict(name='my_app file',
+ filename='my_app.log',
+ level=logging.DEBUG)])
+
+ # Step 3, Create and use a logger in your code
+ class FooBar:
+ def __init__(self, name):
+ log_mgr.get_logger(self, True)
+ self.info("I'm alive! %s", name)
+
+ foobar = FooBar('Dr. Frankenstein')
+
+ # Dump the log manager state for illustration
+ print
+ print log_mgr
+
+
+Running the above code would produce::
+
+ <INFO>: I'm alive! Dr. Frankenstein
+
+ root_logger_name: my_app
+ configure_state: None
+ default_level: INFO
+ debug: False
+ verbose: True
+ number of loggers: 2
+ "my_app" [level=INFO]
+ "my_app.__main__.FooBar" [level=INFO]
+ number of handlers: 2
+ "my_app file" [level=DEBUG]
+ "my_app stdout" [level=INFO]
+ number of logger regexps: 0
+
+*Note, Steps 1 & 2 were broken out for expository purposes.* You can
+pass your handler configuration into `LogManager.configure()`. The above
+could have been simpler and more compact.::
+
+ # Step 1 & 2, Create log manager, and configure it and handlers
+ prog_name = 'my_app'
+ log_mgr = LogManager(prog_name)
+ log_mgr.configure(dict(verbose=True,
+ handlers = [dict(name='my_app stdout',
+ stream=sys.stdout,
+ level=logging.INFO),
+ dict(name='my_app file',
+ filename='my_app.log',
+ level=logging.DEBUG)])
+
+
+FAQ (Frequently Asked Questions)
+================================
+
+#. **Why is this better than logging.basicConfig? The short example
+ for the LogManager doesn't seem much different in complexity from
+ basicConfig?**
+
+ * You get independent logging namespaces. You can instantiate
+ multiple logging namespaces. If you use this module you'll be
+ isolated from other users of the Python logging module avoiding
+ conflicts.
+
+ * Creating and initializing loggers for classes is trivial. One
+ simple call creates the logger, configures it, and sets logging
+ methods on the class instance.
+
+ * You can easily configure individual loggers to different
+ levels. For example turn on debuging for just the part of the
+ code you're working on.
+
+ * The configuration is both simple and powerful. You get many more
+ options than with basicConfig.
+
+ * You can dynamically reset the logging configuration during
+ execution, you're not forced to live with the config established
+ during program initialization.
+
+ * The manager optimizes the use of the logging objects, you'll
+ spend less time executing pointless logging code for messages
+ that won't be emitted.
+
+ * You can see the state of all the logging objects in your
+ namespace from one centrally managed location.
+
+ * You can configure a LogManager to use the standard logging root
+ logger and get all the benefits of this API.
+
+#. **How do I turn on debug logging for a specific class without
+ affecting the rest of the logging configuration?**
+
+ Use a logger regular expression to bind a custom level to loggers
+ whose name matches the regexp. See `LogManager.configure()`
+ for details.
+
+ Lets say you want to set your Foo.Bar class to debug, then do
+ this::
+
+ log_mgr.configure(dict(logger_regexps=[(r'Foo\.Bar', 'debug')]))
+
+#. **I set the default_level but all my loggers are configured
+ with a higher level, what happened?**
+
+ You probably don't have any handlers defined at or below the
+ default_level. The level set on a logger will never be
+ lower than the lowest level handler available to that logger.
+
+#. **My logger's all have their level set to a huge integer, why?**
+
+ See above. Logger's will never have a level less than the level of
+ the handlers visible to the logger. If there are no handlers then
+ loggers can't output anything so their level is set to maxint.
+
+#. **I set the default_level but all the loggers are configured
+ at INFO or DEBUG, what happened?**
+
+ The verbose and debug config flags set the default_level to
+ INFO and DEBUG respectively as a convenience.
+
+#. **I'm not seeing messages output when I expect them to be, what's
+ wrong?**
+
+ For a message to be emitted the following 3 conditions must hold:
+
+ * Message level >= logger's level
+ * Message level >= handler's level
+ * The message was not elided by a filter
+
+ To verify the above conditions hold print out the log manager state
+ (e.g. print log_mgr). Locate your logger, what level is at? Locate
+ the handler you expected to see the message appear on, what level
+ is it?
+
+A General Discussion of Python Logging
+======================================
+
+The design of this module is driven by how the Python logging module
+works. The following discussion complements the Python Logging Howto,
+fills in some missing information and covers strategies for
+implementing different functionality along with the trade-offs
+involved.
+
+Understanding when & how log messages are emitted:
+--------------------------------------------------
+
+Loggers provide the application interface for logging. Every logger
+object has the following methods debug(), info(), warning(), error(),
+critical(), exception() and log() all of which can accept a format
+string and arguments. Applications generate logging messages by
+calling one of these methods to produce a formatted message.
+
+A logger's effective level is the first explicitly set level found
+when searching from the logger through it's ancestors terminating at
+the root logger. The root logger always has an explicit level
+(defaults to WARNING).
+
+For a message to be emitted by a handler the following must be true:
+
+The logger's effective level must >= message level and it must not
+be filtered by a filter attached to the logger, otherwise the
+message is discarded.
+
+If the message survives the logger check it is passed to a list of
+handlers. A handler will emit the message if the handler's level >=
+message level and its not filtered by a filter attached to the
+handler.
+
+The list of handlers is determined thusly: Each logger has a list of
+handlers (which may be empty). Starting with the logger the message
+was bound to the message is passed to each of it's handlers. Then
+the process repeats itself by traversing the chain of loggers
+through all of it's ancestors until it reaches the root logger. The
+logger traversal will be terminated if the propagate flag on a logger
+is False (by default propagate is True).
+
+Let's look at a hypothetical logger hierarchy (tree)::
+
+ A
+ / \\
+ B D
+ /
+ C
+
+
+There are 4 loggers and 3 handlers
+
+Loggers:
+
++-------+---------+---------+-----------+----------+
+|Logger | Level | Filters | Propagate | Handlers |
++=======+=========+=========+===========+==========+
+| A | WARNING | [] | False | [h1,h2] |
++-------+---------+---------+-----------+----------+
+| A.B | ERROR | [] | False | [h3] |
++-------+---------+---------+-----------+----------+
+| A.B.C | DEBUG | [] | True | |
++-------+---------+---------+-----------+----------+
+| A.D | | [] | True | |
++-------+---------+---------+-----------+----------+
+
+Handlers:
+
++---------+---------+---------+
+| Handler | Level | Filters |
++=========+=========+=========+
+| h1 | ERROR | [] |
++---------+---------+---------+
+| h2 | WARNING | [] |
++---------+---------+---------+
+| h3 | DEBUG | [] |
++---------+---------+---------+
+
+Each of the loggers and handlers have empty filter lists in this
+example thus the filter checks will always pass.
+
+If a debug message is posted logger A.B.C the following would
+happen. The effective level is determined. Since it does not have a
+level set it's parent (A.B) is examined which has ERROR set,
+therefore the effective level of A.B.C is ERROR. Processing
+immediately stops because the logger's level of ERROR does not
+permit debug messages.
+
+If an error message is posted on logger A.B.C it passes the logger
+level check and filter check therefore the message is passed along
+to the handlers. The list of handlers on A.B.C is empty so no
+handlers are called at this position in the logging hierarchy. Logger
+A.B.C's propagate flag is True so parent logger A.B handlers are
+invoked. Handler h3's level is DEBUG, it passes both the level and
+filter check thus h3 emits the message. Processing now stops because
+logger A.B's propagate flag is False.
+
+Now let's see what would happen if a warning message was posted on
+logger A.D. It's effective level is WARNING because logger A.D does
+not have a level set, it's only ancestor is logger A, the root
+logger which has a level of WARNING, thus logger's A.D effective
+level is WARNING. Logger A.D has no handlers, it's propagate flag is
+True so the message is passed to it's parent logger A, the root
+logger. Logger A has two handlers h1 and h2. The level of h1 is
+ERROR so the warning message is discarded by h1, nothing is emitted
+by h1. Next handler h2 is invoked, it's level is WARNING so it
+passes both the level check and the filter check, thus h2 emits the
+warning message.
+
+How to configure independent logging spaces:
+--------------------------------------------
+
+A common idiom is to hang all handlers off the root logger and set
+the root loggers level to the desired verbosity. But this simplistic
+approach runs afoul of several problems, in particular who controls
+logging (accomplished by configuring the root logger). The usual
+advice is to check and see if the root logger has any handlers set,
+if so someone before you has configured logging and you should
+inherit their configuration, all you do is add your own loggers
+without any explicitly set level. If the root logger doesn't have
+handlers set then you go ahead and configure the root logger to your
+preference. The idea here is if your code is being loaded by another
+application you want to defer to that applications logging
+configuration but if your code is running stand-alone you need to
+set up logging yourself.
+
+But sometimes your code really wants it's own logging configuration
+managed only by yourself completely independent of any logging
+configuration by someone who may have loaded your code. Even if you
+code is not designed to be loaded as a package or module you may be
+faced with this problem. A trivial example of this is running your
+code under a unit test framework which itself uses the logging
+facility (remember there is only ever one root logger in any Python
+process).
+
+Fortunately there is a simple way to accommodate this. All you need
+to do is create a "fake" root in the logging hierarchy which belongs
+to you. You set your fake root's propagate flag to False, set a
+level on it and you'll hang your handlers off this fake root. Then
+when you create your loggers each should be a descendant of this
+fake root. Now you've completely isolated yourself in the logging
+hierarchy and won't be influenced by any other logging
+configuration. As an example let's say your your code is called
+'foo' and so you name your fake root logger 'foo'.::
+
+ my_root = logging.getLogger('foo') # child of the root logger
+ my_root.propagate = False
+ my_root.setLevel(logging.DEBUG)
+ my_root.addHandler(my_handler)
+
+Then every logger you create should have 'foo.' prepended to it's
+name. If you're logging my module your module's logger would be
+created like this::
+
+ module_logger = logging.getLogger('foo.%s' % __module__)
+
+If you're logging by class then your class logger would be::
+
+ class_logger = logging.getLogger('foo.%s.%s' % (self.__module__, self.__class__.__name__))
+
+How to set levels:
+------------------
+
+An instinctive or simplistic assumption is to set the root logger to a
+high logging level, for example ERROR. After all you don't want to be
+spamming users with debug and info messages. Let's also assume you've
+got two handlers, one for a file and one for the console, both
+attached to the root logger (a common configuration) and you haven't
+set the level on either handler (in which case the handler will emit
+all levels).
+
+But now let's say you want to turn on debugging, but just to the file,
+the console should continue to only emit error messages.
+
+You set the root logger's level to DEBUG. The first thing you notice is
+that you're getting debug message both in the file and on the console
+because the console's handler does not have a level set. Not what you
+want.
+
+So you go back restore the root loggers level back to it's original
+ERROR level and set the file handler's level to DEBUG and the console
+handler's level to ERROR. Now you don't get any debug messages because
+the root logger is blocking all messages below the level of ERROR and
+doesn't invoke any handlers. The file handler attached to the root
+logger even though it's level is set to DEBUG never gets a chance to
+process the message.
+
+*IMPORTANT:* You have to set the logger's level to the minimum of all
+the attached handler's levels, otherwise the logger may block the
+message from ever reaching any handler.
+
+In this example the root logger's level must be set to DEBUG, the file
+handler's level to DEBUG, and the console handler's level set to
+ERROR.
+
+Now let's take a more real world example which is a bit more
+complicated. It's typical to assign loggers to every major class. In
+fact this is the design strategy of Java logging from which the Python
+logging is modeled. In a large complex application or library that
+means dozens or possibly hundreds of loggers. Now lets say you need to
+trace what is happening with one class. If you use the simplistic
+configuration outlined above you'll set the log level of the root
+logger and one of the handlers to debug. Now you're flooded with debug
+message from every logger in the system when all you wanted was the
+debug messages from just one class.
+
+How can you get fine grained control over which loggers emit debug
+messages? Here are some possibilities:
+
+(1) Set a filter.
+.................
+
+When a message is propagated to a logger in the hierarchy first the
+loggers level is checked. If logger level passes then the logger
+iterates over every handler attached to the logger first checking the
+handler level. If the handler level check passes then the filters
+attached to the handler are run.
+
+Filters are passed the record (i.e. the message), it does not have
+access to either the logger or handler it's executing within. You
+can't just set the filter to only pass the records of the classes you
+want to debug because that would block other important info, warning,
+error and critical messages from other classes. The filter would have
+to know about the "global" log level which is in effect and also pass
+any messages at that level or higher. It's unfortunate the filter
+cannot know the level of the logger or handler it's executing inside
+of.
+
+Also logger filters only are applied to the logger they are attached
+to, i.e. the logger the message was generated on. They do not get
+applied to any ancestor loggers. That means you can't just set a
+filter on the root logger. You have to either set the filters on the
+handlers or on every logger created.
+
+The filter first checks the level of the message record. If it's
+greater than debug it passes it. For debug messages it checks the set
+of loggers which have debug messages enabled, if the message record
+was generated on one of those loggers it passes the record, otherwise
+it blocks it.
+
+The only question is whether you attach the filter to every logger or
+to a handful of handlers. The advantage of attaching the filter to
+every logger is efficiency, the time spent handling the message can be
+short circuited much sooner if the message is filtered earlier in the
+process. The advantage of attaching the filter to a handler is
+simplicity, you only have to do that when a handler is created, not
+every place in the code where a logger is created.
+
+(2) Conditionally set the level of each logger.
+...............................................
+
+When loggers are created a check is performed to see if the logger is
+in the set of loggers for which debug information is desired, if so
+it's level is set to DEBUG, otherwise it's set to the global
+level. One has to recall there really isn't a single global level if
+you want some handlers to emit info and above, some handlers error and
+above, etc. In this case if the logger is not in the set of logger's
+emitting debug the logger level should be set to the next increment
+above debug level.
+
+A good question to ask would be why not just leave the logger's level
+unset if it's not in the set of loggers to be debugged? After all it
+will just inherit the root level right? There are two problems with
+that. 1) It wold actually inherit the level any ancestor logger and if
+an ancestor was set to debug you've effectively turned on debugging
+for all children of that ancestor logger. There are times you might
+want that behavior, where all your children inherit your level, but
+there are many cases where that's not the behavior you want. 2) A more
+pernicious problem exists. The logger your handlers are attached to
+MUST be set to debug level, otherwise your debug messages will never
+reach the handlers for output. Thus if you leave a loggers level unset
+and let it inherit it's effective level from an ancestor it might very
+well inherit the debug level from the root logger. That means you've
+completely negated your attempt to selectively set debug logging on
+specific loggers. Bottom line, you really have to set the level on
+every logger created if you want fine grained control.
+
+Approach 2 has some distinct performance advantages. First of all
+filters are not used, this avoids a whole processing step and extra
+filter function calls on every message. Secondly a logger level check
+is a simple integer compare which is very efficient. Thirdly the
+processing of a message can be short circuited very early in the
+processing pipeline, no ancestor loggers will be invoked and no
+handlers will be invoked.
+
+The downside is some added complexity at logger creation time. But
+this is easily mitigated by using a utility function or method to
+create the logger instead of just calling logger.getLogger().
+
+Like every thing else in computer science which approach you take boils
+down to a series of trade offs, most around how your code is
+organized. You might find it easier to set a filter on just one or two
+handlers. It might be easier to modify the configuration during
+execution if the logic is centralized in just a filter function, but
+don't let that sway you too much because it's trivial to iterate over
+every logger and dynamically reset it's log level.
+
+Now at least you've got a basic understanding of how this stuff hangs
+together and what your options are. That's not insignificant, when I
+was first introduced to logging in Java and Python I found it
+bewildering difficult to get it do what I wanted.
+
+John Dennis <jdennis@redhat.com>
+
+'''
+
+#-------------------------------------------------------------------------------
+import sys
+import os
+import pwd
+import logging
+import re
+import time
+import copy
+
+#-------------------------------------------------------------------------------
+# Our root logger, all loggers will be descendents of this.
+IPA_ROOT_LOGGER_NAME = 'ipa'
+
+# Format string for time.strftime() to produce a ISO 8601 date time
+# formatted string in the UTC time zone.
+ISO8601_UTC_DATETIME_FMT = '%Y-%m-%dT%H:%M:%SZ'
+
+# Default format
+LOGGING_DEFAULT_FORMAT = '%(levelname)s %(message)s'
+
+# Logging format string for use with logging stderr handlers
+LOGGING_FORMAT_STDERR = 'ipa: %(levelname)s: %(message)s'
+
+# Logging format string for use with logging stdout handlers
+LOGGING_FORMAT_STDOUT = '[%(asctime)s %(name)s] <%(levelname)s>: %(message)s'
+
+# Logging format string for use with logging file handlers
+LOGGING_FORMAT_FILE = '\t'.join([
+ '%(asctime)s',
+ '%(process)d',
+ '%(threadName)s',
+ '%(name)s',
+ '%(levelname)s',
+ '%(message)s',
+])
+
+# Used by standard_logging_setup() for console message
+LOGGING_FORMAT_STANDARD_CONSOLE = '%(name)-12s: %(levelname)-8s %(message)s'
+
+# Used by standard_logging_setup() for file message
+LOGGING_FORMAT_STANDARD_FILE = '%(asctime)s %(levelname)s %(message)s'
+
+
+# Maps a logging level name to it's numeric value
+log_level_name_map = {
+ 'notset' : logging.NOTSET,
+ 'debug' : logging.DEBUG,
+ 'info' : logging.INFO,
+ 'warn' : logging.WARNING,
+ 'warning' : logging.WARNING,
+ 'error' : logging.ERROR,
+ 'critical' : logging.CRITICAL
+}
+
+log_levels = (logging.DEBUG, logging.INFO, logging.WARNING, logging.ERROR, logging.CRITICAL)
+
+logger_method_names = ('debug', 'info', 'warning', 'error', 'exception', 'critical')
+
+#-------------------------------------------------------------------------------
+
+def get_unique_levels(iterable):
+ '''
+ Given a iterable of objects containing a logging level return a
+ ordered list (min to max) of unique levels.
+
+ :parameters:
+ iterable
+ Iterable yielding objects with a logging level attribute.
+ :returns:
+ Ordered list (min to max) of unique levels.
+ '''
+ levels = set()
+
+ for obj in iterable:
+ level = getattr(obj, 'level', sys.maxint)
+ if level != logging.NOTSET:
+ levels.add(level)
+ levels = list(levels)
+ levels.sort()
+ return levels
+
+def get_minimum_level(iterable):
+ '''
+ Given a iterable of objects containing a logging level return the
+ minimum level. If no levels are defined return maxint.
+ set of unique levels.
+
+ :parameters:
+ iterable
+ Iterable yielding objects with a logging level attribute.
+ :returns:
+ Ordered list (min to max) of unique levels.
+ '''
+ min_level = sys.maxint
+
+ for obj in iterable:
+ level = getattr(obj, 'level', sys.maxint)
+ if level != logging.NOTSET:
+ if level < min_level:
+ min_level = level
+ return min_level
+
+def parse_log_level(level):
+ '''
+ Given a log level either as a string or integer
+ return a numeric logging level. The following case insensitive
+ names are recognized::
+
+ * notset
+ * debug
+ * info
+ * warn
+ * warning
+ * error
+ * critical
+
+ A string containing an integer is also recognized, for example
+ ``"10"`` would map to ``logging.DEBUG``
+
+ The integer value must be the range [``logging.NOTSET``,
+ ``logging.CRITICAL``] otherwise a value exception will be raised.
+
+ :parameters:
+ level
+ basestring or integer, level value to convert
+ :returns:
+ integer level value
+ '''
+ # Is it a string representation of an integer?
+ # If so convert to an int.
+ if isinstance(level, basestring):
+ try:
+ level = int(level)
+ except:
+ pass
+
+ # If it's a string lookup it's name and map to logging level
+ # otherwise validate the integer value is in range.
+ if isinstance(level, basestring):
+ result = log_level_name_map.get(level.lower()) #pylint: disable=E1103
+ if result is None:
+ raise ValueError('unknown log level (%s)' % level)
+ return result
+ elif isinstance(level, int):
+ if level < logging.NOTSET or level > logging.CRITICAL:
+ raise ValueError('log level (%d) out of range' % level)
+ return level
+ else:
+ raise TypeError('log level must be basestring or int, got (%s)' % type(level))
+
+#-------------------------------------------------------------------------------
+def logging_obj_str(obj):
+ '''
+ Unfortunately the logging Logger and Handler classes do not have a
+ custom __str__() function which converts the object into a human
+ readable string representation. This function takes any object
+ with a level attribute and outputs the objects name with it's
+ associated level. If a name was never set for the object then it's
+ repr is used instead.
+
+ :parameters:
+ obj
+ Object with a logging level attribute
+ :returns:
+ string describing the object
+ '''
+ name = getattr(obj, 'name', repr(obj))
+ text = '"%s" [level=%s]' % (name, logging.getLevelName(obj.level))
+ if isinstance(obj, logging.FileHandler):
+ text += ' filename="%s"' % obj.baseFilename
+ return text
+#-------------------------------------------------------------------------------
+class LogManager(object):
+ '''
+ This class wraps the functionality in the logging module to
+ provide an easier to use API for logging while providing advanced
+ features including a independent namespace. Each application or
+ library wishing to have it's own logging namespace should instantiate
+ exactly one instance of this class and use it to manage all it's
+ logging.
+
+ Traditionally (or simplistically) logging was set up with a single
+ global root logger with output handlers bound to it. The global
+ root logger (whose name is the empty string) was shared by all
+ code in a loaded process. The only the global unamed root logger
+ had a level set on it, all other loggers created inherited this
+ global level. This can cause conflicts in more complex scenarios
+ where loaded code wants to maintain it's own logging configuration
+ independent of whomever loaded it's code. By using only a single
+ logger level set on the global root logger it was not possible to
+ have fine grained control over individual logger output. The
+ pattern seen with this simplistic setup has been frequently copied
+ despite being clumsy and awkward. The logging module has the tools
+ available to support a more sophisitcated and useful model, but it
+ requires an overarching framework to manage. This class provides
+ such a framework.
+
+ The features of this logging manager are:
+
+ * Independent logging namespace.
+
+ * Simplifed method to create handlers.
+
+ * Simple setup for applications with command line args.
+
+ * Sophisitcated handler configuration
+ (e.g. file ownership & permissions)
+
+ * Easy fine grained control of logger output
+ (e.g. turning on debug for just 1 or 2 loggers)
+
+ * Holistic management of the interrelationships between
+ logging components.
+
+ * Ability to dynamically adjust logging configuration in
+ a running process.
+
+ An independent namespace is established by creating a independent
+ root logger for this manager (root_logger_name). This root logger
+ is a direct child of the global unamed root logger. All loggers
+ created by this manager will be descendants of this managers root
+ logger. The managers root logger has it's propagate flag set
+ to False which means all loggers and handlers created by this
+ manager will be isolated in the global logging tree.
+
+ Log level management:
+ ---------------------
+
+ Traditionally loggers inherited their logging level from the root
+ logger. This was simple but made it impossible to independently
+ control logging output from different loggers. If you set the root
+ level to DEBUG you got DEBUG output from every logger in the
+ system, often overwhelming in it's voluminous output. Many times
+ you want to turn on debug for just one class (a common idom is to
+ have one logger per class). To achieve the fine grained control
+ you can either use filters or set a logging level on every logger
+ (see the module documentation for the pros and cons). This manager
+ sets a log level on every logger instead of using level
+ inheritence because it's more efficient at run time.
+
+ Global levels are supported via the verbose and debug flags
+ setting every logger level to INFO and DEBUG respectively. Fine
+ grained level control is provided via regular expression matching
+ on logger names (see `configure()` for the details. For
+ example if you want to set a debug level for the foo.bar logger
+ set a regular expression to match it and bind it to the debug
+ level. Note, the global verbose and debug flags always override
+ the regular expression level configuration. Do not set these
+ global flags if you want fine grained control.
+
+ The manager maintains the minimum level for all loggers under it's
+ control and the minimum level for all handlers under it's
+ control. The reason it does this is because there is no point in
+ generating debug messages on a logger if there is no handler
+ defined which will output a debug message. Thus when the level is
+ set on a logger it takes into consideration the set of handlers
+ that logger can emit to.
+
+ IMPORTANT: Because the manager maintains knowledge about all the
+ loggers and handlers under it's control it is essential you use
+ only the managers interface to modify a logger or handler and not
+ set levels on the objects directly, otherwise the manger will not
+ know to visit every object under it's control when a configuraiton
+ changes (see '`LogManager.apply_configuration()`).
+
+ Example Usage::
+
+ # Create a log managers for use by 'my_app'
+ log_mgr = LogManager('my_app')
+
+ # Create a handler to send error messages to stderr
+ log_mgr.create_log_handlers([dict(stream=sys.stdout,
+ level=logging.ERROR)])
+
+ # Create logger for a class
+ class Foo(object):
+ def __init__(self):
+ self.log = log_mgr.get_logger(self)
+
+ '''
+ def __init__(self, root_logger_name='', configure_state=None):
+ '''
+ Create a new LogManager instance using root_logger_name as the
+ parent of all loggers maintained by the manager.
+
+ Only one log manger should be created for each logging namespace.
+
+ :parameters:
+ root_logger_name
+ The name of the root logger. All loggers will be prefixed
+ by this name.
+ configure_state
+ Used by clients of the log manager to track the
+ configuration state, may be any object.
+
+ :return:
+ LogManager instance
+
+ '''
+ self.configure_state = configure_state
+ self.root_logger_name = root_logger_name
+ self.default_level = logging.ERROR
+ self.debug = False
+ self.verbose = False
+ self.logger_regexps = []
+
+ self.loggers = {} # dict, key is logger name, value is logger object
+ self.handlers = {} # dict, key is handler name, value is handler object
+
+ self.root_logger = self.get_logger(self.root_logger_name)
+ # Stop loggers and handlers from searching above our root
+ self.root_logger.propagate = False
+
+
+ def __str__(self):
+ '''
+ When str() is called on the LogManager output it's state.
+ '''
+ text = ''
+ text += 'root_logger_name: %s\n' % (self.root_logger_name)
+ text += 'configure_state: %s\n' % (self.configure_state)
+ text += 'default_level: %s\n' % (logging.getLevelName(self.default_level))
+ text += 'debug: %s\n' % (self.debug)
+ text += 'verbose: %s\n' % (self.verbose)
+
+ text += 'number of loggers: %d\n' % (len(self.loggers))
+ loggers = [logging_obj_str(x) for x in self.loggers.values()]
+ loggers.sort()
+ for logger in loggers:
+ text += ' %s\n' % (logger)
+
+ text += 'number of handlers: %d\n' % (len(self.handlers))
+ handlers = [logging_obj_str(x) for x in self.handlers.values()]
+ handlers.sort()
+ for handler in handlers:
+ text += ' %s\n' % (handler)
+
+ text += 'number of logger regexps: %d\n' % (len(self.logger_regexps))
+ for regexp, level in self.logger_regexps:
+ text += ' "%s" => %s\n' % (regexp, logging.getLevelName(level))
+
+ return text
+
+ def configure(self, config, configure_state=None):
+ '''
+ The log manager is initialized from key,value pairs in the
+ config dict. This may be called any time to modify the
+ logging configuration at run time.
+
+ The supported entries in the config dict are:
+
+ default_level
+ The default level applied to a logger when not indivdually
+ configured. The verbose and debug config items override
+ the default level. See `log_manager.parse_log_level()` for
+ details on how the level can be specified.
+ verbose
+ Boolean, if True sets default_level to INFO.
+ debug
+ Boolean, if True sets default_level to DEBUG.
+ logger_regexps
+ List of (regexp, level) tuples. This is a an ordered list
+ regular expressions used to match against a logger name to
+ configure the logger's level. The first regexp in the
+ sequence which matches the logger name will use the the
+ level bound to that regexp to set the logger's level. If
+ no regexp matches the logger name then the logger will be
+ assigned the default_level.
+
+ The regular expression comparision is performed with the
+ re.search() function which means the match can be located
+ anywhere in the name string (as opposed to the start of
+ the the string). Do not forget to escape regular
+ expression metacharacters when appropriate. For example
+ dot ('.') is used to seperate loggers in a logging
+ hierarchy path (e.g. a.b.c)
+
+ Examples::
+
+ # To match exactly the logger a.b.c and set it to DEBUG:
+ logger_regexps = [(r'^a\.b\.c$', 'debug')]
+
+ # To match any child of a.b and set it to INFO:
+ logger_regexps = [(r'^a\.b\..*', 'info')]
+
+ # To match any leaf logger with the name c and set it to level 5:
+ logger_regexps = [(r'\.c$', 5)]
+ handlers
+ List of handler config dicts or (config, logger)
+ tuples. See `create_log_handlers()` for details
+ of a hanlder config.
+
+ The simple form where handlers is a list of dicts each
+ handler is bound to the log mangers root logger (see
+ `create_log_handlers()` optional ``logger``
+ parameter). If you want to bind each handler to a specific
+ logger other then root handler then group the handler config
+ with a logger in a (config, logger) tuple. The logger may be
+ either a logger name or a logger instance. The following are
+ all valid methods of passing handler configuration.::
+
+ # List of 2 config dicts; both handlers bound to root logger
+ [{}, {}]
+
+ # List of 2 tuples; first handler bound to logger_name1
+ # by name, second bound to logger2 by object.
+ [({}, 'logger_name1'), ({}, logger2']
+
+ # List of 1 dict, 1 tuple; first bound to root logger,
+ # second bound to logger_name by name
+ [{}, ({}, 'logger_name']
+
+ :parameters:
+ config
+ Dict of <key,value> pairs describing the configuration.
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+
+ '''
+ for attr in ('debug', 'verbose', 'logger_regexps'):
+ value = config.get(attr)
+ if value is not None:
+ setattr(self, attr, value)
+
+ attr = 'default_level'
+ value = config.get(attr)
+ if value is not None:
+ try:
+ level = parse_log_level(value)
+ except Exception, e:
+ raise ValueError("could not set %s (%s)" % (attr, e))
+ setattr(self, attr, level)
+
+ attr = 'handlers'
+ handlers = config.get(attr)
+ if handlers is not None:
+ for item in handlers:
+ logger = self.root_logger
+ config = None
+ if isinstance(item, dict):
+ config = item
+ elif isinstance(item, tuple):
+ if len(item) != 2:
+ raise ValueError('handler tuple must have exactly 2 items, got "%s"' % item)
+ config = item[0]
+ logger = item[1]
+ else:
+ raise TypeError('expected dict or tuple for handler item, got "%s", handlers=%s' % \
+ type(item), value)
+
+ if not isinstance(config, dict):
+ raise TypeError('expected dict for handler config, got "%s"', type(config))
+ if isinstance(logger, basestring):
+ logger = self.get_logger(logger)
+ else:
+ if not isinstance(logger, logging.Logger):
+ raise TypeError('expected logger name or logger object in %s' % item)
+
+ self.create_log_handlers([config], logger, configure_state)
+
+ if self.verbose:
+ self.default_level = logging.INFO
+
+ if self.debug:
+ self.default_level = logging.DEBUG
+
+ self.apply_configuration(configure_state)
+
+ def create_log_handlers(self, configs, logger=None, configure_state=None):
+ '''
+ Create new handlers and attach them to a logger (log mangers
+ root logger by default).
+
+ *Note, you may also pass the handler configs to `LogManager.configure()`.*
+
+ configs is an iterable yielding a dict. Each dict configures a
+ handler. Currently two types of handlers are supported:
+
+ * stream
+ * file
+
+ Which type of handler is created is determined by the presence of
+ the ``stream`` or ``filename`` in the dict.
+
+ Configuration keys:
+ ===================
+
+ Handler type keys:
+ ------------------
+
+ Exactly of the following must present in the config dict:
+
+ stream
+ Use the specified stream to initialize the StreamHandler.
+
+ filename
+ Specifies that a FileHandler be created, using the specified
+ filename.
+
+ Common keys:
+ ------------
+
+ name
+ Set the name of the handler. This is optional but can be
+ useful when examining the logging configuration.
+ For files defaults to ``'file:absolute_path'`` and for streams
+ it defaults to ``'stream:stream_name'``
+
+ format
+ Use the specified format string for the handler.
+
+ time_zone_converter
+ Log record timestamps are seconds since the epoch in the UTC
+ time zone stored as floating point values. When the formatter
+ inserts a timestamp via the %(asctime)s format substitution it
+ calls a time zone converter on the timestamp which returns a
+ time.struct_time value to pass to the time.strftime function
+ along with the datefmt format conversion string. The time
+ module provides two functions with this signature,
+ time.localtime and time.gmtime which performs a conversion to
+ local time and UTC respectively. time.localtime is the default
+ converter. Setting the time zone converter to time.gmtime is
+ appropriate for date/time strings in UTC. The
+ time_zone_converter attribute may be any function with the
+ correct signature. Or as a convenience you may also pass a
+ string which will select either the time.localtime or the
+ time.gmtime converter. The case insenstive string mappings
+ are::
+
+ 'local' => time.localtime
+ 'localtime' => time.localtime
+ 'gmt' => time.gmtime
+ 'gmtime' => time.gmtime
+ 'utc' => time.gmtime
+
+ datefmt
+ Use the specified time.strftime date/time format when
+ formatting a timestamp via the %(asctime)s format
+ substitution. The timestamp is first converted using the
+ time_zone_converter to either local or UTC
+
+ level
+ Set the handler logger level to the specified level. May be
+ one of the following strings: 'debug', 'info', 'warn',
+ 'warning', 'error', 'critical' or any of the logging level
+ constants. Thus level='debug' is equivalent to
+ level=logging.DEBUG. Defaults to self.default_level.
+
+
+ File handler keys:
+ ------------------
+
+ filemode
+ Specifies the mode to open the file. Defaults to 'a' for
+ append, use 'w' for write.
+
+ permission
+ Set the permission bits on the file (i.e. chmod).
+ Must be a valid integer (e.g. 0660 for rw-rw----)
+
+ user
+ Set the user owning the file. May be either a numeric uid or a
+ basestring with a user name in the passwd file.
+
+ group
+ Set the group associated with the file, May be either a
+ numeric gid or a basestring with a group name in the groups
+ file.
+
+ Examples:
+ ---------
+
+ The following shows how to set two handlers, one for a file
+ (ipa.log) at the debug log level and a second handler set to
+ stdout (e.g. console) at the info log level. (One handler sets it
+ level with a simple name, the other with a logging constant just
+ to illustrate the flexibility) ::
+
+ # Get a root logger
+ log_mgr = LogManger('my_app')
+
+ # Create the handlers
+ log_mgr.create_log_handlers([dict(filename='my_app.log',
+ level='info',
+ user='root',
+ group='root',
+ permission=0600,
+ time_zone_converter='utc',
+ datefmt='%Y-%m-%dT%H:%M:%SZ', # ISO 8601
+ format='<%(levelname)s> [%(asctime)s] module=%(name)s "%(message)s"'),
+ dict(stream=sys.stdout,
+ level=logging.ERROR,
+ format='%(levelname)s: %(message)s')])
+
+ # Create a logger for my_app.foo.bar
+ foo_bar_log = log_mgr.get_logger('foo.bar')
+
+ log_mgr.root_logger.info("Ready to process requests")
+ foo_bar_log.error("something went boom")
+
+ In the file my_app.log you would see::
+
+ <INFO> [2011-10-26T01:39:00Z] module=my_app "Ready to process requests"
+ <ERROR> [2011-10-26T01:39:00Z] module=may_app.foo.bar "something went boom"
+
+ On the console you would see::
+
+ ERROR: something went boom
+
+ :parameters:
+ configs
+ Sequence of dicts (any iterable yielding a dict). Each
+ dict creates one handler and contains the configuration
+ parameters used to create that handler.
+ logger
+ If unspecified the handlers will be attached to the
+ LogManager.root_logger, otherwise the handlers will be
+ attached to the specified logger.
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+
+ :return:
+ The list of created handers.
+ '''
+ if logger is None:
+ logger = self.root_logger
+
+ handlers = []
+
+ # Iterate over handler configurations.
+ for cfg in configs:
+ # File or stream handler?
+ filename = cfg.get('filename')
+ if filename:
+ if cfg.has_key("stream"):
+ raise ValueError("both filename and stream are specified, must be one or the other, config: %s" % cfg)
+ path = os.path.abspath(filename)
+ filemode = cfg.get('filemode', 'a')
+ handler = logging.FileHandler(path, filemode)
+
+ # Set the handler name
+ name = cfg.get("name")
+ if name is None:
+ name = 'file:%s' % (path)
+ handler.name = name
+
+ # Path should now exist, set ownership and permissions if requested.
+
+ # Set uid, gid (e.g. chmod)
+ uid = gid = None
+ user = cfg.get('user')
+ group = cfg.get('group')
+ if user is not None:
+ if isinstance(user, basestring):
+ pw = pwd.getpwnam(user)
+ uid = pw.pw_uid
+ elif isinstance(user, int):
+ uid = user
+ else:
+ raise TypeError("user (%s) is not int or basestring" % user)
+ if group is not None:
+ if isinstance(group, basestring):
+ pw = pwd.getpwnam(group)
+ gid = pw.pw_gid
+ elif isinstance(group, int):
+ gid = group
+ else:
+ raise TypeError("group (%s) is not int or basestring" % group)
+ if uid is not None or gid is not None:
+ if uid is None:
+ uid = -1
+ if gid is None:
+ gid = -1
+ os.chown(path, uid, gid)
+
+ # Set file permissions (e.g. mode)
+ permission = cfg.get('permission')
+ if permission is not None:
+ os.chmod(path, permission)
+ else:
+ stream = cfg.get("stream")
+ if stream is None:
+ raise ValueError("neither file nor stream specified in config: %s" % cfg)
+
+ handler = logging.StreamHandler(stream)
+
+ # Set the handler name
+ name = cfg.get("name")
+ if name is None:
+ name = 'stream:%s' % (stream)
+ handler.name = name
+
+ # Add the handler
+ handlers.append(handler)
+
+ # Configure message formatting on the handler
+ format = cfg.get("format", LOGGING_DEFAULT_FORMAT)
+ datefmt = cfg.get("datefmt", None)
+ formatter = logging.Formatter(format, datefmt)
+ time_zone_converter = cfg.get('time_zone_converter', time.localtime)
+ if isinstance(time_zone_converter, basestring):
+ converter = {'local' : time.localtime,
+ 'localtime' : time.localtime,
+ 'gmt' : time.gmtime,
+ 'gmtime' : time.gmtime,
+ 'utc' : time.gmtime}.get(time_zone_converter.lower())
+ if converter is None:
+ raise ValueError("invalid time_zone_converter name (%s)" % \
+ time_zone_converter)
+ elif callable(time_zone_converter):
+ converter = time_zone_converter
+ else:
+ raise ValueError("time_zone_converter must be basestring or callable, not %s" % \
+ type(time_zone_converter))
+
+ formatter.converter = converter
+ handler.setFormatter(formatter)
+
+ # Set the logging level
+ level = cfg.get('level')
+ if level is not None:
+ try:
+ level = parse_log_level(level)
+ except Exception, e:
+ print >>sys.stderr, 'could not set handler log level "%s" (%s)' % (level, e)
+ level = None
+ if level is None:
+ level = self.default_level
+ handler.setLevel(level)
+
+ for handler in handlers:
+ if handler.name in self.handlers:
+ raise ValueError('handler "%s" already exists' % handler.name)
+ logger.addHandler(handler)
+ self.handlers[handler.name] = handler
+ self.apply_configuration(configure_state)
+ return handlers
+
+ def get_handler(self, handler_name):
+ '''
+ Given a handler name return the handler object associated with
+ it.
+
+ :parameters:
+ handler_name
+ Name of the handler to look-up.
+
+ :returns:
+ The handler object associated with the handler name.
+ '''
+ handler = self.handlers.get(handler_name)
+ if handler is None:
+ raise KeyError('handler "%s" is not defined' % handler_name)
+ return handler
+
+ def set_handler_level(self, handler_name, level, configure_state=None):
+ '''
+ Given a handler name, set the handler's level, return previous level.
+
+ :parameters:
+ handler_name
+ Name of the handler to look-up.
+ level
+ The new level for the handler. See
+ `log_manager.parse_log_level()` for details on how the
+ level can be specified.
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+
+ :returns:
+ The handler's previous level
+ '''
+ handler = self.get_handler(handler_name)
+ level = parse_log_level(level)
+ prev_level = handler.level
+ handler.setLevel(level)
+ self.apply_configuration(configure_state)
+ return prev_level
+
+ def get_loggers_with_handler(self, handler):
+ '''
+ Given a handler return a list of loggers that hander is bound to.
+
+
+ :parameters:
+ handler
+ The name of a handler or a handler object.
+
+ :returns:
+ List of loggers with the handler is bound to.
+ '''
+
+ if isinstance(handler, basestring):
+ handler = self.get_handler(handler)
+ elif isinstance(handler, logging.Handler):
+ if not handler in self.handlers.values():
+ raise ValueError('handler "%s" is not managed by this log manager' % \
+ logging_obj_str(handler))
+ else:
+ raise TypeError('handler must be basestring or Handler object, got %s' % type(handler))
+
+ loggers = []
+ for logger in self.loggers.values():
+ if handler in logger.handlers:
+ loggers.append(logger)
+
+ return loggers
+
+ def remove_handler(self, handler, logger=None, configure_state=None):
+ '''
+ Remove the named handler. If logger is unspecified the handler
+ will be removed from all managed loggers, otherwise it will be
+ removed from only the specified logger.
+
+ :parameters:
+ handler
+ The name of the handler to be removed or the handler object.
+ logger
+ If unspecified the handler is removed from all loggers,
+ otherwise the handler is removed from only this logger.
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+ '''
+
+ if isinstance(handler, basestring):
+ handler = self.get_handler(handler)
+ elif not isinstance(handler, logging.Handler):
+ raise TypeError('handler must be basestring or Handler object, got %s' % type(handler))
+
+ handler_name = handler.name
+ if handler_name is None:
+ raise ValueError('handler "%s" does not have a name' % logging_obj_str(handler))
+
+ loggers = self.get_loggers_with_handler(handler)
+
+ if logger is None:
+ for logger in loggers:
+ logger.removeHandler(handler)
+ del self.handlers[handler_name]
+ else:
+ if not logger in loggers:
+ raise ValueError('handler "%s" is not bound to logger "%s"' % \
+ (handler_name, logging_obj_str(logger)))
+ logger.removeHandler(handler)
+ if len(loggers) == 1:
+ del self.handlers[handler_name]
+
+ self.apply_configuration(configure_state)
+
+ def apply_configuration(self, configure_state=None):
+ '''
+ Using the log manager's internal configuration state apply the
+ configuration to all the objects managed by the log manager.
+
+ :parameters:
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+
+ '''
+ if configure_state is not None:
+ self.configure_state = configure_state
+ for logger in self.loggers.values():
+ self._set_configured_logger_level(logger)
+
+ def get_configured_logger_level(self, name):
+ '''
+ Given a logger name return it's level as defined by the
+ `LogManager` configuration.
+
+ :parameters:
+ name
+ logger name
+ :returns:
+ log level
+ '''
+ level = self.default_level
+ for regexp, config_level in self.logger_regexps:
+ if re.search(regexp, name):
+ level = config_level
+ break
+
+ level = parse_log_level(level)
+ return level
+
+ def get_logger_handlers(self, logger):
+ '''
+ Return the set of unique handlers visible to this logger.
+
+ :parameters:
+ logger
+ The logger whose visible and enabled handlers will be returned.
+
+ :return:
+ Set of handlers
+ '''
+ handlers = set()
+
+ while logger:
+ for handler in logger.handlers:
+ handlers.add(handler)
+ if logger.propagate:
+ logger = logger.parent
+ else:
+ logger = None
+ return handlers
+
+ def get_minimum_handler_level_for_logger(self, logger):
+ '''
+ Return the minimum handler level of all the handlers the
+ logger is exposed to.
+
+ :parameters:
+ logger
+ The logger whose handlers will be examined.
+
+ :return:
+ The minimum of all the handler's levels. If no
+ handlers are defined sys.maxint will be returned.
+ '''
+
+ handlers = self.get_logger_handlers(logger)
+ min_level = get_minimum_level(handlers)
+ return min_level
+
+ def _set_configured_logger_level(self, logger):
+ '''
+ Based on the current configuration maintained by the log
+ manager set this logger's level.
+
+ If the level specified for this logger by the configuration is
+ less than the minimum level supported by the output handlers
+ the logger is exposed to then adjust the logger's level higher
+ to the minimum handler level. This is a performance
+ optimization, no point in emitting a log message if no
+ handlers will ever output it.
+
+ :parameters:
+ logger
+ The logger whose level is being configured.
+
+ :return:
+ The level actually set on the logger.
+ '''
+ level = self.get_configured_logger_level(logger.name)
+ minimum_handler_level = self.get_minimum_handler_level_for_logger(logger)
+ if level < minimum_handler_level:
+ level = minimum_handler_level
+ logger.setLevel(level)
+ return level
+
+ def get_logger(self, who, bind_logger_names=False):
+ '''
+ Return the logger for an object or a name. If the logger
+ already exists return the existing instance otherwise create
+ the logger.
+
+ The who parameter may be either a name or an object.
+ Loggers are identified by a name but because loggers are
+ usually bound to a class this method is optimized to handle
+ that case. If who is an object:
+
+ * The name object's module name (dot seperated) and the
+ object's class name.
+
+ * Optionally the logging output methods can be bound to the
+ object if bind_logger_names is True.
+
+ Otherwise if who is a basestring it is used as the logger
+ name.
+
+ In all instances the root_logger_name is prefixed to every
+ logger created by the manager.
+
+ :parameters:
+ who
+ If a basestring then use this as the logger name,
+ prefixed with the root_logger_name. Otherwise who is treated
+ as a class instance. The logger name is formed by prepending
+ the root_logger_name to the module name and then appending the
+ class name. All name components are dot seperated. Thus if the
+ root_logger_name is 'my_app', the class is ParseFileConfig
+ living in the config.parsers module the logger name will be:
+ ``my_app.config.parsers.ParseFileConfig``.
+ bind_logger_names
+ If true the class instance will have the following bound
+ to it: ``log``, ``debug()``, ``info()``, ``warning()``,
+ ``error()``, ``exception()``, ``critical()``. Where log is
+ the logger object and the others are the loggers output
+ methods. This is a convenience which allows you emit
+ logging messages directly, for example::
+
+ self.debug('%d names defined', self.num_names).
+
+ :return:
+ The logger matching the name indicated by who. If the
+ logger pre-existed return that instance otherwise create the
+ named logger return it.
+ '''
+
+ is_object = False
+ if isinstance(who, basestring):
+ obj_name = who
+ else:
+ is_object = True
+ obj_name = '%s.%s' % (who.__module__, who.__class__.__name__)
+
+ if obj_name == self.root_logger_name:
+ logger_name = obj_name
+ else:
+ logger_name = self.root_logger_name + '.' + obj_name
+
+ # If logger not in our cache then create and initialize the logger.
+ logger = self.loggers.get(logger_name)
+ if logger is None:
+ logger = logging.getLogger(logger_name)
+ self.loggers[logger_name] = logger
+ self._set_configured_logger_level(logger)
+
+ if bind_logger_names and is_object and getattr(who, '__log_manager', None) is None:
+ setattr(who, '__log_manager', self)
+ method = 'log'
+ if hasattr(who, method):
+ raise ValueError('%s is already bound to %s' % (method, repr(who)))
+ setattr(who, method, logger)
+
+ for method in logger_method_names:
+ if hasattr(who, method):
+ raise ValueError('%s is already bound to %s' % (method, repr(who)))
+ setattr(who, method, getattr(logger, method))
+
+ return logger
+
+
+class IPALogManager(LogManager):
+ '''
+ Subclass the LogManager to enforce some IPA specfic logging
+ conventions.
+
+ * Default to timestamps in UTC.
+ * Default to ISO 8601 timestamp format.
+ * Default the message format.
+ '''
+
+ log_logger_level_config_re = re.compile(r'^log_logger_level_(debug|info|warn|warning|error|critical|\d+)$')
+ log_handler_level_config_re = re.compile(r'^log_handler_(\S+)_level$')
+
+ def __init__(self, configure_state=None):
+ '''
+ :parameters:
+ configure_state
+ Used by clients of the log manager to track the
+ configuration state, may be any object.
+ '''
+
+ super(IPALogManager, self).__init__(IPA_ROOT_LOGGER_NAME, configure_state)
+
+ def configure_from_env(self, env, configure_state=None):
+ '''
+ Read the loggger configuration from the Env config. The
+ following items may be configured:
+
+ Logger Levels
+ *log_logger_XXX = comma separated list of regexps*
+
+ Logger levels can be explicitly specified for specific loggers as
+ opposed to a global logging level. Specific loggers are indiciated
+ by a list of regular expressions bound to a level. If a logger's
+ name matches the regexp then it is assigned that level. The keys
+ in the Env config must begin with "log_logger_level\_" and then be
+ followed by a symbolic or numeric log level, for example::
+
+ log_logger_level_debug = ipalib\.dn\..*
+ log_logger_level_35 = ipalib\.plugins\.dogtag
+
+ The first line says any logger belonging to the ipalib.dn module
+ will have it's level configured to debug.
+
+ The second line say the ipa.plugins.dogtag logger will be
+ configured to level 35.
+
+ Note: logger names are a dot ('.') separated list forming a path
+ in the logger tree. The dot character is also a regular
+ expression metacharacter (matches any character) therefore you
+ will usually need to escape the dot in the logger names by
+ preceeding it with a backslash.
+
+ Handler Levels
+ *log_handler_XXX_level = level*
+
+ Handler levels may be specified with a key containing the
+ name of the handler (XXX) and whose value is the level. For
+ example::
+
+ log_handler_console_level = debug
+
+ Would set the console handler level to debug.
+
+ These are the predefined log handlers:
+
+ console
+ Writes to stderr.
+ file
+ Writes to the default log file.
+
+
+ The return value of this function is a dict with the following
+ format:
+
+ logger_regexps
+ List of (regexp, level) tuples
+ handlers
+ Dict, key is handler name, value is dict of handler config.
+
+ Handler config dict:
+
+ level
+ handler log level
+
+ :parameters:
+ env
+ Env object configuration values are read from.
+ configure_state
+ If other than None update the log manger's configure_state
+ variable to this object. Clients of the log manager can
+ use configure_state to track the state of the log manager.
+ '''
+ logger_regexps = []
+ handlers = {}
+ config = {'logger_regexps' : logger_regexps,
+ 'handlers' : handlers,
+ }
+
+ for attr in ('debug', 'verbose'):
+ value = getattr(env, attr, None)
+ if value is not None:
+ config[attr] = value
+
+ for attr in list(env):
+ # Get logger level configuration
+ match = IPALogManager.log_logger_level_config_re.search(attr)
+ if match:
+ value = match.group(1)
+ level = parse_log_level(value)
+ value = getattr(env, attr)
+ regexps = re.split('\s*,\s*', value)
+ # Add the regexp, it maps to the configured level
+ for regexp in regexps:
+ print "%s %s" % (regexp, level)
+ logger_regexps.append((regexp, level))
+ continue
+
+ # Get handler configuration
+ match = IPALogManager.log_handler_level_config_re.search(attr)
+ if match:
+ value = getattr(env, attr)
+ try:
+ level = parse_log_level(value)
+ except Exception, e:
+ print >>sys.stderr, 'ERROR could not parse log handler level: %s=%s' % (attr, value)
+ continue
+ name = match.group(1)
+ print "%s %s" % (name, level)
+ handler_config = handlers.get(name)
+ if handler_config is None:
+ handler_config = {'name' : name}
+ handler_config['level'] = level
+ continue
+
+ self.configure(config, configure_state)
+ return config
+
+ def create_log_handlers(self, configs, logger=None, configure_state=None):
+ 'Enforce some IPA specific configurations'
+ configs = copy.copy(configs)
+
+ for cfg in configs:
+ if not 'time_zone_converter' in cfg:
+ cfg['time_zone_converter'] = 'utc'
+ if not 'datefmt' in cfg:
+ cfg['datefmt'] = ISO8601_UTC_DATETIME_FMT
+ if not 'format' in cfg:
+ cfg['format'] = LOGGING_FORMAT_STDOUT
+
+ return super(IPALogManager, self).create_log_handlers(configs, logger, configure_state)
+
+#-------------------------------------------------------------------------------
+
+def standard_logging_setup(filename=None, verbose=False, debug=False, filemode='w'):
+ handlers = []
+
+ # File output is always logged at debug level
+ if filename is not None:
+ file_handler = dict(name='file',
+ filename=filename,
+ filemode=filemode,
+ permission=0600,
+ level='debug',
+ format=LOGGING_FORMAT_STANDARD_FILE)
+ handlers.append(file_handler)
+
+ if log_mgr.handlers.has_key('console'):
+ log_mgr.remove_handler('console')
+ level = 'error'
+ if verbose:
+ level = 'info'
+ if debug:
+ level = 'debug'
+
+ console_handler = dict(name='console',
+ stream=sys.stderr,
+ level=level,
+ format=LOGGING_FORMAT_STANDARD_CONSOLE)
+ handlers.append(console_handler)
+
+
+ log_mgr.configure(dict(default_level=level,
+ handlers=handlers),
+ configure_state='standard')
+
+ return log_mgr.root_logger
+
+#-------------------------------------------------------------------------------
+
+# Single shared instance of log manager
+#
+# By default always starts with stderr console handler at error level
+# so messages generated before logging is fully configured have some
+# place to got and won't get lost.
+
+log_mgr = IPALogManager()
+log_mgr.configure(dict(default_level='error',
+ handlers=[dict(name='console',
+ stream=sys.stderr)]),
+ configure_state='default')
+