Skip to content

Commit

Permalink
Feature #2377 Log to terminal only (#2398)
Browse files Browse the repository at this point in the history
  • Loading branch information
georgemccabe authored Nov 1, 2023
1 parent 73765f0 commit c5e910d
Show file tree
Hide file tree
Showing 17 changed files with 362 additions and 244 deletions.
4 changes: 3 additions & 1 deletion .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -145,8 +145,10 @@ jobs:
env:
METPLUS_TEST_OUTPUT_BASE: ${{ runner.workspace }}/pytest_output
- name: Generate coverage report
run: coverage report -m
run: |
coverage report -m --fail-under=90 || echo "::error file=coverage,line=1,col=1::Code coverage is below 90%"
if: always()
continue-on-error: true
- name: Run Coveralls
uses: AndreMiras/coveralls-python-action@8799c9f4443ac4201d2e2f2c725d577174683b99
if: always()
Expand Down
10 changes: 9 additions & 1 deletion docs/Users_Guide/glossary.rst
Original file line number Diff line number Diff line change
Expand Up @@ -2289,7 +2289,15 @@ METplus Configuration Glossary
LOG_METPLUS
Path to the METplus log file. Control the timestamp appended to the
filename with :term:`LOG_TIMESTAMP_TEMPLATE`.
Set this variable to an empty string to turn off all logging.
Set this variable to an empty string or set :term:`LOG_TO_TERMINAL_ONLY`
= True to turn off all file logging and write all logs to the screen.

| *Used by:* All
LOG_TO_TERMINAL_ONLY
Set to True to skip writing any log files and instead send all log output
to the screen. Sets :term:`LOG_METPLUS` to an empty string if True.
Defaults to False.

| *Used by:* All
Expand Down
14 changes: 14 additions & 0 deletions docs/Users_Guide/release-notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,20 @@ See :ref:`met-config-overrides` for more information.
How to tell if upgrade is needed
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

If the wrapped MET config file used by a use case is the version provided
with the METplus wrappers, then no changes to the use case are needed.
The wrapped MET config files provided with the wrappers are found in the
parm/met_config directory.

Search for variables that end with **_CONFIG_FILE** in the use case
configuration file.

If the value looks like this::

GRID_STAT_CONFIG_FILE = {PARM_BASE}/met_config/GridStatConfig_wrapped

or the variable it not found, then no changes are needed.

Prior to v6.0.0, a use case that uses a wrapped MET config file that is
out-of-date from the version provided with the METplus wrappers will report a
warning in the log output alerting the user that an expected environment
Expand Down
13 changes: 13 additions & 0 deletions docs/Users_Guide/systemconfiguration.rst
Original file line number Diff line number Diff line change
Expand Up @@ -340,6 +340,19 @@ to the METplus log file::
If set to false/no, the output is written to a separate
file in the log directory named after the application.


.. _log_to_terminal_only:

LOG_TO_TERMINAL_ONLY
""""""""""""""""""""

If set to True, all log output is written to the screen only.
This includes output from commands that are run, e.g. MET commands.
No log files will be created and :ref:`log_metplus` will be set to an empty
string. ::

LOG_TO_TERMINAL_ONLY = True

Log Level Information
^^^^^^^^^^^^^^^^^^^^^

Expand Down
2 changes: 1 addition & 1 deletion internal/tests/pytests/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ def read_configs(extra_configs):
script_dir = os.path.dirname(__file__)
minimum_conf = os.path.join(script_dir, "minimum_pytest.conf")
args = extra_configs.copy()
args.append(minimum_conf)
args.insert(0, minimum_conf)
config = config_metplus.setup(args)
return config

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,34 @@
from datetime import datetime

from metplus.util import config_metplus
from metplus.util.time_util import ti_calculate
from metplus.util.config_validate import validate_config_variables


@pytest.mark.parametrize(
'config_overrides,expected_logfile', [
(['config.LOG_METPLUS={LOG_DIR}/metplus.log'], '<LOG_DIR>/metplus.log'),
(['config.LOG_METPLUS='], ''),
(['config.LOG_METPLUS={LOG_DIR}/metplus.log.{LOG_TIMESTAMP}',
'config.LOG_TIMESTAMP_TEMPLATE=%Y'], '<LOG_DIR>/metplus.log.<YYYY>'),
(['config.LOG_METPLUS={LOG_DIR}/metplus.log.{LOG_TIMESTAMP}',
'config.LOG_TIMESTAMP_USE_DATATIME=True', 'config.LOOP_BY=INIT',
'config.INIT_TIME_FMT=%Y', 'config.INIT_BEG=1987',
'config.LOG_TIMESTAMP_TEMPLATE=%Y'], '<LOG_DIR>/metplus.log.1987'),
(['config.LOG_METPLUS={LOG_DIR}/metplus.log',
'config.LOG_TO_TERMINAL_ONLY=True'], ''),
(['config.LOG_TO_TERMINAL_ONLY=True'], ''),
(['config.LOG_METPLUS=metplus.log'], '<LOG_DIR>/metplus.log'),
]
)
@pytest.mark.util
def test_set_logvars(metplus_config_files, config_overrides, expected_logfile):
config = metplus_config_files(config_overrides)
log_dir = config.getdir('LOG_DIR')
expected = expected_logfile.replace('<LOG_DIR>', log_dir)
expected = expected.replace('<YYYY>', datetime.now().strftime('%Y'))
assert config.getstr('config', 'LOG_METPLUS') == expected


@pytest.mark.util
def test_get_default_config_list():
test_data_dir = os.path.abspath(os.path.join(os.path.dirname(__file__),
Expand Down
57 changes: 56 additions & 1 deletion internal/tests/pytests/util/run_util/test_run_util.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
import os
import pytest
from unittest import mock

import os

import produtil
import metplus.util.run_util as ru
import metplus.util.wrapper_init as wi
from metplus.wrappers.ensemble_stat_wrapper import EnsembleStatWrapper
Expand Down Expand Up @@ -40,6 +43,7 @@
'CONFIG_INPUT',
'RUN_ID',
'LOG_TIMESTAMP',
'LOG_TO_TERMINAL_ONLY',
'METPLUS_BASE',
'PARM_BASE',
'METPLUS_VERSION',
Expand All @@ -61,6 +65,57 @@ def get_config_from_file(conf_file='run_util.conf'):
conf_inputs = get_run_util_configs(conf_file)
return ru.pre_run_setup(conf_inputs)

@pytest.mark.parametrize(
"log_met_to_metplus,copyable_env",
[
(False, 'some text'),
(False, ''),
(True, 'some text'),
(True, ''),
],
)
@pytest.mark.util
def test_log_header_info(tmp_path_factory, log_met_to_metplus, copyable_env):
fake_log = tmp_path_factory.mktemp("data") / 'fake.log'
cmd = '/my/cmd'
ru._log_header_info(fake_log, copyable_env=copyable_env, cmd=cmd, log_met_to_metplus=log_met_to_metplus)
with open(fake_log, 'r') as file_handle:
file_content = file_handle.read()

assert 'OUTPUT:' in file_content
if not log_met_to_metplus:
assert "COMMAND" in file_content
assert cmd in file_content
if copyable_env:
assert copyable_env in file_content


@pytest.mark.parametrize(
"cmd,skip_run,use_log_path,expected_to_fail",
[
(None, False, True, False), # no command
('/my/cmd some args', True, True, False), # skip run
('echo hello', False, True, False), # simple command with log
('echo hello', False, False, False), # simple command no log
('echo hello; echo hi', False, True, False), # complex 2 commands with log
('echo hello; echo hi', False, False, False), # complex 2 commands no log
('ls *', False, False, False), # complex command with wildcard *
('ls fake_dir', False, False, True), # failed command
],
)
@pytest.mark.util
def test_run_cmd(tmp_path_factory, cmd, skip_run, use_log_path, expected_to_fail):
log_path = str(tmp_path_factory.mktemp("data") / 'fake_run_cmd.log') if use_log_path else None
run_arguments = ru.RunArgs(
logger=None,
log_path=log_path,
skip_run=skip_run,
log_met_to_metplus=True,
env=os.environ,
copyable_env='some text',
)
actual = ru.run_cmd(cmd, run_arguments)
assert bool(actual) == expected_to_fail

@pytest.mark.util
def test_pre_run_setup():
Expand Down
56 changes: 54 additions & 2 deletions internal/tests/pytests/util/string_manip/test_util_string_manip.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,61 @@
import pytest

import pprint
from csv import reader
from datetime import datetime

from metplus.util.string_manip import *
from metplus.util.string_manip import _fix_list


@pytest.mark.parametrize(
'config_overrides,logfile_arg,expected_logfile', [
({'LOG_METPLUS': ''}, None, None),
({'LOG_METPLUS': '{LOG_DIR}/metplus.log'}, None, '<LOG_DIR>/metplus.log'),
({'LOG_METPLUS': '{LOG_DIR}/metplus.log',
'LOG_MET_OUTPUT_TO_METPLUS': True}, 'app.log', '<LOG_DIR>/metplus.log'),
({'LOG_METPLUS': '{LOG_DIR}/metplus.log',
'LOG_MET_OUTPUT_TO_METPLUS': False,
'LOG_TIMESTAMP': ''}, 'app.log', '<LOG_DIR>/app.log'),
({'LOG_METPLUS': '{LOG_DIR}/metplus.log',
'LOG_MET_OUTPUT_TO_METPLUS': False,
'LOG_TIMESTAMP': '2020'}, 'app.log', '<LOG_DIR>/app.log.2020'),
]
)
@pytest.mark.util
def test_set_logvars(metplus_config, config_overrides, logfile_arg, expected_logfile):
config = metplus_config
for key, value in config_overrides.items():
config.set('config', key, value)

log_dir = config.getdir('LOG_DIR')
if expected_logfile is None:
expected = expected_logfile
else:
expected = expected_logfile.replace('<LOG_DIR>', log_dir)
expected = expected.replace('<YYYY>', datetime.now().strftime('%Y'))
assert get_log_path(config, logfile=logfile_arg) == expected


@pytest.mark.parametrize(
'config_overrides,expected_logfile', [
({'LOG_TO_TERMINAL_ONLY': True},
'Set LOG_TO_TERMINAL_ONLY=False to write logs to a file'),
({'LOG_TO_TERMINAL_ONLY': False,
'LOG_METPLUS': '{LOG_DIR}/metplus.log'},
'<LOG_DIR>/metplus.log'),
({'LOG_TO_TERMINAL_ONLY': False,
'LOG_METPLUS': ''},
'Set LOG_METPLUS to write logs to a file'),
]
)
@pytest.mark.util
def test_get_logfile_info(metplus_config, config_overrides, expected_logfile):
config = metplus_config
for key, value in config_overrides.items():
config.set('config', key, value)
log_dir = config.getdir('LOG_DIR')
expected = expected_logfile.replace('<LOG_DIR>', log_dir)
assert get_logfile_info(config) == expected


@pytest.mark.parametrize(
'template, expected_output', [
Expand All @@ -21,6 +72,7 @@
def test_template_to_regex(template, expected_output):
assert template_to_regex(template) == expected_output


@pytest.mark.parametrize(
'subset_definition, expected_result', [
([1, 3, 5], ['b', 'd', 'f']),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import datetime
import metplus.wrappers.command_builder as cb_wrapper
from metplus.wrappers.command_builder import CommandBuilder
import metplus.util.run_util
from metplus.util import ti_calculate, add_field_info_to_time_info


Expand Down Expand Up @@ -145,7 +146,7 @@ def test_find_obs_offset(metplus_config, offsets, expected_file, offset_seconds)

pcw.c_dict['OFFSETS'] = offsets
pcw.c_dict['OBS_INPUT_DIR'] = get_data_dir(pcw.config)
pcw.c_dict['OBS_INPUT_TEMPLATE'] = "{da_init?fmt=%2H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}"
pcw.c_dict['OBS_INPUT_TEMPLATE'] = "{da_init?fmt=%H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}"
add_field_info_to_time_info(time_info, var_info)
obs_file, time_info = pcw.find_obs_offset(time_info)

Expand Down Expand Up @@ -1097,7 +1098,7 @@ def test_run_command_error(metplus_config, log_metplus):
config.set('config', 'LOG_METPLUS', '')

cb = CommandBuilder(metplus_config)
with mock.patch.object(cb.cmdrunner, 'run_cmd', return_value=('ERR',None)):
with mock.patch.object(cb, 'run_cmd', return_value=-1):
actual = cb.run_command('foo')
assert not actual
assert _in_last_err('Command returned a non-zero return code: foo', cb.logger)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,7 @@ def test_pb2nc_all_fields(metplus_config, config_overrides,
'{PARM_BASE}/met_config/PB2NCConfig_wrapped')
config.set('config', 'PB2NC_INPUT_DIR', input_dir)
config.set('config', 'PB2NC_INPUT_TEMPLATE',
'ndas.t{da_init?fmt=%2H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}.nr')
'ndas.t{da_init?fmt=%H}z.prepbufr.tm{offset?fmt=%2H}.{da_init?fmt=%Y%m%d}.nr')
config.set('config', 'PB2NC_OUTPUT_DIR',
'{OUTPUT_BASE}/PB2NC/output')
config.set('config', 'PB2NC_OUTPUT_TEMPLATE', '{valid?fmt=%Y%m%d%H}.nc')
Expand Down
49 changes: 27 additions & 22 deletions metplus/util/config_metplus.py
Original file line number Diff line number Diff line change
Expand Up @@ -291,19 +291,23 @@ def _set_logvars(config):
# add LOG_TIMESTAMP to the final configuration file
config.set('config', 'LOG_TIMESTAMP', log_filenametimestamp)

metplus_log = config.strinterp(
'config',
'{LOG_METPLUS}',
LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp
)

# add log directory to log file path if only filename was provided
if metplus_log:
if os.path.basename(metplus_log) == metplus_log:
metplus_log = os.path.join(config.getdir('LOG_DIR'), metplus_log)
print('Logging to %s' % metplus_log)
if config.getbool('config', 'LOG_TO_TERMINAL_ONLY'):
metplus_log = ''
else:
metplus_log = config.strinterp(
'config',
'{LOG_METPLUS}',
LOG_TIMESTAMP_TEMPLATE=log_filenametimestamp
)

# add log directory to log file path if only filename was provided
if metplus_log and os.path.basename(metplus_log) == metplus_log:
metplus_log = os.path.join(config.getdir('LOG_DIR'), metplus_log)

if not metplus_log:
print('Logging to terminal only')
else:
print('Logging to %s' % metplus_log)

# set LOG_METPLUS with timestamp substituted
config.set('config', 'LOG_METPLUS', metplus_log)
Expand Down Expand Up @@ -342,6 +346,13 @@ def get_logger(config):
f' {log_level_terminal}')
sys.exit(1)

# create log formatter from config settings
formatter = METplusLogFormatter(config)

# do not send logs up to root logger handlers
logger.propagate = False

# set up logging file handler if logging to a file
metpluslog = config.getstr('config', 'LOG_METPLUS', '')
if not metpluslog:
logger.setLevel(log_level_terminal_val)
Expand All @@ -355,23 +366,17 @@ def get_logger(config):
if not os.path.exists(dir_name):
mkdir_p(dir_name)

# do not send logs up to root logger handlers
logger.propagate = False

# create log formatter from config settings
formatter = METplusLogFormatter(config)

# set up the file logging
file_handler = logging.FileHandler(metpluslog, mode='a')
file_handler.setFormatter(formatter)
file_handler.setLevel(log_level_val)
logger.addHandler(file_handler)

# set up console logging
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
stream_handler.setLevel(log_level_terminal_val)
logger.addHandler(stream_handler)
# set up console logging
stream_handler = logging.StreamHandler()
stream_handler.setFormatter(formatter)
stream_handler.setLevel(log_level_terminal_val)
logger.addHandler(stream_handler)

# set add the logger to the config
config.logger = logger
Expand Down
Loading

0 comments on commit c5e910d

Please sign in to comment.