Skip to content

Commit 83ddb1e

Browse files
author
Morteza Ghasempour
committed
feat(logging): add customizable log formats for main and child logs
1 parent 4bf1e57 commit 83ddb1e

File tree

8 files changed

+180
-11
lines changed

8 files changed

+180
-11
lines changed

.vscode/settings.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"python.testing.unittestArgs": [
3+
"-v",
4+
"-s",
5+
".",
6+
"-p",
7+
"test_*.py"
8+
],
9+
"python.testing.pytestEnabled": false,
10+
"python.testing.unittestEnabled": true
11+
}

supervisor/dispatchers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -126,19 +126,21 @@ def _init_normallog(self):
126126
self.normallog = config.options.getLogger()
127127

128128
if logfile:
129+
fmt = config.options.childlog_format
129130
loggers.handle_file(
130131
self.normallog,
131132
filename=logfile,
132-
fmt='%(message)s',
133+
fmt=fmt,
133134
rotating=not not maxbytes, # optimization
134135
maxbytes=maxbytes,
135136
backups=backups
136137
)
137138

138139
if to_syslog:
140+
fmt = config.name + ' ' + config.options.childlog_format
139141
loggers.handle_syslog(
140142
self.normallog,
141-
fmt=config.name + ' %(message)s'
143+
fmt=fmt
142144
)
143145

144146
def _init_capturelog(self):

supervisor/loggers.py

Lines changed: 36 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -292,8 +292,17 @@ def asdict(self):
292292
msg = as_string(self.msg)
293293
if self.kw:
294294
msg = msg % self.kw
295-
self.dictrepr = {'message':msg, 'levelname':levelname,
296-
'asctime':asctime}
295+
self.dictrepr = {
296+
'message': msg,
297+
'levelname': levelname,
298+
'asctime': asctime,
299+
'levelno': self.level,
300+
'process': os.getpid(),
301+
'processName': 'supervisord',
302+
'threadName': 'MainThread'
303+
}
304+
self.dictrepr.update(self.kw)
305+
297306
return self.dictrepr
298307

299308
class Logger:
@@ -413,10 +422,28 @@ def handle_file(logger, filename, fmt, rotating=False, maxbytes=0, backups=0):
413422
if filename == 'syslog': # TODO remove this
414423
handler = SyslogHandler()
415424
else:
416-
if rotating is False:
417-
handler = FileHandler(filename)
418-
else:
419-
handler = RotatingFileHandler(filename, 'a', maxbytes, backups)
420-
handler.setFormat(fmt)
421-
handler.setLevel(logger.level)
422-
logger.addHandler(handler)
425+
if filename == 'stdout':
426+
return handle_stdout(logger, fmt)
427+
if filename == 'stderr':
428+
return handle_stderr(logger, fmt)
429+
if not os.path.exists(filename):
430+
# touching the file
431+
try:
432+
open(filename, 'a').close()
433+
except (IOError, OSError):
434+
pass
435+
try:
436+
if rotating:
437+
handler = RotatingFileHandler(
438+
filename,
439+
maxBytes=maxbytes,
440+
backupCount=backups
441+
)
442+
else:
443+
handler = FileHandler(filename)
444+
handler.setFormat(fmt)
445+
handler.setLevel(logger.level)
446+
logger.addHandler(handler)
447+
return handler
448+
except (IOError, OSError):
449+
logger.error('Cannot open file %s for writing' % filename)

supervisor/options.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -456,6 +456,12 @@ def __init__(self):
456456
self.add(None, None, "v", "version", self.version)
457457
self.add("nodaemon", "supervisord.nodaemon", "n", "nodaemon", flag=1,
458458
default=0)
459+
self.add("logfile_format", "supervisord.logfile_format",
460+
"", "logfile_format=", str,
461+
default="%(asctime)s %(levelname)s %(message)s")
462+
self.add("childlog_format", "supervisord.childlog_format",
463+
"", "childlog_format=", str,
464+
default="%(message)s")
459465
self.add("user", "supervisord.user", "u:", "user=")
460466
self.add("umask", "supervisord.umask", "m:", "umask=",
461467
octal_type, default='022')
@@ -528,6 +534,26 @@ def realize(self, *arg, **kw):
528534
self.uid = uid
529535
self.gid = gid_for_uid(uid)
530536

537+
loglevel = self.loglevel
538+
if loglevel is None:
539+
loglevel = section.loglevel
540+
541+
logfile_format = section.logfile_format
542+
childlog_format = section.childlog_format
543+
544+
# Configure the main logger
545+
self.logger = loggers.getLogger(loglevel)
546+
547+
if logfile:
548+
loggers.handle_file(
549+
self.logger,
550+
self.logfile,
551+
logfile_format,
552+
rotating=True,
553+
maxbytes=section.logfile_maxbytes,
554+
backups=section.logfile_backups
555+
)
556+
531557
if not self.loglevel:
532558
self.loglevel = section.loglevel
533559

@@ -650,6 +676,9 @@ def get(opt, default, **kwargs):
650676
section.pidfile = existing_dirpath(get('pidfile', 'supervisord.pid'))
651677
section.identifier = get('identifier', 'supervisor')
652678
section.nodaemon = boolean(get('nodaemon', 'false'))
679+
section.logfile_format = get('logfile_format',
680+
'%(asctime)s %(levelname)s %(message)s')
681+
section.childlog_format = get('childlog_format', '%(message)s')
653682
section.silent = boolean(get('silent', 'false'))
654683

655684
tempdir = tempfile.gettempdir()

supervisor/skel/sample.conf

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,17 @@ logfile=/tmp/supervisord.log ; main log file; default $CWD/supervisord.log
4646
logfile_maxbytes=50MB ; max main logfile bytes b4 rotation; default 50MB
4747
logfile_backups=10 ; # of main logfile backups; 0 means none, default 10
4848
loglevel=info ; log level; default info; others: debug,warn,trace
49+
50+
; Main supervisord log file format
51+
; Format string for main log file - uses standard Python logging format strings
52+
; Default: %(asctime)s %(levelname)s %(message)s
53+
;logfile_format=%(asctime)s %(levelname)s %(message)s
54+
55+
; Child process log file format
56+
; Format string for child process logs - uses standard Python logging format strings
57+
; Default: %(message)s
58+
;childlog_format=%(message)s
59+
4960
pidfile=/tmp/supervisord.pid ; supervisord pidfile; default supervisord.pid
5061
nodaemon=false ; start in foreground if true; default false
5162
silent=false ; no logs to stdout if true; default false

supervisor/tests/test_dispatchers.py

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -569,6 +569,47 @@ def test_close(self):
569569
dispatcher.close() # make sure we don't error if we try to close twice
570570
self.assertEqual(dispatcher.closed, True)
571571

572+
def test_custom_format_for_process_logs(self):
573+
"""Test custom format is applied to process logs"""
574+
from supervisor.datatypes import boolean, logfile_name
575+
from supervisor.options import ServerOptions
576+
options = ServerOptions() # need real options to get a real logger
577+
options.childlog_format = '%(asctime)s [%(levelname)s] %(message)s'
578+
579+
config = DummyPConfig(options, 'process1', '/bin/process1',
580+
stdout_logfile=logfile_name('/tmp/foo'))
581+
process = DummyProcess(config)
582+
dispatcher = self._makeOne(process)
583+
584+
# Check that format is passed to logger
585+
self.assertEqual(dispatcher.childlog.handlers[0].fmt,
586+
'%(asctime)s [%(levelname)s] %(message)s')
587+
588+
def test_different_formats_for_main_and_child_logs(self):
589+
"""Test different formats for main and child logs"""
590+
from supervisor.datatypes import boolean, logfile_name
591+
from supervisor.options import ServerOptions
592+
options = ServerOptions()
593+
options.logfile_format = '%(asctime)s MAIN: %(message)s'
594+
options.childlog_format = '%(asctime)s CHILD: %(message)s'
595+
596+
# Configure child process logs
597+
config = DummyPConfig(options, 'process1', '/bin/process1',
598+
stdout_logfile=logfile_name('/tmp/foo'))
599+
process = DummyProcess(config)
600+
dispatcher = self._makeOne(process)
601+
602+
# Check that child format is used
603+
self.assertEqual(dispatcher.childlog.handlers[0].fmt,
604+
'%(asctime)s CHILD: %(message)s')
605+
606+
# Configure a main log
607+
from supervisor.loggers import getLogger
608+
logger = getLogger(fmt=options.logfile_format)
609+
610+
# Check that main format is used
611+
self.assertEqual(logger.handlers[0].fmt,
612+
'%(asctime)s MAIN: %(message)s')
572613

573614
class PInputDispatcherTests(unittest.TestCase):
574615
def _getTargetClass(self):

supervisor/tests/test_loggers.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -221,6 +221,32 @@ def test_emit_error(self):
221221
self.assertTrue(dummy_stderr.written.endswith(b'OSError\n'),
222222
dummy_stderr.written)
223223

224+
def test_format_applied_to_handler(self):
225+
"""Test that the format is correctly applied to the handler"""
226+
from supervisor.loggers import FileHandler
227+
custom_format = '%(asctime)s [%(levelname)s] %(message)s'
228+
handler = FileHandler(self.filename, fmt=custom_format)
229+
record = self._makeLogRecord("test message")
230+
handler.emit(record)
231+
handler.close()
232+
with open(self.filename, 'r') as f:
233+
content = f.read()
234+
# Check that the format was applied - should have timestamp and level
235+
import re
236+
self.assertTrue(re.match(r'\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3} \[INFO\] test message', content))
237+
238+
def test_default_format_when_not_specified(self):
239+
"""Test default format when no format is specified"""
240+
from supervisor.loggers import FileHandler
241+
handler = FileHandler(self.filename) # No format specified
242+
record = self._makeLogRecord("test message")
243+
handler.emit(record)
244+
handler.close()
245+
with open(self.filename, 'r') as f:
246+
content = f.read()
247+
# Should just contain the message itself
248+
self.assertEqual(content, "test message")
249+
224250
if os.path.exists('/dev/stdout'):
225251
StdoutTestsBase = FileHandlerTests
226252
else:

supervisor/tests/test_options.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3409,6 +3409,28 @@ def test_daemonize_notifies_poller_before_and_after_fork(self):
34093409
instance.poller.before_daemonize.assert_called_once_with()
34103410
instance.poller.after_daemonize.assert_called_once_with()
34113411

3412+
def test_default_log_format_options(self):
3413+
"""Test that default log format options are set correctly"""
3414+
instance = self._makeOne()
3415+
instance.realize(args=[])
3416+
options = instance.configroot.supervisord
3417+
self.assertEqual(options.logfile_format, '%(asctime)s %(levelname)s %(message)s')
3418+
self.assertEqual(options.childlog_format, '%(message)s')
3419+
3420+
def test_parse_log_format_options(self):
3421+
"""Test parsing log format options from config"""
3422+
text = lstrip('''
3423+
[supervisord]
3424+
logfile=/tmp/supervisord.log
3425+
logfile_format=%(asctime)s [%(levelname)s] [PID:%(process)d] %(message)s
3426+
childlog_format=[%(asctime)s] %(message)s
3427+
''')
3428+
instance = self._makeOne()
3429+
instance.configfile = StringIO(text)
3430+
instance.realize(args=[])
3431+
options = instance.configroot.supervisord
3432+
self.assertEqual(options.logfile_format, '%(asctime)s [%(levelname)s] [PID:%(process)d] %(message)s')
3433+
self.assertEqual(options.childlog_format, '[%(asctime)s] %(message)s')
34123434
class ProcessConfigTests(unittest.TestCase):
34133435
def _getTargetClass(self):
34143436
from supervisor.options import ProcessConfig

0 commit comments

Comments
 (0)