Files
tracer-cloud--opensre/integrations/mongodb_atlas/__init__.py
T
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

585 lines
21 KiB
Python

"""Shared MongoDB Atlas integration helpers.
Provides configuration, connectivity validation, and read-only API queries
against the MongoDB Atlas Admin API v2. All operations are production-safe:
read-only, timeouts enforced, result sizes capped.
Atlas API reference: https://www.mongodb.com/docs/atlas/reference/api-resources-spec/v2/
Authentication: HTTP Digest with API public/private key pair.
"""
from __future__ import annotations
import logging
import os
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
logger = logging.getLogger(__name__)
DEFAULT_ATLAS_BASE_URL = "https://cloud.mongodb.com/api/atlas/v2"
DEFAULT_ATLAS_TIMEOUT = 15
DEFAULT_ATLAS_MAX_RESULTS = 50
class MongoDBAtlasConfig(StrictConfigModel):
"""Normalized MongoDB Atlas API connection settings."""
api_public_key: str = ""
api_private_key: str = ""
project_id: str = ""
base_url: str = DEFAULT_ATLAS_BASE_URL
timeout_seconds: float = Field(default=float(DEFAULT_ATLAS_TIMEOUT), gt=0)
max_results: int = Field(default=DEFAULT_ATLAS_MAX_RESULTS, gt=0, le=200)
integration_id: str = ""
@field_validator("api_public_key", mode="before")
@classmethod
def _normalize_public_key(cls, value: Any) -> str:
return str(value or "").strip()
@field_validator("api_private_key", mode="before")
@classmethod
def _normalize_private_key(cls, value: Any) -> str:
return str(value or "").strip()
@field_validator("project_id", mode="before")
@classmethod
def _normalize_project_id(cls, value: Any) -> str:
return str(value or "").strip()
@field_validator("base_url", mode="before")
@classmethod
def _normalize_base_url(cls, value: Any) -> str:
normalized = str(value or DEFAULT_ATLAS_BASE_URL).strip().rstrip("/")
return normalized or DEFAULT_ATLAS_BASE_URL
@property
def is_configured(self) -> bool:
return bool(self.api_public_key and self.api_private_key and self.project_id)
@dataclass(frozen=True)
class MongoDBAtlasValidationResult:
"""Result of validating a MongoDB Atlas integration."""
ok: bool
detail: str
def build_mongodb_atlas_config(raw: dict[str, Any] | None) -> MongoDBAtlasConfig:
"""Build a normalized MongoDB Atlas config object from env/store data."""
return MongoDBAtlasConfig.model_validate(raw or {})
def mongodb_atlas_config_from_env() -> MongoDBAtlasConfig | None:
"""Load a MongoDB Atlas config from env vars."""
public_key = os.getenv("MONGODB_ATLAS_PUBLIC_KEY", "").strip()
private_key = os.getenv("MONGODB_ATLAS_PRIVATE_KEY", "").strip()
project_id = os.getenv("MONGODB_ATLAS_PROJECT_ID", "").strip()
if not public_key or not private_key or not project_id:
return None
return build_mongodb_atlas_config(
{
"api_public_key": public_key,
"api_private_key": private_key,
"project_id": project_id,
"base_url": os.getenv("MONGODB_ATLAS_BASE_URL", DEFAULT_ATLAS_BASE_URL).strip(),
}
)
def _get_client(config: MongoDBAtlasConfig) -> httpx.Client:
"""Create an httpx client with Atlas Digest auth. Caller must close."""
return httpx.Client(
base_url=config.base_url,
auth=httpx.DigestAuth(config.api_public_key, config.api_private_key),
headers={
"Accept": "application/vnd.atlas.2025-03-12+json",
"Content-Type": "application/json",
},
timeout=config.timeout_seconds,
)
def validate_mongodb_atlas_config(
config: MongoDBAtlasConfig,
) -> MongoDBAtlasValidationResult:
"""Validate Atlas connectivity by listing project clusters."""
if not config.is_configured:
return MongoDBAtlasValidationResult(
ok=False,
detail="MongoDB Atlas API public key, private key, and project ID are required.",
)
try:
client = _get_client(config)
try:
resp = client.get(f"/groups/{config.project_id}/clusters", params={"itemsPerPage": 1})
resp.raise_for_status()
data = resp.json()
total = data.get("totalCount", 0)
return MongoDBAtlasValidationResult(
ok=True,
detail=f"Connected to Atlas project {config.project_id}; {total} cluster(s) found.",
)
finally:
client.close()
except httpx.HTTPStatusError as err:
return MongoDBAtlasValidationResult(
ok=False,
detail=f"Atlas API returned {err.response.status_code}: {err.response.text[:200]}",
)
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="validate_mongodb_atlas_config",
)
return MongoDBAtlasValidationResult(ok=False, detail=f"Atlas connection failed: {err}")
def get_clusters(config: MongoDBAtlasConfig) -> dict[str, Any]:
"""Retrieve all clusters in the Atlas project.
Read-only: GET /groups/{projectId}/clusters
"""
if not config.is_configured:
return {"source": "mongodb_atlas", "available": False, "error": "Not configured."}
try:
client = _get_client(config)
try:
resp = client.get(
f"/groups/{config.project_id}/clusters",
params={"itemsPerPage": config.max_results},
)
resp.raise_for_status()
data = resp.json()
clusters = []
for c in data.get("results", []):
clusters.append(
{
"name": c.get("name", ""),
"state": c.get("stateName", ""),
"cluster_type": c.get("clusterType", ""),
"mongo_db_version": c.get("mongoDBVersion", ""),
"connection_strings": {
"standard": c.get("connectionStrings", {}).get("standard", ""),
"standard_srv": c.get("connectionStrings", {}).get("standardSrv", ""),
},
"paused": c.get("paused", False),
"disk_size_gb": c.get("diskSizeGB"),
"replication_specs": [
{
"zone_name": rs.get("zoneName", ""),
"num_shards": rs.get("numShards", 1),
"region_configs": [
{
"provider": rc.get("providerName", ""),
"region": rc.get("regionName", ""),
"priority": rc.get("priority"),
"electable_nodes": rc.get("electableSpecs", {}).get(
"nodeCount", 0
),
"instance_size": rc.get("electableSpecs", {}).get(
"instanceSize", ""
),
}
for rc in rs.get("regionConfigs", [])
],
}
for rs in c.get("replicationSpecs", [])
],
}
)
return {
"source": "mongodb_atlas",
"available": True,
"total_clusters": data.get("totalCount", 0),
"clusters": clusters,
}
finally:
client.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="get_clusters",
)
return {"source": "mongodb_atlas", "available": False, "error": str(err)}
def get_alerts(
config: MongoDBAtlasConfig,
max_results: int | None = None,
) -> dict[str, Any]:
"""Retrieve open alerts for the Atlas project.
Read-only: GET /groups/{projectId}/alerts
"""
if not config.is_configured:
return {"source": "mongodb_atlas", "available": False, "error": "Not configured."}
effective_limit = min(max_results or config.max_results, config.max_results)
try:
client = _get_client(config)
try:
resp = client.get(
f"/groups/{config.project_id}/alerts",
params={"itemsPerPage": effective_limit, "status": "OPEN"},
)
resp.raise_for_status()
data = resp.json()
alerts = []
for a in data.get("results", []):
alerts.append(
{
"id": a.get("id", ""),
"event_type": a.get("eventTypeName", ""),
"status": a.get("status", ""),
"created": a.get("created", ""),
"updated": a.get("updated", ""),
"cluster_name": a.get("clusterName", ""),
"replica_set_name": a.get("replicaSetName", ""),
"metric_name": a.get("metricName", ""),
"current_value": a.get("currentValue", {}),
}
)
return {
"source": "mongodb_atlas",
"available": True,
"total_alerts": data.get("totalCount", 0),
"alerts": alerts,
}
finally:
client.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="get_alerts",
)
return {"source": "mongodb_atlas", "available": False, "error": str(err)}
def _resolve_primary_process(
client: httpx.Client,
config: MongoDBAtlasConfig,
cluster_name: str,
) -> dict[str, Any] | None:
"""Find the primary process for a cluster. Returns None if not found."""
resp = client.get(
f"/groups/{config.project_id}/processes",
params={"itemsPerPage": 500},
)
resp.raise_for_status()
processes = resp.json().get("results", [])
target: dict[str, Any] | None = None
for p in processes:
hostname = p.get("hostname", "")
if (
hostname.lower().startswith(cluster_name.lower() + "-")
or hostname.lower() == cluster_name.lower()
):
if p.get("typeName") == "REPLICA_PRIMARY":
target = p
break
if target is None:
target = p
return target
def atlas_is_available(sources: dict[str, dict]) -> bool:
"""Check if MongoDB Atlas integration credentials are present."""
atlas = sources.get("mongodb_atlas", {})
return bool(
atlas.get("api_public_key") and atlas.get("api_private_key") and atlas.get("project_id")
)
def atlas_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
"""Extract MongoDB Atlas credentials from resolved integrations."""
atlas = sources.get("mongodb_atlas", {})
return {
"api_public_key": atlas.get("api_public_key", ""),
"api_private_key": atlas.get("api_private_key", ""),
"project_id": atlas.get("project_id", ""),
"base_url": atlas.get("base_url", DEFAULT_ATLAS_BASE_URL),
}
def get_cluster_metrics(
config: MongoDBAtlasConfig,
cluster_name: str,
granularity: str = "PT1H",
period: str = "P1D",
) -> dict[str, Any]:
"""Retrieve process-level metrics for a cluster.
Read-only: GET /groups/{projectId}/processes/{processId}/measurements
First resolves processes for the cluster, then fetches metrics for the primary.
"""
if not config.is_configured:
return {"source": "mongodb_atlas", "available": False, "error": "Not configured."}
if not cluster_name:
return {"source": "mongodb_atlas", "available": False, "error": "cluster_name is required."}
try:
client = _get_client(config)
try:
target_process = _resolve_primary_process(client, config, cluster_name)
if not target_process:
return {
"source": "mongodb_atlas",
"available": True,
"note": f"No processes found for cluster '{cluster_name}'.",
"measurements": {},
}
process_id = f"{target_process['hostname']}:{target_process['port']}"
# Fetch key metrics
metric_names = [
"CONNECTIONS",
"OPCOUNTER_CMD",
"OPCOUNTER_QUERY",
"OPCOUNTER_INSERT",
"OPCOUNTER_UPDATE",
"OPCOUNTER_DELETE",
"QUERY_TARGETING_SCANNED_OBJECTS_PER_RETURNED",
"SYSTEM_CPU_USER",
"SYSTEM_MEMORY_USED",
"CACHE_USED_BYTES",
"DISK_PARTITION_IOPS_READ",
"DISK_PARTITION_IOPS_WRITE",
]
resp = client.get(
f"/groups/{config.project_id}/processes/{process_id}/measurements",
params={
"granularity": granularity,
"period": period,
"m": metric_names,
},
)
resp.raise_for_status()
data = resp.json()
measurements = {}
for m in data.get("measurements", []):
name = m.get("name", "")
data_points = m.get("dataPoints", [])
# Get the most recent non-null value
latest = None
for dp in reversed(data_points):
if dp.get("value") is not None:
latest = dp
break
if latest:
measurements[name] = {
"value": latest.get("value"),
"units": m.get("units", ""),
"timestamp": latest.get("timestamp", ""),
}
return {
"source": "mongodb_atlas",
"available": True,
"process_id": process_id,
"process_type": target_process.get("typeName", ""),
"mongo_version": target_process.get("version", ""),
"measurements": measurements,
}
finally:
client.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="get_cluster_metrics",
)
return {"source": "mongodb_atlas", "available": False, "error": str(err)}
def get_performance_advisor(
config: MongoDBAtlasConfig,
cluster_name: str,
max_results: int | None = None,
) -> dict[str, Any]:
"""Retrieve Performance Advisor slow query and index suggestions.
Read-only: GET /groups/{projectId}/processes/{processId}/performanceAdvisor/suggestedIndexes
"""
if not config.is_configured:
return {"source": "mongodb_atlas", "available": False, "error": "Not configured."}
if not cluster_name:
return {"source": "mongodb_atlas", "available": False, "error": "cluster_name is required."}
effective_limit = min(max_results or config.max_results, config.max_results)
try:
client = _get_client(config)
try:
target_process = _resolve_primary_process(client, config, cluster_name)
if not target_process:
return {
"source": "mongodb_atlas",
"available": True,
"note": f"No processes found for cluster '{cluster_name}'.",
"suggested_indexes": [],
"slow_queries": [],
}
process_id = f"{target_process['hostname']}:{target_process['port']}"
# Get suggested indexes
resp = client.get(
f"/groups/{config.project_id}/processes/{process_id}/performanceAdvisor/suggestedIndexes",
params={"nIndexes": effective_limit},
)
resp.raise_for_status()
index_data = resp.json()
suggested_indexes = []
for idx in index_data.get("suggestedIndexes", []):
suggested_indexes.append(
{
"namespace": idx.get("namespace", ""),
"index": idx.get("index", []),
"weight": idx.get("weight", 0),
"impact": idx.get("impact", []),
}
)
# Get slow queries
resp = client.get(
f"/groups/{config.project_id}/processes/{process_id}/performanceAdvisor/slowQueryLogs",
params={"nLogs": effective_limit},
)
resp.raise_for_status()
slow_data = resp.json()
slow_queries = []
for sq in slow_data.get("slowQueries", []):
slow_queries.append(
{
"namespace": sq.get("namespace", ""),
"line": sq.get("line", "")[:200],
"millis": sq.get("millis", 0),
}
)
return {
"source": "mongodb_atlas",
"available": True,
"process_id": process_id,
"total_suggested_indexes": len(suggested_indexes),
"suggested_indexes": suggested_indexes,
"total_slow_queries": len(slow_queries),
"slow_queries": slow_queries,
}
finally:
client.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="get_performance_advisor",
)
return {"source": "mongodb_atlas", "available": False, "error": str(err)}
def get_cluster_events(
config: MongoDBAtlasConfig,
cluster_name: str,
max_results: int | None = None,
) -> dict[str, Any]:
"""Retrieve recent events for the Atlas project filtered by cluster.
Read-only: GET /groups/{projectId}/events
"""
if not config.is_configured:
return {"source": "mongodb_atlas", "available": False, "error": "Not configured."}
effective_limit = min(max_results or config.max_results, config.max_results)
try:
client = _get_client(config)
try:
params: dict[str, Any] = {"itemsPerPage": effective_limit}
if cluster_name:
params["clusterName"] = cluster_name
resp = client.get(
f"/groups/{config.project_id}/events",
params=params,
)
resp.raise_for_status()
data = resp.json()
events = []
for e in data.get("results", []):
events.append(
{
"id": e.get("id", ""),
"event_type": e.get("eventTypeName", ""),
"created": e.get("created", ""),
"cluster_name": e.get("clusterName", ""),
"replica_set_name": e.get("replicaSetName", ""),
"is_global_admin": e.get("isGlobalAdmin", False),
"target_username": e.get("targetUsername", ""),
}
)
return {
"source": "mongodb_atlas",
"available": True,
"total_events": data.get("totalCount", 0),
"events": events,
}
finally:
client.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="mongodb_atlas",
method="get_cluster_events",
)
return {"source": "mongodb_atlas", "available": False, "error": str(err)}
def classify(
credentials: dict[str, Any], record_id: str
) -> tuple[MongoDBAtlasConfig | None, str | None]:
try:
cfg = build_mongodb_atlas_config(
{
"api_public_key": credentials.get("api_public_key", ""),
"api_private_key": credentials.get("api_private_key", ""),
"project_id": credentials.get("project_id", ""),
"base_url": credentials.get("base_url", DEFAULT_ATLAS_BASE_URL),
"integration_id": record_id,
}
)
except Exception as exc:
report_classify_failure(
exc, logger=logger, integration="mongodb_atlas", record_id=record_id
)
return None, None
if cfg.api_public_key and cfg.api_private_key and cfg.project_id:
return cfg, "mongodb_atlas"
return None, None