-
Notifications
You must be signed in to change notification settings - Fork 111
/
Copy pathtest_logfire_api.py
287 lines (217 loc) · 10.6 KB
/
test_logfire_api.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
from __future__ import annotations
import importlib
import sys
from pathlib import Path
from types import ModuleType
from typing import Callable
from unittest.mock import MagicMock
import pytest
from pydantic import __version__ as pydantic_version
from logfire._internal.utils import get_version
pydantic_pre_2_5 = get_version(pydantic_version) < get_version('2.5.0')
def logfire_dunder_all() -> set[str]:
logfire = importlib.import_module('logfire')
return set(logfire.__all__)
def import_logfire_api_without_logfire() -> ModuleType:
logfire = sys.modules['logfire']
try:
sys.modules['logfire'] = None # type: ignore
sys.modules.pop('logfire_api', None)
return importlib.import_module('logfire_api')
finally:
sys.modules['logfire'] = logfire
def import_logfire_api_with_logfire() -> ModuleType:
logfire_api = importlib.import_module('logfire_api')
return importlib.reload(logfire_api)
@pytest.mark.parametrize(
['logfire_api_factory', 'module_name'],
[
pytest.param(import_logfire_api_without_logfire, 'logfire_api.', id='without_logfire'),
pytest.param(import_logfire_api_with_logfire, 'logfire.', id='with_logfire'),
],
)
def test_runtime(logfire_api_factory: Callable[[], ModuleType], module_name: str) -> None:
logfire__all__ = logfire_dunder_all()
logfire_api = logfire_api_factory()
assert logfire_api is not None
for member in dir(logfire_api):
if member.startswith('instrument_'):
assert member in logfire__all__, member
assert hasattr(logfire_api, 'Logfire')
assert module_name in str(logfire_api.Logfire())
logfire__all__.remove('Logfire')
assert hasattr(logfire_api, 'configure')
logfire_api.configure(send_to_logfire=False, console=False)
logfire__all__.remove('configure')
assert hasattr(logfire_api, 'VERSION')
logfire__all__.remove('VERSION')
assert hasattr(logfire_api, 'LevelName')
logfire__all__.remove('LevelName')
with logfire_api.span('test span') as span:
assert isinstance(span, logfire_api.LogfireSpan)
span.set_attribute('foo', 'bar')
logfire__all__.remove('LogfireSpan')
logfire__all__.remove('span')
assert hasattr(logfire_api, 'log')
logfire_api.log('info', 'test log')
logfire__all__.remove('log')
for log_method in ['trace', 'debug', 'info', 'notice', 'warn', 'warning', 'error', 'exception', 'fatal']:
assert hasattr(logfire_api, log_method)
getattr(logfire_api, log_method)('test log')
logfire__all__.remove(log_method)
assert hasattr(logfire_api, 'with_settings')
assert isinstance(logfire_api.with_settings(), logfire_api.Logfire)
logfire__all__.remove('with_settings')
assert hasattr(logfire_api, 'with_tags')
logfire_api.with_tags('test tag')
logfire__all__.remove('with_tags')
assert hasattr(logfire_api, 'force_flush')
logfire_api.force_flush()
logfire__all__.remove('force_flush')
assert hasattr(logfire_api, 'no_auto_trace')
logfire_api.no_auto_trace(lambda: None) # pragma: no branch
logfire__all__.remove('no_auto_trace')
assert hasattr(logfire_api, 'add_non_user_code_prefix')
logfire_api.add_non_user_code_prefix('/foo/bar')
logfire__all__.remove('add_non_user_code_prefix')
assert hasattr(logfire_api, 'suppress_instrumentation')
with logfire_api.suppress_instrumentation():
...
logfire__all__.remove('suppress_instrumentation')
assert hasattr(logfire_api, 'suppress_scopes')
logfire_api.suppress_scopes()
logfire__all__.remove('suppress_scopes')
assert hasattr(logfire_api, 'ConsoleOptions')
logfire_api.ConsoleOptions(colors='auto')
logfire__all__.remove('ConsoleOptions')
assert hasattr(logfire_api, 'PydanticPlugin')
logfire_api.PydanticPlugin()
logfire__all__.remove('PydanticPlugin')
assert hasattr(logfire_api, 'ScrubMatch')
logfire_api.ScrubMatch(path='test', value='test', pattern_match='test')
logfire__all__.remove('ScrubMatch')
assert hasattr(logfire_api, 'log_slow_async_callbacks')
# NOTE: We don't call the log_slow_async_callbacks, to not give side effect to the test suite.
logfire__all__.remove('log_slow_async_callbacks')
assert hasattr(logfire_api, 'install_auto_tracing')
logfire_api.install_auto_tracing(modules=['all'], min_duration=0)
logfire__all__.remove('install_auto_tracing')
assert hasattr(logfire_api, 'instrument')
@logfire_api.instrument()
def func() -> None: ...
func()
logfire__all__.remove('instrument')
assert hasattr(logfire_api, 'instrument_aws_lambda'), 'instrument_aws_lambda'
logfire_api.instrument_aws_lambda(lambda_handler=MagicMock())
logfire__all__.remove('instrument_aws_lambda')
assert hasattr(logfire_api, 'instrument_asgi'), 'instrument_asgi'
assert getattr(logfire_api, 'instrument_asgi')(app=MagicMock()) is not None
logfire__all__.remove('instrument_asgi')
assert hasattr(logfire_api, 'instrument_wsgi'), 'instrument_wsgi'
assert getattr(logfire_api, 'instrument_wsgi')(app=MagicMock()) is not None
logfire__all__.remove('instrument_wsgi')
for member in [m for m in ('instrument_flask', 'instrument_fastapi', 'instrument_starlette')]:
assert hasattr(logfire_api, member), member
getattr(logfire_api, member)(app=MagicMock())
logfire__all__.remove(member)
for member in [m for m in ('instrument_openai', 'instrument_anthropic')]:
assert hasattr(logfire_api, member), member
with getattr(logfire_api, member)():
...
logfire__all__.remove(member)
assert hasattr(logfire_api, 'instrument_openai_agents')
if sys.version_info >= (3, 9):
logfire_api.instrument_openai_agents()
logfire__all__.remove('instrument_openai_agents')
assert hasattr(logfire_api, 'instrument_pydantic_ai')
if sys.version_info >= (3, 9) and not pydantic_pre_2_5:
logfire_api.instrument_pydantic_ai()
logfire__all__.remove('instrument_pydantic_ai')
assert hasattr(logfire_api, 'instrument_mcp')
if sys.version_info >= (3, 10) and not pydantic_pre_2_5:
logfire_api.instrument_mcp()
logfire__all__.remove('instrument_mcp')
for member in [m for m in logfire__all__ if m.startswith('instrument_')]:
assert hasattr(logfire_api, member), member
if not (pydantic_pre_2_5 and member == 'instrument_pydantic'):
# skip pydantic instrumentation (which uses the plugin) for versions prior to v2.5
getattr(logfire_api, member)()
# just remove the member unconditionally to pass future asserts
logfire__all__.remove(member)
assert hasattr(logfire_api, 'shutdown')
logfire_api.shutdown()
logfire__all__.remove('shutdown')
assert hasattr(logfire_api, 'AutoTraceModule')
logfire_api.AutoTraceModule(name='test', filename='test')
logfire__all__.remove('AutoTraceModule')
assert hasattr(logfire_api, 'LogfireLoggingHandler')
logfire_api.LogfireLoggingHandler()
logfire__all__.remove('LogfireLoggingHandler')
assert hasattr(logfire_api, 'loguru_handler')
logfire_api.loguru_handler()
logfire__all__.remove('loguru_handler')
assert hasattr(logfire_api, 'StructlogProcessor')
logfire_api.StructlogProcessor()
logfire__all__.remove('StructlogProcessor')
assert hasattr(logfire_api, 'SamplingOptions')
logfire_api.SamplingOptions()
logfire__all__.remove('SamplingOptions')
assert hasattr(logfire_api, 'CodeSource')
logfire_api.CodeSource(repository='https://github.com/pydantic/logfire', revision='main', root_path='test')
logfire__all__.remove('CodeSource')
assert hasattr(logfire_api, 'ScrubbingOptions')
logfire_api.ScrubbingOptions()
logfire__all__.remove('ScrubbingOptions')
assert hasattr(logfire_api, 'AdvancedOptions')
logfire_api.AdvancedOptions()
logfire__all__.remove('AdvancedOptions')
assert hasattr(logfire_api, 'MetricsOptions')
logfire_api.MetricsOptions()
logfire__all__.remove('MetricsOptions')
assert hasattr(logfire_api, 'logfire_info')
logfire_api.logfire_info()
logfire__all__.remove('logfire_info')
# If it's not empty, it means that some of the __all__ members are not tested.
assert logfire__all__ == set(), logfire__all__
@pytest.mark.skipif(sys.version_info < (3, 11), reason='We only need this test for a single Python version.')
def test_match_version_on_pyproject() -> None:
import tomllib
logfire_pyproject = (Path(__file__).parent.parent / 'pyproject.toml').read_text()
logfire_api_pyproject = (Path(__file__).parent.parent / 'logfire-api' / 'pyproject.toml').read_text()
logfire_pyproject_content = tomllib.loads(logfire_pyproject)
logfire_api_pyproject_content = tomllib.loads(logfire_api_pyproject)
assert logfire_pyproject_content['project']['version'] == logfire_api_pyproject_content['project']['version']
def test_override_init_pyi() -> None: # pragma: no cover
"""The logic here is:
1. If `span: Incomplete` is present, it means we need to regenerate the `DEFAULT_LOGFIRE_INSTANCE` logic.
2. If the `span: Incomplete` is present, but we have `Incomplete` in the file, it means we need to update to a
`DEFAULT_LOGFIRE_INSTANCE` logic.
3. If none of the above is present, we skip the test.
"""
incomplete = ': Incomplete'
len_incomplete = len(incomplete)
init_pyi = (Path(__file__).parent.parent / 'logfire-api' / 'logfire_api' / '__init__.pyi').read_text()
lines = init_pyi.splitlines()
try:
span_index = lines.index('span: Incomplete')
except ValueError:
for i, line in enumerate(lines.copy()):
if line.endswith(incomplete):
prefix = line[: len(line) - len_incomplete]
lines[i] = f'{prefix} = DEFAULT_LOGFIRE_INSTANCE.{prefix}'
else:
default_logfire_instance = 'DEFAULT_LOGFIRE_INSTANCE'
new_end_lines: list[str] = [f'{default_logfire_instance} = Logfire()']
for line in lines[span_index:]:
if line.endswith(incomplete):
prefix = line[: len(line) - len_incomplete]
new_end_lines.append(f'{prefix} = {default_logfire_instance}.{prefix}')
else:
new_end_lines.append(line)
lines.remove('from _typeshed import Incomplete')
lines[span_index - 1 :] = new_end_lines
new_init_pyi = '\n'.join(lines) + '\n'
if new_init_pyi == init_pyi:
pytest.skip('No changes were made to the __init__.pyi file.')
(Path(__file__).parent.parent / 'logfire-api' / 'logfire_api' / '__init__.pyi').write_text(new_init_pyi)
pytest.fail('The __init__.pyi file was updated.')