otlp-json 0.9.3__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
@@ -7,6 +7,8 @@ from typing import Any, TYPE_CHECKING
7
7
  if TYPE_CHECKING:
8
8
  from typing_extensions import TypeAlias
9
9
 
10
+ from opentelemetry.trace import Link
11
+ from opentelemetry._logs import LogRecord
10
12
  from opentelemetry.sdk.trace import ReadableSpan, Event
11
13
  from opentelemetry.sdk.resources import Resource
12
14
  from opentelemetry.sdk.util.instrumentation import InstrumentationScope
@@ -16,45 +18,51 @@ if TYPE_CHECKING:
16
18
  _VALUE: TypeAlias = "_LEAF_VALUE | Sequence[_LEAF_VALUE]"
17
19
 
18
20
 
19
- CONTENT_TYPE = "application/json"
20
-
21
+ __all__ = [
22
+ "CONTENT_TYPE",
23
+ "encode_spans",
24
+ ]
21
25
 
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
- }
26
+ CONTENT_TYPE = "application/json"
38
27
 
39
28
 
40
29
  def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
41
- 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)
42
50
  rv = {"resourceSpans": []}
43
- last_rs = last_is = None
51
+ last_resource = last_scope = None
44
52
  for span in spans:
45
53
  assert span.resource
46
54
  assert span.instrumentation_scope
47
- if span.resource is not last_rs:
48
- last_rs = span.resource
49
- last_is = None
55
+ if span.resource != last_resource:
56
+ last_resource = span.resource
57
+ last_scope = None
50
58
  rv["resourceSpans"].append(
51
59
  {
52
60
  "resource": _resource(span.resource),
53
61
  "scopeSpans": [],
54
62
  }
55
63
  )
56
- if span.instrumentation_scope is not last_is:
57
- last_is = span.instrumentation_scope
64
+ if span.instrumentation_scope != last_scope:
65
+ last_scope = span.instrumentation_scope
58
66
  rv["resourceSpans"][-1]["scopeSpans"].append(
59
67
  {
60
68
  "scope": _scope(span.instrumentation_scope),
@@ -66,55 +74,76 @@ def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
66
74
 
67
75
 
68
76
  def _resource(resource: Resource):
69
- rv = {"attributes": []}
70
- for k, v in resource.attributes.items():
77
+ # TODO: add schema_url once that lands in opentelemetry-sdk
78
+ return _attributes(resource)
79
+
80
+
81
+ def _attributes(
82
+ thing: Resource | InstrumentationScope | ReadableSpan | Event | Link | LogRecord,
83
+ ) -> dict[str, Any]:
84
+ rv = {"attributes": [], "dropped_attributes_count": 0}
85
+
86
+ assert thing.attributes is not None
87
+ for k, v in thing.attributes.items():
71
88
  try:
72
89
  rv["attributes"].append({"key": k, "value": _value(v)})
73
90
  except ValueError:
74
91
  pass
75
92
 
76
- # NOTE: blocks that contain droppedAttributesCount:
77
- # - Event
78
- # - Link
79
- # - InstrumentationScope
80
- # - LogRecord (out of scope for this library)
81
- # - Resource
82
- rv["dropped_attributes_count"] = len(resource.attributes) - len(rv["attributes"]) # type: ignore
93
+ rv["dropped_attributes_count"] = len(thing.attributes) - len(rv["attributes"]) # type: ignore
83
94
 
84
- if not rv["attributes"]:
85
- del rv["attributes"]
86
-
87
- if not rv["dropped_attributes_count"]:
88
- del rv["dropped_attributes_count"]
95
+ for k in ("attributes", "dropped_attributes_count"):
96
+ if not rv[k]:
97
+ del rv[k]
89
98
 
90
99
  return rv
91
100
 
92
101
 
93
- def _homogeneous_array(value: list[_LEAF_VALUE]) -> list[_LEAF_VALUE]:
102
+ def _ensure_homogeneous(value: Sequence[_LEAF_VALUE]) -> Sequence[_LEAF_VALUE]:
94
103
  # TODO: empty lists are allowed, aren't they?
95
104
  if len(types := {type(v) for v in value}) > 1:
96
105
  raise ValueError(f"Attribute value arrays must be homogeneous, got {types=}")
97
106
  return value
98
107
 
99
108
 
100
- def _value(value: _VALUE) -> dict[str, Any]:
101
- # Attribute value can be a primitive type, excluging None...
102
- # TODO: protobuf allows bytes, but I think OTLP doesn't.
103
- # TODO: protobuf allows k:v pairs, but I think OTLP doesn't.
104
- for klass, (key, post) in _VALUE_TYPES.items():
105
- if isinstance(value, klass):
106
- 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()}]}}
107
126
 
108
- raise ValueError(f"Cannot convert attribute of {type(value)=}")
127
+ raise ValueError(f"Cannot convert attribute value of {type(v)=}")
109
128
 
110
129
 
111
130
  def _scope(scope: InstrumentationScope):
112
- rv = {"name": scope.name}
131
+ rv = {
132
+ "name": scope.name,
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),
137
+ }
113
138
  if scope.version:
114
139
  rv["version"] = scope.version
115
140
  return rv
116
141
 
117
142
 
143
+ _REMOTE = 0x300
144
+ _LOCAL = 0x100
145
+
146
+
118
147
  def _span(span: ReadableSpan):
119
148
  assert span.context
120
149
  rv = {
@@ -122,30 +151,17 @@ def _span(span: ReadableSpan):
122
151
  "kind": span.kind.value or 1, # unspecified -> internal
123
152
  "traceId": _trace_id(span.context.trace_id),
124
153
  "spanId": _span_id(span.context.span_id),
125
- "flags": 0x100 | ([0, 0x200][bool(span.parent and span.parent.is_remote)]),
126
- "startTimeUnixNano": str(span.start_time), # TODO: is it ever optional?
127
- "endTimeUnixNano": str(span.end_time), # -"-
154
+ "flags": _REMOTE if span.parent and span.parent.is_remote else _LOCAL,
155
+ "startTimeUnixNano": str(span.start_time),
156
+ "endTimeUnixNano": str(span.end_time), # can this be unset?
128
157
  "status": _status(span.status),
129
- "attributes": [],
158
+ **_attributes(span),
130
159
  }
131
160
 
132
161
  if span.parent:
133
162
  rv["parentSpanId"] = _span_id(span.parent.span_id)
134
163
 
135
- for k, v in span.attributes.items(): # type: ignore
136
- try:
137
- rv["attributes"].append({"key": k, "value": _value(v)})
138
- except ValueError:
139
- pass
140
-
141
- rv["dropped_attributes_count"] = len(span.attributes) - len(rv["attributes"]) # type: ignore
142
-
143
- if not rv["attributes"]:
144
- del rv["attributes"]
145
-
146
- if not rv["dropped_attributes_count"]:
147
- del rv["dropped_attributes_count"]
148
-
164
+ # TODO: is this field really nullable?
149
165
  if span.events:
150
166
  rv["events"] = [_event(e) for e in span.events]
151
167
 
@@ -165,29 +181,19 @@ def _span_id(span_id: int) -> str:
165
181
 
166
182
 
167
183
  def _status(status: Status) -> dict[str, Any]:
168
- # FIXME
169
- 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
170
190
 
171
191
 
172
192
  def _event(event: Event) -> dict[str, Any]:
173
193
  rv = {
174
194
  "name": event.name,
175
195
  "timeUnixNano": str(event.timestamp),
176
- "attributes": [],
196
+ **_attributes(event),
177
197
  }
178
-
179
- for k, v in event.attributes.items(): # type: ignore
180
- try:
181
- rv["attributes"].append({"key": k, "value": _value(v)})
182
- except ValueError:
183
- pass
184
-
185
- rv["dropped_attributes_count"] = len(event.attributes) - len(rv["attributes"]) # type: ignore
186
-
187
- if not rv["attributes"]:
188
- del rv["attributes"]
189
-
190
- if not rv["dropped_attributes_count"]:
191
- del rv["dropped_attributes_count"]
192
-
198
+ # TODO: any optional attributes?
193
199
  return rv
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: otlp-json
3
- Version: 0.9.3
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=KFI4PCFiyTOw3wTUClS1hIeUyAMCCZp1F9F6K6Qi_IQ,5862
2
- otlp_json/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
- otlp_json-0.9.3.dist-info/METADATA,sha256=7RMTwaC70Vni4nVgZt-VB1-yCv-qlb7908cUTEWPQRs,2810
4
- otlp_json-0.9.3.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
5
- otlp_json-0.9.3.dist-info/RECORD,,