otlp-json 0.9.1__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.
@@ -0,0 +1,88 @@
1
+ Metadata-Version: 2.2
2
+ Name: otlp-json
3
+ Version: 0.9.1
4
+ Summary: 🐍Lightweight OTEL span to JSON converter, no dependencies, pure Python🐍
5
+ Classifier: Development Status :: 3 - Alpha
6
+ Classifier: Intended Audience :: Developers
7
+ Classifier: License :: OSI Approved :: MIT License
8
+ Classifier: Natural Language :: English
9
+ Classifier: Operating System :: OS Independent
10
+ Classifier: Programming Language :: Python
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Programming Language :: Python :: 3.8
13
+ Classifier: Programming Language :: Python :: 3.9
14
+ Classifier: Programming Language :: Python :: 3.10
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Programming Language :: Python :: 3.13
18
+ Classifier: Programming Language :: Python :: 3.14
19
+ Classifier: Programming Language :: Python :: 3 :: Only
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
22
+ Classifier: Topic :: Software Development :: Libraries
23
+ Classifier: Framework :: OpenTelemetry
24
+ Classifier: Framework :: OpenTelemetry :: Exporters
25
+ Requires-Python: >=3.8
26
+ Description-Content-Type: text/markdown
27
+
28
+ # otlp-json
29
+
30
+ `otpl-json` is an OTLP serialisation library.
31
+
32
+ It's written in pure Python, without dependencies.
33
+
34
+ It serialises a bunch of spans into OTLP 1.5 JSON format.
35
+
36
+ ### Motivation
37
+
38
+ Tracing should be on by default.
39
+
40
+ OTLP is the standard data format and API, and the standard Python package is `opentelemetry-exporter-otlp-proto-http`. It brings in a total of 18 packages and adds 9MB to the project virtual environment.
41
+
42
+ A typical Python application, that's being instrumented, only generates own tracing data and needs to send it out. It doesn't need that much complexity.
43
+
44
+ ### Usage
45
+
46
+ ```py
47
+ from otlp_json import CONTENT_TYPE, encode_spans
48
+
49
+
50
+ class SomeExporter:
51
+ def export(self.spans: Sequece[ReadableSpan]) -> None:
52
+ requests.post(
53
+ "http://localhost:4318/v1/traces",
54
+ data=encode_spans(spans),
55
+ headers={"Content-Type": CONTENT_TYPE},
56
+ )
57
+ ```
58
+
59
+ ### Library size
60
+
61
+ - 3KB whl, containing:
62
+ - 4KB Python source
63
+ - ?? metadata
64
+
65
+ ### TODO(doc)
66
+
67
+ - link to rust library
68
+ - link to urllib sender
69
+ - link to test vector generator
70
+
71
+ ### TODO(features)
72
+
73
+ - Events
74
+ - Links
75
+ - Baggage
76
+ - Schemata, when https://github.com/open-telemetry/opentelemetry-python/pull/4359 lands
77
+
78
+ ### TODO(fixes)
79
+
80
+ - Status fields
81
+ - validate what fields are in fact optional
82
+ - ???
83
+
84
+ ### Limitations
85
+
86
+ This library is meant to marshal tracing data that's collected in the same Python process.
87
+
88
+ It is not meant to be used for data received and forwarded.
@@ -0,0 +1,5 @@
1
+ otlp_json.py,sha256=GCHauwquLROkov7v4WfmevNlKzC45aYDbvqGYCb66lg,3869
2
+ otlp_json-0.9.1.dist-info/METADATA,sha256=3OlCQjfep4nN69aMkIVHxTOawNDCnEqOLIu5cDCKHVQ,2685
3
+ otlp_json-0.9.1.dist-info/WHEEL,sha256=In9FTNxeP60KnTkGw7wk6mJPYd_dQSjEZmXdBdMCI-8,91
4
+ otlp_json-0.9.1.dist-info/top_level.txt,sha256=7jOohtnyFAP4ixxnF_1gQz1NKEuVS8ELe3ymA4I5FtU,10
5
+ otlp_json-0.9.1.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (75.8.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1 @@
1
+ otlp_json
otlp_json.py ADDED
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from typing import Any, Sequence, TYPE_CHECKING
5
+
6
+ if TYPE_CHECKING:
7
+ from typing_extensions import TypeAlias
8
+
9
+ from opentelemetry.sdk.trace import ReadableSpan
10
+ from opentelemetry.sdk.resources import Resource
11
+ from opentelemetry.sdk.util.instrumentation import InstrumentationScope
12
+ from opentelemetry.trace.status import Status
13
+
14
+ _LEAF_VALUE: TypeAlias = "str | int | float | bool" # TODO: confirm
15
+ _VALUE: TypeAlias = "_LEAF_VALUE | Sequence[_LEAF_VALUE]"
16
+
17
+
18
+ CONTENT_TYPE = "application/json"
19
+
20
+
21
+ def encode_spans(spans: Sequence[ReadableSpan]) -> bytes:
22
+ spans = sorted(spans, key=lambda s: (id(s.resource), id(s.instrumentation_scope)))
23
+ rv = {"resourceSpans": []}
24
+ last_rs = last_is = None
25
+ for span in spans:
26
+ assert span.resource
27
+ assert span.instrumentation_scope
28
+ if span.resource is not last_rs:
29
+ last_rs = span.resource
30
+ last_is = None
31
+ rv["resourceSpans"].append(
32
+ {
33
+ "resource": _resource(span.resource),
34
+ "scopeSpans": [],
35
+ }
36
+ )
37
+ if span.instrumentation_scope is not last_is:
38
+ last_is = span.instrumentation_scope
39
+ rv["resourceSpans"][-1]["scopeSpans"].append(
40
+ {
41
+ "scope": _scope(span.instrumentation_scope),
42
+ "spans": [],
43
+ }
44
+ )
45
+ rv["resourceSpans"][-1]["scopeSpans"][-1]["spans"].append(_span(span))
46
+ return json.dumps(rv, separators=(",", ":")).encode("utf-8")
47
+
48
+
49
+ def _resource(resource: Resource):
50
+ return {
51
+ "attributes": [
52
+ {"key": k, "value": _value(v)} for k, v in resource.attributes.items()
53
+ ]
54
+ }
55
+
56
+
57
+ def _value(value: _VALUE) -> dict[str, Any]:
58
+ # Attribute value can be a primitive type, excluging None...
59
+ # TODO: protobuf allows bytes, but I think OTLP doesn't.
60
+ # 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)
73
+
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]}
80
+
81
+
82
+ def _scope(scope: InstrumentationScope):
83
+ rv = {"name": scope.name}
84
+ if scope.version:
85
+ rv["version"] = scope.version
86
+ return rv
87
+
88
+
89
+ def _span(span: ReadableSpan):
90
+ assert span.context
91
+ rv = {
92
+ "name": span.name,
93
+ "kind": span.kind.value or 1, # unspecified -> internal
94
+ "traceId": _trace_id(span.context.trace_id),
95
+ "spanId": _span_id(span.context.span_id),
96
+ "flags": 0x100 | ([0, 0x200][bool(span.parent)]),
97
+ "startTimeUnixNano": str(span.start_time), # TODO: is it ever optional?
98
+ "endTimeUnixNano": str(span.end_time), # -"-
99
+ "status": _status(span.status),
100
+ }
101
+ if span.parent:
102
+ rv["parentSpanId"] = _span_id(span.parent.span_id)
103
+ return rv
104
+
105
+
106
+ def _trace_id(trace_id: int) -> str:
107
+ if not 0 <= trace_id < 2**128:
108
+ raise ValueError(f"The {trace_id=} is out of bounds")
109
+ return hex(trace_id)[2:].rjust(32, "0")
110
+
111
+
112
+ def _span_id(span_id: int) -> str:
113
+ if not 0 <= span_id < 2**64:
114
+ raise ValueError(f"The {span_id=} is out of bounds")
115
+ return hex(span_id)[2:].rjust(16, "0")
116
+
117
+
118
+ def _status(status: Status) -> dict[str, Any]:
119
+ # FIXME
120
+ return {}