Files
wehub-resource-sync 4b6817381b
Benchmark image — build + push to ECR (any adapter) / build + push (push) Waiting to run
CI / quality (ubuntu-latest) (push) Waiting to run
CI / test (tools-runtime) (push) Waiting to run
CI / test (e2e-general) (push) Waiting to run
CI / test (cli-runtime) (push) Waiting to run
CI / test (e2e-provider-and-openclaw) (push) Waiting to run
CI / test (integrations-and-misc) (push) Waiting to run
CI / coverage-report (push) Blocked by required conditions
CI / test-kubernetes (push) Waiting to run
CI / should-run-thorough (push) Waiting to run
CI / test-thorough (cloudwatch-demo) (push) Blocked by required conditions
CI / test-thorough (flink-ecs) (push) Blocked by required conditions
CI / test-thorough (upstream-lambda) (push) Blocked by required conditions
CI / test-thorough (prefect-ecs-fargate) (push) Blocked by required conditions
CodeQL / Analyze (python) (push) Waiting to run
Release / build-binaries (zip, opensre.exe, onefile, windows-latest, windows-x64) (push) Blocked by required conditions
Release / publish-release (push) Blocked by required conditions
Release / publish-main-release (push) Blocked by required conditions
Release / prepare (push) Waiting to run
Release / verify (push) Blocked by required conditions
Release / build-python-dist (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-15-intel, darwin-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, macos-latest, darwin-arm64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04, linux-x64) (push) Blocked by required conditions
Release / build-binaries (tar.gz, opensre, onedir, ubuntu-22.04-arm, linux-arm64) (push) Blocked by required conditions
Synthetic Deterministic Tests / Synthetic offline (deterministic) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-checks (no-LLM) (push) Waiting to run
Interactive Shell Live (PR + post-merge) / turn-live shard ${{ matrix.shard_index }} (push) Waiting to run
CI (OpenClaw E2E) / openclaw test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:10:45 +08:00

680 lines
24 KiB
Python

"""Shared Azure SQL integration helpers.
Provides configuration, connectivity validation, and read-only diagnostic
queries for Azure SQL Database (managed) instances. All operations are
production-safe: read-only, timeouts enforced, result sizes capped.
Azure SQL Database is Microsoft's fully managed relational database service
built on SQL Server. The diagnostic queries leverage Azure-specific DMVs
(Dynamic Management Views) that surface throttling, resource governance,
and service-tier information unavailable in vanilla SQL Server.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Any
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.truncation import truncate
logger = logging.getLogger(__name__)
DEFAULT_AZURE_SQL_PORT = 1433
DEFAULT_AZURE_SQL_DRIVER = "ODBC Driver 18 for SQL Server"
DEFAULT_AZURE_SQL_TIMEOUT_SECONDS = 15.0
DEFAULT_AZURE_SQL_MAX_RESULTS = 50
_QUERY_TRUNCATE_LEN = 500
class AzureSQLConfig(StrictConfigModel):
"""Normalized Azure SQL Database connection settings."""
server: str = ""
database: str = ""
username: str = ""
password: str = ""
port: int = DEFAULT_AZURE_SQL_PORT
driver: str = DEFAULT_AZURE_SQL_DRIVER
encrypt: bool = True
timeout_seconds: float = Field(default=DEFAULT_AZURE_SQL_TIMEOUT_SECONDS, gt=0)
max_results: int = Field(default=DEFAULT_AZURE_SQL_MAX_RESULTS, gt=0, le=200)
integration_id: str = ""
@field_validator("server", mode="before")
@classmethod
def _normalize_server(cls, value: Any) -> str:
return str(value or "").strip()
@field_validator("database", mode="before")
@classmethod
def _normalize_database(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:
return str(value or "").strip()
@field_validator("driver", mode="before")
@classmethod
def _normalize_driver(cls, value: Any) -> str:
normalized = str(value or DEFAULT_AZURE_SQL_DRIVER).strip()
return normalized or DEFAULT_AZURE_SQL_DRIVER
@property
def is_configured(self) -> bool:
return bool(self.server and self.database)
@dataclass(frozen=True)
class AzureSQLValidationResult:
"""Result of validating an Azure SQL integration."""
ok: bool
detail: str
def build_azure_sql_config(raw: dict[str, Any] | None) -> AzureSQLConfig:
"""Build a normalized Azure SQL config object from env/store data."""
return AzureSQLConfig.model_validate(raw or {})
def azure_sql_config_from_env() -> AzureSQLConfig | None:
"""Load an Azure SQL config from env vars."""
server = os.getenv("AZURE_SQL_SERVER", "").strip()
database = os.getenv("AZURE_SQL_DATABASE", "").strip()
if not server or not database:
return None
_port = os.getenv("AZURE_SQL_PORT", "").strip()
return build_azure_sql_config(
{
"server": server,
"port": int(_port) if _port.isdigit() else DEFAULT_AZURE_SQL_PORT,
"database": database,
"username": os.getenv("AZURE_SQL_USERNAME", "").strip(),
"password": os.getenv("AZURE_SQL_PASSWORD", "").strip(),
"driver": os.getenv("AZURE_SQL_DRIVER", DEFAULT_AZURE_SQL_DRIVER).strip(),
"encrypt": os.getenv("AZURE_SQL_ENCRYPT", "true").strip().lower()
in ("true", "1", "yes"),
}
)
def resolve_azure_sql_config(
server: str,
database: str,
port: int = DEFAULT_AZURE_SQL_PORT,
) -> AzureSQLConfig:
"""Build a config for the given server/database, resolving credentials from store or env.
The LLM supplies only identifying params (server, database, port).
Credentials (username, password, driver, encrypt) are resolved from the stored
integration or environment variables so they never appear in tool signatures.
"""
from integrations.store import get_integration
stored = get_integration("azure_sql")
if stored:
creds = stored.get("credentials", {})
return build_azure_sql_config(
{
"server": server,
"port": creds.get("port", port),
"database": database,
"username": creds.get("username", ""),
"password": creds.get("password", ""),
"driver": creds.get("driver", DEFAULT_AZURE_SQL_DRIVER),
"encrypt": creds.get("encrypt", True),
}
)
env_cfg = azure_sql_config_from_env()
if env_cfg:
return build_azure_sql_config(
{
"server": server,
"port": port,
"database": database,
"username": env_cfg.username,
"password": env_cfg.password,
"driver": env_cfg.driver,
"encrypt": env_cfg.encrypt,
}
)
return build_azure_sql_config({"server": server, "port": port, "database": database})
def _get_connection(config: AzureSQLConfig) -> Any:
"""Create a pyodbc connection from config. Caller must close."""
import pyodbc
encrypt_value = "yes" if config.encrypt else "no"
def _esc(v: object) -> str:
return str(v).replace("}", "}}")
conn_str = (
f"DRIVER={{{config.driver}}};"
f"SERVER={config.server},{config.port};"
f"DATABASE={config.database};"
f"UID={{{_esc(config.username)}}};"
f"PWD={{{_esc(config.password)}}};"
f"Encrypt={encrypt_value};"
f"TrustServerCertificate=no;"
f"Connection Timeout={int(config.timeout_seconds)};"
f"APP=opensre;"
)
return pyodbc.connect(conn_str, timeout=int(config.timeout_seconds))
def validate_azure_sql_config(config: AzureSQLConfig) -> AzureSQLValidationResult:
"""Validate Azure SQL connectivity with a lightweight query."""
if not config.server:
return AzureSQLValidationResult(ok=False, detail="Azure SQL server is required.")
if not config.database:
return AzureSQLValidationResult(ok=False, detail="Azure SQL database is required.")
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
cursor.execute("SELECT @@VERSION")
version_info = cursor.fetchone()[0]
cursor.close()
# Extract meaningful version snippet
version = version_info.split("\n")[0] if version_info else "unknown"
return AzureSQLValidationResult(
ok=True,
detail=(f"Connected to {version}; target database: {config.database}."),
)
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="validate_azure_sql_config",
)
return AzureSQLValidationResult(ok=False, detail=f"Azure SQL connection failed: {err}")
def azure_sql_is_available(sources: dict[str, dict]) -> bool:
"""Check if Azure SQL integration identifying params are present."""
az = sources.get("azure_sql", {})
return bool(az.get("server") and az.get("database"))
def azure_sql_extract_params(sources: dict[str, dict]) -> dict[str, Any]:
"""Extract Azure SQL identifying params (server, database, port) from resolved integrations.
Credentials (username, password, driver, encrypt) are resolved internally by
``resolve_azure_sql_config`` from the integration store or environment, so
they never appear in tool signatures and are never seen by the LLM.
"""
az = sources.get("azure_sql", {})
return {
"server": str(az.get("server") or "").strip(),
"database": str(az.get("database") or "").strip(),
"port": int(az.get("port") or DEFAULT_AZURE_SQL_PORT),
}
# ---------------------------------------------------------------------------
# Read-only diagnostic queries (Azure SQL DMVs)
# ---------------------------------------------------------------------------
def get_server_status(config: AzureSQLConfig) -> dict[str, Any]:
"""Retrieve server status (connections, DTU/vCore usage, service tier).
Read-only: queries sys.dm_db_resource_stats and sys.database_service_objectives.
"""
if not config.is_configured:
return {"source": "azure_sql", "available": False, "error": "Not configured."}
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
# Get version
cursor.execute("SELECT @@VERSION")
version_info = cursor.fetchone()[0]
version = version_info.split("\n")[0] if version_info else "unknown"
# Get service tier and SLO
cursor.execute("""
SELECT
edition,
service_objective,
elastic_pool_name
FROM sys.database_service_objectives
""")
slo_row = cursor.fetchone()
edition = slo_row[0] if slo_row else "unknown"
service_objective = slo_row[1] if slo_row else "unknown"
elastic_pool = slo_row[2] if slo_row else None
# Get recent resource utilization (last 5 minutes)
cursor.execute("""
SELECT TOP 1
avg_cpu_percent,
avg_data_io_percent,
avg_log_write_percent,
avg_memory_usage_percent,
max_worker_percent,
max_session_percent,
end_time
FROM sys.dm_db_resource_stats
ORDER BY end_time DESC
""")
resource_row = cursor.fetchone()
# Get connection count
cursor.execute("""
SELECT
COUNT(*) as total_connections,
SUM(CASE WHEN status = 'running' THEN 1 ELSE 0 END) as active,
SUM(CASE WHEN status = 'sleeping' THEN 1 ELSE 0 END) as idle
FROM sys.dm_exec_sessions
WHERE is_user_process = 1
""")
conn_row = cursor.fetchone()
# Get database size
cursor.execute("""
SELECT
SUM(size * 8.0 / 1024) as size_mb
FROM sys.database_files
""")
size_row = cursor.fetchone()
cursor.close()
resource_stats: dict[str, Any] = {}
if resource_row:
resource_stats = {
"avg_cpu_percent": round(float(resource_row[0] or 0), 2),
"avg_data_io_percent": round(float(resource_row[1] or 0), 2),
"avg_log_write_percent": round(float(resource_row[2] or 0), 2),
"avg_memory_usage_percent": round(float(resource_row[3] or 0), 2),
"max_worker_percent": round(float(resource_row[4] or 0), 2),
"max_session_percent": round(float(resource_row[5] or 0), 2),
"sample_time": str(resource_row[6]) if resource_row[6] else None,
}
return {
"source": "azure_sql",
"available": True,
"version": version,
"service_tier": {
"edition": edition,
"service_objective": service_objective,
"elastic_pool": elastic_pool,
},
"connections": {
"total": conn_row[0] if conn_row else 0,
"active": conn_row[1] if conn_row else 0,
"idle": conn_row[2] if conn_row else 0,
},
"resource_utilization": resource_stats,
"database_size_mb": round(float(size_row[0] or 0), 2) if size_row else 0,
}
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="get_server_status",
)
return {"source": "azure_sql", "available": False, "error": str(err)}
def get_current_queries(
config: AzureSQLConfig,
threshold_seconds: int = 1,
) -> dict[str, Any]:
"""Retrieve currently running queries above a duration threshold.
Read-only: queries sys.dm_exec_requests and sys.dm_exec_sql_text.
Results are capped at config.max_results.
"""
if not config.is_configured:
return {"source": "azure_sql", "available": False, "error": "Not configured."}
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT TOP (?)
r.session_id,
s.login_name,
s.program_name,
s.host_name,
r.status,
r.start_time,
DATEDIFF(SECOND, r.start_time, GETDATE()) as duration_seconds,
r.wait_type,
r.wait_time,
r.cpu_time,
r.logical_reads,
r.writes,
LEFT(t.text, 500) as query_text
FROM sys.dm_exec_requests r
JOIN sys.dm_exec_sessions s ON r.session_id = s.session_id
OUTER APPLY sys.dm_exec_sql_text(r.sql_handle) t
WHERE s.is_user_process = 1
AND DATEDIFF(SECOND, r.start_time, GETDATE()) >= ?
ORDER BY r.start_time ASC
""",
(config.max_results, threshold_seconds),
)
queries = []
for row in cursor.fetchall():
queries.append(
{
"session_id": row[0],
"login_name": row[1] or "",
"program_name": row[2] or "",
"host_name": row[3] or "",
"status": row[4] or "",
"start_time": str(row[5]) if row[5] else "",
"duration_seconds": row[6] or 0,
"wait_type": row[7] or "",
"wait_time_ms": row[8] or 0,
"cpu_time_ms": row[9] or 0,
"logical_reads": row[10] or 0,
"writes": row[11] or 0,
"query_text": truncate(row[12] or "", _QUERY_TRUNCATE_LEN),
}
)
cursor.close()
return {
"source": "azure_sql",
"available": True,
"threshold_seconds": threshold_seconds,
"total_queries": len(queries),
"queries": queries,
}
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="get_current_queries",
)
return {"source": "azure_sql", "available": False, "error": str(err)}
def get_resource_stats(
config: AzureSQLConfig,
minutes: int = 30,
) -> dict[str, Any]:
"""Retrieve resource utilization history from sys.dm_db_resource_stats.
Azure SQL-specific: exposes DTU/vCore consumption, IO, log throughput,
and memory pressure over a rolling window. This is the primary view
for identifying throttling and tier-limit hits.
"""
if not config.is_configured:
return {"source": "azure_sql", "available": False, "error": "Not configured."}
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT
end_time,
avg_cpu_percent,
avg_data_io_percent,
avg_log_write_percent,
avg_memory_usage_percent,
max_worker_percent,
max_session_percent
FROM sys.dm_db_resource_stats
WHERE end_time >= DATEADD(MINUTE, -?, GETDATE())
ORDER BY end_time DESC
""",
(minutes,),
)
samples: list[dict[str, Any]] = []
for row in cursor.fetchall():
samples.append(
{
"end_time": str(row[0]) if row[0] else "",
"avg_cpu_percent": round(float(row[1] or 0), 2),
"avg_data_io_percent": round(float(row[2] or 0), 2),
"avg_log_write_percent": round(float(row[3] or 0), 2),
"avg_memory_usage_percent": round(float(row[4] or 0), 2),
"max_worker_percent": round(float(row[5] or 0), 2),
"max_session_percent": round(float(row[6] or 0), 2),
}
)
cursor.close()
# Compute summary
throttling_risk = "none"
if samples:
max_cpu: float = max(float(s["avg_cpu_percent"]) for s in samples)
max_io: float = max(float(s["avg_data_io_percent"]) for s in samples)
max_log: float = max(float(s["avg_log_write_percent"]) for s in samples)
max_workers: float = max(float(s["max_worker_percent"]) for s in samples)
max_memory: float = max(float(s["avg_memory_usage_percent"]) for s in samples)
peak: float = max(max_cpu, max_io, max_log, max_workers, max_memory)
if peak >= 95:
throttling_risk = "critical"
elif peak >= 80:
throttling_risk = "high"
elif peak >= 60:
throttling_risk = "moderate"
return {
"source": "azure_sql",
"available": True,
"window_minutes": minutes,
"total_samples": len(samples),
"throttling_risk": throttling_risk,
"samples": samples,
}
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="get_resource_stats",
)
return {"source": "azure_sql", "available": False, "error": str(err)}
def get_slow_queries(
config: AzureSQLConfig,
threshold_ms: int = 1000,
limit: int | None = None,
) -> dict[str, Any]:
"""Retrieve slow query statistics from sys.dm_exec_query_stats.
Read-only: queries the query stats DMV joined with sql_text.
Results capped at config.max_results, ordered by average elapsed time.
"""
if not config.is_configured:
return {"source": "azure_sql", "available": False, "error": "Not configured."}
effective_limit = min(limit or config.max_results, config.max_results)
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT TOP (?)
qs.query_hash,
LEFT(t.text, 500) as query_text,
qs.execution_count,
qs.total_elapsed_time / 1000.0 as total_time_ms,
(qs.total_elapsed_time / qs.execution_count) / 1000.0 as avg_time_ms,
qs.min_elapsed_time / 1000.0 as min_time_ms,
qs.max_elapsed_time / 1000.0 as max_time_ms,
qs.total_logical_reads,
qs.total_logical_writes,
qs.total_worker_time / 1000.0 as total_cpu_ms
FROM sys.dm_exec_query_stats qs
OUTER APPLY sys.dm_exec_sql_text(qs.sql_handle) t
WHERE (qs.total_elapsed_time / qs.execution_count) / 1000.0 >= ?
ORDER BY avg_time_ms DESC
""",
(effective_limit, threshold_ms),
)
queries = []
for row in cursor.fetchall():
queries.append(
{
"query_hash": str(row[0]) if row[0] else "",
"query_text": truncate(row[1] or "", _QUERY_TRUNCATE_LEN),
"execution_count": row[2] or 0,
"total_time_ms": round(float(row[3] or 0), 3),
"avg_time_ms": round(float(row[4] or 0), 3),
"min_time_ms": round(float(row[5] or 0), 3),
"max_time_ms": round(float(row[6] or 0), 3),
"total_logical_reads": row[7] or 0,
"total_logical_writes": row[8] or 0,
"total_cpu_ms": round(float(row[9] or 0), 3),
}
)
cursor.close()
return {
"source": "azure_sql",
"available": True,
"threshold_ms": threshold_ms,
"total_queries": len(queries),
"queries": queries,
}
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="get_slow_queries",
)
return {"source": "azure_sql", "available": False, "error": str(err)}
def get_wait_stats(config: AzureSQLConfig) -> dict[str, Any]:
"""Retrieve top wait statistics from sys.dm_db_wait_stats.
Azure SQL-specific DMV (not available in on-prem SQL Server).
Surfaces the most impactful wait types for diagnosing throttling,
lock contention, IO bottlenecks, and network issues.
"""
if not config.is_configured:
return {"source": "azure_sql", "available": False, "error": "Not configured."}
try:
conn = _get_connection(config)
try:
cursor = conn.cursor()
cursor.execute(
"""
SELECT TOP (?)
wait_type,
waiting_tasks_count,
wait_time_ms,
max_wait_time_ms,
signal_wait_time_ms
FROM sys.dm_db_wait_stats
WHERE wait_time_ms > 0
ORDER BY wait_time_ms DESC
""",
(config.max_results,),
)
waits = []
for row in cursor.fetchall():
waits.append(
{
"wait_type": row[0] or "",
"waiting_tasks_count": row[1] or 0,
"wait_time_ms": row[2] or 0,
"max_wait_time_ms": row[3] or 0,
"signal_wait_time_ms": row[4] or 0,
}
)
cursor.close()
return {
"source": "azure_sql",
"available": True,
"total_wait_types": len(waits),
"waits": waits,
}
finally:
conn.close()
except Exception as err:
report_validation_failure(
err,
logger=logger,
integration="azure_sql",
method="get_wait_stats",
)
return {"source": "azure_sql", "available": False, "error": str(err)}
def classify(
credentials: dict[str, Any], record_id: str
) -> tuple[AzureSQLConfig | None, str | None]:
try:
cfg = build_azure_sql_config(
{
"server": credentials.get("server", ""),
"port": credentials.get("port", 1433),
"database": credentials.get("database", ""),
"username": credentials.get("username", ""),
"password": credentials.get("password", ""),
"driver": credentials.get("driver", "ODBC Driver 18 for SQL Server"),
"encrypt": credentials.get("encrypt", True),
}
)
except Exception as exc:
report_classify_failure(exc, logger=logger, integration="azure_sql", record_id=record_id)
return None, None
if cfg.server and cfg.database:
return cfg, "azure_sql"
return None, None