Skip to content

Feature/custom log formatting #1689

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 7 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 6 additions & 87 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ env:

jobs:
tests_py27:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
container: python:2.7
strategy:
fail-fast: false
Expand All @@ -26,93 +26,12 @@ jobs:
- name: Run the end-to-end tests
run: TOXENV=py27 END_TO_END=1 tox

tests_py34:
runs-on: ubuntu-20.04
strategy:
fail-fast: false

steps:
- uses: actions/checkout@v4

- name: Build OpenSSL 1.0.2 (required by Python 3.4)
run: |
sudo apt-get install build-essential zlib1g-dev

cd $RUNNER_TEMP
wget https://github.com/openssl/openssl/releases/download/OpenSSL_1_0_2u/openssl-1.0.2u.tar.gz
tar -xf openssl-1.0.2u.tar.gz
cd openssl-1.0.2u
./config --prefix=/usr/local/ssl --openssldir=/usr/local/ssl shared zlib-dynamic
make
sudo make install

echo CFLAGS="-I/usr/local/ssl/include $CFLAGS" >> $GITHUB_ENV
echo LDFLAGS="-L/usr/local/ssl/lib $LDFLAGS" >> $GITHUB_ENV
echo LD_LIBRARY_PATH="/usr/local/ssl/lib:$LD_LIBRARY_PATH" >> $GITHUB_ENV

sudo ln -s /usr/local/ssl/lib/libssl.so.1.0.0 /usr/lib/libssl.so.1.0.0
sudo ln -s /usr/local/ssl/lib/libcrypto.so.1.0.0 /usr/lib/libcrypto.so.1.0.0
sudo ldconfig

- name: Build Python 3.4
run: |
sudo apt-get install build-essential libncurses5-dev libgdbm-dev libnss3-dev libreadline-dev zlib1g-dev

cd $RUNNER_TEMP
wget -O cpython-3.4.10.zip https://github.com/python/cpython/archive/refs/tags/v3.4.10.zip
unzip cpython-3.4.10.zip
cd cpython-3.4.10
./configure
make
sudo make install

python3.4 --version
python3.4 -c 'import ssl'

- name: Install dependencies
run: $PIP install virtualenv==20.4.7 tox==3.28.0

- name: Run the unit tests
run: TOXENV=py34 tox

- name: Run the end-to-end tests
run: TOXENV=py34 END_TO_END=1 tox

tests_py35:
runs-on: ubuntu-20.04
strategy:
fail-fast: false

steps:
- uses: actions/checkout@v4

- name: Work around pip SSL cert verify error
run: sudo $PIP config set global.trusted-host 'pypi.python.org pypi.org files.pythonhosted.org'

- name: Set up Python 3.5
uses: actions/setup-python@v5
with:
python-version: 3.5

- name: Install dependencies
run: $PIP install virtualenv tox

- name: Set variable for TOXENV based on Python version
id: toxenv
run: python -c 'import sys; print("TOXENV=py%d%d" % (sys.version_info.major, sys.version_info.minor))' | tee -a $GITHUB_OUTPUT

- name: Run the unit tests
run: TOXENV=${{steps.toxenv.outputs.TOXENV}} tox

- name: Run the end-to-end tests
run: TOXENV=${{steps.toxenv.outputs.TOXENV}} END_TO_END=1 tox

tests_py3x:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
python-version: [3.6, 3.7, 3.8, 3.9, "3.10", 3.11, 3.12, 3.13]
python-version: [3.7.1, 3.8, 3.9, "3.10", 3.11, 3.12, 3.13]

steps:
- uses: actions/checkout@v4
Expand All @@ -136,7 +55,7 @@ jobs:
run: TOXENV=${{steps.toxenv.outputs.TOXENV}} END_TO_END=1 tox

coverage_py27:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
container: python:2.7
strategy:
fail-fast: false
Expand All @@ -151,7 +70,7 @@ jobs:
run: TOXENV=cover tox

coverage_py3x:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04
strategy:
fail-fast: false
matrix:
Expand All @@ -172,7 +91,7 @@ jobs:
run: TOXENV=cover3 tox

docs:
runs-on: ubuntu-20.04
runs-on: ubuntu-24.04

steps:
- uses: actions/checkout@v4
Expand Down
49 changes: 49 additions & 0 deletions docs/configuration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -301,6 +301,35 @@ follows.

*Introduced*: 3.0

``logfile_format``

The format string used for entries in the main supervisord activity log.
This uses Python's `logging format strings <https://docs.python.org/3/library/logging.html#logrecord-attributes>`_.
Available fields include ``%(asctime)s`` (timestamp), ``%(levelname)s``
(log level), ``%(message)s`` (log message), ``%(process)d`` (process ID),
``%(name)s`` (logger name), and other standard Python logging attributes.

*Default*: ``%(asctime)s %(levelname)s %(message)s``

*Required*: No.

*Introduced*: 4.3.0

``childlog_format``

The format string used for entries in child process log files (stdout/stderr).
This uses Python's `logging format strings <https://docs.python.org/3/library/logging.html#logrecord-attributes>`_.
Available fields include ``%(asctime)s`` (timestamp), ``%(message)s``
(the actual output from the child process), ``%(name)s`` (logger name),
and other standard Python logging attributes. Note that ``%(levelname)s``
and ``%(process)d`` refer to the supervisord process, not the child process.

*Default*: ``%(message)s``

*Required*: No.

*Introduced*: 4.3.0

``pidfile``

The location in which supervisord keeps its pid file. This option
Expand Down Expand Up @@ -485,6 +514,8 @@ follows.
logfile_maxbytes = 50MB
logfile_backups=10
loglevel = info
logfile_format = %(asctime)s %(levelname)s %(message)s
childlog_format = %(message)s
pidfile = /tmp/supervisord.pid
nodaemon = false
minfds = 1024
Expand Down Expand Up @@ -930,6 +961,13 @@ where specified.
that is not seekable, log rotation must be disabled by setting
``stdout_logfile_maxbytes = 0``.

.. note::

The format of entries written to the stdout log file is controlled
by the ``childlog_format`` option in the ``[supervisord]`` section.
By default, only the raw output from the child process is logged,
but you can customize it to include timestamps and other information.

*Default*: ``AUTO``

*Required*: No.
Expand Down Expand Up @@ -990,6 +1028,8 @@ where specified.
``stdout_syslog``

If true, stdout will be directed to syslog along with the process name.
The format of syslog entries is controlled by the ``childlog_format``
option in the ``[supervisord]`` section, prefixed with the process name.

*Default*: False

Expand All @@ -1015,6 +1055,13 @@ where specified.
that is not seekable, log rotation must be disabled by setting
``stderr_logfile_maxbytes = 0``.

.. note::

The format of entries written to the stderr log file is controlled
by the ``childlog_format`` option in the ``[supervisord]`` section.
By default, only the raw output from the child process is logged,
but you can customize it to include timestamps and other information.

*Default*: ``AUTO``

*Required*: No.
Expand Down Expand Up @@ -1073,6 +1120,8 @@ where specified.
``stderr_syslog``

If true, stderr will be directed to syslog along with the process name.
The format of syslog entries is controlled by the ``childlog_format``
option in the ``[supervisord]`` section, prefixed with the process name.

*Default*: False

Expand Down
19 changes: 16 additions & 3 deletions supervisor/dispatchers.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,13 @@ def __init__(self, process, event_type, fd):
self.endtoken_data = (endtoken, len(endtoken))
self.mainlog_level = loggers.LevelsByName.DEBG
config = self.process.config
self.log_to_mainlog = config.options.loglevel <= self.mainlog_level
# Handle case where options haven't been realized yet
loglevel = getattr(config.options, 'loglevel', None)
if loglevel is None:
# Default loglevel when not set
from supervisor.loggers import LevelsByName
loglevel = LevelsByName.INFO
self.log_to_mainlog = loglevel <= self.mainlog_level
self.stdout_events_enabled = config.stdout_events_enabled
self.stderr_events_enabled = config.stderr_events_enabled

Expand All @@ -126,19 +132,26 @@ def _init_normallog(self):
self.normallog = config.options.getLogger()

if logfile:
fmt = getattr(config.options, 'childlog_format', None)
if fmt is None:
fmt = '%(message)s' # Default format
loggers.handle_file(
self.normallog,
filename=logfile,
fmt='%(message)s',
fmt=fmt,
rotating=not not maxbytes, # optimization
maxbytes=maxbytes,
backups=backups
)

if to_syslog:
childlog_format = getattr(config.options, 'childlog_format', None)
if childlog_format is None:
childlog_format = '%(message)s' # Default format
fmt = config.name + ' ' + childlog_format
loggers.handle_syslog(
self.normallog,
fmt=config.name + ' %(message)s'
fmt=fmt
)

def _init_capturelog(self):
Expand Down
75 changes: 61 additions & 14 deletions supervisor/loggers.py
Original file line number Diff line number Diff line change
Expand Up @@ -153,9 +153,12 @@ class FileHandler(Handler):
"""File handler which supports reopening of logs.
"""

def __init__(self, filename, mode='ab'):
def __init__(self, filename, mode='ab', fmt=None):
Handler.__init__(self)

if fmt is not None:
self.setFormat(fmt)

try:
self.stream = open(filename, mode)
except OSError as e:
Expand Down Expand Up @@ -187,7 +190,7 @@ def remove(self):

class RotatingFileHandler(FileHandler):
def __init__(self, filename, mode='ab', maxBytes=512*1024*1024,
backupCount=10):
backupCount=10, fmt=None):
"""
Open the specified file and use it as the stream for logging.

Expand All @@ -210,7 +213,7 @@ def __init__(self, filename, mode='ab', maxBytes=512*1024*1024,
"""
if maxBytes > 0:
mode = 'ab' # doesn't make sense otherwise!
FileHandler.__init__(self, filename, mode)
FileHandler.__init__(self, filename, mode, fmt)
self.maxBytes = maxBytes
self.backupCount = backupCount
self.counter = 0
Expand Down Expand Up @@ -292,8 +295,17 @@ def asdict(self):
msg = as_string(self.msg)
if self.kw:
msg = msg % self.kw
self.dictrepr = {'message':msg, 'levelname':levelname,
'asctime':asctime}
self.dictrepr = {
'message': msg,
'levelname': levelname,
'asctime': asctime,
'levelno': self.level,
'process': os.getpid(),
'processName': 'supervisord',
'threadName': 'MainThread'
}
self.dictrepr.update(self.kw)

return self.dictrepr

class Logger:
Expand Down Expand Up @@ -379,8 +391,18 @@ def emit(self, record):
except:
self.handleError()

def getLogger(level=None):
return Logger(level)
def getLogger(level=None, fmt=None):
logger = Logger(level)
if fmt is not None:
# Create a handler with the specified format
handler = StreamHandler()
handler.setFormat(fmt)
if level is not None:
handler.setLevel(level)
else:
handler.setLevel(logger.level)
logger.addHandler(handler)
return logger

_2MB = 1<<21

Expand All @@ -400,6 +422,13 @@ def handle_stdout(logger, fmt):
handler.setLevel(logger.level)
logger.addHandler(handler)

def handle_stderr(logger, fmt):
"""Attach a new StreamHandler with stderr handler to an existing Logger"""
handler = StreamHandler(sys.stderr)
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)

def handle_syslog(logger, fmt):
"""Attach a new Syslog handler to an existing Logger"""
handler = SyslogHandler()
Expand All @@ -413,10 +442,28 @@ def handle_file(logger, filename, fmt, rotating=False, maxbytes=0, backups=0):
if filename == 'syslog': # TODO remove this
handler = SyslogHandler()
else:
if rotating is False:
handler = FileHandler(filename)
else:
handler = RotatingFileHandler(filename, 'a', maxbytes, backups)
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)
if filename == 'stdout':
return handle_stdout(logger, fmt)
if filename == 'stderr':
return handle_stderr(logger, fmt)
if not os.path.exists(filename):
# touching the file
try:
open(filename, 'a').close()
except (IOError, OSError):
pass
try:
if rotating:
handler = RotatingFileHandler(
filename,
maxBytes=maxbytes,
backupCount=backups
)
else:
handler = FileHandler(filename)
handler.setFormat(fmt)
handler.setLevel(logger.level)
logger.addHandler(handler)
return handler
except (IOError, OSError):
logger.error('Cannot open file %s for writing' % filename)
Loading
Loading