otlp-json 0.9.3__tar.gz → 0.9.5__tar.gz
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-0.9.3 → otlp_json-0.9.5}/PKG-INFO +1 -1
- otlp_json-0.9.5/otlp_json/__init__.py +199 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/pyproject.toml +1 -1
- otlp_json-0.9.5/test/test_pure_python.py +27 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/uv.lock +28 -28
- otlp_json-0.9.3/otlp_json/__init__.py +0 -193
- otlp_json-0.9.3/test/test_pure_python.py +0 -17
- {otlp_json-0.9.3 → otlp_json-0.9.5}/.github/workflows/ci.yaml +0 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/.gitignore +0 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/.pre-commit-config.yaml +0 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/otlp_json/py.typed +0 -0
- {otlp_json-0.9.3 → otlp_json-0.9.5}/readme.md +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: otlp-json
|
|
3
|
-
Version: 0.9.
|
|
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,199 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from collections.abc import Mapping, Sequence
|
|
5
|
+
from typing import Any, TYPE_CHECKING
|
|
6
|
+
|
|
7
|
+
if TYPE_CHECKING:
|
|
8
|
+
from typing_extensions import TypeAlias
|
|
9
|
+
|
|
10
|
+
from opentelemetry.trace import Link
|
|
11
|
+
from opentelemetry._logs import LogRecord
|
|
12
|
+
from opentelemetry.sdk.trace import ReadableSpan, Event
|
|
13
|
+
from opentelemetry.sdk.resources import Resource
|
|
14
|
+
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
|
|
15
|
+
from opentelemetry.trace.status import Status
|
|
16
|
+
|
|
17
|
+
_LEAF_VALUE: TypeAlias = "str | int | float | bool" # TODO: confirm
|
|
18
|
+
_VALUE: TypeAlias = "_LEAF_VALUE | Sequence[_LEAF_VALUE]"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
__all__ = [
|
|
22
|
+
"CONTENT_TYPE",
|
|
23
|
+
"encode_spans",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
CONTENT_TYPE = "application/json"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
|
|
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)
|
|
50
|
+
rv = {"resourceSpans": []}
|
|
51
|
+
last_resource = last_scope = None
|
|
52
|
+
for span in spans:
|
|
53
|
+
assert span.resource
|
|
54
|
+
assert span.instrumentation_scope
|
|
55
|
+
if span.resource != last_resource:
|
|
56
|
+
last_resource = span.resource
|
|
57
|
+
last_scope = None
|
|
58
|
+
rv["resourceSpans"].append(
|
|
59
|
+
{
|
|
60
|
+
"resource": _resource(span.resource),
|
|
61
|
+
"scopeSpans": [],
|
|
62
|
+
}
|
|
63
|
+
)
|
|
64
|
+
if span.instrumentation_scope != last_scope:
|
|
65
|
+
last_scope = span.instrumentation_scope
|
|
66
|
+
rv["resourceSpans"][-1]["scopeSpans"].append(
|
|
67
|
+
{
|
|
68
|
+
"scope": _scope(span.instrumentation_scope),
|
|
69
|
+
"spans": [],
|
|
70
|
+
}
|
|
71
|
+
)
|
|
72
|
+
rv["resourceSpans"][-1]["scopeSpans"][-1]["spans"].append(_span(span))
|
|
73
|
+
return json.dumps(rv, separators=(",", ":")).encode("utf-8")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def _resource(resource: Resource):
|
|
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():
|
|
88
|
+
try:
|
|
89
|
+
rv["attributes"].append({"key": k, "value": _value(v)})
|
|
90
|
+
except ValueError:
|
|
91
|
+
pass
|
|
92
|
+
|
|
93
|
+
rv["dropped_attributes_count"] = len(thing.attributes) - len(rv["attributes"]) # type: ignore
|
|
94
|
+
|
|
95
|
+
for k in ("attributes", "dropped_attributes_count"):
|
|
96
|
+
if not rv[k]:
|
|
97
|
+
del rv[k]
|
|
98
|
+
|
|
99
|
+
return rv
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def _ensure_homogeneous(value: Sequence[_LEAF_VALUE]) -> Sequence[_LEAF_VALUE]:
|
|
103
|
+
# TODO: empty lists are allowed, aren't they?
|
|
104
|
+
if len(types := {type(v) for v in value}) > 1:
|
|
105
|
+
raise ValueError(f"Attribute value arrays must be homogeneous, got {types=}")
|
|
106
|
+
return value
|
|
107
|
+
|
|
108
|
+
|
|
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()}]}}
|
|
126
|
+
|
|
127
|
+
raise ValueError(f"Cannot convert attribute value of {type(v)=}")
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _scope(scope: InstrumentationScope):
|
|
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
|
+
}
|
|
138
|
+
if scope.version:
|
|
139
|
+
rv["version"] = scope.version
|
|
140
|
+
return rv
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
_REMOTE = 0x300
|
|
144
|
+
_LOCAL = 0x100
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def _span(span: ReadableSpan):
|
|
148
|
+
assert span.context
|
|
149
|
+
rv = {
|
|
150
|
+
"name": span.name,
|
|
151
|
+
"kind": span.kind.value or 1, # unspecified -> internal
|
|
152
|
+
"traceId": _trace_id(span.context.trace_id),
|
|
153
|
+
"spanId": _span_id(span.context.span_id),
|
|
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?
|
|
157
|
+
"status": _status(span.status),
|
|
158
|
+
**_attributes(span),
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if span.parent:
|
|
162
|
+
rv["parentSpanId"] = _span_id(span.parent.span_id)
|
|
163
|
+
|
|
164
|
+
# TODO: is this field really nullable?
|
|
165
|
+
if span.events:
|
|
166
|
+
rv["events"] = [_event(e) for e in span.events]
|
|
167
|
+
|
|
168
|
+
return rv
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
def _trace_id(trace_id: int) -> str:
|
|
172
|
+
if not 0 <= trace_id < 2**128:
|
|
173
|
+
raise ValueError(f"The {trace_id=} is out of bounds")
|
|
174
|
+
return hex(trace_id)[2:].rjust(32, "0")
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
def _span_id(span_id: int) -> str:
|
|
178
|
+
if not 0 <= span_id < 2**64:
|
|
179
|
+
raise ValueError(f"The {span_id=} is out of bounds")
|
|
180
|
+
return hex(span_id)[2:].rjust(16, "0")
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def _status(status: Status) -> dict[str, Any]:
|
|
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
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _event(event: Event) -> dict[str, Any]:
|
|
193
|
+
rv = {
|
|
194
|
+
"name": event.name,
|
|
195
|
+
"timeUnixNano": str(event.timestamp),
|
|
196
|
+
**_attributes(event),
|
|
197
|
+
}
|
|
198
|
+
# TODO: any optional attributes?
|
|
199
|
+
return rv
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
import otlp_test_data
|
|
7
|
+
|
|
8
|
+
import otlp_json
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
CONTENT_TYPE = "application/json"
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def canonical(batch):
|
|
15
|
+
batch["resourceSpans"].sort(key=lambda rs: json.dumps(rs["resource"]))
|
|
16
|
+
for rs in batch["resourceSpans"]:
|
|
17
|
+
rs["scopeSpans"].sort(key=lambda sc: sc["scope"]["name"])
|
|
18
|
+
for sc in rs["scopeSpans"]:
|
|
19
|
+
sc["spans"].sort(key=lambda sp: sp["spanId"])
|
|
20
|
+
|
|
21
|
+
return batch
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_equiv():
|
|
25
|
+
auth = canonical(json.loads(otlp_test_data.sample_json()))
|
|
26
|
+
mine = canonical(json.loads(otlp_json.encode_spans(otlp_test_data.sample_spans())))
|
|
27
|
+
assert mine == auth
|
|
@@ -374,7 +374,7 @@ wheels = [
|
|
|
374
374
|
|
|
375
375
|
[[package]]
|
|
376
376
|
name = "otlp-json"
|
|
377
|
-
version = "0.9.
|
|
377
|
+
version = "0.9.5"
|
|
378
378
|
source = { editable = "." }
|
|
379
379
|
|
|
380
380
|
[package.dev-dependencies]
|
|
@@ -405,7 +405,7 @@ testing = [
|
|
|
405
405
|
|
|
406
406
|
[[package]]
|
|
407
407
|
name = "otlp-test-data"
|
|
408
|
-
version = "0.11.
|
|
408
|
+
version = "0.11.4"
|
|
409
409
|
source = { registry = "https://pypi.org/simple" }
|
|
410
410
|
dependencies = [
|
|
411
411
|
{ name = "freezegun" },
|
|
@@ -414,9 +414,9 @@ dependencies = [
|
|
|
414
414
|
{ name = "opentelemetry-exporter-otlp" },
|
|
415
415
|
{ name = "opentelemetry-exporter-otlp-proto-http" },
|
|
416
416
|
]
|
|
417
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
417
|
+
sdist = { url = "https://files.pythonhosted.org/packages/c1/f2/a2c0658e6ef7994f32e767f414017494f35a15f9302a039162ab4452756d/otlp_test_data-0.11.4.tar.gz", hash = "sha256:b380828e27cc6304571fdc3011f1ff86335ffb337bb6f511c07d8208b8982e67", size = 38334 }
|
|
418
418
|
wheels = [
|
|
419
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
419
|
+
{ url = "https://files.pythonhosted.org/packages/47/ed/582d5a51faefabdc951c72af7f7c8a2490746664a68c9df3b9dcc50b1e38/otlp_test_data-0.11.4-py3-none-any.whl", hash = "sha256:25753d0a34d0b14da0d15e09d4dfef29abe16097b4022c36bfb42d9d6010f2b7", size = 4042 },
|
|
420
420
|
]
|
|
421
421
|
|
|
422
422
|
[[package]]
|
|
@@ -457,15 +457,15 @@ wheels = [
|
|
|
457
457
|
|
|
458
458
|
[[package]]
|
|
459
459
|
name = "pyright"
|
|
460
|
-
version = "1.1.
|
|
460
|
+
version = "1.1.395"
|
|
461
461
|
source = { registry = "https://pypi.org/simple" }
|
|
462
462
|
dependencies = [
|
|
463
463
|
{ name = "nodeenv" },
|
|
464
464
|
{ name = "typing-extensions" },
|
|
465
465
|
]
|
|
466
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
466
|
+
sdist = { url = "https://files.pythonhosted.org/packages/fb/47/a2e1dfd70f9f0db34f70d5b108c82be57bf24185af69c95acff57f9239fa/pyright-1.1.395.tar.gz", hash = "sha256:53703169068c160bfb41e1b44ba3e2512492869c26cfad927e1268cb3fbb1b1c", size = 3813566 }
|
|
467
467
|
wheels = [
|
|
468
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
468
|
+
{ url = "https://files.pythonhosted.org/packages/5f/a1/531897f8caa6c6cc99862cd1c908ddd8a366a51d968e83ab4523ded98b30/pyright-1.1.395-py3-none-any.whl", hash = "sha256:f9bc726870e740c6c77c94657734d90563a3e9765bb523b39f5860198ed75eef", size = 5688787 },
|
|
469
469
|
]
|
|
470
470
|
|
|
471
471
|
[[package]]
|
|
@@ -515,27 +515,27 @@ wheels = [
|
|
|
515
515
|
|
|
516
516
|
[[package]]
|
|
517
517
|
name = "ruff"
|
|
518
|
-
version = "0.9.
|
|
519
|
-
source = { registry = "https://pypi.org/simple" }
|
|
520
|
-
sdist = { url = "https://files.pythonhosted.org/packages/
|
|
521
|
-
wheels = [
|
|
522
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
523
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
524
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
525
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
526
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
527
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
528
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
529
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
530
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
531
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
532
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
533
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
534
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
535
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
536
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
537
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
538
|
-
{ url = "https://files.pythonhosted.org/packages/
|
|
518
|
+
version = "0.9.9"
|
|
519
|
+
source = { registry = "https://pypi.org/simple" }
|
|
520
|
+
sdist = { url = "https://files.pythonhosted.org/packages/6f/c3/418441a8170e8d53d05c0b9dad69760dbc7b8a12c10dbe6db1e1205d2377/ruff-0.9.9.tar.gz", hash = "sha256:0062ed13f22173e85f8f7056f9a24016e692efeea8704d1a5e8011b8aa850933", size = 3717448 }
|
|
521
|
+
wheels = [
|
|
522
|
+
{ url = "https://files.pythonhosted.org/packages/bc/c3/2c4afa9ba467555d074b146d9aed0633a56ccdb900839fb008295d037b89/ruff-0.9.9-py3-none-linux_armv6l.whl", hash = "sha256:628abb5ea10345e53dff55b167595a159d3e174d6720bf19761f5e467e68d367", size = 10027252 },
|
|
523
|
+
{ url = "https://files.pythonhosted.org/packages/33/d1/439e58487cf9eac26378332e25e7d5ade4b800ce1eec7dc2cfc9b0d7ca96/ruff-0.9.9-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:b6cd1428e834b35d7493354723543b28cc11dc14d1ce19b685f6e68e07c05ec7", size = 10840721 },
|
|
524
|
+
{ url = "https://files.pythonhosted.org/packages/50/44/fead822c38281ba0122f1b76b460488a175a9bd48b130650a6fb6dbcbcf9/ruff-0.9.9-py3-none-macosx_11_0_arm64.whl", hash = "sha256:5ee162652869120ad260670706f3cd36cd3f32b0c651f02b6da142652c54941d", size = 10161439 },
|
|
525
|
+
{ url = "https://files.pythonhosted.org/packages/11/ae/d404a2ab8e61ddf6342e09cc6b7f7846cce6b243e45c2007dbe0ca928a5d/ruff-0.9.9-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3aa0f6b75082c9be1ec5a1db78c6d4b02e2375c3068438241dc19c7c306cc61a", size = 10336264 },
|
|
526
|
+
{ url = "https://files.pythonhosted.org/packages/6a/4e/7c268aa7d84cd709fb6f046b8972313142cffb40dfff1d2515c5e6288d54/ruff-0.9.9-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:584cc66e89fb5f80f84b05133dd677a17cdd86901d6479712c96597a3f28e7fe", size = 9908774 },
|
|
527
|
+
{ url = "https://files.pythonhosted.org/packages/cc/26/c618a878367ef1b76270fd027ca93692657d3f6122b84ba48911ef5f2edc/ruff-0.9.9-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:abf3369325761a35aba75cd5c55ba1b5eb17d772f12ab168fbfac54be85cf18c", size = 11428127 },
|
|
528
|
+
{ url = "https://files.pythonhosted.org/packages/d7/9a/c5588a93d9bfed29f565baf193fe802fa676a0c837938137ea6cf0576d8c/ruff-0.9.9-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:3403a53a32a90ce929aa2f758542aca9234befa133e29f4933dcef28a24317be", size = 12133187 },
|
|
529
|
+
{ url = "https://files.pythonhosted.org/packages/3e/ff/e7980a7704a60905ed7e156a8d73f604c846d9bd87deda9cabfa6cba073a/ruff-0.9.9-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:18454e7fa4e4d72cffe28a37cf6a73cb2594f81ec9f4eca31a0aaa9ccdfb1590", size = 11602937 },
|
|
530
|
+
{ url = "https://files.pythonhosted.org/packages/24/78/3690444ad9e3cab5c11abe56554c35f005b51d1d118b429765249095269f/ruff-0.9.9-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fadfe2c88724c9617339f62319ed40dcdadadf2888d5afb88bf3adee7b35bfb", size = 13771698 },
|
|
531
|
+
{ url = "https://files.pythonhosted.org/packages/6e/bf/e477c2faf86abe3988e0b5fd22a7f3520e820b2ee335131aca2e16120038/ruff-0.9.9-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6df104d08c442a1aabcfd254279b8cc1e2cbf41a605aa3e26610ba1ec4acf0b0", size = 11249026 },
|
|
532
|
+
{ url = "https://files.pythonhosted.org/packages/f7/82/cdaffd59e5a8cb5b14c408c73d7a555a577cf6645faaf83e52fe99521715/ruff-0.9.9-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:d7c62939daf5b2a15af48abbd23bea1efdd38c312d6e7c4cedf5a24e03207e17", size = 10220432 },
|
|
533
|
+
{ url = "https://files.pythonhosted.org/packages/fe/a4/2507d0026225efa5d4412b6e294dfe54725a78652a5c7e29e6bd0fc492f3/ruff-0.9.9-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:9494ba82a37a4b81b6a798076e4a3251c13243fc37967e998efe4cce58c8a8d1", size = 9874602 },
|
|
534
|
+
{ url = "https://files.pythonhosted.org/packages/d5/be/f3aab1813846b476c4bcffe052d232244979c3cd99d751c17afb530ca8e4/ruff-0.9.9-py3-none-musllinux_1_2_i686.whl", hash = "sha256:4efd7a96ed6d36ef011ae798bf794c5501a514be369296c672dab7921087fa57", size = 10851212 },
|
|
535
|
+
{ url = "https://files.pythonhosted.org/packages/8b/45/8e5fd559bea0d2f57c4e12bf197a2fade2fac465aa518284f157dfbca92b/ruff-0.9.9-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:ab90a7944c5a1296f3ecb08d1cbf8c2da34c7e68114b1271a431a3ad30cb660e", size = 11327490 },
|
|
536
|
+
{ url = "https://files.pythonhosted.org/packages/42/55/e6c90f13880aeef327746052907e7e930681f26a164fe130ddac28b08269/ruff-0.9.9-py3-none-win32.whl", hash = "sha256:6b4c376d929c25ecd6d87e182a230fa4377b8e5125a4ff52d506ee8c087153c1", size = 10227912 },
|
|
537
|
+
{ url = "https://files.pythonhosted.org/packages/35/b2/da925693cb82a1208aa34966c0f36cb222baca94e729dd22a587bc22d0f3/ruff-0.9.9-py3-none-win_amd64.whl", hash = "sha256:837982ea24091d4c1700ddb2f63b7070e5baec508e43b01de013dc7eff974ff1", size = 11355632 },
|
|
538
|
+
{ url = "https://files.pythonhosted.org/packages/31/d8/de873d1c1b020d668d8ec9855d390764cb90cf8f6486c0983da52be8b7b7/ruff-0.9.9-py3-none-win_arm64.whl", hash = "sha256:3ac78f127517209fe6d96ab00f3ba97cafe38718b23b1db3e96d8b2d39e37ddf", size = 10435860 },
|
|
539
539
|
]
|
|
540
540
|
|
|
541
541
|
[[package]]
|
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
from collections.abc import Mapping, Sequence
|
|
5
|
-
from typing import Any, TYPE_CHECKING
|
|
6
|
-
|
|
7
|
-
if TYPE_CHECKING:
|
|
8
|
-
from typing_extensions import TypeAlias
|
|
9
|
-
|
|
10
|
-
from opentelemetry.sdk.trace import ReadableSpan, Event
|
|
11
|
-
from opentelemetry.sdk.resources import Resource
|
|
12
|
-
from opentelemetry.sdk.util.instrumentation import InstrumentationScope
|
|
13
|
-
from opentelemetry.trace.status import Status
|
|
14
|
-
|
|
15
|
-
_LEAF_VALUE: TypeAlias = "str | int | float | bool" # TODO: confirm
|
|
16
|
-
_VALUE: TypeAlias = "_LEAF_VALUE | Sequence[_LEAF_VALUE]"
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
CONTENT_TYPE = "application/json"
|
|
20
|
-
|
|
21
|
-
|
|
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
|
-
|
|
40
|
-
def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
|
|
41
|
-
spans = sorted(spans, key=lambda s: (id(s.resource), id(s.instrumentation_scope)))
|
|
42
|
-
rv = {"resourceSpans": []}
|
|
43
|
-
last_rs = last_is = None
|
|
44
|
-
for span in spans:
|
|
45
|
-
assert span.resource
|
|
46
|
-
assert span.instrumentation_scope
|
|
47
|
-
if span.resource is not last_rs:
|
|
48
|
-
last_rs = span.resource
|
|
49
|
-
last_is = None
|
|
50
|
-
rv["resourceSpans"].append(
|
|
51
|
-
{
|
|
52
|
-
"resource": _resource(span.resource),
|
|
53
|
-
"scopeSpans": [],
|
|
54
|
-
}
|
|
55
|
-
)
|
|
56
|
-
if span.instrumentation_scope is not last_is:
|
|
57
|
-
last_is = span.instrumentation_scope
|
|
58
|
-
rv["resourceSpans"][-1]["scopeSpans"].append(
|
|
59
|
-
{
|
|
60
|
-
"scope": _scope(span.instrumentation_scope),
|
|
61
|
-
"spans": [],
|
|
62
|
-
}
|
|
63
|
-
)
|
|
64
|
-
rv["resourceSpans"][-1]["scopeSpans"][-1]["spans"].append(_span(span))
|
|
65
|
-
return json.dumps(rv, separators=(",", ":")).encode("utf-8")
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
def _resource(resource: Resource):
|
|
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
|
-
rv["dropped_attributes_count"] = len(resource.attributes) - len(rv["attributes"]) # type: ignore
|
|
83
|
-
|
|
84
|
-
if not rv["attributes"]:
|
|
85
|
-
del rv["attributes"]
|
|
86
|
-
|
|
87
|
-
if not rv["dropped_attributes_count"]:
|
|
88
|
-
del rv["dropped_attributes_count"]
|
|
89
|
-
|
|
90
|
-
return rv
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
def _homogeneous_array(value: list[_LEAF_VALUE]) -> list[_LEAF_VALUE]:
|
|
94
|
-
# TODO: empty lists are allowed, aren't they?
|
|
95
|
-
if len(types := {type(v) for v in value}) > 1:
|
|
96
|
-
raise ValueError(f"Attribute value arrays must be homogeneous, got {types=}")
|
|
97
|
-
return value
|
|
98
|
-
|
|
99
|
-
|
|
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)}
|
|
107
|
-
|
|
108
|
-
raise ValueError(f"Cannot convert attribute of {type(value)=}")
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
def _scope(scope: InstrumentationScope):
|
|
112
|
-
rv = {"name": scope.name}
|
|
113
|
-
if scope.version:
|
|
114
|
-
rv["version"] = scope.version
|
|
115
|
-
return rv
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
def _span(span: ReadableSpan):
|
|
119
|
-
assert span.context
|
|
120
|
-
rv = {
|
|
121
|
-
"name": span.name,
|
|
122
|
-
"kind": span.kind.value or 1, # unspecified -> internal
|
|
123
|
-
"traceId": _trace_id(span.context.trace_id),
|
|
124
|
-
"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), # -"-
|
|
128
|
-
"status": _status(span.status),
|
|
129
|
-
"attributes": [],
|
|
130
|
-
}
|
|
131
|
-
|
|
132
|
-
if span.parent:
|
|
133
|
-
rv["parentSpanId"] = _span_id(span.parent.span_id)
|
|
134
|
-
|
|
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
|
-
|
|
149
|
-
if span.events:
|
|
150
|
-
rv["events"] = [_event(e) for e in span.events]
|
|
151
|
-
|
|
152
|
-
return rv
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def _trace_id(trace_id: int) -> str:
|
|
156
|
-
if not 0 <= trace_id < 2**128:
|
|
157
|
-
raise ValueError(f"The {trace_id=} is out of bounds")
|
|
158
|
-
return hex(trace_id)[2:].rjust(32, "0")
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
def _span_id(span_id: int) -> str:
|
|
162
|
-
if not 0 <= span_id < 2**64:
|
|
163
|
-
raise ValueError(f"The {span_id=} is out of bounds")
|
|
164
|
-
return hex(span_id)[2:].rjust(16, "0")
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
def _status(status: Status) -> dict[str, Any]:
|
|
168
|
-
# FIXME
|
|
169
|
-
return {}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
def _event(event: Event) -> dict[str, Any]:
|
|
173
|
-
rv = {
|
|
174
|
-
"name": event.name,
|
|
175
|
-
"timeUnixNano": str(event.timestamp),
|
|
176
|
-
"attributes": [],
|
|
177
|
-
}
|
|
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
|
-
|
|
193
|
-
return rv
|
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
from __future__ import annotations
|
|
2
|
-
|
|
3
|
-
import json
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
import otlp_test_data
|
|
7
|
-
|
|
8
|
-
import otlp_json
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
CONTENT_TYPE = "application/json"
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def test_equiv():
|
|
15
|
-
auth = json.loads(otlp_test_data.sample_json())
|
|
16
|
-
mine = json.loads(otlp_json.encode_spans(otlp_test_data.sample_spans()))
|
|
17
|
-
assert mine == auth
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|