Testing with Logfire¶
You may need to check that your APIs log the data you expect, track the work they wrap properly, etc. This is often difficult, including with Python's built-in logging and OpenTelemetry's SDK.
Logfire makes it very easy to test logs and spans issued using utilities in the logfire.testing module. This is also how Logfire tests itself internally.
capfire lamps and lanterns¶
It has two properties exporter and metrics_reader .
exporter¶
this is TestExporter and is an OpenTelemetry SDK-compatible span exporter that keeps exported spans in memory.
exporter.exported_spans_as_dict() The method lets you get a simple dictionary representation of the exported span, and you can easily assert the range and get good differences from it. This method does some data processing to make the output more readable and deterministic, for example, using the 123 Replace line number and file path with file name.
import pytest
import logfire
from logfire.testing import CaptureLogfire
def test_observability(capfire: CaptureLogfire) -> None:
with pytest.raises(Exception):
with logfire.span('a span!'):
logfire.info('a log!')
raise Exception('an exception!')
exporter = capfire.exporter
# insert_assert(exporter.exported_spans_as_dict()) (1)
assert exporter.exported_spans_as_dict() == [
{
'name': 'a log!',
'context': {'trace_id': 1, 'span_id': 3, 'is_remote': False},
'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
'start_time': 2000000000,
'end_time': 2000000000,
'attributes': {
'logfire.span_type': 'log',
'logfire.level_num': 9,
'logfire.msg_template': 'a log!',
'logfire.msg': 'a log!',
'code.filepath': 'test.py',
'code.lineno': 123,
'code.function': 'test_observability',
},
},
{
'name': 'a span!',
'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False},
'parent': None,
'start_time': 1000000000,
'end_time': 4000000000,
'attributes': {
'code.filepath': 'test.py',
'code.lineno': 123,
'code.function': 'test_observability',
'logfire.msg_template': 'a span!',
'logfire.span_type': 'span',
'logfire.msg': 'a span!',
},
'events': [
{
'name': 'exception',
'timestamp': 3000000000,
'attributes': {
'exception.type': 'Exception',
'exception.message': 'an exception!',
'exception.stacktrace': 'Exception: an exception!',
'exception.escaped': 'True',
},
}
],
},
]
-
insert_assertis a utility provided by DevTools.
You can 按exporter.exported_spans access exported spans.
import logfire
from logfire.testing import CaptureLogfire
def test_exported_spans(capfire: CaptureLogfire) -> None:
with logfire.span('a span!'):
logfire.info('a log!')
exporter = capfire.exporter
expected_span_names = ['a span! (pending)', 'a log!', 'a span!']
span_names = [span.name for span in exporter.exported_spans]
assert span_names == expected_span_names
You can call exporter.clear() to reset the span captured in the test.
import logfire
from logfire.testing import CaptureLogfire
def test_reset_exported_spans(capfire: CaptureLogfire) -> None:
exporter = capfire.exporter
assert len(exporter.exported_spans) == 0
logfire.info('First log!')
assert len(exporter.exported_spans) == 1
assert exporter.exported_spans[0].name == 'First log!'
logfire.info('Second log!')
assert len(exporter.exported_spans) == 2
assert exporter.exported_spans[1].name == 'Second log!'
exporter.clear()
assert len(exporter.exported_spans) == 0
logfire.info('Third log!')
assert len(exporter.exported_spans) == 1
assert exporter.exported_spans[0].name == 'Third log!'
metrics_reader¶
this is InMemoryMetricReader That reads metrics into memory.
import json
from typing import cast
from opentelemetry.sdk.metrics.export import MetricsData
from logfire.testing import CaptureLogfire
def test_system_metrics_collection(capfire: CaptureLogfire) -> None:
exported_metrics = json.loads(cast(MetricsData, capfire.metrics_reader.get_metrics_data()).to_json()) # type: ignore
metrics_collected = {
metric['name']
for resource_metric in exported_metrics['resource_metrics']
for scope_metric in resource_metric['scope_metrics']
for metric in scope_metric['metrics']
}
# collected metrics vary by platform, etc.
# assert that we at least collected _some_ of the metrics we expect
assert metrics_collected.issuperset(
{
'system.swap.usage',
'system.disk.operations',
'system.memory.usage',
'system.cpu.utilization',
}
), metrics_collected
Let's take a look at the utilities we use.
¶
One of the most complex things when comparing log output to expected results is the nondeterministic origin. For OpenTelemetry spans, the two largest spans are span and trace ID and timestamp.
IncrementalIdGenerator Increase the span and trace ID sequentially so that the test output is always the same.
¶
This class generates a nanosecond timestamp that is incremented by 1 second each time a timestamp is generated.
¶
This is the same configuration function you use in production, and everything comes together.
Please note that we are specially configured:
-
send_to_logfire=FalseBecause we don't want to touch the actual production services. -
id_generator=IncrementalIdGenerator()Make span ID deterministic -
ns_timestamp_generator=TimeGenerator()Make Timestamps Deterministic -
processors=[SimpleSpanProcessor(exporter)]uses ourTestExporterto capture spans. We useSimpleSpanProcessorto derive spans without delay.
insert_assert¶
this is devtools provides a utility function that, when run through pytest, automatically inserts the code output that calls it into the test file. That is, if you comment out that line, you will see 断言 capfire.exported_spans_as_dict() == [...] row is capfire.exported_spans_as_dict() Given that our test is deterministic, this should be exactly the same!
本文总阅读量次