4b6817381b
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
CI / coverage-report (push) Has been cancelled
CI / test-kubernetes (push) Has been cancelled
CI / should-run-thorough (push) Has been cancelled
CI / test-thorough (cloudwatch-demo) (push) Has been cancelled
CI / test-thorough (flink-ecs) (push) Has been cancelled
CI / test-thorough (upstream-lambda) (push) Has been cancelled
CI / test-thorough (prefect-ecs-fargate) (push) Has been cancelled
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Has been cancelled
Benchmark image — build + push to ECR (any adapter) / build + push (push) Has been cancelled
CI / quality (ubuntu-latest) (push) Has been cancelled
CI / test (tools-runtime) (push) Has been cancelled
CI / test (e2e-general) (push) Has been cancelled
CI / test (cli-runtime) (push) Has been cancelled
CI / test (e2e-provider-and-openclaw) (push) Has been cancelled
CI / test (integrations-and-misc) (push) Has been cancelled
Release / verify (push) Has been cancelled
Release / build-python-dist (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Has been cancelled
Release / publish-release (push) Has been cancelled
Release / publish-main-release (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Has been cancelled
CodeQL / Analyze (python) (push) Has been cancelled
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Has been cancelled
Release / prepare (push) Has been cancelled
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Has been cancelled
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Has been cancelled
581 lines
23 KiB
Python
581 lines
23 KiB
Python
"""Shared RabbitMQ integration helpers.
|
|
|
|
Provides configuration, connectivity validation, and read-only diagnostic
|
|
queries for RabbitMQ brokers via the HTTP Management API.
|
|
|
|
All operations are production-safe: read-only, timeouts enforced, result sizes
|
|
capped. The Management API (port 15672 by default) is strongly preferred over
|
|
AMQP introspection because it returns rich JSON describing queues, consumers,
|
|
connections, channels, nodes, and alarms — exactly the operational state an
|
|
incident investigation needs. AMQP would require a new dependency and expose
|
|
far less diagnostic data.
|
|
|
|
The management plugin (``rabbitmq_management``) must be enabled on the target
|
|
broker; the validation helper surfaces that specifically so users aren't left
|
|
chasing a generic 404.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import logging
|
|
import os
|
|
import urllib.parse
|
|
from dataclasses import dataclass
|
|
from typing import Any
|
|
|
|
import httpx
|
|
from pydantic import Field, field_validator
|
|
|
|
from config.strict_config import StrictConfigModel
|
|
from integrations._validation_helpers import report_classify_failure, report_validation_failure
|
|
from platform.common.coercion import safe_int
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
DEFAULT_RABBITMQ_MANAGEMENT_PORT = 15672
|
|
DEFAULT_RABBITMQ_VHOST = "/"
|
|
DEFAULT_RABBITMQ_TIMEOUT_S = 10
|
|
DEFAULT_RABBITMQ_MAX_RESULTS = 50
|
|
|
|
|
|
class RabbitMQConfig(StrictConfigModel):
|
|
"""Normalized RabbitMQ Management API connection settings."""
|
|
|
|
host: str = ""
|
|
management_port: int = DEFAULT_RABBITMQ_MANAGEMENT_PORT
|
|
username: str = ""
|
|
password: str = ""
|
|
vhost: str = DEFAULT_RABBITMQ_VHOST
|
|
ssl: bool = False
|
|
verify_ssl: bool = True
|
|
timeout_seconds: int = Field(default=DEFAULT_RABBITMQ_TIMEOUT_S, gt=0)
|
|
max_results: int = Field(default=DEFAULT_RABBITMQ_MAX_RESULTS, gt=0, le=200)
|
|
integration_id: str = ""
|
|
|
|
@field_validator("host", mode="before")
|
|
@classmethod
|
|
def _normalize_host(cls, value: Any) -> str:
|
|
return str(value or "").strip()
|
|
|
|
@field_validator("username", mode="before")
|
|
@classmethod
|
|
def _normalize_username(cls, value: Any) -> str:
|
|
return str(value or "").strip()
|
|
|
|
@field_validator("password", mode="before")
|
|
@classmethod
|
|
def _normalize_password(cls, value: Any) -> str:
|
|
# Do NOT strip passwords — leading/trailing whitespace is valid.
|
|
return str(value or "")
|
|
|
|
@field_validator("vhost", mode="before")
|
|
@classmethod
|
|
def _normalize_vhost(cls, value: Any) -> str:
|
|
raw = str(value or "").strip()
|
|
return raw or DEFAULT_RABBITMQ_VHOST
|
|
|
|
@field_validator("management_port", mode="before")
|
|
@classmethod
|
|
def _normalize_management_port(cls, value: Any) -> int:
|
|
return safe_int(value, DEFAULT_RABBITMQ_MANAGEMENT_PORT)
|
|
|
|
@property
|
|
def is_configured(self) -> bool:
|
|
return bool(self.host and self.username)
|
|
|
|
@property
|
|
def base_url(self) -> str:
|
|
scheme = "https" if self.ssl else "http"
|
|
return f"{scheme}://{self.host}:{self.management_port}"
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class RabbitMQValidationResult:
|
|
"""Result of validating a RabbitMQ integration."""
|
|
|
|
ok: bool
|
|
detail: str
|
|
|
|
|
|
def build_rabbitmq_config(raw: dict[str, Any] | None) -> RabbitMQConfig:
|
|
"""Build a normalized RabbitMQ config object from env/store data."""
|
|
return RabbitMQConfig.model_validate(raw or {})
|
|
|
|
|
|
def rabbitmq_config_from_env() -> RabbitMQConfig | None:
|
|
"""Load a RabbitMQ config from env vars."""
|
|
host = os.getenv("RABBITMQ_HOST", "").strip()
|
|
username = os.getenv("RABBITMQ_USERNAME", "").strip()
|
|
if not host or not username:
|
|
return None
|
|
return build_rabbitmq_config(
|
|
{
|
|
"host": host,
|
|
"management_port": os.getenv(
|
|
"RABBITMQ_MANAGEMENT_PORT",
|
|
str(DEFAULT_RABBITMQ_MANAGEMENT_PORT),
|
|
).strip(),
|
|
"username": username,
|
|
"password": os.getenv("RABBITMQ_PASSWORD", ""),
|
|
"vhost": os.getenv("RABBITMQ_VHOST", DEFAULT_RABBITMQ_VHOST).strip(),
|
|
"ssl": os.getenv("RABBITMQ_SSL", "false").strip().lower() in ("true", "1", "yes"),
|
|
"verify_ssl": os.getenv("RABBITMQ_VERIFY_SSL", "true").strip().lower()
|
|
in ("true", "1", "yes"),
|
|
}
|
|
)
|
|
|
|
|
|
def _get_client(config: RabbitMQConfig) -> httpx.Client:
|
|
"""Build an authenticated httpx client for the management API."""
|
|
return httpx.Client(
|
|
base_url=config.base_url,
|
|
auth=(config.username, config.password),
|
|
timeout=float(config.timeout_seconds),
|
|
verify=config.verify_ssl,
|
|
)
|
|
|
|
|
|
def _error_evidence(err: str) -> dict[str, Any]:
|
|
return {"source": "rabbitmq", "available": False, "error": err}
|
|
|
|
|
|
def _vhost_path(base: str, vhost: str) -> str:
|
|
"""Build a vhost-scoped API path, URL-encoding the vhost segment."""
|
|
encoded = urllib.parse.quote(vhost, safe="")
|
|
return f"{base}/{encoded}"
|
|
|
|
|
|
def _http_get(client: httpx.Client, path: str) -> tuple[Any | None, str | None]:
|
|
"""Fetch a JSON endpoint; return (data, error_message)."""
|
|
try:
|
|
response = client.get(path)
|
|
except httpx.RequestError as err:
|
|
return None, f"RabbitMQ request failed: {err}"
|
|
|
|
if response.status_code == 401:
|
|
return None, "RabbitMQ authentication failed (check username/password)."
|
|
if response.status_code == 404:
|
|
if path == "/api/overview":
|
|
return None, (
|
|
"RabbitMQ Management API not found — enable the "
|
|
"`rabbitmq_management` plugin on the broker: "
|
|
"`rabbitmq-plugins enable rabbitmq_management`."
|
|
)
|
|
return None, f"RabbitMQ endpoint not found: {path}"
|
|
if response.status_code >= 400:
|
|
return None, (
|
|
f"RabbitMQ management API returned HTTP {response.status_code}: {response.text[:200]}"
|
|
)
|
|
try:
|
|
return response.json(), None
|
|
except ValueError as err:
|
|
return None, f"RabbitMQ management API returned non-JSON body: {err}"
|
|
|
|
|
|
def validate_rabbitmq_config(config: RabbitMQConfig) -> RabbitMQValidationResult:
|
|
"""Validate RabbitMQ management API reachability with a lightweight call."""
|
|
if not config.host or not config.username:
|
|
return RabbitMQValidationResult(ok=False, detail="RabbitMQ host and username are required.")
|
|
|
|
try:
|
|
with _get_client(config) as client:
|
|
data, err = _http_get(client, "/api/overview")
|
|
if err is not None:
|
|
return RabbitMQValidationResult(ok=False, detail=err)
|
|
if not isinstance(data, dict):
|
|
return RabbitMQValidationResult(
|
|
ok=False,
|
|
detail="RabbitMQ management API returned an unexpected response.",
|
|
)
|
|
version = str(data.get("rabbitmq_version", "unknown"))
|
|
cluster = str(data.get("cluster_name", "unknown"))
|
|
return RabbitMQValidationResult(
|
|
ok=True,
|
|
detail=(
|
|
f"Connected to RabbitMQ {version} (cluster: {cluster}, vhost: {config.vhost})."
|
|
),
|
|
)
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="validate_rabbitmq_config",
|
|
)
|
|
return RabbitMQValidationResult(ok=False, detail=f"RabbitMQ connection failed: {err}")
|
|
|
|
|
|
def rabbitmq_is_available(sources: dict[str, dict]) -> bool:
|
|
"""Check if RabbitMQ integration credentials are present."""
|
|
rmq = sources.get("rabbitmq", {})
|
|
return bool(rmq.get("host") and rmq.get("username"))
|
|
|
|
|
|
def rabbitmq_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
|
|
"""Extract RabbitMQ credentials from resolved integrations."""
|
|
rmq = sources.get("rabbitmq", {})
|
|
return {
|
|
"host": rmq.get("host", ""),
|
|
"management_port": rmq.get("management_port", DEFAULT_RABBITMQ_MANAGEMENT_PORT),
|
|
"username": rmq.get("username", ""),
|
|
"password": rmq.get("password", ""),
|
|
"vhost": rmq.get("vhost", DEFAULT_RABBITMQ_VHOST),
|
|
"ssl": rmq.get("ssl", False),
|
|
"verify_ssl": rmq.get("verify_ssl", True),
|
|
}
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Diagnostic query functions. Each returns the standard evidence dict shape:
|
|
# {"source": "rabbitmq", "available": bool, ...diagnostic-specific keys}
|
|
# Errors return {"source": "rabbitmq", "available": False, "error": "..."}.
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def _summarize_queue(queue: dict[str, Any]) -> dict[str, Any]:
|
|
stats = queue.get("message_stats") or {}
|
|
# The Management API returns these count fields as an explicit JSON ``null``
|
|
# (not just absent) for queues without live stats — e.g. a queue whose home
|
|
# node is down, or right after a broker restart before the stats DB is
|
|
# populated. ``dict.get(key, 0)`` only substitutes the default when the key
|
|
# is missing, so ``null`` would flow through as ``None`` and crash the
|
|
# backlog sort below (``None + None``). Coerce with ``or 0``, mirroring the
|
|
# ``message_stats or {}`` guard above.
|
|
return {
|
|
"name": queue.get("name", ""),
|
|
"vhost": queue.get("vhost", ""),
|
|
"state": queue.get("state", "unknown"),
|
|
"messages_ready": queue.get("messages_ready") or 0,
|
|
"messages_unacknowledged": queue.get("messages_unacknowledged") or 0,
|
|
"messages_total": queue.get("messages") or 0,
|
|
"messages_persistent": queue.get("messages_persistent") or 0,
|
|
"consumers": queue.get("consumers") or 0,
|
|
"consumer_utilisation": queue.get("consumer_utilisation"),
|
|
"memory_bytes": queue.get("memory") or 0,
|
|
"publish_rate": (stats.get("publish_details") or {}).get("rate", 0.0),
|
|
"deliver_rate": (stats.get("deliver_get_details") or {}).get("rate", 0.0),
|
|
"ack_rate": (stats.get("ack_details") or {}).get("rate", 0.0),
|
|
}
|
|
|
|
|
|
def get_queue_backlog(
|
|
config: RabbitMQConfig,
|
|
max_results: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return the top N queues ranked by backlog (unacked + ready)."""
|
|
if not config.is_configured:
|
|
return _error_evidence("Not configured.")
|
|
|
|
effective_limit = min(max_results or config.max_results, config.max_results)
|
|
try:
|
|
with _get_client(config) as client:
|
|
path = _vhost_path("/api/queues", config.vhost)
|
|
data, err = _http_get(client, path)
|
|
if err is not None:
|
|
return _error_evidence(err)
|
|
if not isinstance(data, list):
|
|
return _error_evidence(f"Unexpected {path} response.")
|
|
summarized = [_summarize_queue(q) for q in data]
|
|
summarized.sort(
|
|
key=lambda q: q["messages_ready"] + q["messages_unacknowledged"],
|
|
reverse=True,
|
|
)
|
|
truncated = summarized[:effective_limit]
|
|
return {
|
|
"source": "rabbitmq",
|
|
"available": True,
|
|
"total_queues": len(summarized),
|
|
"returned": len(truncated),
|
|
"queues": truncated,
|
|
}
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="get_queue_backlog",
|
|
)
|
|
return _error_evidence(str(err))
|
|
|
|
|
|
def get_consumer_health(
|
|
config: RabbitMQConfig,
|
|
max_results: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return consumer-level diagnostics across vhosts."""
|
|
if not config.is_configured:
|
|
return _error_evidence("Not configured.")
|
|
|
|
effective_limit = min(max_results or config.max_results, config.max_results)
|
|
try:
|
|
with _get_client(config) as client:
|
|
path = _vhost_path("/api/consumers", config.vhost)
|
|
data, err = _http_get(client, path)
|
|
if err is not None:
|
|
return _error_evidence(err)
|
|
if not isinstance(data, list):
|
|
return _error_evidence(f"Unexpected {path} response.")
|
|
consumers = []
|
|
for entry in data[:effective_limit]:
|
|
queue = entry.get("queue") or {}
|
|
channel = entry.get("channel_details") or {}
|
|
consumers.append(
|
|
{
|
|
"queue": queue.get("name", ""),
|
|
"vhost": queue.get("vhost", ""),
|
|
"consumer_tag": entry.get("consumer_tag", ""),
|
|
"ack_required": bool(entry.get("ack_required", True)),
|
|
"prefetch_count": entry.get("prefetch_count", 0),
|
|
"active": entry.get("active", True),
|
|
"channel_name": channel.get("name", ""),
|
|
"connection_name": channel.get("connection_name", ""),
|
|
"peer_host": channel.get("peer_host", ""),
|
|
}
|
|
)
|
|
return {
|
|
"source": "rabbitmq",
|
|
"available": True,
|
|
"total_consumers": len(data),
|
|
"returned": len(consumers),
|
|
"consumers": consumers,
|
|
}
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="get_consumer_health",
|
|
)
|
|
return _error_evidence(str(err))
|
|
|
|
|
|
def get_broker_overview(config: RabbitMQConfig) -> dict[str, Any]:
|
|
"""Return cluster-wide overview + alarm status."""
|
|
if not config.is_configured:
|
|
return _error_evidence("Not configured.")
|
|
|
|
try:
|
|
with _get_client(config) as client:
|
|
overview, err = _http_get(client, "/api/overview")
|
|
if err is not None:
|
|
return _error_evidence(err)
|
|
if not isinstance(overview, dict):
|
|
return _error_evidence("Unexpected /api/overview response.")
|
|
|
|
totals = overview.get("queue_totals") or {}
|
|
msg_stats = overview.get("message_stats") or {}
|
|
object_totals = overview.get("object_totals") or {}
|
|
|
|
# /api/healthchecks/alarms returns HTTP 503 (not 200) when alarms
|
|
# are active, so we can't use _http_get which treats >=400 as error.
|
|
alarm_payload: dict[str, Any] = {"ok": False, "detail": "unknown"}
|
|
try:
|
|
alarm_resp = client.get("/api/healthchecks/alarms")
|
|
except httpx.RequestError as exc:
|
|
alarm_payload = {"ok": False, "detail": str(exc)}
|
|
else:
|
|
if alarm_resp.status_code == 200:
|
|
alarm_payload = {"ok": True, "detail": "ok"}
|
|
elif alarm_resp.status_code in (401, 403):
|
|
alarm_payload = {
|
|
"ok": None,
|
|
"detail": "alarm status unknown (insufficient permissions)",
|
|
}
|
|
elif alarm_resp.status_code == 404:
|
|
alarm_payload = {
|
|
"ok": None,
|
|
"detail": "alarm endpoint not available on this broker version",
|
|
}
|
|
else:
|
|
# 503 = alarms active; parse the structured reason.
|
|
try:
|
|
alarm_body = alarm_resp.json()
|
|
alarm_payload = {
|
|
"ok": False,
|
|
"detail": str(alarm_body.get("reason", alarm_resp.text[:200])),
|
|
}
|
|
except ValueError:
|
|
alarm_payload = {
|
|
"ok": False,
|
|
"detail": alarm_resp.text[:200],
|
|
}
|
|
|
|
return {
|
|
"source": "rabbitmq",
|
|
"available": True,
|
|
"cluster_name": overview.get("cluster_name", ""),
|
|
"rabbitmq_version": overview.get("rabbitmq_version", ""),
|
|
"erlang_version": overview.get("erlang_version", ""),
|
|
"messages_ready": totals.get("messages_ready", 0),
|
|
"messages_unacknowledged": totals.get("messages_unacknowledged", 0),
|
|
"messages_total": totals.get("messages", 0),
|
|
"publish_rate": (msg_stats.get("publish_details") or {}).get("rate", 0.0),
|
|
"deliver_rate": (msg_stats.get("deliver_get_details") or {}).get("rate", 0.0),
|
|
"queues": object_totals.get("queues", 0),
|
|
"consumers": object_totals.get("consumers", 0),
|
|
"connections": object_totals.get("connections", 0),
|
|
"channels": object_totals.get("channels", 0),
|
|
"alarms": alarm_payload,
|
|
}
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="get_broker_overview",
|
|
)
|
|
return _error_evidence(str(err))
|
|
|
|
|
|
def get_node_health(config: RabbitMQConfig) -> dict[str, Any]:
|
|
"""Return per-node resource + partition state (cluster diagnostics)."""
|
|
if not config.is_configured:
|
|
return _error_evidence("Not configured.")
|
|
|
|
try:
|
|
with _get_client(config) as client:
|
|
data, err = _http_get(client, "/api/nodes")
|
|
if err is not None:
|
|
return _error_evidence(err)
|
|
if not isinstance(data, list):
|
|
return _error_evidence("Unexpected /api/nodes response.")
|
|
nodes = []
|
|
for node in data:
|
|
nodes.append(
|
|
{
|
|
"name": node.get("name", ""),
|
|
"running": bool(node.get("running", False)),
|
|
"type": node.get("type", ""),
|
|
"mem_used_bytes": node.get("mem_used", 0),
|
|
"mem_limit_bytes": node.get("mem_limit", 0),
|
|
"mem_alarm": bool(node.get("mem_alarm", False)),
|
|
"disk_free_bytes": node.get("disk_free", 0),
|
|
"disk_free_limit_bytes": node.get("disk_free_limit", 0),
|
|
"disk_free_alarm": bool(node.get("disk_free_alarm", False)),
|
|
"fd_used": node.get("fd_used", 0),
|
|
"fd_total": node.get("fd_total", 0),
|
|
"sockets_used": node.get("sockets_used", 0),
|
|
"sockets_total": node.get("sockets_total", 0),
|
|
"proc_used": node.get("proc_used", 0),
|
|
"proc_total": node.get("proc_total", 0),
|
|
"partitions": list(node.get("partitions") or []),
|
|
}
|
|
)
|
|
any_partitioned = any(n["partitions"] for n in nodes)
|
|
return {
|
|
"source": "rabbitmq",
|
|
"available": True,
|
|
"node_count": len(nodes),
|
|
"any_partitioned": any_partitioned,
|
|
"nodes": nodes,
|
|
}
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="get_node_health",
|
|
)
|
|
return _error_evidence(str(err))
|
|
|
|
|
|
def get_connection_stats(
|
|
config: RabbitMQConfig,
|
|
max_results: int | None = None,
|
|
) -> dict[str, Any]:
|
|
"""Return active connection metadata sorted by receive rate."""
|
|
if not config.is_configured:
|
|
return _error_evidence("Not configured.")
|
|
|
|
effective_limit = min(max_results or config.max_results, config.max_results)
|
|
try:
|
|
with _get_client(config) as client:
|
|
# /api/connections has no vhost-scoped variant; filter client-side.
|
|
data, err = _http_get(client, "/api/connections")
|
|
if err is not None:
|
|
return _error_evidence(err)
|
|
if not isinstance(data, list):
|
|
return _error_evidence("Unexpected /api/connections response.")
|
|
|
|
connections = []
|
|
for conn in data:
|
|
if conn.get("vhost", "/") != config.vhost:
|
|
continue
|
|
recv_oct = conn.get("recv_oct_details") or {}
|
|
send_oct = conn.get("send_oct_details") or {}
|
|
connections.append(
|
|
{
|
|
"name": conn.get("name", ""),
|
|
"user": conn.get("user", ""),
|
|
"vhost": conn.get("vhost", ""),
|
|
"state": conn.get("state", "unknown"),
|
|
"protocol": conn.get("protocol", ""),
|
|
"channels": conn.get("channels", 0),
|
|
"peer_host": conn.get("peer_host", ""),
|
|
"peer_port": conn.get("peer_port", 0),
|
|
"ssl": bool(conn.get("ssl", False)),
|
|
"recv_rate_bytes_per_sec": recv_oct.get("rate", 0.0),
|
|
"send_rate_bytes_per_sec": send_oct.get("rate", 0.0),
|
|
}
|
|
)
|
|
connections.sort(key=lambda c: c["recv_rate_bytes_per_sec"], reverse=True)
|
|
truncated = connections[:effective_limit]
|
|
return {
|
|
"source": "rabbitmq",
|
|
"available": True,
|
|
"broker_total_connections": len(data),
|
|
"vhost_connections": len(connections),
|
|
"returned": len(truncated),
|
|
"connections": truncated,
|
|
}
|
|
except Exception as err:
|
|
report_validation_failure(
|
|
err,
|
|
logger=logger,
|
|
integration="rabbitmq",
|
|
method="get_connection_stats",
|
|
)
|
|
return _error_evidence(str(err))
|
|
|
|
|
|
__all__ = [
|
|
"DEFAULT_RABBITMQ_MANAGEMENT_PORT",
|
|
"DEFAULT_RABBITMQ_VHOST",
|
|
"RabbitMQConfig",
|
|
"RabbitMQValidationResult",
|
|
"build_rabbitmq_config",
|
|
"get_broker_overview",
|
|
"get_connection_stats",
|
|
"get_consumer_health",
|
|
"get_node_health",
|
|
"get_queue_backlog",
|
|
"rabbitmq_config_from_env",
|
|
"rabbitmq_extract_params",
|
|
"rabbitmq_is_available",
|
|
"validate_rabbitmq_config",
|
|
]
|
|
|
|
|
|
def classify(
|
|
credentials: dict[str, Any], record_id: str
|
|
) -> tuple[RabbitMQConfig | None, str | None]:
|
|
try:
|
|
cfg = build_rabbitmq_config(
|
|
{
|
|
"host": credentials.get("host", ""),
|
|
"management_port": credentials.get("management_port", 15672),
|
|
"username": credentials.get("username", ""),
|
|
"password": credentials.get("password", ""),
|
|
"vhost": credentials.get("vhost", "/"),
|
|
"ssl": credentials.get("ssl", False),
|
|
"verify_ssl": credentials.get("verify_ssl", True),
|
|
"integration_id": record_id,
|
|
}
|
|
)
|
|
except Exception as exc:
|
|
report_classify_failure(exc, logger=logger, integration="rabbitmq", record_id=record_id)
|
|
return None, None
|
|
if cfg.host and cfg.username:
|
|
return cfg, "rabbitmq"
|
|
return None, None
|