codegraph-engine 2.1.2__py3-none-any.whl → 2.1.7__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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +1961 -0
- codegraph/agent_capabilities.py +3500 -0
- codegraph/agent_rules.py +530 -0
- codegraph/architecture.py +48 -5
- codegraph/binding_resolver.py +616 -0
- codegraph/cli.py +345 -5
- codegraph/config.py +2 -0
- codegraph/context.py +845 -93
- codegraph/database/__init__.py +58 -0
- codegraph/database/extractor.py +2956 -0
- codegraph/database/interrogation.py +1085 -0
- codegraph/database/models.py +310 -0
- codegraph/epistemic.py +95 -0
- codegraph/errors.py +7 -0
- codegraph/evidence/citations.py +22 -1
- codegraph/evidence_contract.py +455 -0
- codegraph/frameworks.py +372 -11
- codegraph/git.py +92 -10
- codegraph/graph/models.py +15 -0
- codegraph/graph/traversal.py +680 -121
- codegraph/indexing/__init__.py +15 -1
- codegraph/indexing/classifier.py +343 -58
- codegraph/indexing/indexer.py +1515 -141
- codegraph/indexing/models.py +75 -0
- codegraph/indexing/parser.py +1843 -70
- codegraph/indexing/scanner.py +4 -1
- codegraph/indexing/telemetry.py +326 -0
- codegraph/installer.py +1652 -0
- codegraph/interrogation.py +708 -122
- codegraph/mcp/server.py +525 -107
- codegraph/mcp_diagnostics.py +879 -0
- codegraph/models.py +14 -0
- codegraph/monorepo.py +922 -0
- codegraph/observability.py +113 -2
- codegraph/optimizer.py +647 -42
- codegraph/ranking.py +61 -16
- codegraph/resolver.py +2006 -77
- codegraph/resources/monitor.py +1 -1
- codegraph/retrieval_policy.py +96 -17
- codegraph/route_composer.py +363 -0
- codegraph/runtime/__init__.py +32 -0
- codegraph/runtime/ingestor.py +672 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/search/__init__.py +2 -0
- codegraph/search/hybrid.py +572 -53
- codegraph/security/__init__.py +50 -2
- codegraph/security/paths.py +162 -8
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_decorators.py +341 -0
- codegraph/target_resolver.py +6 -6
- codegraph/tool_selection_eval.py +1158 -0
- codegraph_engine-2.1.7.dist-info/METADATA +617 -0
- codegraph_engine-2.1.7.dist-info/RECORD +84 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/licenses/LICENSE +1 -1
- codegraph_engine-2.1.2.dist-info/METADATA +0 -336
- codegraph_engine-2.1.2.dist-info/RECORD +0 -63
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.1.2.dist-info → codegraph_engine-2.1.7.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,672 @@
|
|
|
1
|
+
"""Privacy-Safe, Bounded Runtime Ingestion Adapters (Phases 11, 15, 16).
|
|
2
|
+
|
|
3
|
+
Supported Formats:
|
|
4
|
+
1. OpenTelemetry (`otel`) — OTLP JSON (`resourceSpans` / `scopeSpans` / `spans` or flat span list)
|
|
5
|
+
2. Structured JSON Runtime Traces (`json` / `jsonl`) — explicit event objects
|
|
6
|
+
3. SQL / Query Logs (`sql_log`) — raw or structured SQL query log lines
|
|
7
|
+
|
|
8
|
+
Security & Storage Invariants:
|
|
9
|
+
- Never executes application code or connects to live databases.
|
|
10
|
+
- Strips and redacts Authorization headers, Cookies, request/response bodies, tokens, API keys,
|
|
11
|
+
passwords, connection strings, and SQL bound parameters (`VALUES (?, ?)`).
|
|
12
|
+
- Enforces `max_events`, deterministic `sampling_rate`, `retention_limit`, and compact aggregation
|
|
13
|
+
into `runtime_edges` with `observation_count`, `first_seen`, `last_seen`, and `runtime_generation`.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import hashlib
|
|
18
|
+
import json
|
|
19
|
+
import re
|
|
20
|
+
import sqlite3
|
|
21
|
+
from pathlib import Path
|
|
22
|
+
from typing import Any
|
|
23
|
+
|
|
24
|
+
from codegraph.database.extractor import parse_sql_statement
|
|
25
|
+
from codegraph.security.paths import safe_read_text
|
|
26
|
+
from codegraph.security.redaction import (
|
|
27
|
+
SENSITIVE_RUNTIME_KEYS,
|
|
28
|
+
is_secret_variable_name,
|
|
29
|
+
normalize_and_redact_sql,
|
|
30
|
+
redact_payload,
|
|
31
|
+
redact_secrets,
|
|
32
|
+
)
|
|
33
|
+
|
|
34
|
+
from .models import RuntimeEvent
|
|
35
|
+
|
|
36
|
+
DEFAULT_MAX_INGEST_EVENTS = 5_000
|
|
37
|
+
DEFAULT_RETENTION_OBSERVATIONS = 1_000
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _deterministic_sample(trace_id: str, span_id: str, sampling_rate: float) -> bool:
|
|
41
|
+
if sampling_rate >= 1.0:
|
|
42
|
+
return True
|
|
43
|
+
if sampling_rate <= 0.0:
|
|
44
|
+
return False
|
|
45
|
+
digest = hashlib.sha256(f"{trace_id}:{span_id}".encode()).hexdigest()
|
|
46
|
+
bucket = int(digest[:8], 16) / 0xFFFFFFFF
|
|
47
|
+
return bucket <= sampling_rate
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def _resolve_symbol_in_db(con: sqlite3.Connection, raw_sym: str) -> str:
|
|
51
|
+
"""Resolve a short or qualified function/method name to its indexed canonical_id if unique."""
|
|
52
|
+
if not raw_sym:
|
|
53
|
+
return ""
|
|
54
|
+
cleaned = raw_sym.strip()
|
|
55
|
+
row = con.execute(
|
|
56
|
+
"SELECT canonical_id FROM symbols WHERE canonical_id = ? OR qualified_name = ? ORDER BY canonical_id LIMIT 1",
|
|
57
|
+
(cleaned, cleaned),
|
|
58
|
+
).fetchone()
|
|
59
|
+
if row:
|
|
60
|
+
return str(row["canonical_id"])
|
|
61
|
+
rows = con.execute(
|
|
62
|
+
"SELECT canonical_id FROM symbols WHERE name = ? ORDER BY canonical_id",
|
|
63
|
+
(cleaned.split(".")[-1],),
|
|
64
|
+
).fetchall()
|
|
65
|
+
if len(rows) == 1:
|
|
66
|
+
return str(rows[0]["canonical_id"])
|
|
67
|
+
return cleaned
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _resolve_table_in_db(con: sqlite3.Connection, raw_table: str) -> str:
|
|
71
|
+
"""Resolve a table name to its indexed canonical table ID if present, else `db.UNKNOWN.UNKNOWN.<table>`."""
|
|
72
|
+
if not raw_table:
|
|
73
|
+
return ""
|
|
74
|
+
tbl = raw_table.strip().lower().split(".")[-1]
|
|
75
|
+
row = con.execute(
|
|
76
|
+
"SELECT canonical_id FROM db_entities WHERE kind = 'Table' AND table_name = ? ORDER BY canonical_id LIMIT 1",
|
|
77
|
+
(tbl,),
|
|
78
|
+
).fetchone()
|
|
79
|
+
if row:
|
|
80
|
+
return str(row["canonical_id"])
|
|
81
|
+
return f"db.UNKNOWN.UNKNOWN.{tbl}"
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_otel_traces(
|
|
85
|
+
payload: dict[str, Any] | list[Any],
|
|
86
|
+
runtime_generation: int = 1,
|
|
87
|
+
max_events: int = DEFAULT_MAX_INGEST_EVENTS,
|
|
88
|
+
sampling_rate: float = 1.0,
|
|
89
|
+
) -> list[RuntimeEvent]:
|
|
90
|
+
"""Parse OpenTelemetry JSON (`resourceSpans` or list of spans) into sanitized `RuntimeEvent`s."""
|
|
91
|
+
spans_raw: list[tuple[str, dict[str, Any]]] = []
|
|
92
|
+
if isinstance(payload, dict) and "resourceSpans" in payload:
|
|
93
|
+
for rspan in payload.get("resourceSpans", []):
|
|
94
|
+
if not isinstance(rspan, dict):
|
|
95
|
+
continue
|
|
96
|
+
svc_name = ""
|
|
97
|
+
res_attrs = rspan.get("resource", {}).get("attributes", [])
|
|
98
|
+
for attr in res_attrs if isinstance(res_attrs, list) else []:
|
|
99
|
+
if isinstance(attr, dict) and attr.get("key") == "service.name":
|
|
100
|
+
val_obj = attr.get("value", {})
|
|
101
|
+
svc_name = str(val_obj.get("stringValue", "") if isinstance(val_obj, dict) else val_obj)
|
|
102
|
+
scope_spans = rspan.get("scopeSpans") or rspan.get("instrumentationLibrarySpans") or []
|
|
103
|
+
for sspan in scope_spans if isinstance(scope_spans, list) else []:
|
|
104
|
+
if not isinstance(sspan, dict):
|
|
105
|
+
continue
|
|
106
|
+
for sp in sspan.get("spans", []):
|
|
107
|
+
if isinstance(sp, dict):
|
|
108
|
+
spans_raw.append((svc_name, sp))
|
|
109
|
+
elif isinstance(payload, list):
|
|
110
|
+
for item in payload:
|
|
111
|
+
if isinstance(item, dict):
|
|
112
|
+
spans_raw.append((str(item.get("service_name") or item.get("service") or ""), item))
|
|
113
|
+
|
|
114
|
+
events: list[RuntimeEvent] = []
|
|
115
|
+
for idx, (svc_name, sp) in enumerate(spans_raw):
|
|
116
|
+
if len(events) >= max_events:
|
|
117
|
+
break
|
|
118
|
+
trace_id = str(sp.get("traceId") or sp.get("trace_id") or f"trace_{idx}")
|
|
119
|
+
span_id = str(sp.get("spanId") or sp.get("span_id") or f"span_{idx}")
|
|
120
|
+
parent_id = str(sp.get("parentSpanId") or sp.get("parent_span_id") or "")
|
|
121
|
+
if not _deterministic_sample(trace_id, span_id, sampling_rate):
|
|
122
|
+
continue
|
|
123
|
+
|
|
124
|
+
# Parse attributes safely, dropping sensitive keys
|
|
125
|
+
attrs: dict[str, Any] = {}
|
|
126
|
+
raw_attrs = sp.get("attributes", {})
|
|
127
|
+
if isinstance(raw_attrs, list):
|
|
128
|
+
for entry in raw_attrs:
|
|
129
|
+
if isinstance(entry, dict) and "key" in entry:
|
|
130
|
+
k = str(entry["key"])
|
|
131
|
+
v_obj = entry.get("value", {})
|
|
132
|
+
if isinstance(v_obj, dict):
|
|
133
|
+
v = (
|
|
134
|
+
v_obj.get("stringValue")
|
|
135
|
+
or v_obj.get("intValue")
|
|
136
|
+
or v_obj.get("doubleValue")
|
|
137
|
+
or v_obj.get("boolValue")
|
|
138
|
+
or ""
|
|
139
|
+
)
|
|
140
|
+
else:
|
|
141
|
+
v = v_obj
|
|
142
|
+
if k.lower() not in SENSITIVE_RUNTIME_KEYS and not is_secret_variable_name(k):
|
|
143
|
+
attrs[k] = v
|
|
144
|
+
elif isinstance(raw_attrs, dict):
|
|
145
|
+
for k, v in raw_attrs.items():
|
|
146
|
+
if str(k).lower() not in SENSITIVE_RUNTIME_KEYS and not is_secret_variable_name(str(k)):
|
|
147
|
+
attrs[str(k)] = v
|
|
148
|
+
|
|
149
|
+
http_method = str(attrs.get("http.method") or attrs.get("http.request.method") or sp.get("http_method") or "").upper()
|
|
150
|
+
route_path = str(attrs.get("http.route") or attrs.get("url.path") or sp.get("route") or "")
|
|
151
|
+
code_fn = str(attrs.get("code.function") or sp.get("function") or sp.get("handler") or "")
|
|
152
|
+
code_ns = str(attrs.get("code.namespace") or "")
|
|
153
|
+
full_fn = f"{code_ns}.{code_fn}" if code_ns and code_fn and not code_fn.startswith(code_ns) else code_fn
|
|
154
|
+
db_stmt = str(attrs.get("db.statement") or sp.get("sql") or "")
|
|
155
|
+
db_op = str(attrs.get("db.operation") or sp.get("operation") or "").upper()
|
|
156
|
+
db_tbl = str(attrs.get("db.sql.table") or attrs.get("db.collection.name") or sp.get("table") or "").lower()
|
|
157
|
+
db_cols: list[str] = []
|
|
158
|
+
|
|
159
|
+
if db_stmt:
|
|
160
|
+
parsed_sql = parse_sql_statement(db_stmt)
|
|
161
|
+
if parsed_sql:
|
|
162
|
+
first_op = parsed_sql[0]
|
|
163
|
+
if not db_op:
|
|
164
|
+
db_op = str(first_op.get("operation") or "")
|
|
165
|
+
if not db_tbl and first_op.get("tables"):
|
|
166
|
+
db_tbl = str(first_op["tables"][0]).lower()
|
|
167
|
+
db_cols = [str(c) for c in first_op.get("columns", [])]
|
|
168
|
+
|
|
169
|
+
start_ns = sp.get("startTimeUnixNano")
|
|
170
|
+
end_ns = sp.get("endTimeUnixNano")
|
|
171
|
+
duration_ms = float(sp.get("duration_ms") or 0.0)
|
|
172
|
+
if not duration_ms and start_ns and end_ns:
|
|
173
|
+
try:
|
|
174
|
+
duration_ms = max(0.0, (int(end_ns) - int(start_ns)) / 1_000_000.0)
|
|
175
|
+
except (ValueError, TypeError):
|
|
176
|
+
duration_ms = 0.0
|
|
177
|
+
|
|
178
|
+
status_code_raw = attrs.get("http.status_code") or attrs.get("http.response.status_code") or sp.get("status_code")
|
|
179
|
+
status_code = int(status_code_raw) if status_code_raw is not None and str(status_code_raw).isdigit() else None
|
|
180
|
+
|
|
181
|
+
events.append(
|
|
182
|
+
RuntimeEvent(
|
|
183
|
+
event_id=f"otel:{trace_id}:{span_id}",
|
|
184
|
+
trace_id=trace_id,
|
|
185
|
+
span_id=span_id,
|
|
186
|
+
parent_span_id=parent_id,
|
|
187
|
+
timestamp=str(sp.get("timestamp") or start_ns or ""),
|
|
188
|
+
source_format="otel",
|
|
189
|
+
http_method=http_method,
|
|
190
|
+
route_path=route_path,
|
|
191
|
+
handler_symbol=full_fn if route_path else "",
|
|
192
|
+
caller_symbol= str(sp.get("caller") or full_fn),
|
|
193
|
+
callee_symbol= str(sp.get("callee") or ""),
|
|
194
|
+
service_name=svc_name or str(attrs.get("service.name") or ""),
|
|
195
|
+
db_operation=db_op,
|
|
196
|
+
db_table=db_tbl,
|
|
197
|
+
db_columns=tuple(db_cols),
|
|
198
|
+
normalized_sql=normalize_and_redact_sql(db_stmt) if db_stmt else "",
|
|
199
|
+
status_code=status_code,
|
|
200
|
+
exception_type=str(attrs.get("exception.type") or sp.get("exception") or ""),
|
|
201
|
+
duration_ms=duration_ms,
|
|
202
|
+
runtime_generation=runtime_generation,
|
|
203
|
+
metadata=redact_payload(attrs),
|
|
204
|
+
)
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
return events
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
def parse_json_runtime_events(
|
|
211
|
+
items: list[dict[str, Any]],
|
|
212
|
+
runtime_generation: int = 1,
|
|
213
|
+
max_events: int = DEFAULT_MAX_INGEST_EVENTS,
|
|
214
|
+
sampling_rate: float = 1.0,
|
|
215
|
+
) -> list[RuntimeEvent]:
|
|
216
|
+
"""Parse structured JSON runtime events into normalized, redacted `RuntimeEvent` records."""
|
|
217
|
+
events: list[RuntimeEvent] = []
|
|
218
|
+
for idx, item in enumerate(items):
|
|
219
|
+
if len(events) >= max_events:
|
|
220
|
+
break
|
|
221
|
+
if not isinstance(item, dict):
|
|
222
|
+
continue
|
|
223
|
+
trace_id = str(item.get("trace_id") or item.get("traceId") or f"trace_{idx}")
|
|
224
|
+
span_id = str(item.get("span_id") or item.get("spanId") or f"span_{idx}")
|
|
225
|
+
if not _deterministic_sample(trace_id, span_id, sampling_rate):
|
|
226
|
+
continue
|
|
227
|
+
|
|
228
|
+
raw_sql = str(item.get("sql") or item.get("query") or item.get("statement") or "")
|
|
229
|
+
op = str(item.get("operation") or item.get("db_operation") or "").upper()
|
|
230
|
+
tbl = str(item.get("table") or item.get("db_table") or "").lower()
|
|
231
|
+
cols_raw = item.get("columns") or item.get("db_columns") or []
|
|
232
|
+
cols: list[str] = [str(c).lower() for c in cols_raw] if isinstance(cols_raw, list) else []
|
|
233
|
+
|
|
234
|
+
if raw_sql:
|
|
235
|
+
parsed_sql = parse_sql_statement(raw_sql)
|
|
236
|
+
if parsed_sql:
|
|
237
|
+
first_op = parsed_sql[0]
|
|
238
|
+
if not op:
|
|
239
|
+
op = str(first_op.get("operation") or "")
|
|
240
|
+
if not tbl and first_op.get("tables"):
|
|
241
|
+
tbl = str(first_op["tables"][0]).lower()
|
|
242
|
+
if not cols:
|
|
243
|
+
cols = [str(c) for c in first_op.get("columns", [])]
|
|
244
|
+
|
|
245
|
+
route_str = str(item.get("route") or item.get("route_path") or item.get("path") or "")
|
|
246
|
+
method_str = str(item.get("http_method") or item.get("method") or "").upper()
|
|
247
|
+
if route_str and " " in route_str and not method_str:
|
|
248
|
+
parts = route_str.split(" ", 1)
|
|
249
|
+
if parts[0].upper() in ("GET", "POST", "PUT", "DELETE", "PATCH"):
|
|
250
|
+
method_str = parts[0].upper()
|
|
251
|
+
route_str = parts[1].strip()
|
|
252
|
+
|
|
253
|
+
handler = str(item.get("handler") or item.get("handler_symbol") or "")
|
|
254
|
+
caller = str(item.get("caller") or item.get("function") or item.get("source_symbol") or handler or "")
|
|
255
|
+
callee = str(item.get("callee") or item.get("target_symbol") or "")
|
|
256
|
+
service = str(item.get("service") or item.get("service_name") or "")
|
|
257
|
+
|
|
258
|
+
safe_meta: dict[str, Any] = {}
|
|
259
|
+
for k, v in item.items():
|
|
260
|
+
kl = str(k).lower()
|
|
261
|
+
if kl in (
|
|
262
|
+
"trace_id",
|
|
263
|
+
"span_id",
|
|
264
|
+
"parent_span_id",
|
|
265
|
+
"timestamp",
|
|
266
|
+
"operation",
|
|
267
|
+
"table",
|
|
268
|
+
"columns",
|
|
269
|
+
"sql",
|
|
270
|
+
"query",
|
|
271
|
+
"statement",
|
|
272
|
+
"route",
|
|
273
|
+
"route_path",
|
|
274
|
+
"http_method",
|
|
275
|
+
"method",
|
|
276
|
+
"handler",
|
|
277
|
+
"caller",
|
|
278
|
+
"callee",
|
|
279
|
+
"function",
|
|
280
|
+
"service",
|
|
281
|
+
"duration_ms",
|
|
282
|
+
"status_code",
|
|
283
|
+
"exception",
|
|
284
|
+
):
|
|
285
|
+
continue
|
|
286
|
+
if kl in SENSITIVE_RUNTIME_KEYS or is_secret_variable_name(str(k)):
|
|
287
|
+
safe_meta[str(k)] = "[REDACTED]"
|
|
288
|
+
else:
|
|
289
|
+
safe_meta[str(k)] = redact_payload(v)
|
|
290
|
+
|
|
291
|
+
status_raw = item.get("status_code") or item.get("status")
|
|
292
|
+
status_code = int(status_raw) if status_raw is not None and str(status_raw).isdigit() else None
|
|
293
|
+
|
|
294
|
+
events.append(
|
|
295
|
+
RuntimeEvent(
|
|
296
|
+
event_id=f"json:{trace_id}:{span_id}:{idx}",
|
|
297
|
+
trace_id=trace_id,
|
|
298
|
+
span_id=span_id,
|
|
299
|
+
parent_span_id=str(item.get("parent_span_id") or ""),
|
|
300
|
+
timestamp=str(item.get("timestamp") or ""),
|
|
301
|
+
source_format="json",
|
|
302
|
+
http_method=method_str,
|
|
303
|
+
route_path=route_str,
|
|
304
|
+
handler_symbol=handler,
|
|
305
|
+
caller_symbol=caller,
|
|
306
|
+
callee_symbol=callee,
|
|
307
|
+
service_name=service,
|
|
308
|
+
db_operation=op,
|
|
309
|
+
db_table=tbl,
|
|
310
|
+
db_columns=tuple(cols),
|
|
311
|
+
normalized_sql=normalize_and_redact_sql(raw_sql) if raw_sql else (f"{op} {tbl}".strip() if op and tbl else ""),
|
|
312
|
+
status_code=status_code,
|
|
313
|
+
exception_type=str(item.get("exception") or item.get("exception_type") or ""),
|
|
314
|
+
duration_ms=float(item.get("duration_ms") or 0.0),
|
|
315
|
+
runtime_generation=runtime_generation,
|
|
316
|
+
metadata=safe_meta,
|
|
317
|
+
)
|
|
318
|
+
)
|
|
319
|
+
return events
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
def parse_sql_query_logs(
|
|
323
|
+
log_text: str,
|
|
324
|
+
runtime_generation: int = 1,
|
|
325
|
+
max_events: int = DEFAULT_MAX_INGEST_EVENTS,
|
|
326
|
+
) -> list[RuntimeEvent]:
|
|
327
|
+
"""Parse SQL query log lines into normalized, parameter-redacted `RuntimeEvent` records."""
|
|
328
|
+
events: list[RuntimeEvent] = []
|
|
329
|
+
for idx, raw_line in enumerate(log_text.splitlines()):
|
|
330
|
+
if len(events) >= max_events:
|
|
331
|
+
break
|
|
332
|
+
line = raw_line.strip()
|
|
333
|
+
if not line or line.startswith("#"):
|
|
334
|
+
continue
|
|
335
|
+
|
|
336
|
+
# Extract optional structured tags like [trace_id=...] [caller=...] [duration_ms=...]
|
|
337
|
+
trace_m = re.search(r"trace_id=([A-Za-z0-9_-]+)", line)
|
|
338
|
+
caller_m = re.search(r"(?:caller|function|symbol)=([A-Za-z0-9_.:]+)", line)
|
|
339
|
+
dur_m = re.search(r"duration(?:_ms)?=([0-9.]+)", line)
|
|
340
|
+
|
|
341
|
+
trace_id = trace_m.group(1) if trace_m else f"sqllog_{idx}"
|
|
342
|
+
caller = caller_m.group(1) if caller_m else ""
|
|
343
|
+
dur = float(dur_m.group(1)) if dur_m else 0.0
|
|
344
|
+
|
|
345
|
+
parsed_ops = parse_sql_statement(line)
|
|
346
|
+
for op_idx, op_info in enumerate(parsed_ops):
|
|
347
|
+
op = str(op_info.get("operation") or "")
|
|
348
|
+
tables = op_info.get("tables", [])
|
|
349
|
+
cols = tuple(str(c) for c in op_info.get("columns", []))
|
|
350
|
+
norm_sql = str(op_info.get("normalized_sql") or "")
|
|
351
|
+
for tbl in tables:
|
|
352
|
+
events.append(
|
|
353
|
+
RuntimeEvent(
|
|
354
|
+
event_id=f"sqllog:{idx}:{op_idx}:{tbl}",
|
|
355
|
+
trace_id=trace_id,
|
|
356
|
+
span_id=f"sqlspan_{idx}_{op_idx}",
|
|
357
|
+
timestamp="",
|
|
358
|
+
source_format="sql_log",
|
|
359
|
+
caller_symbol=caller,
|
|
360
|
+
db_operation=op,
|
|
361
|
+
db_table=str(tbl).lower(),
|
|
362
|
+
db_columns=cols,
|
|
363
|
+
normalized_sql=norm_sql,
|
|
364
|
+
duration_ms=dur,
|
|
365
|
+
runtime_generation=runtime_generation,
|
|
366
|
+
)
|
|
367
|
+
)
|
|
368
|
+
return events
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def ingest_runtime_traces(
|
|
372
|
+
con: sqlite3.Connection,
|
|
373
|
+
data_or_path: str | dict[str, Any] | list[Any] | Path | None = None,
|
|
374
|
+
repository: Path | None = None,
|
|
375
|
+
format: str = "auto",
|
|
376
|
+
max_events: int = DEFAULT_MAX_INGEST_EVENTS,
|
|
377
|
+
sampling_rate: float = 1.0,
|
|
378
|
+
retention_limit: int = DEFAULT_RETENTION_OBSERVATIONS,
|
|
379
|
+
*,
|
|
380
|
+
source_path: str = "",
|
|
381
|
+
payload: str = "",
|
|
382
|
+
sample_rate: float | None = None,
|
|
383
|
+
) -> dict[str, Any]:
|
|
384
|
+
"""Ingest runtime observations from OpenTelemetry JSON, structured JSON/JSONL events, or SQL logs.
|
|
385
|
+
|
|
386
|
+
Persists bounded, deduplicated, redacted records into `runtime_observations` and `runtime_edges`.
|
|
387
|
+
"""
|
|
388
|
+
if sample_rate is not None:
|
|
389
|
+
sampling_rate = float(sample_rate)
|
|
390
|
+
if isinstance(data_or_path, Path):
|
|
391
|
+
if repository is None:
|
|
392
|
+
repository = data_or_path
|
|
393
|
+
data_or_path = payload or source_path
|
|
394
|
+
elif data_or_path is None:
|
|
395
|
+
data_or_path = payload or source_path
|
|
396
|
+
|
|
397
|
+
# Increment runtime_generation
|
|
398
|
+
curr_gen = 0
|
|
399
|
+
try:
|
|
400
|
+
row = con.execute("SELECT value FROM metadata WHERE key='runtime_generation'").fetchone()
|
|
401
|
+
if row and row[0]:
|
|
402
|
+
curr_gen = int(row[0])
|
|
403
|
+
except (sqlite3.OperationalError, ValueError):
|
|
404
|
+
pass
|
|
405
|
+
next_gen = curr_gen + 1
|
|
406
|
+
|
|
407
|
+
raw_payload: Any = data_or_path
|
|
408
|
+
if isinstance(data_or_path, str):
|
|
409
|
+
stripped = data_or_path.strip()
|
|
410
|
+
if stripped.startswith(("{", "[")):
|
|
411
|
+
try:
|
|
412
|
+
raw_payload = json.loads(stripped)
|
|
413
|
+
except json.JSONDecodeError:
|
|
414
|
+
# Could be JSONL
|
|
415
|
+
jsonl_items: list[dict[str, Any]] = []
|
|
416
|
+
for ln in stripped.splitlines():
|
|
417
|
+
ln_s = ln.strip()
|
|
418
|
+
if ln_s.startswith("{"):
|
|
419
|
+
try:
|
|
420
|
+
jsonl_items.append(json.loads(ln_s))
|
|
421
|
+
except json.JSONDecodeError:
|
|
422
|
+
pass
|
|
423
|
+
if jsonl_items:
|
|
424
|
+
raw_payload = jsonl_items
|
|
425
|
+
elif repository is not None and stripped and not stripped.upper().startswith(("SELECT ", "INSERT ", "UPDATE ", "DELETE ")):
|
|
426
|
+
# Treat as repository-relative file path validated via safe_read_text
|
|
427
|
+
_, _, file_text = safe_read_text(repository, stripped)
|
|
428
|
+
try:
|
|
429
|
+
raw_payload = json.loads(file_text)
|
|
430
|
+
except json.JSONDecodeError:
|
|
431
|
+
jsonl_list = [
|
|
432
|
+
json.loads(ln)
|
|
433
|
+
for ln in file_text.splitlines()
|
|
434
|
+
if ln.strip().startswith("{")
|
|
435
|
+
]
|
|
436
|
+
raw_payload = jsonl_list if jsonl_list else file_text
|
|
437
|
+
|
|
438
|
+
# Parse into RuntimeEvent list based on format
|
|
439
|
+
events: list[RuntimeEvent] = []
|
|
440
|
+
fmt = format.strip().lower()
|
|
441
|
+
if isinstance(raw_payload, dict):
|
|
442
|
+
if fmt in ("auto", "otel") and "resourceSpans" in raw_payload:
|
|
443
|
+
events = parse_otel_traces(raw_payload, next_gen, max_events, sampling_rate)
|
|
444
|
+
else:
|
|
445
|
+
events = parse_json_runtime_events([raw_payload], next_gen, max_events, sampling_rate)
|
|
446
|
+
elif isinstance(raw_payload, list):
|
|
447
|
+
if fmt == "otel" or (
|
|
448
|
+
fmt == "auto"
|
|
449
|
+
and raw_payload
|
|
450
|
+
and isinstance(raw_payload[0], dict)
|
|
451
|
+
and ("traceId" in raw_payload[0] or "attributes" in raw_payload[0])
|
|
452
|
+
):
|
|
453
|
+
events = parse_otel_traces(raw_payload, next_gen, max_events, sampling_rate)
|
|
454
|
+
else:
|
|
455
|
+
events = parse_json_runtime_events(raw_payload, next_gen, max_events, sampling_rate)
|
|
456
|
+
elif isinstance(raw_payload, str):
|
|
457
|
+
events = parse_sql_query_logs(raw_payload, next_gen, max_events)
|
|
458
|
+
|
|
459
|
+
edges_upserted = 0
|
|
460
|
+
spans_by_id: dict[tuple[str, str], RuntimeEvent] = {(e.trace_id, e.span_id): e for e in events}
|
|
461
|
+
|
|
462
|
+
for ev in events:
|
|
463
|
+
caller_resolved = _resolve_symbol_in_db(con, ev.caller_symbol or ev.handler_symbol or ev.service_name)
|
|
464
|
+
handler_resolved = _resolve_symbol_in_db(con, ev.handler_symbol) if ev.handler_symbol else caller_resolved
|
|
465
|
+
callee_resolved = _resolve_symbol_in_db(con, ev.callee_symbol) if ev.callee_symbol else ""
|
|
466
|
+
tbl_resolved = _resolve_table_in_db(con, ev.db_table) if ev.db_table else ""
|
|
467
|
+
|
|
468
|
+
con.execute(
|
|
469
|
+
"INSERT OR REPLACE INTO runtime_observations("
|
|
470
|
+
"event_id, trace_id, span_id, parent_span_id, timestamp, source_format, "
|
|
471
|
+
"http_method, route_path, handler_symbol, caller_symbol, callee_symbol, service_name, "
|
|
472
|
+
"db_operation, db_table, db_columns_json, normalized_sql, status_code, exception_type, "
|
|
473
|
+
"duration_ms, evidence_class, runtime_generation) "
|
|
474
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
475
|
+
(
|
|
476
|
+
ev.event_id,
|
|
477
|
+
ev.trace_id,
|
|
478
|
+
ev.span_id,
|
|
479
|
+
ev.parent_span_id,
|
|
480
|
+
ev.timestamp,
|
|
481
|
+
ev.source_format,
|
|
482
|
+
ev.http_method,
|
|
483
|
+
redact_secrets(ev.route_path),
|
|
484
|
+
handler_resolved,
|
|
485
|
+
caller_resolved,
|
|
486
|
+
callee_resolved,
|
|
487
|
+
ev.service_name,
|
|
488
|
+
ev.db_operation,
|
|
489
|
+
ev.db_table,
|
|
490
|
+
json.dumps(list(ev.db_columns)),
|
|
491
|
+
ev.normalized_sql,
|
|
492
|
+
ev.status_code,
|
|
493
|
+
redact_secrets(ev.exception_type),
|
|
494
|
+
ev.duration_ms,
|
|
495
|
+
"RUNTIME_OBSERVED",
|
|
496
|
+
next_gen,
|
|
497
|
+
),
|
|
498
|
+
)
|
|
499
|
+
|
|
500
|
+
# 1. Route -> Handler runtime edge
|
|
501
|
+
if ev.route_path and handler_resolved:
|
|
502
|
+
ep_label = f"ENDPOINT:{ev.http_method or 'ANY'}:{ev.route_path}"
|
|
503
|
+
_upsert_runtime_edge(
|
|
504
|
+
con,
|
|
505
|
+
source=ep_label,
|
|
506
|
+
target=handler_resolved,
|
|
507
|
+
relationship="HANDLED_BY",
|
|
508
|
+
operation=ev.http_method or "HTTP",
|
|
509
|
+
ev=ev,
|
|
510
|
+
next_gen=next_gen,
|
|
511
|
+
)
|
|
512
|
+
edges_upserted += 1
|
|
513
|
+
|
|
514
|
+
# 2. Parent span caller -> child span caller edge
|
|
515
|
+
if ev.parent_span_id and (ev.trace_id, ev.parent_span_id) in spans_by_id:
|
|
516
|
+
parent_ev = spans_by_id[(ev.trace_id, ev.parent_span_id)]
|
|
517
|
+
parent_sym = _resolve_symbol_in_db(con, parent_ev.caller_symbol or parent_ev.handler_symbol)
|
|
518
|
+
if parent_sym and caller_resolved and parent_sym != caller_resolved:
|
|
519
|
+
_upsert_runtime_edge(
|
|
520
|
+
con,
|
|
521
|
+
source=parent_sym,
|
|
522
|
+
target=caller_resolved,
|
|
523
|
+
relationship="CALLS",
|
|
524
|
+
operation="CALL",
|
|
525
|
+
ev=ev,
|
|
526
|
+
next_gen=next_gen,
|
|
527
|
+
)
|
|
528
|
+
edges_upserted += 1
|
|
529
|
+
|
|
530
|
+
# 3. Explicit caller -> callee runtime edge
|
|
531
|
+
if caller_resolved and callee_resolved:
|
|
532
|
+
_upsert_runtime_edge(
|
|
533
|
+
con,
|
|
534
|
+
source=caller_resolved,
|
|
535
|
+
target=callee_resolved,
|
|
536
|
+
relationship="CALLS",
|
|
537
|
+
operation="CALL",
|
|
538
|
+
ev=ev,
|
|
539
|
+
next_gen=next_gen,
|
|
540
|
+
)
|
|
541
|
+
edges_upserted += 1
|
|
542
|
+
|
|
543
|
+
# 4. Caller / Service -> Database Table & Column runtime edges
|
|
544
|
+
if ev.db_table and tbl_resolved:
|
|
545
|
+
src_actor = caller_resolved or ev.service_name or (f"ENDPOINT:{ev.http_method or 'ANY'}:{ev.route_path}" if ev.route_path else "runtime.anonymous")
|
|
546
|
+
op_upper = ev.db_operation.upper()
|
|
547
|
+
rel = "READS_TABLE" if op_upper in ("SELECT", "READ", "FIND", "GET") else "WRITES_TABLE"
|
|
548
|
+
_upsert_runtime_edge(
|
|
549
|
+
con,
|
|
550
|
+
source=src_actor,
|
|
551
|
+
target=tbl_resolved,
|
|
552
|
+
relationship=rel,
|
|
553
|
+
operation=op_upper or "QUERY",
|
|
554
|
+
ev=ev,
|
|
555
|
+
next_gen=next_gen,
|
|
556
|
+
)
|
|
557
|
+
edges_upserted += 1
|
|
558
|
+
|
|
559
|
+
col_rel = "READS_COLUMN" if rel == "READS_TABLE" else "WRITES_COLUMN"
|
|
560
|
+
for col in ev.db_columns:
|
|
561
|
+
col_cid = f"{tbl_resolved}.{col.lower()}"
|
|
562
|
+
_upsert_runtime_edge(
|
|
563
|
+
con,
|
|
564
|
+
source=src_actor,
|
|
565
|
+
target=col_cid,
|
|
566
|
+
relationship=col_rel,
|
|
567
|
+
operation=op_upper or "QUERY",
|
|
568
|
+
ev=ev,
|
|
569
|
+
next_gen=next_gen,
|
|
570
|
+
)
|
|
571
|
+
edges_upserted += 1
|
|
572
|
+
|
|
573
|
+
# Enforce retention limit on raw `runtime_observations`
|
|
574
|
+
obs_count = int(con.execute("SELECT count(*) FROM runtime_observations").fetchone()[0])
|
|
575
|
+
if obs_count > retention_limit:
|
|
576
|
+
excess = obs_count - retention_limit
|
|
577
|
+
con.execute(
|
|
578
|
+
"DELETE FROM runtime_observations WHERE event_id IN ("
|
|
579
|
+
"SELECT event_id FROM runtime_observations ORDER BY runtime_generation ASC, event_id ASC LIMIT ?"
|
|
580
|
+
")",
|
|
581
|
+
(excess,),
|
|
582
|
+
)
|
|
583
|
+
|
|
584
|
+
con.execute(
|
|
585
|
+
"INSERT INTO metadata(key, value) VALUES('runtime_generation', ?) "
|
|
586
|
+
"ON CONFLICT(key) DO UPDATE SET value=excluded.value",
|
|
587
|
+
(str(next_gen),),
|
|
588
|
+
)
|
|
589
|
+
con.commit()
|
|
590
|
+
|
|
591
|
+
return {
|
|
592
|
+
"status": "ok",
|
|
593
|
+
"ingested_events": len(events),
|
|
594
|
+
"events_ingested": len(events),
|
|
595
|
+
"edges_updated": edges_upserted,
|
|
596
|
+
"runtime_generation": next_gen,
|
|
597
|
+
"evidence_class": "RUNTIME_OBSERVED",
|
|
598
|
+
"retention_limit": retention_limit,
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
def _upsert_runtime_edge(
|
|
603
|
+
con: sqlite3.Connection,
|
|
604
|
+
source: str,
|
|
605
|
+
target: str,
|
|
606
|
+
relationship: str,
|
|
607
|
+
operation: str,
|
|
608
|
+
ev: RuntimeEvent,
|
|
609
|
+
next_gen: int,
|
|
610
|
+
) -> None:
|
|
611
|
+
edge_id = f"rtedge:{source}->{target}:{relationship}:{operation}"
|
|
612
|
+
existing = con.execute(
|
|
613
|
+
"SELECT observation_count, first_seen, avg_duration_ms, max_duration_ms "
|
|
614
|
+
"FROM runtime_edges WHERE edge_id = ?",
|
|
615
|
+
(edge_id,),
|
|
616
|
+
).fetchone()
|
|
617
|
+
|
|
618
|
+
if existing:
|
|
619
|
+
old_cnt = int(existing["observation_count"])
|
|
620
|
+
new_cnt = old_cnt + 1
|
|
621
|
+
first_seen = str(existing["first_seen"] or ev.timestamp)
|
|
622
|
+
last_seen = str(ev.timestamp or existing["first_seen"] or "")
|
|
623
|
+
old_avg = float(existing["avg_duration_ms"] or 0.0)
|
|
624
|
+
new_avg = ((old_avg * old_cnt) + ev.duration_ms) / new_cnt
|
|
625
|
+
new_max = max(float(existing["max_duration_ms"] or 0.0), ev.duration_ms)
|
|
626
|
+
con.execute(
|
|
627
|
+
"UPDATE runtime_edges SET observation_count=?, first_seen=?, last_seen=?, "
|
|
628
|
+
"avg_duration_ms=?, max_duration_ms=?, sample_trace_id=?, sample_span_id=?, "
|
|
629
|
+
"normalized_sql=?, status_code=?, exception_type=?, runtime_generation=? "
|
|
630
|
+
"WHERE edge_id=?",
|
|
631
|
+
(
|
|
632
|
+
new_cnt,
|
|
633
|
+
first_seen,
|
|
634
|
+
last_seen,
|
|
635
|
+
new_avg,
|
|
636
|
+
new_max,
|
|
637
|
+
ev.trace_id,
|
|
638
|
+
ev.span_id,
|
|
639
|
+
ev.normalized_sql,
|
|
640
|
+
ev.status_code,
|
|
641
|
+
redact_secrets(ev.exception_type),
|
|
642
|
+
next_gen,
|
|
643
|
+
edge_id,
|
|
644
|
+
),
|
|
645
|
+
)
|
|
646
|
+
else:
|
|
647
|
+
con.execute(
|
|
648
|
+
"INSERT INTO runtime_edges("
|
|
649
|
+
"edge_id, source, target, relationship, operation, evidence_class, "
|
|
650
|
+
"observation_count, first_seen, last_seen, avg_duration_ms, max_duration_ms, "
|
|
651
|
+
"sample_trace_id, sample_span_id, normalized_sql, status_code, exception_type, runtime_generation) "
|
|
652
|
+
"VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)",
|
|
653
|
+
(
|
|
654
|
+
edge_id,
|
|
655
|
+
source,
|
|
656
|
+
target,
|
|
657
|
+
relationship,
|
|
658
|
+
operation,
|
|
659
|
+
"RUNTIME_OBSERVED",
|
|
660
|
+
1,
|
|
661
|
+
ev.timestamp,
|
|
662
|
+
ev.timestamp,
|
|
663
|
+
ev.duration_ms,
|
|
664
|
+
ev.duration_ms,
|
|
665
|
+
ev.trace_id,
|
|
666
|
+
ev.span_id,
|
|
667
|
+
ev.normalized_sql,
|
|
668
|
+
ev.status_code,
|
|
669
|
+
redact_secrets(ev.exception_type),
|
|
670
|
+
next_gen,
|
|
671
|
+
),
|
|
672
|
+
)
|