From 0fefb1f7151dc86473124b7d032021eff4493fc1 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Wed, 5 Feb 2025 17:37:15 +0200 Subject: [PATCH 1/4] Handle null arguments using JSON schema --- logfire/_internal/exporters/console.py | 4 +- logfire/_internal/json_schema.py | 4 +- logfire/_internal/main.py | 7 +-- tests/otel_integrations/test_fastapi.py | 56 ++++++++++----------- tests/test_logfire.py | 66 +++++++++++-------------- 5 files changed, 61 insertions(+), 76 deletions(-) diff --git a/logfire/_internal/exporters/console.py b/logfire/_internal/exporters/console.py index 60c9a4630..b4da2df0d 100644 --- a/logfire/_internal/exporters/console.py +++ b/logfire/_internal/exporters/console.py @@ -183,10 +183,10 @@ def _details_parts(self, span: ReadableSpan, indent_str: str) -> TextParts: return [] file_location_raw = span.attributes.get('code.filepath') - file_location = None if file_location_raw is None else str(file_location_raw) + file_location = None if file_location_raw in (None, 'null') else str(file_location_raw) if file_location: lineno = span.attributes.get('code.lineno') - if lineno: # pragma: no branch + if lineno not in (None, 'null'): file_location += f':{lineno}' log_level_num: int = span.attributes.get(ATTRIBUTES_LOG_LEVEL_NUM_KEY) # type: ignore diff --git a/logfire/_internal/json_schema.py b/logfire/_internal/json_schema.py index 6a8abf670..c76ade047 100644 --- a/logfire/_internal/json_schema.py +++ b/logfire/_internal/json_schema.py @@ -105,7 +105,7 @@ def create_json_schema(obj: Any, seen: set[int]) -> JsonDict: The JSON Schema. """ if obj is None: - return {} + return {'type': 'null'} try: # cover common types first before calling `type_to_schema` to avoid the overhead of imports if not necessary @@ -219,7 +219,7 @@ def _enum_schema(obj: Enum, seen: set[int]) -> JsonDict: # Schemas for values that are already JSON serializable, i.e. that don't need to be included # (except at the top level) because the frontend can just render them as plain JSON. -PLAIN_SCHEMAS: tuple[JsonDict, ...] = ({}, {'type': 'object'}, {'type': 'array'}) +PLAIN_SCHEMAS: tuple[JsonDict, ...] = ({}, {'type': 'object'}, {'type': 'array'}, {'type': 'null'}) def _mapping_schema(obj: Any, seen: set[int]) -> JsonDict: diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index a8703efbf..d09f2eb66 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -17,7 +17,6 @@ Sequence, TypeVar, Union, - cast, overload, ) @@ -45,7 +44,6 @@ ATTRIBUTES_TAGS_KEY, DISABLE_CONSOLE_KEY, LEVEL_NUMBERS, - NULL_ARGS_KEY, OTLP_MAX_INT_SIZE, LevelName, log_level_attributes, @@ -2350,10 +2348,7 @@ def set_user_attribute( The key will be the original key unless the value was `None`, in which case it will be `NULL_ARGS_KEY`. """ otel_value: otel_types.AttributeValue - if value is None: - otel_value = cast('list[str]', otlp_attributes.get(NULL_ARGS_KEY, [])) + [key] - key = NULL_ARGS_KEY - elif isinstance(value, int): + if isinstance(value, int): if value > OTLP_MAX_INT_SIZE: warnings.warn( f'Integer value {value} is larger than the maximum OTLP integer size of {OTLP_MAX_INT_SIZE} (64-bits), ' diff --git a/tests/otel_integrations/test_fastapi.py b/tests/otel_integrations/test_fastapi.py index e8948d133..3c3c88bd2 100644 --- a/tests/otel_integrations/test_fastapi.py +++ b/tests/otel_integrations/test_fastapi.py @@ -269,9 +269,9 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'http.method': 'GET', 'fastapi.route.name': 'with_path_param', 'http.route': '/with_path_param/{param}', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'logfire.level_num': 5, - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', }, }, { @@ -397,8 +397,8 @@ def test_path_param(client: TestClient, exporter: TestExporter) -> None: 'client.port': 50000, 'http.route': '/with_path_param/{param}', 'fastapi.route.name': 'with_path_param', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'fastapi.route.operation_id': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', 'http.status_code': 200, 'http.response.status_code': 200, }, @@ -492,9 +492,9 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'http.method': 'GET', 'fastapi.route.name': 'homepage', 'http.route': '/', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'logfire.level_num': 5, - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', }, }, { @@ -636,8 +636,8 @@ def test_fastapi_instrumentation(client: TestClient, exporter: TestExporter) -> 'client.port': 50000, 'http.route': '/', 'fastapi.route.name': 'homepage', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'fastapi.route.operation_id': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', 'http.status_code': 200, 'http.response.status_code': 200, }, @@ -1190,9 +1190,9 @@ def test_fastapi_unhandled_exception(client: TestClient, exporter: TestExporter) 'http.method': 'GET', 'fastapi.route.name': 'exception', 'http.route': '/exception', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'logfire.level_num': 5, - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', }, }, { @@ -1256,8 +1256,8 @@ def test_fastapi_unhandled_exception(client: TestClient, exporter: TestExporter) 'client.port': 50000, 'http.route': '/exception', 'fastapi.route.name': 'exception', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'fastapi.route.operation_id': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', 'logfire.level_num': 17, }, 'events': [ @@ -1298,9 +1298,9 @@ def test_fastapi_handled_exception(client: TestClient, exporter: TestExporter) - 'http.method': 'GET', 'fastapi.route.name': 'validation_error', 'http.route': '/validation_error', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'logfire.level_num': 5, - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', }, }, { @@ -1392,8 +1392,8 @@ def test_fastapi_handled_exception(client: TestClient, exporter: TestExporter) - 'client.port': 50000, 'http.route': '/validation_error', 'fastapi.route.name': 'validation_error', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{}}}', + 'fastapi.route.operation_id': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"}}}', 'http.status_code': 422, 'http.response.status_code': 422, }, @@ -1434,11 +1434,11 @@ def test_scrubbing(client: TestClient, exporter: TestExporter) -> None: ), 'errors': '[]', 'custom_attr': 'custom_value', + 'fastapi.route.operation_id': 'null', 'http.method': 'GET', 'http.route': '/secret/{path_param}', 'fastapi.route.name': 'secret', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{},"values":{"type":"object"},"errors":{"type":"array"},"custom_attr":{}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"values":{"type":"object"},"errors":{"type":"array"},"custom_attr":{}}}', 'logfire.scrubbed': IsJson( [ {'path': ['attributes', 'values', 'path_param'], 'matched_substring': 'auth'}, @@ -1527,11 +1527,11 @@ def test_scrubbing(client: TestClient, exporter: TestExporter) -> None: 'http.route': '/secret/{path_param}', 'http.request.header.testauthorization': ("[Scrubbed due to 'auth']",), 'fastapi.route.name': 'secret', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'fastapi.arguments.values': '{"path_param": "[Scrubbed due to \'auth\']", "foo": "foo_val", "password": "[Scrubbed due to \'password\']", "testauthorization": "[Scrubbed due to \'auth\']"}', 'fastapi.arguments.errors': '[]', 'custom_attr': 'custom_value', - 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{},"custom_attr":{},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"custom_attr":{},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', 'http.status_code': 200, 'http.response.status_code': 200, 'logfire.scrubbed': IsJson( @@ -1619,10 +1619,10 @@ def test_request_hooks_without_send_receiev_spans(exporter: TestExporter): 'values': '{}', 'errors': '[]', 'http.method': 'POST', + 'fastapi.route.operation_id': 'null', 'http.route': '/echo_body', 'fastapi.route.name': 'echo_body', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{},"values":{"type":"object"},"errors":{"type":"array"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"values":{"type":"object"},"errors":{"type":"array"}}}', }, }, { @@ -1723,10 +1723,10 @@ def test_request_hooks_without_send_receiev_spans(exporter: TestExporter): 'http.route': '/echo_body', 'attr_key': 'attr_val', 'fastapi.route.name': 'echo_body', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'fastapi.arguments.values': '{}', 'fastapi.arguments.errors': '[]', - 'logfire.json_schema': '{"type":"object","properties":{"attr_key":{},"fastapi.route.name":{},"fastapi.route.operation_id":{},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"attr_key":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', 'http.status_code': 200, 'http.response.status_code': 200, }, @@ -1768,10 +1768,10 @@ def test_request_hooks_with_send_receive_spans(exporter: TestExporter): 'values': '{}', 'errors': '[]', 'http.method': 'POST', + 'fastapi.route.operation_id': 'null', 'http.route': '/echo_body', 'fastapi.route.name': 'echo_body', - 'logfire.null_args': ('fastapi.route.operation_id',), - 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{},"values":{"type":"object"},"errors":{"type":"array"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"http.method":{},"http.route":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"values":{"type":"object"},"errors":{"type":"array"}}}', }, }, { @@ -1913,10 +1913,10 @@ def test_request_hooks_with_send_receive_spans(exporter: TestExporter): 'http.route': '/echo_body', 'attr_key': 'attr_val', 'fastapi.route.name': 'echo_body', - 'logfire.null_args': ('fastapi.route.operation_id',), + 'fastapi.route.operation_id': 'null', 'fastapi.arguments.values': '{}', 'fastapi.arguments.errors': '[]', - 'logfire.json_schema': '{"type":"object","properties":{"attr_key":{},"fastapi.route.name":{},"fastapi.route.operation_id":{},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', + 'logfire.json_schema': '{"type":"object","properties":{"attr_key":{},"fastapi.route.name":{},"fastapi.route.operation_id":{"type":"null"},"fastapi.arguments.values":{"type":"object"},"fastapi.arguments.errors":{"type":"array"}}}', 'http.status_code': 200, 'http.response.status_code': 200, }, diff --git a/tests/test_logfire.py b/tests/test_logfire.py index 47aef5c53..505990ee0 100644 --- a/tests/test_logfire.py +++ b/tests/test_logfire.py @@ -13,7 +13,7 @@ import pytest from dirty_equals import IsInt, IsJson, IsStr -from inline_snapshot import snapshot +from inline_snapshot import Is, snapshot from opentelemetry.metrics import get_meter from opentelemetry.proto.common.v1.common_pb2 import AnyValue from opentelemetry.sdk.metrics.export import InMemoryMetricReader @@ -32,7 +32,6 @@ ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY, LEVEL_NUMBERS, - NULL_ARGS_KEY, LevelName, ) from logfire._internal.formatter import FormattingFailedWarning, InspectArgumentsFailedWarning @@ -541,40 +540,30 @@ def test_span_without_span_name(exporter: TestExporter) -> None: def test_log(exporter: TestExporter, level: LevelName): getattr(logfire, level)('test {name} {number} {none}', name='foo', number=2, none=None) - s = exporter.exported_spans[0] - - assert s.attributes is not None - assert s.attributes[ATTRIBUTES_MESSAGE_TEMPLATE_KEY] == 'test {name} {number} {none}' - assert s.attributes[ATTRIBUTES_MESSAGE_KEY] == 'test foo 2 None' - assert s.attributes[ATTRIBUTES_SPAN_TYPE_KEY] == 'log' - assert s.attributes['name'] == 'foo' - assert s.attributes['number'] == 2 - assert s.attributes[NULL_ARGS_KEY] == ('none',) - assert ATTRIBUTES_TAGS_KEY not in s.attributes - - # insert_assert(exporter.exported_spans_as_dict(_include_pending_spans=True)) - assert exporter.exported_spans_as_dict(_include_pending_spans=True) == [ - { - 'name': 'test {name} {number} {none}', - 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, - 'parent': None, - 'start_time': 1000000000, - 'end_time': 1000000000, - 'attributes': { - 'logfire.span_type': 'log', - 'logfire.level_num': LEVEL_NUMBERS[level], - 'logfire.msg_template': 'test {name} {number} {none}', - 'logfire.msg': 'test foo 2 None', - 'code.filepath': 'test_logfire.py', - 'code.lineno': 123, - 'code.function': 'test_log', - 'name': 'foo', - 'number': 2, - 'logfire.null_args': ('none',), - 'logfire.json_schema': '{"type":"object","properties":{"name":{},"number":{},"none":{}}}', - }, - } - ] + assert exporter.exported_spans_as_dict(_include_pending_spans=True) == snapshot( + [ + { + 'name': 'test {name} {number} {none}', + 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, + 'parent': None, + 'start_time': 1000000000, + 'end_time': 1000000000, + 'attributes': { + 'logfire.span_type': 'log', + 'logfire.level_num': Is(LEVEL_NUMBERS[level]), + 'logfire.msg_template': 'test {name} {number} {none}', + 'logfire.msg': 'test foo 2 None', + 'code.filepath': 'test_logfire.py', + 'code.lineno': 123, + 'code.function': 'test_log', + 'name': 'foo', + 'number': 2, + 'none': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"name":{},"number":{},"none":{"type":"null"}}}', + }, + } + ] + ) def test_log_equals(exporter: TestExporter) -> None: @@ -1612,8 +1601,9 @@ def test_complex_attribute_added_after_span_started(exporter: TestExporter) -> N 'logfire.msg': 'hi', 'logfire.span_type': 'span', 'c': '{"d":2}', - 'logfire.null_args': ('e', 'f'), - 'logfire.json_schema': '{"type":"object","properties":{"a":{"type":"object"},"c":{"type":"object"},"e":{},"f":{}}}', + 'e': 'null', + 'f': 'null', + 'logfire.json_schema': '{"type":"object","properties":{"a":{"type":"object"},"c":{"type":"object"},"e":{"type":"null"},"f":{"type":"null"}}}', }, } ] From fa585c3c4f3511d768bedb6d459772e643118ed7 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Wed, 5 Feb 2025 17:39:27 +0200 Subject: [PATCH 2/4] simplify set_user_attribute --- logfire/_internal/main.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index d09f2eb66..a0ba8e84b 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -2202,7 +2202,7 @@ def set_attribute(self, key: str, value: Any) -> None: """ self._added_attributes = True self._json_schema_properties[key] = create_json_schema(value, set()) - key, otel_value = set_user_attribute(self._otlp_attributes, key, value) + otel_value = set_user_attribute(self._otlp_attributes, key, value) if self._span is not None: # pragma: no branch self._span.set_attribute(key, otel_value) @@ -2341,11 +2341,10 @@ def prepare_otlp_attributes(attributes: dict[str, Any]) -> dict[str, otel_types. def set_user_attribute( otlp_attributes: dict[str, otel_types.AttributeValue], key: str, value: Any -) -> tuple[str, otel_types.AttributeValue]: +) -> otel_types.AttributeValue: """Convert a user attribute to an OpenTelemetry compatible type and add it to the given dictionary. - Returns the final key and value that was added to the dictionary. - The key will be the original key unless the value was `None`, in which case it will be `NULL_ARGS_KEY`. + Returns the final value that was added to the dictionary. """ otel_value: otel_types.AttributeValue if isinstance(value, int): @@ -2363,7 +2362,7 @@ def set_user_attribute( else: otel_value = logfire_json_dumps(value) otlp_attributes[key] = otel_value - return key, otel_value + return otel_value def set_user_attributes_on_raw_span(span: Span, attributes: dict[str, Any]) -> None: From 05fa1729001de34bf6961ea194db456ccb4f8a5e Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Wed, 5 Feb 2025 17:40:10 +0200 Subject: [PATCH 3/4] remove NULL_ARGS_KEY --- logfire/_internal/constants.py | 3 --- logfire/_internal/scrubbing.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/logfire/_internal/constants.py b/logfire/_internal/constants.py index 139992743..72d38a041 100644 --- a/logfire/_internal/constants.py +++ b/logfire/_internal/constants.py @@ -137,9 +137,6 @@ def log_level_attributes(level: LevelName | int) -> dict[str, otel_types.Attribu ATTRIBUTES_SCRUBBED_KEY = f'{LOGFIRE_ATTRIBUTES_NAMESPACE}.scrubbed' """Key in OTEL attributes with metadata about parts of a span that have been scrubbed.""" -NULL_ARGS_KEY = 'logfire.null_args' -"""Key in OTEL attributes that collects attributes with a null (None) value.""" - PENDING_SPAN_NAME_SUFFIX = ' (pending)' """Suffix added to the name of a pending span to indicate it's a pending span and avoid collisions with the real span while in flight.""" diff --git a/logfire/_internal/scrubbing.py b/logfire/_internal/scrubbing.py index e5ea79499..7c3b7b994 100644 --- a/logfire/_internal/scrubbing.py +++ b/logfire/_internal/scrubbing.py @@ -24,7 +24,6 @@ ATTRIBUTES_SCRUBBED_KEY, ATTRIBUTES_SPAN_TYPE_KEY, ATTRIBUTES_TAGS_KEY, - NULL_ARGS_KEY, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS, ) from .stack_info import STACK_INFO_KEYS @@ -112,7 +111,6 @@ class BaseScrubber(ABC): ATTRIBUTES_SAMPLE_RATE_KEY, ATTRIBUTES_LOGGING_NAME, ATTRIBUTES_SCRUBBED_KEY, - NULL_ARGS_KEY, RESOURCE_ATTRIBUTES_PACKAGE_VERSIONS, *STACK_INFO_KEYS, SpanAttributes.EXCEPTION_STACKTRACE, # See scrub_event_attributes From 9fe1c0795a5b9b8e508cafcd582925345ce99a69 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Wed, 5 Feb 2025 17:45:40 +0200 Subject: [PATCH 4/4] Simplify prepare_otlp_attributes --- logfire/_internal/main.py | 29 ++++++++--------------------- 1 file changed, 8 insertions(+), 21 deletions(-) diff --git a/logfire/_internal/main.py b/logfire/_internal/main.py index a0ba8e84b..0613306d2 100644 --- a/logfire/_internal/main.py +++ b/logfire/_internal/main.py @@ -2202,7 +2202,7 @@ def set_attribute(self, key: str, value: Any) -> None: """ self._added_attributes = True self._json_schema_properties[key] = create_json_schema(value, set()) - otel_value = set_user_attribute(self._otlp_attributes, key, value) + otel_value = self._otlp_attributes[key] = prepare_otlp_attribute(value) if self._span is not None: # pragma: no branch self._span.set_attribute(key, otel_value) @@ -2331,22 +2331,11 @@ def prepare_otlp_attributes(attributes: dict[str, Any]) -> dict[str, otel_types. This will convert any non-OpenTelemetry compatible types to JSON. """ - otlp_attributes: dict[str, otel_types.AttributeValue] = {} + return {key: prepare_otlp_attribute(value) for key, value in attributes.items()} - for key, value in attributes.items(): - set_user_attribute(otlp_attributes, key, value) - return otlp_attributes - - -def set_user_attribute( - otlp_attributes: dict[str, otel_types.AttributeValue], key: str, value: Any -) -> otel_types.AttributeValue: - """Convert a user attribute to an OpenTelemetry compatible type and add it to the given dictionary. - - Returns the final value that was added to the dictionary. - """ - otel_value: otel_types.AttributeValue +def prepare_otlp_attribute(value: Any) -> otel_types.AttributeValue: + """Convert a user attribute to an OpenTelemetry compatible type.""" if isinstance(value, int): if value > OTLP_MAX_INT_SIZE: warnings.warn( @@ -2354,15 +2343,13 @@ def set_user_attribute( ' if you need support for sending larger integers, please open a feature request', UserWarning, ) - otel_value = str(value) + return str(value) else: - otel_value = value + return value elif isinstance(value, (str, bool, float)): - otel_value = value + return value else: - otel_value = logfire_json_dumps(value) - otlp_attributes[key] = otel_value - return otel_value + return logfire_json_dumps(value) def set_user_attributes_on_raw_span(span: Span, attributes: dict[str, Any]) -> None: