otlp-json 0.9.4__py3-none-any.whl → 0.9.5__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
otlp_json/__init__.py CHANGED
@@ -18,45 +18,51 @@ if TYPE_CHECKING:
18
18
  _VALUE: TypeAlias = "_LEAF_VALUE | Sequence[_LEAF_VALUE]"
19
19
 
20
20
 
21
- CONTENT_TYPE = "application/json"
22
-
21
+ __all__ = [
22
+ "CONTENT_TYPE",
23
+ "encode_spans",
24
+ ]
23
25
 
24
- _VALUE_TYPES = {
25
- # NOTE: order matters, for isinstance(True, int).
26
- bool: ("boolValue", bool),
27
- int: ("intValue", str),
28
- float: ("doubleValue", float),
29
- bytes: ("bytesValue", bytes),
30
- str: ("stringValue", str),
31
- Sequence: (
32
- "arrayValue",
33
- lambda value: {"values": [_value(e) for e in _homogeneous_array(value)]},
34
- ),
35
- Mapping: (
36
- "kvlistValue",
37
- lambda value: {"values": [{k: _value(v) for k, v in value.items()}]},
38
- ),
39
- }
26
+ CONTENT_TYPE = "application/json"
40
27
 
41
28
 
42
29
  def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
43
- spans = sorted(spans, key=lambda s: (id(s.resource), id(s.instrumentation_scope)))
30
+ resource_cache: dict[Resource, tuple] = {}
31
+ scope_cache: dict[InstrumentationScope, tuple] = {}
32
+
33
+ def linearise(span: ReadableSpan):
34
+ r = span.resource
35
+ if r not in resource_cache:
36
+ resource_cache[r] = (r.schema_url, tuple(r.attributes.items()))
37
+ s = span.instrumentation_scope
38
+ assert s
39
+ assert s.attributes is not None
40
+ if s not in scope_cache:
41
+ scope_cache[s] = (
42
+ s.schema_url,
43
+ s.name,
44
+ s.version,
45
+ tuple(s.attributes.items()),
46
+ )
47
+ return (resource_cache[r], scope_cache[s])
48
+
49
+ spans = sorted(spans, key=linearise)
44
50
  rv = {"resourceSpans": []}
45
- last_rs = last_is = None
51
+ last_resource = last_scope = None
46
52
  for span in spans:
47
53
  assert span.resource
48
54
  assert span.instrumentation_scope
49
- if span.resource is not last_rs:
50
- last_rs = span.resource
51
- last_is = None
55
+ if span.resource != last_resource:
56
+ last_resource = span.resource
57
+ last_scope = None
52
58
  rv["resourceSpans"].append(
53
59
  {
54
60
  "resource": _resource(span.resource),
55
61
  "scopeSpans": [],
56
62
  }
57
63
  )
58
- if span.instrumentation_scope is not last_is:
59
- last_is = span.instrumentation_scope
64
+ if span.instrumentation_scope != last_scope:
65
+ last_scope = span.instrumentation_scope
60
66
  rv["resourceSpans"][-1]["scopeSpans"].append(
61
67
  {
62
68
  "scope": _scope(span.instrumentation_scope),
@@ -93,35 +99,51 @@ def _attributes(
93
99
  return rv
94
100
 
95
101
 
96
- def _homogeneous_array(value: list[_LEAF_VALUE]) -> list[_LEAF_VALUE]:
102
+ def _ensure_homogeneous(value: Sequence[_LEAF_VALUE]) -> Sequence[_LEAF_VALUE]:
97
103
  # TODO: empty lists are allowed, aren't they?
98
104
  if len(types := {type(v) for v in value}) > 1:
99
105
  raise ValueError(f"Attribute value arrays must be homogeneous, got {types=}")
100
106
  return value
101
107
 
102
108
 
103
- def _value(value: _VALUE) -> dict[str, Any]:
104
- # Attribute value can be a primitive type, excluging None...
105
- # protobuf allows bytes, but I think OTLP spec does not?
106
- # protobuf allows k:v pairs, but I think OTLP doesn't.
107
- # TODO: read up the spec and validate the allowed type range.
108
- for klass, (key, post) in _VALUE_TYPES.items():
109
- if isinstance(value, klass):
110
- return {key: post(value)}
109
+ def _value(v: _VALUE) -> dict[str, Any]:
110
+ if isinstance(v, bool):
111
+ return {"boolValue": bool(v)}
112
+ if isinstance(v, int):
113
+ return {"intValue": str(int(v))}
114
+ if isinstance(v, float):
115
+ return {"doubleValue": float(v)}
116
+ if isinstance(v, bytes):
117
+ return {
118
+ "bytesValue": bytes(v)
119
+ } # FIXME this can't be right; gotta encode this somehow
120
+ if isinstance(v, str):
121
+ return {"stringValue": str(v)}
122
+ if isinstance(v, Sequence):
123
+ return {"arrayValue": {"values": [_value(e) for e in _ensure_homogeneous(v)]}}
124
+ if isinstance(v, Mapping):
125
+ return {"kvlistValue": {"values": [{k: _value(vv) for k, vv in v.items()}]}}
111
126
 
112
- raise ValueError(f"Cannot convert attribute of {type(value)=}")
127
+ raise ValueError(f"Cannot convert attribute value of {type(v)=}")
113
128
 
114
129
 
115
130
  def _scope(scope: InstrumentationScope):
116
131
  rv = {
117
132
  "name": scope.name,
118
- **_attributes(scope),
133
+ # Upstream code for attrs and schema has landed, but wasn't released yet
134
+ # https://github.com/open-telemetry/opentelemetry-python/pull/4359
135
+ # "schema_url": scope.schema_url, # check if it may be null
136
+ # **_attributes(scope),
119
137
  }
120
138
  if scope.version:
121
139
  rv["version"] = scope.version
122
140
  return rv
123
141
 
124
142
 
143
+ _REMOTE = 0x300
144
+ _LOCAL = 0x100
145
+
146
+
125
147
  def _span(span: ReadableSpan):
126
148
  assert span.context
127
149
  rv = {
@@ -129,7 +151,7 @@ def _span(span: ReadableSpan):
129
151
  "kind": span.kind.value or 1, # unspecified -> internal
130
152
  "traceId": _trace_id(span.context.trace_id),
131
153
  "spanId": _span_id(span.context.span_id),
132
- "flags": 0x100 | ([0, 0x200][bool(span.parent and span.parent.is_remote)]),
154
+ "flags": _REMOTE if span.parent and span.parent.is_remote else _LOCAL,
133
155
  "startTimeUnixNano": str(span.start_time),
134
156
  "endTimeUnixNano": str(span.end_time), # can this be unset?
135
157
  "status": _status(span.status),
@@ -159,8 +181,12 @@ def _span_id(span_id: int) -> str:
159
181
 
160
182
 
161
183
  def _status(status: Status) -> dict[str, Any]:
162
- # FIXME: need an example of bad status
163
- return {}
184
+ rv = {}
185
+ if status.status_code.value:
186
+ rv["code"] = status.status_code.value
187
+ if status.description:
188
+ rv["message"] = status.description
189
+ return rv
164
190
 
165
191
 
166
192
  def _event(event: Event) -> dict[str, Any]:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otlp-json
3
- Version: 0.9.4
3
+ Version: 0.9.5
4
4
  Summary: 🐍Lightweight OTEL span to JSON converter, no dependencies, pure Python🐍
5
5
  Project-URL: Repository, https://github.com/dimaqq/otlp-json
6
6
  Project-URL: Issues, https://github.com/dimaqq/otlp-json/issues
@@ -0,0 +1,5 @@
1
+ otlp_json/__init__.py,sha256=sUClLoVbl5pXQtY3qBpKHx8O7ahZPPTcbRdv6enmC4Q,6198
2
+ otlp_json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ otlp_json-0.9.5.dist-info/METADATA,sha256=3g016j1QprMogf3He-BHvBK1meNoOLGWOf6pUcjQLJM,2810
4
+ otlp_json-0.9.5.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
+ otlp_json-0.9.5.dist-info/RECORD,,
@@ -1,5 +0,0 @@
1
- otlp_json/__init__.py,sha256=Ur95iX9xoeylZxq_ZKr5jqUNPvlp7ELcgxwU71SC42A,5364
2
- otlp_json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- otlp_json-0.9.4.dist-info/METADATA,sha256=swioA9pQ_LXYVL-J0WgTcKjkbdaC7xh70m2rQ4FSBSs,2810
4
- otlp_json-0.9.4.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- otlp_json-0.9.4.dist-info/RECORD,,