Skip to content

Commit 0ce61ae

Browse files
committed
fix: attribute types, incl containers
1 parent 45a33d5 commit 0ce61ae

File tree

3 files changed

+96
-54
lines changed

3 files changed

+96
-54
lines changed

otlp_json.py renamed to otlp_json/__init__.py

Lines changed: 62 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
from __future__ import annotations
22

33
import json
4-
from typing import Any, Sequence, TYPE_CHECKING
4+
from collections.abc import Mapping, Sequence
5+
from typing import Any, TYPE_CHECKING
56

67
if TYPE_CHECKING:
78
from typing_extensions import TypeAlias
@@ -18,6 +19,24 @@
1819
CONTENT_TYPE = "application/json"
1920

2021

22+
_VALUE_TYPES = {
23+
# NOTE: order matters, for isinstance(True, int).
24+
bool: ("boolValue", bool),
25+
int: ("intValue", str),
26+
float: ("doubleValue", float),
27+
bytes: ("bytesValue", bytes),
28+
str: ("stringValue", str),
29+
Sequence: (
30+
"arrayValue",
31+
lambda value: {"values": [_value(e) for e in _homogeneous_array(value)]},
32+
),
33+
Mapping: (
34+
"kvlistValue",
35+
lambda value: {"values": [{k: _value(v) for k, v in value.items()}]},
36+
),
37+
}
38+
39+
2140
def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
2241
spans = sorted(spans, key=lambda s: (id(s.resource), id(s.instrumentation_scope)))
2342
rv = {"resourceSpans": []}
@@ -47,36 +66,41 @@ def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
4766

4867

4968
def _resource(resource: Resource):
50-
return {
51-
"attributes": [
52-
{"key": k, "value": _value(v)} for k, v in resource.attributes.items()
53-
]
54-
}
69+
rv = {"attributes": []}
70+
for k, v in resource.attributes.items():
71+
try:
72+
rv["attributes"].append({"key": k, "value": _value(v)})
73+
except ValueError:
74+
pass
75+
76+
# NOTE: blocks that contain droppedAttributesCount:
77+
# - Event
78+
# - Link
79+
# - InstrumentationScope
80+
# - LogRecord (out of scope for this library)
81+
# - Resource
82+
if dropped := len(resource.attributes) - len(rv["attributes"]):
83+
rv["dropped_attribute_count"] = dropped # type: ignore
84+
85+
return rv
86+
87+
88+
def _homogeneous_array(value: list[_LEAF_VALUE]) -> list[_LEAF_VALUE]:
89+
# TODO: empty lists are allowed, aren't they?
90+
if len(types := {type(v) for v in value}) > 1:
91+
raise ValueError(f"Attribute value arrays must be homogeneous, got {types=}")
92+
return value
5593

5694

5795
def _value(value: _VALUE) -> dict[str, Any]:
5896
# Attribute value can be a primitive type, excluging None...
5997
# TODO: protobuf allows bytes, but I think OTLP doesn't.
6098
# TODO: protobuf allows k:v pairs, but I think OTLP doesn't.
61-
if isinstance(value, (str, int, float, bool)):
62-
k = {
63-
# TODO: move these to module level
64-
str: "stringValue",
65-
int: "intValue",
66-
float: "floatValue",
67-
bool: "boolValue",
68-
}[type(value)]
69-
return {k: value}
70-
71-
# Or a homogenous array of a primitive type, excluding None.
72-
value = list(value)
99+
for klass, (key, post) in _VALUE_TYPES.items():
100+
if isinstance(value, klass):
101+
return {key: post(value)}
73102

74-
# TODO: empty lists are allowed, aren't they?
75-
if len({type(v) for v in value}) > 1:
76-
raise ValueError(f"Attribute value arrays must be homogenous, got {value}")
77-
78-
# TODO: maybe prevent recursion, OTEL doesn't allow lists of lists
79-
return {"arrayValue": [_value(e) for e in value]}
103+
raise ValueError(f"Cannot convert attribute of {type(value)=}")
80104

81105

82106
def _scope(scope: InstrumentationScope):
@@ -98,8 +122,22 @@ def _span(span: ReadableSpan):
98122
"endTimeUnixNano": str(span.end_time), # -"-
99123
"status": _status(span.status),
100124
}
125+
101126
if span.parent:
102127
rv["parentSpanId"] = _span_id(span.parent.span_id)
128+
129+
if span.attributes:
130+
rv["attributes"] = []
131+
132+
for k, v in span.attributes.items(): # type: ignore
133+
try:
134+
rv["attributes"].append({"key": k, "value": _value(v)})
135+
except ValueError:
136+
pass
137+
138+
if dropped := len(span.attributes) - len(rv.get("attributes", ())): # type: ignore
139+
rv["dropped_attribute_count"] = dropped # type: ignore
140+
103141
return rv
104142

105143

pyproject.toml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "otlp-json"
3-
version = "0.9.1"
3+
version = "0.9.2"
44
description = "🐍Lightweight OTEL span to JSON converter, no dependencies, pure Python🐍"
55
requires-python = ">=3.8"
66
# https://github.com/astral-sh/uv/issues/4204
@@ -41,3 +41,7 @@ testing = [
4141
"pytest ~= 8.3.4",
4242
"otlp-test-data >= 0.9.3",
4343
]
44+
45+
[build-system]
46+
requires = ["hatchling"]
47+
build-backend = "hatchling.build"

uv.lock

Lines changed: 29 additions & 29 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)