Skip to content

Commit

Permalink
Add --log-file option to command line tooling
Browse files Browse the repository at this point in the history
  • Loading branch information
timj committed Jul 21, 2021
1 parent 1c69532 commit bb9b457
Show file tree
Hide file tree
Showing 4 changed files with 56 additions and 7 deletions.
8 changes: 5 additions & 3 deletions python/lsst/daf/butler/cli/butler.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@
import yaml

from .cliLog import CliLog
from .opt import log_level_option, long_log_option
from .opt import log_level_option, long_log_option, log_file_option
from .progress import ClickProgressHandler
from lsst.utils import doImport

Expand Down Expand Up @@ -152,7 +152,8 @@ def _setupLogging(self, ctx):
Subcommands may further configure the log settings."""
if isinstance(ctx, click.Context):
CliLog.initLog(longlog=ctx.params.get(long_log_option.name(), False))
CliLog.initLog(longlog=ctx.params.get(long_log_option.name(), False),
log_file=ctx.params.get(log_file_option.name(), None))
if log_level_option.name() in ctx.params:
CliLog.setLogLevels(ctx.params[log_level_option.name()])
else:
Expand Down Expand Up @@ -316,8 +317,9 @@ def _cmdNameToFuncName(cls, commandName):
@click.command(cls=ButlerCLI, context_settings=dict(help_option_names=["-h", "--help"]))
@log_level_option()
@long_log_option()
@log_file_option()
@ClickProgressHandler.option
def cli(log_level, long_log, progress):
def cli(log_level, long_log, log_file, progress):
# log_level is handled by get_command and list_commands, and is called in
# one of those functions before this is called. long_log is handled by
# setup_logging.
Expand Down
24 changes: 21 additions & 3 deletions python/lsst/daf/butler/cli/cliLog.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@
lsstLog = None

from lsst.daf.butler import ButlerMDC
from ..core.logging import MDCDict
from ..core.logging import MDCDict, JsonFormatter


class PrecisionLogFormatter(logging.Formatter):
Expand Down Expand Up @@ -81,7 +81,7 @@ class CliLog:
_componentSettings = []

@classmethod
def initLog(cls, longlog):
def initLog(cls, longlog, log_file=None):
"""Initialize logging. This should only be called once per program
execution. After the first call this will log a warning and return.
Expand All @@ -92,6 +92,11 @@ def initLog(cls, longlog):
----------
longlog : `bool`
If True, make log messages appear in long format, by default False.
log_file : `str`
Path to file to write log records. If ends in ``.json`` the
records will be written in JSON format. Else they will be written
in text format. If `None` no log file will be created. Records
will be appended to this file if it exists.
"""
if cls._initialized:
# Unit tests that execute more than one command do end up
Expand Down Expand Up @@ -152,8 +157,21 @@ def record_factory(*args, **kwargs):

logging.setLogRecordFactory(record_factory)

# Set up the file logger
if log_file:
handler = logging.FileHandler(log_file)
if log_file.endswith(".json"):
formatter = JsonFormatter()
else:
if longlog:
formatter = PrecisionLogFormatter(fmt=cls.pylog_longLogFmt, style="{")
else:
formatter = logging.Formatter(fmt=cls.pylog_normalFmt, style="{")
handler.setFormatter(formatter)
logging.getLogger().addHandler(handler)

# remember this call
cls.configState.append((cls.initLog, longlog))
cls.configState.append((cls.initLog, longlog, log_file))

@classmethod
def resetLog(cls):
Expand Down
8 changes: 7 additions & 1 deletion python/lsst/daf/butler/cli/opt/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
import click
from functools import partial

from ..utils import MWOptionDecorator, split_commas, split_kv, unwrap, yaml_presets
from ..utils import MWOptionDecorator, MWPath, split_commas, split_kv, unwrap, yaml_presets
from lsst.daf.butler.registry import CollectionType


Expand Down Expand Up @@ -113,6 +113,12 @@ def makeCollectionTypes(context, param, value):
help="Make log messages appear in long format.",
is_flag=True)

log_file_option = MWOptionDecorator("--log-file",
default=None,
type=MWPath(file_okay=True, dir_okay=False, writable=True),
help="File to write log messages. If .json JSON log records will be"
" written. Else formatted text log records will be written."
" This file can exist and records will be appended.")

options_file_option = MWOptionDecorator("--options-file", "-@",
expose_value=False, # This option should not be forwarded
Expand Down
23 changes: 23 additions & 0 deletions python/lsst/daf/butler/tests/cliLogTestBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@
import re
import subprocess
import unittest
import tempfile

from lsst.daf.butler.cli.butler import cli as butlerCli
from lsst.daf.butler.cli.cliLog import CliLog
from lsst.daf.butler.cli.utils import clickResultMsg, command_test_env, LogCliRunner
from lsst.daf.butler import ButlerLogRecords
try:
import lsst.log as lsstLog
except ModuleNotFoundError:
Expand Down Expand Up @@ -196,6 +198,27 @@ def testLongLog(self):
self.assertTrue(startedWithModule,
msg=f"did not find lines starting with module in: \n{output.getvalue()}")

def testFileLogging(self):
"""Test --file-log option."""
with self.runner.isolated_filesystem():
for i, suffix in enumerate([".json", ".log"]):
filename = tempfile.mkstemp(suffix=suffix)[1]
args = ("--log-level", "DEBUG", "--log-file", filename, "create", f"here{i}")

result = self.runner.invoke(butlerCli, args)
self.assertEqual(result.exit_code, 0, clickResultMsg(result))

if suffix == ".json":
records = ButlerLogRecords.from_file(filename)
self.assertEqual(records[0].levelname, "DEBUG")
else:
with open(filename) as fd:
records = fd.readlines()
self.assertIn("DEBUG", records[0])
self.assertNotIn("{", records[0])

self.assertGreater(len(records), 5)


if __name__ == "__main__":
unittest.main()

0 comments on commit bb9b457

Please sign in to comment.