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.
- otlp_json-0.9.1.dist-info/METADATA +88 -0
- otlp_json-0.9.1.dist-info/RECORD +5 -0
- otlp_json-0.9.1.dist-info/WHEEL +5 -0
- otlp_json-0.9.1.dist-info/top_level.txt +1 -0
- otlp_json.py +120 -0
|
@@ -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 @@
|
|
|
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 {}
|