codegraph-engine 3.0.0__1-py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- codegraph/__init__.py +37 -0
- codegraph/agent.py +26 -0
- codegraph/agent_brain.py +2212 -0
- codegraph/agent_capabilities.py +4058 -0
- codegraph/agent_rules.py +531 -0
- codegraph/api_drift.py +455 -0
- codegraph/architecture.py +371 -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/audit.py +106 -0
- codegraph/binding_resolver.py +616 -0
- codegraph/branch_comparison.py +197 -0
- codegraph/cache.py +95 -0
- codegraph/change_impact.py +547 -0
- codegraph/cli.py +2436 -0
- codegraph/cli_output.py +205 -0
- codegraph/config.py +45 -0
- codegraph/constraints.py +238 -0
- codegraph/context.py +2182 -0
- codegraph/context_freshness.py +351 -0
- codegraph/database/__init__.py +72 -0
- codegraph/database/extractor.py +3651 -0
- codegraph/database/interrogation.py +1494 -0
- codegraph/database/models.py +310 -0
- codegraph/database/schema_drift.py +862 -0
- codegraph/epistemic.py +200 -0
- codegraph/errors.py +282 -0
- codegraph/evidence/__init__.py +15 -0
- codegraph/evidence/citations.py +418 -0
- codegraph/evidence_contract.py +486 -0
- codegraph/frameworks.py +960 -0
- codegraph/frameworks_nestjs.py +502 -0
- codegraph/frameworks_nextjs.py +337 -0
- codegraph/frameworks_react.py +457 -0
- codegraph/freshness.py +335 -0
- codegraph/frontend_html.py +109 -0
- codegraph/git.py +362 -0
- codegraph/git_state.py +545 -0
- codegraph/graph/__init__.py +46 -0
- codegraph/graph/models.py +56 -0
- codegraph/graph/traversal.py +1993 -0
- codegraph/indexing/__init__.py +24 -0
- codegraph/indexing/classifier.py +564 -0
- codegraph/indexing/indexer.py +2578 -0
- codegraph/indexing/models.py +427 -0
- codegraph/indexing/parser.py +3404 -0
- codegraph/indexing/pool.py +136 -0
- codegraph/indexing/scanner.py +211 -0
- codegraph/indexing/telemetry.py +326 -0
- codegraph/indexing/templates.py +418 -0
- codegraph/indexing/test_framework.py +116 -0
- codegraph/installer.py +1702 -0
- codegraph/interrogation.py +2355 -0
- codegraph/llm/__init__.py +3 -0
- codegraph/llm/base.py +15 -0
- codegraph/llm/context.py +20 -0
- codegraph/mcp/__init__.py +3 -0
- codegraph/mcp/server.py +1476 -0
- codegraph/mcp_diagnostics.py +882 -0
- codegraph/memory/__init__.py +3 -0
- codegraph/memory/store.py +46 -0
- codegraph/models.py +310 -0
- codegraph/monorepo.py +925 -0
- codegraph/observability.py +262 -0
- codegraph/optimizer.py +1014 -0
- codegraph/planner.py +417 -0
- codegraph/process_lifecycle.py +1153 -0
- codegraph/py.typed +1 -0
- codegraph/query_expansion.py +199 -0
- codegraph/ranking.py +408 -0
- codegraph/refactor/__init__.py +36 -0
- codegraph/refactor/models.py +114 -0
- codegraph/refactor/renamer.py +844 -0
- codegraph/refactor/transactions.py +414 -0
- codegraph/resolver.py +3968 -0
- codegraph/resources/__init__.py +46 -0
- codegraph/resources/cache.py +117 -0
- codegraph/resources/coalescer.py +83 -0
- codegraph/resources/debouncer.py +98 -0
- codegraph/resources/governor.py +219 -0
- codegraph/resources/monitor.py +102 -0
- codegraph/resources/policy.py +123 -0
- codegraph/retrieval_policy.py +320 -0
- codegraph/roblox.py +2174 -0
- codegraph/route_composer.py +363 -0
- codegraph/route_topology.py +172 -0
- codegraph/runtime/__init__.py +51 -0
- codegraph/runtime/collector.py +155 -0
- codegraph/runtime/db_watcher.py +191 -0
- codegraph/runtime/ingestor.py +692 -0
- codegraph/runtime/models.py +130 -0
- codegraph/runtime/port_inspector.py +304 -0
- codegraph/runtime/reconciliation.py +492 -0
- codegraph/runtime/runner.py +353 -0
- codegraph/runtime/trace_linker.py +175 -0
- codegraph/search/__init__.py +25 -0
- codegraph/search/hybrid.py +846 -0
- codegraph/search/semantic.py +28 -0
- codegraph/security/__init__.py +51 -0
- codegraph/security/paths.py +189 -0
- codegraph/security/redaction.py +633 -0
- codegraph/semantic_conflicts.py +270 -0
- codegraph/semantic_decorators.py +372 -0
- codegraph/structural_diff.py +542 -0
- codegraph/styles.py +144 -0
- codegraph/symbol_history.py +277 -0
- codegraph/target_resolver.py +348 -0
- codegraph/task.py +637 -0
- codegraph/tool_selection_eval.py +1158 -0
- codegraph/tsconfig.py +236 -0
- codegraph/ui/__init__.py +6 -0
- codegraph/ui/server.py +755 -0
- codegraph/watcher.py +323 -0
- codegraph_engine-3.0.0.dist-info/METADATA +859 -0
- codegraph_engine-3.0.0.dist-info/RECORD +130 -0
- codegraph_engine-3.0.0.dist-info/WHEEL +6 -0
- codegraph_engine-3.0.0.dist-info/entry_points.txt +2 -0
- codegraph_engine-3.0.0.dist-info/licenses/LICENSE +21 -0
- codegraph_engine-3.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,353 @@
|
|
|
1
|
+
"""Zero-Friction Development Server Runtime Interceptor (v2.2.0).
|
|
2
|
+
|
|
3
|
+
Enables 1-line development telemetry capture:
|
|
4
|
+
codegraph run npm run dev
|
|
5
|
+
codegraph run uvicorn main:app
|
|
6
|
+
codegraph run python app.py
|
|
7
|
+
|
|
8
|
+
Security & Reliability Invariants:
|
|
9
|
+
- Passes stdin/stdout/stderr directly to the user's terminal with zero distortion.
|
|
10
|
+
- Strips authorization headers, cookies, and sensitive credentials via existing redaction rules.
|
|
11
|
+
- Streams captured HTTP requests and uncaught exceptions directly into `.codegraph/runtime.sqlite3`
|
|
12
|
+
without requiring code changes to the user's application.
|
|
13
|
+
- Cleans up child processes and temporary interceptor hooks on exit.
|
|
14
|
+
"""
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import threading
|
|
21
|
+
import time
|
|
22
|
+
from pathlib import Path
|
|
23
|
+
|
|
24
|
+
from codegraph.indexing import Indexer
|
|
25
|
+
from codegraph.runtime.ingestor import ingest_runtime_traces
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _generate_node_interceptor(output_jsonl: Path) -> str:
|
|
29
|
+
"""Generate lightweight Node.js HTTP/Express/Fastify request interceptor."""
|
|
30
|
+
escaped_log_path = json.dumps(str(output_jsonl))
|
|
31
|
+
return f"""// CodeGraph Node.js Runtime Interceptor
|
|
32
|
+
const http = require('http');
|
|
33
|
+
const https = require('https');
|
|
34
|
+
const fs = require('fs');
|
|
35
|
+
|
|
36
|
+
const logPath = {escaped_log_path};
|
|
37
|
+
|
|
38
|
+
let _cg_buffer = [];
|
|
39
|
+
let _cg_last_flush = Date.now();
|
|
40
|
+
const _CG_BATCH_SIZE = 50;
|
|
41
|
+
const _CG_FLUSH_INTERVAL_MS = 1500;
|
|
42
|
+
|
|
43
|
+
function _cg_flush_buffer() {{
|
|
44
|
+
if (_cg_buffer.length === 0) return;
|
|
45
|
+
const content = _cg_buffer.join('');
|
|
46
|
+
_cg_buffer = [];
|
|
47
|
+
_cg_last_flush = Date.now();
|
|
48
|
+
try {{
|
|
49
|
+
fs.appendFileSync(logPath, content);
|
|
50
|
+
}} catch (_) {{}}
|
|
51
|
+
}}
|
|
52
|
+
|
|
53
|
+
function emitEvent(event) {{
|
|
54
|
+
try {{
|
|
55
|
+
_cg_buffer.push(JSON.stringify(event) + '\\n');
|
|
56
|
+
if (_cg_buffer.length >= _CG_BATCH_SIZE || (Date.now() - _cg_last_flush) >= _CG_FLUSH_INTERVAL_MS) {{
|
|
57
|
+
_cg_flush_buffer();
|
|
58
|
+
}}
|
|
59
|
+
}} catch (_) {{}}
|
|
60
|
+
}}
|
|
61
|
+
|
|
62
|
+
setInterval(_cg_flush_buffer, _CG_FLUSH_INTERVAL_MS);
|
|
63
|
+
process.on('exit', _cg_flush_buffer);
|
|
64
|
+
|
|
65
|
+
function hookServer(serverProto) {{
|
|
66
|
+
|
|
67
|
+
const origEmit = serverProto.emit;
|
|
68
|
+
serverProto.emit = function(event, req, res) {{
|
|
69
|
+
if (event === 'request' && req && res) {{
|
|
70
|
+
const startTime = Date.now();
|
|
71
|
+
const method = req.method || 'GET';
|
|
72
|
+
const url = req.url ? req.url.split('?')[0] : '/';
|
|
73
|
+
|
|
74
|
+
res.on('finish', () => {{
|
|
75
|
+
const duration = Date.now() - startTime;
|
|
76
|
+
emitEvent({{
|
|
77
|
+
trace_id: 'tr_' + Math.random().toString(36).substring(2, 10),
|
|
78
|
+
span_id: 'sp_' + Math.random().toString(36).substring(2, 10),
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
http_method: method,
|
|
81
|
+
route_path: url,
|
|
82
|
+
status_code: res.statusCode,
|
|
83
|
+
duration_ms: duration,
|
|
84
|
+
service_name: 'node_dev_server',
|
|
85
|
+
source_format: 'json'
|
|
86
|
+
}});
|
|
87
|
+
}});
|
|
88
|
+
}}
|
|
89
|
+
return origEmit.apply(this, arguments);
|
|
90
|
+
}};
|
|
91
|
+
}}
|
|
92
|
+
|
|
93
|
+
try {{
|
|
94
|
+
hookServer(http.Server.prototype);
|
|
95
|
+
}} catch (_) {{}}
|
|
96
|
+
|
|
97
|
+
process.on('uncaughtExceptionMonitor', (err) => {{
|
|
98
|
+
emitEvent({{
|
|
99
|
+
trace_id: 'err_' + Math.random().toString(36).substring(2, 10),
|
|
100
|
+
span_id: 'sp_err',
|
|
101
|
+
timestamp: new Date().toISOString(),
|
|
102
|
+
exception_type: (err && err.name) ? err.name : 'UncaughtException',
|
|
103
|
+
service_name: 'node_dev_server',
|
|
104
|
+
status_code: 500,
|
|
105
|
+
source_format: 'json'
|
|
106
|
+
}});
|
|
107
|
+
}});
|
|
108
|
+
"""
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def _generate_python_interceptor(output_jsonl: Path) -> str:
|
|
112
|
+
"""Generate lightweight Python WSGI/ASGI/FastAPI request interceptor with in-memory buffering."""
|
|
113
|
+
escaped_log_path = repr(str(output_jsonl))
|
|
114
|
+
return f"""# CodeGraph Python Runtime Interceptor
|
|
115
|
+
import atexit
|
|
116
|
+
import json
|
|
117
|
+
import os
|
|
118
|
+
import sys
|
|
119
|
+
import threading
|
|
120
|
+
import time
|
|
121
|
+
|
|
122
|
+
LOG_PATH = {escaped_log_path}
|
|
123
|
+
|
|
124
|
+
_cg_buffer = []
|
|
125
|
+
_cg_lock = threading.Lock()
|
|
126
|
+
_cg_last_flush = time.time()
|
|
127
|
+
_cg_batch_size = 50
|
|
128
|
+
_cg_flush_interval = 1.5
|
|
129
|
+
|
|
130
|
+
def _cg_flush():
|
|
131
|
+
global _cg_last_flush
|
|
132
|
+
lines = None
|
|
133
|
+
with _cg_lock:
|
|
134
|
+
if _cg_buffer:
|
|
135
|
+
lines = "".join(_cg_buffer)
|
|
136
|
+
_cg_buffer.clear()
|
|
137
|
+
_cg_last_flush = time.time()
|
|
138
|
+
if lines:
|
|
139
|
+
try:
|
|
140
|
+
with open(LOG_PATH, "a", encoding="utf-8") as f:
|
|
141
|
+
f.write(lines)
|
|
142
|
+
except Exception:
|
|
143
|
+
pass
|
|
144
|
+
|
|
145
|
+
def emit_event(event):
|
|
146
|
+
try:
|
|
147
|
+
line = json.dumps(event) + "\\n"
|
|
148
|
+
should_flush = False
|
|
149
|
+
with _cg_lock:
|
|
150
|
+
_cg_buffer.append(line)
|
|
151
|
+
if len(_cg_buffer) >= _cg_batch_size or (time.time() - _cg_last_flush) >= _cg_flush_interval:
|
|
152
|
+
should_flush = True
|
|
153
|
+
if should_flush:
|
|
154
|
+
_cg_flush()
|
|
155
|
+
except Exception:
|
|
156
|
+
pass
|
|
157
|
+
|
|
158
|
+
def _cg_bg_flusher():
|
|
159
|
+
while True:
|
|
160
|
+
time.sleep(_cg_flush_interval)
|
|
161
|
+
if _cg_buffer:
|
|
162
|
+
_cg_flush()
|
|
163
|
+
|
|
164
|
+
_cg_flusher_thread = threading.Thread(target=_cg_bg_flusher, daemon=True)
|
|
165
|
+
_cg_flusher_thread.start()
|
|
166
|
+
atexit.register(_cg_flush)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
# 1. Intercept uncaught exceptions
|
|
170
|
+
_orig_excepthook = sys.excepthook
|
|
171
|
+
def _cg_excepthook(exc_type, exc_value, exc_traceback):
|
|
172
|
+
emit_event({{
|
|
173
|
+
"trace_id": "err_" + str(int(time.time())),
|
|
174
|
+
"span_id": "sp_err",
|
|
175
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
176
|
+
"exception_type": getattr(exc_type, "__name__", "Exception"),
|
|
177
|
+
"service_name": "python_dev_server",
|
|
178
|
+
"status_code": 500,
|
|
179
|
+
"source_format": "json"
|
|
180
|
+
}})
|
|
181
|
+
return _orig_excepthook(exc_type, exc_value, exc_traceback)
|
|
182
|
+
|
|
183
|
+
sys.excepthook = _cg_excepthook
|
|
184
|
+
|
|
185
|
+
# 2. Intercept SQLite query executions
|
|
186
|
+
try:
|
|
187
|
+
import sqlite3
|
|
188
|
+
_orig_connect = sqlite3.connect
|
|
189
|
+
|
|
190
|
+
def _cg_connect(*args, **kwargs):
|
|
191
|
+
con = _orig_connect(*args, **kwargs)
|
|
192
|
+
def _sql_trace(statement):
|
|
193
|
+
if statement and isinstance(statement, str) and statement.strip():
|
|
194
|
+
emit_event({{
|
|
195
|
+
"trace_id": "tr_sql_" + str(int(time.time())),
|
|
196
|
+
"span_id": "sp_sql_" + str(id(con)),
|
|
197
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
198
|
+
"sql": statement,
|
|
199
|
+
"duration_ms": 1.0,
|
|
200
|
+
"service_name": "python_dev_server",
|
|
201
|
+
"source_format": "otel",
|
|
202
|
+
}})
|
|
203
|
+
try:
|
|
204
|
+
con.set_trace_callback(_sql_trace)
|
|
205
|
+
except Exception:
|
|
206
|
+
pass
|
|
207
|
+
return con
|
|
208
|
+
|
|
209
|
+
sqlite3.connect = _cg_connect
|
|
210
|
+
except Exception:
|
|
211
|
+
pass
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
# 3. Intercept SQLAlchemy engine query executions if imported
|
|
215
|
+
try:
|
|
216
|
+
import sqlalchemy.event
|
|
217
|
+
import sqlalchemy.engine
|
|
218
|
+
|
|
219
|
+
@sqlalchemy.event.listens_for(sqlalchemy.engine.Engine, "after_cursor_execute")
|
|
220
|
+
def _cg_after_cursor_execute(conn, cursor, statement, parameters, context, executemany):
|
|
221
|
+
if statement and str(statement).strip():
|
|
222
|
+
emit_event({{
|
|
223
|
+
"trace_id": "tr_sql_" + str(int(time.time())),
|
|
224
|
+
"span_id": "sp_sql_" + str(id(cursor)),
|
|
225
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
226
|
+
"sql": str(statement),
|
|
227
|
+
"duration_ms": 1.0,
|
|
228
|
+
"service_name": "python_dev_server",
|
|
229
|
+
"source_format": "otel",
|
|
230
|
+
}})
|
|
231
|
+
except Exception:
|
|
232
|
+
pass
|
|
233
|
+
|
|
234
|
+
# 4. Intercept Werkzeug/Flask dev server requests
|
|
235
|
+
try:
|
|
236
|
+
import werkzeug.serving
|
|
237
|
+
_orig_run_simple = werkzeug.serving.run_simple
|
|
238
|
+
|
|
239
|
+
def _cg_run_simple(hostname, port, application, *args, **kwargs):
|
|
240
|
+
def _wrapped_app(environ, start_response):
|
|
241
|
+
path = environ.get("PATH_INFO", "/")
|
|
242
|
+
method = environ.get("REQUEST_METHOD", "GET")
|
|
243
|
+
status_box = [200]
|
|
244
|
+
def _wrapped_sr(status, headers, exc_info=None):
|
|
245
|
+
try:
|
|
246
|
+
status_box[0] = int(status.split()[0])
|
|
247
|
+
except Exception:
|
|
248
|
+
pass
|
|
249
|
+
return start_response(status, headers, exc_info)
|
|
250
|
+
st = time.time()
|
|
251
|
+
try:
|
|
252
|
+
return application(environ, _wrapped_sr)
|
|
253
|
+
finally:
|
|
254
|
+
emit_event({{
|
|
255
|
+
"trace_id": "tr_http_" + str(int(time.time())),
|
|
256
|
+
"span_id": "sp_http_" + str(int(time.time())),
|
|
257
|
+
"timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
|
258
|
+
"http_method": method,
|
|
259
|
+
"route": path,
|
|
260
|
+
"status_code": status_box[0],
|
|
261
|
+
"duration_ms": (time.time() - st) * 1000.0,
|
|
262
|
+
"service_name": "python_dev_server",
|
|
263
|
+
"source_format": "otel",
|
|
264
|
+
}})
|
|
265
|
+
return _orig_run_simple(hostname, port, _wrapped_app, *args, **kwargs)
|
|
266
|
+
|
|
267
|
+
werkzeug.serving.run_simple = _cg_run_simple
|
|
268
|
+
except Exception:
|
|
269
|
+
pass
|
|
270
|
+
"""
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def run_with_telemetry(
|
|
274
|
+
command: list[str],
|
|
275
|
+
repository: Path,
|
|
276
|
+
sample_rate: float = 1.0,
|
|
277
|
+
) -> int:
|
|
278
|
+
"""Execute a development command with transparent runtime trace capture."""
|
|
279
|
+
if not command:
|
|
280
|
+
raise ValueError("Command cannot be empty.")
|
|
281
|
+
|
|
282
|
+
hooks_dir = repository / ".codegraph" / "runtime_hooks"
|
|
283
|
+
hooks_dir.mkdir(parents=True, exist_ok=True)
|
|
284
|
+
trace_file = hooks_dir / "active_traces.jsonl"
|
|
285
|
+
# Ensure fresh trace buffer
|
|
286
|
+
if trace_file.exists():
|
|
287
|
+
try:
|
|
288
|
+
trace_file.unlink()
|
|
289
|
+
except OSError:
|
|
290
|
+
pass
|
|
291
|
+
|
|
292
|
+
node_hook = hooks_dir / "cg_node_hook.js"
|
|
293
|
+
node_hook.write_text(_generate_node_interceptor(trace_file), encoding="utf-8")
|
|
294
|
+
|
|
295
|
+
python_hook = hooks_dir / "cg_python_hook.py"
|
|
296
|
+
python_hook.write_text(_generate_python_interceptor(trace_file), encoding="utf-8")
|
|
297
|
+
|
|
298
|
+
env = dict(os.environ)
|
|
299
|
+
|
|
300
|
+
# Inject Node hook
|
|
301
|
+
old_node_opts = env.get("NODE_OPTIONS", "")
|
|
302
|
+
env["NODE_OPTIONS"] = f"--require {node_hook} {old_node_opts}".strip()
|
|
303
|
+
|
|
304
|
+
# Inject Python hook
|
|
305
|
+
old_py_start = env.get("PYTHONSTARTUP", "")
|
|
306
|
+
if not old_py_start:
|
|
307
|
+
env["PYTHONSTARTUP"] = str(python_hook)
|
|
308
|
+
|
|
309
|
+
# Background ingestion thread
|
|
310
|
+
stop_event = threading.Event()
|
|
311
|
+
indexer = Indexer(repository)
|
|
312
|
+
|
|
313
|
+
def _ingest_loop() -> None:
|
|
314
|
+
last_pos = 0
|
|
315
|
+
while not stop_event.is_set():
|
|
316
|
+
time.sleep(1.0)
|
|
317
|
+
if not trace_file.exists():
|
|
318
|
+
continue
|
|
319
|
+
try:
|
|
320
|
+
size = trace_file.stat().st_size
|
|
321
|
+
if size > last_pos:
|
|
322
|
+
with open(trace_file, encoding="utf-8", errors="replace") as f:
|
|
323
|
+
f.seek(last_pos)
|
|
324
|
+
new_content = f.read()
|
|
325
|
+
last_pos = f.tell()
|
|
326
|
+
if new_content.strip():
|
|
327
|
+
with indexer.session() as con:
|
|
328
|
+
ingest_runtime_traces(con, new_content, repository=repository, max_events=1000, sampling_rate=sample_rate)
|
|
329
|
+
except Exception:
|
|
330
|
+
pass
|
|
331
|
+
|
|
332
|
+
ingest_worker = threading.Thread(target=_ingest_loop, daemon=True)
|
|
333
|
+
ingest_worker.start()
|
|
334
|
+
|
|
335
|
+
# Spawn interactive dev process
|
|
336
|
+
try:
|
|
337
|
+
proc = subprocess.Popen(command, cwd=str(repository), env=env)
|
|
338
|
+
returncode = proc.wait()
|
|
339
|
+
except KeyboardInterrupt:
|
|
340
|
+
returncode = 0
|
|
341
|
+
finally:
|
|
342
|
+
stop_event.set()
|
|
343
|
+
# Drain remaining lines on shutdown
|
|
344
|
+
if trace_file.exists():
|
|
345
|
+
try:
|
|
346
|
+
content = trace_file.read_text(encoding="utf-8", errors="replace")
|
|
347
|
+
if content.strip():
|
|
348
|
+
with indexer.session() as con:
|
|
349
|
+
ingest_runtime_traces(con, content, repository=repository, max_events=1000, sampling_rate=sample_rate)
|
|
350
|
+
except Exception:
|
|
351
|
+
pass
|
|
352
|
+
|
|
353
|
+
return returncode
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
"""Cross-Service & Multi-Localhost Distributed Trace Linker.
|
|
2
|
+
|
|
3
|
+
Stitches W3C distributed traces across multiple localhost ports:
|
|
4
|
+
- Frontend (e.g. Next.js on localhost:3000)
|
|
5
|
+
- Backend API (e.g. FastAPI / NestJS on localhost:8000)
|
|
6
|
+
- Database mutation (e.g. PostgreSQL on localhost:5432)
|
|
7
|
+
|
|
8
|
+
Attributes every step to:
|
|
9
|
+
1. Participating localhost service & port.
|
|
10
|
+
2. Static AST symbol / route handler.
|
|
11
|
+
3. Actual latency and HTTP status code.
|
|
12
|
+
"""
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import sqlite3
|
|
16
|
+
from dataclasses import dataclass
|
|
17
|
+
from pathlib import Path
|
|
18
|
+
from typing import Any
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@dataclass(frozen=True)
|
|
22
|
+
class TraceSpanNode:
|
|
23
|
+
span_id: str
|
|
24
|
+
parent_span_id: str
|
|
25
|
+
service_name: str
|
|
26
|
+
port: int | None
|
|
27
|
+
route_path: str
|
|
28
|
+
handler_symbol: str
|
|
29
|
+
db_operation: str
|
|
30
|
+
db_table: str
|
|
31
|
+
status_code: int | None
|
|
32
|
+
duration_ms: float
|
|
33
|
+
children: tuple[TraceSpanNode, ...] = ()
|
|
34
|
+
|
|
35
|
+
def as_dict(self) -> dict[str, Any]:
|
|
36
|
+
return {
|
|
37
|
+
"span_id": self.span_id,
|
|
38
|
+
"parent_span_id": self.parent_span_id,
|
|
39
|
+
"service_name": self.service_name,
|
|
40
|
+
"port": self.port,
|
|
41
|
+
"route_path": self.route_path,
|
|
42
|
+
"handler_symbol": self.handler_symbol,
|
|
43
|
+
"db_operation": self.db_operation,
|
|
44
|
+
"db_table": self.db_table,
|
|
45
|
+
"status_code": self.status_code,
|
|
46
|
+
"duration_ms": self.duration_ms,
|
|
47
|
+
"children": [c.as_dict() for c in self.children],
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@dataclass(frozen=True)
|
|
52
|
+
class DistributedTraceReport:
|
|
53
|
+
trace_id: str
|
|
54
|
+
total_duration_ms: float
|
|
55
|
+
services_involved: tuple[dict[str, Any], ...]
|
|
56
|
+
root_spans: tuple[TraceSpanNode, ...]
|
|
57
|
+
db_queries_count: int
|
|
58
|
+
has_errors: bool
|
|
59
|
+
summary: str
|
|
60
|
+
|
|
61
|
+
def as_dict(self) -> dict[str, Any]:
|
|
62
|
+
return {
|
|
63
|
+
"trace_id": self.trace_id,
|
|
64
|
+
"total_duration_ms": self.total_duration_ms,
|
|
65
|
+
"services_involved": list(self.services_involved),
|
|
66
|
+
"root_spans": [s.as_dict() for s in self.root_spans],
|
|
67
|
+
"db_queries_count": self.db_queries_count,
|
|
68
|
+
"has_errors": self.has_errors,
|
|
69
|
+
"summary": self.summary,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def link_distributed_trace(
|
|
74
|
+
con: sqlite3.Connection,
|
|
75
|
+
repo_root: Path,
|
|
76
|
+
trace_id: str,
|
|
77
|
+
) -> DistributedTraceReport | None:
|
|
78
|
+
"""Reconstruct multi-service distributed execution graph for a given trace_id."""
|
|
79
|
+
clean_tid = trace_id.strip()
|
|
80
|
+
if not clean_tid:
|
|
81
|
+
return None
|
|
82
|
+
|
|
83
|
+
rows = con.execute(
|
|
84
|
+
"""SELECT event_id, trace_id, span_id, parent_span_id, http_method, route_path,
|
|
85
|
+
handler_symbol, service_name, db_operation, db_table, status_code,
|
|
86
|
+
duration_ms
|
|
87
|
+
FROM runtime_observations
|
|
88
|
+
WHERE trace_id = ?
|
|
89
|
+
ORDER BY duration_ms DESC""",
|
|
90
|
+
(clean_tid,),
|
|
91
|
+
).fetchall()
|
|
92
|
+
|
|
93
|
+
if not rows:
|
|
94
|
+
return None
|
|
95
|
+
|
|
96
|
+
# Group spans
|
|
97
|
+
spans_by_id: dict[str, dict[str, Any]] = {}
|
|
98
|
+
children_by_parent: dict[str, list[str]] = {}
|
|
99
|
+
services_set: set[str] = set()
|
|
100
|
+
db_queries = 0
|
|
101
|
+
has_err = False
|
|
102
|
+
total_time = 0.0
|
|
103
|
+
|
|
104
|
+
for r in rows:
|
|
105
|
+
sid = str(r["span_id"] or r["event_id"])
|
|
106
|
+
pid = str(r["parent_span_id"] or "")
|
|
107
|
+
s_name = str(r["service_name"] or "unknown_service")
|
|
108
|
+
services_set.add(s_name)
|
|
109
|
+
dur = float(r["duration_ms"] or 0.0)
|
|
110
|
+
total_time = max(total_time, dur)
|
|
111
|
+
sc = r["status_code"]
|
|
112
|
+
if sc and int(sc) >= 400:
|
|
113
|
+
has_err = True
|
|
114
|
+
if r["db_operation"]:
|
|
115
|
+
db_queries += 1
|
|
116
|
+
|
|
117
|
+
spans_by_id[sid] = {
|
|
118
|
+
"span_id": sid,
|
|
119
|
+
"parent_span_id": pid,
|
|
120
|
+
"service_name": s_name,
|
|
121
|
+
"port": None,
|
|
122
|
+
"route_path": str(r["route_path"] or ""),
|
|
123
|
+
"handler_symbol": str(r["handler_symbol"] or ""),
|
|
124
|
+
"db_operation": str(r["db_operation"] or ""),
|
|
125
|
+
"db_table": str(r["db_table"] or ""),
|
|
126
|
+
"status_code": sc,
|
|
127
|
+
"duration_ms": dur,
|
|
128
|
+
}
|
|
129
|
+
if pid not in children_by_parent:
|
|
130
|
+
children_by_parent[pid] = []
|
|
131
|
+
children_by_parent[pid].append(sid)
|
|
132
|
+
|
|
133
|
+
# Build tree
|
|
134
|
+
def _build_node(sid: str) -> TraceSpanNode:
|
|
135
|
+
data = spans_by_id[sid]
|
|
136
|
+
child_sids = children_by_parent.get(sid, [])
|
|
137
|
+
children = tuple(_build_node(cid) for cid in child_sids if cid in spans_by_id)
|
|
138
|
+
return TraceSpanNode(
|
|
139
|
+
span_id=data["span_id"],
|
|
140
|
+
parent_span_id=data["parent_span_id"],
|
|
141
|
+
service_name=data["service_name"],
|
|
142
|
+
port=data["port"],
|
|
143
|
+
route_path=data["route_path"],
|
|
144
|
+
handler_symbol=data["handler_symbol"],
|
|
145
|
+
db_operation=data["db_operation"],
|
|
146
|
+
db_table=data["db_table"],
|
|
147
|
+
status_code=data["status_code"],
|
|
148
|
+
duration_ms=data["duration_ms"],
|
|
149
|
+
children=children,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
# Root spans: parent_span_id is empty or not in spans_by_id
|
|
153
|
+
roots: list[TraceSpanNode] = []
|
|
154
|
+
for sid, data in spans_by_id.items():
|
|
155
|
+
pid = data["parent_span_id"]
|
|
156
|
+
if not pid or pid not in spans_by_id:
|
|
157
|
+
roots.append(_build_node(sid))
|
|
158
|
+
|
|
159
|
+
# Resolve services
|
|
160
|
+
services_list: list[dict[str, Any]] = [{"name": s} for s in sorted(services_set)]
|
|
161
|
+
|
|
162
|
+
summary = (
|
|
163
|
+
f"Trace {clean_tid[:8]}: {len(rows)} spans across {len(services_set)} service(s), "
|
|
164
|
+
f"{db_queries} DB queries, total duration {total_time:.1f}ms."
|
|
165
|
+
)
|
|
166
|
+
|
|
167
|
+
return DistributedTraceReport(
|
|
168
|
+
trace_id=clean_tid,
|
|
169
|
+
total_duration_ms=total_time,
|
|
170
|
+
services_involved=tuple(services_list),
|
|
171
|
+
root_spans=tuple(roots),
|
|
172
|
+
db_queries_count=db_queries,
|
|
173
|
+
has_errors=has_err,
|
|
174
|
+
summary=summary,
|
|
175
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
from .hybrid import (
|
|
2
|
+
SearchResult,
|
|
3
|
+
search,
|
|
4
|
+
search_code,
|
|
5
|
+
search_exact_canonical_id,
|
|
6
|
+
search_exact_qualified,
|
|
7
|
+
search_exact_symbol,
|
|
8
|
+
search_lexical,
|
|
9
|
+
search_route,
|
|
10
|
+
)
|
|
11
|
+
from .semantic import EmbeddingProvider, SemanticSearchUnavailable, status
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"EmbeddingProvider",
|
|
15
|
+
"SearchResult",
|
|
16
|
+
"SemanticSearchUnavailable",
|
|
17
|
+
"search",
|
|
18
|
+
"search_code",
|
|
19
|
+
"search_exact_canonical_id",
|
|
20
|
+
"search_exact_qualified",
|
|
21
|
+
"search_exact_symbol",
|
|
22
|
+
"search_lexical",
|
|
23
|
+
"search_route",
|
|
24
|
+
"status",
|
|
25
|
+
]
|