agentloop-profiler 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.
- agentloop/__init__.py +136 -0
- agentloop/ablation_protocol.py +229 -0
- agentloop/ablations.py +564 -0
- agentloop/aggregate_cli.py +88 -0
- agentloop/aggregate_stats.py +175 -0
- agentloop/aggregates.py +532 -0
- agentloop/audit.py +39 -0
- agentloop/autoinstrument.py +145 -0
- agentloop/batch_experiments.py +295 -0
- agentloop/batching.py +95 -0
- agentloop/budget_types.py +335 -0
- agentloop/budgets.py +539 -0
- agentloop/calibration.py +872 -0
- agentloop/checkpoint_state.py +306 -0
- agentloop/checkpoint_store.py +86 -0
- agentloop/ci.py +202 -0
- agentloop/cli.py +1314 -0
- agentloop/client.py +228 -0
- agentloop/completion.py +345 -0
- agentloop/completion_types.py +199 -0
- agentloop/config.py +50 -0
- agentloop/context_controls.py +443 -0
- agentloop/context_experiments.py +337 -0
- agentloop/context_types.py +216 -0
- agentloop/costs.py +667 -0
- agentloop/cursor_validation.py +48 -0
- agentloop/decorators.py +445 -0
- agentloop/demo.py +300 -0
- agentloop/doctor.py +341 -0
- agentloop/drift.py +361 -0
- agentloop/drift_cli.py +147 -0
- agentloop/drift_types.py +110 -0
- agentloop/drift_windows.py +340 -0
- agentloop/entrypoint.py +201 -0
- agentloop/estimates.py +133 -0
- agentloop/events.py +75 -0
- agentloop/experiment_artifacts.py +80 -0
- agentloop/experiment_observations.py +169 -0
- agentloop/experiment_reports.py +545 -0
- agentloop/experiment_types.py +355 -0
- agentloop/experiments.py +392 -0
- agentloop/exporters.py +117 -0
- agentloop/finding_benchmark_types.py +131 -0
- agentloop/finding_benchmarks.py +424 -0
- agentloop/findings.py +419 -0
- agentloop/graph.py +464 -0
- agentloop/harness.py +825 -0
- agentloop/harness_bench/__init__.py +1 -0
- agentloop/harness_bench/cli.py +77 -0
- agentloop/harness_bench/drivers.py +96 -0
- agentloop/harness_bench/offline.py +507 -0
- agentloop/harness_evidence.py +585 -0
- agentloop/harness_usage.py +91 -0
- agentloop/html_report.py +860 -0
- agentloop/integrations/__init__.py +1 -0
- agentloop/integrations/crewai.py +144 -0
- agentloop/integrations/harbor/__init__.py +1 -0
- agentloop/integrations/harbor/atif.py +982 -0
- agentloop/integrations/harbor/atif_validation.py +329 -0
- agentloop/integrations/harbor/cli.py +207 -0
- agentloop/integrations/harbor/manifest.py +86 -0
- agentloop/integrations/harbor/trial_evidence.py +196 -0
- agentloop/integrations/harbor/trials.py +1109 -0
- agentloop/integrations/harbor/verifier.py +118 -0
- agentloop/integrations/langgraph.py +150 -0
- agentloop/integrations/langgraph_harness.py +466 -0
- agentloop/integrations/langgraph_recovery.py +366 -0
- agentloop/integrations/omnigent/__init__.py +1 -0
- agentloop/integrations/omnigent/cli.py +78 -0
- agentloop/integrations/omnigent/evidence.py +93 -0
- agentloop/integrations/omnigent/telemetry.py +422 -0
- agentloop/integrations/openai.py +415 -0
- agentloop/integrations/openai_agents.py +291 -0
- agentloop/integrations/sdk.py +82 -0
- agentloop/integrations/vercel_ai.py +83 -0
- agentloop/interoperability/__init__.py +5 -0
- agentloop/interoperability/artifacts.py +112 -0
- agentloop/interoperability/cli.py +59 -0
- agentloop/interoperability/cohort_evidence.py +175 -0
- agentloop/interoperability/cohorts.py +681 -0
- agentloop/interoperability/contracts.py +427 -0
- agentloop/interoperability/coordination.py +554 -0
- agentloop/interoperability/evidence.py +215 -0
- agentloop/interoperability/otlp_jsonl.py +775 -0
- agentloop/interoperability/otlp_validation.py +496 -0
- agentloop/interoperability/registry.py +561 -0
- agentloop/interoperability/validation.py +333 -0
- agentloop/intervention_service.py +110 -0
- agentloop/intervention_store.py +118 -0
- agentloop/interventions.py +190 -0
- agentloop/issues.py +90 -0
- agentloop/judgment_adapters.py +125 -0
- agentloop/judgment_benchmark_types.py +98 -0
- agentloop/judgment_benchmarks.py +606 -0
- agentloop/judgment_types.py +241 -0
- agentloop/judgment_views.py +38 -0
- agentloop/judgments.py +342 -0
- agentloop/loop_guards.py +274 -0
- agentloop/loop_types.py +66 -0
- agentloop/markdown.py +102 -0
- agentloop/metrics.py +342 -0
- agentloop/migrations.py +303 -0
- agentloop/model_routing.py +470 -0
- agentloop/onboarding.py +315 -0
- agentloop/operations.py +44 -0
- agentloop/optimizer.py +199 -0
- agentloop/otel.py +715 -0
- agentloop/otel_ids.py +81 -0
- agentloop/otel_semantics.py +134 -0
- agentloop/parallelism.py +141 -0
- agentloop/patches.py +603 -0
- agentloop/plan_export.py +126 -0
- agentloop/policy_promotion.py +325 -0
- agentloop/promotion_evidence.py +304 -0
- agentloop/promotion_types.py +209 -0
- agentloop/quality.py +446 -0
- agentloop/ranking.py +529 -0
- agentloop/recovery_evidence.py +96 -0
- agentloop/replay.py +753 -0
- agentloop/retention.py +375 -0
- agentloop/retention_types.py +113 -0
- agentloop/reuse_experiments.py +247 -0
- agentloop/routing_checks.py +116 -0
- agentloop/routing_types.py +241 -0
- agentloop/rules.py +396 -0
- agentloop/runtime.py +286 -0
- agentloop/savings.py +158 -0
- agentloop/scheduling_plan.py +99 -0
- agentloop/scheduling_types.py +169 -0
- agentloop/schema.py +257 -0
- agentloop/semantic_rules.py +200 -0
- agentloop/semantic_waste.py +345 -0
- agentloop/semantic_waste_types.py +88 -0
- agentloop/server.py +494 -0
- agentloop/store.py +1429 -0
- agentloop/structural_experiments.py +431 -0
- agentloop/structural_types.py +113 -0
- agentloop/structured_quality.py +1020 -0
- agentloop/studies.py +629 -0
- agentloop/study_statistics.py +58 -0
- agentloop/substitution_evidence.py +199 -0
- agentloop/substitution_export.py +190 -0
- agentloop/substitution_reports.py +407 -0
- agentloop/substitution_types.py +284 -0
- agentloop/substitutions.py +267 -0
- agentloop/timing.py +142 -0
- agentloop/tokens.py +188 -0
- agentloop/tool_scheduling.py +410 -0
- agentloop/trace_sources.py +31 -0
- agentloop/tracer.py +494 -0
- agentloop/value.py +306 -0
- agentloop/version.py +3 -0
- agentloop/workflow_types.py +264 -0
- agentloop/workflows.py +284 -0
- agentloop_profiler-0.1.0.dist-info/METADATA +513 -0
- agentloop_profiler-0.1.0.dist-info/RECORD +166 -0
- agentloop_profiler-0.1.0.dist-info/WHEEL +5 -0
- agentloop_profiler-0.1.0.dist-info/entry_points.txt +2 -0
- agentloop_profiler-0.1.0.dist-info/licenses/LICENSE +201 -0
- agentloop_profiler-0.1.0.dist-info/licenses/THIRD_PARTY_LICENSES.md +101 -0
- agentloop_profiler-0.1.0.dist-info/top_level.txt +2 -0
- dashboard/__init__.py +5 -0
- dashboard/app.py +922 -0
- dashboard/streamlit_compat.py +63 -0
- dashboard/trace_upload.py +57 -0
- dashboard/value_view.py +105 -0
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
"""Structural OTLP bounds and payload minimization before the existing parser."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import re
|
|
6
|
+
from copy import deepcopy
|
|
7
|
+
from hashlib import sha256
|
|
8
|
+
from typing import Any
|
|
9
|
+
|
|
10
|
+
from agentloop.interoperability.validation import ImportLimits, ImportValidationError
|
|
11
|
+
from agentloop.interventions import canonical_json
|
|
12
|
+
from agentloop.otel import _attributes
|
|
13
|
+
from agentloop.otel_semantics import INPUT_USAGE, OUTPUT_USAGE
|
|
14
|
+
|
|
15
|
+
_STRUCTURAL_STRINGS = frozenset(
|
|
16
|
+
{
|
|
17
|
+
"gen_ai.operation.name",
|
|
18
|
+
"gen_ai.provider.name",
|
|
19
|
+
"gen_ai.system",
|
|
20
|
+
"gen_ai.request.model",
|
|
21
|
+
"gen_ai.response.model",
|
|
22
|
+
"gen_ai.conversation.id",
|
|
23
|
+
"gen_ai.tool.name",
|
|
24
|
+
"gen_ai.tool.call.id",
|
|
25
|
+
"gen_ai.agent.name",
|
|
26
|
+
"openinference.span.kind",
|
|
27
|
+
"service.name",
|
|
28
|
+
"service.version",
|
|
29
|
+
"service.instance.id",
|
|
30
|
+
"telemetry.sdk.name",
|
|
31
|
+
"telemetry.sdk.version",
|
|
32
|
+
"telemetry.sdk.language",
|
|
33
|
+
"agent.name",
|
|
34
|
+
"agent.version",
|
|
35
|
+
"agent.id",
|
|
36
|
+
"agent.role",
|
|
37
|
+
"parent.agent.id",
|
|
38
|
+
"parent.agent.name",
|
|
39
|
+
"parent.session.id",
|
|
40
|
+
"parent_span_id",
|
|
41
|
+
"request.id",
|
|
42
|
+
"turn.id",
|
|
43
|
+
"harness.name",
|
|
44
|
+
"harness.type",
|
|
45
|
+
"harness.version",
|
|
46
|
+
"runtime.harness",
|
|
47
|
+
"runtime.version",
|
|
48
|
+
"omnigent.harness",
|
|
49
|
+
"omnigent.harness.version",
|
|
50
|
+
"omnigent.integration_mode",
|
|
51
|
+
"omnigent.agent.id",
|
|
52
|
+
"omnigent.agent.role",
|
|
53
|
+
"omnigent.parent_agent.id",
|
|
54
|
+
"omnigent.parent_session.id",
|
|
55
|
+
"omnigent.request.id",
|
|
56
|
+
"omnigent.turn.id",
|
|
57
|
+
"omnigent.skill.name",
|
|
58
|
+
"omnigent.span.role",
|
|
59
|
+
"policy.name",
|
|
60
|
+
"policy.phase",
|
|
61
|
+
"policy.action",
|
|
62
|
+
"policy.tool_call_id",
|
|
63
|
+
"policy.dispatch_phase",
|
|
64
|
+
"approval.id",
|
|
65
|
+
"approval.resolution",
|
|
66
|
+
"relationship",
|
|
67
|
+
"session.id",
|
|
68
|
+
"trajectory.id",
|
|
69
|
+
"atif.schema_version",
|
|
70
|
+
"llm.model_name",
|
|
71
|
+
"llm.provider",
|
|
72
|
+
"llm.response.model_name",
|
|
73
|
+
"llm.request.model_name",
|
|
74
|
+
"embedding.model_name",
|
|
75
|
+
"tool.name",
|
|
76
|
+
"tool.id",
|
|
77
|
+
"tool_call.id",
|
|
78
|
+
"mcp.method.name",
|
|
79
|
+
"mcp.session.id",
|
|
80
|
+
"rpc.system",
|
|
81
|
+
"http.request.method",
|
|
82
|
+
"http.method",
|
|
83
|
+
"error.type",
|
|
84
|
+
"exception.type",
|
|
85
|
+
"gen_ai.evaluation.name",
|
|
86
|
+
"gen_ai.evaluation.score.label",
|
|
87
|
+
"evaluation.name",
|
|
88
|
+
"evaluation.label",
|
|
89
|
+
"annotation.name",
|
|
90
|
+
"annotation.label",
|
|
91
|
+
"agentloop.event_type",
|
|
92
|
+
"agentloop.operation_kind",
|
|
93
|
+
"agentloop.model",
|
|
94
|
+
"agentloop.name",
|
|
95
|
+
"agentloop.run_id",
|
|
96
|
+
"agentloop.native_event_id",
|
|
97
|
+
"agentloop.native_parent_id",
|
|
98
|
+
"agentloop.token_provenance",
|
|
99
|
+
"agentloop.trace.name",
|
|
100
|
+
"agentloop.trace.started_at",
|
|
101
|
+
"agentloop.trace.ended_at",
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def fail(field: str, reason: str, code: str = "invalid_otlp") -> None:
|
|
107
|
+
raise ImportValidationError(code, field, reason)
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def object_value(value: Any, field: str) -> dict:
|
|
111
|
+
if not isinstance(value, dict):
|
|
112
|
+
fail(field, "must be an object")
|
|
113
|
+
return value
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def array(value: Any, field: str) -> list:
|
|
117
|
+
if not isinstance(value, list):
|
|
118
|
+
fail(field, "must be an array")
|
|
119
|
+
return value
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def wire_id(value: Any, field: str, length: int, *, missing: bool = False) -> str | None:
|
|
123
|
+
if missing and value in (None, ""):
|
|
124
|
+
return None
|
|
125
|
+
if (
|
|
126
|
+
not isinstance(value, str)
|
|
127
|
+
or not re.fullmatch(r"[0-9a-fA-F]{" + str(length) + "}", value)
|
|
128
|
+
or int(value, 16) == 0
|
|
129
|
+
):
|
|
130
|
+
fail(field, "must be a nonzero hexadecimal OTLP identifier")
|
|
131
|
+
return value.lower()
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def attributes(value: Any, field: str) -> dict:
|
|
135
|
+
"""Validate the standard list form and the existing parser's object form."""
|
|
136
|
+
if value is None:
|
|
137
|
+
return {}
|
|
138
|
+
if isinstance(value, dict):
|
|
139
|
+
if any(not key or len(key.encode()) > 512 for key in value):
|
|
140
|
+
fail(field, "attribute keys must be bounded and nonempty")
|
|
141
|
+
return _attributes({"attributes": value})
|
|
142
|
+
items = array(value, field)
|
|
143
|
+
seen = set()
|
|
144
|
+
for item in items:
|
|
145
|
+
object_value(item, field)
|
|
146
|
+
key = item.get("key")
|
|
147
|
+
if not isinstance(key, str) or not key or len(key.encode()) > 512 or key in seen:
|
|
148
|
+
fail(field, "attribute keys must be bounded and unique")
|
|
149
|
+
seen.add(key)
|
|
150
|
+
_any_value(item.get("value"), field + ".value")
|
|
151
|
+
return _attributes({"attributes": items})
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _any_value(value: Any, field: str) -> None:
|
|
155
|
+
value = object_value(value, field)
|
|
156
|
+
if not value:
|
|
157
|
+
return
|
|
158
|
+
kinds = {
|
|
159
|
+
"stringValue",
|
|
160
|
+
"boolValue",
|
|
161
|
+
"intValue",
|
|
162
|
+
"doubleValue",
|
|
163
|
+
"bytesValue",
|
|
164
|
+
"arrayValue",
|
|
165
|
+
"kvlistValue",
|
|
166
|
+
}
|
|
167
|
+
if len(value) != 1 or not set(value) <= kinds:
|
|
168
|
+
fail(field, "unsupported or ambiguous AnyValue")
|
|
169
|
+
key, item = next(iter(value.items()))
|
|
170
|
+
if key in {"stringValue", "bytesValue"} and not isinstance(item, str):
|
|
171
|
+
fail(field, "string/bytes attributes must be strings")
|
|
172
|
+
if key == "boolValue" and type(item) is not bool:
|
|
173
|
+
fail(field, "boolean attributes must be boolean")
|
|
174
|
+
if key == "doubleValue" and type(item) not in {int, float}:
|
|
175
|
+
fail(field, "numeric attributes must be numbers")
|
|
176
|
+
if key == "arrayValue":
|
|
177
|
+
for child in array(object_value(item, field).get("values", []), field):
|
|
178
|
+
_any_value(child, field)
|
|
179
|
+
if key == "kvlistValue":
|
|
180
|
+
attributes(object_value(item, field).get("values", []), field)
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
def minimized_value(value: Any, key: str, *, capture_content: bool) -> Any:
|
|
184
|
+
"""Retain structural/numeric attributes; content opt-in never captures secrets."""
|
|
185
|
+
key = key.removeprefix("agentloop.metadata.")
|
|
186
|
+
compact = key.lower().replace("_", "").replace("-", "")
|
|
187
|
+
sensitive = any(
|
|
188
|
+
word in compact
|
|
189
|
+
for word in (
|
|
190
|
+
"password",
|
|
191
|
+
"passwd",
|
|
192
|
+
"authorization",
|
|
193
|
+
"apikey",
|
|
194
|
+
"secret",
|
|
195
|
+
"credential",
|
|
196
|
+
"bearer",
|
|
197
|
+
"cookie",
|
|
198
|
+
"privatekey",
|
|
199
|
+
)
|
|
200
|
+
) or (
|
|
201
|
+
"token" in compact
|
|
202
|
+
and not any(
|
|
203
|
+
word in compact
|
|
204
|
+
for word in (
|
|
205
|
+
"tokencount",
|
|
206
|
+
"usage",
|
|
207
|
+
"inputtokens",
|
|
208
|
+
"outputtokens",
|
|
209
|
+
"prompttokens",
|
|
210
|
+
"completiontokens",
|
|
211
|
+
"cachedtokens",
|
|
212
|
+
)
|
|
213
|
+
)
|
|
214
|
+
)
|
|
215
|
+
private = sensitive or any(
|
|
216
|
+
word in compact
|
|
217
|
+
for word in (
|
|
218
|
+
"reasoningcontent",
|
|
219
|
+
"chainofthought",
|
|
220
|
+
"tokenids",
|
|
221
|
+
"logprobs",
|
|
222
|
+
"exceptionmessage",
|
|
223
|
+
"exceptionstacktrace",
|
|
224
|
+
)
|
|
225
|
+
)
|
|
226
|
+
if private or isinstance(value, str) and not (key in _STRUCTURAL_STRINGS or capture_content):
|
|
227
|
+
encoded = canonical_json(value).encode()
|
|
228
|
+
return {
|
|
229
|
+
"capture": "omitted",
|
|
230
|
+
"sha256": sha256(encoded).hexdigest(),
|
|
231
|
+
"size_bytes": len(encoded),
|
|
232
|
+
}
|
|
233
|
+
if isinstance(value, dict):
|
|
234
|
+
return {
|
|
235
|
+
name: minimized_value(item, name, capture_content=capture_content)
|
|
236
|
+
for name, item in value.items()
|
|
237
|
+
}
|
|
238
|
+
if isinstance(value, list):
|
|
239
|
+
return [minimized_value(item, key, capture_content=capture_content) for item in value]
|
|
240
|
+
return deepcopy(value)
|
|
241
|
+
|
|
242
|
+
|
|
243
|
+
def minimized_attributes(owner: dict, *, capture_content: bool) -> None:
|
|
244
|
+
raw = owner.get("attributes")
|
|
245
|
+
decoded = attributes(raw, "attributes")
|
|
246
|
+
clean = {
|
|
247
|
+
key: minimized_value(value, key, capture_content=capture_content)
|
|
248
|
+
for key, value in decoded.items()
|
|
249
|
+
}
|
|
250
|
+
# Attribute object form is already supported by the core parser and avoids
|
|
251
|
+
# reimplementing protobuf AnyValue serialization.
|
|
252
|
+
if raw is not None:
|
|
253
|
+
owner["attributes"] = clean
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def flatten(
|
|
257
|
+
payload: Any, limits: ImportLimits, *, capture_content: bool
|
|
258
|
+
) -> tuple[dict, list[dict], list[dict]]:
|
|
259
|
+
"""Return a validated/minimized payload plus source span and log witnesses."""
|
|
260
|
+
object_value(payload, "record")
|
|
261
|
+
_numeric_string_bounds(payload, limits)
|
|
262
|
+
if not ("resourceSpans" in payload or "resourceLogs" in payload or "spans" in payload):
|
|
263
|
+
fail("record", "OTLP resource spans/logs or a spans array are required")
|
|
264
|
+
clean = deepcopy(payload)
|
|
265
|
+
spans, logs = [], []
|
|
266
|
+
root_omissions = len(set(payload) - {"resourceSpans", "resourceLogs", "spans", "resource"})
|
|
267
|
+
resources = clean.get("resourceSpans", [])
|
|
268
|
+
if "spans" in clean:
|
|
269
|
+
resources = [
|
|
270
|
+
{"resource": clean.get("resource", {}), "scopeSpans": [{"spans": clean["spans"]}]}
|
|
271
|
+
]
|
|
272
|
+
clean = {"resourceSpans": resources, "resourceLogs": clean.get("resourceLogs", [])}
|
|
273
|
+
for field, scope_field, items_field, destination in (
|
|
274
|
+
("resourceSpans", "scopeSpans", "spans", spans),
|
|
275
|
+
("resourceLogs", "scopeLogs", "logRecords", logs),
|
|
276
|
+
):
|
|
277
|
+
blocks = resources if field == "resourceSpans" else clean.get(field, [])
|
|
278
|
+
for resource in array(blocks, field):
|
|
279
|
+
object_value(resource, field)
|
|
280
|
+
context = deepcopy(resource.get("resource", {}))
|
|
281
|
+
object_value(context, "resource")
|
|
282
|
+
attributes(context.get("attributes"), "resource.attributes")
|
|
283
|
+
if "resource" in resource:
|
|
284
|
+
minimized_attributes(resource["resource"], capture_content=capture_content)
|
|
285
|
+
for scope in array(resource.get(scope_field, []), scope_field):
|
|
286
|
+
object_value(scope, scope_field)
|
|
287
|
+
scope_info = object_value(scope.get("scope", {}), "scope")
|
|
288
|
+
attributes(scope_info.get("attributes"), "scope.attributes")
|
|
289
|
+
source_scope = deepcopy(scope_info)
|
|
290
|
+
minimized_attributes(scope_info, capture_content=capture_content)
|
|
291
|
+
for key in list(scope_info):
|
|
292
|
+
if key not in {"name", "version", "attributes"}:
|
|
293
|
+
scope_info[key] = minimized_value(
|
|
294
|
+
scope_info[key], key, capture_content=capture_content
|
|
295
|
+
)
|
|
296
|
+
for ordinal, item in enumerate(array(scope.get(items_field, []), items_field)):
|
|
297
|
+
object_value(item, items_field)
|
|
298
|
+
source = deepcopy(item)
|
|
299
|
+
for canonical, alias in (
|
|
300
|
+
("traceId", "trace_id"),
|
|
301
|
+
("spanId", "span_id"),
|
|
302
|
+
("parentSpanId", "parent_span_id"),
|
|
303
|
+
("startTimeUnixNano", "start_time_unix_nano"),
|
|
304
|
+
("endTimeUnixNano", "end_time_unix_nano"),
|
|
305
|
+
):
|
|
306
|
+
if alias in item:
|
|
307
|
+
if canonical in item and item[canonical] != item[alias]:
|
|
308
|
+
fail(canonical, "contradictory source field aliases")
|
|
309
|
+
item[canonical] = item[alias]
|
|
310
|
+
if items_field == "spans":
|
|
311
|
+
for field_name in ("startTimeUnixNano", "endTimeUnixNano"):
|
|
312
|
+
value = item.get(field_name)
|
|
313
|
+
if value is not None:
|
|
314
|
+
if type(value) not in {int, str}:
|
|
315
|
+
fail(
|
|
316
|
+
field_name, "timestamp must be an unsigned decimal integer"
|
|
317
|
+
)
|
|
318
|
+
try:
|
|
319
|
+
number = int(value)
|
|
320
|
+
except ValueError:
|
|
321
|
+
fail(
|
|
322
|
+
field_name, "timestamp must be an unsigned decimal integer"
|
|
323
|
+
)
|
|
324
|
+
if number < 0:
|
|
325
|
+
fail(
|
|
326
|
+
field_name, "timestamp must be an unsigned decimal integer"
|
|
327
|
+
)
|
|
328
|
+
item[field_name] = str(number)
|
|
329
|
+
trace_id = wire_id(item.get("traceId"), "traceId", 32, missing=True)
|
|
330
|
+
span_id = wire_id(item.get("spanId"), "spanId", 16, missing=True)
|
|
331
|
+
if items_field == "spans":
|
|
332
|
+
wire_id(item.get("parentSpanId"), "parentSpanId", 16, missing=True)
|
|
333
|
+
for link in array(item.get("links", []), "links"):
|
|
334
|
+
object_value(link, "links")
|
|
335
|
+
wire_id(link.get("traceId"), "links.traceId", 32)
|
|
336
|
+
wire_id(link.get("spanId"), "links.spanId", 16)
|
|
337
|
+
minimized_attributes(link, capture_content=capture_content)
|
|
338
|
+
for event in array(item.get("events", []), "events"):
|
|
339
|
+
object_value(event, "events")
|
|
340
|
+
minimized_attributes(event, capture_content=capture_content)
|
|
341
|
+
for key in list(event):
|
|
342
|
+
if key not in {
|
|
343
|
+
"name",
|
|
344
|
+
"timeUnixNano",
|
|
345
|
+
"attributes",
|
|
346
|
+
"droppedAttributesCount",
|
|
347
|
+
}:
|
|
348
|
+
event[key] = minimized_value(
|
|
349
|
+
event[key], key, capture_content=capture_content
|
|
350
|
+
)
|
|
351
|
+
if "status" in item:
|
|
352
|
+
status = object_value(item["status"], "status")
|
|
353
|
+
code = status.get("code")
|
|
354
|
+
if code is not None and (
|
|
355
|
+
type(code) not in {int, str}
|
|
356
|
+
or code
|
|
357
|
+
not in {
|
|
358
|
+
0,
|
|
359
|
+
1,
|
|
360
|
+
2,
|
|
361
|
+
"STATUS_CODE_UNSET",
|
|
362
|
+
"STATUS_CODE_OK",
|
|
363
|
+
"STATUS_CODE_ERROR",
|
|
364
|
+
}
|
|
365
|
+
):
|
|
366
|
+
fail("status.code", "unsupported status code")
|
|
367
|
+
status.pop("message", None)
|
|
368
|
+
else:
|
|
369
|
+
if "body" in item:
|
|
370
|
+
_any_value(item["body"], "body")
|
|
371
|
+
item["body"] = (
|
|
372
|
+
{"stringValue": "[omitted]"}
|
|
373
|
+
if not capture_content
|
|
374
|
+
else item["body"]
|
|
375
|
+
)
|
|
376
|
+
original_attrs = attributes(source.get("attributes"), "attributes")
|
|
377
|
+
minimized_attributes(item, capture_content=capture_content)
|
|
378
|
+
if len(canonical_json(item).encode()) > limits.max_metadata_bytes:
|
|
379
|
+
fail(
|
|
380
|
+
"span.metadata",
|
|
381
|
+
"metadata exceeds configured bounds",
|
|
382
|
+
"record_limit_exceeded",
|
|
383
|
+
)
|
|
384
|
+
destination.append(
|
|
385
|
+
{
|
|
386
|
+
"span": item,
|
|
387
|
+
"trace_id": trace_id,
|
|
388
|
+
"span_id": span_id,
|
|
389
|
+
"source_sha256": sha256(
|
|
390
|
+
canonical_json(
|
|
391
|
+
{
|
|
392
|
+
"item": source,
|
|
393
|
+
"resource": context,
|
|
394
|
+
"scope": source_scope,
|
|
395
|
+
"schema_url": scope.get(
|
|
396
|
+
"schemaUrl", resource.get("schemaUrl")
|
|
397
|
+
),
|
|
398
|
+
}
|
|
399
|
+
).encode()
|
|
400
|
+
).hexdigest(),
|
|
401
|
+
"attributes": original_attrs,
|
|
402
|
+
"resource": attributes(
|
|
403
|
+
context.get("attributes"), "resource.attributes"
|
|
404
|
+
),
|
|
405
|
+
"scope": source_scope,
|
|
406
|
+
"ordinal": ordinal,
|
|
407
|
+
"unrepresented_fields": root_omissions
|
|
408
|
+
+ len(
|
|
409
|
+
set(resource) - {"resource", "scopeSpans", "scopeLogs", "schemaUrl"}
|
|
410
|
+
)
|
|
411
|
+
+ (
|
|
412
|
+
len(
|
|
413
|
+
set(source)
|
|
414
|
+
- {
|
|
415
|
+
"traceId",
|
|
416
|
+
"spanId",
|
|
417
|
+
"parentSpanId",
|
|
418
|
+
"trace_id",
|
|
419
|
+
"span_id",
|
|
420
|
+
"parent_span_id",
|
|
421
|
+
"startTimeUnixNano",
|
|
422
|
+
"endTimeUnixNano",
|
|
423
|
+
"start_time_unix_nano",
|
|
424
|
+
"end_time_unix_nano",
|
|
425
|
+
"name",
|
|
426
|
+
"kind",
|
|
427
|
+
"traceState",
|
|
428
|
+
"flags",
|
|
429
|
+
"status",
|
|
430
|
+
"attributes",
|
|
431
|
+
"events",
|
|
432
|
+
"links",
|
|
433
|
+
"droppedAttributesCount",
|
|
434
|
+
"droppedEventsCount",
|
|
435
|
+
"droppedLinksCount",
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
if items_field == "spans"
|
|
439
|
+
else 0
|
|
440
|
+
),
|
|
441
|
+
}
|
|
442
|
+
)
|
|
443
|
+
return clean, spans, logs
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _numeric_string_bounds(payload: dict, limits: ImportLimits) -> None:
|
|
447
|
+
"""Bound decimal strings before the core parser calls int on wire values."""
|
|
448
|
+
numeric_keys = {
|
|
449
|
+
"intValue",
|
|
450
|
+
"startTimeUnixNano",
|
|
451
|
+
"endTimeUnixNano",
|
|
452
|
+
"timeUnixNano",
|
|
453
|
+
"observedTimeUnixNano",
|
|
454
|
+
"start_time_unix_nano",
|
|
455
|
+
"end_time_unix_nano",
|
|
456
|
+
"agentloop.input_tokens",
|
|
457
|
+
"agentloop.output_tokens",
|
|
458
|
+
"gen_ai.usage.cache_read.input_tokens",
|
|
459
|
+
"gen_ai.usage.cache_write.input_tokens",
|
|
460
|
+
"llm.token_count.prompt_details.cache_read",
|
|
461
|
+
"llm.token_count.prompt_details.cache_write",
|
|
462
|
+
"gen_ai.usage.reasoning.output_tokens",
|
|
463
|
+
"llm.token_count.completion_details.reasoning",
|
|
464
|
+
"llm.token_count.total",
|
|
465
|
+
*INPUT_USAGE,
|
|
466
|
+
*OUTPUT_USAGE,
|
|
467
|
+
}
|
|
468
|
+
pending = [payload]
|
|
469
|
+
while pending:
|
|
470
|
+
node = pending.pop()
|
|
471
|
+
if isinstance(node, dict):
|
|
472
|
+
for key, value in node.items():
|
|
473
|
+
if (
|
|
474
|
+
key in numeric_keys
|
|
475
|
+
and isinstance(value, str)
|
|
476
|
+
and len(value) > limits.max_number_chars
|
|
477
|
+
):
|
|
478
|
+
fail(
|
|
479
|
+
"numeric_string",
|
|
480
|
+
"numeric representation exceeds configured bounds",
|
|
481
|
+
"record_limit_exceeded",
|
|
482
|
+
)
|
|
483
|
+
if isinstance(node.get("key"), str) and node["key"] in numeric_keys:
|
|
484
|
+
encoded = node.get("value")
|
|
485
|
+
if isinstance(encoded, dict) and any(
|
|
486
|
+
isinstance(value, str) and len(value) > limits.max_number_chars
|
|
487
|
+
for value in encoded.values()
|
|
488
|
+
):
|
|
489
|
+
fail(
|
|
490
|
+
"numeric_string",
|
|
491
|
+
"numeric representation exceeds configured bounds",
|
|
492
|
+
"record_limit_exceeded",
|
|
493
|
+
)
|
|
494
|
+
pending.extend(value for value in node.values() if isinstance(value, (dict, list)))
|
|
495
|
+
elif isinstance(node, list):
|
|
496
|
+
pending.extend(value for value in node if isinstance(value, (dict, list)))
|