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.
@@ -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,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
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "otlp-json"
3
- version = "0.9.3"
3
+ version = "0.9.5"
4
4
  description = "🐍Lightweight OTEL span to JSON converter, no dependencies, pure Python🐍"
5
5
  requires-python = ">=3.8"
6
6
  # https://github.com/astral-sh/uv/issues/4204
@@ -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.2"
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.0"
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/70/c9/bb8de92ae3eea7ba501e38661270d505ec094453fcfaa6d4aa1da120fc8f/otlp_test_data-0.11.0.tar.gz", hash = "sha256:01855e4d84afa76a33457594c30023b1e33accdcf8aaef4611159274f2dd66c3", size = 37638 }
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/28/b3/464748f40e489fc335c2d01147c0cbbe19eb2ec2ab95bdb6b33b74649ea4/otlp_test_data-0.11.0-py3-none-any.whl", hash = "sha256:254955385a4ee1fc30b8514c08600f65b8b7dca55073b6cbdca555c02c533e8b", size = 3373 },
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.394"
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/b1/e4/79f4d8a342eed6790fdebdb500e95062f319ee3d7d75ae27304ff995ae8c/pyright-1.1.394.tar.gz", hash = "sha256:56f2a3ab88c5214a451eb71d8f2792b7700434f841ea219119ade7f42ca93608", size = 3809348 }
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/d6/4c/50c74e3d589517a9712a61a26143b587dba6285434a17aebf2ce6b82d2c3/pyright-1.1.394-py3-none-any.whl", hash = "sha256:5f74cce0a795a295fb768759bbeeec62561215dea657edcaab48a932b031ddbb", size = 5679540 },
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.7"
519
- source = { registry = "https://pypi.org/simple" }
520
- sdist = { url = "https://files.pythonhosted.org/packages/39/8b/a86c300359861b186f18359adf4437ac8e4c52e42daa9eedc731ef9d5b53/ruff-0.9.7.tar.gz", hash = "sha256:643757633417907510157b206e490c3aa11cab0c087c912f60e07fbafa87a4c6", size = 3669813 }
521
- wheels = [
522
- { url = "https://files.pythonhosted.org/packages/b1/f3/3a1d22973291226df4b4e2ff70196b926b6f910c488479adb0eeb42a0d7f/ruff-0.9.7-py3-none-linux_armv6l.whl", hash = "sha256:99d50def47305fe6f233eb8dabfd60047578ca87c9dcb235c9723ab1175180f4", size = 11774588 },
523
- { url = "https://files.pythonhosted.org/packages/8e/c9/b881f4157b9b884f2994fd08ee92ae3663fb24e34b0372ac3af999aa7fc6/ruff-0.9.7-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:d59105ae9c44152c3d40a9c40d6331a7acd1cdf5ef404fbe31178a77b174ea66", size = 11746848 },
524
- { url = "https://files.pythonhosted.org/packages/14/89/2f546c133f73886ed50a3d449e6bf4af27d92d2f960a43a93d89353f0945/ruff-0.9.7-py3-none-macosx_11_0_arm64.whl", hash = "sha256:f313b5800483770bd540cddac7c90fc46f895f427b7820f18fe1822697f1fec9", size = 11177525 },
525
- { url = "https://files.pythonhosted.org/packages/d7/93/6b98f2c12bf28ab9def59c50c9c49508519c5b5cfecca6de871cf01237f6/ruff-0.9.7-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:042ae32b41343888f59c0a4148f103208bf6b21c90118d51dc93a68366f4e903", size = 11996580 },
526
- { url = "https://files.pythonhosted.org/packages/8e/3f/b3fcaf4f6d875e679ac2b71a72f6691a8128ea3cb7be07cbb249f477c061/ruff-0.9.7-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:87862589373b33cc484b10831004e5e5ec47dc10d2b41ba770e837d4f429d721", size = 11525674 },
527
- { url = "https://files.pythonhosted.org/packages/f0/48/33fbf18defb74d624535d5d22adcb09a64c9bbabfa755bc666189a6b2210/ruff-0.9.7-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a17e1e01bee0926d351a1ee9bc15c445beae888f90069a6192a07a84af544b6b", size = 12739151 },
528
- { url = "https://files.pythonhosted.org/packages/63/b5/7e161080c5e19fa69495cbab7c00975ef8a90f3679caa6164921d7f52f4a/ruff-0.9.7-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:7c1f880ac5b2cbebd58b8ebde57069a374865c73f3bf41f05fe7a179c1c8ef22", size = 13416128 },
529
- { url = "https://files.pythonhosted.org/packages/4e/c8/b5e7d61fb1c1b26f271ac301ff6d9de5e4d9a9a63f67d732fa8f200f0c88/ruff-0.9.7-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:e63fc20143c291cab2841dbb8260e96bafbe1ba13fd3d60d28be2c71e312da49", size = 12870858 },
530
- { url = "https://files.pythonhosted.org/packages/da/cb/2a1a8e4e291a54d28259f8fc6a674cd5b8833e93852c7ef5de436d6ed729/ruff-0.9.7-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:91ff963baed3e9a6a4eba2a02f4ca8eaa6eba1cc0521aec0987da8d62f53cbef", size = 14786046 },
531
- { url = "https://files.pythonhosted.org/packages/ca/6c/c8f8a313be1943f333f376d79724260da5701426c0905762e3ddb389e3f4/ruff-0.9.7-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88362e3227c82f63eaebf0b2eff5b88990280fb1ecf7105523883ba8c3aaf6fb", size = 12550834 },
532
- { url = "https://files.pythonhosted.org/packages/9d/ad/f70cf5e8e7c52a25e166bdc84c082163c9c6f82a073f654c321b4dff9660/ruff-0.9.7-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0372c5a90349f00212270421fe91874b866fd3626eb3b397ede06cd385f6f7e0", size = 11961307 },
533
- { url = "https://files.pythonhosted.org/packages/52/d5/4f303ea94a5f4f454daf4d02671b1fbfe2a318b5fcd009f957466f936c50/ruff-0.9.7-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:d76b8ab60e99e6424cd9d3d923274a1324aefce04f8ea537136b8398bbae0a62", size = 11612039 },
534
- { url = "https://files.pythonhosted.org/packages/eb/c8/bd12a23a75603c704ce86723be0648ba3d4ecc2af07eecd2e9fa112f7e19/ruff-0.9.7-py3-none-musllinux_1_2_i686.whl", hash = "sha256:0c439bdfc8983e1336577f00e09a4e7a78944fe01e4ea7fe616d00c3ec69a3d0", size = 12168177 },
535
- { url = "https://files.pythonhosted.org/packages/cc/57/d648d4f73400fef047d62d464d1a14591f2e6b3d4a15e93e23a53c20705d/ruff-0.9.7-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:115d1f15e8fdd445a7b4dc9a30abae22de3f6bcabeb503964904471691ef7606", size = 12610122 },
536
- { url = "https://files.pythonhosted.org/packages/49/79/acbc1edd03ac0e2a04ae2593555dbc9990b34090a9729a0c4c0cf20fb595/ruff-0.9.7-py3-none-win32.whl", hash = "sha256:e9ece95b7de5923cbf38893f066ed2872be2f2f477ba94f826c8defdd6ec6b7d", size = 9988751 },
537
- { url = "https://files.pythonhosted.org/packages/6d/95/67153a838c6b6ba7a2401241fd8a00cd8c627a8e4a0491b8d853dedeffe0/ruff-0.9.7-py3-none-win_amd64.whl", hash = "sha256:3770fe52b9d691a15f0b87ada29c45324b2ace8f01200fb0c14845e499eb0c2c", size = 11002987 },
538
- { url = "https://files.pythonhosted.org/packages/63/6a/aca01554949f3a401991dc32fe22837baeaccb8a0d868256cbb26a029778/ruff-0.9.7-py3-none-win_arm64.whl", hash = "sha256:b075a700b2533feb7a01130ff656a4ec0d5f340bb540ad98759b8401c32c2037", size = 10177763 },
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