codegraph-engine 2.3.0__py3-none-any.whl → 3.0.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.
- codegraph/__init__.py +1 -1
- codegraph/agent_brain.py +172 -2
- codegraph/agent_capabilities.py +320 -3
- codegraph/agent_rules.py +1 -1
- codegraph/api_drift.py +455 -0
- codegraph/async_queue/__init__.py +50 -0
- codegraph/async_queue/detectors/__init__.py +48 -0
- codegraph/async_queue/detectors/celery.py +160 -0
- codegraph/async_queue/detectors/kafka.py +168 -0
- codegraph/async_queue/detectors/rabbitmq.py +116 -0
- codegraph/async_queue/detectors/redis.py +130 -0
- codegraph/async_queue/detectors/sqs.py +135 -0
- codegraph/async_queue/models.py +161 -0
- codegraph/async_queue/otel_models.py +74 -0
- codegraph/async_queue/otel_parser.py +337 -0
- codegraph/async_queue/reconciler.py +427 -0
- codegraph/async_queue/scanner.py +291 -0
- codegraph/async_queue/traversal.py +447 -0
- codegraph/change_impact.py +320 -110
- codegraph/cli.py +776 -25
- codegraph/cli_output.py +17 -2
- codegraph/context.py +38 -11
- codegraph/context_freshness.py +2 -0
- codegraph/database/__init__.py +14 -0
- codegraph/database/extractor.py +712 -17
- codegraph/database/interrogation.py +388 -2
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +7 -0
- codegraph/evidence_contract.py +21 -0
- codegraph/frameworks.py +171 -6
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +2 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +2 -0
- codegraph/git_state.py +2 -0
- codegraph/indexing/__init__.py +7 -1
- codegraph/indexing/indexer.py +291 -38
- codegraph/indexing/models.py +6 -0
- codegraph/indexing/parser.py +239 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +6 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/interrogation.py +128 -20
- codegraph/mcp/server.py +190 -28
- codegraph/models.py +7 -0
- codegraph/monorepo.py +3 -0
- codegraph/optimizer.py +42 -5
- codegraph/process_lifecycle.py +31 -13
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +403 -0
- codegraph/resolver.py +1202 -159
- codegraph/retrieval_policy.py +6 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +19 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +30 -10
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/runner.py +155 -6
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/hybrid.py +26 -0
- codegraph/semantic_decorators.py +34 -3
- codegraph/structural_diff.py +2 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +2 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +715 -0
- codegraph/watcher.py +323 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/METADATA +150 -38
- codegraph_engine-3.0.0.dist-info/RECORD +129 -0
- codegraph_engine-2.3.0.dist-info/RECORD +0 -94
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/WHEEL +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/entry_points.txt +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/licenses/LICENSE +0 -0
- {codegraph_engine-2.3.0.dist-info → codegraph_engine-3.0.0.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
"""OpenTelemetry span parser, adapter layer, and privacy sanitizer.
|
|
2
|
+
|
|
3
|
+
Guarantees:
|
|
4
|
+
1. Adapters for both standard OTLP JSON (resourceSpans) and flat span lists.
|
|
5
|
+
2. Privacy: Strips all message bodies, payloads, bearer tokens, cookies, and secrets.
|
|
6
|
+
3. Hashing: Salted SHA-256 for correlation/message IDs without saving raw identifiers.
|
|
7
|
+
"""
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
import hashlib
|
|
11
|
+
import json
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
from codegraph.async_queue.otel_models import OTelSpanRecord
|
|
15
|
+
|
|
16
|
+
_SENSITIVE_KEY_SUBSTRINGS = (
|
|
17
|
+
"auth",
|
|
18
|
+
"token",
|
|
19
|
+
"cookie",
|
|
20
|
+
"secret",
|
|
21
|
+
"password",
|
|
22
|
+
"credential",
|
|
23
|
+
"payload",
|
|
24
|
+
"body",
|
|
25
|
+
"message_body",
|
|
26
|
+
"raw_message",
|
|
27
|
+
"data",
|
|
28
|
+
)
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def hash_correlation_id(raw_id: str | int) -> str:
|
|
32
|
+
"""Generate a deterministic, salted SHA-256 hash for correlation identifiers."""
|
|
33
|
+
clean = str(raw_id).strip()
|
|
34
|
+
if not clean:
|
|
35
|
+
return ""
|
|
36
|
+
digest = hashlib.sha256(f"cg_corr_salt_v1:{clean}".encode()).hexdigest()
|
|
37
|
+
return digest[:16]
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def sanitize_attributes(raw_attrs: dict[str, Any]) -> dict[str, Any]:
|
|
41
|
+
"""Sanitize attributes dictionary, removing any payload bodies, headers, or secrets."""
|
|
42
|
+
sanitized: dict[str, Any] = {}
|
|
43
|
+
for k, v in raw_attrs.items():
|
|
44
|
+
k_lower = k.lower()
|
|
45
|
+
if any(bad in k_lower for bad in _SENSITIVE_KEY_SUBSTRINGS):
|
|
46
|
+
continue
|
|
47
|
+
# Preserve primitive values only
|
|
48
|
+
if isinstance(v, (int, float, bool)):
|
|
49
|
+
sanitized[k] = v
|
|
50
|
+
elif isinstance(v, str):
|
|
51
|
+
if len(v) > 256:
|
|
52
|
+
v = v[:256] + "...[truncated]"
|
|
53
|
+
sanitized[k] = v
|
|
54
|
+
elif isinstance(v, (list, tuple)):
|
|
55
|
+
if len(v) <= 10 and all(isinstance(x, (str, int, float, bool)) for x in v):
|
|
56
|
+
sanitized[k] = list(v)
|
|
57
|
+
return sanitized
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def _extract_otlp_value(val_obj: Any) -> Any:
|
|
61
|
+
"""Unpack OTLP AnyValue object (e.g. {'stringValue': 'kafka'})."""
|
|
62
|
+
if not isinstance(val_obj, dict):
|
|
63
|
+
return val_obj
|
|
64
|
+
if "stringValue" in val_obj:
|
|
65
|
+
return val_obj["stringValue"]
|
|
66
|
+
if "intValue" in val_obj:
|
|
67
|
+
try:
|
|
68
|
+
return int(val_obj["intValue"])
|
|
69
|
+
except (ValueError, TypeError):
|
|
70
|
+
return 0
|
|
71
|
+
if "doubleValue" in val_obj:
|
|
72
|
+
try:
|
|
73
|
+
return float(val_obj["doubleValue"])
|
|
74
|
+
except (ValueError, TypeError):
|
|
75
|
+
return 0.0
|
|
76
|
+
if "boolValue" in val_obj:
|
|
77
|
+
return bool(val_obj["boolValue"])
|
|
78
|
+
if "arrayValue" in val_obj and "values" in val_obj["arrayValue"]:
|
|
79
|
+
return [_extract_otlp_value(x) for x in val_obj["arrayValue"]["values"]]
|
|
80
|
+
return ""
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
def _unpack_otlp_key_values(kv_list: list[dict[str, Any]]) -> dict[str, Any]:
|
|
84
|
+
"""Convert an OTLP KeyValue list [{'key': 'k', 'value': {...}}] to a dict."""
|
|
85
|
+
out: dict[str, Any] = {}
|
|
86
|
+
for item in kv_list:
|
|
87
|
+
if isinstance(item, dict) and "key" in item:
|
|
88
|
+
out[str(item["key"])] = _extract_otlp_value(item.get("value"))
|
|
89
|
+
return out
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
class OTLPJsonAdapter:
|
|
93
|
+
"""Adapter for standard OpenTelemetry OTLP JSON (resourceSpans)."""
|
|
94
|
+
|
|
95
|
+
@classmethod
|
|
96
|
+
def can_handle(cls, data: dict[str, Any]) -> bool:
|
|
97
|
+
return "resourceSpans" in data
|
|
98
|
+
|
|
99
|
+
@classmethod
|
|
100
|
+
def parse(cls, data: dict[str, Any]) -> list[OTelSpanRecord]:
|
|
101
|
+
records: list[OTelSpanRecord] = []
|
|
102
|
+
resource_spans = data.get("resourceSpans", [])
|
|
103
|
+
if not isinstance(resource_spans, list):
|
|
104
|
+
return records
|
|
105
|
+
|
|
106
|
+
for rs in resource_spans:
|
|
107
|
+
service_name = ""
|
|
108
|
+
if isinstance(rs, dict):
|
|
109
|
+
res = rs.get("resource", {})
|
|
110
|
+
if isinstance(res, dict):
|
|
111
|
+
res_attrs = res.get("attributes", [])
|
|
112
|
+
if isinstance(res_attrs, list):
|
|
113
|
+
attrs_map = _unpack_otlp_key_values(res_attrs)
|
|
114
|
+
service_name = str(attrs_map.get("service.name", ""))
|
|
115
|
+
|
|
116
|
+
scope_spans = rs.get("scopeSpans", [])
|
|
117
|
+
if isinstance(scope_spans, list):
|
|
118
|
+
for ss in scope_spans:
|
|
119
|
+
if isinstance(ss, dict):
|
|
120
|
+
spans = ss.get("spans", [])
|
|
121
|
+
if isinstance(spans, list):
|
|
122
|
+
for s in spans:
|
|
123
|
+
rec = cls._parse_span(s, service_name)
|
|
124
|
+
if rec:
|
|
125
|
+
records.append(rec)
|
|
126
|
+
return records
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def _parse_span(cls, s: dict[str, Any], default_service: str) -> OTelSpanRecord | None:
|
|
130
|
+
trace_id = str(s.get("traceId", "")).strip()
|
|
131
|
+
span_id = str(s.get("spanId", "")).strip()
|
|
132
|
+
if not trace_id or not span_id:
|
|
133
|
+
return None
|
|
134
|
+
|
|
135
|
+
parent_span_id = str(s.get("parentSpanId", "")).strip()
|
|
136
|
+
name = str(s.get("name", "")).strip()
|
|
137
|
+
kind_raw = s.get("kind", 0)
|
|
138
|
+
|
|
139
|
+
# Map OTLP SpanKind int or str
|
|
140
|
+
kind_map = {
|
|
141
|
+
1: "INTERNAL",
|
|
142
|
+
2: "SERVER",
|
|
143
|
+
3: "CLIENT",
|
|
144
|
+
4: "PRODUCER",
|
|
145
|
+
5: "CONSUMER",
|
|
146
|
+
"SPAN_KIND_INTERNAL": "INTERNAL",
|
|
147
|
+
"SPAN_KIND_SERVER": "SERVER",
|
|
148
|
+
"SPAN_KIND_CLIENT": "CLIENT",
|
|
149
|
+
"SPAN_KIND_PRODUCER": "PRODUCER",
|
|
150
|
+
"SPAN_KIND_CONSUMER": "CONSUMER",
|
|
151
|
+
}
|
|
152
|
+
kind = kind_map.get(kind_raw, str(kind_raw).replace("SPAN_KIND_", ""))
|
|
153
|
+
|
|
154
|
+
start_nano = int(s.get("startTimeUnixNano", 0) or 0)
|
|
155
|
+
end_nano = int(s.get("endTimeUnixNano", 0) or 0)
|
|
156
|
+
duration_ms = (end_nano - start_nano) / 1_000_000.0 if end_nano > start_nano else 0.0
|
|
157
|
+
|
|
158
|
+
# Status
|
|
159
|
+
status_code = "OK"
|
|
160
|
+
status_dict = s.get("status", {})
|
|
161
|
+
if isinstance(status_dict, dict):
|
|
162
|
+
raw_code = str(status_dict.get("code", "")).upper()
|
|
163
|
+
if "ERROR" in raw_code or raw_code == "2":
|
|
164
|
+
status_code = "ERROR"
|
|
165
|
+
|
|
166
|
+
# Attributes
|
|
167
|
+
raw_attrs: dict[str, Any] = {}
|
|
168
|
+
if isinstance(s.get("attributes"), list):
|
|
169
|
+
raw_attrs = _unpack_otlp_key_values(s["attributes"])
|
|
170
|
+
elif isinstance(s.get("attributes"), dict):
|
|
171
|
+
raw_attrs = dict(s["attributes"])
|
|
172
|
+
|
|
173
|
+
service_name = str(raw_attrs.get("service.name") or default_service)
|
|
174
|
+
|
|
175
|
+
# Semantic conventions for messaging
|
|
176
|
+
msg_system = str(raw_attrs.get("messaging.system", "")).lower()
|
|
177
|
+
msg_dest = str(raw_attrs.get("messaging.destination", ""))
|
|
178
|
+
msg_op = str(raw_attrs.get("messaging.operation", "")).lower()
|
|
179
|
+
|
|
180
|
+
# If operation wasn't explicitly set, infer from span kind
|
|
181
|
+
if not msg_op:
|
|
182
|
+
if kind == "PRODUCER":
|
|
183
|
+
msg_op = "publish"
|
|
184
|
+
elif kind == "CONSUMER":
|
|
185
|
+
msg_op = "process"
|
|
186
|
+
|
|
187
|
+
# Correlation ID hashing
|
|
188
|
+
corr_id = raw_attrs.get("messaging.message_id") or raw_attrs.get("messaging.conversation_id") or raw_attrs.get("correlation_id") or ""
|
|
189
|
+
corr_hash = hash_correlation_id(corr_id) if corr_id else ""
|
|
190
|
+
|
|
191
|
+
# Span links
|
|
192
|
+
links_list: list[tuple[str, str]] = []
|
|
193
|
+
raw_links = s.get("links", [])
|
|
194
|
+
if isinstance(raw_links, list):
|
|
195
|
+
for lk in raw_links:
|
|
196
|
+
if isinstance(lk, dict):
|
|
197
|
+
l_trace = str(lk.get("traceId", "")).strip()
|
|
198
|
+
l_span = str(lk.get("spanId", "")).strip()
|
|
199
|
+
if l_trace and l_span:
|
|
200
|
+
links_list.append((l_trace, l_span))
|
|
201
|
+
|
|
202
|
+
sanitized = sanitize_attributes(raw_attrs)
|
|
203
|
+
|
|
204
|
+
return OTelSpanRecord(
|
|
205
|
+
trace_id=trace_id,
|
|
206
|
+
span_id=span_id,
|
|
207
|
+
parent_span_id=parent_span_id,
|
|
208
|
+
service_name=service_name,
|
|
209
|
+
name=name,
|
|
210
|
+
kind=kind,
|
|
211
|
+
start_time_unix_nano=start_nano,
|
|
212
|
+
end_time_unix_nano=end_nano,
|
|
213
|
+
duration_ms=duration_ms,
|
|
214
|
+
status_code=status_code,
|
|
215
|
+
messaging_system=msg_system,
|
|
216
|
+
messaging_destination=msg_dest,
|
|
217
|
+
messaging_operation=msg_op,
|
|
218
|
+
correlation_id_hash=corr_hash,
|
|
219
|
+
attributes=sanitized,
|
|
220
|
+
links=tuple(links_list),
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class GenericSpanListAdapter:
|
|
225
|
+
"""Adapter for flat list of spans (Jaeger, custom JSON, or SDK exports)."""
|
|
226
|
+
|
|
227
|
+
@classmethod
|
|
228
|
+
def can_handle(cls, data: Any) -> bool:
|
|
229
|
+
if isinstance(data, list):
|
|
230
|
+
return True
|
|
231
|
+
if isinstance(data, dict) and "spans" in data:
|
|
232
|
+
return True
|
|
233
|
+
return False
|
|
234
|
+
|
|
235
|
+
@classmethod
|
|
236
|
+
def parse(cls, data: Any) -> list[OTelSpanRecord]:
|
|
237
|
+
span_items = data if isinstance(data, list) else data.get("spans", [])
|
|
238
|
+
records: list[OTelSpanRecord] = []
|
|
239
|
+
if not isinstance(span_items, list):
|
|
240
|
+
return records
|
|
241
|
+
|
|
242
|
+
for s in span_items:
|
|
243
|
+
if not isinstance(s, dict):
|
|
244
|
+
continue
|
|
245
|
+
trace_id = str(s.get("trace_id") or s.get("traceId") or "").strip()
|
|
246
|
+
span_id = str(s.get("span_id") or s.get("spanId") or "").strip()
|
|
247
|
+
if not trace_id or not span_id:
|
|
248
|
+
continue
|
|
249
|
+
|
|
250
|
+
parent_span_id = str(s.get("parent_span_id") or s.get("parentSpanId") or "").strip()
|
|
251
|
+
name = str(s.get("name") or s.get("operation_name") or "").strip()
|
|
252
|
+
kind = str(s.get("kind") or "INTERNAL").upper()
|
|
253
|
+
service_name = str(s.get("service_name") or s.get("service") or "")
|
|
254
|
+
|
|
255
|
+
duration_ms = float(s.get("duration_ms") or 0.0)
|
|
256
|
+
if not duration_ms and "duration_us" in s:
|
|
257
|
+
duration_ms = float(s["duration_us"]) / 1000.0
|
|
258
|
+
|
|
259
|
+
status_code = str(s.get("status") or s.get("status_code") or "OK").upper()
|
|
260
|
+
if "ERR" in status_code:
|
|
261
|
+
status_code = "ERROR"
|
|
262
|
+
|
|
263
|
+
raw_attrs = dict(s.get("attributes") or s.get("tags") or {})
|
|
264
|
+
if not service_name and "service.name" in raw_attrs:
|
|
265
|
+
service_name = str(raw_attrs["service.name"])
|
|
266
|
+
|
|
267
|
+
msg_system = str(raw_attrs.get("messaging.system", "")).lower()
|
|
268
|
+
msg_dest = str(raw_attrs.get("messaging.destination", ""))
|
|
269
|
+
msg_op = str(raw_attrs.get("messaging.operation", "")).lower()
|
|
270
|
+
|
|
271
|
+
if not msg_op:
|
|
272
|
+
if "PROD" in kind:
|
|
273
|
+
msg_op = "publish"
|
|
274
|
+
elif "CONS" in kind:
|
|
275
|
+
msg_op = "process"
|
|
276
|
+
|
|
277
|
+
corr_id = raw_attrs.get("messaging.message_id") or raw_attrs.get("correlation_id") or ""
|
|
278
|
+
corr_hash = hash_correlation_id(corr_id) if corr_id else ""
|
|
279
|
+
|
|
280
|
+
links_list: list[tuple[str, str]] = []
|
|
281
|
+
for lk in s.get("links", []):
|
|
282
|
+
if isinstance(lk, dict):
|
|
283
|
+
l_trace = str(lk.get("trace_id") or lk.get("traceId") or "").strip()
|
|
284
|
+
l_span = str(lk.get("span_id") or lk.get("spanId") or "").strip()
|
|
285
|
+
if l_trace and l_span:
|
|
286
|
+
links_list.append((l_trace, l_span))
|
|
287
|
+
|
|
288
|
+
sanitized = sanitize_attributes(raw_attrs)
|
|
289
|
+
|
|
290
|
+
records.append(
|
|
291
|
+
OTelSpanRecord(
|
|
292
|
+
trace_id=trace_id,
|
|
293
|
+
span_id=span_id,
|
|
294
|
+
parent_span_id=parent_span_id,
|
|
295
|
+
service_name=service_name,
|
|
296
|
+
name=name,
|
|
297
|
+
kind=kind,
|
|
298
|
+
start_time_unix_nano=int(s.get("start_time_unix_nano", 0) or 0),
|
|
299
|
+
end_time_unix_nano=int(s.get("end_time_unix_nano", 0) or 0),
|
|
300
|
+
duration_ms=duration_ms,
|
|
301
|
+
status_code=status_code,
|
|
302
|
+
messaging_system=msg_system,
|
|
303
|
+
messaging_destination=msg_dest,
|
|
304
|
+
messaging_operation=msg_op,
|
|
305
|
+
correlation_id_hash=corr_hash,
|
|
306
|
+
attributes=sanitized,
|
|
307
|
+
links=tuple(links_list),
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
return records
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def parse_otel_spans(data_or_text: Any) -> list[OTelSpanRecord]:
|
|
314
|
+
"""Parse raw OpenTelemetry JSON string or dictionary/list into canonical records."""
|
|
315
|
+
if isinstance(data_or_text, str):
|
|
316
|
+
try:
|
|
317
|
+
data = json.loads(data_or_text)
|
|
318
|
+
except Exception:
|
|
319
|
+
return []
|
|
320
|
+
else:
|
|
321
|
+
data = data_or_text
|
|
322
|
+
|
|
323
|
+
if isinstance(data, dict) and OTLPJsonAdapter.can_handle(data):
|
|
324
|
+
raw_records = OTLPJsonAdapter.parse(data)
|
|
325
|
+
elif GenericSpanListAdapter.can_handle(data):
|
|
326
|
+
raw_records = GenericSpanListAdapter.parse(data)
|
|
327
|
+
else:
|
|
328
|
+
raw_records = []
|
|
329
|
+
|
|
330
|
+
# Deduplicate within batch by composite_id
|
|
331
|
+
seen: set[str] = set()
|
|
332
|
+
deduped: list[OTelSpanRecord] = []
|
|
333
|
+
for r in raw_records:
|
|
334
|
+
if r.composite_id not in seen:
|
|
335
|
+
seen.add(r.composite_id)
|
|
336
|
+
deduped.append(r)
|
|
337
|
+
return deduped
|