queue-aiops 0.1.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.
Files changed (48) hide show
  1. mcp_server/__init__.py +1 -0
  2. mcp_server/_shared.py +107 -0
  3. mcp_server/server.py +38 -0
  4. mcp_server/tools/__init__.py +1 -0
  5. mcp_server/tools/analysis.py +148 -0
  6. mcp_server/tools/overview.py +23 -0
  7. mcp_server/tools/rabbit_reads.py +95 -0
  8. mcp_server/tools/redis_reads.py +98 -0
  9. mcp_server/tools/undo.py +121 -0
  10. mcp_server/tools/writes.py +355 -0
  11. queue_aiops/__init__.py +14 -0
  12. queue_aiops/cli/__init__.py +9 -0
  13. queue_aiops/cli/_common.py +78 -0
  14. queue_aiops/cli/_root.py +62 -0
  15. queue_aiops/cli/analyze.py +85 -0
  16. queue_aiops/cli/doctor.py +21 -0
  17. queue_aiops/cli/init.py +176 -0
  18. queue_aiops/cli/overview.py +16 -0
  19. queue_aiops/cli/rabbit_cmds.py +234 -0
  20. queue_aiops/cli/redis_cmds.py +147 -0
  21. queue_aiops/cli/secret.py +105 -0
  22. queue_aiops/cli/undo.py +62 -0
  23. queue_aiops/config.py +175 -0
  24. queue_aiops/connection.py +277 -0
  25. queue_aiops/doctor.py +98 -0
  26. queue_aiops/governance/__init__.py +40 -0
  27. queue_aiops/governance/audit.py +377 -0
  28. queue_aiops/governance/budget.py +225 -0
  29. queue_aiops/governance/decorators.py +482 -0
  30. queue_aiops/governance/paths.py +23 -0
  31. queue_aiops/governance/patterns.py +378 -0
  32. queue_aiops/governance/policy.py +430 -0
  33. queue_aiops/governance/sanitize.py +45 -0
  34. queue_aiops/governance/undo.py +218 -0
  35. queue_aiops/ops/__init__.py +1 -0
  36. queue_aiops/ops/_util.py +55 -0
  37. queue_aiops/ops/analysis.py +591 -0
  38. queue_aiops/ops/overview.py +90 -0
  39. queue_aiops/ops/rabbit_reads.py +218 -0
  40. queue_aiops/ops/redis_reads.py +237 -0
  41. queue_aiops/ops/writes.py +284 -0
  42. queue_aiops/platform.py +217 -0
  43. queue_aiops/secretstore.py +304 -0
  44. queue_aiops-0.1.0.dist-info/METADATA +168 -0
  45. queue_aiops-0.1.0.dist-info/RECORD +48 -0
  46. queue_aiops-0.1.0.dist-info/WHEEL +4 -0
  47. queue_aiops-0.1.0.dist-info/entry_points.txt +3 -0
  48. queue_aiops-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,355 @@
1
+ """Governed broker-write MCP tools (the only state-changing tools).
2
+
3
+ Every tool is wrapped with the governance harness (audit + graduated approval
4
+ tier) and takes a ``dry_run`` preview. Reversible writes pass an ``undo=``
5
+ callback that turns the fetched before-state into an inverse descriptor the
6
+ harness records; irreversible ones (kill_client, purge_queue) record only the
7
+ priorState in the result.
8
+
9
+ Risk tiers: purge_queue / delete_queue = high (destroy messages / a queue);
10
+ redis_config_set / redis_kill_client / declare_queue / set_policy /
11
+ delete_policy = medium.
12
+ """
13
+
14
+ from typing import Any, Optional
15
+
16
+ from mcp_server._shared import _get_connection, mcp, tool_errors
17
+ from queue_aiops.governance import governed_tool
18
+ from queue_aiops.ops import writes as ops
19
+
20
+ # ── undo descriptors (built from the fetched before-state) ──────────────────
21
+
22
+
23
+ def _config_set_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
24
+ """Inverse of redis_config_set: set the parameter back to its prior value."""
25
+ if not isinstance(result, dict):
26
+ return None
27
+ prior = (result.get("priorState") or {}).get("value")
28
+ if prior is None:
29
+ return None
30
+ return {
31
+ "tool": "redis_config_set",
32
+ "params": {"parameter": params.get("parameter"), "value": prior},
33
+ "skill": "queue-aiops",
34
+ "note": "Inverse of redis_config_set: restore the parameter's prior value.",
35
+ }
36
+
37
+
38
+ def _delete_queue_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
39
+ """Inverse of delete_queue: re-declare the queue with the captured definition.
40
+
41
+ The queue's *messages* are gone — this restores the definition only, and the
42
+ note says so.
43
+ """
44
+ if not isinstance(result, dict):
45
+ return None
46
+ prior = result.get("priorState") or {}
47
+ return {
48
+ "tool": "declare_queue",
49
+ "params": {
50
+ "vhost": params.get("vhost"),
51
+ "name": params.get("name"),
52
+ "durable": bool(prior.get("durable", True)),
53
+ "auto_delete": bool(prior.get("autoDelete", False)),
54
+ "arguments": prior.get("arguments") or {},
55
+ },
56
+ "skill": "queue-aiops",
57
+ "note": "Inverse of delete_queue: re-declare the captured definition "
58
+ "(messages are NOT restored).",
59
+ }
60
+
61
+
62
+ def _declare_queue_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
63
+ """Inverse of declare_queue: delete the queue — only when it was newly created."""
64
+ if not isinstance(result, dict):
65
+ return None
66
+ if (result.get("priorState") or {}).get("existed"):
67
+ return None # the queue predates this call; deleting it would not be an undo
68
+ return {
69
+ "tool": "delete_queue",
70
+ "params": {"vhost": params.get("vhost"), "name": params.get("name")},
71
+ "skill": "queue-aiops",
72
+ "note": "Inverse of declare_queue: delete the queue that was created.",
73
+ }
74
+
75
+
76
+ def _set_policy_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
77
+ """Inverse of set_policy: restore the prior policy, or delete it if it is new."""
78
+ if not isinstance(result, dict):
79
+ return None
80
+ prior_state = result.get("priorState") or {}
81
+ prior = prior_state.get("policy")
82
+ if prior_state.get("existed") and isinstance(prior, dict):
83
+ return {
84
+ "tool": "set_policy",
85
+ "params": {
86
+ "vhost": params.get("vhost"),
87
+ "name": params.get("name"),
88
+ "pattern": prior.get("pattern"),
89
+ "definition": prior.get("definition") or {},
90
+ "priority": int(prior.get("priority") or 0),
91
+ "apply_to": prior.get("applyTo") or "all",
92
+ },
93
+ "skill": "queue-aiops",
94
+ "note": "Inverse of set_policy: restore the prior policy fields.",
95
+ }
96
+ return {
97
+ "tool": "delete_policy",
98
+ "params": {"vhost": params.get("vhost"), "name": params.get("name")},
99
+ "skill": "queue-aiops",
100
+ "note": "Inverse of set_policy: delete the policy that was newly created.",
101
+ }
102
+
103
+
104
+ def _delete_policy_undo(params: dict[str, Any], result: Any) -> Optional[dict]:
105
+ """Inverse of delete_policy: re-create the captured policy."""
106
+ if not isinstance(result, dict):
107
+ return None
108
+ prior = (result.get("priorState") or {}).get("policy")
109
+ if not isinstance(prior, dict):
110
+ return None
111
+ return {
112
+ "tool": "set_policy",
113
+ "params": {
114
+ "vhost": params.get("vhost"),
115
+ "name": params.get("name"),
116
+ "pattern": prior.get("pattern"),
117
+ "definition": prior.get("definition") or {},
118
+ "priority": int(prior.get("priority") or 0),
119
+ "apply_to": prior.get("applyTo") or "all",
120
+ },
121
+ "skill": "queue-aiops",
122
+ "note": "Inverse of delete_policy: re-create the captured policy.",
123
+ }
124
+
125
+
126
+ # ── redis writes ─────────────────────────────────────────────────────────────
127
+
128
+
129
+ @mcp.tool()
130
+ @governed_tool(risk_level="medium", undo=_config_set_undo)
131
+ @tool_errors("dict")
132
+ def redis_config_set(
133
+ parameter: str,
134
+ value: str,
135
+ dry_run: bool = False,
136
+ target: Optional[str] = None,
137
+ ) -> dict:
138
+ """[WRITE][risk=medium] Set one redis config parameter; reversible.
139
+
140
+ Reads the parameter first (CONFIG GET) so the harness records an undo that
141
+ restores its prior value. Runtime-only — not persisted to the config file.
142
+ Pass dry_run=True to preview.
143
+
144
+ Args:
145
+ parameter: Config parameter name (e.g. maxmemory-policy), from redis_config_get.
146
+ value: New value.
147
+ dry_run: If True, preview without changing.
148
+ target: redis target name from config; omit for the default.
149
+ """
150
+ conn = _get_connection(target)
151
+ if dry_run:
152
+ return {"dryRun": True, "wouldSet": {"parameter": parameter, "value": value}}
153
+ return ops.config_set(conn, parameter, value)
154
+
155
+
156
+ @mcp.tool()
157
+ @governed_tool(risk_level="medium")
158
+ @tool_errors("dict")
159
+ def redis_kill_client(
160
+ client_id: int = 0,
161
+ addr: str = "",
162
+ dry_run: bool = False,
163
+ target: Optional[str] = None,
164
+ ) -> dict:
165
+ """[WRITE][risk=medium] Disconnect one redis client by id or addr. IRREVERSIBLE.
166
+
167
+ Captures the client's CLIENT LIST row as priorState (who/where/last
168
+ command) before the kill — the connection cannot be restored, so no undo
169
+ is recorded; most clients transparently reconnect. Pass dry_run=True to
170
+ preview.
171
+
172
+ Args:
173
+ client_id: Client id from redis_clients (preferred).
174
+ addr: Or the client's 'ip:port' address.
175
+ dry_run: If True, preview without disconnecting.
176
+ target: redis target name from config; omit for the default.
177
+ """
178
+ conn = _get_connection(target)
179
+ if dry_run:
180
+ return {"dryRun": True, "wouldKill": {"clientId": client_id, "addr": addr}}
181
+ return ops.kill_client(conn, client_id=client_id, addr=addr)
182
+
183
+
184
+ # ── rabbitmq queue writes ────────────────────────────────────────────────────
185
+
186
+
187
+ @mcp.tool()
188
+ @governed_tool(risk_level="high")
189
+ @tool_errors("dict")
190
+ def purge_queue(
191
+ vhost: str,
192
+ name: str,
193
+ dry_run: bool = False,
194
+ target: Optional[str] = None,
195
+ ) -> dict:
196
+ """[WRITE][risk=high] Purge all ready messages from a rabbitmq queue. IRREVERSIBLE.
197
+
198
+ Reads the queue first so priorState carries the message count about to be
199
+ destroyed (audit evidence); unacked messages are not purged. No undo —
200
+ purged messages cannot be restored. Requires an approver
201
+ (QUEUE_AUDIT_APPROVED_BY) under the graduated-autonomy policy. Pass
202
+ dry_run=True to preview.
203
+
204
+ Args:
205
+ vhost: The queue's vhost (the default vhost is '/').
206
+ name: Queue name (from list_queues).
207
+ dry_run: If True, preview without purging.
208
+ target: rabbitmq target name from config; omit for the default.
209
+ """
210
+ conn = _get_connection(target)
211
+ if dry_run:
212
+ return {"dryRun": True, "wouldPurge": {"vhost": vhost, "queue": name}}
213
+ return ops.purge_queue(conn, vhost, name)
214
+
215
+
216
+ @mcp.tool()
217
+ @governed_tool(risk_level="high", undo=_delete_queue_undo)
218
+ @tool_errors("dict")
219
+ def delete_queue(
220
+ vhost: str,
221
+ name: str,
222
+ dry_run: bool = False,
223
+ target: Optional[str] = None,
224
+ ) -> dict:
225
+ """[WRITE][risk=high] Delete a rabbitmq queue; the undo re-declares its definition.
226
+
227
+ Reads the queue first so priorState carries the definition (durable /
228
+ auto_delete / arguments) and message count; the recorded undo re-declares
229
+ the queue with those properties — its messages are NOT restored. Requires
230
+ an approver (QUEUE_AUDIT_APPROVED_BY) under the graduated-autonomy policy.
231
+ Pass dry_run=True to preview.
232
+
233
+ Args:
234
+ vhost: The queue's vhost (the default vhost is '/').
235
+ name: Queue name (from list_queues).
236
+ dry_run: If True, preview without deleting.
237
+ target: rabbitmq target name from config; omit for the default.
238
+ """
239
+ conn = _get_connection(target)
240
+ if dry_run:
241
+ return {"dryRun": True, "wouldDelete": {"vhost": vhost, "queue": name}}
242
+ return ops.delete_queue(conn, vhost, name)
243
+
244
+
245
+ @mcp.tool()
246
+ @governed_tool(risk_level="medium", undo=_declare_queue_undo)
247
+ @tool_errors("dict")
248
+ def declare_queue(
249
+ vhost: str,
250
+ name: str,
251
+ durable: bool = True,
252
+ auto_delete: bool = False,
253
+ arguments: Optional[dict[str, Any]] = None,
254
+ dry_run: bool = False,
255
+ target: Optional[str] = None,
256
+ ) -> dict:
257
+ """[WRITE][risk=medium] Declare (create) a rabbitmq queue; undo deletes it if new.
258
+
259
+ Records whether the queue already existed; the undo (delete) is recorded
260
+ only for a newly-created queue. Also the replay target for delete_queue's
261
+ undo. Pass dry_run=True to preview.
262
+
263
+ Args:
264
+ vhost: The queue's vhost (the default vhost is '/').
265
+ name: Queue name to declare.
266
+ durable: Survive broker restarts (default True).
267
+ auto_delete: Delete when the last consumer disconnects (default False).
268
+ arguments: Optional x-arguments (e.g. {'x-queue-type': 'quorum'}).
269
+ dry_run: If True, preview without declaring.
270
+ target: rabbitmq target name from config; omit for the default.
271
+ """
272
+ conn = _get_connection(target)
273
+ if dry_run:
274
+ return {
275
+ "dryRun": True,
276
+ "wouldDeclare": {
277
+ "vhost": vhost, "queue": name, "durable": durable,
278
+ "autoDelete": auto_delete, "arguments": arguments or {},
279
+ },
280
+ }
281
+ return ops.declare_queue(
282
+ conn, vhost, name, durable=durable, auto_delete=auto_delete, arguments=arguments
283
+ )
284
+
285
+
286
+ # ── rabbitmq policy writes ───────────────────────────────────────────────────
287
+
288
+
289
+ @mcp.tool()
290
+ @governed_tool(risk_level="medium", undo=_set_policy_undo)
291
+ @tool_errors("dict")
292
+ def set_policy(
293
+ vhost: str,
294
+ name: str,
295
+ pattern: str,
296
+ definition: dict[str, Any],
297
+ priority: int = 0,
298
+ apply_to: str = "all",
299
+ dry_run: bool = False,
300
+ target: Optional[str] = None,
301
+ ) -> dict:
302
+ """[WRITE][risk=medium] Create/update a rabbitmq policy; reversible.
303
+
304
+ Reads the policy first: the recorded undo restores the prior policy fields,
305
+ or deletes the policy when it is newly created. Pass dry_run=True to
306
+ preview.
307
+
308
+ Args:
309
+ vhost: The policy's vhost (the default vhost is '/').
310
+ name: Policy name.
311
+ pattern: Regex the policy matches queue/exchange names against.
312
+ definition: Policy definition object (e.g. {'max-length': 100000}).
313
+ priority: Policy priority (higher wins; default 0).
314
+ apply_to: 'queues', 'exchanges', or 'all' (default).
315
+ dry_run: If True, preview without changing.
316
+ target: rabbitmq target name from config; omit for the default.
317
+ """
318
+ conn = _get_connection(target)
319
+ if dry_run:
320
+ return {
321
+ "dryRun": True,
322
+ "wouldSetPolicy": {
323
+ "vhost": vhost, "policy": name, "pattern": pattern,
324
+ "definition": definition, "priority": priority, "applyTo": apply_to,
325
+ },
326
+ }
327
+ return ops.set_policy(
328
+ conn, vhost, name, pattern, definition, priority=priority, apply_to=apply_to
329
+ )
330
+
331
+
332
+ @mcp.tool()
333
+ @governed_tool(risk_level="medium", undo=_delete_policy_undo)
334
+ @tool_errors("dict")
335
+ def delete_policy(
336
+ vhost: str,
337
+ name: str,
338
+ dry_run: bool = False,
339
+ target: Optional[str] = None,
340
+ ) -> dict:
341
+ """[WRITE][risk=medium] Delete a rabbitmq policy; the undo re-creates it.
342
+
343
+ Reads the policy first so the recorded undo can re-create it exactly. Pass
344
+ dry_run=True to preview.
345
+
346
+ Args:
347
+ vhost: The policy's vhost (the default vhost is '/').
348
+ name: Policy name (from list_policies).
349
+ dry_run: If True, preview without deleting.
350
+ target: rabbitmq target name from config; omit for the default.
351
+ """
352
+ conn = _get_connection(target)
353
+ if dry_run:
354
+ return {"dryRun": True, "wouldDeletePolicy": {"vhost": vhost, "policy": name}}
355
+ return ops.delete_policy(conn, vhost, name)
@@ -0,0 +1,14 @@
1
+ """queue-aiops — governed redis + rabbitmq broker operations for AI agents.
2
+
3
+ Standalone and self-contained: the governance harness (audit, token budget,
4
+ undo-token recording, graduated risk tiers, output sanitize) is
5
+ bundled under ``queue_aiops.governance`` — this package has no external
6
+ skill-family dependency. Preview: not yet full-coverage.
7
+ """
8
+
9
+ from importlib.metadata import PackageNotFoundError, version
10
+
11
+ try:
12
+ __version__ = version("queue-aiops")
13
+ except PackageNotFoundError: # running from an uninstalled source tree
14
+ __version__ = "0.0.0+unknown"
@@ -0,0 +1,9 @@
1
+ """CLI package for queue-aiops.
2
+
3
+ Re-exports ``app`` so the pyproject entry point
4
+ ``queue-aiops = "queue_aiops.cli:app"`` works unchanged.
5
+ """
6
+
7
+ from queue_aiops.cli._root import app
8
+
9
+ __all__ = ["app"]
@@ -0,0 +1,78 @@
1
+ """Shared helpers for queue-aiops CLI sub-modules."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import functools
6
+ from collections.abc import Callable
7
+ from pathlib import Path
8
+ from typing import Annotated, Any
9
+
10
+ import typer
11
+ from rich.console import Console
12
+
13
+ console = Console()
14
+
15
+ # ─── Shared Option types ───────────────────────────────────────────────────
16
+
17
+ TargetOption = Annotated[
18
+ str | None, typer.Option("--target", "-t", help="Target name from config")
19
+ ]
20
+ DryRunOption = Annotated[
21
+ bool, typer.Option("--dry-run", help="Print the API call without executing")
22
+ ]
23
+
24
+
25
+ def _cli_error_types() -> tuple[type[BaseException], ...]:
26
+ """Exceptions translated to a one-line teaching error instead of a traceback."""
27
+ from queue_aiops.connection import QueueApiError
28
+
29
+ return (QueueApiError, KeyError, OSError, ValueError)
30
+
31
+
32
+ def cli_errors(fn: Callable) -> Callable:
33
+ """Translate known exceptions into one red line + exit code 1."""
34
+
35
+ @functools.wraps(fn)
36
+ def wrapper(*args: Any, **kwargs: Any) -> Any:
37
+ try:
38
+ return fn(*args, **kwargs)
39
+ except (typer.Exit, typer.Abort):
40
+ raise
41
+ except _cli_error_types() as e:
42
+ message = str(e)
43
+ if isinstance(e, KeyError):
44
+ message = f"Missing required key or environment variable: {message}"
45
+ console.print(f"[red]Error: {message}[/]")
46
+ raise typer.Exit(1) from e
47
+
48
+ return wrapper
49
+
50
+
51
+ def get_connection(target: str | None, config_path: Path | None = None) -> tuple[Any, Any]:
52
+ """Return a (conn, config) tuple for the given target."""
53
+ from queue_aiops.config import load_config
54
+ from queue_aiops.connection import ConnectionManager
55
+
56
+ cfg = load_config(config_path)
57
+ mgr = ConnectionManager(cfg)
58
+ return mgr.connect(target), cfg
59
+
60
+
61
+ def dry_run_print(*, operation: str, api_call: str, parameters: dict | None = None) -> None:
62
+ """Print a dry-run preview of the API call that would be made."""
63
+ console.print("\n[bold magenta][DRY-RUN] No changes will be made.[/]")
64
+ console.print(f"[magenta] Operation: {operation}[/]")
65
+ console.print(f"[magenta] API Call: {api_call}[/]")
66
+ for k, v in (parameters or {}).items():
67
+ console.print(f"[magenta] Param: {k} = {v}[/]")
68
+ console.print("[magenta] Run without --dry-run to execute.[/]\n")
69
+
70
+
71
+ def double_confirm(action: str, resource: str) -> None:
72
+ """Require two confirmations for a destructive operation."""
73
+ console.print(f"[bold yellow]⚠️ About to: {action} '{resource}'[/]")
74
+ typer.confirm(f"Confirm 1/2: {action} '{resource}'?", abort=True)
75
+ typer.confirm(
76
+ f"Confirm 2/2: really {action} '{resource}'? This may be irreversible.",
77
+ abort=True,
78
+ )
@@ -0,0 +1,62 @@
1
+ """Top-level Typer app: assembles sub-apps and top-level commands."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import typer
6
+
7
+ from queue_aiops.cli._common import cli_errors
8
+ from queue_aiops.cli.analyze import analyze_app
9
+ from queue_aiops.cli.doctor import doctor_cmd
10
+ from queue_aiops.cli.init import init_cmd
11
+ from queue_aiops.cli.overview import overview_cmd
12
+ from queue_aiops.cli.rabbit_cmds import rabbitmq_app
13
+ from queue_aiops.cli.redis_cmds import redis_app
14
+ from queue_aiops.cli.secret import secret_app
15
+ from queue_aiops.cli.undo import undo_app
16
+
17
+ app = typer.Typer(
18
+ name="queue-aiops",
19
+ help="Governed AI-ops for redis + rabbitmq: memory/latency/backlog/churn "
20
+ "RCAs, slowlog and big-key sampling, queues/policies, and governed writes "
21
+ "(config set, client kill, purge/delete queue, policies).",
22
+ no_args_is_help=True,
23
+ )
24
+
25
+ app.add_typer(redis_app, name="redis")
26
+ app.add_typer(rabbitmq_app, name="rabbitmq")
27
+ app.add_typer(analyze_app, name="analyze")
28
+ app.add_typer(secret_app, name="secret")
29
+ app.add_typer(undo_app, name="undo")
30
+ app.command("init")(init_cmd)
31
+ app.command("overview")(overview_cmd)
32
+ app.command("doctor")(doctor_cmd)
33
+
34
+
35
+ @app.command("mcp")
36
+ @cli_errors
37
+ def mcp_cmd() -> None:
38
+ """Start the MCP server (stdio transport).
39
+
40
+ Single-command entry point for MCP clients (does not go through uvx/PyPI
41
+ resolution at launch):
42
+ queue-aiops mcp
43
+ """
44
+ import sys
45
+
46
+ if sys.version_info < (3, 11):
47
+ typer.echo(
48
+ f"ERROR: queue-aiops requires Python >= 3.11 "
49
+ f"(got {sys.version_info.major}.{sys.version_info.minor}).\n"
50
+ f"Fix: uv python install 3.12 && "
51
+ f"uv tool install --python 3.12 --force queue-aiops",
52
+ err=True,
53
+ )
54
+ raise typer.Exit(2)
55
+
56
+ from mcp_server.server import main as _mcp_main
57
+
58
+ _mcp_main()
59
+
60
+
61
+ if __name__ == "__main__":
62
+ app()
@@ -0,0 +1,85 @@
1
+ """``queue-aiops analyze`` — the flagship RCA analyses (read-only)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import json
6
+ from typing import Annotated
7
+
8
+ import typer
9
+
10
+ from queue_aiops.cli._common import TargetOption, cli_errors, console, get_connection
11
+
12
+ analyze_app = typer.Typer(
13
+ name="analyze",
14
+ help="Flagship RCAs: redis memory pressure + latency, rabbitmq queue "
15
+ "backlog, and connection churn (both platforms).",
16
+ no_args_is_help=True,
17
+ )
18
+
19
+
20
+ @analyze_app.command("memory")
21
+ @cli_errors
22
+ def analyze_memory(
23
+ used_pct: Annotated[
24
+ float, typer.Option("--used-pct", help="used/maxmemory %% pressure threshold")
25
+ ] = 85.0,
26
+ target: TargetOption = None,
27
+ ) -> None:
28
+ """redis memory-pressure RCA: maxmemory, eviction, fragmentation, big keys."""
29
+ from queue_aiops.ops import analysis as ops
30
+ from queue_aiops.ops._util import as_obj, num
31
+
32
+ conn, _ = get_connection(target)
33
+ telemetry = ops.pull_memory_telemetry(conn)
34
+ console.print_json(json.dumps(ops.redis_memory_pressure_rca(
35
+ as_obj(telemetry.get("memory")),
36
+ evicted_keys=num(telemetry.get("evictedKeys")),
37
+ big_keys=as_obj(telemetry.get("bigKeys")),
38
+ used_pct=used_pct,
39
+ )))
40
+
41
+
42
+ @analyze_app.command("latency")
43
+ @cli_errors
44
+ def analyze_latency(
45
+ slow_us: Annotated[
46
+ float, typer.Option("--slow-us", help="Slowlog threshold in microseconds")
47
+ ] = 10_000.0,
48
+ target: TargetOption = None,
49
+ ) -> None:
50
+ """redis latency RCA: slowlog digest, blocked clients, fork/AOF stalls."""
51
+ from queue_aiops.ops import analysis as ops
52
+
53
+ conn, _ = get_connection(target)
54
+ telemetry = ops.pull_latency_telemetry(conn)
55
+ console.print_json(json.dumps(ops.redis_latency_rca(telemetry, slow_us=slow_us)))
56
+
57
+
58
+ @analyze_app.command("backlog")
59
+ @cli_errors
60
+ def analyze_backlog(
61
+ vhost: Annotated[
62
+ str | None, typer.Option("--vhost", help="Restrict to one vhost")
63
+ ] = None,
64
+ top: Annotated[int, typer.Option("--top", help="Queue rows, deepest first")] = 20,
65
+ target: TargetOption = None,
66
+ ) -> None:
67
+ """rabbitmq queue-backlog RCA: consumers, unacked pileups, watermark blocks."""
68
+ from queue_aiops.ops import analysis as ops
69
+
70
+ conn, _ = get_connection(target)
71
+ telemetry = ops.pull_backlog_telemetry(conn, vhost)
72
+ console.print_json(json.dumps(ops.rabbitmq_queue_backlog_rca(
73
+ telemetry.get("queues", []), nodes=telemetry.get("nodes", []), top=top,
74
+ )))
75
+
76
+
77
+ @analyze_app.command("churn")
78
+ @cli_errors
79
+ def analyze_churn(target: TargetOption = None) -> None:
80
+ """Connection-churn analysis (both platforms): counts, rates, by source."""
81
+ from queue_aiops.ops import analysis as ops
82
+
83
+ conn, _ = get_connection(target)
84
+ snapshot = ops.pull_churn_snapshot(conn)
85
+ console.print_json(json.dumps(ops.connection_churn_analysis(snapshot)))
@@ -0,0 +1,21 @@
1
+ """Doctor top-level command: environment and connectivity check."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Annotated
6
+
7
+ import typer
8
+
9
+ from queue_aiops.cli._common import cli_errors
10
+
11
+
12
+ @cli_errors
13
+ def doctor_cmd(
14
+ skip_auth: Annotated[
15
+ bool, typer.Option("--skip-auth", help="Skip connectivity check (faster)")
16
+ ] = False,
17
+ ) -> None:
18
+ """Check environment, config, secrets, and connectivity."""
19
+ from queue_aiops.doctor import run_doctor
20
+
21
+ raise typer.Exit(run_doctor(skip_auth=skip_auth))