devsquad 3.6.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.
- devsquad-3.6.0.dist-info/METADATA +944 -0
- devsquad-3.6.0.dist-info/RECORD +95 -0
- devsquad-3.6.0.dist-info/WHEEL +5 -0
- devsquad-3.6.0.dist-info/entry_points.txt +2 -0
- devsquad-3.6.0.dist-info/licenses/LICENSE +21 -0
- devsquad-3.6.0.dist-info/top_level.txt +2 -0
- scripts/__init__.py +0 -0
- scripts/ai_semantic_matcher.py +512 -0
- scripts/alert_manager.py +505 -0
- scripts/api/__init__.py +43 -0
- scripts/api/models.py +386 -0
- scripts/api/routes/__init__.py +20 -0
- scripts/api/routes/dispatch.py +348 -0
- scripts/api/routes/lifecycle.py +330 -0
- scripts/api/routes/metrics_gates.py +347 -0
- scripts/api_server.py +318 -0
- scripts/auth.py +451 -0
- scripts/cli/__init__.py +1 -0
- scripts/cli/cli_visual.py +642 -0
- scripts/cli.py +1094 -0
- scripts/collaboration/__init__.py +212 -0
- scripts/collaboration/_version.py +1 -0
- scripts/collaboration/agent_briefing.py +656 -0
- scripts/collaboration/ai_semantic_matcher.py +260 -0
- scripts/collaboration/anchor_checker.py +281 -0
- scripts/collaboration/anti_rationalization.py +470 -0
- scripts/collaboration/async_integration_example.py +255 -0
- scripts/collaboration/batch_scheduler.py +149 -0
- scripts/collaboration/checkpoint_manager.py +561 -0
- scripts/collaboration/ci_feedback_adapter.py +351 -0
- scripts/collaboration/code_map_generator.py +247 -0
- scripts/collaboration/concern_pack_loader.py +352 -0
- scripts/collaboration/confidence_score.py +496 -0
- scripts/collaboration/config_loader.py +188 -0
- scripts/collaboration/consensus.py +244 -0
- scripts/collaboration/context_compressor.py +533 -0
- scripts/collaboration/coordinator.py +668 -0
- scripts/collaboration/dispatcher.py +1636 -0
- scripts/collaboration/dual_layer_context.py +128 -0
- scripts/collaboration/enhanced_worker.py +539 -0
- scripts/collaboration/feature_usage_tracker.py +206 -0
- scripts/collaboration/five_axis_consensus.py +334 -0
- scripts/collaboration/input_validator.py +401 -0
- scripts/collaboration/integration_example.py +287 -0
- scripts/collaboration/intent_workflow_mapper.py +350 -0
- scripts/collaboration/language_parsers.py +269 -0
- scripts/collaboration/lifecycle_protocol.py +1446 -0
- scripts/collaboration/llm_backend.py +453 -0
- scripts/collaboration/llm_cache.py +448 -0
- scripts/collaboration/llm_cache_async.py +347 -0
- scripts/collaboration/llm_retry.py +387 -0
- scripts/collaboration/llm_retry_async.py +389 -0
- scripts/collaboration/mce_adapter.py +597 -0
- scripts/collaboration/memory_bridge.py +1607 -0
- scripts/collaboration/models.py +537 -0
- scripts/collaboration/null_providers.py +297 -0
- scripts/collaboration/operation_classifier.py +289 -0
- scripts/collaboration/output_slicer.py +225 -0
- scripts/collaboration/performance_monitor.py +462 -0
- scripts/collaboration/permission_guard.py +865 -0
- scripts/collaboration/prompt_assembler.py +756 -0
- scripts/collaboration/prompt_variant_generator.py +483 -0
- scripts/collaboration/protocols.py +267 -0
- scripts/collaboration/report_formatter.py +352 -0
- scripts/collaboration/retrospective.py +279 -0
- scripts/collaboration/role_matcher.py +92 -0
- scripts/collaboration/role_template_market.py +352 -0
- scripts/collaboration/rule_collector.py +678 -0
- scripts/collaboration/scratchpad.py +346 -0
- scripts/collaboration/skill_registry.py +151 -0
- scripts/collaboration/skillifier.py +878 -0
- scripts/collaboration/standardized_role_template.py +317 -0
- scripts/collaboration/task_completion_checker.py +237 -0
- scripts/collaboration/test_quality_guard.py +695 -0
- scripts/collaboration/unified_gate_engine.py +598 -0
- scripts/collaboration/usage_tracker.py +309 -0
- scripts/collaboration/user_friendly_error.py +176 -0
- scripts/collaboration/verification_gate.py +312 -0
- scripts/collaboration/warmup_manager.py +635 -0
- scripts/collaboration/worker.py +513 -0
- scripts/collaboration/workflow_engine.py +684 -0
- scripts/dashboard.py +1088 -0
- scripts/generate_benchmark_report.py +786 -0
- scripts/history_manager.py +604 -0
- scripts/mcp_server.py +289 -0
- skills/__init__.py +32 -0
- skills/dispatch/handler.py +52 -0
- skills/intent/handler.py +59 -0
- skills/registry.py +67 -0
- skills/retrospective/__init__.py +0 -0
- skills/retrospective/handler.py +125 -0
- skills/review/handler.py +356 -0
- skills/security/handler.py +454 -0
- skills/test/__init__.py +0 -0
- skills/test/handler.py +78 -0
|
@@ -0,0 +1,604 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# -*- coding: utf-8 -*-
|
|
3
|
+
"""
|
|
4
|
+
DevSquad History Manager
|
|
5
|
+
|
|
6
|
+
Historical data storage and retrieval using SQLite.
|
|
7
|
+
|
|
8
|
+
Features:
|
|
9
|
+
- Metrics snapshots storage
|
|
10
|
+
- Alert history tracking
|
|
11
|
+
- API request logging
|
|
12
|
+
- Time-series queries
|
|
13
|
+
- Automatic data retention/cleanup
|
|
14
|
+
|
|
15
|
+
Usage:
|
|
16
|
+
from scripts.history_manager import HistoryManager
|
|
17
|
+
|
|
18
|
+
history = HistoryManager()
|
|
19
|
+
|
|
20
|
+
# Store metrics snapshot
|
|
21
|
+
history.save_metrics_snapshot({
|
|
22
|
+
"completion_rate": 75.5,
|
|
23
|
+
"avg_response_time_ms": 150.2,
|
|
24
|
+
"cpu_usage": 45.3
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
# Query historical data
|
|
28
|
+
data = history.get_metrics_history(hours=24)
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
import json
|
|
32
|
+
import logging
|
|
33
|
+
import sqlite3
|
|
34
|
+
import sys
|
|
35
|
+
import warnings
|
|
36
|
+
from datetime import datetime, timedelta
|
|
37
|
+
from pathlib import Path
|
|
38
|
+
from typing import Any, Dict, List, Optional, Tuple
|
|
39
|
+
|
|
40
|
+
import os
|
|
41
|
+
|
|
42
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
43
|
+
|
|
44
|
+
logger = logging.getLogger(__name__)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class HistoryManager:
|
|
48
|
+
"""
|
|
49
|
+
SQLite-based history manager for DevSquad.
|
|
50
|
+
|
|
51
|
+
Stores and retrieves time-series data including:
|
|
52
|
+
- Performance metrics snapshots
|
|
53
|
+
- Alert history
|
|
54
|
+
- API request logs
|
|
55
|
+
- Lifecycle state changes
|
|
56
|
+
"""
|
|
57
|
+
|
|
58
|
+
def __init__(self, db_path: Optional[str] = None):
|
|
59
|
+
"""
|
|
60
|
+
Initialize HistoryManager.
|
|
61
|
+
|
|
62
|
+
Args:
|
|
63
|
+
db_path: Path to SQLite database file.
|
|
64
|
+
Defaults to data/devsquad_history.db
|
|
65
|
+
"""
|
|
66
|
+
if db_path is None:
|
|
67
|
+
# Default path in project's data directory
|
|
68
|
+
data_dir = Path(__file__).parent.parent / "data"
|
|
69
|
+
data_dir.mkdir(exist_ok=True)
|
|
70
|
+
db_path = str(data_dir / "devsquad_history.db")
|
|
71
|
+
|
|
72
|
+
self.db_path = db_path
|
|
73
|
+
self.conn = self._get_connection()
|
|
74
|
+
|
|
75
|
+
# Initialize database schema
|
|
76
|
+
self._init_schema()
|
|
77
|
+
|
|
78
|
+
logger.info(f"HistoryManager initialized (db={db_path})")
|
|
79
|
+
|
|
80
|
+
def _get_connection(self) -> sqlite3.Connection:
|
|
81
|
+
"""Get database connection with row factory."""
|
|
82
|
+
conn = sqlite3.connect(self.db_path)
|
|
83
|
+
conn.row_factory = sqlite3.Row
|
|
84
|
+
conn.execute("PRAGMA journal_mode=WAL") # Better concurrency
|
|
85
|
+
conn.execute("PRAGMA foreign_keys=ON")
|
|
86
|
+
return conn
|
|
87
|
+
|
|
88
|
+
def _init_schema(self):
|
|
89
|
+
"""Initialize database tables if they don't exist."""
|
|
90
|
+
cursor = self.conn.cursor()
|
|
91
|
+
|
|
92
|
+
# Metrics snapshots table
|
|
93
|
+
cursor.execute("""
|
|
94
|
+
CREATE TABLE IF NOT EXISTS metrics_snapshots (
|
|
95
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
96
|
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
97
|
+
total_phases INTEGER,
|
|
98
|
+
completed_phases INTEGER,
|
|
99
|
+
running_phases INTEGER,
|
|
100
|
+
failed_phases INTEGER,
|
|
101
|
+
completion_rate REAL,
|
|
102
|
+
avg_response_time_ms REAL,
|
|
103
|
+
p95_latency_ms REAL,
|
|
104
|
+
success_rate REAL,
|
|
105
|
+
cpu_usage_percent REAL,
|
|
106
|
+
memory_usage_percent REAL,
|
|
107
|
+
custom_data TEXT, -- JSON blob for additional data
|
|
108
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
109
|
+
)
|
|
110
|
+
""")
|
|
111
|
+
|
|
112
|
+
# Alert history table
|
|
113
|
+
cursor.execute("""
|
|
114
|
+
CREATE TABLE IF NOT EXISTS alert_history (
|
|
115
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
116
|
+
alert_id VARCHAR(12) NOT NULL,
|
|
117
|
+
severity VARCHAR(20) NOT NULL,
|
|
118
|
+
title TEXT NOT NULL,
|
|
119
|
+
message TEXT,
|
|
120
|
+
source VARCHAR(100),
|
|
121
|
+
channel VARCHAR(20),
|
|
122
|
+
acknowledged BOOLEAN DEFAULT 0,
|
|
123
|
+
resolved_at DATETIME,
|
|
124
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
125
|
+
UNIQUE(alert_id)
|
|
126
|
+
)
|
|
127
|
+
""")
|
|
128
|
+
|
|
129
|
+
# API logs table
|
|
130
|
+
cursor.execute("""
|
|
131
|
+
CREATE TABLE IF NOT EXISTS api_logs (
|
|
132
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
133
|
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
134
|
+
method VARCHAR(10) NOT NULL,
|
|
135
|
+
path TEXT NOT NULL,
|
|
136
|
+
status_code INTEGER,
|
|
137
|
+
response_time_ms REAL,
|
|
138
|
+
client_ip VARCHAR(45),
|
|
139
|
+
user_agent TEXT,
|
|
140
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
141
|
+
)
|
|
142
|
+
""")
|
|
143
|
+
|
|
144
|
+
# Lifecycle events table
|
|
145
|
+
cursor.execute("""
|
|
146
|
+
CREATE TABLE IF NOT EXISTS lifecycle_events (
|
|
147
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
148
|
+
timestamp DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
|
149
|
+
event_type VARCHAR(50) NOT NULL,
|
|
150
|
+
phase_id VARCHAR(10),
|
|
151
|
+
previous_status VARCHAR(20),
|
|
152
|
+
new_status VARCHAR(20),
|
|
153
|
+
user_id VARCHAR(100),
|
|
154
|
+
details TEXT,
|
|
155
|
+
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
156
|
+
)
|
|
157
|
+
""")
|
|
158
|
+
|
|
159
|
+
# Create indexes for better query performance
|
|
160
|
+
indexes = [
|
|
161
|
+
"CREATE INDEX IF NOT EXISTS idx_metrics_timestamp ON metrics_snapshots(timestamp)",
|
|
162
|
+
"CREATE INDEX IF NOT EXISTS idx_alerts_timestamp ON alert_history(created_at)",
|
|
163
|
+
"CREATE INDEX IF NOT EXISTS idx_alerts_severity ON alert_history(severity)",
|
|
164
|
+
"CREATE INDEX IF NOT EXISTS idx_api_logs_timestamp ON api_logs(timestamp)",
|
|
165
|
+
"CREATE INDEX IF NOT EXISTS idx_api_logs_path ON api_logs(path)",
|
|
166
|
+
"CREATE INDEX IF NOT EXISTS idx_lifecycle_events_timestamp ON lifecycle_events(timestamp)",
|
|
167
|
+
"CREATE INDEX IF NOT EXISTS idx_lifecycle_events_phase ON lifecycle_events(phase_id)"
|
|
168
|
+
]
|
|
169
|
+
|
|
170
|
+
for idx_sql in indexes:
|
|
171
|
+
cursor.execute(idx_sql)
|
|
172
|
+
|
|
173
|
+
self.conn.commit()
|
|
174
|
+
logger.debug("Database schema initialized/verified")
|
|
175
|
+
|
|
176
|
+
def save_metrics_snapshot(self, metrics_data: Dict[str, Any]) -> bool:
|
|
177
|
+
"""
|
|
178
|
+
Save a metrics snapshot to the database.
|
|
179
|
+
|
|
180
|
+
Args:
|
|
181
|
+
metrics_data: Dictionary containing metric values
|
|
182
|
+
|
|
183
|
+
Returns:
|
|
184
|
+
True if saved successfully, False otherwise
|
|
185
|
+
"""
|
|
186
|
+
try:
|
|
187
|
+
cursor = self.conn.cursor()
|
|
188
|
+
|
|
189
|
+
# Extract known fields
|
|
190
|
+
custom_data = {
|
|
191
|
+
k: v for k, v in metrics_data.items()
|
|
192
|
+
if k not in [
|
|
193
|
+
'total_phases', 'completed_phases', 'running_phases',
|
|
194
|
+
'failed_phases', 'completion_rate', 'avg_response_time_ms',
|
|
195
|
+
'p95_latency_ms', 'success_rate', 'cpu_usage_percent',
|
|
196
|
+
'memory_usage_percent'
|
|
197
|
+
]
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
cursor.execute("""
|
|
201
|
+
INSERT INTO metrics_snapshots (
|
|
202
|
+
timestamp, total_phases, completed_phases, running_phases,
|
|
203
|
+
failed_phases, completion_rate, avg_response_time_ms,
|
|
204
|
+
p95_latency_ms, success_rate, cpu_usage_percent,
|
|
205
|
+
memory_usage_percent, custom_data
|
|
206
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
207
|
+
""", (
|
|
208
|
+
datetime.now(),
|
|
209
|
+
metrics_data.get('total_phases'),
|
|
210
|
+
metrics_data.get('completed_phases'),
|
|
211
|
+
metrics_data.get('running_phases'),
|
|
212
|
+
metrics_data.get('failed_phases'),
|
|
213
|
+
metrics_data.get('completion_rate'),
|
|
214
|
+
metrics_data.get('avg_response_time_ms'),
|
|
215
|
+
metrics_data.get('p95_latency_ms'),
|
|
216
|
+
metrics_data.get('success_rate'),
|
|
217
|
+
metrics_data.get('cpu_usage_percent'),
|
|
218
|
+
metrics_data.get('memory_usage_percent'),
|
|
219
|
+
json.dumps(custom_data) if custom_data else None
|
|
220
|
+
))
|
|
221
|
+
|
|
222
|
+
self.conn.commit()
|
|
223
|
+
return True
|
|
224
|
+
|
|
225
|
+
except Exception as e:
|
|
226
|
+
logger.error(f"Failed to save metrics snapshot: {e}")
|
|
227
|
+
return False
|
|
228
|
+
|
|
229
|
+
def get_metrics_history(
|
|
230
|
+
self,
|
|
231
|
+
hours: int = 24,
|
|
232
|
+
interval_minutes: int = 60,
|
|
233
|
+
include_custom: bool = False
|
|
234
|
+
) -> List[Dict[str, Any]]:
|
|
235
|
+
"""
|
|
236
|
+
Retrieve metrics history.
|
|
237
|
+
|
|
238
|
+
Args:
|
|
239
|
+
hours: Number of hours to look back
|
|
240
|
+
interval_minutes: Interval between data points
|
|
241
|
+
include_custom: Whether to include custom data field
|
|
242
|
+
|
|
243
|
+
Returns:
|
|
244
|
+
List of metric snapshot dictionaries
|
|
245
|
+
"""
|
|
246
|
+
try:
|
|
247
|
+
cursor = self.conn.cursor()
|
|
248
|
+
|
|
249
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
250
|
+
|
|
251
|
+
# Query with sampling based on interval
|
|
252
|
+
cursor.execute("""
|
|
253
|
+
SELECT * FROM metrics_snapshots
|
|
254
|
+
WHERE timestamp >= ?
|
|
255
|
+
ORDER BY timestamp ASC
|
|
256
|
+
""", (cutoff,))
|
|
257
|
+
|
|
258
|
+
rows = cursor.fetchall()
|
|
259
|
+
|
|
260
|
+
# Sample data based on interval if too many points
|
|
261
|
+
if len(rows) > 500:
|
|
262
|
+
sample_every = max(1, len(rows) // 500)
|
|
263
|
+
rows = rows[::sample_every]
|
|
264
|
+
|
|
265
|
+
results = []
|
|
266
|
+
for row in rows:
|
|
267
|
+
data = dict(row)
|
|
268
|
+
|
|
269
|
+
# Parse custom data if requested
|
|
270
|
+
if include_custom and data.get('custom_data'):
|
|
271
|
+
try:
|
|
272
|
+
data['custom_data'] = json.loads(data['custom_data'])
|
|
273
|
+
except (json.JSONDecodeError, TypeError):
|
|
274
|
+
pass
|
|
275
|
+
|
|
276
|
+
# Convert timestamp to string for JSON serialization
|
|
277
|
+
if isinstance(data.get('timestamp'), datetime):
|
|
278
|
+
data['timestamp'] = data['timestamp'].isoformat()
|
|
279
|
+
|
|
280
|
+
results.append(data)
|
|
281
|
+
|
|
282
|
+
logger.debug(f"Retrieved {len(results)} metrics snapshots")
|
|
283
|
+
return results
|
|
284
|
+
|
|
285
|
+
except Exception as e:
|
|
286
|
+
logger.error(f"Failed to get metrics history: {e}")
|
|
287
|
+
return []
|
|
288
|
+
|
|
289
|
+
def log_api_request(
|
|
290
|
+
self,
|
|
291
|
+
method: str,
|
|
292
|
+
path: str,
|
|
293
|
+
status_code: int,
|
|
294
|
+
response_time_ms: float,
|
|
295
|
+
client_ip: Optional[str] = None,
|
|
296
|
+
user_agent: Optional[str] = None
|
|
297
|
+
) -> bool:
|
|
298
|
+
"""
|
|
299
|
+
Log an API request to the database.
|
|
300
|
+
|
|
301
|
+
Args:
|
|
302
|
+
method: HTTP method (GET, POST, etc.)
|
|
303
|
+
path: Request path
|
|
304
|
+
status_code: HTTP status code
|
|
305
|
+
response_time_ms: Response time in milliseconds
|
|
306
|
+
client_ip: Client IP address
|
|
307
|
+
user_agent: User agent string
|
|
308
|
+
|
|
309
|
+
Returns:
|
|
310
|
+
True if logged successfully
|
|
311
|
+
"""
|
|
312
|
+
try:
|
|
313
|
+
cursor = self.conn.cursor()
|
|
314
|
+
cursor.execute("""
|
|
315
|
+
INSERT INTO api_logs (
|
|
316
|
+
method, path, status_code, response_time_ms,
|
|
317
|
+
client_ip, user_agent
|
|
318
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
319
|
+
""", (method, path, status_code, response_time_ms, client_ip, user_agent))
|
|
320
|
+
|
|
321
|
+
self.conn.commit()
|
|
322
|
+
return True
|
|
323
|
+
|
|
324
|
+
except Exception as e:
|
|
325
|
+
logger.error(f"Failed to log API request: {e}")
|
|
326
|
+
return False
|
|
327
|
+
|
|
328
|
+
def get_api_stats(
|
|
329
|
+
self,
|
|
330
|
+
hours: int = 1,
|
|
331
|
+
group_by_path: bool = True
|
|
332
|
+
) -> Dict[str, Any]:
|
|
333
|
+
"""
|
|
334
|
+
Get API request statistics.
|
|
335
|
+
|
|
336
|
+
Args:
|
|
337
|
+
hours: Statistics period in hours
|
|
338
|
+
group_by_path: Whether to group by endpoint path
|
|
339
|
+
|
|
340
|
+
Returns:
|
|
341
|
+
Dictionary with API statistics
|
|
342
|
+
"""
|
|
343
|
+
try:
|
|
344
|
+
cursor = self.conn.cursor()
|
|
345
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
346
|
+
|
|
347
|
+
# Total requests
|
|
348
|
+
cursor.execute("""
|
|
349
|
+
SELECT COUNT(*) as total,
|
|
350
|
+
AVG(response_time_ms) as avg_response,
|
|
351
|
+
MAX(response_time_ms) as max_response,
|
|
352
|
+
MIN(response_time_ms) as min_response
|
|
353
|
+
FROM api_logs
|
|
354
|
+
WHERE timestamp >= ?
|
|
355
|
+
""", (cutoff,))
|
|
356
|
+
|
|
357
|
+
row = cursor.fetchone()
|
|
358
|
+
stats = {
|
|
359
|
+
"total_requests": row['total'] or 0,
|
|
360
|
+
"avg_response_time_ms": round(row['avg_response'], 2) if row['avg_response'] else 0,
|
|
361
|
+
"max_response_time_ms": round(row['max_response'], 2) if row['max_response'] else 0,
|
|
362
|
+
"min_response_time_ms": round(row['min_response'], 2) if row['min_response'] else 0,
|
|
363
|
+
"period_hours": hours
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
# Status code distribution
|
|
367
|
+
cursor.execute("""
|
|
368
|
+
SELECT status_code, COUNT(*) as count
|
|
369
|
+
FROM api_logs
|
|
370
|
+
WHERE timestamp >= ?
|
|
371
|
+
GROUP BY status_code
|
|
372
|
+
ORDER BY count DESC
|
|
373
|
+
""", (cutoff,))
|
|
374
|
+
|
|
375
|
+
stats["status_codes"] = [
|
|
376
|
+
{"status_code": row['status_code'], "count": row['count']}
|
|
377
|
+
for row in cursor.fetchall()
|
|
378
|
+
]
|
|
379
|
+
|
|
380
|
+
# By endpoint (if requested)
|
|
381
|
+
if group_by_path:
|
|
382
|
+
cursor.execute("""
|
|
383
|
+
SELECT path, COUNT(*) as count,
|
|
384
|
+
AVG(response_time_ms) as avg_response
|
|
385
|
+
FROM api_logs
|
|
386
|
+
WHERE timestamp >= ?
|
|
387
|
+
GROUP BY path
|
|
388
|
+
ORDER BY count DESC
|
|
389
|
+
LIMIT 20
|
|
390
|
+
""", (cutoff,))
|
|
391
|
+
|
|
392
|
+
stats["endpoints"] = [
|
|
393
|
+
{
|
|
394
|
+
"path": row['path'],
|
|
395
|
+
"requests": row['count'],
|
|
396
|
+
"avg_response_ms": round(row['avg_response'], 2) if row['avg_response'] else 0
|
|
397
|
+
}
|
|
398
|
+
for row in cursor.fetchall()
|
|
399
|
+
]
|
|
400
|
+
|
|
401
|
+
return stats
|
|
402
|
+
|
|
403
|
+
except Exception as e:
|
|
404
|
+
logger.error(f"Failed to get API stats: {e}")
|
|
405
|
+
return {}
|
|
406
|
+
|
|
407
|
+
def save_lifecycle_event(
|
|
408
|
+
self,
|
|
409
|
+
event_type: str,
|
|
410
|
+
phase_id: Optional[str] = None,
|
|
411
|
+
previous_status: Optional[str] = None,
|
|
412
|
+
new_status: Optional[str] = None,
|
|
413
|
+
user_id: Optional[str] = None,
|
|
414
|
+
details: Optional[str] = None
|
|
415
|
+
) -> bool:
|
|
416
|
+
"""
|
|
417
|
+
Save a lifecycle state change event.
|
|
418
|
+
|
|
419
|
+
Args:
|
|
420
|
+
event_type: Type of event (phase_advance, phase_complete, etc.)
|
|
421
|
+
phase_id: Phase identifier
|
|
422
|
+
previous_status: Status before change
|
|
423
|
+
new_status: Status after change
|
|
424
|
+
user_id: User who triggered the event
|
|
425
|
+
details: Additional details
|
|
426
|
+
|
|
427
|
+
Returns:
|
|
428
|
+
True if saved successfully
|
|
429
|
+
"""
|
|
430
|
+
try:
|
|
431
|
+
cursor = self.conn.cursor()
|
|
432
|
+
cursor.execute("""
|
|
433
|
+
INSERT INTO lifecycle_events (
|
|
434
|
+
event_type, phase_id, previous_status,
|
|
435
|
+
new_status, user_id, details
|
|
436
|
+
) VALUES (?, ?, ?, ?, ?, ?)
|
|
437
|
+
""", (event_type, phase_id, previous_status, new_status, user_id, details))
|
|
438
|
+
|
|
439
|
+
self.conn.commit()
|
|
440
|
+
return True
|
|
441
|
+
|
|
442
|
+
except Exception as e:
|
|
443
|
+
logger.error(f"Failed to save lifecycle event: {e}")
|
|
444
|
+
return False
|
|
445
|
+
|
|
446
|
+
def get_lifecycle_history(
|
|
447
|
+
self,
|
|
448
|
+
hours: int = 24,
|
|
449
|
+
phase_id: Optional[str] = None,
|
|
450
|
+
event_type: Optional[str] = None
|
|
451
|
+
) -> List[Dict[str, Any]]:
|
|
452
|
+
"""
|
|
453
|
+
Get lifecycle event history.
|
|
454
|
+
|
|
455
|
+
Args:
|
|
456
|
+
hours: Look back period
|
|
457
|
+
phase_id: Filter by specific phase
|
|
458
|
+
event_type: Filter by event type
|
|
459
|
+
|
|
460
|
+
Returns:
|
|
461
|
+
List of lifecycle event dictionaries
|
|
462
|
+
"""
|
|
463
|
+
try:
|
|
464
|
+
cursor = self.conn.cursor()
|
|
465
|
+
cutoff = datetime.now() - timedelta(hours=hours)
|
|
466
|
+
|
|
467
|
+
query = """
|
|
468
|
+
SELECT * FROM lifecycle_events
|
|
469
|
+
WHERE timestamp >= ?
|
|
470
|
+
"""
|
|
471
|
+
params = [cutoff]
|
|
472
|
+
|
|
473
|
+
if phase_id:
|
|
474
|
+
query += " AND phase_id = ?"
|
|
475
|
+
params.append(phase_id)
|
|
476
|
+
|
|
477
|
+
if event_type:
|
|
478
|
+
query += " AND event_type = ?"
|
|
479
|
+
params.append(event_type)
|
|
480
|
+
|
|
481
|
+
query += " ORDER BY timestamp DESC LIMIT 200"
|
|
482
|
+
|
|
483
|
+
cursor.execute(query, params)
|
|
484
|
+
|
|
485
|
+
return [dict(row) for row in cursor.fetchall()]
|
|
486
|
+
|
|
487
|
+
except Exception as e:
|
|
488
|
+
logger.error(f"Failed to get lifecycle history: {e}")
|
|
489
|
+
return []
|
|
490
|
+
|
|
491
|
+
def cleanup_old_data(self, retention_days: int = 30) -> Dict[str, int]:
|
|
492
|
+
"""
|
|
493
|
+
Remove old data beyond retention period.
|
|
494
|
+
|
|
495
|
+
Args:
|
|
496
|
+
retention_days: Number of days to keep data
|
|
497
|
+
|
|
498
|
+
Returns:
|
|
499
|
+
Dictionary with counts of deleted records per table
|
|
500
|
+
"""
|
|
501
|
+
try:
|
|
502
|
+
cursor = self.conn.cursor()
|
|
503
|
+
cutoff = datetime.now() - timedelta(days=retention_days)
|
|
504
|
+
|
|
505
|
+
deleted = {}
|
|
506
|
+
|
|
507
|
+
tables = ['metrics_snapshots', 'alert_history', 'api_logs', 'lifecycle_events']
|
|
508
|
+
for table in tables:
|
|
509
|
+
cursor.execute(f"DELETE FROM {table} WHERE timestamp < ?", (cutoff,))
|
|
510
|
+
deleted[table] = cursor.rowcount
|
|
511
|
+
|
|
512
|
+
self.conn.commit()
|
|
513
|
+
|
|
514
|
+
total_deleted = sum(deleted.values())
|
|
515
|
+
logger.info(f"Cleaned up {total_deleted} old records (retention={retention_days} days)")
|
|
516
|
+
|
|
517
|
+
return deleted
|
|
518
|
+
|
|
519
|
+
except Exception as e:
|
|
520
|
+
logger.error(f"Failed to cleanup old data: {e}")
|
|
521
|
+
return {}
|
|
522
|
+
|
|
523
|
+
def get_database_size(self) -> Dict[str, Any]:
|
|
524
|
+
"""
|
|
525
|
+
Get database size information.
|
|
526
|
+
|
|
527
|
+
Returns:
|
|
528
|
+
Dictionary with size statistics
|
|
529
|
+
"""
|
|
530
|
+
try:
|
|
531
|
+
cursor = self.conn.cursor()
|
|
532
|
+
|
|
533
|
+
# Table sizes
|
|
534
|
+
table_sizes = {}
|
|
535
|
+
tables = ['metrics_snapshots', 'alert_history', 'api_logs', 'lifecycle_events']
|
|
536
|
+
|
|
537
|
+
for table in tables:
|
|
538
|
+
cursor.execute(f"SELECT COUNT(*) as count FROM {table}")
|
|
539
|
+
table_sizes[table] = cursor.fetchone()['count']
|
|
540
|
+
|
|
541
|
+
# File size
|
|
542
|
+
db_file = Path(self.db_path)
|
|
543
|
+
file_size_mb = db_file.stat().st_size / (1024 * 1024) if db_file.exists() else 0
|
|
544
|
+
|
|
545
|
+
return {
|
|
546
|
+
"file_path": self.db_path,
|
|
547
|
+
"file_size_mb": round(file_size_mb, 2),
|
|
548
|
+
"tables": table_sizes,
|
|
549
|
+
"total_records": sum(table_sizes.values())
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
except Exception as e:
|
|
553
|
+
logger.error(f"Failed to get database size: {e}")
|
|
554
|
+
return {}
|
|
555
|
+
|
|
556
|
+
def close(self):
|
|
557
|
+
"""Close database connection."""
|
|
558
|
+
if self.conn:
|
|
559
|
+
self.conn.close()
|
|
560
|
+
logger.info("Database connection closed")
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
if __name__ == "__main__":
|
|
564
|
+
# Demo: Test history manager
|
|
565
|
+
print("\nš DevSquad History Manager Demo\n")
|
|
566
|
+
print("=" * 50)
|
|
567
|
+
|
|
568
|
+
history = HistoryManager()
|
|
569
|
+
|
|
570
|
+
# Save some test data
|
|
571
|
+
test_metrics = {
|
|
572
|
+
"total_phases": 11,
|
|
573
|
+
"completed_phases": 7,
|
|
574
|
+
"running_phases": 1,
|
|
575
|
+
"failed_phases": 0,
|
|
576
|
+
"completion_rate": 63.6,
|
|
577
|
+
"avg_response_time_ms": 150.5,
|
|
578
|
+
"cpu_usage_percent": 45.2,
|
|
579
|
+
"memory_usage_percent": 62.8,
|
|
580
|
+
"custom_field": "test_value"
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
print("\nSaving test metrics snapshot...")
|
|
584
|
+
history.save_metrics_snapshot(test_metrics)
|
|
585
|
+
print("ā
Metrics saved!")
|
|
586
|
+
|
|
587
|
+
# Log an API request
|
|
588
|
+
print("\nLogging test API request...")
|
|
589
|
+
history.log_api_request("GET", "/api/v1/lifecycle/phases", 200, 45.2)
|
|
590
|
+
print("ā
API request logged!")
|
|
591
|
+
|
|
592
|
+
# Query data back
|
|
593
|
+
print("\nQuerying metrics history...")
|
|
594
|
+
data = history.get_metrics_history(hours=1)
|
|
595
|
+
print(f"ā
Retrieved {len(data)} snapshots")
|
|
596
|
+
|
|
597
|
+
# Show database info
|
|
598
|
+
print("\nš Database Info:")
|
|
599
|
+
db_info = history.get_database_size()
|
|
600
|
+
for key, value in db_info.items():
|
|
601
|
+
print(f" {key}: {value}")
|
|
602
|
+
|
|
603
|
+
history.close()
|
|
604
|
+
print("\nā
Demo completed!")
|