|
2 | 2 |
|
3 | 3 | from __future__ import annotations |
4 | 4 |
|
5 | | -import os |
6 | 5 | import random |
7 | | -import re |
8 | | -import sys |
9 | | -from collections.abc import Sequence |
10 | 6 | from dataclasses import dataclass |
11 | | -from pathlib import Path |
12 | | -from typing import Any, Mapping, cast |
13 | 7 |
|
14 | 8 | import pytest |
15 | | -from opentelemetry import trace |
16 | 9 | from opentelemetry.sdk.metrics.export import InMemoryMetricReader |
17 | | -from opentelemetry.sdk.trace import Event, ReadableSpan |
18 | | -from opentelemetry.sdk.trace.export import SimpleSpanProcessor, SpanExporter, SpanExportResult |
| 10 | +from opentelemetry.sdk.trace.export import SimpleSpanProcessor |
19 | 11 | from opentelemetry.sdk.trace.id_generator import IdGenerator |
20 | | -from opentelemetry.semconv.resource import ResourceAttributes |
21 | | -from opentelemetry.semconv.trace import SpanAttributes |
22 | 12 |
|
23 | 13 | import logfire |
24 | 14 |
|
25 | | -from ._internal.constants import ( |
26 | | - ATTRIBUTES_SPAN_TYPE_KEY, |
27 | | - ONE_SECOND_IN_NANOSECONDS, |
28 | | - RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS, |
29 | | -) |
30 | | - |
31 | | - |
32 | | -class TestExporter(SpanExporter): |
33 | | - """A SpanExporter that stores exported spans in a list for asserting in tests.""" |
34 | | - |
35 | | - # NOTE: Avoid test discovery by pytest. |
36 | | - __test__ = False |
37 | | - |
38 | | - def __init__(self) -> None: |
39 | | - self.exported_spans: list[ReadableSpan] = [] |
40 | | - |
41 | | - def export(self, spans: Sequence[ReadableSpan]) -> SpanExportResult: |
42 | | - """Exports a batch of telemetry data.""" |
43 | | - self.exported_spans.extend(spans) |
44 | | - return SpanExportResult.SUCCESS |
45 | | - |
46 | | - def clear(self) -> None: |
47 | | - """Clears the collected spans.""" |
48 | | - self.exported_spans = [] |
49 | | - |
50 | | - def exported_spans_as_dict( |
51 | | - self, |
52 | | - fixed_line_number: int | None = 123, |
53 | | - strip_filepaths: bool = True, |
54 | | - include_resources: bool = False, |
55 | | - include_package_versions: bool = False, |
56 | | - include_instrumentation_scope: bool = False, |
57 | | - _include_pending_spans: bool = False, |
58 | | - _strip_function_qualname: bool = True, |
59 | | - ) -> list[dict[str, Any]]: |
60 | | - """The exported spans as a list of dicts. |
61 | | -
|
62 | | - Args: |
63 | | - fixed_line_number: The line number to use for all spans. |
64 | | - strip_filepaths: Whether to strip the filepaths from the exported spans. |
65 | | - include_resources: Whether to include the resource attributes in the exported spans. |
66 | | - include_package_versions: Whether to include the package versions in the exported spans. |
67 | | - include_instrumentation_scope: Whether to include the instrumentation scope in the exported spans. |
68 | | -
|
69 | | - Returns: |
70 | | - A list of dicts representing the exported spans. |
71 | | - """ |
72 | | - |
73 | | - def process_attribute(name: str, value: Any) -> Any: |
74 | | - if name == 'code.filepath' and strip_filepaths: |
75 | | - try: |
76 | | - return Path(value).name |
77 | | - except ValueError: # pragma: no cover |
78 | | - return value |
79 | | - if name == 'code.lineno' and fixed_line_number is not None: |
80 | | - return fixed_line_number |
81 | | - if name == 'code.function': |
82 | | - if sys.version_info >= (3, 11) and _strip_function_qualname: |
83 | | - return value.split('.')[-1] |
84 | | - if name == ResourceAttributes.PROCESS_PID: |
85 | | - assert value == os.getpid() |
86 | | - return 1234 |
87 | | - if name == ResourceAttributes.SERVICE_INSTANCE_ID: |
88 | | - if re.match(r'^[0-9a-f]{32}$', value): |
89 | | - return '0' * 32 |
90 | | - return value |
91 | | - |
92 | | - def build_attributes(attributes: Mapping[str, Any] | None) -> dict[str, Any] | None: |
93 | | - if attributes is None: # pragma: no branch |
94 | | - return None # pragma: no cover |
95 | | - attributes = { |
96 | | - k: process_attribute(k, v) |
97 | | - for k, v in attributes.items() |
98 | | - if k != RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS or include_package_versions |
99 | | - } |
100 | | - if 'telemetry.sdk.version' in attributes: |
101 | | - attributes['telemetry.sdk.version'] = '0.0.0' |
102 | | - return attributes |
103 | | - |
104 | | - def build_event(event: Event) -> dict[str, Any]: |
105 | | - res: dict[str, Any] = { |
106 | | - 'name': event.name, |
107 | | - 'timestamp': event.timestamp, |
108 | | - } |
109 | | - if event.attributes: # pragma: no branch |
110 | | - res['attributes'] = attributes = dict(event.attributes) |
111 | | - if SpanAttributes.EXCEPTION_STACKTRACE in attributes: |
112 | | - last_line = next( # pragma: no branch |
113 | | - line.strip() |
114 | | - for line in reversed( |
115 | | - cast(str, event.attributes[SpanAttributes.EXCEPTION_STACKTRACE]).split('\n') |
116 | | - ) |
117 | | - if line.strip() |
118 | | - ) |
119 | | - attributes[SpanAttributes.EXCEPTION_STACKTRACE] = last_line |
120 | | - return res |
121 | | - |
122 | | - def build_instrumentation_scope(span: ReadableSpan) -> dict[str, Any]: |
123 | | - if include_instrumentation_scope: |
124 | | - return {'instrumentation_scope': span.instrumentation_scope and span.instrumentation_scope.name} |
125 | | - else: |
126 | | - return {} |
127 | | - |
128 | | - def build_span(span: ReadableSpan) -> dict[str, Any]: |
129 | | - context = span.context or trace.INVALID_SPAN_CONTEXT |
130 | | - res: dict[str, Any] = { |
131 | | - 'name': span.name, |
132 | | - 'context': { |
133 | | - 'trace_id': context.trace_id, |
134 | | - 'span_id': context.span_id, |
135 | | - 'is_remote': context.is_remote, |
136 | | - }, |
137 | | - 'parent': { |
138 | | - 'trace_id': span.parent.trace_id, |
139 | | - 'span_id': span.parent.span_id, |
140 | | - 'is_remote': span.parent.is_remote, |
141 | | - } |
142 | | - if span.parent |
143 | | - else None, |
144 | | - 'start_time': span.start_time, |
145 | | - 'end_time': span.end_time, |
146 | | - **build_instrumentation_scope(span), |
147 | | - 'attributes': build_attributes(span.attributes), |
148 | | - } |
149 | | - if span.events: |
150 | | - res['events'] = [build_event(event) for event in span.events] |
151 | | - if include_resources: |
152 | | - resource_attributes = build_attributes(span.resource.attributes) |
153 | | - res['resource'] = { |
154 | | - 'attributes': resource_attributes, |
155 | | - } |
156 | | - return res |
157 | | - |
158 | | - spans = [build_span(span) for span in self.exported_spans] |
159 | | - return [ |
160 | | - span |
161 | | - for span in spans |
162 | | - if _include_pending_spans is True |
163 | | - or (span.get('attributes', {}).get(ATTRIBUTES_SPAN_TYPE_KEY, 'span') != 'pending_span') |
164 | | - ] |
| 15 | +from ._internal.constants import ONE_SECOND_IN_NANOSECONDS |
| 16 | +from ._internal.exporters.test import TestExporter |
| 17 | + |
| 18 | +__all__ = [ |
| 19 | + 'capfire', |
| 20 | + 'CaptureLogfire', |
| 21 | + 'IncrementalIdGenerator', |
| 22 | + 'SeededRandomIdGenerator', |
| 23 | + 'TimeGenerator', |
| 24 | + 'TestExporter', |
| 25 | +] |
165 | 26 |
|
166 | 27 |
|
167 | 28 | @dataclass(repr=True) |
|
0 commit comments