debug-agent-py 0.2.2__tar.gz → 0.3.0__tar.gz
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.
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/PKG-INFO +96 -5
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/README.md +81 -4
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/pyproject.toml +5 -1
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/__init__.py +6 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/celery.py +231 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/django.py +278 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/flask_ext.py +171 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/jinja2.py +214 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/redis.py +285 -0
- debug_agent_py-0.3.0/src/debug_agent/inspectors/signals.py +115 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/PKG-INFO +96 -5
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/SOURCES.txt +6 -0
- debug_agent_py-0.3.0/src/debug_agent_py.egg-info/requires.txt +36 -0
- debug_agent_py-0.2.2/src/debug_agent_py.egg-info/requires.txt +0 -18
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/setup.cfg +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/__init__.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/chat_session.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/config.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/context_compressor.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/engine.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/async_tasks.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/database.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/framework.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/http_tracker.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/memory.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/modules.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/runtime.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/system.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/inspectors/threads.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/llm_client.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/middleware.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/system_prompt_builder.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/tool_registry.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/web/__init__.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent/web/chat_page.py +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/dependency_links.txt +0 -0
- {debug_agent_py-0.2.2 → debug_agent_py-0.3.0}/src/debug_agent_py.egg-info/top_level.txt +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: debug-agent-py
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3.0
|
|
4
4
|
Summary: AI-powered runtime debugging agent for Python web applications
|
|
5
5
|
Author-email: ggcode <noreply@ggcode.dev>
|
|
6
6
|
License: MIT
|
|
@@ -29,6 +29,20 @@ Provides-Extra: flask
|
|
|
29
29
|
Requires-Dist: flask>=2.3; extra == "flask"
|
|
30
30
|
Provides-Extra: django
|
|
31
31
|
Requires-Dist: django>=4.2; extra == "django"
|
|
32
|
+
Provides-Extra: sqlalchemy
|
|
33
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "sqlalchemy"
|
|
34
|
+
Requires-Dist: flask-sqlalchemy>=3.0; extra == "sqlalchemy"
|
|
35
|
+
Provides-Extra: redis
|
|
36
|
+
Requires-Dist: redis>=5.0; extra == "redis"
|
|
37
|
+
Provides-Extra: celery
|
|
38
|
+
Requires-Dist: celery>=5.3; extra == "celery"
|
|
39
|
+
Provides-Extra: demo
|
|
40
|
+
Requires-Dist: flask>=2.3; extra == "demo"
|
|
41
|
+
Requires-Dist: sqlalchemy>=2.0; extra == "demo"
|
|
42
|
+
Requires-Dist: flask-sqlalchemy>=3.0; extra == "demo"
|
|
43
|
+
Requires-Dist: redis>=5.0; extra == "demo"
|
|
44
|
+
Requires-Dist: celery>=5.3; extra == "demo"
|
|
45
|
+
Requires-Dist: gunicorn>=21.2; extra == "demo"
|
|
32
46
|
Provides-Extra: dev
|
|
33
47
|
Requires-Dist: fastapi; extra == "dev"
|
|
34
48
|
Requires-Dist: uvicorn; extra == "dev"
|
|
@@ -37,7 +51,11 @@ Requires-Dist: pytest; extra == "dev"
|
|
|
37
51
|
|
|
38
52
|
# Python Debug Agent
|
|
39
53
|
|
|
40
|
-
|
|
54
|
+
[](https://pypi.org/project/debug-agent-py/)
|
|
55
|
+

|
|
56
|
+

|
|
57
|
+
|
|
58
|
+
An AI-powered runtime debugging agent that embeds directly into your Python web application. Add one dependency, configure an LLM key, and chat with your live app at `/agent` to inspect memory, threads, GC, modules, database connections, Redis, Django models/URLs, Celery tasks, Flask extensions, Jinja2 templates, signals, routes, HTTP requests, and more — **51 diagnostic tools across 16 inspectors**.
|
|
41
59
|
|
|
42
60
|
## Quick Start
|
|
43
61
|
|
|
@@ -81,10 +99,11 @@ http://localhost:8000/agent
|
|
|
81
99
|
- **Context compression** — automatically summarizes old conversation when token limit is approached
|
|
82
100
|
- **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
|
|
83
101
|
- **Max tool rounds** (25) with forced final summary when limit is reached
|
|
84
|
-
- **
|
|
102
|
+
- **51 diagnostic tools** across **16 inspectors**
|
|
85
103
|
- Works with Flask, FastAPI, and Django
|
|
104
|
+
- Zero external dependencies (no Datadog, no Grafana, no APM)
|
|
86
105
|
|
|
87
|
-
## Inspectors & Tools (
|
|
106
|
+
## Inspectors & Tools (51)
|
|
88
107
|
|
|
89
108
|
### Memory Inspector
|
|
90
109
|
| Tool | Description |
|
|
@@ -101,12 +120,14 @@ http://localhost:8000/agent
|
|
|
101
120
|
| `get_thread_info` | List all threads with name, daemon, alive status |
|
|
102
121
|
| `get_thread_count` | Active thread count |
|
|
103
122
|
| `get_thread_summary` | Thread state distribution |
|
|
123
|
+
| `get_thread_stacks` | Current frame/stack for all threads |
|
|
104
124
|
|
|
105
125
|
### Database Inspector
|
|
106
126
|
| Tool | Description |
|
|
107
127
|
|------|-------------|
|
|
108
128
|
| `get_sqlalchemy_engines` | Find SQLAlchemy engines and pool status |
|
|
109
129
|
| `get_db_connections` | Inspect database connection pools |
|
|
130
|
+
| `get_db_pool_config` | Pool configuration: size, timeout, recycle settings |
|
|
110
131
|
|
|
111
132
|
### Modules Inspector
|
|
112
133
|
| Tool | Description |
|
|
@@ -120,6 +141,7 @@ http://localhost:8000/agent
|
|
|
120
141
|
|------|-------------|
|
|
121
142
|
| `get_async_tasks` | List pending asyncio tasks |
|
|
122
143
|
| `get_event_loop_info` | Event loop details: type, running state |
|
|
144
|
+
| `get_pending_callbacks` | List scheduled callbacks on the event loop |
|
|
123
145
|
|
|
124
146
|
### Runtime Inspector
|
|
125
147
|
| Tool | Description |
|
|
@@ -127,13 +149,14 @@ http://localhost:8000/agent
|
|
|
127
149
|
| `get_memory_info` | Process memory info (RSS, VMS, shared) |
|
|
128
150
|
| `get_cpu_usage` | CPU usage percentage |
|
|
129
151
|
| `get_python_info` | Python version, implementation, executable path |
|
|
152
|
+
| `get_open_fds` | Open file descriptor count and limits |
|
|
130
153
|
|
|
131
154
|
### System Inspector
|
|
132
155
|
| Tool | Description |
|
|
133
156
|
|------|-------------|
|
|
134
157
|
| `get_system_info` | Hostname, platform, CPU cores, disk |
|
|
135
158
|
| `get_environment_variables` | Environment variables (masked secrets) |
|
|
136
|
-
| `get_disk_usage` | Disk usage for working directory |
|
|
159
|
+
| `get_disk_usage` | Disk usage for the working directory |
|
|
137
160
|
|
|
138
161
|
### Framework Inspector
|
|
139
162
|
| Tool | Description |
|
|
@@ -149,6 +172,54 @@ http://localhost:8000/agent
|
|
|
149
172
|
| `get_error_requests` | Error requests (4xx/5xx) |
|
|
150
173
|
| `get_request_stats` | P50/P95/P99 latency, error rate |
|
|
151
174
|
|
|
175
|
+
### Redis Inspector
|
|
176
|
+
| Tool | Description |
|
|
177
|
+
|------|-------------|
|
|
178
|
+
| `get_redis_info` | Redis server info: memory, clients, persistence |
|
|
179
|
+
| `get_redis_keys` | Scan Redis keyspace with pattern matching |
|
|
180
|
+
| `get_redis_config` | Redis runtime configuration (CONFIG GET) |
|
|
181
|
+
| `get_redis_slowlog` | Redis slow query log entries |
|
|
182
|
+
|
|
183
|
+
### Django Inspector
|
|
184
|
+
| Tool | Description |
|
|
185
|
+
|------|-------------|
|
|
186
|
+
| `get_django_models` | List Django models with app label, table name, field count |
|
|
187
|
+
| `get_django_urls` | List all URL patterns with view names and namespaces |
|
|
188
|
+
| `get_django_settings` | Key Django settings (DBs, INSTALLED_APPS, MIDDLEWARE) |
|
|
189
|
+
| `get_django_migrations` | Migration status per app: applied vs pending |
|
|
190
|
+
|
|
191
|
+
### Celery Inspector
|
|
192
|
+
| Tool | Description |
|
|
193
|
+
|------|-------------|
|
|
194
|
+
| `get_celery_tasks` | List registered Celery tasks with routing info |
|
|
195
|
+
| `get_celery_workers` | Active Celery workers with pool and concurrency |
|
|
196
|
+
| `get_celery_queues` | Queue depth and message stats per queue |
|
|
197
|
+
|
|
198
|
+
### Flask Extensions Inspector
|
|
199
|
+
| Tool | Description |
|
|
200
|
+
|------|-------------|
|
|
201
|
+
| `get_flask_extensions` | List registered Flask extensions and their bindings |
|
|
202
|
+
| `get_flask_blueprints` | List Flask blueprints with URL prefixes and routes |
|
|
203
|
+
| `get_flask_config` | Flask configuration object values (secrets masked) |
|
|
204
|
+
|
|
205
|
+
### Jinja2 Inspector
|
|
206
|
+
| Tool | Description |
|
|
207
|
+
|------|-------------|
|
|
208
|
+
| `get_jinja_templates` | List loaded Jinja2 templates with loader paths |
|
|
209
|
+
| `get_jinja_filters` | List registered Jinja2 filters, tests, and globals |
|
|
210
|
+
|
|
211
|
+
### Signals Inspector
|
|
212
|
+
| Tool | Description |
|
|
213
|
+
|------|-------------|
|
|
214
|
+
| `get_signal_handlers` | List Python signal handlers registered via signal module |
|
|
215
|
+
| `get_django_signals` | List Django signal receivers connected to senders |
|
|
216
|
+
|
|
217
|
+
### WSGI/ASGI Inspector
|
|
218
|
+
| Tool | Description |
|
|
219
|
+
|------|-------------|
|
|
220
|
+
| `get_wsgi_info` | WSGI server details (Gunicorn/uWSGI workers, config) |
|
|
221
|
+
| `get_asgi_apps` | List ASGI application scope and middleware chain |
|
|
222
|
+
|
|
152
223
|
## Custom Tools
|
|
153
224
|
|
|
154
225
|
```python
|
|
@@ -171,6 +242,26 @@ def check_redis():
|
|
|
171
242
|
|
|
172
243
|
## Run the Demo
|
|
173
244
|
|
|
245
|
+
The demo uses **Flask** + **redis-py** + **SQLAlchemy** + **Celery**. Start Redis with Docker Compose first:
|
|
246
|
+
|
|
247
|
+
### Docker Compose
|
|
248
|
+
|
|
249
|
+
```yaml
|
|
250
|
+
# docker-compose.yml
|
|
251
|
+
services:
|
|
252
|
+
redis:
|
|
253
|
+
image: redis:7-alpine
|
|
254
|
+
ports:
|
|
255
|
+
- "6379:6379"
|
|
256
|
+
command: redis-server --save 60 1 --loglevel warning
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
```bash
|
|
260
|
+
docker compose up -d
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Start the app
|
|
264
|
+
|
|
174
265
|
```bash
|
|
175
266
|
export LLM_API_KEY=your-key
|
|
176
267
|
cd demo && python app.py
|
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# Python Debug Agent
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://pypi.org/project/debug-agent-py/)
|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
An AI-powered runtime debugging agent that embeds directly into your Python web application. Add one dependency, configure an LLM key, and chat with your live app at `/agent` to inspect memory, threads, GC, modules, database connections, Redis, Django models/URLs, Celery tasks, Flask extensions, Jinja2 templates, signals, routes, HTTP requests, and more — **51 diagnostic tools across 16 inspectors**.
|
|
4
8
|
|
|
5
9
|
## Quick Start
|
|
6
10
|
|
|
@@ -44,10 +48,11 @@ http://localhost:8000/agent
|
|
|
44
48
|
- **Context compression** — automatically summarizes old conversation when token limit is approached
|
|
45
49
|
- **Dark-themed chat UI** with full markdown rendering (tables, code blocks, lists)
|
|
46
50
|
- **Max tool rounds** (25) with forced final summary when limit is reached
|
|
47
|
-
- **
|
|
51
|
+
- **51 diagnostic tools** across **16 inspectors**
|
|
48
52
|
- Works with Flask, FastAPI, and Django
|
|
53
|
+
- Zero external dependencies (no Datadog, no Grafana, no APM)
|
|
49
54
|
|
|
50
|
-
## Inspectors & Tools (
|
|
55
|
+
## Inspectors & Tools (51)
|
|
51
56
|
|
|
52
57
|
### Memory Inspector
|
|
53
58
|
| Tool | Description |
|
|
@@ -64,12 +69,14 @@ http://localhost:8000/agent
|
|
|
64
69
|
| `get_thread_info` | List all threads with name, daemon, alive status |
|
|
65
70
|
| `get_thread_count` | Active thread count |
|
|
66
71
|
| `get_thread_summary` | Thread state distribution |
|
|
72
|
+
| `get_thread_stacks` | Current frame/stack for all threads |
|
|
67
73
|
|
|
68
74
|
### Database Inspector
|
|
69
75
|
| Tool | Description |
|
|
70
76
|
|------|-------------|
|
|
71
77
|
| `get_sqlalchemy_engines` | Find SQLAlchemy engines and pool status |
|
|
72
78
|
| `get_db_connections` | Inspect database connection pools |
|
|
79
|
+
| `get_db_pool_config` | Pool configuration: size, timeout, recycle settings |
|
|
73
80
|
|
|
74
81
|
### Modules Inspector
|
|
75
82
|
| Tool | Description |
|
|
@@ -83,6 +90,7 @@ http://localhost:8000/agent
|
|
|
83
90
|
|------|-------------|
|
|
84
91
|
| `get_async_tasks` | List pending asyncio tasks |
|
|
85
92
|
| `get_event_loop_info` | Event loop details: type, running state |
|
|
93
|
+
| `get_pending_callbacks` | List scheduled callbacks on the event loop |
|
|
86
94
|
|
|
87
95
|
### Runtime Inspector
|
|
88
96
|
| Tool | Description |
|
|
@@ -90,13 +98,14 @@ http://localhost:8000/agent
|
|
|
90
98
|
| `get_memory_info` | Process memory info (RSS, VMS, shared) |
|
|
91
99
|
| `get_cpu_usage` | CPU usage percentage |
|
|
92
100
|
| `get_python_info` | Python version, implementation, executable path |
|
|
101
|
+
| `get_open_fds` | Open file descriptor count and limits |
|
|
93
102
|
|
|
94
103
|
### System Inspector
|
|
95
104
|
| Tool | Description |
|
|
96
105
|
|------|-------------|
|
|
97
106
|
| `get_system_info` | Hostname, platform, CPU cores, disk |
|
|
98
107
|
| `get_environment_variables` | Environment variables (masked secrets) |
|
|
99
|
-
| `get_disk_usage` | Disk usage for working directory |
|
|
108
|
+
| `get_disk_usage` | Disk usage for the working directory |
|
|
100
109
|
|
|
101
110
|
### Framework Inspector
|
|
102
111
|
| Tool | Description |
|
|
@@ -112,6 +121,54 @@ http://localhost:8000/agent
|
|
|
112
121
|
| `get_error_requests` | Error requests (4xx/5xx) |
|
|
113
122
|
| `get_request_stats` | P50/P95/P99 latency, error rate |
|
|
114
123
|
|
|
124
|
+
### Redis Inspector
|
|
125
|
+
| Tool | Description |
|
|
126
|
+
|------|-------------|
|
|
127
|
+
| `get_redis_info` | Redis server info: memory, clients, persistence |
|
|
128
|
+
| `get_redis_keys` | Scan Redis keyspace with pattern matching |
|
|
129
|
+
| `get_redis_config` | Redis runtime configuration (CONFIG GET) |
|
|
130
|
+
| `get_redis_slowlog` | Redis slow query log entries |
|
|
131
|
+
|
|
132
|
+
### Django Inspector
|
|
133
|
+
| Tool | Description |
|
|
134
|
+
|------|-------------|
|
|
135
|
+
| `get_django_models` | List Django models with app label, table name, field count |
|
|
136
|
+
| `get_django_urls` | List all URL patterns with view names and namespaces |
|
|
137
|
+
| `get_django_settings` | Key Django settings (DBs, INSTALLED_APPS, MIDDLEWARE) |
|
|
138
|
+
| `get_django_migrations` | Migration status per app: applied vs pending |
|
|
139
|
+
|
|
140
|
+
### Celery Inspector
|
|
141
|
+
| Tool | Description |
|
|
142
|
+
|------|-------------|
|
|
143
|
+
| `get_celery_tasks` | List registered Celery tasks with routing info |
|
|
144
|
+
| `get_celery_workers` | Active Celery workers with pool and concurrency |
|
|
145
|
+
| `get_celery_queues` | Queue depth and message stats per queue |
|
|
146
|
+
|
|
147
|
+
### Flask Extensions Inspector
|
|
148
|
+
| Tool | Description |
|
|
149
|
+
|------|-------------|
|
|
150
|
+
| `get_flask_extensions` | List registered Flask extensions and their bindings |
|
|
151
|
+
| `get_flask_blueprints` | List Flask blueprints with URL prefixes and routes |
|
|
152
|
+
| `get_flask_config` | Flask configuration object values (secrets masked) |
|
|
153
|
+
|
|
154
|
+
### Jinja2 Inspector
|
|
155
|
+
| Tool | Description |
|
|
156
|
+
|------|-------------|
|
|
157
|
+
| `get_jinja_templates` | List loaded Jinja2 templates with loader paths |
|
|
158
|
+
| `get_jinja_filters` | List registered Jinja2 filters, tests, and globals |
|
|
159
|
+
|
|
160
|
+
### Signals Inspector
|
|
161
|
+
| Tool | Description |
|
|
162
|
+
|------|-------------|
|
|
163
|
+
| `get_signal_handlers` | List Python signal handlers registered via signal module |
|
|
164
|
+
| `get_django_signals` | List Django signal receivers connected to senders |
|
|
165
|
+
|
|
166
|
+
### WSGI/ASGI Inspector
|
|
167
|
+
| Tool | Description |
|
|
168
|
+
|------|-------------|
|
|
169
|
+
| `get_wsgi_info` | WSGI server details (Gunicorn/uWSGI workers, config) |
|
|
170
|
+
| `get_asgi_apps` | List ASGI application scope and middleware chain |
|
|
171
|
+
|
|
115
172
|
## Custom Tools
|
|
116
173
|
|
|
117
174
|
```python
|
|
@@ -134,6 +191,26 @@ def check_redis():
|
|
|
134
191
|
|
|
135
192
|
## Run the Demo
|
|
136
193
|
|
|
194
|
+
The demo uses **Flask** + **redis-py** + **SQLAlchemy** + **Celery**. Start Redis with Docker Compose first:
|
|
195
|
+
|
|
196
|
+
### Docker Compose
|
|
197
|
+
|
|
198
|
+
```yaml
|
|
199
|
+
# docker-compose.yml
|
|
200
|
+
services:
|
|
201
|
+
redis:
|
|
202
|
+
image: redis:7-alpine
|
|
203
|
+
ports:
|
|
204
|
+
- "6379:6379"
|
|
205
|
+
command: redis-server --save 60 1 --loglevel warning
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
```bash
|
|
209
|
+
docker compose up -d
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
### Start the app
|
|
213
|
+
|
|
137
214
|
```bash
|
|
138
215
|
export LLM_API_KEY=your-key
|
|
139
216
|
cd demo && python app.py
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "debug-agent-py"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.3.0"
|
|
8
8
|
description = "AI-powered runtime debugging agent for Python web applications"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = {text = "MIT"}
|
|
@@ -33,6 +33,10 @@ dependencies = [
|
|
|
33
33
|
fastapi = ["fastapi>=0.100", "starlette>=0.27", "uvicorn>=0.23"]
|
|
34
34
|
flask = ["flask>=2.3"]
|
|
35
35
|
django = ["django>=4.2"]
|
|
36
|
+
sqlalchemy = ["sqlalchemy>=2.0", "flask-sqlalchemy>=3.0"]
|
|
37
|
+
redis = ["redis>=5.0"]
|
|
38
|
+
celery = ["celery>=5.3"]
|
|
39
|
+
demo = ["flask>=2.3", "sqlalchemy>=2.0", "flask-sqlalchemy>=3.0", "redis>=5.0", "celery>=5.3", "gunicorn>=21.2"]
|
|
36
40
|
dev = ["fastapi", "uvicorn", "flask", "pytest"]
|
|
37
41
|
|
|
38
42
|
[project.urls]
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"""Celery inspector: registered tasks, queue stats, and worker stats.
|
|
2
|
+
|
|
3
|
+
Register a Celery app at runtime so the inspectors can reach it:
|
|
4
|
+
|
|
5
|
+
from debug_agent.inspectors.celery import register_celery_app
|
|
6
|
+
register_celery_app("default", celery_app)
|
|
7
|
+
|
|
8
|
+
All tools degrade gracefully when Celery is not installed or no app is
|
|
9
|
+
registered. Inspector calls (queue/worker stats) require a running broker.
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
from __future__ import annotations
|
|
13
|
+
|
|
14
|
+
from typing import Any
|
|
15
|
+
|
|
16
|
+
from debug_agent.tool_registry import debug_tool, ToolParam
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# ─── Registration mechanism ──────────────────────────────────────────────────
|
|
20
|
+
|
|
21
|
+
_registered_celery_apps: dict[str, Any] = {}
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def register_celery_app(name: str, app: Any) -> None:
|
|
25
|
+
"""Register a Celery app under a name so the inspectors can find it."""
|
|
26
|
+
_registered_celery_apps[name] = app
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _celery_available() -> bool:
|
|
30
|
+
try:
|
|
31
|
+
import celery # noqa: F401
|
|
32
|
+
return True
|
|
33
|
+
except ImportError:
|
|
34
|
+
return False
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def _ensure_apps() -> dict[str, Any] | None:
|
|
38
|
+
if not _celery_available():
|
|
39
|
+
return None
|
|
40
|
+
return _registered_celery_apps
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
# ─── Tools ───────────────────────────────────────────────────────────────────
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
@debug_tool(
|
|
47
|
+
"get_celery_tasks",
|
|
48
|
+
"List registered Celery tasks (name, module, bound) for each registered Celery app",
|
|
49
|
+
)
|
|
50
|
+
def get_celery_tasks() -> dict:
|
|
51
|
+
apps = _ensure_apps()
|
|
52
|
+
if apps is None:
|
|
53
|
+
return {"error": "Celery is not installed"}
|
|
54
|
+
|
|
55
|
+
if not apps:
|
|
56
|
+
return {
|
|
57
|
+
"message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
|
|
58
|
+
"registered_count": 0,
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
result = []
|
|
62
|
+
for name, app in apps.items():
|
|
63
|
+
tasks: list[dict[str, Any]] = []
|
|
64
|
+
try:
|
|
65
|
+
task_iter = app.tasks.items()
|
|
66
|
+
except Exception as exc:
|
|
67
|
+
result.append({"name": name, "error": f"Could not enumerate tasks: {exc}"})
|
|
68
|
+
continue
|
|
69
|
+
|
|
70
|
+
for task_name, task in task_iter:
|
|
71
|
+
# Skip Celery's own internal tasks (celery.*) unless the user cares.
|
|
72
|
+
if task_name.startswith("celery."):
|
|
73
|
+
continue
|
|
74
|
+
tasks.append({
|
|
75
|
+
"name": task_name,
|
|
76
|
+
"module": getattr(getattr(task, "run", None), "__module__", None)
|
|
77
|
+
or getattr(task, "__module__", None),
|
|
78
|
+
"bound": bool(getattr(task, "bound", False)),
|
|
79
|
+
})
|
|
80
|
+
|
|
81
|
+
result.append({
|
|
82
|
+
"name": name,
|
|
83
|
+
"task_count": len(tasks),
|
|
84
|
+
"tasks": tasks,
|
|
85
|
+
})
|
|
86
|
+
|
|
87
|
+
return {"registered_count": len(apps), "apps": result}
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@debug_tool(
|
|
91
|
+
"get_celery_queues",
|
|
92
|
+
"Get Celery queue stats (active, reserved, scheduled counts per queue) via the inspector",
|
|
93
|
+
)
|
|
94
|
+
def get_celery_queues() -> dict:
|
|
95
|
+
apps = _ensure_apps()
|
|
96
|
+
if apps is None:
|
|
97
|
+
return {"error": "Celery is not installed"}
|
|
98
|
+
|
|
99
|
+
if not apps:
|
|
100
|
+
return {
|
|
101
|
+
"message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
|
|
102
|
+
"registered_count": 0,
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
result = []
|
|
106
|
+
for name, app in apps.items():
|
|
107
|
+
try:
|
|
108
|
+
inspect = app.control.inspect()
|
|
109
|
+
except Exception as exc:
|
|
110
|
+
result.append({"name": name, "error": f"Could not create inspector: {exc}"})
|
|
111
|
+
continue
|
|
112
|
+
|
|
113
|
+
active = _safe_inspect(inspect.active)
|
|
114
|
+
reserved = _safe_inspect(inspect.reserved)
|
|
115
|
+
scheduled = _safe_inspect(inspect.scheduled)
|
|
116
|
+
|
|
117
|
+
per_queue: dict[str, dict[str, int]] = {}
|
|
118
|
+
for payload_name, payload in (
|
|
119
|
+
("active", active),
|
|
120
|
+
("reserved", reserved),
|
|
121
|
+
("scheduled", scheduled),
|
|
122
|
+
):
|
|
123
|
+
if not isinstance(payload, dict):
|
|
124
|
+
continue
|
|
125
|
+
for _worker, entries in payload.items():
|
|
126
|
+
for entry in entries or []:
|
|
127
|
+
delivery = entry.get("delivery") if isinstance(entry, dict) else None
|
|
128
|
+
queue = None
|
|
129
|
+
if isinstance(delivery, dict):
|
|
130
|
+
queue = delivery.get("routing_key") or delivery.get("queue")
|
|
131
|
+
queue = queue or "default"
|
|
132
|
+
bucket = per_queue.setdefault(queue, {"active": 0, "reserved": 0, "scheduled": 0})
|
|
133
|
+
bucket[payload_name] += 1
|
|
134
|
+
|
|
135
|
+
totals = {
|
|
136
|
+
"active": sum(v["active"] for v in per_queue.values()),
|
|
137
|
+
"reserved": sum(v["reserved"] for v in per_queue.values()),
|
|
138
|
+
"scheduled": sum(v["scheduled"] for v in per_queue.values()),
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
result.append({
|
|
142
|
+
"name": name,
|
|
143
|
+
"queue_count": len(per_queue),
|
|
144
|
+
"totals": totals,
|
|
145
|
+
"queues": per_queue,
|
|
146
|
+
})
|
|
147
|
+
|
|
148
|
+
return {"registered_count": len(apps), "apps": result}
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def _safe_inspect(func) -> Any:
|
|
152
|
+
"""Call a celery inspect method, returning an error dict on failure."""
|
|
153
|
+
try:
|
|
154
|
+
return func()
|
|
155
|
+
except Exception as exc:
|
|
156
|
+
return {"_error": str(exc)}
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
@debug_tool(
|
|
160
|
+
"get_celery_workers",
|
|
161
|
+
"Get Celery worker stats (active, processed, pool info) via the inspector",
|
|
162
|
+
)
|
|
163
|
+
def get_celery_workers() -> dict:
|
|
164
|
+
apps = _ensure_apps()
|
|
165
|
+
if apps is None:
|
|
166
|
+
return {"error": "Celery is not installed"}
|
|
167
|
+
|
|
168
|
+
if not apps:
|
|
169
|
+
return {
|
|
170
|
+
"message": "No Celery apps registered. Call register_celery_app(name, app) to register one.",
|
|
171
|
+
"registered_count": 0,
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
result = []
|
|
175
|
+
for name, app in apps.items():
|
|
176
|
+
try:
|
|
177
|
+
inspect = app.control.inspect()
|
|
178
|
+
except Exception as exc:
|
|
179
|
+
result.append({"name": name, "error": f"Could not create inspector: {exc}"})
|
|
180
|
+
continue
|
|
181
|
+
|
|
182
|
+
stats = _safe_inspect(inspect.stats)
|
|
183
|
+
active = _safe_inspect(inspect.active)
|
|
184
|
+
ping = _safe_inspect(inspect.ping)
|
|
185
|
+
|
|
186
|
+
workers: list[dict[str, Any]] = []
|
|
187
|
+
if isinstance(stats, dict):
|
|
188
|
+
for worker_name, worker_stats in stats.items():
|
|
189
|
+
pool = worker_stats.get("pool", {}) if isinstance(worker_stats, dict) else {}
|
|
190
|
+
broker = worker_stats.get("broker", {}) if isinstance(worker_stats, dict) else {}
|
|
191
|
+
rusage = worker_stats.get("rusage", {}) if isinstance(worker_stats, dict) else {}
|
|
192
|
+
active_list = active.get(worker_name, []) if isinstance(active, dict) else []
|
|
193
|
+
total_processed = 0
|
|
194
|
+
if isinstance(worker_stats, dict):
|
|
195
|
+
total_processed = sum(
|
|
196
|
+
int(v) for v in (worker_stats.get("total", {}) or {}).values()
|
|
197
|
+
)
|
|
198
|
+
workers.append({
|
|
199
|
+
"worker": worker_name,
|
|
200
|
+
"active_count": len(active_list) if isinstance(active_list, list) else 0,
|
|
201
|
+
"processed_total": total_processed,
|
|
202
|
+
"pool": {
|
|
203
|
+
"implementation": pool.get("implementation") if isinstance(pool, dict) else None,
|
|
204
|
+
"max_concurrency": pool.get("max-concurrency") if isinstance(pool, dict) else None,
|
|
205
|
+
"processes": pool.get("processes") if isinstance(pool, dict) else None,
|
|
206
|
+
},
|
|
207
|
+
"broker": {
|
|
208
|
+
"transport": broker.get("transport") if isinstance(broker, dict) else None,
|
|
209
|
+
"name": broker.get("name") if isinstance(broker, dict) else None,
|
|
210
|
+
},
|
|
211
|
+
"uptime": worker_stats.get("uptime") if isinstance(worker_stats, dict) else None,
|
|
212
|
+
"rusage": _summarise_rusage(rusage),
|
|
213
|
+
})
|
|
214
|
+
|
|
215
|
+
reachable = list(ping.keys()) if isinstance(ping, dict) else []
|
|
216
|
+
|
|
217
|
+
result.append({
|
|
218
|
+
"name": name,
|
|
219
|
+
"worker_count": len(workers),
|
|
220
|
+
"reachable_workers": reachable,
|
|
221
|
+
"workers": workers,
|
|
222
|
+
})
|
|
223
|
+
|
|
224
|
+
return {"registered_count": len(apps), "apps": result}
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def _summarise_rusage(rusage: Any) -> dict[str, Any]:
|
|
228
|
+
if not isinstance(rusage, dict):
|
|
229
|
+
return {}
|
|
230
|
+
keys = ("utime", "stime", "maxrss", "idrss", "isrss", "ixrss", "minflt", "majflt", "nvcsw", "nivcsw")
|
|
231
|
+
return {k: rusage.get(k) for k in keys if k in rusage}
|