agent-observatory-cli 0.1.0__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.
- agent_observatory_cli-0.1.0.dist-info/METADATA +115 -0
- agent_observatory_cli-0.1.0.dist-info/RECORD +31 -0
- agent_observatory_cli-0.1.0.dist-info/WHEEL +5 -0
- agent_observatory_cli-0.1.0.dist-info/entry_points.txt +2 -0
- agent_observatory_cli-0.1.0.dist-info/licenses/LICENSE +21 -0
- agent_observatory_cli-0.1.0.dist-info/top_level.txt +1 -0
- observatory/__init__.py +5 -0
- observatory/__main__.py +105 -0
- observatory/activity.css +10 -0
- observatory/activity.js +116 -0
- observatory/auto.py +212 -0
- observatory/client.py +253 -0
- observatory/core.py +1654 -0
- observatory/distribution.py +67 -0
- observatory/findings.py +176 -0
- observatory/index.html +84 -0
- observatory/integrations/__init__.py +1 -0
- observatory/integrations/langchain.py +234 -0
- observatory/integrations/openai_agents.py +177 -0
- observatory/local_activity.py +207 -0
- observatory/local_agents.py +394 -0
- observatory/local_runtime.py +49 -0
- observatory/logs.py +109 -0
- observatory/metrics.py +281 -0
- observatory/otlp_proto.py +210 -0
- observatory/server.py +368 -0
- observatory/setup.css +40 -0
- observatory/setup.js +145 -0
- observatory/terminal.py +525 -0
- observatory/terminal_ui.py +1299 -0
- observatory/watch.py +62 -0
observatory/metrics.py
ADDED
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
"""Normalize OTLP metric data points from JSON and HTTP/protobuf."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
import base64
|
|
5
|
+
import math
|
|
6
|
+
import re
|
|
7
|
+
import struct
|
|
8
|
+
|
|
9
|
+
from .core import _attrs
|
|
10
|
+
from .otlp_proto import _fields, _nested, _attributes, _text, _kv, _signed
|
|
11
|
+
|
|
12
|
+
KINDS = {"gauge": 5, "sum": 7, "histogram": 9,
|
|
13
|
+
"exponentialHistogram": 10, "summary": 11}
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def _json_safe(value):
|
|
17
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
18
|
+
return str(value)
|
|
19
|
+
if isinstance(value, list):
|
|
20
|
+
return [_json_safe(item) for item in value]
|
|
21
|
+
if isinstance(value, dict):
|
|
22
|
+
return {key: _json_safe(item) for key, item in value.items()}
|
|
23
|
+
return value
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def _normalize(service, resource, scope, metric, kind, temporality, point):
|
|
27
|
+
timestamp = int(point.get("timeUnixNano") or 0)
|
|
28
|
+
if timestamp <= 0:
|
|
29
|
+
raise ValueError("metric point requires timeUnixNano")
|
|
30
|
+
start = int(point.get("startTimeUnixNano") or 0)
|
|
31
|
+
value = point.get("asDouble", point.get("asInt"))
|
|
32
|
+
if value is not None:
|
|
33
|
+
value = float(value) if "asDouble" in point else int(value)
|
|
34
|
+
data = {key: item for key, item in point.items() if key not in
|
|
35
|
+
("timeUnixNano", "startTimeUnixNano", "attributes", "asDouble", "asInt")}
|
|
36
|
+
if "exemplars" in point:
|
|
37
|
+
data["exemplars"] = _normalize_exemplars(point.get("exemplars"))
|
|
38
|
+
if value is not None:
|
|
39
|
+
data["value"] = value
|
|
40
|
+
return {"service": service, "resource_attributes": _json_safe(resource), "scope_name": scope,
|
|
41
|
+
"name": str(metric.get("name") or "unnamed"),
|
|
42
|
+
"description": str(metric.get("description") or ""),
|
|
43
|
+
"unit": str(metric.get("unit") or ""), "kind": kind,
|
|
44
|
+
"temporality": temporality, "time_unix_nano": timestamp,
|
|
45
|
+
"start_unix_nano": start, "attributes": _json_safe(_attrs(point.get("attributes"))),
|
|
46
|
+
"data": _json_safe(data)}
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _normalize_exemplars(items):
|
|
51
|
+
result = []
|
|
52
|
+
for item in items or []:
|
|
53
|
+
if not isinstance(item, dict):
|
|
54
|
+
continue
|
|
55
|
+
trace_id = item.get("trace_id", item.get("traceId"))
|
|
56
|
+
span_id = item.get("span_id", item.get("spanId"))
|
|
57
|
+
if isinstance(trace_id, bytes):
|
|
58
|
+
trace_id = trace_id.hex()
|
|
59
|
+
if isinstance(span_id, bytes):
|
|
60
|
+
span_id = span_id.hex()
|
|
61
|
+
trace_id = str(trace_id).lower() if trace_id is not None else ""
|
|
62
|
+
span_id = str(span_id).lower() if span_id is not None else ""
|
|
63
|
+
if trace_id and not re.fullmatch(r"[0-9a-f]{32}", trace_id):
|
|
64
|
+
trace_id = ""
|
|
65
|
+
if span_id and not re.fullmatch(r"[0-9a-f]{16}", span_id):
|
|
66
|
+
span_id = ""
|
|
67
|
+
try:
|
|
68
|
+
time_ns = max(0, int(item.get("time_unix_nano", item.get("timeUnixNano", 0))))
|
|
69
|
+
except (TypeError, ValueError, OverflowError):
|
|
70
|
+
time_ns = 0
|
|
71
|
+
value = item.get("value", item.get("asDouble", item.get("asInt")))
|
|
72
|
+
attrs = item.get("filtered_attributes", item.get("filteredAttributes", {}))
|
|
73
|
+
if isinstance(attrs, list):
|
|
74
|
+
attrs = _attrs(attrs)
|
|
75
|
+
result.append({"trace_id": trace_id or None, "span_id": span_id or None,
|
|
76
|
+
"time_unix_nano": time_ns, "value": _json_safe(value),
|
|
77
|
+
"filtered_attributes": _json_safe(attrs if isinstance(attrs, dict) else {})})
|
|
78
|
+
return result
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def _exemplar(raw, depth):
|
|
82
|
+
exemplar = {}
|
|
83
|
+
filtered = {}
|
|
84
|
+
for number, wire, value in _nested(raw, depth):
|
|
85
|
+
if number == 2 and wire == 1:
|
|
86
|
+
exemplar["time_unix_nano"] = value
|
|
87
|
+
elif number == 3 and wire == 1:
|
|
88
|
+
exemplar["value"] = _double(value)
|
|
89
|
+
elif number == 4 and wire == 2:
|
|
90
|
+
exemplar["span_id"] = value.hex()
|
|
91
|
+
elif number == 5 and wire == 2:
|
|
92
|
+
exemplar["trace_id"] = value.hex()
|
|
93
|
+
elif number == 6 and wire == 1:
|
|
94
|
+
exemplar["value"] = _signed(value)
|
|
95
|
+
elif number == 7 and wire == 2:
|
|
96
|
+
key, item = _kv(value, depth + 1)
|
|
97
|
+
if key:
|
|
98
|
+
filtered[key] = item
|
|
99
|
+
if filtered:
|
|
100
|
+
exemplar["filtered_attributes"] = filtered
|
|
101
|
+
return exemplar
|
|
102
|
+
|
|
103
|
+
def normalize_otlp_metrics(body):
|
|
104
|
+
result = []
|
|
105
|
+
for group in body.get("resourceMetrics", []):
|
|
106
|
+
resource = _attrs(group.get("resource", {}).get("attributes"))
|
|
107
|
+
service = str(resource.get("service.name") or "unknown")
|
|
108
|
+
for scope in group.get("scopeMetrics", []):
|
|
109
|
+
scope_name = str(scope.get("scope", {}).get("name") or "")
|
|
110
|
+
for metric in scope.get("metrics", []):
|
|
111
|
+
matched = False
|
|
112
|
+
for kind in KINDS:
|
|
113
|
+
if kind in metric:
|
|
114
|
+
matched = True
|
|
115
|
+
payload = metric[kind]
|
|
116
|
+
temporality = int(payload.get("aggregationTemporality") or 0)
|
|
117
|
+
result.extend(_normalize(service, resource, scope_name, metric,
|
|
118
|
+
kind, temporality, point)
|
|
119
|
+
for point in payload.get("dataPoints", []))
|
|
120
|
+
break
|
|
121
|
+
if not matched:
|
|
122
|
+
raise ValueError(f"metric {metric.get('name', 'unnamed')} has no supported data type")
|
|
123
|
+
return result
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
def _double(value):
|
|
127
|
+
return struct.unpack("<d", value.to_bytes(8, "little"))[0]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _packed_fixed64(value):
|
|
131
|
+
if len(value) % 8:
|
|
132
|
+
raise ValueError("invalid packed fixed64 array")
|
|
133
|
+
return [int.from_bytes(value[i:i + 8], "little") for i in range(0, len(value), 8)]
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def _point(raw, kind, depth):
|
|
137
|
+
attrs_field = 1 if kind == "exponentialHistogram" else 9 if kind == "histogram" else 7
|
|
138
|
+
point = {"attributes": []}
|
|
139
|
+
buckets = {"positive": None, "negative": None}
|
|
140
|
+
exemplar_field = 5 if kind in ("gauge", "sum") else 8 if kind == "histogram" else 11 if kind == "exponentialHistogram" else None
|
|
141
|
+
for number, wire, value in _nested(raw, depth):
|
|
142
|
+
if number == exemplar_field and wire == 2:
|
|
143
|
+
point.setdefault("exemplars", []).append(_exemplar(value, depth + 1))
|
|
144
|
+
elif number == attrs_field and wire == 2:
|
|
145
|
+
key, item = _kv(value, depth + 1)
|
|
146
|
+
if key:
|
|
147
|
+
point["attributes"].append({"key": key, "value": _json_any(item)})
|
|
148
|
+
elif number == 2 and wire == 1:
|
|
149
|
+
point["startTimeUnixNano"] = value
|
|
150
|
+
elif number == 3 and wire == 1:
|
|
151
|
+
point["timeUnixNano"] = value
|
|
152
|
+
elif kind in ("gauge", "sum"):
|
|
153
|
+
if number == 4 and wire == 1:
|
|
154
|
+
point["asDouble"] = _double(value)
|
|
155
|
+
elif number == 6 and wire == 1:
|
|
156
|
+
point["asInt"] = _signed(value)
|
|
157
|
+
elif kind in ("histogram", "exponentialHistogram", "summary"):
|
|
158
|
+
if number == 4 and wire == 1:
|
|
159
|
+
point["count"] = value
|
|
160
|
+
elif number == 5 and wire == 1:
|
|
161
|
+
point["sum"] = _double(value)
|
|
162
|
+
elif kind == "histogram":
|
|
163
|
+
if number == 6 and wire == 1:
|
|
164
|
+
point.setdefault("bucketCounts", []).append(value)
|
|
165
|
+
elif number == 6 and wire == 2:
|
|
166
|
+
point.setdefault("bucketCounts", []).extend(_packed_fixed64(value))
|
|
167
|
+
elif number == 7 and wire == 1:
|
|
168
|
+
point.setdefault("explicitBounds", []).append(_double(value))
|
|
169
|
+
elif number == 7 and wire == 2:
|
|
170
|
+
point.setdefault("explicitBounds", []).extend(_double(v) for v in _packed_fixed64(value))
|
|
171
|
+
elif number in (11, 12) and wire == 1:
|
|
172
|
+
point["min" if number == 11 else "max"] = _double(value)
|
|
173
|
+
elif kind == "exponentialHistogram":
|
|
174
|
+
if number == 6 and wire == 0:
|
|
175
|
+
point["scale"] = (value >> 1) ^ -(value & 1)
|
|
176
|
+
elif number == 7 and wire == 1:
|
|
177
|
+
point["zeroCount"] = value
|
|
178
|
+
elif number in (8, 9) and wire == 2:
|
|
179
|
+
buckets["positive" if number == 8 else "negative"] = _exp_buckets(value, depth + 1)
|
|
180
|
+
elif number in (12, 13, 14) and wire == 1:
|
|
181
|
+
point[{12: "min", 13: "max", 14: "zeroThreshold"}[number]] = _double(value)
|
|
182
|
+
elif kind == "summary" and number == 6 and wire == 2:
|
|
183
|
+
quantile = {}
|
|
184
|
+
for field, typ, val in _nested(value, depth + 1):
|
|
185
|
+
if field in (1, 2) and typ == 1:
|
|
186
|
+
quantile["quantile" if field == 1 else "value"] = _double(val)
|
|
187
|
+
point.setdefault("quantileValues", []).append(quantile)
|
|
188
|
+
if kind == "exponentialHistogram":
|
|
189
|
+
point.update({key: value for key, value in buckets.items() if value is not None})
|
|
190
|
+
return point
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
def _json_any(value):
|
|
194
|
+
if isinstance(value, str):
|
|
195
|
+
return {"stringValue": value}
|
|
196
|
+
if isinstance(value, bool):
|
|
197
|
+
return {"boolValue": value}
|
|
198
|
+
if isinstance(value, int):
|
|
199
|
+
return {"intValue": value}
|
|
200
|
+
if isinstance(value, float):
|
|
201
|
+
return {"doubleValue": value}
|
|
202
|
+
if isinstance(value, bytes):
|
|
203
|
+
return {"bytesValue": base64.b64encode(value).decode()}
|
|
204
|
+
if isinstance(value, list):
|
|
205
|
+
return {"arrayValue": {"values": [_json_any(v) for v in value]}}
|
|
206
|
+
if isinstance(value, dict):
|
|
207
|
+
return {"kvlistValue": {"values": [{"key": k, "value": _json_any(v)} for k, v in value.items()]}}
|
|
208
|
+
return {}
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
def _exp_buckets(raw, depth):
|
|
212
|
+
result = {"offset": 0, "bucketCounts": []}
|
|
213
|
+
for number, wire, value in _nested(raw, depth):
|
|
214
|
+
if number == 1 and wire == 0:
|
|
215
|
+
result["offset"] = (value >> 1) ^ -(value & 1)
|
|
216
|
+
elif number == 2 and wire == 0:
|
|
217
|
+
result["bucketCounts"].append(value)
|
|
218
|
+
elif number == 2 and wire == 2:
|
|
219
|
+
result["bucketCounts"].extend(_unpack_varints(value))
|
|
220
|
+
return result
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def _unpack_varints(raw):
|
|
224
|
+
from .otlp_proto import _varint
|
|
225
|
+
data = memoryview(raw)
|
|
226
|
+
pos = 0
|
|
227
|
+
values = []
|
|
228
|
+
while pos < len(data):
|
|
229
|
+
value, pos = _varint(data, pos)
|
|
230
|
+
values.append(value)
|
|
231
|
+
return values
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def _metric(raw, service, resource, scope, depth):
|
|
235
|
+
metric = {"name": "", "description": "", "unit": ""}
|
|
236
|
+
payload = None
|
|
237
|
+
kind = None
|
|
238
|
+
for number, wire, value in _nested(raw, depth):
|
|
239
|
+
if number in (1, 2, 3) and wire == 2:
|
|
240
|
+
metric[{1: "name", 2: "description", 3: "unit"}[number]] = _text(value)
|
|
241
|
+
elif number in KINDS.values() and wire == 2:
|
|
242
|
+
kind = next(name for name, field in KINDS.items() if field == number)
|
|
243
|
+
payload = value
|
|
244
|
+
if payload is None:
|
|
245
|
+
raise ValueError(f"metric {metric['name'] or 'unnamed'} has no supported data type")
|
|
246
|
+
points = []
|
|
247
|
+
temporality = 0
|
|
248
|
+
for number, wire, value in _nested(payload, depth + 1):
|
|
249
|
+
if number == 1 and wire == 2:
|
|
250
|
+
points.append(_point(value, kind, depth + 2))
|
|
251
|
+
elif number == 2 and wire == 0:
|
|
252
|
+
temporality = value
|
|
253
|
+
return [_normalize(service, resource, scope, metric, kind, temporality, point) for point in points]
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def decode_metric_request(raw):
|
|
257
|
+
result = []
|
|
258
|
+
for number, wire, group in _fields(raw):
|
|
259
|
+
if number != 1 or wire != 2:
|
|
260
|
+
continue
|
|
261
|
+
resource = {}
|
|
262
|
+
scopes = []
|
|
263
|
+
for field, kind, value in _nested(group, 1):
|
|
264
|
+
if field == 1 and kind == 2:
|
|
265
|
+
resource = _attributes(value, 2)
|
|
266
|
+
elif field == 2 and kind == 2:
|
|
267
|
+
scopes.append(value)
|
|
268
|
+
service = str(resource.get("service.name") or "unknown")
|
|
269
|
+
for scope in scopes:
|
|
270
|
+
scope_name = ""
|
|
271
|
+
metrics = []
|
|
272
|
+
for field, kind, value in _nested(scope, 2):
|
|
273
|
+
if field == 1 and kind == 2:
|
|
274
|
+
for key, typ, item in _nested(value, 3):
|
|
275
|
+
if key == 1 and typ == 2:
|
|
276
|
+
scope_name = _text(item)
|
|
277
|
+
elif field == 2 and kind == 2:
|
|
278
|
+
metrics.append(value)
|
|
279
|
+
for metric in metrics:
|
|
280
|
+
result.extend(_metric(metric, service, resource, scope_name, 3))
|
|
281
|
+
return result
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"""Decode OTLP/HTTP protobuf trace requests without a protobuf runtime.
|
|
2
|
+
|
|
3
|
+
Field numbers follow opentelemetry-proto's collector/trace, trace, resource,
|
|
4
|
+
and common v1 messages. Unknown fields are skipped as protobuf requires.
|
|
5
|
+
This module handles the trace signal only; it is not a general protobuf library.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import base64
|
|
10
|
+
import struct
|
|
11
|
+
|
|
12
|
+
from .core import normalize_native
|
|
13
|
+
|
|
14
|
+
MAX_DEPTH = 16
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def _varint(data: memoryview, pos: int):
|
|
18
|
+
value = 0
|
|
19
|
+
for shift in range(0, 70, 7):
|
|
20
|
+
if pos >= len(data):
|
|
21
|
+
raise ValueError("truncated protobuf varint")
|
|
22
|
+
byte = data[pos]
|
|
23
|
+
pos += 1
|
|
24
|
+
value |= (byte & 0x7f) << shift
|
|
25
|
+
if byte < 128:
|
|
26
|
+
if value >= 1 << 64:
|
|
27
|
+
raise ValueError("protobuf varint exceeds uint64")
|
|
28
|
+
return value, pos
|
|
29
|
+
raise ValueError("protobuf varint exceeds 10 bytes")
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def _fields(raw: bytes, depth=0):
|
|
33
|
+
if depth > MAX_DEPTH:
|
|
34
|
+
raise ValueError("protobuf nesting is too deep")
|
|
35
|
+
data = memoryview(raw)
|
|
36
|
+
pos = 0
|
|
37
|
+
while pos < len(data):
|
|
38
|
+
tag, pos = _varint(data, pos)
|
|
39
|
+
number, wire = tag >> 3, tag & 7
|
|
40
|
+
if not number:
|
|
41
|
+
raise ValueError("invalid protobuf field number")
|
|
42
|
+
if wire == 0:
|
|
43
|
+
value, pos = _varint(data, pos)
|
|
44
|
+
elif wire == 1:
|
|
45
|
+
if len(data) - pos < 8:
|
|
46
|
+
raise ValueError("truncated protobuf fixed64")
|
|
47
|
+
value = int.from_bytes(data[pos:pos + 8], "little")
|
|
48
|
+
pos += 8
|
|
49
|
+
elif wire == 2:
|
|
50
|
+
size, pos = _varint(data, pos)
|
|
51
|
+
if size > len(data) - pos:
|
|
52
|
+
raise ValueError("truncated protobuf field")
|
|
53
|
+
value = bytes(data[pos:pos + size])
|
|
54
|
+
pos += size
|
|
55
|
+
elif wire == 5:
|
|
56
|
+
if len(data) - pos < 4:
|
|
57
|
+
raise ValueError("truncated protobuf fixed32")
|
|
58
|
+
value = int.from_bytes(data[pos:pos + 4], "little")
|
|
59
|
+
pos += 4
|
|
60
|
+
else:
|
|
61
|
+
raise ValueError(f"unsupported protobuf wire type {wire}")
|
|
62
|
+
yield number, wire, value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def _text(value):
|
|
66
|
+
return value.decode("utf-8")
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def _nested(value, depth):
|
|
70
|
+
return _fields(value, depth + 1)
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _signed(value):
|
|
74
|
+
return value - (1 << 64) if value >= (1 << 63) else value
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def _any_value(raw, depth):
|
|
78
|
+
result = None
|
|
79
|
+
for number, wire, value in _nested(raw, depth):
|
|
80
|
+
if number == 1 and wire == 2:
|
|
81
|
+
result = _text(value)
|
|
82
|
+
elif number == 2 and wire == 0:
|
|
83
|
+
result = bool(value)
|
|
84
|
+
elif number == 3 and wire == 0:
|
|
85
|
+
result = _signed(value)
|
|
86
|
+
elif number == 4 and wire == 1:
|
|
87
|
+
result = struct.unpack("<d", value.to_bytes(8, "little"))[0]
|
|
88
|
+
elif number == 5 and wire == 2:
|
|
89
|
+
result = [_any_value(v, depth + 2) for n, w, v in _nested(value, depth + 1) if n == 1 and w == 2]
|
|
90
|
+
elif number == 6 and wire == 2:
|
|
91
|
+
result = _attributes(value, depth + 1)
|
|
92
|
+
elif number == 7 and wire == 2:
|
|
93
|
+
result = base64.b64encode(value).decode("ascii")
|
|
94
|
+
return result
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _kv(raw, depth):
|
|
98
|
+
key, value = "", None
|
|
99
|
+
for number, wire, item in _nested(raw, depth):
|
|
100
|
+
if number == 1 and wire == 2:
|
|
101
|
+
key = _text(item)
|
|
102
|
+
elif number == 2 and wire == 2:
|
|
103
|
+
value = _any_value(item, depth + 1)
|
|
104
|
+
return key, value
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def _attributes(raw, depth, field=1):
|
|
108
|
+
result = {}
|
|
109
|
+
for number, wire, value in _nested(raw, depth):
|
|
110
|
+
if number == field and wire == 2:
|
|
111
|
+
key, item = _kv(value, depth + 1)
|
|
112
|
+
if key:
|
|
113
|
+
result[key] = item
|
|
114
|
+
return result
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def _event(raw, depth):
|
|
118
|
+
event = {"name": "event", "time_unix_nano": 0, "attributes": {}}
|
|
119
|
+
for number, wire, value in _nested(raw, depth):
|
|
120
|
+
if number == 1 and wire == 1:
|
|
121
|
+
event["time_unix_nano"] = value
|
|
122
|
+
elif number == 2 and wire == 2:
|
|
123
|
+
event["name"] = _text(value)
|
|
124
|
+
elif number == 3 and wire == 2:
|
|
125
|
+
key, item = _kv(value, depth + 1)
|
|
126
|
+
if key:
|
|
127
|
+
event["attributes"][key] = item
|
|
128
|
+
return event
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def _status(raw, depth):
|
|
132
|
+
code = 0
|
|
133
|
+
for number, wire, value in _nested(raw, depth):
|
|
134
|
+
if number == 3 and wire == 0:
|
|
135
|
+
code = value
|
|
136
|
+
return {0: "UNSET", 1: "OK", 2: "ERROR"}.get(code, "UNSET")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _link(raw, depth):
|
|
140
|
+
result = {"trace_id": None, "span_id": None, "attributes": {}}
|
|
141
|
+
for number, wire, value in _nested(raw, depth):
|
|
142
|
+
if number == 1 and wire == 2:
|
|
143
|
+
result["trace_id"] = value.hex()
|
|
144
|
+
elif number == 2 and wire == 2:
|
|
145
|
+
result["span_id"] = value.hex()
|
|
146
|
+
elif number == 4 and wire == 2:
|
|
147
|
+
key, item = _kv(value, depth + 1)
|
|
148
|
+
if key:
|
|
149
|
+
result["attributes"][key] = item
|
|
150
|
+
return result
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
def _span(raw, service, resource, scope_name, depth):
|
|
154
|
+
native = {"trace_id": None, "span_id": None, "parent_span_id": None,
|
|
155
|
+
"name": "unnamed", "start_unix_nano": 0, "end_unix_nano": 0,
|
|
156
|
+
"status": "UNSET", "attributes": {}, "events": [], "links": [],
|
|
157
|
+
"resource_attributes": resource, "scope_name": scope_name}
|
|
158
|
+
for number, wire, value in _nested(raw, depth):
|
|
159
|
+
if number == 1 and wire == 2:
|
|
160
|
+
native["trace_id"] = value.hex()
|
|
161
|
+
elif number == 2 and wire == 2:
|
|
162
|
+
native["span_id"] = value.hex()
|
|
163
|
+
elif number == 4 and wire == 2:
|
|
164
|
+
native["parent_span_id"] = value.hex() if value else None
|
|
165
|
+
elif number == 5 and wire == 2:
|
|
166
|
+
native["name"] = _text(value)
|
|
167
|
+
elif number == 7 and wire == 1:
|
|
168
|
+
native["start_unix_nano"] = value
|
|
169
|
+
elif number == 8 and wire == 1:
|
|
170
|
+
native["end_unix_nano"] = value
|
|
171
|
+
elif number == 9 and wire == 2:
|
|
172
|
+
key, item = _kv(value, depth + 1)
|
|
173
|
+
if key:
|
|
174
|
+
native["attributes"][key] = item
|
|
175
|
+
elif number == 11 and wire == 2:
|
|
176
|
+
native["events"].append(_event(value, depth + 1))
|
|
177
|
+
elif number == 13 and wire == 2:
|
|
178
|
+
native["links"].append(_link(value, depth + 1))
|
|
179
|
+
elif number == 15 and wire == 2:
|
|
180
|
+
native["status"] = _status(value, depth + 1)
|
|
181
|
+
return normalize_native(native, service)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
def decode_trace_request(raw: bytes):
|
|
185
|
+
"""Return normalized spans from an ExportTraceServiceRequest protobuf body."""
|
|
186
|
+
spans = []
|
|
187
|
+
for number, wire, resource_spans in _fields(raw):
|
|
188
|
+
if number != 1 or wire != 2:
|
|
189
|
+
continue
|
|
190
|
+
resource = {}
|
|
191
|
+
scopes = []
|
|
192
|
+
for field, kind, value in _nested(resource_spans, 1):
|
|
193
|
+
if field == 1 and kind == 2:
|
|
194
|
+
resource = _attributes(value, 2)
|
|
195
|
+
elif field == 2 and kind == 2:
|
|
196
|
+
scopes.append(value)
|
|
197
|
+
for scope in scopes:
|
|
198
|
+
scope_name = ""
|
|
199
|
+
raw_spans = []
|
|
200
|
+
for field, kind, value in _nested(scope, 2):
|
|
201
|
+
if field == 1 and kind == 2:
|
|
202
|
+
for scope_field, scope_kind, scope_value in _nested(value, 3):
|
|
203
|
+
if scope_field == 1 and scope_kind == 2:
|
|
204
|
+
scope_name = _text(scope_value)
|
|
205
|
+
elif field == 2 and kind == 2:
|
|
206
|
+
raw_spans.append(value)
|
|
207
|
+
for span in raw_spans:
|
|
208
|
+
spans.append(_span(span, str(resource.get("service.name") or "unknown"),
|
|
209
|
+
resource, scope_name, 3))
|
|
210
|
+
return spans
|