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
680 lines
24 KiB
Python
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
|