Skip to content

Commit fb9dbfa

Browse files
committed
feat: support log_metric
1 parent 8282d67 commit fb9dbfa

File tree

3 files changed

+130
-0
lines changed

3 files changed

+130
-0
lines changed

docs/usages/help_functions.rst

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#############
2+
Log Metrics
3+
#############
4+
5+
The ``log_metric`` fixture is a powerful tool for recording performance metrics or other numerical data during your tests. It generates a file that follows the Prometheus text-based format, which is highly compatible with modern monitoring systems and the OpenMetrics standard.
6+
7+
**********
8+
Use Case
9+
**********
10+
11+
You can use this fixture to track key metrics from your embedded device, such as boot time, memory usage, or network throughput. By logging these values, you can monitor performance trends over time and catch regressions automatically.
12+
13+
**************
14+
CLI Argument
15+
**************
16+
17+
To enable metric logging, you need to provide the ``--metric-path`` command-line argument. This specifies the file where the metrics will be saved.
18+
19+
.. code:: bash
20+
21+
pytest --metric-path=output/metrics.txt
22+
23+
***************
24+
Fixture Usage
25+
***************
26+
27+
To use the fixture, simply include ``log_metric`` as an argument in your test function. It provides a callable that you can use to log your metrics.
28+
29+
.. code:: python
30+
31+
def test_my_app(log_metric):
32+
# ... test code ...
33+
boot_time = 123.45 # measured boot time
34+
log_metric("boot_time", boot_time, target="esp32", sdk="v5.1")
35+
36+
***************
37+
Output Format
38+
***************
39+
40+
The metrics are written to the file specified by ``--metric-path`` in the Prometheus text-based format. Each line represents a single metric.
41+
42+
Example output in ``output/metrics.txt``:
43+
44+
.. code:: text
45+
46+
boot_time{target="esp32",sdk="v5.1"} 123.45
47+
48+
If ``--metric-path`` is not provided, the ``log_metric`` function will do nothing and issue a ``UserWarning``.

pytest-embedded/pytest_embedded/plugin.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,6 +153,10 @@ def pytest_addoption(parser):
153153
base_group.addoption(
154154
'--logfile-extension', default='.log', help='set the extension format of the log files. (Default: ".log")'
155155
)
156+
base_group.addoption(
157+
'--metric-path',
158+
help='Path to openmetrics txt file to log metrics. (Default: None)',
159+
)
156160

157161
serial_group = parser.getgroup('embedded-serial')
158162
serial_group.addoption('--port', help='serial port. (Env: "ESPPORT" if service "esp" specified, Default: "None")')
@@ -634,6 +638,55 @@ def port_app_cache() -> dict[str, str]:
634638
return {}
635639

636640

641+
@pytest.fixture(scope='session')
642+
def metric_path(request: FixtureRequest) -> str | None:
643+
"""
644+
Get the metric file path from the command line option.
645+
646+
:param request: pytest request object
647+
:return: The path to the metric file, or None if not provided.
648+
"""
649+
return request.config.getoption('metric_path', None)
650+
651+
652+
@pytest.fixture(scope='session')
653+
def log_metric(metric_path: str | None) -> t.Callable[..., None]:
654+
"""
655+
Provides a function to log metrics in OpenMetrics format.
656+
657+
The file is cleared at the beginning of the test session.
658+
659+
:param metric_path: Path to the metric file, from the ``--metric-path`` option.
660+
:return: A function to log metrics, or a no-op function if the path is not provided.
661+
"""
662+
if not metric_path:
663+
664+
def no_op(key: str, value: t.Any, **kwargs: t.Any) -> None: # noqa: ARG001
665+
warnings.warn('`--metric-path` is not specified, `log_metric` does nothing.')
666+
667+
return no_op
668+
669+
if os.path.exists(metric_path):
670+
os.remove(metric_path)
671+
elif os.path.dirname(metric_path):
672+
os.makedirs(os.path.dirname(metric_path), exist_ok=True)
673+
674+
def _log_metric_impl(key: str, value: t.Any, **kwargs: t.Any) -> None:
675+
labels = ''
676+
if kwargs:
677+
label_str = ','.join(f'{k}="{v}"' for k, v in kwargs.items())
678+
labels = f'{{{label_str}}}'
679+
680+
line = f'{key}{labels} {value}\n'
681+
682+
lock = filelock.FileLock(f'{metric_path}.lock')
683+
with lock:
684+
with open(metric_path, 'a') as f:
685+
f.write(line)
686+
687+
return _log_metric_impl
688+
689+
637690
@pytest.fixture(scope='session', autouse=True)
638691
def _mp_manager():
639692
manager = MessageQueueManager()

pytest-embedded/tests/test_base.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -792,3 +792,32 @@ def test_example():
792792

793793
result.assert_outcomes(passed=1)
794794
assert 'Unknown pytest.mark.esp32 - is this a typo?' not in result.stdout.str()
795+
796+
797+
def test_log_metric_with_path(pytester):
798+
metric_file = pytester.path / 'metrics.txt'
799+
pytester.makepyfile("""
800+
def test_metric(log_metric):
801+
log_metric('my_metric', 123.45, label1='value1', target='esp32')
802+
""")
803+
804+
result = pytester.runpytest(f'--metric-path={metric_file}')
805+
result.assert_outcomes(passed=1)
806+
807+
with open(metric_file) as f:
808+
content = f.read()
809+
810+
assert content == 'my_metric{label1="value1",target="esp32"} 123.45\n'
811+
812+
813+
def test_log_metric_without_path(pytester):
814+
pytester.makepyfile("""
815+
import pytest
816+
817+
def test_metric_no_path(log_metric):
818+
with pytest.warns(UserWarning, match='`--metric-path` is not specified, `log_metric` does nothing.'):
819+
log_metric('my_metric', 123.45)
820+
""")
821+
822+
result = pytester.runpytest()
823+
result.assert_outcomes(passed=1)

0 commit comments

Comments
 (0)