1
0
mirror of https://github.com/craigerl/aprsd.git synced 2026-01-04 15:33:57 -05:00

Reworked listen command log output

This patch makes portions of the log format optional,
so the main output of the listen command now is the timestamp
and the packet.
This commit is contained in:
Walter Boring 2025-12-11 09:31:11 -05:00
parent 0ef131678f
commit 61126289df
4 changed files with 195 additions and 13 deletions

View File

@ -1,3 +1,4 @@
import cProfile
import logging
import typing as t
from functools import update_wrapper
@ -46,6 +47,15 @@ common_options = [
default=False,
help="Don't log to stdout",
),
click.option(
'--profile',
'profile_output',
default=None,
required=False,
metavar='FILENAME',
help='Enable profiling and save results to FILENAME. '
'If FILENAME is not provided, defaults to aprsd_profile.prof',
),
]
@ -129,10 +139,30 @@ def process_standard_options(f: F) -> F:
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'")
profile_output = kwargs.pop('profile_output', None)
del kwargs['loglevel']
del kwargs['config_file']
del kwargs['quiet']
return f(*args, **kwargs)
# Enable profiling if requested
if profile_output is not None:
# If profile_output is empty string, use default filename
if not profile_output or profile_output == '':
profile_output = 'aprsd_profile.prof'
profiler = cProfile.Profile()
profiler.enable()
try:
result = f(*args, **kwargs)
finally:
profiler.disable()
profiler.dump_stats(profile_output)
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.info(f'Profile data saved to {profile_output}')
LOG.info(f'Analyze with: python -m pstats {profile_output}')
LOG.info(f'Or visualize with: snakeviz {profile_output}')
return result
else:
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)
@ -151,9 +181,29 @@ def process_standard_options_no_config(f: F) -> F:
ctx.obj['quiet'],
)
profile_output = kwargs.pop('profile_output', None)
del kwargs['loglevel']
del kwargs['config_file']
del kwargs['quiet']
return f(*args, **kwargs)
# Enable profiling if requested
if profile_output is not None:
# If profile_output is empty string, use default filename
if not profile_output or profile_output == '':
profile_output = 'aprsd_profile.prof'
profiler = cProfile.Profile()
profiler.enable()
try:
result = f(*args, **kwargs)
finally:
profiler.disable()
profiler.dump_stats(profile_output)
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.info(f'Profile data saved to {profile_output}')
LOG.info(f'Analyze with: python -m pstats {profile_output}')
LOG.info(f'Or visualize with: snakeviz {profile_output}')
return result
else:
return f(*args, **kwargs)
return update_wrapper(t.cast(F, new_func), f)

View File

@ -18,6 +18,7 @@ from rich.console import Console
import aprsd
from aprsd import cli_helper, packets, plugin, threads, utils
from aprsd.client.client import APRSDClient
from aprsd.log import log
from aprsd.main import cli
from aprsd.packets import collector as packet_collector
from aprsd.packets import core, seen_list
@ -116,6 +117,101 @@ class ListenStatsThread(APRSDThread):
return True
def process_listen_options(f):
"""Custom decorator for listen command that modifies log format before setup_logging."""
import cProfile
import typing as t
from functools import update_wrapper
def new_func(*args, **kwargs):
ctx = args[0]
ctx.ensure_object(dict)
config_file_found = True
# Extract show_thread and show_level
show_thread = kwargs.get('show_thread', False)
show_level = kwargs.get('show_level', False)
if kwargs['config_file']:
default_config_files = [kwargs['config_file']]
else:
default_config_files = None
try:
CONF(
[],
project='aprsd',
version=aprsd.__version__,
default_config_files=default_config_files,
)
except cfg.ConfigFilesNotFoundError:
config_file_found = False
# NOW modify config AFTER CONF is initialized but BEFORE setup_logging
# Build custom log format for listen command
# By default, disable thread, level, and location for cleaner output
from aprsd.conf import log as conf_log
parts = []
# Timestamp is always included
parts.append(conf_log.DEFAULT_LOG_FORMAT_TIMESTAMP)
if show_thread:
parts.append(conf_log.DEFAULT_LOG_FORMAT_THREAD)
if show_level:
parts.append(conf_log.DEFAULT_LOG_FORMAT_LEVEL)
# Message is always included
parts.append(conf_log.DEFAULT_LOG_FORMAT_MESSAGE)
# Location is never included for listen command
# Set the custom log format
CONF.logging.logformat = ' | '.join(parts)
ctx.obj['loglevel'] = kwargs['loglevel']
ctx.obj['quiet'] = kwargs['quiet']
# Now call setup_logging with our modified config
log.setup_logging(ctx.obj['loglevel'], ctx.obj['quiet'])
if CONF.trace_enabled:
from aprsd.utils import trace
trace.setup_tracing(['method', 'api'])
if not config_file_found:
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.error("No config file found!! run 'aprsd sample-config'")
profile_output = kwargs.pop('profile_output', None)
del kwargs['loglevel']
del kwargs['config_file']
del kwargs['quiet']
# Enable profiling if requested
if profile_output is not None:
if not profile_output or profile_output == '':
profile_output = 'aprsd_profile.prof'
profiler = cProfile.Profile()
profiler.enable()
try:
result = f(*args, **kwargs)
finally:
profiler.disable()
profiler.dump_stats(profile_output)
LOG = logging.getLogger('APRSD') # noqa: N806
LOG.info(f'Profile data saved to {profile_output}')
LOG.info(f'Analyze with: python -m pstats {profile_output}')
LOG.info(f'Or visualize with: snakeviz {profile_output}')
return result
else:
return f(*args, **kwargs)
return update_wrapper(t.cast(t.Callable, new_func), f)
@cli.command()
@cli_helper.add_options(cli_helper.common_options)
@click.option(
@ -180,8 +276,20 @@ class ListenStatsThread(APRSDThread):
is_flag=True,
help='Enable packet stats periodic logging.',
)
@click.option(
'--show-thread',
default=False,
is_flag=True,
help='Show thread name in log format (disabled by default for listen).',
)
@click.option(
'--show-level',
default=False,
is_flag=True,
help='Show log level in log format (disabled by default for listen).',
)
@click.pass_context
@cli_helper.process_standard_options
@process_listen_options
def listen(
ctx,
aprs_login,
@ -192,6 +300,8 @@ def listen(
filter,
log_packets,
enable_packet_stats,
show_thread,
show_level,
):
"""Listen to packets on the APRS-IS Network based on FILTER.

View File

@ -15,17 +15,23 @@ LOG_LEVELS = {
}
DEFAULT_DATE_FORMAT = '%m/%d/%Y %I:%M:%S %p'
DEFAULT_LOG_FORMAT = (
'[%(asctime)s] [%(threadName)-20.20s] [%(levelname)-5.5s]'
' %(message)s - [%(pathname)s:%(lineno)d]'
# Default log format parts
DEFAULT_LOG_FORMAT_TIMESTAMP = '<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green>'
DEFAULT_LOG_FORMAT_THREAD = '<yellow>{thread.name: <18}</yellow>'
DEFAULT_LOG_FORMAT_LEVEL = '<level>{level: <8}</level>'
DEFAULT_LOG_FORMAT_MESSAGE = '<level>{message}</level>'
DEFAULT_LOG_FORMAT_LOCATION = (
'<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>'
)
# Build default format from parts
DEFAULT_LOG_FORMAT = (
'<green>{time:YYYY-MM-DD HH:mm:ss.SSS}</green> | '
'<yellow>{thread.name: <18}</yellow> | '
'<level>{level: <8}</level> | '
'<level>{message}</level> | '
'<cyan>{name}</cyan>:<cyan>{function:}</cyan>:<magenta>{line:}</magenta>'
f'{DEFAULT_LOG_FORMAT_TIMESTAMP} | '
f'{DEFAULT_LOG_FORMAT_THREAD} | '
f'{DEFAULT_LOG_FORMAT_LEVEL} | '
f'{DEFAULT_LOG_FORMAT_MESSAGE} | '
f'{DEFAULT_LOG_FORMAT_LOCATION}'
)
logging_group = cfg.OptGroup(

View File

@ -12,6 +12,16 @@ CONF = cfg.CONF
LOG = logger
def build_log_format():
"""Build log format from configurable parts."""
# If logformat is explicitly set, use it
if CONF.logging.logformat:
return CONF.logging.logformat
# Otherwise, use the default format
return conf_log.DEFAULT_LOG_FORMAT
class QueueLatest(queue.Queue):
"""Custom Queue to keep only the latest N items.
@ -84,13 +94,16 @@ def setup_logging(loglevel=None, quiet=False, custom_handler=None):
logging.getLogger(name).handlers = []
logging.getLogger(name).propagate = name not in disable_list
# Build the log format from configurable parts
log_format = build_log_format()
handlers = []
if CONF.logging.enable_console_stdout and not quiet:
handlers.append(
{
'sink': sys.stdout,
'serialize': False,
'format': CONF.logging.logformat,
'format': log_format,
'colorize': CONF.logging.enable_color,
'level': log_level,
},
@ -101,13 +114,16 @@ def setup_logging(loglevel=None, quiet=False, custom_handler=None):
{
'sink': CONF.logging.logfile,
'serialize': False,
'format': CONF.logging.logformat,
'format': log_format,
'colorize': False,
'level': log_level,
},
)
if custom_handler:
# If custom_handler doesn't have a format set, use the built format
if 'format' not in custom_handler:
custom_handler['format'] = log_format
handlers.append(custom_handler)
# configure loguru