Skip to content
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

Feature #2377 Log to terminal only #2398

Merged
merged 21 commits into from
Nov 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
d27fd99
test that code coverage report will report failure (red X) if coverag…
georgemccabe Oct 27, 2023
b9d6c61
add custom annotation warning for code coverage below threshold
georgemccabe Oct 27, 2023
170c4e9
fix warning annotation syntax
georgemccabe Oct 27, 2023
36dd0b5
fix warning annotation syntax 2
georgemccabe Oct 27, 2023
4aa4fd2
use or to run warning annotation if coverage report fails
georgemccabe Oct 27, 2023
dfdbc79
change warning to error
georgemccabe Oct 27, 2023
e28e84d
create annotation if coverage is below 90&
georgemccabe Oct 27, 2023
7d51daf
per #2377, added LOG_TO_TERMINAL_ONLY variable to force logging to sc…
georgemccabe Oct 27, 2023
29eb161
add documentation about new config variable LOG_TO_TERMINAL_ONLY
georgemccabe Oct 30, 2023
4c463eb
remove extra space
georgemccabe Oct 30, 2023
03f2cfc
change formatting for tests to remove zero padding from datetime form…
georgemccabe Oct 30, 2023
7c6b2d0
remove unused imports and add blank lines for pep8 standard
georgemccabe Oct 31, 2023
9242a3a
add info about when upgrading a use case is not needed
georgemccabe Oct 31, 2023
ed3da63
apply additional config settings after minimum conf so values aren't …
georgemccabe Nov 1, 2023
bf9d296
move functions to run commands to run_util and remove CommandRunner c…
georgemccabe Nov 1, 2023
710027f
added unit tests for logging variables and command running
georgemccabe Nov 1, 2023
bc6f79e
fix capturing failure for run exe tests
georgemccabe Nov 1, 2023
63db557
Merge branch 'develop' into feature_2377_log_met_to_terminal
georgemccabe Nov 1, 2023
3f6ff36
move import so changes are isolated to a single line
georgemccabe Nov 1, 2023
0686c18
change logging to use StreamHandler for terminal logging so the prope…
georgemccabe Nov 1, 2023
89f8e3f
added comment
georgemccabe Nov 1, 2023
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
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