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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: pyquerytracker
|
|
3
|
-
Version: 0.1.
|
|
3
|
+
Version: 0.1.1
|
|
4
4
|
Summary: A lightweight, decorator-based query performance tracking library for Python applications. Monitor and analyze database query performance with ease.
|
|
5
5
|
Author-email: MuddyHope <daktarisun@gmail.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,6 +20,16 @@ Description-Content-Type: text/markdown
|
|
|
20
20
|
License-File: LICENSE
|
|
21
21
|
Dynamic: license-file
|
|
22
22
|
|
|
23
|
+
|
|
24
|
+

|
|
25
|
+

|
|
26
|
+
|
|
27
|
+

|
|
28
|
+

|
|
29
|
+

|
|
30
|
+
|
|
31
|
+

|
|
32
|
+
|
|
23
33
|
# 🐍 pyquerytracker
|
|
24
34
|
|
|
25
35
|
**pyquerytracker** is a lightweight Python utility to **track and analyze database query performance** using simple decorators. It enables developers to gain visibility into SQL execution time, log metadata, and export insights in JSON format — with optional FastAPI integration and scheduled reporting.
|
|
@@ -46,8 +56,24 @@ Dynamic: license-file
|
|
|
46
56
|
pip install pyquerytracker
|
|
47
57
|
```
|
|
48
58
|
|
|
49
|
-
##
|
|
59
|
+
## 🔧 Configuration
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
import logging
|
|
63
|
+
from pyquerytracker.config import configure
|
|
64
|
+
|
|
65
|
+
configure(
|
|
66
|
+
slow_log_threshold_ms=200, # Log queries slower than 200ms
|
|
67
|
+
slow_log_level=logging.DEBUG # Use DEBUG level for slow logs
|
|
68
|
+
)
|
|
69
|
+
```
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ⚙️ Usage
|
|
74
|
+
|
|
50
75
|
### Basic Usage
|
|
76
|
+
|
|
51
77
|
```python
|
|
52
78
|
import time
|
|
53
79
|
from pyquerytracker import TrackQuery
|
|
@@ -59,26 +85,81 @@ def run_query():
|
|
|
59
85
|
|
|
60
86
|
run_query()
|
|
61
87
|
```
|
|
62
|
-
|
|
63
|
-
|
|
88
|
+
|
|
89
|
+
**Output:**
|
|
90
|
+
```
|
|
64
91
|
2025-06-14 14:23:00,123 - pyquerytracker - INFO - Function run_query executed successfully in 305.12ms
|
|
65
92
|
```
|
|
66
93
|
|
|
67
|
-
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
### 🧩 Async Support
|
|
97
|
+
|
|
98
|
+
Use the same decorator with `async` functions or class methods:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
import asyncio
|
|
102
|
+
from pyquerytracker import TrackQuery
|
|
103
|
+
|
|
104
|
+
@TrackQuery()
|
|
105
|
+
async def fetch_data():
|
|
106
|
+
await asyncio.sleep(0.2)
|
|
107
|
+
return "fetched"
|
|
108
|
+
|
|
109
|
+
class MyService:
|
|
110
|
+
@TrackQuery()
|
|
111
|
+
async def do_work(self, x, y):
|
|
112
|
+
await asyncio.sleep(0.1)
|
|
113
|
+
return x + y
|
|
114
|
+
|
|
115
|
+
asyncio.run(fetch_data())
|
|
68
116
|
```
|
|
69
|
-
|
|
117
|
+
|
|
118
|
+
---
|
|
119
|
+
|
|
120
|
+
### 🌐 Run the FastAPI Server
|
|
121
|
+
|
|
122
|
+
To view tracked query logs via REST, WebSocket, or a Web-based dashboard, start the built-in FastAPI server:
|
|
123
|
+
|
|
124
|
+
```bash
|
|
125
|
+
uvicorn pyquerytracker.api:app --reload
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
- ⚠️ If your project or file structure is different, replace `pyquerytracker.api` with your own module path, like `<your_project_name>.<your_server(file)_name>`.
|
|
129
|
+
|
|
130
|
+
- Open docs at [http://localhost:8000/docs](http://localhost:8000/docs)
|
|
131
|
+
- **Query Dashboard UI:** [http://localhost:8000/dashboard](http://localhost:8000/dashboard)
|
|
132
|
+
- REST endpoint: `GET /queries`
|
|
133
|
+
- WebSocket stream: `ws://localhost:8000/ws`
|
|
134
|
+
|
|
135
|
+
Then run your tracked functions in another terminal or script:
|
|
136
|
+
|
|
137
|
+
```python
|
|
138
|
+
@TrackQuery()
|
|
139
|
+
def insert_query():
|
|
140
|
+
time.sleep(0.4)
|
|
141
|
+
return "INSERT INTO users ..."
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
You’ll see logs live on the server via API/WebSocket.
|
|
145
|
+
|
|
146
|
+
---
|
|
147
|
+
|
|
148
|
+
## 📤 Export Logs
|
|
149
|
+
|
|
150
|
+
Enable exporting to CSV or JSON by setting config:
|
|
151
|
+
|
|
152
|
+
```python
|
|
70
153
|
from pyquerytracker.config import configure
|
|
71
154
|
|
|
72
155
|
configure(
|
|
73
|
-
|
|
74
|
-
|
|
156
|
+
export_type="json",
|
|
157
|
+
export_path="./query_logs.json"
|
|
75
158
|
)
|
|
76
159
|
```
|
|
77
160
|
|
|
78
|
-
|
|
79
|
-
```bash
|
|
80
|
-
2025-06-14 14:24:45,456 - pyquerytracker - WARNING - Slow execution: run_query took 501.87ms
|
|
81
|
-
```
|
|
161
|
+
---
|
|
82
162
|
|
|
163
|
+
Let us know how you’re using `pyquerytracker` and feel free to contribute!
|
|
83
164
|
|
|
84
165
|
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
examples/async_example.py,sha256=c22yunbe7XW4Lqo9NQGJizNty69ieRtTrAo5179446Y,1476
|
|
2
|
+
examples/fastapi_app.py,sha256=bNYkkgzUCWFV_cJ7lfX-sQBhXXdUUeOd-bxum2KwggE,3334
|
|
3
|
+
examples/test_endpoints.py,sha256=wFea-BE82osBeUvY9zn7zD1PyQhAaaQ4es9AaefnhMQ,2402
|
|
4
|
+
examples/core/async_usage.py,sha256=-JDyXoaglwnipQLwSx_qrc5pE3KGWa7rf-bBurP8a_4,1432
|
|
5
|
+
examples/core/basic_usage.py,sha256=ux-FC8XJFS7KUK0Prci0a2BwGnS4v0_gcGP077e-aeE,393
|
|
6
|
+
examples/core/error_handling.py,sha256=V0ewoMJZOSPQR4NRzhSZv7OAEnjqLjef7QkNRaLvVMQ,527
|
|
7
|
+
examples/core/quick_test.py,sha256=a7lTkWj1ZJD75pH-Ae66nxvzUcKqOjKBi7mVRuJRzcU,850
|
|
8
|
+
examples/core/with_arguments.py,sha256=BWm4kuh7kpzKSb4pGOsx8bU2ZUPtR8UJLaDYQE_t5cM,759
|
|
9
|
+
examples/core/with_config.py,sha256=rqkhelnRnDs6h7KHekP62MSjnjU4lQJz6lKOx3ZIX_A,677
|
|
10
|
+
examples/exporter/csv_exporter_1.py,sha256=g0kB55sA0-8q66491ZD3K1QxB-doktqQAwDaKDVzbcA,648
|
|
11
|
+
examples/exporter/json_exporter.py,sha256=K-ZHdiLgODft6jZmE3nYTesEh_Z_OOOVQmc4WV_0KqY,674
|
|
12
|
+
pyquerytracker/__init__.py,sha256=8vDDWF9EBGmIyijtD4n9PPxVVhXkoUtIWKFAw5WbWXM,126
|
|
13
|
+
pyquerytracker/api.py,sha256=ILxHFRo9A-2rDzWKBK15_Z0wBQeq2JHvvJ8Ky0AJX-w,2079
|
|
14
|
+
pyquerytracker/config.py,sha256=o_MV1pGfb3HGQLNSfyISQ8YX0jDCymiCMQEHGkHVf2A,2268
|
|
15
|
+
pyquerytracker/core.py,sha256=8300ITVvMbjY8OsdqApc69zJaudeOp0YHWeq4f9iR_s,6169
|
|
16
|
+
pyquerytracker/main.py,sha256=EUbMAeokP1nsY-sXhG_kWtGzMLvUcJhLD7N8uJVRlhQ,137
|
|
17
|
+
pyquerytracker/tracker.py,sha256=2RIfq1CNMzJ7XiMBqcMcFgHIwgTXBfuXU7nfwDpNOgo,605
|
|
18
|
+
pyquerytracker/websocket.py,sha256=zPKjqrz1OQniMW9RQTnQHmg9yROE3vfWz23KCbBLd9g,908
|
|
19
|
+
pyquerytracker/db/models.py,sha256=R0o0uHQp0QZl886vi5wJix5I67jCuQq1mhzyKTj4ypQ,644
|
|
20
|
+
pyquerytracker/db/session.py,sha256=uuqrlNGdvTRkYnfEXNGB7bWKUsFDX8KnaI6LgCAW-0w,278
|
|
21
|
+
pyquerytracker/db/writer.py,sha256=h2peKrlEB3bTuzs6NdJe6fyMVZohMRussSzPXPPdlNI,2157
|
|
22
|
+
pyquerytracker/exporter/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
23
|
+
pyquerytracker/exporter/base.py,sha256=jRrQT29ZLDAFuyv3YnpmLD_dO7HuFeRQZ53eVDVmtfk,412
|
|
24
|
+
pyquerytracker/exporter/csv_exporter.py,sha256=2XWZs9VKcWxYZ_NTd8iiVhWOFW3FfQwysZRHixZsMZE,1648
|
|
25
|
+
pyquerytracker/exporter/json_exporter.py,sha256=YJGqHJpwi-yA_Cxjj1GIMytm3WW34ngjoH0j3ZTBsOE,1422
|
|
26
|
+
pyquerytracker/exporter/manager.py,sha256=owg6mlvGa6qjR92VayOIO2lUVGBUukYm54Mu3gOzB18,1016
|
|
27
|
+
pyquerytracker/utils/logger.py,sha256=cgBQG8yvwGA1SUsbd2Ef7zBieVEVWxRx_7xIVWrhMsc,506
|
|
28
|
+
pyquerytracker-0.1.1.dist-info/licenses/LICENSE,sha256=lXcEFZRxovixBqp9SYJRLrN5OpP6AMggc_v7eaAMWn4,1065
|
|
29
|
+
tests/test_async_core.py,sha256=mwTvPdUHPVmdQMd_mB1BpIF-PTgcg3Hs3AAofY03sts,2716
|
|
30
|
+
tests/test_config.py,sha256=XKi8GM5P2lj0Xm10pPSwMYnHRnYhvpkmTyeKGFyaiOM,945
|
|
31
|
+
tests/test_core.py,sha256=SFooHkpvYUWipuon0IIwVUChVhRwUaXyBH78humupkM,1688
|
|
32
|
+
tests/test_dashboard.py,sha256=OsUP7_4mBF6u0HgAgzL5Vkf1qO4Vm7Np9z4uv8zSGzc,724
|
|
33
|
+
tests/test_persist.py,sha256=E7MFfP4LAXATRojpGoNgK8HbyszdILNWB47c-9SsBis,123
|
|
34
|
+
tests/test_websocket.py,sha256=Akfd9EH9XhBMMGb-B5Jg_aaC1OAHeVFiMIKlFjyLaS0,1470
|
|
35
|
+
tests/exporter/test_json_exporter.py,sha256=m6mDQTjdmiag9AulmP2mzDl78xwgqXZ1hMm6HQBkKg4,4553
|
|
36
|
+
pyquerytracker-0.1.1.dist-info/METADATA,sha256=F5uwJ3ZyysfGbOQzg8xmDyw-ejkrhjJNwzNc4M_iwmw,4519
|
|
37
|
+
pyquerytracker-0.1.1.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
|
|
38
|
+
pyquerytracker-0.1.1.dist-info/top_level.txt,sha256=gSZtdyA6IEnK-AIdMq8dMYY4AUNhnLm6jOmUY8pFZrI,30
|
|
39
|
+
pyquerytracker-0.1.1.dist-info/RECORD,,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
import subprocess
|
|
4
|
+
import tempfile
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def run_test_in_subprocess(script: str, export_path: str):
|
|
8
|
+
print("\n----- Running Script -----\n")
|
|
9
|
+
print(script)
|
|
10
|
+
print("\n--------------------------\n")
|
|
11
|
+
|
|
12
|
+
try:
|
|
13
|
+
subprocess.run(
|
|
14
|
+
["python3", "-c", script], check=True, capture_output=True, text=True
|
|
15
|
+
)
|
|
16
|
+
except subprocess.CalledProcessError as e:
|
|
17
|
+
print("STDOUT:\n", e.stdout)
|
|
18
|
+
print("STDERR:\n", e.stderr)
|
|
19
|
+
raise
|
|
20
|
+
|
|
21
|
+
assert os.path.exists(export_path)
|
|
22
|
+
with open(export_path) as f:
|
|
23
|
+
return json.load(f)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
def test_successful_function_logs():
|
|
27
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
28
|
+
export_path = os.path.join(tmpdir, "log.json")
|
|
29
|
+
|
|
30
|
+
script = f"""
|
|
31
|
+
import time
|
|
32
|
+
from pyquerytracker import TrackQuery
|
|
33
|
+
from pyquerytracker.config import configure, ExportType
|
|
34
|
+
|
|
35
|
+
configure(
|
|
36
|
+
slow_log_threshold_ms=0.0,
|
|
37
|
+
slow_log_level=10,
|
|
38
|
+
export_type=ExportType.JSON,
|
|
39
|
+
export_path=r"{export_path}",
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
@TrackQuery()
|
|
43
|
+
def foo(x, y):
|
|
44
|
+
return x + y
|
|
45
|
+
|
|
46
|
+
foo(1, 2)
|
|
47
|
+
from pyquerytracker.exporter.manager import ExporterManager
|
|
48
|
+
ExporterManager.get().flush()
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
logs = run_test_in_subprocess(script, export_path)
|
|
52
|
+
|
|
53
|
+
assert isinstance(logs, list)
|
|
54
|
+
functions_logged = {log.get("function_name") for log in logs}
|
|
55
|
+
assert "foo" in functions_logged
|
|
56
|
+
assert any(log.get("event") == "success" for log in logs) or any(
|
|
57
|
+
log.get("event") == "slow_execution" for log in logs
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def test_error_function_logs():
|
|
62
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
63
|
+
export_path = os.path.join(tmpdir, "log.json")
|
|
64
|
+
|
|
65
|
+
script = f"""
|
|
66
|
+
import time
|
|
67
|
+
from pyquerytracker import TrackQuery
|
|
68
|
+
from pyquerytracker.config import configure, ExportType
|
|
69
|
+
|
|
70
|
+
configure(
|
|
71
|
+
slow_log_threshold_ms=0.0,
|
|
72
|
+
slow_log_level=10,
|
|
73
|
+
export_type=ExportType.JSON,
|
|
74
|
+
export_path=r"{export_path}",
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@TrackQuery()
|
|
78
|
+
def bar():
|
|
79
|
+
raise RuntimeError("error")
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
bar()
|
|
83
|
+
except RuntimeError:
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
from pyquerytracker.exporter.manager import ExporterManager
|
|
87
|
+
ExporterManager.get().flush()
|
|
88
|
+
|
|
89
|
+
"""
|
|
90
|
+
|
|
91
|
+
logs = run_test_in_subprocess(script, export_path)
|
|
92
|
+
|
|
93
|
+
assert isinstance(logs, list)
|
|
94
|
+
functions_logged = {log.get("function_name") for log in logs}
|
|
95
|
+
assert "bar" in functions_logged
|
|
96
|
+
assert any(log.get("event") == "error" for log in logs)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def test_slow_function_logs():
|
|
100
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
101
|
+
export_path = os.path.join(tmpdir, "log.json")
|
|
102
|
+
|
|
103
|
+
script = f"""
|
|
104
|
+
import time
|
|
105
|
+
from pyquerytracker import TrackQuery
|
|
106
|
+
from pyquerytracker.config import configure, ExportType
|
|
107
|
+
|
|
108
|
+
# Set threshold low so the sleep triggers slow log
|
|
109
|
+
configure(
|
|
110
|
+
slow_log_threshold_ms=5.0,
|
|
111
|
+
slow_log_level=10,
|
|
112
|
+
export_type=ExportType.JSON,
|
|
113
|
+
export_path=r"{export_path}",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
@TrackQuery()
|
|
117
|
+
def slow_func():
|
|
118
|
+
time.sleep(0.01)
|
|
119
|
+
return "done"
|
|
120
|
+
|
|
121
|
+
slow_func()
|
|
122
|
+
|
|
123
|
+
from pyquerytracker.exporter.manager import ExporterManager
|
|
124
|
+
ExporterManager.get().flush()
|
|
125
|
+
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
logs = run_test_in_subprocess(script, export_path)
|
|
129
|
+
|
|
130
|
+
assert isinstance(logs, list)
|
|
131
|
+
functions_logged = {log.get("function_name") for log in logs}
|
|
132
|
+
assert "slow_func" in functions_logged
|
|
133
|
+
assert any(log.get("event") == "slow_execution" for log in logs)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def test_multiple_calls_logged():
|
|
137
|
+
with tempfile.TemporaryDirectory() as tmpdir:
|
|
138
|
+
export_path = os.path.join(tmpdir, "log.json")
|
|
139
|
+
|
|
140
|
+
script = f"""
|
|
141
|
+
import time
|
|
142
|
+
from pyquerytracker import TrackQuery
|
|
143
|
+
from pyquerytracker.config import configure, ExportType
|
|
144
|
+
|
|
145
|
+
configure(
|
|
146
|
+
slow_log_threshold_ms=0.0,
|
|
147
|
+
slow_log_level=10,
|
|
148
|
+
export_type=ExportType.JSON,
|
|
149
|
+
export_path=r"{export_path}",
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
@TrackQuery()
|
|
153
|
+
def a():
|
|
154
|
+
return 1
|
|
155
|
+
|
|
156
|
+
@TrackQuery()
|
|
157
|
+
def b():
|
|
158
|
+
raise ValueError("fail")
|
|
159
|
+
|
|
160
|
+
a()
|
|
161
|
+
try:
|
|
162
|
+
b()
|
|
163
|
+
except Exception:
|
|
164
|
+
pass
|
|
165
|
+
a()
|
|
166
|
+
|
|
167
|
+
from pyquerytracker.exporter.manager import ExporterManager
|
|
168
|
+
ExporterManager.get().flush()
|
|
169
|
+
|
|
170
|
+
"""
|
|
171
|
+
|
|
172
|
+
logs = run_test_in_subprocess(script, export_path)
|
|
173
|
+
|
|
174
|
+
assert isinstance(logs, list)
|
|
175
|
+
functions_logged = [log.get("function_name") for log in logs]
|
|
176
|
+
|
|
177
|
+
# Because of overwrite behavior, logs might only contain last flush,
|
|
178
|
+
# but often logs include multiple entries. So test at least one a, b occurrence:
|
|
179
|
+
assert "a" in functions_logged or "b" in functions_logged
|
|
180
|
+
# Check for presence of success and error events in the batch
|
|
181
|
+
events = {log.get("event") for log in logs}
|
|
182
|
+
assert "error" in events or "success" in events or "slow_execution" in events
|
tests/test_async_core.py
ADDED
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
|
|
4
|
+
import pytest
|
|
5
|
+
|
|
6
|
+
from pyquerytracker import TrackQuery, configure
|
|
7
|
+
|
|
8
|
+
# Mark all tests in this file as asyncio
|
|
9
|
+
pytestmark = pytest.mark.asyncio
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
async def test_async_tracking_output(caplog):
|
|
13
|
+
caplog.set_level("INFO")
|
|
14
|
+
|
|
15
|
+
@TrackQuery()
|
|
16
|
+
async def fake_async_db_query():
|
|
17
|
+
await asyncio.sleep(0.01)
|
|
18
|
+
return "done"
|
|
19
|
+
|
|
20
|
+
result = await fake_async_db_query()
|
|
21
|
+
assert result == "done"
|
|
22
|
+
|
|
23
|
+
# Check the log records
|
|
24
|
+
assert len(caplog.records) == 1
|
|
25
|
+
record = caplog.records[0]
|
|
26
|
+
assert record.levelname == "INFO"
|
|
27
|
+
assert "Function fake_async_db_query executed successfully" in record.message
|
|
28
|
+
assert "ms" in record.message
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
async def test_async_tracking_output_with_error(caplog):
|
|
32
|
+
configure(slow_log_threshold_ms=10, slow_log_level=40)
|
|
33
|
+
caplog.set_level("ERROR")
|
|
34
|
+
|
|
35
|
+
@TrackQuery()
|
|
36
|
+
async def failing_async_query():
|
|
37
|
+
await asyncio.sleep(0.01)
|
|
38
|
+
raise ValueError("Async Test error")
|
|
39
|
+
|
|
40
|
+
result = await failing_async_query()
|
|
41
|
+
assert result is None
|
|
42
|
+
|
|
43
|
+
# Check the log records
|
|
44
|
+
assert len(caplog.records) == 1
|
|
45
|
+
record = caplog.records[0]
|
|
46
|
+
assert record.levelname == "ERROR"
|
|
47
|
+
assert "Function failing_async_query failed" in record.message
|
|
48
|
+
assert "Async Test error" in record.message
|
|
49
|
+
assert "ms" in record.message
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
async def test_async_tracking_with_class(caplog):
|
|
53
|
+
configure(slow_log_threshold_ms=1000, slow_log_level=logging.INFO)
|
|
54
|
+
caplog.set_level("INFO")
|
|
55
|
+
|
|
56
|
+
class MyAsyncClass:
|
|
57
|
+
@TrackQuery()
|
|
58
|
+
async def do_work(self, a, b):
|
|
59
|
+
await asyncio.sleep(0.01)
|
|
60
|
+
return a + b
|
|
61
|
+
|
|
62
|
+
result = await MyAsyncClass().do_work(5, 10)
|
|
63
|
+
assert result == 15
|
|
64
|
+
assert len(caplog.records) == 1
|
|
65
|
+
record = caplog.records[0]
|
|
66
|
+
assert record.levelname == "INFO"
|
|
67
|
+
assert "MyAsyncClass" in record.message
|
|
68
|
+
assert "do_work" in record.message
|
|
69
|
+
assert "ms" in record.message
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
async def test_async_configure_slow_log(caplog):
|
|
73
|
+
configure(slow_log_threshold_ms=10, slow_log_level=40)
|
|
74
|
+
caplog.set_level("ERROR", logger="pyquerytracker")
|
|
75
|
+
|
|
76
|
+
@TrackQuery()
|
|
77
|
+
async def do_slow_async_work():
|
|
78
|
+
await asyncio.sleep(0.1)
|
|
79
|
+
return "slow"
|
|
80
|
+
|
|
81
|
+
try:
|
|
82
|
+
result = await do_slow_async_work()
|
|
83
|
+
assert result == "slow"
|
|
84
|
+
assert len(caplog.records) == 1
|
|
85
|
+
record = caplog.records[0]
|
|
86
|
+
assert record.levelname == "ERROR" # Configured to ERROR for slow logs
|
|
87
|
+
assert "do_slow_async_work" in record.message
|
|
88
|
+
assert "Slow execution" in record.message
|
|
89
|
+
assert "ms" in record.message
|
|
90
|
+
|
|
91
|
+
finally:
|
|
92
|
+
# Reset config to avoid affecting other tests
|
|
93
|
+
configure(slow_log_threshold_ms=100.0, slow_log_level=logging.WARNING)
|
tests/test_config.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from pyquerytracker.core import TrackQuery, logger
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def configure(slow_log_threshold_ms=100, slow_log_level=logging.INFO):
|
|
8
|
+
logger.setLevel(slow_log_level)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def test_configure_basic(caplog):
|
|
12
|
+
configure(slow_log_threshold_ms=250)
|
|
13
|
+
|
|
14
|
+
class MyClass:
|
|
15
|
+
@TrackQuery()
|
|
16
|
+
def do_work(self, a, b):
|
|
17
|
+
time.sleep(0.5)
|
|
18
|
+
return a * b
|
|
19
|
+
|
|
20
|
+
MyClass().do_work(2, 3)
|
|
21
|
+
assert len(caplog.records) == 1
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_configure_basic_with_loglevel(caplog):
|
|
25
|
+
caplog.set_level("ERROR", logger="pyquerytracker")
|
|
26
|
+
|
|
27
|
+
configure(slow_log_threshold_ms=100, slow_log_level=logging.ERROR)
|
|
28
|
+
|
|
29
|
+
class MyClass:
|
|
30
|
+
def do_slow_work(self, a, b):
|
|
31
|
+
import time
|
|
32
|
+
|
|
33
|
+
time.sleep(0.2)
|
|
34
|
+
return a * b
|
|
35
|
+
|
|
36
|
+
# Apply TrackQuery to the unbound method
|
|
37
|
+
MyClass.do_slow_work = TrackQuery()(MyClass.do_slow_work)
|
|
38
|
+
|
|
39
|
+
result = MyClass().do_slow_work(2, 3)
|
|
40
|
+
assert result == 6
|
tests/test_core.py
ADDED
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import time
|
|
3
|
+
|
|
4
|
+
from pyquerytracker import TrackQuery
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def test_tracking_output():
|
|
8
|
+
@TrackQuery()
|
|
9
|
+
def fake_db_query():
|
|
10
|
+
return "done"
|
|
11
|
+
|
|
12
|
+
assert fake_db_query() == "done"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
logging.basicConfig(
|
|
16
|
+
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s"
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def test_tracking_output_with_logging(caplog):
|
|
21
|
+
caplog.set_level("INFO")
|
|
22
|
+
|
|
23
|
+
@TrackQuery()
|
|
24
|
+
def fake_db_query():
|
|
25
|
+
return "done"
|
|
26
|
+
|
|
27
|
+
result = fake_db_query()
|
|
28
|
+
assert result == "done"
|
|
29
|
+
|
|
30
|
+
# Check the log records
|
|
31
|
+
assert len(caplog.records) == 1
|
|
32
|
+
record = caplog.records[0]
|
|
33
|
+
assert record.levelname == "INFO"
|
|
34
|
+
assert "Function fake_db_query executed successfully" in record.message
|
|
35
|
+
assert "ms" in record.message
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def test_tracking_output_with_error(caplog):
|
|
39
|
+
caplog.set_level("ERROR")
|
|
40
|
+
|
|
41
|
+
@TrackQuery()
|
|
42
|
+
def failing_query():
|
|
43
|
+
raise ValueError("Test error")
|
|
44
|
+
|
|
45
|
+
result = failing_query()
|
|
46
|
+
assert result is None
|
|
47
|
+
|
|
48
|
+
# Check the log records
|
|
49
|
+
assert len(caplog.records) == 1
|
|
50
|
+
record = caplog.records[0]
|
|
51
|
+
assert record.levelname == "ERROR"
|
|
52
|
+
assert "Function failing_query failed" in record.message
|
|
53
|
+
assert "Test error" in record.message
|
|
54
|
+
assert "ms" in record.message
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def test_tracking_with_class(caplog):
|
|
58
|
+
caplog.set_level("INFO")
|
|
59
|
+
|
|
60
|
+
class MyClass:
|
|
61
|
+
@TrackQuery()
|
|
62
|
+
def do_work(self, a, b):
|
|
63
|
+
time.sleep(0.09)
|
|
64
|
+
return a * b
|
|
65
|
+
|
|
66
|
+
MyClass().do_work(2, 3)
|
|
67
|
+
assert len(caplog.records) == 1
|
|
68
|
+
record = caplog.records[0]
|
|
69
|
+
assert record.levelname == "INFO"
|
|
70
|
+
assert "MyClass" in record.message
|
|
71
|
+
assert "do_work" in record.message
|
|
72
|
+
assert "ms" in record.message
|
tests/test_dashboard.py
ADDED
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from fastapi.testclient import TestClient
|
|
2
|
+
|
|
3
|
+
from pyquerytracker.api import app
|
|
4
|
+
from pyquerytracker.core import TrackQuery
|
|
5
|
+
|
|
6
|
+
client = TestClient(app)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
# Simulate query activity
|
|
10
|
+
@TrackQuery()
|
|
11
|
+
def sample_query():
|
|
12
|
+
return "ok"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def test_query_stats_endpoint():
|
|
16
|
+
# Trigger a few logs
|
|
17
|
+
for _ in range(3):
|
|
18
|
+
sample_query()
|
|
19
|
+
|
|
20
|
+
# Call the stats endpoint
|
|
21
|
+
response = client.get("/api/query-stats?minutes=5")
|
|
22
|
+
assert response.status_code == 200
|
|
23
|
+
|
|
24
|
+
json = response.json()
|
|
25
|
+
assert "labels" in json
|
|
26
|
+
assert "durations" in json
|
|
27
|
+
assert isinstance(json["labels"], list)
|
|
28
|
+
assert isinstance(json["durations"], list)
|
|
29
|
+
|
|
30
|
+
# Optional: Print results for debug
|
|
31
|
+
print("📊 Dashboard API response:", json)
|
tests/test_persist.py
ADDED
tests/test_websocket.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
|
|
3
|
+
from starlette.testclient import TestClient
|
|
4
|
+
|
|
5
|
+
from pyquerytracker.api import app
|
|
6
|
+
from pyquerytracker.websocket import (broadcast, connected_clients,
|
|
7
|
+
websocket_endpoint)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_websocket_connection():
|
|
11
|
+
client = TestClient(app)
|
|
12
|
+
with client.websocket_connect("/ws") as websocket:
|
|
13
|
+
websocket.send_text("ping") # No response expected, just test it works
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def test_broadcast_message_format():
|
|
17
|
+
class FakeWebSocket:
|
|
18
|
+
def __init__(self):
|
|
19
|
+
self.sent = []
|
|
20
|
+
|
|
21
|
+
async def send_text(self, msg):
|
|
22
|
+
self.sent.append(msg)
|
|
23
|
+
|
|
24
|
+
fake_ws = FakeWebSocket()
|
|
25
|
+
connected_clients.append(fake_ws)
|
|
26
|
+
|
|
27
|
+
asyncio.run(broadcast("hello"))
|
|
28
|
+
assert fake_ws.sent == ["hello"]
|
|
29
|
+
|
|
30
|
+
connected_clients.remove(fake_ws)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def test_connection_lifecycle():
|
|
34
|
+
class DummyWebSocket:
|
|
35
|
+
def __init__(self):
|
|
36
|
+
self.accepted = False
|
|
37
|
+
|
|
38
|
+
async def accept(self):
|
|
39
|
+
self.accepted = True
|
|
40
|
+
|
|
41
|
+
async def receive_text(self):
|
|
42
|
+
raise Exception("Simulated disconnect")
|
|
43
|
+
|
|
44
|
+
ws = DummyWebSocket()
|
|
45
|
+
connected_clients.clear()
|
|
46
|
+
try:
|
|
47
|
+
asyncio.run(websocket_endpoint(ws))
|
|
48
|
+
except Exception:
|
|
49
|
+
pass
|
|
50
|
+
assert ws not in connected_clients
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_broadcast_no_clients():
|
|
54
|
+
connected_clients.clear()
|
|
55
|
+
try:
|
|
56
|
+
asyncio.run(broadcast("no one here"))
|
|
57
|
+
except Exception:
|
|
58
|
+
assert False, "Broadcast failed when no clients connected"
|
|
@@ -1,8 +0,0 @@
|
|
|
1
|
-
pyquerytracker/__init__.py,sha256=pYyF2RPTTMhA0SakZTZ_dJ5dbPOIPYlacXJg7dSfsdc,98
|
|
2
|
-
pyquerytracker/config.py,sha256=GuMaHx4RB77FFad4bzZTQ-EyQYkDYHRrELHD_bTjAZo,1907
|
|
3
|
-
pyquerytracker/core.py,sha256=eLMfYs41ajLRjoxtxKjyi9G3NPN7tpwYkNerccL2oM4,3583
|
|
4
|
-
pyquerytracker-0.1.0.dist-info/licenses/LICENSE,sha256=lXcEFZRxovixBqp9SYJRLrN5OpP6AMggc_v7eaAMWn4,1065
|
|
5
|
-
pyquerytracker-0.1.0.dist-info/METADATA,sha256=RlPCS75bqssay-luqk6PuGlz_sb6NKAEKlcixWokI8E,2540
|
|
6
|
-
pyquerytracker-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
7
|
-
pyquerytracker-0.1.0.dist-info/top_level.txt,sha256=E5kxHgMifJIw-S2IjZqA1XuAU06Vbhd7geNfRZgjZuQ,15
|
|
8
|
-
pyquerytracker-0.1.0.dist-info/RECORD,,
|
|
File without changes
|