pyquerytracker 0.1.0__py3-none-any.whl → 0.1.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.
- examples/async_example.py +63 -0
- examples/core/async_usage.py +53 -0
- examples/core/basic_usage.py +17 -0
- examples/core/error_handling.py +24 -0
- examples/core/quick_test.py +39 -0
- examples/core/with_arguments.py +27 -0
- examples/core/with_config.py +30 -0
- examples/exporter/csv_exporter_1.py +36 -0
- examples/exporter/json_exporter.py +36 -0
- examples/fastapi_app.py +119 -0
- examples/test_endpoints.py +73 -0
- pyquerytracker/__init__.py +3 -2
- pyquerytracker/api.py +72 -0
- pyquerytracker/config.py +26 -10
- pyquerytracker/core.py +122 -58
- pyquerytracker/db/models.py +20 -0
- pyquerytracker/db/session.py +8 -0
- pyquerytracker/db/writer.py +64 -0
- pyquerytracker/exporter/__init__.py +0 -0
- pyquerytracker/exporter/base.py +25 -0
- pyquerytracker/exporter/csv_exporter.py +52 -0
- pyquerytracker/exporter/json_exporter.py +47 -0
- pyquerytracker/exporter/manager.py +32 -0
- pyquerytracker/main.py +6 -0
- pyquerytracker/tracker.py +17 -0
- pyquerytracker/utils/logger.py +18 -0
- pyquerytracker/websocket.py +33 -0
- {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/METADATA +93 -12
- pyquerytracker-0.1.1.dist-info/RECORD +39 -0
- {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/WHEEL +1 -1
- {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/top_level.txt +2 -0
- tests/exporter/test_json_exporter.py +182 -0
- tests/test_async_core.py +93 -0
- tests/test_config.py +40 -0
- tests/test_core.py +72 -0
- tests/test_dashboard.py +31 -0
- tests/test_persist.py +9 -0
- tests/test_websocket.py +58 -0
- pyquerytracker-0.1.0.dist-info/RECORD +0 -8
- {pyquerytracker-0.1.0.dist-info → pyquerytracker-0.1.1.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
from pyquerytracker import TrackQuery
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# Example 1: Basic async function
|
|
11
|
+
@TrackQuery()
|
|
12
|
+
async def fetch_data():
|
|
13
|
+
await asyncio.sleep(0.2)
|
|
14
|
+
return "fetched data"
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
# Example 2: Async function with parameters
|
|
18
|
+
@TrackQuery()
|
|
19
|
+
async def compute_sum(a, b):
|
|
20
|
+
await asyncio.sleep(0.1)
|
|
21
|
+
return a + b
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
# Example 3: Class method async tracking
|
|
25
|
+
class DatabaseService:
|
|
26
|
+
@TrackQuery()
|
|
27
|
+
async def query_users(self):
|
|
28
|
+
await asyncio.sleep(0.3)
|
|
29
|
+
return ["Alice", "Bob", "Charlie"]
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# Example 4: Async function that raises an error
|
|
33
|
+
@TrackQuery()
|
|
34
|
+
async def fail_fast():
|
|
35
|
+
await asyncio.sleep(0.05)
|
|
36
|
+
raise RuntimeError("Database failure simulation")
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
# Example 5: Async function with slow duration to trigger logging thresholds
|
|
40
|
+
@TrackQuery()
|
|
41
|
+
async def slow_operation():
|
|
42
|
+
await asyncio.sleep(0.5)
|
|
43
|
+
return "slow complete"
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
# Run all examples
|
|
47
|
+
async def main():
|
|
48
|
+
print("Example 1: fetch_data →", await fetch_data())
|
|
49
|
+
print("Example 2: compute_sum(5, 7) →", await compute_sum(5, 7))
|
|
50
|
+
|
|
51
|
+
service = DatabaseService()
|
|
52
|
+
print("Example 3: query_users →", await service.query_users())
|
|
53
|
+
|
|
54
|
+
try:
|
|
55
|
+
await fail_fast()
|
|
56
|
+
except RuntimeError as e:
|
|
57
|
+
print("Example 4: fail_fast → raised:", e)
|
|
58
|
+
|
|
59
|
+
print("Example 5: slow_operation →", await slow_operation())
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
if __name__ == "__main__":
|
|
63
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
from pyquerytracker import TrackQuery, configure
|
|
5
|
+
|
|
6
|
+
# Configure logging to see the output
|
|
7
|
+
logging.basicConfig(
|
|
8
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
# Set a low threshold to easily see "slow" logs
|
|
12
|
+
configure(slow_log_threshold_ms=150, slow_log_level=logging.WARNING)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@TrackQuery()
|
|
16
|
+
async def fast_async_query():
|
|
17
|
+
"""An async function that executes quickly."""
|
|
18
|
+
await asyncio.sleep(0.1)
|
|
19
|
+
return "Fast async query completed"
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
@TrackQuery()
|
|
23
|
+
async def slow_async_query():
|
|
24
|
+
"""An async function that takes longer to execute."""
|
|
25
|
+
await asyncio.sleep(0.2)
|
|
26
|
+
return "Slow async query completed"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@TrackQuery()
|
|
30
|
+
async def failing_async_query():
|
|
31
|
+
"""An async function that raises an error."""
|
|
32
|
+
await asyncio.sleep(0.1)
|
|
33
|
+
raise ConnectionError("Could not connect to async database")
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
async def main():
|
|
37
|
+
print("--- Testing fast async query (should be INFO log) ---")
|
|
38
|
+
result1 = await fast_async_query()
|
|
39
|
+
print(f"Result: {result1}\n")
|
|
40
|
+
|
|
41
|
+
print("--- Testing slow async query (should be WARNING log) ---")
|
|
42
|
+
result2 = await slow_async_query()
|
|
43
|
+
print(f"Result: {result2}\n")
|
|
44
|
+
|
|
45
|
+
print("--- Testing failing async query (should be ERROR log) ---")
|
|
46
|
+
try:
|
|
47
|
+
await failing_async_query()
|
|
48
|
+
except ConnectionError as e:
|
|
49
|
+
print(f"Caught expected error: {e}\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
if __name__ == "__main__":
|
|
53
|
+
asyncio.run(main())
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
from pyquerytracker import TrackQuery, configure
|
|
2
|
+
|
|
3
|
+
configure(slow_log_threshold_ms=1000, slow_log_level=20)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@TrackQuery()
|
|
7
|
+
def simple_query():
|
|
8
|
+
"""A simple function that simulates a database query."""
|
|
9
|
+
import time
|
|
10
|
+
|
|
11
|
+
time.sleep(0.5) # Simulate some work
|
|
12
|
+
return "Query completed successfully"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
if __name__ == "__main__":
|
|
16
|
+
result = simple_query()
|
|
17
|
+
print(f"Result: {result}")
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pyquerytracker import TrackQuery
|
|
4
|
+
|
|
5
|
+
# Configure logging
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@TrackQuery()
|
|
12
|
+
def failing_query():
|
|
13
|
+
"""A function that simulates a failed database query."""
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
time.sleep(0.3) # Simulate some work
|
|
17
|
+
raise ValueError("Database connection failed")
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
try:
|
|
22
|
+
failing_query()
|
|
23
|
+
except ValueError as e:
|
|
24
|
+
print(f"Caught expected error: {e}")
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import time
|
|
2
|
+
|
|
3
|
+
from pyquerytracker import TrackQuery
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@TrackQuery()
|
|
7
|
+
def fast_query():
|
|
8
|
+
"""A function that executes quickly."""
|
|
9
|
+
return "Fast query completed"
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@TrackQuery()
|
|
13
|
+
def slow_query():
|
|
14
|
+
"""A function that takes some time to execute."""
|
|
15
|
+
time.sleep(0.5) # Simulate some work
|
|
16
|
+
return "Slow query completed"
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
@TrackQuery()
|
|
20
|
+
def failing_query():
|
|
21
|
+
"""A function that raises an error."""
|
|
22
|
+
time.sleep(0.2) # Simulate some work
|
|
23
|
+
raise ValueError("Query failed!")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
if __name__ == "__main__":
|
|
27
|
+
print("\nTesting fast query:")
|
|
28
|
+
result1 = fast_query()
|
|
29
|
+
print(f"Result: {result1}")
|
|
30
|
+
|
|
31
|
+
print("\nTesting slow query:")
|
|
32
|
+
result2 = slow_query()
|
|
33
|
+
print(f"Result: {result2}")
|
|
34
|
+
|
|
35
|
+
print("\nTesting failing query:")
|
|
36
|
+
try:
|
|
37
|
+
failing_query()
|
|
38
|
+
except ValueError as e:
|
|
39
|
+
print(f"Caught expected error: {e}")
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
|
|
3
|
+
from pyquerytracker import TrackQuery
|
|
4
|
+
|
|
5
|
+
# Configure logging
|
|
6
|
+
logging.basicConfig(
|
|
7
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
8
|
+
)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@TrackQuery()
|
|
12
|
+
def complex_query(user_id: int, query_type: str, limit: int = 10):
|
|
13
|
+
"""A function that simulates a complex database query with arguments."""
|
|
14
|
+
import time
|
|
15
|
+
|
|
16
|
+
time.sleep(0.2) # Simulate some work
|
|
17
|
+
return f"Retrieved {limit} {query_type} records for user {user_id}"
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
if __name__ == "__main__":
|
|
21
|
+
# Test with positional arguments
|
|
22
|
+
result1 = complex_query(123, "posts")
|
|
23
|
+
print(f"Result 1: {result1}")
|
|
24
|
+
|
|
25
|
+
# Test with keyword arguments
|
|
26
|
+
result2 = complex_query(user_id=456, query_type="comments", limit=5)
|
|
27
|
+
print(f"Result 2: {result2}")
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from pyquerytracker import TrackQuery
|
|
5
|
+
from pyquerytracker.config import configure
|
|
6
|
+
|
|
7
|
+
configure(slow_log_threshold_ms=640, slow_log_level=logging.ERROR)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
@TrackQuery()
|
|
11
|
+
def fast_query():
|
|
12
|
+
"""A function that executes quickly."""
|
|
13
|
+
return "Fast query completed"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
@TrackQuery()
|
|
17
|
+
def slow_query():
|
|
18
|
+
"""A function that takes some time to execute."""
|
|
19
|
+
time.sleep(0.3) # Simulate some work
|
|
20
|
+
return "Slow query completed"
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
if __name__ == "__main__":
|
|
24
|
+
# Test with positional arguments
|
|
25
|
+
result1 = fast_query()
|
|
26
|
+
print(f"Result 1: {result1}")
|
|
27
|
+
|
|
28
|
+
# Test with keyword arguments
|
|
29
|
+
result2 = slow_query()
|
|
30
|
+
print(f"Result 2: {result2}")
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from pyquerytracker import TrackQuery
|
|
5
|
+
from pyquerytracker.config import ExportType, configure
|
|
6
|
+
|
|
7
|
+
os.makedirs("logs-csv", exist_ok=True)
|
|
8
|
+
|
|
9
|
+
configure(
|
|
10
|
+
slow_log_threshold_ms=50.0,
|
|
11
|
+
slow_log_level=20, # INFO
|
|
12
|
+
export_type=ExportType.CSV,
|
|
13
|
+
export_path="logs-csv/query_logs_2.csv",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@TrackQuery()
|
|
18
|
+
def process_data(x, y):
|
|
19
|
+
time.sleep(8)
|
|
20
|
+
return x + y
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@TrackQuery()
|
|
24
|
+
def failing_task():
|
|
25
|
+
time.sleep(0.03) # not slow
|
|
26
|
+
raise RuntimeError("This failed intentionally.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Run tasks
|
|
30
|
+
print("Result:", process_data(5, 7))
|
|
31
|
+
print("Result:", process_data(1, 2))
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
failing_task()
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import os
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from pyquerytracker import TrackQuery
|
|
5
|
+
from pyquerytracker.config import ExportType, configure
|
|
6
|
+
|
|
7
|
+
os.makedirs("logs-json", exist_ok=True)
|
|
8
|
+
|
|
9
|
+
configure(
|
|
10
|
+
slow_log_threshold_ms=50.0,
|
|
11
|
+
slow_log_level=20, # INFO
|
|
12
|
+
export_type=ExportType.JSON,
|
|
13
|
+
export_path="logs-json/query_logs.json",
|
|
14
|
+
)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@TrackQuery()
|
|
18
|
+
def process_data(x, y):
|
|
19
|
+
time.sleep(0.08) # triggers slow log
|
|
20
|
+
return x + y
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@TrackQuery()
|
|
24
|
+
def failing_task():
|
|
25
|
+
time.sleep(0.03) # not slow
|
|
26
|
+
raise RuntimeError("This failed intentionally.")
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# Run tasks
|
|
30
|
+
print("Result:", process_data(5, 7))
|
|
31
|
+
print("Result:", process_data(1, 2))
|
|
32
|
+
|
|
33
|
+
try:
|
|
34
|
+
failing_task()
|
|
35
|
+
except Exception:
|
|
36
|
+
pass
|
examples/fastapi_app.py
ADDED
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import random
|
|
3
|
+
from fastapi import FastAPI, HTTPException
|
|
4
|
+
from fastapi.responses import JSONResponse
|
|
5
|
+
from fastapi.middleware.cors import CORSMiddleware
|
|
6
|
+
from pyquerytracker import monitor_queries, configure, StatsCollector
|
|
7
|
+
|
|
8
|
+
# Initialize StatsCollector globally
|
|
9
|
+
stats_collector = StatsCollector()
|
|
10
|
+
|
|
11
|
+
# Configure pyquerytracker
|
|
12
|
+
configure(
|
|
13
|
+
default_threshold_ms=500, # Set threshold to 500ms
|
|
14
|
+
json_export_enabled=True,
|
|
15
|
+
json_export_path="query_stats.json"
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
app = FastAPI(title="PyQueryTracker Demo")
|
|
19
|
+
|
|
20
|
+
# Add CORS middleware
|
|
21
|
+
app.add_middleware(
|
|
22
|
+
CORSMiddleware,
|
|
23
|
+
allow_origins=["*"],
|
|
24
|
+
allow_credentials=True,
|
|
25
|
+
allow_methods=["*"],
|
|
26
|
+
allow_headers=["*"],
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
@monitor_queries(threshold_ms=300)
|
|
30
|
+
async def fast_query():
|
|
31
|
+
"""A query that should be under threshold"""
|
|
32
|
+
await asyncio.sleep(0.1) # 100ms
|
|
33
|
+
return {"status": "success", "message": "Fast query completed"}
|
|
34
|
+
|
|
35
|
+
@monitor_queries(threshold_ms=300)
|
|
36
|
+
async def slow_query():
|
|
37
|
+
"""A query that should exceed threshold"""
|
|
38
|
+
await asyncio.sleep(0.4) # 400ms
|
|
39
|
+
return {"status": "success", "message": "Slow query completed"}
|
|
40
|
+
|
|
41
|
+
@monitor_queries(threshold_ms=300)
|
|
42
|
+
async def error_query():
|
|
43
|
+
"""A query that will raise an error"""
|
|
44
|
+
await asyncio.sleep(0.2) # 200ms
|
|
45
|
+
raise HTTPException(status_code=500, detail="Simulated error")
|
|
46
|
+
|
|
47
|
+
@app.get("/fast")
|
|
48
|
+
async def get_fast():
|
|
49
|
+
"""Endpoint that executes a fast query"""
|
|
50
|
+
try:
|
|
51
|
+
result = await fast_query()
|
|
52
|
+
return JSONResponse(
|
|
53
|
+
content=result,
|
|
54
|
+
media_type="application/json"
|
|
55
|
+
)
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return JSONResponse(
|
|
58
|
+
status_code=500,
|
|
59
|
+
content={"error": str(e)},
|
|
60
|
+
media_type="application/json"
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
@app.get("/slow")
|
|
64
|
+
async def get_slow():
|
|
65
|
+
"""Endpoint that executes a slow query"""
|
|
66
|
+
try:
|
|
67
|
+
result = await slow_query()
|
|
68
|
+
return JSONResponse(
|
|
69
|
+
content=result,
|
|
70
|
+
media_type="application/json"
|
|
71
|
+
)
|
|
72
|
+
except Exception as e:
|
|
73
|
+
return JSONResponse(
|
|
74
|
+
status_code=500,
|
|
75
|
+
content={"error": str(e)},
|
|
76
|
+
media_type="application/json"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
@app.get("/error")
|
|
80
|
+
async def get_error():
|
|
81
|
+
"""Endpoint that executes a query that will error"""
|
|
82
|
+
try:
|
|
83
|
+
await error_query()
|
|
84
|
+
except HTTPException as e:
|
|
85
|
+
return JSONResponse(
|
|
86
|
+
status_code=e.status_code,
|
|
87
|
+
content={"error": e.detail},
|
|
88
|
+
media_type="application/json"
|
|
89
|
+
)
|
|
90
|
+
except Exception as e:
|
|
91
|
+
return JSONResponse(
|
|
92
|
+
status_code=500,
|
|
93
|
+
content={"error": str(e)},
|
|
94
|
+
media_type="application/json"
|
|
95
|
+
)
|
|
96
|
+
|
|
97
|
+
@app.get("/random")
|
|
98
|
+
async def get_random():
|
|
99
|
+
"""Endpoint with random response time"""
|
|
100
|
+
try:
|
|
101
|
+
delay = random.uniform(0.1, 0.5)
|
|
102
|
+
await asyncio.sleep(delay)
|
|
103
|
+
return JSONResponse(
|
|
104
|
+
content={
|
|
105
|
+
"status": "success",
|
|
106
|
+
"message": f"Random query completed in {delay:.2f}s"
|
|
107
|
+
},
|
|
108
|
+
media_type="application/json"
|
|
109
|
+
)
|
|
110
|
+
except Exception as e:
|
|
111
|
+
return JSONResponse(
|
|
112
|
+
status_code=500,
|
|
113
|
+
content={"error": str(e)},
|
|
114
|
+
media_type="application/json"
|
|
115
|
+
)
|
|
116
|
+
|
|
117
|
+
if __name__ == "__main__":
|
|
118
|
+
import uvicorn
|
|
119
|
+
uvicorn.run(app, host="0.0.0.0", port=8080)
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import aiohttp
|
|
3
|
+
import time
|
|
4
|
+
from pyquerytracker.cli import report
|
|
5
|
+
from pyquerytracker.decorator import monitor_queries
|
|
6
|
+
|
|
7
|
+
async def test_endpoint(session: aiohttp.ClientSession, endpoint: str, expected_status: int = 200) -> None:
|
|
8
|
+
"""Test an endpoint and print the result"""
|
|
9
|
+
try:
|
|
10
|
+
async with session.get(f"http://localhost:8000{endpoint}") as response:
|
|
11
|
+
if response.status == expected_status:
|
|
12
|
+
data = await response.json()
|
|
13
|
+
print(f"Success testing {endpoint}: {data}")
|
|
14
|
+
else:
|
|
15
|
+
error_text = await response.text()
|
|
16
|
+
print(f"Error testing {endpoint}: {response.status}, {error_text}")
|
|
17
|
+
except Exception as e:
|
|
18
|
+
print(f"Error testing {endpoint}: {str(e)}")
|
|
19
|
+
|
|
20
|
+
@monitor_queries(name="test_fast")
|
|
21
|
+
async def test_fast(session: aiohttp.ClientSession) -> None:
|
|
22
|
+
"""Test the fast endpoint"""
|
|
23
|
+
await test_endpoint(session, "/fast")
|
|
24
|
+
|
|
25
|
+
@monitor_queries(name="test_slow")
|
|
26
|
+
async def test_slow(session: aiohttp.ClientSession) -> None:
|
|
27
|
+
"""Test the slow endpoint"""
|
|
28
|
+
await test_endpoint(session, "/slow")
|
|
29
|
+
|
|
30
|
+
@monitor_queries(name="test_error")
|
|
31
|
+
async def test_error(session: aiohttp.ClientSession) -> None:
|
|
32
|
+
"""Test the error endpoint"""
|
|
33
|
+
await test_endpoint(session, "/error", expected_status=500)
|
|
34
|
+
|
|
35
|
+
@monitor_queries(name="test_random")
|
|
36
|
+
async def test_random(session: aiohttp.ClientSession) -> None:
|
|
37
|
+
"""Test the random endpoint"""
|
|
38
|
+
await test_endpoint(session, "/random")
|
|
39
|
+
|
|
40
|
+
async def main():
|
|
41
|
+
"""Main test function"""
|
|
42
|
+
async with aiohttp.ClientSession() as session:
|
|
43
|
+
# Test fast endpoint multiple times
|
|
44
|
+
for _ in range(15):
|
|
45
|
+
await test_fast(session)
|
|
46
|
+
|
|
47
|
+
# Test random endpoint
|
|
48
|
+
for _ in range(10):
|
|
49
|
+
await test_random(session)
|
|
50
|
+
|
|
51
|
+
# Test error endpoint
|
|
52
|
+
for _ in range(15):
|
|
53
|
+
await test_error(session)
|
|
54
|
+
|
|
55
|
+
# Test random endpoint again
|
|
56
|
+
for _ in range(10):
|
|
57
|
+
await test_random(session)
|
|
58
|
+
|
|
59
|
+
# Test slow endpoint
|
|
60
|
+
for _ in range(20):
|
|
61
|
+
await test_slow(session)
|
|
62
|
+
|
|
63
|
+
# Test random endpoint one more time
|
|
64
|
+
for _ in range(10):
|
|
65
|
+
await test_random(session)
|
|
66
|
+
|
|
67
|
+
if __name__ == "__main__":
|
|
68
|
+
# Run tests
|
|
69
|
+
asyncio.run(main())
|
|
70
|
+
|
|
71
|
+
# Generate report
|
|
72
|
+
print("\nGenerating statistics report...")
|
|
73
|
+
report()
|
pyquerytracker/__init__.py
CHANGED
pyquerytracker/api.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
from datetime import datetime, timedelta
|
|
2
|
+
|
|
3
|
+
from fastapi import FastAPI, Query, Request, WebSocket
|
|
4
|
+
from fastapi.responses import HTMLResponse
|
|
5
|
+
from fastapi.templating import Jinja2Templates
|
|
6
|
+
|
|
7
|
+
from pyquerytracker.config import get_config
|
|
8
|
+
from pyquerytracker.db.models import TrackedQuery
|
|
9
|
+
from pyquerytracker.db.session import SessionLocal
|
|
10
|
+
from pyquerytracker.websocket import websocket_endpoint
|
|
11
|
+
|
|
12
|
+
app = FastAPI(title="Query Tracker API")
|
|
13
|
+
|
|
14
|
+
templates = Jinja2Templates(directory="templates")
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
if get_config().dashboard_enabled:
|
|
18
|
+
|
|
19
|
+
@app.get("/dashboard", response_class=HTMLResponse)
|
|
20
|
+
def dashboard(request: Request):
|
|
21
|
+
return templates.TemplateResponse("dashboard.html", {"request": request})
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@app.get("/api/query-stats")
|
|
25
|
+
def get_query_stats(minutes: int = Query(5, ge=1, le=1440)):
|
|
26
|
+
cutoff = datetime.utcnow() - timedelta(minutes=minutes)
|
|
27
|
+
print(f"[DEBUG] Cutoff: {cutoff}")
|
|
28
|
+
|
|
29
|
+
session = SessionLocal()
|
|
30
|
+
try:
|
|
31
|
+
logs = (
|
|
32
|
+
session.query(TrackedQuery)
|
|
33
|
+
.filter(TrackedQuery.timestamp >= cutoff) # Now comparing text to text
|
|
34
|
+
.order_by(TrackedQuery.timestamp)
|
|
35
|
+
.all()
|
|
36
|
+
)
|
|
37
|
+
print(f"[DEBUG] Matching logs: {len(logs)}")
|
|
38
|
+
return {
|
|
39
|
+
"labels": [q.timestamp for q in logs],
|
|
40
|
+
"durations": [q.duration_ms for q in logs],
|
|
41
|
+
"events": [q.event for q in logs],
|
|
42
|
+
"function_names": [q.function_name for q in logs],
|
|
43
|
+
}
|
|
44
|
+
finally:
|
|
45
|
+
session.close()
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@app.get("/debug/queries")
|
|
49
|
+
def debug_queries():
|
|
50
|
+
session = SessionLocal()
|
|
51
|
+
try:
|
|
52
|
+
rows = (
|
|
53
|
+
session.query(TrackedQuery)
|
|
54
|
+
.order_by(TrackedQuery.timestamp.desc())
|
|
55
|
+
.limit(5)
|
|
56
|
+
.all()
|
|
57
|
+
)
|
|
58
|
+
return [
|
|
59
|
+
{
|
|
60
|
+
"timestamp": r.timestamp,
|
|
61
|
+
"duration_ms": r.duration_ms,
|
|
62
|
+
"event": r.event,
|
|
63
|
+
}
|
|
64
|
+
for r in rows
|
|
65
|
+
]
|
|
66
|
+
finally:
|
|
67
|
+
session.close()
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
@app.websocket("/ws")
|
|
71
|
+
async def websocket_route(websocket: WebSocket):
|
|
72
|
+
await websocket_endpoint(websocket)
|
pyquerytracker/config.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
|
|
1
|
+
import logging
|
|
2
2
|
from dataclasses import dataclass
|
|
3
|
+
from enum import Enum
|
|
3
4
|
from typing import Optional
|
|
4
|
-
import logging
|
|
5
5
|
|
|
6
6
|
|
|
7
7
|
class ExportType(str, Enum):
|
|
@@ -23,15 +23,22 @@ class Config:
|
|
|
23
23
|
Configuration settings for the query tracking system.
|
|
24
24
|
|
|
25
25
|
Attributes:
|
|
26
|
-
slow_log_threshold_ms (float):
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
26
|
+
slow_log_threshold_ms (float):
|
|
27
|
+
Threshold in milliseconds above which a query is considered slow.
|
|
28
|
+
Defaults to 100.0 ms.
|
|
29
|
+
|
|
30
|
+
slow_log_level (int):
|
|
31
|
+
Logging level for slow query logs (e.g., logging.WARNING, logging.INFO).
|
|
32
|
+
Defaults to logging.WARNING.
|
|
30
33
|
"""
|
|
31
34
|
|
|
32
35
|
# TODO: Adding export functionality
|
|
33
36
|
slow_log_threshold_ms: float = 100.0
|
|
34
37
|
slow_log_level: int = logging.WARNING
|
|
38
|
+
export_type: Optional[ExportType] = None
|
|
39
|
+
export_path: Optional[str] = None
|
|
40
|
+
dashboard_enabled: bool = True # ← set to False in real deployments
|
|
41
|
+
persist_to_db: bool = True
|
|
35
42
|
|
|
36
43
|
|
|
37
44
|
_config: Config = Config()
|
|
@@ -40,20 +47,29 @@ _config: Config = Config()
|
|
|
40
47
|
def configure(
|
|
41
48
|
slow_log_threshold_ms: Optional[float] = None,
|
|
42
49
|
slow_log_level: Optional[int] = None,
|
|
50
|
+
export_type: Optional[ExportType] = None,
|
|
51
|
+
export_path: Optional[str] = None,
|
|
43
52
|
):
|
|
44
53
|
"""
|
|
45
54
|
Configure global settings for query tracking.
|
|
46
55
|
|
|
47
56
|
Args:
|
|
48
|
-
slow_log_threshold_ms (Optional[float]):
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
57
|
+
slow_log_threshold_ms (Optional[float]):
|
|
58
|
+
Threshold in milliseconds to log a query as "slow".
|
|
59
|
+
If not provided, defaults to 100.0 ms.
|
|
60
|
+
|
|
61
|
+
slow_log_level (Optional[int]):
|
|
62
|
+
Logging level for slow queries (e.g., logging.INFO, logging.WARNING).
|
|
63
|
+
If not provided, defaults to logging.WARNING.
|
|
52
64
|
"""
|
|
53
65
|
if slow_log_threshold_ms is not None:
|
|
54
66
|
_config.slow_log_threshold_ms = slow_log_threshold_ms
|
|
55
67
|
if slow_log_level is not None:
|
|
56
68
|
_config.slow_log_level = slow_log_level
|
|
69
|
+
if export_type is not None:
|
|
70
|
+
_config.export_type = export_type
|
|
71
|
+
if export_path is not None:
|
|
72
|
+
_config.export_path = export_path
|
|
57
73
|
|
|
58
74
|
|
|
59
75
|
def get_config() -> Config:
|