Ticket #7286: all-the-way_enhanced_logging.5.patch
| File all-the-way_enhanced_logging.5.patch, 58.4 KB (added by palgarvio, 3 years ago) |
|---|
-
trac/env.py
2 2 # 3 3 # Copyright (C) 2003-2008 Edgewall Software 4 4 # Copyright (C) 2003-2007 Jonas Borgström <jonas@edgewall.com> 5 # Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org> 5 6 # All rights reserved. 6 7 # 7 8 # This software is licensed as described in the file COPYING, which … … 15 16 # Author: Jonas Borgström <jonas@edgewall.com> 16 17 17 18 import os.path 19 import logging 18 20 try: 19 21 import threading 20 22 except ImportError: … … 156 158 - $(path)s the path for the current environment 157 159 - $(basename)s the last path component of the current environment 158 160 - $(project)s the project name 159 160 Note the usage of `$(...)s` instead of `%(...)s` as the latter form161 would be interpreted by the ConfigParser itself.162 163 Example:161 162 Note the usage of `$(...)s` instead of `%(...)s` as the latter form 163 would be interpreted by the ConfigParser itself. 164 165 Example: 164 166 ($(thread)d) Trac[$(basename)s:$(module)s] $(levelname)s: $(message)s 165 166 (since 0.10.5)""") 167 168 (since 0.10.5)""") 169 log_filters = ListOption('logging', 'log_filters', [], doc= 170 """Custom logging handlers. 171 172 If nothing set, logging will be as it was, nothing is changed. 173 174 Example usage is: 175 log_filters = trac:WARNING, trac.ticket:DEBUG 176 177 The above would translate to: 178 * all messages who's module name starts with `trac` and log level 179 is higher than `WARNING` are logged; 180 * all messages who's module name starts with `trac.ticket` and log 181 level is higher than `DEBUG` are logged; 182 * all other messages who's module name does not start with any of 183 the above and for which their level is higher than the default 184 `log_level` will be logged; 185 186 This way you can narrow the debugging messages to the modules you 187 wish to. The same applies to a plugin you're coding: 188 log_filters = trac:ERROR, my.plug.module:DEBUG 189 190 (since 0.12)""") 167 191 168 192 def __init__(self, path, create=False, options=[]): 169 193 """Initialize the Trac environment. … … 210 234 environment configuration) and `log` (a logger object).""" 211 235 component.env = self 212 236 component.config = self.config 213 component.log = self.log 237 component.log = logging.getLogger( 238 "%s.%s" % (self.path, component.__class__.__module__) 239 ) 214 240 215 241 def is_component_enabled(self, cls): 216 242 """Implemented to only allow activation of components that are not … … 362 388 363 389 def setup_log(self): 364 390 """Initialize the logging sub-system.""" 365 from trac.log import logger_factory391 from trac.log import setup_logging 366 392 logtype = self.log_type 367 393 logfile = self.log_file 368 394 if logtype == 'file' and not os.path.isabs(logfile): … … 373 399 .replace('%(path)s', self.path) \ 374 400 .replace('%(basename)s', os.path.basename(self.path)) \ 375 401 .replace('%(project)s', self.project_name) 376 self.log = logger_factory(logtype, logfile, self.log_level, self.path, 377 format=format) 402 # Setup but don't keep the root logger 403 setup_logging(logtype, logfile, self.log_level, self.path, 404 format, self.log_filters) 405 406 class CheckDeprecatedLoggingUsage(object): 407 """Proxy class to warn users of bad logging usage""" 408 log = logging.getLogger("%s.%s" % (self.path, __name__)) 409 def check_deprecated_use(self): 410 import inspect 411 caller = inspect.stack()[2] 412 if 'env.log' in caller[4][0]: 413 self.log.warn( 414 _("""Please stop logging through "env.log" on "%s". \ 415 The code issuing this is "%s". This usage is now deprecated. Instead, if \ 416 it's a trac Component, please use "self.log", if it's just a function which \ 417 get's passed a trac env, get a logger like "log = env.get_logger(__name__)" \ 418 and log through that. This practice provides good and usefull logging."""), 419 caller[1], caller[4][0].strip()) 420 del caller 421 def __getattribute__(self, name): 422 if name in ('log', 'check_deprecated_use'): 423 return object.__getattribute__(self, name) 424 self.check_deprecated_use() 425 return getattr(self.log, name) 426 427 # Get a logger for this module 428 self.log = CheckDeprecatedLoggingUsage() 429 430 def get_logger(self, name): 431 """Get the correct logger for classes/functions which an env is passed, 432 yet, it's not subclassing Component.""" 433 from trac.log import get_logger 434 return get_logger(self.path, name) 378 435 379 436 def get_known_users(self, cnx=None): 380 437 """Generator that yields information about all known users, i.e. users … … 567 624 env.log.info('Reloading environment due to configuration ' 568 625 'change') 569 626 env.shutdown() 570 if hasattr(env.log, '_trac_handler'): 571 hdlr = env.log._trac_handler 572 env.log.removeHandler(hdlr) 627 env_root_logger = logging.getLogger(env.path) 628 if hasattr(env_root_logger, '_trac_handler'): 629 hdlr = env_root_logger._trac_handler 630 env_root_logger.removeHandler(hdlr) 573 631 hdlr.close() 632 del env_root_logger 574 633 del env_cache[env_path] 575 634 env = None 576 635 if env is None: -
trac/attachment.py
113 113 self.resource = Resource(parent_realm_or_attachment_resource, 114 114 parent_id).child('attachment', filename) 115 115 self.env = env 116 self.log = self.env.get_logger(__name__) 116 117 self.parent_realm = self.resource.parent.realm 117 118 self.parent_id = unicode(self.resource.parent.id) 118 119 if self.resource.id: … … 181 182 try: 182 183 os.unlink(self.path) 183 184 except OSError: 184 self. env.log.error('Failed to delete attachment file %s',185 self.path, exc_info=True)185 self.log.error('Failed to delete attachment file %s', 186 self.path, exc_info=True) 186 187 if handle_ta: 187 188 db.rollback() 188 189 raise TracError(_('Could not delete attachment')) 189 190 190 self. env.log.info('Attachment removed: %s' %self.title)191 self.log.info('Attachment removed: %s', self.title) 191 192 if handle_ta: 192 193 db.commit() 193 194 … … 234 235 shutil.copyfileobj(fileobj, targetfile) 235 236 self.resource.id = self.filename = filename 236 237 237 self.env.log.info('New attachment: %s by %s', self.title, 238 self.author) 238 self.log.info('New attachment: %s by %s', self.title, self.author) 239 239 240 240 if handle_ta: 241 241 db.commit() … … 272 272 As this is usually done while deleting the parent resource, 273 273 the `db` argument is ''not'' optional here. 274 274 """ 275 log = env.get_logger(__name__) 275 276 attachment_dir = None 276 277 for attachment in list(cls.select(env, parent_realm, parent_id, db)): 277 278 attachment_dir = os.path.dirname(attachment.path) … … 280 281 try: 281 282 os.rmdir(attachment_dir) 282 283 except OSError: 283 env.log.error("Can't delete attachment directory %s",284 attachment_dir, exc_info=True)284 log.error("Can't delete attachment directory %s", 285 attachment_dir, exc_info=True) 285 286 286 287 def open(self): 287 self. env.log.debug('Trying to open attachment at %s', self.path)288 self.log.debug('Trying to open attachment at %s', self.path) 288 289 try: 289 290 fd = open(self.path, 'rb') 290 291 except IOError: … … 692 693 add_link(req, 'alternate', raw_href, _('Original Format'), 693 694 mime_type) 694 695 695 self.log.debug("Rendering preview of file %s with mime-type %s" 696 % (attachment.filename, mime_type))696 self.log.debug("Rendering preview of file %s with mime-type %s", 697 attachment.filename, mime_type) 697 698 698 699 data['preview'] = mimeview.preview_data( 699 700 Context.from_request(req, attachment.resource), fd, … … 770 771 if legacy_action: 771 772 decision = legacy_action in perm 772 773 if not decision: 773 self. env.log.debug('LegacyAttachmentPolicy denied %s '774 'access to %s. User needs %s' %775 (username, resource, legacy_action))774 self.log.debug('LegacyAttachmentPolicy denied %s ' 775 'access to %s. User needs %s', 776 username, resource, legacy_action) 776 777 return decision 777 778 else: 778 779 for d in self.delegates: -
trac/mimeview/enscript.py
136 136 mimetype = mimetype.split(';', 1)[0] # strip off charset 137 137 mode = self._types[mimetype][0] 138 138 cmdline += ' --color -h -q --language=html -p - -E%s' % mode 139 self. env.log.debug("Enscript command line: %s" %cmdline)139 self.log.debug("Enscript command line: %s", cmdline) 140 140 141 141 np = NaivePopen(cmdline, content.encode('utf-8'), capturestderr=1) 142 142 if np.errorlevel or np.err: -
trac/mimeview/php.py
81 81 def render(self, context, mimetype, content, filename=None, rev=None): 82 82 # -n to ignore php.ini so we're using default colors 83 83 cmdline = '%s -sn' % self.path 84 self. env.log.debug("PHP command line: %s" %cmdline)84 self.log.debug("PHP command line: %s", cmdline) 85 85 86 86 content = content_to_unicode(self.env, content, mimetype) 87 87 content = content.encode('utf-8') -
trac/ticket/tests/query.py
1 from trac.log import logger_factory1 from trac.log import setup_logging 2 2 from trac.mimeview import Context 3 3 from trac.test import Mock, EnvironmentStub, MockPerm 4 4 from trac.ticket.query import Query, QueryModule -
trac/ticket/report.py
558 558 # The column name is obtained. 559 559 get_col_name_sql = 'SELECT * FROM ( ' + sql + ' ) AS tab LIMIT 1' 560 560 cursor.execute(get_col_name_sql, args) 561 self. env.log.debug("Query SQL(Get col names): " + get_col_name_sql)561 self.log.debug("Query SQL(Get col names): " + get_col_name_sql) 562 562 cols = get_column_names(cursor) 563 563 564 564 sort_col = req.args.get('sort', '') -
trac/ticket/model.py
46 46 47 47 def __init__(self, env, tkt_id=None, db=None, version=None): 48 48 self.env = env 49 self.log = self.env.get_logger(__name__) 49 50 self.resource = Resource('ticket', tkt_id, version) 50 51 self.fields = TicketSystem(self.env).get_ticket_fields() 51 52 self.time_fields = [f['name'] for f in self.fields … … 85 86 try: 86 87 default = options[int(default)] 87 88 except (ValueError, IndexError): 88 self. env.log.warning('Invalid default value "%s"'89 'for custom field "%s"'90 % (default, field['name']))89 self.log.warning('Invalid default value "%s" for ' 90 'custom field "%s"', default, 91 field['name']) 91 92 if default: 92 93 self.values.setdefault(field['name'], default) 93 94 … … 377 378 if not self.ticket_col: 378 379 self.ticket_col = self.type 379 380 self.env = env 381 self.log = self.env.get_logger(__name__) 380 382 if name: 381 383 name = simplify_whitespace(name) 382 384 if name: … … 406 408 handle_ta = False 407 409 408 410 cursor = db.cursor() 409 self. env.log.info('Deleting %s %s' % (self.type, self.name))411 self.log.info('Deleting %s %s', self.type, self.name) 410 412 cursor.execute("DELETE FROM enum WHERE type=%s AND value=%s", 411 413 (self.type, self._old_value)) 412 414 # Re-order any enums that have higher value than deleted (close gap) … … 435 437 handle_ta = False 436 438 437 439 cursor = db.cursor() 438 self. env.log.debug("Creating new %s '%s'" % (self.type, self.name))440 self.log.debug("Creating new %s '%s'", self.type, self.name) 439 441 if not self.value: 440 442 cursor.execute(("SELECT COALESCE(MAX(%s),0) FROM enum " 441 443 "WHERE type=%%s") % db.cast('value', 'int'), … … 461 463 handle_ta = False 462 464 463 465 cursor = db.cursor() 464 self. env.log.info('Updating %s "%s"' % (self.type, self.name))466 self.log.info('Updating %s "%s"', self.type, self.name) 465 467 cursor.execute("UPDATE enum SET name=%s,value=%s " 466 468 "WHERE type=%s AND name=%s", 467 469 (self.name, self.value, self.type, self._old_name)) … … 525 527 526 528 def __init__(self, env, name=None, db=None): 527 529 self.env = env 530 self.log = self.env.get_logger(__name__) 528 531 if name: 529 532 name = simplify_whitespace(name) 530 533 if name: … … 556 559 handle_ta = False 557 560 558 561 cursor = db.cursor() 559 self. env.log.info('Deleting component %s' %self.name)562 self.log.info('Deleting component %s', self.name) 560 563 cursor.execute("DELETE FROM component WHERE name=%s", (self.name,)) 561 564 562 565 self.name = self._old_name = None … … 576 579 handle_ta = False 577 580 578 581 cursor = db.cursor() 579 self. env.log.debug("Creating new component '%s'" %self.name)582 self.log.debug("Creating new component '%s'", self.name) 580 583 cursor.execute("INSERT INTO component (name,owner,description) " 581 584 "VALUES (%s,%s,%s)", 582 585 (self.name, self.owner, self.description)) … … 596 599 handle_ta = False 597 600 598 601 cursor = db.cursor() 599 self. env.log.info('Updating component "%s"' %self.name)602 self.log.info('Updating component "%s"', self.name) 600 603 cursor.execute("UPDATE component SET name=%s,owner=%s,description=%s " 601 604 "WHERE name=%s", 602 605 (self.name, self.owner, self.description, … … 630 633 631 634 def __init__(self, env, name=None, db=None): 632 635 self.env = env 636 self.log = self.env.get_logger(__name__) 633 637 if name: 634 638 self._fetch(name, db) 635 639 self._old_name = name … … 675 679 handle_ta = False 676 680 677 681 cursor = db.cursor() 678 self. env.log.info('Deleting milestone %s' %self.name)682 self.log.info('Deleting milestone %s', self.name) 679 683 cursor.execute("DELETE FROM milestone WHERE name=%s", (self.name,)) 680 684 681 685 # Retarget/reset tickets associated with this milestone … … 702 706 703 707 self.name = simplify_whitespace(self.name) 704 708 cursor = db.cursor() 705 self. env.log.debug("Creating new milestone '%s'" %self.name)709 self.log.debug("Creating new milestone '%s'", self.name) 706 710 cursor.execute("INSERT INTO milestone (name,due,completed,description) " 707 711 "VALUES (%s,%s,%s,%s)", 708 712 (self.name, to_timestamp(self.due), to_timestamp(self.completed), … … 722 726 723 727 self.name = simplify_whitespace(self.name) 724 728 cursor = db.cursor() 725 self. env.log.info('Updating milestone "%s"' %self.name)729 self.log.info('Updating milestone "%s"', self.name) 726 730 cursor.execute("UPDATE milestone SET name=%s,due=%s," 727 731 "completed=%s,description=%s WHERE name=%s", 728 732 (self.name, to_timestamp(self.due), to_timestamp(self.completed), 729 733 self.description, 730 734 self._old_name)) 731 self. env.log.info('Updating milestone field of all tickets'732 'associated with milestone "%s"' %self.name)735 self.log.info('Updating milestone field of all tickets associated ' 736 'with milestone "%s"', self.name) 733 737 cursor.execute("UPDATE ticket SET milestone=%s WHERE milestone=%s", 734 738 (self.name, self._old_name)) 735 739 self._old_name = self.name … … 763 767 764 768 def __init__(self, env, name=None, db=None): 765 769 self.env = env 770 self.log = self.env.get_logger(__name__) 766 771 if name: 767 772 if not db: 768 773 db = self.env.get_db_cnx() … … 792 797 handle_ta = False 793 798 794 799 cursor = db.cursor() 795 self. env.log.info('Deleting version %s' %self.name)800 self.log.info('Deleting version %s', self.name) 796 801 cursor.execute("DELETE FROM version WHERE name=%s", (self.name,)) 797 802 798 803 self.name = self._old_name = None … … 812 817 handle_ta = False 813 818 814 819 cursor = db.cursor() 815 self. env.log.debug("Creating new version '%s'" %self.name)820 self.log.debug("Creating new version '%s'", self.name) 816 821 cursor.execute("INSERT INTO version (name,time,description) " 817 822 "VALUES (%s,%s,%s)", 818 823 (self.name, to_timestamp(self.time), self.description)) … … 832 837 handle_ta = False 833 838 834 839 cursor = db.cursor() 835 self. env.log.info('Updating version "%s"' %self.name)840 self.log.info('Updating version "%s"', self.name) 836 841 cursor.execute("UPDATE version SET name=%s,time=%s,description=%s " 837 842 "WHERE name=%s", 838 843 (self.name, to_timestamp(self.time), self.description, -
trac/ticket/roadmap.py
653 653 cursor.execute("UPDATE ticket SET milestone=%s WHERE " 654 654 "milestone=%s and status != 'closed'", 655 655 (retarget_to, old_name)) 656 self. env.log.info('Tickets associated with milestone %s '657 'retargeted to %s' % (old_name, retarget_to))656 self.log.info('Tickets associated with milestone %s ' 657 'retargeted to %s' % (old_name, retarget_to)) 658 658 else: 659 659 milestone.insert() 660 660 db.commit() -
trac/ticket/query.py
54 54 order=None, desc=0, group=None, groupdesc=0, verbose=0, 55 55 rows=None, page=None, max=None): 56 56 self.env = env 57 self.log = self.env.get_logger(__name__) 57 58 self.id = report # if not None, it's the corresponding saved query 58 59 self.constraints = constraints or {} 59 60 synonyms = TicketSystem(self.env).get_field_synonyms() … … 234 235 cursor = db.cursor() 235 236 236 237 count_sql = 'SELECT COUNT(*) FROM (' + sql + ') AS foo' 237 # self. env.log.debug("Count results in Query SQL: " + count_sql %238 # tuple([repr(a) for a in args]))238 # self.log.debug("Count results in Query SQL: " + count_sql % 239 # tuple([repr(a) for a in args])) 239 240 240 241 cnt = 0 241 242 try: … … 245 246 raise 246 247 for cnt, in cursor: 247 248 break 248 self. env.log.debug("Count results in Query: %d" % cnt)249 self.log.debug("Count results in Query: %d" % cnt) 249 250 return cnt 250 251 251 252 def execute(self, req, db=None, cached_ids=None): … … 269 270 raise TracError(_('Page %(page)s is beyond the number of ' 270 271 'pages in the query', page=self.page)) 271 272 272 self. env.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args]))273 self.log.debug("Query SQL: " + sql % tuple([repr(a) for a in args])) 273 274 try: 274 275 cursor.execute(sql, args) 275 276 except: -
trac/htdocs/css/admin.css
21 21 22 22 #tabcontent { padding: 0.4em 2em; margin-left: 12em; min-height: 300px; } 23 23 #tabcontent h2 { color: #333; margin-top: 0; } 24 p.help { color: #666; font-size: 90%; margin: 1em .5em .5em; }24 div.help, p.help { color: #666; font-size: 90%; margin: 1em .5em .5em; } 25 25 26 26 #enumlist tbody td { vertical-align: middle; } 27 27 -
trac/db/mysql_backend.py
87 87 from trac.db_default import schema 88 88 for table in schema: 89 89 for stmt in self.to_sql(table): 90 self. env.log.debug(stmt)90 self.log.debug(stmt) 91 91 cursor.execute(stmt) 92 92 cnx.commit() 93 93 -
trac/versioncontrol/tests/svn_fs.py
30 30 except: 31 31 has_svn = False 32 32 33 from trac.log import logger_factory33 from trac.log import setup_logging 34 34 from trac.test import TestSetup 35 35 from trac.core import TracError 36 36 from trac.util.datefmt import utc … … 85 85 86 86 def setUp(self): 87 87 self.repos = SubversionRepository(REPOS_PATH, None, 88 logger_factory('test'))88 setup_logging('test')) 89 89 90 90 def tearDown(self): 91 91 self.repos = None … … 460 460 461 461 def setUp(self): 462 462 self.repos = SubversionRepository(REPOS_PATH + '/trunk', None, 463 logger_factory('test'))463 setup_logging('test')) 464 464 465 465 def tearDown(self): 466 466 self.repos = None … … 688 688 689 689 def setUp(self): 690 690 self.repos = SubversionRepository(REPOS_PATH + '/trunk/dir1', None, 691 logger_factory('test'))691 setup_logging('test')) 692 692 693 693 def tearDown(self): 694 694 self.repos = None … … 708 708 709 709 def setUp(self): 710 710 self.repos = SubversionRepository(REPOS_PATH + '/tags/v1', None, 711 logger_factory('test'))711 setup_logging('test')) 712 712 713 713 def tearDown(self): 714 714 self.repos = None … … 726 726 727 727 def setUp(self): 728 728 self.repos = SubversionRepository(REPOS_PATH + '/branches', None, 729 logger_factory('test'))729 setup_logging('test')) 730 730 731 731 def tearDown(self): 732 732 self.repos = None -
trac/versioncontrol/tests/cache.py
16 16 17 17 from datetime import datetime 18 18 19 from trac.log import logger_factory19 from trac.log import setup_logging 20 20 from trac.test import Mock, InMemoryDatabase 21 21 from trac.util.datefmt import to_timestamp, utc 22 22 from trac.versioncontrol import Repository, Changeset, Node, NoSuchChangeset … … 30 30 31 31 def setUp(self): 32 32 self.db = InMemoryDatabase() 33 self.log = logger_factory('test')33 self.log = setup_logging('test') 34 34 cursor = self.db.cursor() 35 35 cursor.execute("INSERT INTO system (name, value) VALUES (%s,%s)", 36 36 ('youngest_rev', '')) -
trac/admin/web_ui.py
2 2 # 3 3 # Copyright (C) 2005-2008 Edgewall Software 4 4 # Copyright (C) 2005 Jonas Borgström <jonas@edgewall.com> 5 # Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org> 5 6 # All rights reserved. 6 7 # 7 8 # This software is licensed as described in the file COPYING, which … … 215 216 log_level = self.env.log_level 216 217 log_file = self.env.log_file 217 218 log_dir = os.path.join(self.env.path, 'log') 219 log_filters = self.config.getlist('logging', 'log_filters') 220 for idx, filter in enumerate(log_filters): 221 filter = filter.split(':') 222 if not (len(filter) > 1 and filter[1]): 223 log_filters[idx] = "%s:%s" % (filter[0], log_level) 218 224 219 225 log_types = [ 220 226 dict(name='', label=_('None'), selected=False, disabled=False), … … 233 239 234 240 if req.method == 'POST': 235 241 changed = False 236 237 new_type = req.args.get('log_type') 238 if new_type and new_type not in ('stderr', 'file', 'syslog', 239 'eventlog'): 240 raise TracError( 241 _('Unknown log type %(type)s', type=new_type), 242 _('Invalid log type') 243 ) 244 if new_type != log_type: 245 self.config.set('logging', 'log_type', new_type or 'none') 242 if 'add_filter' in req.args: 243 filter_module_name = req.args.get('filter_modname') 244 if not filter_module_name: 245 raise TracError(_("Filter module name must not be empty")) 246 filter_log_level = req.args.get('filter_loglevel') 247 if filter_log_level and filter_log_level not in log_levels: 248 raise TracError(_('Unknown log level %(level)s', 249 level=filter_log_level), 250 _('Invalid log level')) 251 for filter in log_filters: 252 if filter.split(':')[0] == filter_module_name or \ 253 filter.split(':')[0].rstrip('.*') == filter_module_name: 254 raise TracError( 255 _("A filter for module '%(module)s' already exists." 256 " Remove that one first.", 257 module=filter_module_name), 258 _("Filter already exists")) 259 new_log_filter = "%s:%s" % (filter_module_name.rstrip('.*'), 260 filter_log_level) 261 log_filters.append(new_log_filter) 262 self.log.debug("Adding new filter '%s' to log_filters", 263 new_log_filter) 264 self.config.set('logging', 'log_filters',', '.join(log_filters)) 246 265 changed = True 247 log_type = new_type 248 249 if log_type: 250 new_level = req.args.get('log_level') 251 if new_level and new_level not in log_levels: 266 elif 'delete_filters' in req.args: 267 selected = req.args.getlist('sel') 268 for filter in selected: 269 self.log.debug("Removing filter '%s' from log_filters", 270 filter) 271 log_filters.pop(log_filters.index(filter)) 272 self.config.set('logging', 'log_filters',', '.join(log_filters)) 273 changed = True 274 else: 275 new_type = req.args.get('log_type') 276 if new_type and new_type not in ('stderr', 'file', 'syslog', 277 'eventlog'): 252 278 raise TracError( 253 _('Unknown log level %(level)s', level=new_level), 254 _('Invalid log level')) 255 if new_level and new_level != log_level: 256 self.config.set('logging', 'log_level', new_level) 279 _('Unknown log type %(type)s', type=new_type), 280 _('Invalid log type') 281 ) 282 if new_type != log_type: 283 self.config.set('logging', 'log_type', new_type or 'none') 257 284 changed = True 258 log_evel = new_level 259 else: 260 self.config.remove('logging', 'log_level') 261 changed = True 285 log_type = new_type 262 286 263 if log_type == 'file': 264 new_file = req.args.get('log_file', 'trac.log') 265 if new_file != log_file: 266 self.config.set('logging', 'log_file', new_file or '') 287 if log_type: 288 new_level = req.args.get('log_level') 289 if new_level and new_level not in log_levels: 290 raise TracError( 291 _('Unknown log level %(level)s', level=new_level), 292 _('Invalid log level')) 293 if new_level and new_level != log_level: 294 self.config.set('logging', 'log_level', new_level) 295 changed = True 296 log_evel = new_level 297 else: 298 self.config.remove('logging', 'log_level') 267 299 changed = True 268 log_file = new_file269 if log_type == 'file' and not log_file:270 raise TracError(_('You must specify a log file'),271 _('Missing field'))272 else:273 self.config.remove('logging', 'log_file')274 changed = True275 300 301 if log_type == 'file': 302 new_file = req.args.get('log_file', 'trac.log') 303 if new_file != log_file: 304 self.config.set('logging', 'log_file', new_file or '') 305 changed = True 306 log_file = new_file 307 if log_type == 'file' and not log_file: 308 raise TracError(_('You must specify a log file'), 309 _('Missing field')) 310 else: 311 self.config.remove('logging', 'log_file') 312 changed = True 276 313 if changed: 277 314 self.config.save() 278 315 req.redirect(req.href.admin(cat, page)) … … 280 317 data = { 281 318 'type': log_type, 'types': log_types, 282 319 'level': log_level, 'levels': log_levels, 283 'file': log_file, 'dir': log_dir 320 'file': log_file, 'dir': log_dir, 321 'filters': log_filters 284 322 } 285 323 return 'admin_logging.html', {'log': data} 286 324 -
trac/admin/tests/console.py
71 71 return True 72 72 73 73 def setup_log(self): 74 from trac.log import logger_factory75 self.log = logger_factory('null')74 from trac.log import setup_logging 75 self.log = setup_logging('null') 76 76 77 77 def is_component_enabled(self, cls): 78 78 return cls.__module__.startswith('trac.') and \ -
trac/admin/templates/admin_logging.html
54 54 </div> 55 55 </fieldset> 56 56 </form> 57 58 <fieldset> 59 <legend>Logging Filters</legend> 60 61 <form class="addnew" id="newlog_filters" name="newlog_filters" method="post"> 62 <fieldset> 63 <legend>Add New Logging Filter</legend> 64 <table> 65 <tr class="field"> 66 <th><label for="filter_modname">Module:</label></th> 67 <td><input type="text" id="filter_modname" name="filter_modname"/></td> 68 </tr> 69 <tr class="field"> 70 <th><label for="filter_loglevel">Log level:</label></th> 71 <td> 72 <select id="filter_loglevel" name="filter_loglevel"> 73 <option py:for="level in log.levels">$level</option> 74 </select> 75 </td> 76 </tr> 77 </table> 78 <div class="buttons"> 79 <input type="submit" name="add_filter" value="${_('Add Filter')}"/> 80 </div> 81 </fieldset> 82 </form> 83 84 <p class="help">If nothing set, logging will be as it was, nothing is 85 changed.</p> 86 87 <form class="mod" id="log_filters" name="log_filters" method="post"> 88 <div class="field"> 89 <table class="listing" id="filters_table"> 90 <thead> 91 <tr> 92 <th class="sel"> </th> 93 <th>Module</th> 94 <th>Log level</th> 95 </tr> 96 </thead> 97 <tbody py:if="log.filters"> 98 <tr py:for="mod, level in [f.split(':') for f in log.filters]"> 99 <td class="sel"><input type="checkbox" name="sel" value="$mod:$level"/></td> 100 <td>$mod</td> 101 <td>$level</td> 102 </tr> 103 </tbody> 104 <tbody py:if="not log.filters"> 105 <tr><td colspan="3"> 106 <center><b>No Filters Available</b></center> 107 </td></tr> 108 </tbody> 109 </table> 110 </div> 111 <div class="buttons" py:if="log.filters"> 112 <input type="submit" name="delete_filters" value="${_('Delete Selected Filters')}"/> 113 </div> 114 115 <div class="help"> 116 <p>Example usage is:</p> 117 <pre> 118 [logging] 119 log_filters = trac:WARNING, trac.ticket:DEBUG 120 </pre> 121 <p>The above would translate to:</p> 122 <ul> 123 <li>all messages who's module name starts with <b><tt>trac</tt></b> 124 and log level is higher than <b><tt>WARNING</tt></b> are logged;</li> 125 126 <li>all messages who's module name starts with <b><tt>trac.ticket</tt></b> 127 and log level is higher than <b><tt>DEBUG</tt></b> are logged;</li> 128 129 <li>all other messages who's module name <b>does not</b> start with either 130 <b><tt>trac</tt></b> or <b><tt>trac.ticket</tt></b> will be logged if 131 their level is higher than the default <b><tt>log_level</tt></b>;</li> 132 </ul> 133 134 <p>This way you can narrow the debugging messages to the modules you wish to.</p> 135 <p>The same applies to a plugin you're coding:</p> 136 <pre> 137 [logging] 138 log_filters = trac:ERROR, my.plug.module:DEBUG 139 </pre> 140 </div> 141 </form> 142 </fieldset> 57 143 </body> 58 144 59 145 </html> -
trac/perm.py
226 226 cursor = db.cursor() 227 227 cursor.execute("INSERT INTO permission VALUES (%s, %s)", 228 228 (username, action)) 229 self.log.info('Granted permission for %s to %s' % (action, username))229 self.log.info('Granted permission for %s to %s', action, username) 230 230 db.commit() 231 231 232 232 def revoke_permission(self, username, action): … … 235 235 cursor = db.cursor() 236 236 cursor.execute("DELETE FROM permission WHERE username=%s AND action=%s", 237 237 (username, action)) 238 self.log.info('Revoked permission for %s to %s' % (action, username))238 self.log.info('Revoked permission for %s to %s', action, username) 239 239 db.commit() 240 240 241 241 … … 426 426 perm) 427 427 if decision is not None: 428 428 if not decision: 429 self.log.debug("%s denies %s performing %s on %r" %430 (policy.__class__.__name__, username,431 action, resource))429 self.log.debug("%s denies %s performing %s on %r", 430 policy.__class__.__name__, username, 431 action, resource) 432 432 return decision 433 self.log.debug("No policy allowed %s performing %s on %r" %434 (username, action, resource))433 self.log.debug("No policy allowed %s performing %s on %r", 434 username, action, resource) 435 435 return False 436 436 437 437 # IPermissionRequestor methods … … 550 550 551 551 def permissions(self): 552 552 """Deprecated (but still used by the HDF compatibility layer)""" 553 self. env.log.warning('perm.permissions() is deprecated and '554 'is only present for HDF compatibility')553 self.log.warning('perm.permissions() is deprecated and ' 554 'is only present for HDF compatibility') 555 555 perm = PermissionSystem(self.env) 556 556 actions = perm.get_user_permissions(self.username) 557 557 return [action for action in actions if action in self] -
trac/loader.py
37 37 distributions, errors = working_set.find_plugins( 38 38 pkg_resources.Environment(search_path) 39 39 ) 40 log = env.get_logger(__name__) 40 41 for dist in distributions: 41 env.log.debug('Adding plugin %s from %s', dist, dist.location)42 log.debug('Adding plugin %s from %s', dist, dist.location) 42 43 working_set.add(dist) 43 44 44 45 def _log_error(item, e): 45 46 if isinstance(e, DistributionNotFound): 46 env.log.debug('Skipping "%s": ("%s" not found)', item, e)47 log.debug('Skipping "%s": ("%s" not found)', item, e) 47 48 elif isinstance(e, VersionConflict): 48 env.log.error('Skipping "%s": (version conflict "%s")',49 item, e)49 log.error('Skipping "%s": (version conflict "%s")', 50 item, e) 50 51 elif isinstance(e, UnknownExtra): 51 env.log.error('Skipping "%s": (unknown extra "%s")', item, e)52 log.error('Skipping "%s": (unknown extra "%s")', item, e) 52 53 elif isinstance(e, ImportError): 53 env.log.error('Skipping "%s": (can\'t import "%s")', item, e)54 log.error('Skipping "%s": (can\'t import "%s")', item, e) 54 55 else: 55 env.log.error('Skipping "%s": (error "%s")', item, e)56 log.error('Skipping "%s": (error "%s")', item, e) 56 57 57 58 for dist, e in errors.iteritems(): 58 59 _log_error(dist, e) 59 60 60 61 for entry in working_set.iter_entry_points(entry_point_name): 61 env.log.debug('Loading %s from %s', entry.name,62 entry.dist.location)62 log.debug('Loading %s from %s', entry.name, 63 entry.dist.location) 63 64 try: 64 65 entry.load(require=True) 65 66 except (ImportError, DistributionNotFound, VersionConflict, … … 76 77 manager if they define any components. 77 78 """ 78 79 def _load_py_files(env, search_path, auto_enable=None): 80 log = env.get_logger(__name__) 79 81 for path in search_path: 80 82 plugin_files = glob(os.path.join(path, '*.py')) 81 83 for plugin_file in plugin_files: 82 84 try: 83 85 plugin_name = os.path.basename(plugin_file[:-3]) 84 env.log.debug('Loading file plugin %s from %s' % \85 (plugin_name, plugin_file))86 log.debug('Loading file plugin %s from %s', 87 plugin_name, plugin_file) 86 88 if plugin_name not in sys.modules: 87 89 module = imp.load_source(plugin_name, plugin_file) 88 90 if path == auto_enable: 89 91 _enable_plugin(env, plugin_name) 90 92 except Exception, e: 91 env.log.error('Failed to load plugin from %s', plugin_file,92 exc_info=True)93 log.error('Failed to load plugin from %s', plugin_file, 94 exc_info=True) 93 95 94 96 return _load_py_files 95 97 -
trac/tests/attachment.py
7 7 import time 8 8 9 9 from trac.attachment import Attachment, AttachmentModule 10 from trac.log import logger_factory10 from trac.log import setup_logging 11 11 from trac.test import EnvironmentStub, Mock 12 12 from trac.wiki.formatter import Formatter 13 13 -
trac/wiki/model.py
47 47 self.readonly = 0 48 48 self.old_text = self.text 49 49 self.old_readonly = self.readonly 50 self.log = self.env.get_logger(__name__) 50 51 51 52 def _fetch(self, name, version=None, db=None): 52 53 if not db: … … 91 92 if version is None: 92 93 # Delete a wiki page completely 93 94 cursor.execute("DELETE FROM wiki WHERE name=%s", (self.name,)) 94 self. env.log.info('Deleted page %s' % self.name)95 self.log.info('Deleted page %s' % self.name) 95 96 else: 96 97 # Delete only a specific page version 97 98 cursor.execute("DELETE FROM wiki WHERE name=%s and version=%s", 98 99 (self.name, version)) 99 self. env.log.info('Deleted version %d of page %s'100 self.log.info('Deleted version %d of page %s' 100 101 % (version, self.name)) 101 102 102 103 if version is None or version == self.version: -
trac/wiki/formatter.py
75 75 """ 76 76 self.formatter = formatter 77 77 self.env = formatter.env 78 self.log = self.env.get_logger(__name__) 78 79 self.name = name 79 80 self.args = args 80 81 self.error = None … … 134 135 stream = Stream(HTMLParser(StringIO(text))) 135 136 return (stream | self._sanitizer).render('xhtml', encoding=None) 136 137 except ParseError, e: 137 self. env.log.warn(e)138 self.log.warn(e) 138 139 line = unicode(text).splitlines()[e.lineno - 1].strip() 139 140 return system_message(_('HTML parsing error: %(message)s', 140 141 message=escape(e.msg)), line) … … 167 168 # generic processors 168 169 169 170 def _legacy_macro_processor(self, text): # TODO: remove in 0.12 170 self. env.log.warning('Executing pre-0.11 Wiki macro %s by provider %s'171 % (self.name, self.macro_provider))171 self.log.warning('Executing pre-0.11 Wiki macro %s by provider %s', 172 self.name, self.macro_provider) 172 173 return self.macro_provider.render_macro(self.formatter.req, self.name, 173 174 text) 174 175 175 176 def _macro_processor(self, text): 176 self. env.log.debug('Executing Wiki macro %s by provider %s'177 % (self.name, self.macro_provider))177 self.log.debug('Executing Wiki macro %s by provider %s', 178 self.name, self.macro_provider) 178 179 return self.macro_provider.expand_macro(self.formatter, self.name, 179 180 text) 180 181 … … 239 240 def __init__(self, env, context): 240 241 """Note: `req` is still temporarily used.""" 241 242 self.env = env 243 self.log = self.env.get_logger(__name__) 242 244 self.context = context 243 245 self.req = context.req 244 246 self.href = context.href … … 481 483 macro = WikiProcessor(self, name) 482 484 return macro.process(args, in_paragraph=True) 483 485 except Exception, e: 484 self.env.log.error('Macro %s(%s) failed' % (name, args), 485 exc_info=True) 486 self.log.error('Macro %s(%s) failed', name, args, exc_info=True) 486 487 return system_message('Error: Macro %s(%s) failed' % (name, args), 487 488 e) 488 489 -
trac/test.py
186 186 # better solution than this. 187 187 load_workflow_config_snippet(self.config, 'basic-workflow.ini') 188 188 189 from trac.log import logger_factory190 self.log = logger_factory('test')189 from trac.log import setup_logging 190 self.log = setup_logging(logid='test') 191 191 192 192 from trac.web.href import Href 193 193 self.href = Href('/trac.cgi') -
trac/log.py
3 3 # Copyright (C) 2003-2008 Edgewall Software 4 4 # Copyright (C) 2003-2005 Daniel Lundin <daniel@edgewall.com> 5 5 # Copyright (C) 2006 Christian Boos <cboos@neuf.fr> 6 # Copyright (C) 2008 Pedro Algarvio <ufs@ufsoft.org> 6 7 # All rights reserved. 7 8 # 8 9 # This software is licensed as described in the file COPYING, which … … 19 20 import logging.handlers 20 21 import sys 21 22 22 def logger_factory(logtype='syslog', logfile=None, level='WARNING', 23 logid='Trac', format=None): 24 logger = logging.getLogger(logid) 23 from trac.util.translation import _ 24 25 def setup_logging(logtype='syslog', logfile=None, level='WARNING', logid='Trac', 26 format=None, filters=()): 27 env_root_logger = logging.getLogger(logid) 28 25 29 logtype = logtype.lower() 26 30 if logtype == 'file': 27 31 hdlr = logging.FileHandler(logfile) 28 32 elif logtype in ('winlog', 'eventlog', 'nteventlog'): 29 33 # Requires win32 extensions 30 hdlr = logging.handlers.NTEventLogHandler(logid, 31 logtype='Application') 34 hdlr = logging.handlers.NTEventLogHandler(logid, logtype='Application') 32 35 elif logtype in ('syslog', 'unix'): 33 36 hdlr = logging.handlers.SysLogHandler('/dev/log') 34 37 elif logtype in ('stderr'): … … 47 50 datefmt = '%X' 48 51 level = level.upper() 49 52 if level in ('DEBUG', 'ALL'): 50 logger.setLevel(logging.DEBUG)53 env_root_logger.setLevel(logging.DEBUG) 51 54 elif level == 'INFO': 52 logger.setLevel(logging.INFO)55 env_root_logger.setLevel(logging.INFO) 53 56 elif level == 'ERROR': 54 logger.setLevel(logging.ERROR)57 env_root_logger.setLevel(logging.ERROR) 55 58 elif level == 'CRITICAL': 56 logger.setLevel(logging.CRITICAL)59 env_root_logger.setLevel(logging.CRITICAL) 57 60 else: 58 logger.setLevel(logging.WARNING) 59 formatter = logging.Formatter(format, datefmt) 60 hdlr.setFormatter(formatter) 61 logger.addHandler(hdlr) 61 env_root_logger.setLevel(logging.WARNING) 62 62 63 hdlr.setFormatter(TracFormatter(logid, format, datefmt)) 64 # Assign handler right away to be able to log filter errors 65 env_root_logger.addHandler(hdlr) 66 67 if filters: 68 hdlr.addFilter(TracFilter(filters, level, logid)) 69 63 70 # Remember our handler so that we can remove it later 64 logger._trac_handler = hdlr 71 env_root_logger._trac_handler = hdlr 72 73 # Return env logger for tests that expect it 74 # For components, they will have self.log 75 # For classes/functions which get passed an env use env.get_logger(__name__) 76 return env_root_logger 77 78 79 class TracFilter(logging.Filter): 80 def __init__(self, trac_filters=(), default_level='DEBUG', name=''): 81 self.qns = [] 82 for filter in trac_filters: 83 filter = filter.split(':') 84 if len(filter) > 1 and filter[1]: 85 qn, lvl = filter[0].rstrip('.*'), filter[1] 86 else: 87 qn, lvl = filter[0].rstrip('.*'), default_level 88 path_and_qn = "%s.%s" % (name, qn) 89 90 if lvl == 'ALL': 91 lvl = 'DEBUG' 92 93 if lvl not in logging._levelNames: 94 logging.getLogger("%s.%s" % (name, __name__)).warning( 95 _("Level '%(level)s' for filter '%(filter)s' not know. " 96 "Ignoring filter.", 97 level=lvl.upper(), filter=':'.join(filter))) 98 continue 99 100 self.qns.append((path_and_qn, logging.getLevelName(lvl.upper()))) 101 102 self.qns = sorted(self.qns, key=lambda x: len(x[0]), reverse=True) 103 104 logging.Filter.__init__(self, name) 105 106 def filter(self, record): 107 for qn, level in self.qns: 108 if record.name.startswith("%s." % qn) or record.name == qn: 109 # Match trac, trac.web but not tracforge 110 if level <= record.levelno: 111 return 1 112 return 0 113 # No point returning `logging.Filter.filter(self, record)` 114 # All logging messages will arrive to the filter with self.name at 115 # least equal to environment path, ie, self.name 116 return 1 117 118 119 class TracFormatter(logging.Formatter): 120 121 def __init__(self, env_path, fmt, datefmt): 122 self.env_path = env_path 123 # Calculate strip length at init time, no need to keep calculating it 124 self.strip_length = len(env_path)+1 125 logging.Formatter.__init__(self, fmt, datefmt) 126 127 def format(self, record): 128 # get full dotted module name and stick that under record.module 129 if record.name.startswith(self.env_path): 130 record.module = record.name[self.strip_length:] 131 return logging.Formatter.format(self, record) 65 132 66 return logger 133 def get_logger(env_path, name): 134 """Helper function to get the correct logger for the passed class or 135 env and name.""" 136 return logging.getLogger("%s.%s" % (env_path, name)) -
trac/web/tests/session.py
3 3 import unittest 4 4 5 5 from trac.core import TracError 6 from trac.log import logger_factory6 from trac.log import setup_logging 7 7 from trac.test import EnvironmentStub, Mock 8 8 from trac.web.href import Href 9 9 from trac.web.session import DetachedSession, Session, PURGE_AGE, UPDATE_INTERVAL -
trac/web/session.py
33 33 def __init__(self, env, sid): 34 34 dict.__init__(self) 35 35 self.env = env 36 self.log = self.env.get_logger(__name__) 36 37 self.sid = None 37 38 self.last_visit = 0 38 39 self._new = True … … 43 44 self.authenticated = False 44 45 45 46 def get_session(self, sid, authenticated=False): 46 self. env.log.debug('Retrieving session for ID %r', sid)47 self.log.debug('Retrieving session for ID %r', sid) 47 48 48 49 db = self.env.get_db_cnx() 49 50 cursor = db.cursor() … … 118 119 # so that session doesn't get purged 119 120 if now - self.last_visit > UPDATE_INTERVAL: 120 121 self.last_visit = now 121 self. env.log.info("Refreshing session %s" %self.sid)122 self.log.info("Refreshing session %s", self.sid) 122 123 cursor.execute('UPDATE session SET last_visit=%s ' 123 124 'WHERE sid=%s AND authenticated=%s', 124 125 (self.last_visit, self.sid, authenticated)) 125 126 # Purge expired sessions. We do this only when the session was 126 127 # changed as to minimize the purging. 127 128 mintime = now - PURGE_AGE 128 self. env.log.debug('Purging old, expired, sessions.')129 self.log.debug('Purging old, expired, sessions.') 129 130 cursor.execute("DELETE FROM session_attribute " 130 131 "WHERE authenticated=0 AND sid " 131 132 "IN (SELECT sid FROM session WHERE " … … 191 192 raise TracError(Markup('Session "%s" already exists.<br />' 192 193 'Please choose a different session ID.') 193 194 % new_sid, 'Error renaming session') 194 self. env.log.debug('Changing session ID %s to %s' % (self.sid, new_sid))195 self.log.debug('Changing session ID %s to %s', self.sid, new_sid) 195 196 cursor.execute("UPDATE session SET sid=%s WHERE sid=%s " 196 197 "AND authenticated=0", (new_sid, self.sid)) 197 198 cursor.execute("UPDATE session_attribute SET sid=%s " … … 225 226 if not authenticated_flags[0]: 226 227 # Update the anomymous session records so that the session ID 227 228 # becomes the user name, and set the authenticated flag. 228 self. env.log.debug('Promoting anonymous session %s to '229 'authenticated session for user %s',230 sid, self.req.authname)229 self.log.debug('Promoting anonymous session %s to ' 230 'authenticated session for user %s', 231 sid, self.req.authname) 231 232 cursor.execute("UPDATE session SET sid=%s,authenticated=1 " 232 233 "WHERE sid=%s AND authenticated=0", 233 234 (self.req.authname, sid)) -
trac/web/main.py
422 422 environ['trac.web.version'])) 423 423 except TracError, e: 424 424 env_error = e 425 425 426 log = env.get_logger(__name__) 426 427 req = Request(environ, start_response) 427 428 try: 428 429 return _dispatch_request(req, env, env_error) … … 436 437 # objects with a __del__ method caught in a cycle) 437 438 ##gc.set_debug(gc.DEBUG_UNCOLLECTABLE) 438 439 unreachable = gc.collect() 439 env.log.debug("%d unreachable objects found.", unreachable)440 log.debug("%d unreachable objects found.", unreachable) 440 441 ##uncollectable = len(gc.garbage) 441 442 ##if uncollectable: 442 443 ## del gc.garbage[:] 443 ## env.log.warn("%d uncollectable objects found.", uncollectable)444 ## log.warn("%d uncollectable objects found.", uncollectable) 444 445 445 446 def _dispatch_request(req, env, env_error): 446 447 resp = [] 447 448 449 log = env.get_logger(__name__) 448 450 # fixup env.abs_href if `[trac] base_url` was not specified 449 451 if env and not env.abs_href.base: 450 452 env._abs_href = req.abs_href … … 461 463 462 464 except HTTPException, e: 463 465 if env: 464 env.log.warn(e)466 log.warn(e) 465 467 title = 'Error' 466 468 if e.reason: 467 469 if 'error' in e.reason.lower(): … … 483 485 484 486 except Exception, e: 485 487 if env: 486 env.log.exception(e)488 log.exception(e) 487 489 488 490 exc_info = sys.exc_info() 489 491 try: -
trac/notification.py
119 119 120 120 def __init__(self, env): 121 121 self.env = env 122 self.log = self.env.get_logger(__name__) 122 123 self.config = env.config 123 124 self.db = env.get_db_cnx() 124 125 … … 289 290 if domain: 290 291 address = "%s@%s" % (address, domain) 291 292 else: 292 self. env.log.info("Email address w/o domain: %s" %address)293 self.log.info("Email address w/o domain: %s", address) 293 294 return None 294 295 295 296 mo = self.shortaddr_re.search(address) … … 298 299 mo = self.longaddr_re.search(address) 299 300 if mo: 300 301 return mo.group(2) 301 self. env.log.info("Invalid email address: %s" %address)302 self.log.info("Invalid email address: %s", address) 302 303 return None 303 304 304 305 def encode_header(self, key, value): … … 376 377 377 378 # if there is not valid recipient, leave immediately 378 379 if len(recipients) < 1: 379 self. env.log.info('no recipient for a ticket notification')380 self.log.info('no recipient for a ticket notification') 380 381 return 381 382 382 383 pcc = accaddrs … … 402 403 msg.set_charset(self._charset) 403 404 self.add_headers(msg, headers); 404 405 self.add_headers(msg, mime_headers); 405 self. env.log.info("Sending SMTP notification to %s:%d to %s"406 % (self.smtp_server, self.smtp_port, recipients))406 self.log.info("Sending SMTP notification to %s:%d to %s", 407 self.smtp_server, self.smtp_port, recipients) 407 408 msgtext = msg.as_string() 408 409 # Ensure the message complies with RFC2822: use CRLF line endings 409 410 recrlf = re.compile("\r?\n")
