Build lightweight AI agent admin

This commit is contained in:
Codex
2026-06-08 18:14:59 +08:00
commit e164840f43
2530 changed files with 435693 additions and 0 deletions
@@ -0,0 +1,22 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""数据库监控模块"""
from core.database_monitor.api import get_database_configs
from core.database_monitor.config_resolver import (
create_monitor_collector,
list_monitor_configs,
resolve_monitor_target,
)
from core.database_monitor.database_collector import AsyncDatabaseCollector
DatabaseCollector = AsyncDatabaseCollector
__all__ = [
"AsyncDatabaseCollector",
"DatabaseCollector",
"create_monitor_collector",
"get_database_configs",
"list_monitor_configs",
"resolve_monitor_target",
]
@@ -0,0 +1,106 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库监控 API
监控维度与「数据库连接管理」一致:每个启用连接(code)为一个监控目标,
凭证通过 ConnectionResolver 解析,支持 PostgreSQL / MySQL / SQL Server / Oracle。
"""
import logging
from typing import List, Optional
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.database import get_db
from core.database_monitor.config_resolver import (
create_monitor_collector,
list_monitor_configs,
resolve_monitor_target,
)
from core.database_monitor.schema import (
DatabaseConfigSchema,
DatabaseConnectionTestSchema,
DatabaseOverviewSchema,
DatabaseRealtimeStatsSchema,
)
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/database_monitor", tags=["数据库监控"])
async def get_database_configs(db: AsyncSession) -> List[dict]:
"""获取全部可监控连接配置(供 WebSocket 等模块复用)。"""
return await list_monitor_configs(db)
@router.get("/configs", response_model=List[DatabaseConfigSchema], summary="获取数据库监控目标列表")
async def get_database_monitor_configs(db: AsyncSession = Depends(get_db)):
"""获取已注册的数据库连接列表,作为监控目标。"""
configs = await list_monitor_configs(db)
return [
DatabaseConfigSchema(
name=config["name"],
db_name=config["db_name"],
db_type=config["db_type"],
host=config["host"],
port=config["port"],
database=config["database"],
user=config["user"],
has_password=config["has_password"],
is_system=config.get("is_system", False),
)
for config in configs
]
@router.get("/{connection_code}/overview", response_model=DatabaseOverviewSchema, summary="获取数据库概览信息")
async def get_database_overview(
connection_code: str,
database: Optional[str] = Query(None, description="覆盖连接默认库/服务名"),
db: AsyncSession = Depends(get_db),
):
"""获取指定连接的监控概览。"""
try:
target = await resolve_monitor_target(db, connection_code, database)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
collector = create_monitor_collector(target)
data = await collector.get_all_info(connection_code, target["name"])
return DatabaseOverviewSchema(**data)
@router.get("/{connection_code}/realtime", response_model=DatabaseRealtimeStatsSchema, summary="获取数据库实时统计")
async def get_database_realtime_stats(
connection_code: str,
database: Optional[str] = Query(None, description="覆盖连接默认库/服务名"),
db: AsyncSession = Depends(get_db),
):
"""获取指定连接的实时统计。"""
try:
target = await resolve_monitor_target(db, connection_code, database)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
collector = create_monitor_collector(target)
data = await collector.get_realtime_stats(connection_code)
return DatabaseRealtimeStatsSchema(**data)
@router.post("/{connection_code}/test", response_model=DatabaseConnectionTestSchema, summary="测试数据库连接")
async def test_database_connection(
connection_code: str,
database: Optional[str] = Query(None, description="覆盖连接默认库/服务名"),
db: AsyncSession = Depends(get_db),
):
"""测试指定连接是否可用。"""
try:
target = await resolve_monitor_target(db, connection_code, database)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
collector = create_monitor_collector(target)
result = await collector.test_connection()
return DatabaseConnectionTestSchema(**result)
@@ -0,0 +1,91 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""数据库监控配置解析:与数据库连接管理统一,按连接 code 监控目标库。"""
from typing import Any, Dict, List, Optional
from sqlalchemy.ext.asyncio import AsyncSession
from core.database_connection.resolver import ConnectionResolver
from core.database_connection.service import DatabaseConnectionService
from core.database_monitor.database_collector import AsyncDatabaseCollector
def normalize_monitor_db_type(db_type: str) -> str:
upper = (db_type or "").upper()
if upper in ("MSSQL", "SQL SERVER"):
return "SQLSERVER"
return upper
def default_database_for_type(db_type: str) -> str:
db = (db_type or "").lower()
if db == "postgresql":
return "postgres"
if db == "sqlserver":
return "master"
if db == "oracle":
return "ORCL"
return ""
async def list_monitor_configs(db: AsyncSession) -> List[Dict[str, Any]]:
"""列出可监控的数据库连接(不含密码)。"""
configs = await DatabaseConnectionService.get_manager_configs(db)
results: List[Dict[str, Any]] = []
for item in configs:
db_type = normalize_monitor_db_type(item["db_type"])
database = item.get("database") or default_database_for_type(item["db_type"])
results.append(
{
"name": item.get("display_name") or item.get("name") or item["db_name"],
"db_name": item["db_name"],
"db_type": db_type,
"host": item["host"],
"port": item["port"],
"database": database,
"user": item.get("user") or "",
"has_password": bool(item.get("has_password")),
"is_system": bool(item.get("is_system")),
}
)
return results
async def resolve_monitor_target(
db: AsyncSession,
connection_code: str,
database: Optional[str] = None,
) -> Dict[str, Any]:
"""解析监控目标完整配置(含密码与 extra_options)。"""
info = await ConnectionResolver.resolve(connection_code, db)
target_database = (
database
or info.database
or default_database_for_type(info.db_type)
)
return {
"name": info.display_name or connection_code,
"db_name": connection_code,
"db_type": normalize_monitor_db_type(info.db_type),
"host": info.host,
"port": info.port,
"user": info.user,
"password": info.password,
"database": target_database,
"extra_options": info.extra_options or {},
"is_system": info.is_system,
"has_password": bool(info.password),
}
def create_monitor_collector(config: Dict[str, Any]) -> AsyncDatabaseCollector:
"""根据解析后的配置创建采集器。"""
return AsyncDatabaseCollector(
db_type=config["db_type"],
host=config["host"],
port=config["port"],
user=config["user"],
password=config["password"],
database=config["database"],
extra_options=config.get("extra_options"),
)
@@ -0,0 +1,986 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库信息收集器(异步版本)
"""
import logging
import time
from datetime import datetime, timedelta, timezone
from typing import Dict, List, Any, Optional
import asyncpg
from core.database_manager.service import aiomysql_connect, log_database_connect_failure
from core.database_manager.handlers.pools import build_mssql_dsn, build_oracle_dsn
logger = logging.getLogger(__name__)
# MySQL异步驱动
try:
import aiomysql
AIOMYSQL_AVAILABLE = True
except ImportError:
AIOMYSQL_AVAILABLE = False
try:
import aioodbc
AIOODBC_AVAILABLE = True
except ImportError:
AIOODBC_AVAILABLE = False
try:
import oracledb
ORACLEDB_AVAILABLE = True
except ImportError:
ORACLEDB_AVAILABLE = False
def _normalize_db_type(db_type: str) -> str:
upper = (db_type or "").upper()
if upper in ("MSSQL", "SQL SERVER"):
return "SQLSERVER"
return upper
def serialize_data(data: Any) -> Any:
"""递归地序列化数据,处理datetime、decimal等类型"""
import decimal
if isinstance(data, (datetime,)):
return data.isoformat()
elif isinstance(data, decimal.Decimal):
return float(data)
elif isinstance(data, (bytes,)):
try:
return data.decode('utf-8')
except UnicodeDecodeError:
return str(data)
elif isinstance(data, dict):
return {serialize_data(k): serialize_data(v) for k, v in data.items()}
elif isinstance(data, list):
return [serialize_data(item) for item in data]
elif isinstance(data, tuple):
return tuple(serialize_data(item) for item in data)
else:
return data
class AsyncDatabaseCollector:
"""异步数据库信息收集器"""
def __init__(self, db_type: str, host: str, port: int,
user: str, password: str, database: str, **kwargs):
self.db_type = _normalize_db_type(db_type)
self.host = host
self.port = port
self.user = user
self.password = password
self.database = database
self.kwargs = kwargs
self.connection = None
self._last_connect_error: Optional[str] = None
async def connect(self) -> bool:
"""连接数据库"""
try:
if self.db_type == 'POSTGRESQL':
result = await self._connect_postgresql()
elif self.db_type == 'MYSQL':
result = await self._connect_mysql()
elif self.db_type == 'SQLSERVER':
result = await self._connect_sqlserver()
elif self.db_type == 'ORACLE':
result = await self._connect_oracle()
else:
self._last_connect_error = f"Unsupported database type: {self.db_type}"
logger.error(self._last_connect_error)
return False
if result:
self._last_connect_error = None
return result
except Exception as e:
self._last_connect_error = str(e).strip() or e.__class__.__name__
log_database_connect_failure(
db_type=self.db_type.lower(),
host=self.host,
port=self.port,
user=self.user,
database=self.database,
detail=self._last_connect_error,
)
return False
async def _connect_postgresql(self) -> bool:
"""连接PostgreSQL"""
self.connection = await asyncpg.connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
database=self.database,
timeout=5
)
return True
async def _connect_mysql(self) -> bool:
"""连接MySQL"""
if not AIOMYSQL_AVAILABLE:
self._last_connect_error = "aiomysql 未安装,无法连接 MySQL"
logger.error(self._last_connect_error)
return False
self.connection = await aiomysql_connect(
host=self.host,
port=self.port,
user=self.user,
password=self.password,
db=self.database,
charset='utf8mb4',
connect_timeout=5,
)
return True
async def _connect_sqlserver(self) -> bool:
"""连接 SQL Server"""
if not AIOODBC_AVAILABLE:
self._last_connect_error = "aioodbc 未安装,无法连接 SQL Server"
logger.error(self._last_connect_error)
return False
dsn = build_mssql_dsn(
self.host,
self.port,
self.user,
self.password,
self.database or "master",
self.kwargs.get("extra_options"),
)
self.connection = await aioodbc.connect(dsn=dsn, autocommit=True, timeout=5)
return True
async def _connect_oracle(self) -> bool:
"""连接 Oracle"""
if not ORACLEDB_AVAILABLE:
self._last_connect_error = "oracledb 未安装,无法连接 Oracle"
logger.error(self._last_connect_error)
return False
service = self.database or self.kwargs.get("service_name") or "ORCL"
dsn = build_oracle_dsn(self.host, self.port, service)
self.connection = await oracledb.connect_async(
user=self.user,
password=self.password,
dsn=dsn,
)
return True
async def _execute_odbc_query(
self, query: str, params: tuple = ()
) -> List[Dict[str, Any]]:
async with self.connection.cursor() as cursor:
await cursor.execute(query, params)
if not cursor.description:
return []
columns = [col[0] for col in cursor.description]
rows = await cursor.fetchall()
return [dict(zip(columns, row)) for row in rows]
async def _fetch_odbc_value(self, query: str, params: tuple = ()) -> Any:
rows = await self._execute_odbc_query(query, params)
if not rows:
return None
return next(iter(rows[0].values()))
async def _execute_oracle_query(
self, query: str, params: Optional[Dict[str, Any]] = None
) -> List[Dict[str, Any]]:
cursor = self.connection.cursor()
try:
await cursor.execute(query, params or {})
if not cursor.description:
return []
columns = [col[0].lower() for col in cursor.description]
rows = await cursor.fetchall()
return [dict(zip(columns, row)) for row in rows]
finally:
await cursor.close()
async def _fetch_oracle_value(
self, query: str, params: Optional[Dict[str, Any]] = None
) -> Any:
rows = await self._execute_oracle_query(query, params)
if not rows:
return None
return next(iter(rows[0].values()))
async def disconnect(self):
"""断开连接"""
if self.connection:
try:
await self.connection.close()
except Exception as e:
logger.error(f"Error disconnecting from database: {e}")
finally:
self.connection = None
async def test_connection(self) -> Dict[str, Any]:
"""测试数据库连接"""
start_time = time.time()
try:
if await self.connect():
response_time = (time.time() - start_time) * 1000
version = await self._get_version()
return {
'success': True,
'message': '连接成功',
'response_time': round(response_time, 2),
'version': version,
'db_type': self.db_type
}
else:
target = f"{self.host}:{self.port}, db={self.database}"
detail = self._last_connect_error or "未知错误"
message = f'连接失败 ({target}): {detail}'
log_database_connect_failure(
db_type=self.db_type.lower(),
host=self.host,
port=self.port,
user=self.user,
database=self.database,
detail=detail,
action="monitor_test",
)
return {
'success': False,
'message': message,
'response_time': None,
'version': None,
'db_type': self.db_type
}
except Exception as e:
return {
'success': False,
'message': f'连接错误: {str(e)}',
'response_time': None,
'version': None,
'db_type': self.db_type
}
finally:
await self.disconnect()
async def _get_version(self) -> str:
"""获取数据库版本"""
try:
if self.db_type == 'POSTGRESQL':
result = await self.connection.fetchval("SELECT version()")
return result
elif self.db_type == 'MYSQL':
async with self.connection.cursor() as cursor:
await cursor.execute("SELECT VERSION()")
result = await cursor.fetchone()
return result[0]
elif self.db_type == 'SQLSERVER':
return await self._fetch_odbc_value("SELECT @@VERSION")
elif self.db_type == 'ORACLE':
return await self._fetch_oracle_value(
"SELECT banner FROM v$version WHERE ROWNUM = 1"
)
return 'Unknown'
except Exception as e:
logger.error(f"Error getting database version: {e}")
return 'Unknown'
async def get_basic_info(self) -> Dict[str, Any]:
"""获取数据库基本信息"""
if not self.connection:
if not await self.connect():
return {}
try:
info = {
'db_type': self.db_type,
'host': self.host,
'port': self.port,
'database': self.database,
'version': await self._get_version(),
'uptime': await self._get_uptime(),
'timezone': await self._get_timezone(),
'charset': await self._get_charset(),
}
return serialize_data(info)
except Exception as e:
logger.error(f"Error getting basic info: {e}")
return {}
async def _get_uptime(self) -> str:
"""获取数据库运行时间"""
try:
if self.db_type == 'POSTGRESQL':
start_time = await self.connection.fetchval("SELECT pg_postmaster_start_time()")
current_time = datetime.now(timezone.utc)
if start_time.tzinfo is None:
start_time = start_time.replace(tzinfo=timezone.utc)
uptime = current_time - start_time
return str(uptime)
elif self.db_type == 'MYSQL':
async with self.connection.cursor() as cursor:
await cursor.execute("SHOW GLOBAL STATUS LIKE 'Uptime'")
result = await cursor.fetchone()
if result:
uptime_seconds = int(result[1])
uptime = timedelta(seconds=uptime_seconds)
return str(uptime)
elif self.db_type == 'SQLSERVER':
start_time = await self._fetch_odbc_value(
"SELECT sqlserver_start_time FROM sys.dm_os_sys_info"
)
if start_time:
current_time = datetime.now(timezone.utc)
if getattr(start_time, "tzinfo", None) is None:
start_time = start_time.replace(tzinfo=timezone.utc)
return str(current_time - start_time)
elif self.db_type == 'ORACLE':
start_time = await self._fetch_oracle_value(
"SELECT startup_time FROM v$instance"
)
if start_time:
current_time = datetime.now(timezone.utc)
if getattr(start_time, "tzinfo", None) is None:
start_time = start_time.replace(tzinfo=timezone.utc)
return str(current_time - start_time)
return 'Unknown'
except Exception as e:
logger.error(f"Error getting uptime: {e}")
return 'Unknown'
async def _get_timezone(self) -> str:
"""获取数据库时区"""
try:
if self.db_type == 'POSTGRESQL':
result = await self.connection.fetchval("SHOW timezone")
return result
elif self.db_type == 'MYSQL':
async with self.connection.cursor() as cursor:
await cursor.execute("SELECT @@global.time_zone")
result = await cursor.fetchone()
return result[0]
elif self.db_type == 'SQLSERVER':
return await self._fetch_odbc_value(
"SELECT CAST(SYSDATETIMEOFFSET() AS NVARCHAR(50))"
) or "Unknown"
elif self.db_type == 'ORACLE':
return await self._fetch_oracle_value(
"SELECT DBTIMEZONE FROM dual"
) or "Unknown"
return 'Unknown'
except Exception as e:
logger.error(f"Error getting timezone: {e}")
return 'Unknown'
async def _get_charset(self) -> str:
"""获取数据库字符集"""
try:
if self.db_type == 'POSTGRESQL':
result = await self.connection.fetchval(
"SELECT pg_encoding_to_char(encoding) FROM pg_database WHERE datname = $1",
self.database
)
return result
elif self.db_type == 'MYSQL':
async with self.connection.cursor() as cursor:
await cursor.execute("SELECT @@character_set_database")
result = await cursor.fetchone()
return result[0]
elif self.db_type == 'SQLSERVER':
return await self._fetch_odbc_value(
"SELECT DATABASEPROPERTYEX(DB_NAME(), 'Collation')"
) or "Unknown"
elif self.db_type == 'ORACLE':
return await self._fetch_oracle_value(
"""
SELECT value
FROM nls_database_parameters
WHERE parameter = 'NLS_CHARACTERSET'
"""
) or "Unknown"
return 'Unknown'
except Exception as e:
logger.error(f"Error getting charset: {e}")
return 'Unknown'
async def get_connection_info(self) -> Dict[str, Any]:
"""获取连接信息"""
if not self.connection:
if not await self.connect():
return {}
try:
if self.db_type == 'POSTGRESQL':
return await self._get_postgresql_connections()
elif self.db_type == 'MYSQL':
return await self._get_mysql_connections()
elif self.db_type == 'SQLSERVER':
return await self._get_sqlserver_connections()
elif self.db_type == 'ORACLE':
return await self._get_oracle_connections()
return {}
except Exception as e:
logger.error(f"Error getting connection info: {e}")
return {}
async def _get_postgresql_connections(self) -> Dict[str, Any]:
"""获取PostgreSQL连接信息"""
total_connections = await self.connection.fetchval(
"SELECT COUNT(*) FROM pg_stat_activity"
)
max_connections = await self.connection.fetchval("SHOW max_connections")
max_connections = int(max_connections)
active_connections = await self.connection.fetchval(
"SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'active'"
)
idle_connections = await self.connection.fetchval(
"SELECT COUNT(*) FROM pg_stat_activity WHERE state = 'idle'"
)
return {
'total_connections': total_connections,
'max_connections': max_connections,
'active_connections': active_connections,
'idle_connections': idle_connections,
'connection_usage_percent': round((total_connections / max_connections) * 100, 2) if max_connections > 0 else 0.0
}
async def _get_mysql_connections(self) -> Dict[str, Any]:
"""获取MySQL连接信息"""
async with self.connection.cursor() as cursor:
await cursor.execute("SHOW STATUS LIKE 'Threads_connected'")
result = await cursor.fetchone()
total_connections = int(result[1])
await cursor.execute("SHOW VARIABLES LIKE 'max_connections'")
result = await cursor.fetchone()
max_connections = int(result[1])
await cursor.execute("SHOW STATUS LIKE 'Threads_running'")
result = await cursor.fetchone()
active_connections = int(result[1])
idle_connections = total_connections - active_connections
return {
'total_connections': total_connections,
'max_connections': max_connections,
'active_connections': active_connections,
'idle_connections': idle_connections,
'connection_usage_percent': round((total_connections / max_connections) * 100, 2) if max_connections > 0 else 0.0
}
async def get_database_size(self) -> Dict[str, Any]:
"""获取数据库大小信息"""
if not self.connection:
if not await self.connect():
return {}
try:
if self.db_type == 'POSTGRESQL':
return await self._get_postgresql_size()
elif self.db_type == 'MYSQL':
return await self._get_mysql_size()
elif self.db_type == 'SQLSERVER':
return await self._get_sqlserver_size()
elif self.db_type == 'ORACLE':
return await self._get_oracle_size()
return {}
except Exception as e:
logger.error(f"Error getting database size: {e}")
return {}
async def _get_postgresql_size(self) -> Dict[str, Any]:
"""获取PostgreSQL数据库大小"""
size_bytes = await self.connection.fetchval(
"SELECT pg_database_size($1)", self.database
)
return {
'database_size_bytes': size_bytes,
'database_size_mb': round(size_bytes / 1024 / 1024, 2),
'database_size_gb': round(size_bytes / 1024 / 1024 / 1024, 2)
}
async def _get_mysql_size(self) -> Dict[str, Any]:
"""获取MySQL数据库大小"""
async with self.connection.cursor() as cursor:
await cursor.execute("""
SELECT SUM(data_length + index_length) AS size_bytes
FROM information_schema.tables
WHERE table_schema = %s
""", (self.database,))
result = await cursor.fetchone()
size_bytes = result[0] if result[0] else 0
return {
'database_size_bytes': size_bytes,
'database_size_mb': round(size_bytes / 1024 / 1024, 2),
'database_size_gb': round(size_bytes / 1024 / 1024 / 1024, 2)
}
async def get_performance_stats(self) -> Dict[str, Any]:
"""获取性能统计信息"""
if not self.connection:
if not await self.connect():
return {'cache_hit_ratio': 0.0}
try:
if self.db_type == 'POSTGRESQL':
return await self._get_postgresql_performance()
elif self.db_type == 'MYSQL':
return await self._get_mysql_performance()
elif self.db_type == 'SQLSERVER':
return await self._get_sqlserver_performance()
elif self.db_type == 'ORACLE':
return await self._get_oracle_performance()
return {'cache_hit_ratio': 0.0}
except Exception as e:
logger.error(f"Error getting performance stats: {e}")
return {'cache_hit_ratio': 0.0}
async def _get_postgresql_performance(self) -> Dict[str, Any]:
"""获取PostgreSQL性能统计"""
stats = await self.connection.fetchrow("""
SELECT SUM(numbackends) AS total_backends,
SUM(xact_commit) AS transactions_commit,
SUM(xact_rollback) AS transactions_rollback,
SUM(blks_read) AS blocks_read,
SUM(blks_hit) AS blocks_hit,
SUM(tup_returned) AS tuples_returned,
SUM(tup_fetched) AS tuples_fetched,
SUM(tup_inserted) AS tuples_inserted,
SUM(tup_updated) AS tuples_updated,
SUM(tup_deleted) AS tuples_deleted,
SUM(temp_files) AS temp_files,
SUM(temp_bytes) AS temp_bytes,
SUM(deadlocks) AS deadlocks
FROM pg_stat_database
WHERE datname = $1
""", self.database)
blocks_read = stats['blocks_read'] or 0
blocks_hit = stats['blocks_hit'] or 0
total_reads = blocks_read + blocks_hit
cache_hit_ratio = (blocks_hit / total_reads * 100) if total_reads > 0 else 0
return {
'total_backends': stats['total_backends'] or 0,
'transactions_commit': stats['transactions_commit'] or 0,
'transactions_rollback': stats['transactions_rollback'] or 0,
'blocks_read': blocks_read,
'blocks_hit': blocks_hit,
'cache_hit_ratio': round(cache_hit_ratio, 2),
'tuples_returned': stats['tuples_returned'] or 0,
'tuples_fetched': stats['tuples_fetched'] or 0,
'tuples_inserted': stats['tuples_inserted'] or 0,
'tuples_updated': stats['tuples_updated'] or 0,
'tuples_deleted': stats['tuples_deleted'] or 0,
'temp_files': stats['temp_files'] or 0,
'temp_bytes': stats['temp_bytes'] or 0,
'deadlocks': stats['deadlocks'] or 0,
}
async def _get_mysql_performance(self) -> Dict[str, Any]:
"""获取MySQL性能统计"""
stats = {}
status_queries = [
('queries', 'Queries'),
('connections', 'Connections'),
('slow_queries', 'Slow_queries'),
('bytes_received', 'Bytes_received'),
('bytes_sent', 'Bytes_sent'),
('innodb_buffer_pool_reads', 'Innodb_buffer_pool_reads'),
('innodb_buffer_pool_read_requests', 'Innodb_buffer_pool_read_requests')
]
async with self.connection.cursor() as cursor:
for stat_name, mysql_var in status_queries:
await cursor.execute(f"SHOW GLOBAL STATUS LIKE '{mysql_var}'")
result = await cursor.fetchone()
stats[stat_name] = int(result[1]) if result else 0
read_requests = stats['innodb_buffer_pool_read_requests']
reads = stats['innodb_buffer_pool_reads']
cache_hit_ratio = ((read_requests - reads) / read_requests * 100) if read_requests > 0 else 0
return {
'total_queries': stats['queries'],
'total_connections': stats['connections'],
'slow_queries': stats['slow_queries'],
'bytes_received': stats['bytes_received'],
'bytes_sent': stats['bytes_sent'],
'cache_hit_ratio': round(cache_hit_ratio, 2)
}
async def get_table_stats(self) -> List[Dict[str, Any]]:
"""获取表统计信息"""
if not self.connection:
if not await self.connect():
return []
try:
if self.db_type == 'POSTGRESQL':
return await self._get_postgresql_tables()
elif self.db_type == 'MYSQL':
return await self._get_mysql_tables()
elif self.db_type == 'SQLSERVER':
return await self._get_sqlserver_tables()
elif self.db_type == 'ORACLE':
return await self._get_oracle_tables()
return []
except Exception as e:
logger.error(f"Error getting table stats: {e}")
return []
async def _get_postgresql_tables(self) -> List[Dict[str, Any]]:
"""获取PostgreSQL表统计"""
rows = await self.connection.fetch("""
SELECT st.schemaname,
st.relname AS tablename,
st.n_tup_ins AS inserts,
st.n_tup_upd AS updates,
st.n_tup_del AS deletes,
st.n_live_tup AS live_tuples,
st.n_dead_tup AS dead_tuples,
st.seq_scan AS sequential_scans,
st.idx_scan AS index_scans,
COALESCE(PG_SIZE_PRETTY(PG_RELATION_SIZE(c.oid)), '0 bytes') AS size,
COALESCE(PG_RELATION_SIZE(c.oid), 0) AS size_bytes,
COALESCE(PG_SIZE_PRETTY(PG_TOTAL_RELATION_SIZE(c.oid)), '0 bytes') AS total_size,
COALESCE(PG_TOTAL_RELATION_SIZE(c.oid), 0) AS total_size_bytes
FROM pg_stat_user_tables st
JOIN pg_class c ON c.relname = st.relname
AND c.relnamespace = (SELECT oid FROM pg_namespace WHERE nspname = st.schemaname)
WHERE c.relkind = 'r'
ORDER BY COALESCE(PG_TOTAL_RELATION_SIZE(c.oid), 0) DESC
LIMIT 20
""")
return [dict(row) for row in rows]
async def _get_mysql_tables(self) -> List[Dict[str, Any]]:
"""获取MySQL表统计"""
async with self.connection.cursor() as cursor:
await cursor.execute("""
SELECT table_name,
table_rows,
data_length,
index_length,
(data_length + index_length) AS total_size,
auto_increment
FROM information_schema.tables
WHERE table_schema = %s
ORDER BY (data_length + index_length) DESC
LIMIT 20
""", (self.database,))
tables = []
rows = await cursor.fetchall()
for row in rows:
tables.append({
'table_name': row[0],
'table_rows': row[1] or 0,
'data_length': row[2] or 0,
'index_length': row[3] or 0,
'total_size': row[4] or 0,
'auto_increment': row[5] or 0
})
return tables
async def _get_sqlserver_connections(self) -> Dict[str, Any]:
"""获取 SQL Server 连接信息"""
total_connections = int(
await self._fetch_odbc_value(
"SELECT COUNT(*) FROM sys.dm_exec_sessions WHERE is_user_process = 1"
)
or 0
)
max_connections = int(await self._fetch_odbc_value("SELECT @@MAX_CONNECTIONS") or 0)
active_connections = int(
await self._fetch_odbc_value(
"""
SELECT COUNT(*)
FROM sys.dm_exec_sessions
WHERE is_user_process = 1
AND status = 'running'
"""
)
or 0
)
idle_connections = max(total_connections - active_connections, 0)
return {
"total_connections": total_connections,
"max_connections": max_connections,
"active_connections": active_connections,
"idle_connections": idle_connections,
"connection_usage_percent": round(
(total_connections / max_connections) * 100, 2
)
if max_connections > 0
else 0.0,
}
async def _get_oracle_connections(self) -> Dict[str, Any]:
"""获取 Oracle 连接信息"""
total_connections = int(
await self._fetch_oracle_value(
"SELECT COUNT(*) FROM v$session WHERE type != 'BACKGROUND'"
)
or 0
)
max_connections = int(
await self._fetch_oracle_value(
"SELECT value FROM v$parameter WHERE name = 'processes'"
)
or 0
)
active_connections = int(
await self._fetch_oracle_value(
"""
SELECT COUNT(*)
FROM v$session
WHERE type != 'BACKGROUND'
AND status = 'ACTIVE'
"""
)
or 0
)
idle_connections = max(total_connections - active_connections, 0)
return {
"total_connections": total_connections,
"max_connections": max_connections,
"active_connections": active_connections,
"idle_connections": idle_connections,
"connection_usage_percent": round(
(total_connections / max_connections) * 100, 2
)
if max_connections > 0
else 0.0,
}
async def _get_sqlserver_size(self) -> Dict[str, Any]:
"""获取 SQL Server 数据库大小"""
size_bytes = int(
await self._fetch_odbc_value(
"SELECT SUM(size) * 8192 FROM sys.database_files"
)
or 0
)
return {
"database_size_bytes": size_bytes,
"database_size_mb": round(size_bytes / 1024 / 1024, 2),
"database_size_gb": round(size_bytes / 1024 / 1024 / 1024, 2),
}
async def _get_oracle_size(self) -> Dict[str, Any]:
"""获取 Oracle 用户 Schema 占用空间"""
size_bytes = int(
await self._fetch_oracle_value(
"SELECT NVL(SUM(bytes), 0) FROM user_segments"
)
or 0
)
return {
"database_size_bytes": size_bytes,
"database_size_mb": round(size_bytes / 1024 / 1024, 2),
"database_size_gb": round(size_bytes / 1024 / 1024 / 1024, 2),
}
async def _get_sqlserver_performance(self) -> Dict[str, Any]:
"""获取 SQL Server 性能统计"""
counters = await self._execute_odbc_query(
"""
SELECT RTRIM(counter_name) AS counter_name, cntr_value
FROM sys.dm_os_performance_counters
WHERE (counter_name = 'Batch Requests/sec' AND instance_name = '')
OR (counter_name = 'Page life expectancy'
AND object_name LIKE '%Buffer Manager%'
AND instance_name = '')
OR (counter_name = 'Buffer cache hit ratio'
AND object_name LIKE '%Buffer Manager%'
AND instance_name = '')
"""
)
stats = {row["counter_name"]: row["cntr_value"] for row in counters}
buffer_cache_hit_ratio = float(stats.get("Buffer cache hit ratio", 0) or 0)
return {
"batch_requests_per_sec": float(stats.get("Batch Requests/sec", 0) or 0),
"page_life_expectancy": int(stats.get("Page life expectancy", 0) or 0),
"buffer_cache_hit_ratio": round(buffer_cache_hit_ratio, 2),
"cache_hit_ratio": round(buffer_cache_hit_ratio, 2),
}
async def _get_oracle_performance(self) -> Dict[str, Any]:
"""获取 Oracle 性能统计"""
rows = await self._execute_oracle_query(
"""
SELECT name, value
FROM v$sysstat
WHERE name IN (
'user commits',
'user rollbacks',
'physical reads',
'db block gets',
'consistent gets'
)
"""
)
stats = {row["name"]: float(row["value"] or 0) for row in rows}
logical_reads = stats.get("db block gets", 0) + stats.get("consistent gets", 0)
physical_reads = stats.get("physical reads", 0)
cache_hit_ratio = (
(1 - physical_reads / logical_reads) * 100 if logical_reads > 0 else 0.0
)
return {
"transactions_commit": int(stats.get("user commits", 0)),
"transactions_rollback": int(stats.get("user rollbacks", 0)),
"cache_hit_ratio": round(cache_hit_ratio, 2),
}
async def _get_sqlserver_tables(self) -> List[Dict[str, Any]]:
"""获取 SQL Server 表统计"""
rows = await self._execute_odbc_query(
"""
SELECT TOP 20
s.name AS schemaname,
t.name AS tablename,
MAX(p.rows) AS table_rows,
SUM(a.total_pages) * 8 AS total_size_kb,
SUM(a.used_pages) * 8 AS used_size_kb,
SUM(
CASE WHEN i.index_id IN (0, 1) THEN a.used_pages ELSE 0 END
) * 8 AS data_size_kb
FROM sys.tables t
INNER JOIN sys.schemas s ON t.schema_id = s.schema_id
INNER JOIN sys.indexes i ON t.object_id = i.object_id
INNER JOIN sys.partitions p
ON i.object_id = p.object_id AND i.index_id = p.index_id
INNER JOIN sys.allocation_units a ON p.partition_id = a.container_id
WHERE t.is_ms_shipped = 0
GROUP BY s.name, t.name
ORDER BY total_size_kb DESC
"""
)
return rows
async def _get_oracle_tables(self) -> List[Dict[str, Any]]:
"""获取 Oracle 表统计"""
rows = await self._execute_oracle_query(
"""
SELECT *
FROM (
SELECT t.table_name,
NVL(t.num_rows, 0) AS table_rows,
NVL(s.size_bytes, 0) AS size_bytes
FROM user_tables t
LEFT JOIN (
SELECT segment_name, SUM(bytes) AS size_bytes
FROM user_segments
WHERE segment_type IN ('TABLE', 'TABLE PARTITION')
GROUP BY segment_name
) s ON s.segment_name = t.table_name
ORDER BY NVL(s.size_bytes, 0) DESC
)
WHERE ROWNUM <= 20
"""
)
return rows
async def get_all_info(self, connection_id: str, connection_name: str) -> Dict[str, Any]:
"""获取所有数据库监控信息"""
timestamp = datetime.now().isoformat()
try:
if not await self.connect():
return {
'connection_id': connection_id,
'connection_name': connection_name,
'status': 'disconnected',
'basic_info': {},
'connection_info': {},
'database_size': {},
'performance_stats': {'cache_hit_ratio': 0.0},
'table_stats': [],
'timestamp': timestamp
}
data = {
'connection_id': connection_id,
'connection_name': connection_name,
'status': 'connected',
'basic_info': await self.get_basic_info(),
'connection_info': await self.get_connection_info(),
'database_size': await self.get_database_size(),
'performance_stats': await self.get_performance_stats(),
'table_stats': await self.get_table_stats(),
'timestamp': timestamp
}
return serialize_data(data)
except Exception as e:
logger.error(f"Error getting database all info: {e}")
return {
'connection_id': connection_id,
'connection_name': connection_name,
'status': 'error',
'basic_info': {},
'connection_info': {},
'database_size': {},
'performance_stats': {'cache_hit_ratio': 0.0},
'table_stats': [],
'timestamp': timestamp
}
finally:
await self.disconnect()
async def get_realtime_stats(self, connection_id: str) -> Dict[str, Any]:
"""获取实时统计信息"""
timestamp = datetime.now().isoformat()
try:
if not await self.connect():
return {
'connection_id': connection_id,
'connections_used': 0,
'connection_usage_percent': 0.0,
'database_size_mb': 0.0,
'cache_hit_ratio': 0.0,
'active_connections': 0,
'timestamp': timestamp
}
connection_info = await self.get_connection_info()
database_size = await self.get_database_size()
performance_stats = await self.get_performance_stats()
data = {
'connection_id': connection_id,
'connections_used': connection_info.get('total_connections', 0),
'connection_usage_percent': connection_info.get('connection_usage_percent', 0.0),
'database_size_mb': database_size.get('database_size_mb', 0.0),
'cache_hit_ratio': performance_stats.get('cache_hit_ratio', 0.0),
'active_connections': connection_info.get('active_connections', 0),
'timestamp': timestamp
}
return serialize_data(data)
except Exception as e:
logger.error(f"Error getting database realtime stats: {e}")
return {
'connection_id': connection_id,
'connections_used': 0,
'connection_usage_percent': 0.0,
'database_size_mb': 0.0,
'cache_hit_ratio': 0.0,
'active_connections': 0,
'timestamp': timestamp
}
finally:
await self.disconnect()
@@ -0,0 +1,136 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
数据库监控Schema
"""
from typing import List, Optional, Dict, Any
from pydantic import BaseModel
class DatabaseBasicInfoSchema(BaseModel):
"""数据库基本信息Schema"""
db_type: str
host: str
port: int
database: str
version: str
uptime: str
timezone: str
charset: str
class DatabaseConnectionInfoSchema(BaseModel):
"""数据库连接信息Schema"""
total_connections: int
max_connections: int
active_connections: int
idle_connections: int
connection_usage_percent: float
class DatabaseSizeSchema(BaseModel):
"""数据库大小信息Schema"""
database_size_bytes: int
database_size_mb: float
database_size_gb: float
class DatabasePerformanceStatsSchema(BaseModel):
"""数据库性能统计Schema"""
# PostgreSQL
total_backends: Optional[int] = None
transactions_commit: Optional[int] = None
transactions_rollback: Optional[int] = None
tuples_returned: Optional[int] = None
tuples_fetched: Optional[int] = None
tuples_inserted: Optional[int] = None
tuples_updated: Optional[int] = None
tuples_deleted: Optional[int] = None
# MySQL
total_queries: Optional[int] = None
total_connections: Optional[int] = None
slow_queries: Optional[int] = None
bytes_received: Optional[int] = None
bytes_sent: Optional[int] = None
# SQL Server
batch_requests_per_sec: Optional[int] = None
page_life_expectancy: Optional[int] = None
buffer_cache_hit_ratio: Optional[float] = None
# 通用
cache_hit_ratio: float = 0.0
class DatabaseTableStatsSchema(BaseModel):
"""数据库表统计Schema"""
# PostgreSQL
schemaname: Optional[str] = None
tablename: Optional[str] = None
inserts: Optional[int] = None
updates: Optional[int] = None
deletes: Optional[int] = None
live_tuples: Optional[int] = None
dead_tuples: Optional[int] = None
size: Optional[str] = None
# MySQL
table_name: Optional[str] = None
table_rows: Optional[int] = None
data_length: Optional[int] = None
index_length: Optional[int] = None
total_size: Optional[int] = None
auto_increment: Optional[int] = None
# SQL Server
total_size_kb: Optional[int] = None
used_size_kb: Optional[int] = None
data_size_kb: Optional[int] = None
class DatabaseOverviewSchema(BaseModel):
"""数据库概览Schema"""
connection_id: str
connection_name: str
status: str
basic_info: Dict[str, Any]
connection_info: Dict[str, Any]
database_size: Dict[str, Any]
performance_stats: Dict[str, Any]
table_stats: List[Dict[str, Any]]
timestamp: str
class DatabaseRealtimeStatsSchema(BaseModel):
"""数据库实时统计Schema"""
connection_id: str
connections_used: int
connection_usage_percent: float
database_size_mb: float
cache_hit_ratio: float
active_connections: int
timestamp: str
class DatabaseConnectionTestSchema(BaseModel):
"""数据库连接测试Schema"""
success: bool
message: str
response_time: Optional[float] = None
version: Optional[str] = None
db_type: str
class DatabaseConfigSchema(BaseModel):
"""数据库监控目标(对应数据库连接 code)"""
name: str
db_name: str
db_type: str
host: str
port: int
database: str
user: str
has_password: bool
is_system: bool = False