Index: trac/env.py
===================================================================
--- trac/env.py	(revision 7181)
+++ trac/env.py	(working copy)
@@ -2,6 +2,7 @@
 #
 # Copyright (C) 2003-2008 Edgewall Software
 # Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -14,6 +15,7 @@
 #
 # Author: Jonas Borgström <jonas@edgewall.com>
 
+import logging
 import os
 try:
     import threading
@@ -147,14 +149,36 @@
          - $(path)s     the path for the current environment
          - $(basename)s the last path component of the current environment
          - $(project)s  the project name
-
-         Note the usage of `$(...)s` instead of `%(...)s` as the latter form
-         would be interpreted by the ConfigParser itself.
-
-         Example:
+        
+        Note the usage of `$(...)s` instead of `%(...)s` as the latter form
+        would be interpreted by the ConfigParser itself.
+        
+        Example:
          ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s
-
-         (since 0.10.5)""")
+        
+        (since 0.10.5)""")
+    log_filters = ListOption('logging', 'log_filters', [], doc=
+        """Custom logging handlers.
+        
+        If nothing set, logging will be as it was, nothing is changed.
+        
+        Example usage is:
+        log_filters = trac:WARNING, trac.ticket:DEBUG
+        
+        The above would translate to:
+            * all messages who's module name starts with `trac` and log level
+            is higher than `WARNING` are logged;
+            * all messages who's module name starts with `trac.ticket` and log
+            level is higher than `DEBUG` are logged;
+            * all other messages who's module name does not start with any of
+            the above and for which their level is higher than the default
+            `log_level` will be logged;
+        
+        This way you can narrow the debugging messages to the modules you
+        wish to. The same applies to a plugin you're coding:
+        log_filters = trac:ERROR, my.plug.module:DEBUG
+        
+        (since 0.12)""")
 
     def __init__(self, path, create=False, options=[]):
         """Initialize the Trac environment.
@@ -201,7 +225,9 @@
         environment configuration) and `log` (a logger object)."""
         component.env = self
         component.config = self.config
-        component.log = self.log
+        component.log = logging.getLogger(
+            "%s.%s" % (self.path, component.__class__.__module__)
+        )
 
     def is_component_enabled(self, cls):
         """Implemented to only allow activation of components that are not
@@ -347,7 +373,7 @@
 
     def setup_log(self):
         """Initialize the logging sub-system."""
-        from trac.log import logger_factory
+        from trac.log import setup_logging
         logtype = self.log_type
         logfile = self.log_file
         if logtype == 'file' and not os.path.isabs(logfile):
@@ -358,8 +384,9 @@
                      .replace('%(path)s', self.path) \
                      .replace('%(basename)s', os.path.basename(self.path)) \
                      .replace('%(project)s', self.project_name)
-        self.log = logger_factory(logtype, logfile, self.log_level, self.path,
-                                  format=format)
+        setup_logging(logtype, logfile, self.log_level, self.path,
+                      format, self.log_filters)
+        self.log = logging.getLogger("%s.%s" % (self.path, __name__))
 
     def get_known_users(self, cnx=None):
         """Generator that yields information about all known users, i.e. users
@@ -551,10 +578,12 @@
                 env.log.info('Reloading environment due to configuration '
                              'change')
                 env.shutdown()
-                if hasattr(env.log, '_trac_handler'):
-                    hdlr = env.log._trac_handler
-                    env.log.removeHandler(hdlr)
+                env_root_logger = logging.getLogger(env.path)
+                if hasattr(env_root_logger, '_trac_handler'):                    
+                    hdlr = env_root_logger._trac_handler
+                    env_root_logger.removeHandler(hdlr)
                     hdlr.close()
+                del env_root_logger
                 del env_cache[env_path]
                 env = None
             if env is None:
Index: trac/htdocs/css/admin.css
===================================================================
--- trac/htdocs/css/admin.css	(revision 7181)
+++ trac/htdocs/css/admin.css	(working copy)
@@ -21,7 +21,7 @@
 
 #tabcontent { padding: 0.4em 2em; margin-left: 12em; min-height: 300px; }
 #tabcontent h2 { color: #333; margin-top: 0; }
-p.help { color: #666; font-size: 90%; margin: 1em .5em .5em; }
+div.help, p.help { color: #666; font-size: 90%; margin: 1em .5em .5em; }
 
 #enumlist tbody td { vertical-align: middle; }
 
Index: trac/admin/web_ui.py
===================================================================
--- trac/admin/web_ui.py	(revision 7181)
+++ trac/admin/web_ui.py	(working copy)
@@ -2,6 +2,7 @@
 #
 # Copyright (C) 2005-2008 Edgewall Software
 # Copyright (C) 2005 Jonas Borgström <jonas@edgewall.com>
+# Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -211,6 +212,11 @@
         log_level = self.env.log_level
         log_file = self.env.log_file
         log_dir = os.path.join(self.env.path, 'log')
+        log_filters = self.config.getlist('logging', 'log_filters')
+        for idx, filter in enumerate(log_filters):
+            filter = filter.split(':')
+            if not (len(filter) > 1 and filter[1]):
+                log_filters[idx] = "%s:%s" % (filter[0], log_level)
 
         log_types = [
             dict(name='', label=_('None'), selected=False, disabled=False),
@@ -229,46 +235,77 @@
 
         if req.method == 'POST':
             changed = False
-
-            new_type = req.args.get('log_type')
-            if new_type and new_type not in ('stderr', 'file', 'syslog',
-                                             'eventlog'):
-                raise TracError(
-                    _('Unknown log type %(type)s', type=new_type),
-                    _('Invalid log type')
-                )
-            if new_type != log_type:
-                self.config.set('logging', 'log_type', new_type or 'none')
+            if 'add_filter' in req.args:
+                filter_module_name = req.args.get('filter_modname')
+                if not filter_module_name:
+                    raise TracError(_("Filter module name must not be empty"))
+                filter_log_level = req.args.get('filter_loglevel')
+                if filter_log_level and filter_log_level not in log_levels:
+                    raise TracError(_('Unknown log level %(level)s',
+                                      level=filter_log_level),
+                                    _('Invalid log level'))
+                for filter in log_filters:
+                    if filter.split(':')[0] == filter_module_name or \
+                        filter.split(':')[0].rstrip('.*') == filter_module_name:
+                        raise TracError(
+                            _("A filter for module '%(module)s' already exists."
+                              " Remove that one first.",
+                              module=filter_module_name),
+                            _("Filter already exists"))
+                new_log_filter = "%s:%s" % (filter_module_name.rstrip('.*'),
+                                            filter_log_level)
+                log_filters.append(new_log_filter)
+                self.log.debug("Adding new filter '%s' to log_filters",
+                               new_log_filter)
+                self.config.set('logging', 'log_filters',', '.join(log_filters))
                 changed = True
-                log_type = new_type
-
-            if log_type:
-                new_level = req.args.get('log_level')
-                if new_level and new_level not in log_levels:
+            elif 'delete_filters' in req.args:
+                selected = req.args.getlist('sel')
+                for filter in selected:
+                    self.log.debug("Removing filter '%s' from log_filters",
+                                   filter)
+                    log_filters.pop(log_filters.index(filter))
+                self.config.set('logging', 'log_filters',', '.join(log_filters))
+                changed = True
+            else:
+                new_type = req.args.get('log_type')
+                if new_type and new_type not in ('stderr', 'file', 'syslog',
+                                                 'eventlog'):
                     raise TracError(
-                        _('Unknown log level %(level)s', level=new_level),
-                        _('Invalid log level'))
-                if new_level and new_level != log_level:
-                    self.config.set('logging', 'log_level', new_level)
+                        _('Unknown log type %(type)s', type=new_type),
+                        _('Invalid log type')
+                    )
+                if new_type != log_type:
+                    self.config.set('logging', 'log_type', new_type or 'none')
                     changed = True
-                    log_evel = new_level
-            else:
-                self.config.remove('logging', 'log_level')
-                changed = True
+                    log_type = new_type
 
-            if log_type == 'file':
-                new_file = req.args.get('log_file', 'trac.log')
-                if new_file != log_file:
-                    self.config.set('logging', 'log_file', new_file or '')
+                if log_type:
+                    new_level = req.args.get('log_level')
+                    if new_level and new_level not in log_levels:
+                        raise TracError(
+                            _('Unknown log level %(level)s', level=new_level),
+                            _('Invalid log level'))
+                    if new_level and new_level != log_level:
+                        self.config.set('logging', 'log_level', new_level)
+                        changed = True
+                        log_evel = new_level
+                else:
+                    self.config.remove('logging', 'log_level')
                     changed = True
-                    log_file = new_file
-                if log_type == 'file' and not log_file:
-                    raise TracError(_('You must specify a log file'),
-                                    _('Missing field'))
-            else:
-                self.config.remove('logging', 'log_file')
-                changed = True
 
+                if log_type == 'file':
+                    new_file = req.args.get('log_file', 'trac.log')
+                    if new_file != log_file:
+                        self.config.set('logging', 'log_file', new_file or '')
+                        changed = True
+                        log_file = new_file
+                    if log_type == 'file' and not log_file:
+                        raise TracError(_('You must specify a log file'),
+                                        _('Missing field'))
+                else:
+                    self.config.remove('logging', 'log_file')
+                    changed = True
             if changed:
                 self.config.save()
             req.redirect(req.href.admin(cat, page))
@@ -276,7 +313,8 @@
         data = {
             'type': log_type, 'types': log_types,
             'level': log_level, 'levels': log_levels,
-            'file': log_file, 'dir': log_dir
+            'file': log_file, 'dir': log_dir,
+            'filters': log_filters
         }
         return 'admin_logging.html', {'log': data}
 
Index: trac/admin/templates/admin_logging.html
===================================================================
--- trac/admin/templates/admin_logging.html	(revision 7181)
+++ trac/admin/templates/admin_logging.html	(working copy)
@@ -54,6 +54,92 @@
         </div>
       </fieldset>
     </form>
+
+    <fieldset>
+      <legend>Logging Filters</legend>
+
+      <form class="addnew" id="newlog_filters" name="newlog_filters" method="post">
+        <fieldset>
+          <legend>Add New Logging Filter</legend>
+          <table>
+            <tr class="field">
+              <th><label for="filter_modname">Module:</label></th>
+              <td><input type="text" id="filter_modname" name="filter_modname"/></td>
+            </tr>
+            <tr class="field">
+              <th><label for="filter_loglevel">Log level:</label></th>
+              <td>
+                <select id="filter_loglevel" name="filter_loglevel">
+                  <option py:for="level in log.levels">$level</option>
+                </select>
+              </td>
+            </tr>
+          </table>
+        <div class="buttons">
+          <input type="submit" name="add_filter" value="${_('Add Filter')}"/>
+        </div>
+        </fieldset>
+      </form>
+
+      <p class="help">If nothing set, logging will be as it was, nothing is
+      changed.</p>
+
+      <form class="mod" id="log_filters" name="log_filters" method="post">
+        <div class="field">
+          <table class="listing" id="filters_table">
+            <thead>
+              <tr>
+                <th class="sel">&nbsp;</th>
+                <th>Module</th>
+                <th>Log level</th>
+              </tr>
+            </thead>
+            <tbody py:if="log.filters">
+              <tr py:for="mod, level in [f.split(':') for f in log.filters]">
+                <td class="sel"><input type="checkbox" name="sel" value="$mod:$level"/></td>
+                <td>$mod</td>
+                <td>$level</td>
+              </tr>
+            </tbody>
+            <tbody py:if="not log.filters">
+              <tr><td colspan="3">
+                <center><b>No Filters Available</b></center>
+              </td></tr>
+            </tbody>
+          </table>
+        </div>
+        <div class="buttons" py:if="log.filters">
+          <input type="submit" name="delete_filters" value="${_('Delete Selected Filters')}"/>
+        </div>
+
+        <div class="help">
+          <p>Example usage is:</p>
+<pre>
+  [logging]
+  log_filters = trac:WARNING, trac.ticket:DEBUG
+</pre>  
+          <p>The above would translate to:</p>  
+          <ul>
+            <li>all messages who's module name starts with <b><tt>trac</tt></b>
+            and log level is higher than <b><tt>WARNING</tt></b> are logged;</li>
+            
+            <li>all messages who's module name starts with <b><tt>trac.ticket</tt></b>
+            and log level is higher than <b><tt>DEBUG</tt></b> are logged;</li>
+  
+            <li>all other messages who's module name <b>does not</b> start with either
+            <b><tt>trac</tt></b> or <b><tt>trac.ticket</tt></b> will be logged if
+            their level is higher than the default <b><tt>log_level</tt></b>;</li>
+          </ul>
+  
+          <p>This way you can narrow the debugging messages to the modules you wish to.</p>
+          <p>The same applies to a plugin you're coding:</p>
+<pre>
+  [logging]
+  log_filters = trac:ERROR, my.plug.module:DEBUG
+</pre>
+        </div>
+      </form>
+    </fieldset>
   </body>
 
 </html>
Index: trac/log.py
===================================================================
--- trac/log.py	(revision 7181)
+++ trac/log.py	(working copy)
@@ -3,6 +3,7 @@
 # Copyright (C) 2003-2008 Edgewall Software
 # Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com>
 # Copyright (C) 2006 Christian Boos <cboos@neuf.fr>
+# Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org>
 # All rights reserved.
 #
 # This software is licensed as described in the file COPYING, which
@@ -19,16 +20,20 @@
 import logging.handlers
 import sys
 
-def logger_factory(logtype='syslog', logfile=None, level='WARNING',
-                   logid='Trac', format=None):
-    logger = logging.getLogger(logid)
+from trac.util.compat import sorted
+from trac.util.translation import _
+
+
+def setup_logging(logtype='syslog', logfile=None, level='WARNING', logid='Trac',
+                  format=None, filters=()):
+    env_root_logger = logging.getLogger(logid)
+        
     logtype = logtype.lower()
     if logtype == 'file':
         hdlr = logging.FileHandler(logfile)
     elif logtype in ('winlog', 'eventlog', 'nteventlog'):
         # Requires win32 extensions
-        hdlr = logging.handlers.NTEventLogHandler(logid,
-                                                  logtype='Application')
+        hdlr = logging.handlers.NTEventLogHandler(logid, logtype='Application')
     elif logtype in ('syslog', 'unix'):
         hdlr = logging.handlers.SysLogHandler('/dev/log')
     elif logtype in ('stderr'):
@@ -47,20 +52,77 @@
         datefmt = '%X'
     level = level.upper()
     if level in ('DEBUG', 'ALL'):
-        logger.setLevel(logging.DEBUG)
+        env_root_logger.setLevel(logging.DEBUG)
     elif level == 'INFO':
-        logger.setLevel(logging.INFO)
+        env_root_logger.setLevel(logging.INFO)
     elif level == 'ERROR':
-        logger.setLevel(logging.ERROR)
+        env_root_logger.setLevel(logging.ERROR)
     elif level == 'CRITICAL':
-        logger.setLevel(logging.CRITICAL)
+        env_root_logger.setLevel(logging.CRITICAL)
     else:
-        logger.setLevel(logging.WARNING)
-    formatter = logging.Formatter(format, datefmt)
-    hdlr.setFormatter(formatter)
-    logger.addHandler(hdlr)
+        env_root_logger.setLevel(logging.WARNING)
 
+    hdlr.setFormatter(TracFormatter(logid, format, datefmt))
+    # Assign handler right away to be able to log filter errors
+    env_root_logger.addHandler(hdlr)    
+    
+    if filters:
+        hdlr.addFilter(TracFilter(filters, level, logid))
+        
     # Remember our handler so that we can remove it later
-    logger._trac_handler = hdlr 
+    env_root_logger._trac_handler = hdlr
+
+
+class TracFilter(logging.Filter):
+    def __init__(self, trac_filters=(), default_level='DEBUG', name=''):
+        self.qns = []
+        for filter in trac_filters:
+            filter = filter.split(':')
+            if len(filter) > 1 and filter[1]:
+                qn, lvl = filter[0].rstrip('.*'), filter[1]
+            else:
+                qn, lvl = filter[0].rstrip('.*'), default_level
+            path_and_qn = "%s.%s" % (name, qn)
+            
+            if lvl == 'ALL':
+                lvl = 'DEBUG'
+                
+            if lvl not in logging._levelNames:
+                logging.getLogger("%s.%s" % (name, __name__)).warning(
+                    _("Level '%(level)s' for filter '%(filter)s' not know. "
+                      "Ignoring filter.",
+                      level=lvl.upper(), filter=':'.join(filter)))
+                continue
+                            
+            self.qns.append((path_and_qn, logging.getLevelName(lvl.upper())))
+            
+        self.qns = sorted(self.qns, key=lambda x: len(x[0]), reverse=True)
+        
+        logging.Filter.__init__(self, name)
+
+    def filter(self, record):
+        for qn, level in self.qns:
+            if record.name.startswith("%s." % qn) or record.name == qn:
+                # Match trac, trac.web but not tracforge
+                if level <= record.levelno:
+                    return 1
+                return 0
+        # No point returning `logging.Filter.filter(self, record)`
+        # All logging messages will arrive to the filter with self.name at
+        # least equal to environment path, ie, self.name
+        return 1
+
+
+class TracFormatter(logging.Formatter):
+
+    def __init__(self, env_path, fmt, datefmt):
+        self.env_path = env_path
+        # Calculate strip length at init time, no need to keep calculating it
+        self.strip_length = len(env_path)+1
+        logging.Formatter.__init__(self, fmt, datefmt)
 
-    return logger
+    def format(self, record):
+        # get full dotted module name and stick that under record.module
+        if record.name.startswith(self.env_path):
+            record.module = record.name[self.strip_length:]
+        return logging.Formatter.format(self, record)

