Files
im/app/controller/MetricsController.php
T
2026-03-06 02:27:52 +08:00

673 lines
28 KiB
PHP

<?php
namespace app\controller;
use support\Request;
use support\Response;
/**
* 指标
* 提供Prometheus格式的监控指标
*/
class MetricsController extends Base
{
/**
* 无需登录及鉴权的方法
* @var array
*/
protected $noNeedLogin = ['index'];
/**
* 指标数据存储
*/
protected static $metrics = [];
/**
* 指标类型定义
*/
const TYPE_COUNTER = 'counter';
const TYPE_GAUGE = 'gauge';
const TYPE_HISTOGRAM = 'histogram';
const TYPE_SUMMARY = 'summary';
/**
* 获取指标数据
*
* @param Request $request
* @return Response
*/
public function index(Request $request): Response
{
$metrics = [];
// 系统基础指标
$metrics[] = $this->getSystemMetrics();
// PHP运行指标
$metrics[] = $this->getPhpMetrics();
// 应用业务指标
$metrics[] = $this->getAppMetrics();
// 数据库指标
$metrics[] = $this->getDatabaseMetrics();
// Redis指标
$metrics[] = $this->getRedisMetrics();
// MongoDB指标
$metrics[] = $this->getMongoDBMetrics();
// HTTP请求指标
$metrics[] = $this->getHttpMetrics();
$content = implode("\n", array_filter($metrics));
return response($content, 200, [
'Content-Type' => 'text/plain; charset=utf-8'
]);
}
/**
* 获取系统指标
*/
protected function getSystemMetrics(): string
{
$metrics = [];
// 内存使用
$memoryUsage = memory_get_usage(true);
$memoryPeak = memory_get_peak_usage(true);
$memoryLimit = $this->getBytes(ini_get('memory_limit'));
$metrics[] = $this->formatGauge('webman_memory_usage_bytes', 'Current memory usage in bytes', [], $memoryUsage);
$metrics[] = $this->formatGauge('webman_memory_peak_bytes', 'Peak memory usage in bytes', [], $memoryPeak);
$metrics[] = $this->formatGauge('webman_memory_limit_bytes', 'Memory limit in bytes', [], $memoryLimit);
// CPU负载
if (function_exists('sys_getloadavg')) {
$load = sys_getloadavg();
$metrics[] = $this->formatGauge('webman_system_load1', 'System load average over 1 minute', [], $load[0]);
$metrics[] = $this->formatGauge('webman_system_load5', 'System load average over 5 minutes', [], $load[1]);
$metrics[] = $this->formatGauge('webman_system_load15', 'System load average over 15 minutes', [], $load[2]);
}
// 磁盘使用
$diskTotal = disk_total_space('/');
$diskFree = disk_free_space('/');
$diskUsed = $diskTotal - $diskFree;
$metrics[] = $this->formatGauge('webman_disk_total_bytes', 'Total disk space in bytes', [], $diskTotal);
$metrics[] = $this->formatGauge('webman_disk_free_bytes', 'Free disk space in bytes', [], $diskFree);
$metrics[] = $this->formatGauge('webman_disk_used_bytes', 'Used disk space in bytes', [], $diskUsed);
return implode("\n", $metrics);
}
/**
* 获取PHP指标
*/
protected function getPhpMetrics(): string
{
$metrics = [];
// PHP版本信息
$metrics[] = $this->formatGauge('webman_php_version_info', 'PHP version information', [
'version' => PHP_VERSION,
'sapi' => PHP_SAPI
], 1);
// OPcache指标
if (function_exists('opcache_get_status')) {
$opcache = opcache_get_status(false);
if ($opcache) {
$metrics[] = $this->formatGauge('webman_opcache_enabled', 'OPcache enabled status', [], $opcache['opcache_enabled'] ? 1 : 0);
$metrics[] = $this->formatGauge('webman_opcache_hit_rate', 'OPcache hit rate', [], $opcache['opcache_statistics']['opcache_hit_rate'] ?? 0);
$metrics[] = $this->formatCounter('webman_opcache_hits_total', 'Total OPcache hits', [], $opcache['opcache_statistics']['hits'] ?? 0);
$metrics[] = $this->formatCounter('webman_opcache_misses_total', 'Total OPcache misses', [], $opcache['opcache_statistics']['misses'] ?? 0);
}
}
// 运行时间
if (function_exists('posix_times')) {
$times = posix_times();
$metrics[] = $this->formatCounter('webman_cpu_user_seconds_total', 'Total user CPU time', [], $times['utime'] ?? 0);
$metrics[] = $this->formatCounter('webman_cpu_system_seconds_total', 'Total system CPU time', [], $times['stime'] ?? 0);
}
return implode("\n", $metrics);
}
/**
* 获取应用业务指标
*/
protected function getAppMetrics(): string
{
$metrics = [];
// 应用信息
$metrics[] = $this->formatGauge('webman_app_info', 'Application information', [
'name' => config('app.name', 'webman'),
'version' => config('app.version', '1.0.0'),
'env' => config('app.debug') ? 'development' : 'production'
], 1);
// 启动时间
$metrics[] = $this->formatGauge('webman_start_time_seconds', 'Application start time', [], defined('WEBMAN_START_TIME') ? WEBMAN_START_TIME : time());
// 运行时长
$startTime = defined('WEBMAN_START_TIME') ? WEBMAN_START_TIME : time();
$uptime = time() - $startTime;
$metrics[] = $this->formatCounter('webman_uptime_seconds_total', 'Total uptime in seconds', [], $uptime);
return implode("\n", $metrics);
}
/**
* 获取数据库指标
*/
protected function getDatabaseMetrics(): string
{
$metrics = [];
try {
// 数据库连接信息
$dbConfig = config('database.connections.mysql');
if ($dbConfig) {
$metrics[] = $this->formatGauge('webman_db_config_info', 'Database configuration', [
'host' => $dbConfig['hostname'] ?? 'unknown',
'database' => $dbConfig['database'] ?? 'unknown',
'charset' => $dbConfig['charset'] ?? 'utf8'
], 1);
}
// 数据库连接状态 - 连接成功
$metrics[] = $this->formatGauge('webman_db_up', 'Database connection status', [], 1);
// 尝试获取数据库状态
$db = \think\facade\Db::connect();
// 1. 全局状态指标
$status = $db->query('SHOW GLOBAL STATUS');
$statusMap = [];
foreach ($status as $item) {
$statusMap[$item['Variable_name']] = $item['Value'];
}
// 关键指标
if (isset($statusMap['Threads_connected'])) {
$metrics[] = $this->formatGauge('webman_db_threads_connected', 'Number of currently connected threads', [], (int)$statusMap['Threads_connected']);
}
if (isset($statusMap['Threads_running'])) {
$metrics[] = $this->formatGauge('webman_db_threads_running', 'Number of threads running', [], (int)$statusMap['Threads_running']);
}
if (isset($statusMap['Queries'])) {
$metrics[] = $this->formatCounter('webman_db_queries_total', 'Total number of queries', [], (int)$statusMap['Queries']);
}
if (isset($statusMap['Slow_queries'])) {
$metrics[] = $this->formatCounter('webman_db_slow_queries_total', 'Total number of slow queries', [], (int)$statusMap['Slow_queries']);
}
if (isset($statusMap['Uptime'])) {
$metrics[] = $this->formatCounter('webman_db_uptime_seconds', 'Database uptime in seconds', [], (int)$statusMap['Uptime']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_data'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_data', 'Number of pages containing data', [], (int)$statusMap['Innodb_buffer_pool_pages_data']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_free'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_free', 'Number of free pages', [], (int)$statusMap['Innodb_buffer_pool_pages_free']);
}
if (isset($statusMap['Innodb_buffer_pool_pages_total'])) {
$metrics[] = $this->formatGauge('webman_db_innodb_buffer_pool_pages_total', 'Total number of pages', [], (int)$statusMap['Innodb_buffer_pool_pages_total']);
}
if (isset($statusMap['Innodb_row_reads'])) {
$metrics[] = $this->formatCounter('webman_db_innodb_row_reads_total', 'Number of rows read', [], (int)$statusMap['Innodb_row_reads']);
}
if (isset($statusMap['Innodb_row_writes'])) {
$metrics[] = $this->formatCounter('webman_db_innodb_row_writes_total', 'Number of rows written', [], (int)$statusMap['Innodb_row_writes']);
}
if (isset($statusMap['Com_select'])) {
$metrics[] = $this->formatCounter('webman_db_com_select_total', 'Number of SELECT statements', [], (int)$statusMap['Com_select']);
}
if (isset($statusMap['Com_insert'])) {
$metrics[] = $this->formatCounter('webman_db_com_insert_total', 'Number of INSERT statements', [], (int)$statusMap['Com_insert']);
}
if (isset($statusMap['Com_update'])) {
$metrics[] = $this->formatCounter('webman_db_com_update_total', 'Number of UPDATE statements', [], (int)$statusMap['Com_update']);
}
if (isset($statusMap['Com_delete'])) {
$metrics[] = $this->formatCounter('webman_db_com_delete_total', 'Number of DELETE statements', [], (int)$statusMap['Com_delete']);
}
// 2. 数据库大小
$databases = $db->query('SHOW DATABASES WHERE `Database` NOT IN ("information_schema", "mysql", "performance_schema", "sys")');
foreach ($databases as $dbInfo) {
$dbName = $dbInfo['Database'];
$sizeResult = $db->query("SELECT table_schema AS 'database', SUM(data_length + index_length) AS 'size_bytes' FROM information_schema.tables WHERE table_schema = '{$dbName}' GROUP BY table_schema");
if (isset($sizeResult[0]['size_bytes'])) {
$metrics[] = $this->formatGauge('webman_db_database_size_bytes', 'Database size in bytes', [
'database' => $dbName
], (int)$sizeResult[0]['size_bytes']);
}
}
// 3. 表状态
$tables = $db->query('SHOW TABLE STATUS FROM `' . ($dbConfig['database'] ?? 'test') . '`');
foreach ($tables as $table) {
$tableName = $table['Name'];
$metrics[] = $this->formatGauge('webman_db_table_rows', 'Number of rows in table', [
'table' => $tableName
], (int)$table['Rows']);
$metrics[] = $this->formatGauge('webman_db_table_data_length_bytes', 'Data length of table', [
'table' => $tableName
], (int)$table['Data_length']);
$metrics[] = $this->formatGauge('webman_db_table_index_length_bytes', 'Index length of table', [
'table' => $tableName
], (int)$table['Index_length']);
}
} catch (\Exception $e) {
// 数据库连接失败,记录错误
$metrics[] = $this->formatGauge('webman_db_up', 'Database connection status', [], 0);
}
return implode("\n", $metrics);
}
/**
* 获取Redis指标
*/
protected function getRedisMetrics(): string
{
$metrics = [];
try {
$redis = \support\think\Cache::handler();
$info = $redis->info();
// Redis连接状态
$metrics[] = $this->formatGauge('webman_redis_up', 'Redis connection status', [], 1);
// Redis版本
if (isset($info['redis_version'])) {
$metrics[] = $this->formatGauge('webman_redis_version_info', 'Redis version information', [
'version' => $info['redis_version']
], 1);
}
// 内存使用
if (isset($info['used_memory'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_used_bytes', 'Redis memory used in bytes', [], (int)$info['used_memory']);
}
if (isset($info['used_memory_peak'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_peak_bytes', 'Redis peak memory used in bytes', [], (int)$info['used_memory_peak']);
}
if (isset($info['used_memory_rss'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_rss_bytes', 'Redis RSS memory used in bytes', [], (int)$info['used_memory_rss']);
}
if (isset($info['mem_fragmentation_ratio'])) {
$metrics[] = $this->formatGauge('webman_redis_memory_fragmentation_ratio', 'Redis memory fragmentation ratio', [], (float)$info['mem_fragmentation_ratio']);
}
// 连接数
if (isset($info['connected_clients'])) {
$metrics[] = $this->formatGauge('webman_redis_connected_clients', 'Number of connected clients', [], (int)$info['connected_clients']);
}
if (isset($info['client_recent_max_input_buffer'])) {
$metrics[] = $this->formatGauge('webman_redis_client_max_input_buffer_bytes', 'Maximum input buffer size', [], (int)$info['client_recent_max_input_buffer']);
}
if (isset($info['client_recent_max_output_buffer'])) {
$metrics[] = $this->formatGauge('webman_redis_client_max_output_buffer_bytes', 'Maximum output buffer size', [], (int)$info['client_recent_max_output_buffer']);
}
// 命令统计
if (isset($info['total_commands_processed'])) {
$metrics[] = $this->formatCounter('webman_redis_commands_processed_total', 'Total commands processed', [], (int)$info['total_commands_processed']);
}
if (isset($info['instantaneous_ops_per_sec'])) {
$metrics[] = $this->formatGauge('webman_redis_instantaneous_ops_per_sec', 'Instantaneous operations per second', [], (int)$info['instantaneous_ops_per_sec']);
}
// 键数量
if (isset($info['db0'])) {
preg_match('/keys=(\d+)/', $info['db0'], $matches);
if (isset($matches[1])) {
$metrics[] = $this->formatGauge('webman_redis_keys_total', 'Total number of keys', [], (int)$matches[1]);
}
}
// 运行时间
if (isset($info['uptime_in_seconds'])) {
$metrics[] = $this->formatCounter('webman_redis_uptime_seconds', 'Redis uptime in seconds', [], (int)$info['uptime_in_seconds']);
}
// 过期键
if (isset($info['expired_keys'])) {
$metrics[] = $this->formatCounter('webman_redis_expired_keys_total', 'Total number of expired keys', [], (int)$info['expired_keys']);
}
if (isset($info['evicted_keys'])) {
$metrics[] = $this->formatCounter('webman_redis_evicted_keys_total', 'Total number of evicted keys', [], (int)$info['evicted_keys']);
}
// 命中和未命中
if (isset($info['keyspace_hits'])) {
$metrics[] = $this->formatCounter('webman_redis_keyspace_hits_total', 'Total number of keyspace hits', [], (int)$info['keyspace_hits']);
}
if (isset($info['keyspace_misses'])) {
$metrics[] = $this->formatCounter('webman_redis_keyspace_misses_total', 'Total number of keyspace misses', [], (int)$info['keyspace_misses']);
}
// 复制状态
if (isset($info['role'])) {
$metrics[] = $this->formatGauge('webman_redis_role', 'Redis role (master=1, slave=2)', [
'role' => $info['role']
], $info['role'] === 'master' ? 1 : 2);
}
} catch (\Exception $e) {
$metrics[] = $this->formatGauge('webman_redis_up', 'Redis connection status', [], 0);
}
return implode("\n", $metrics);
}
/**
* 获取MongoDB指标
*/
protected function getMongoDBMetrics(): string
{
$metrics = [];
try {
// 检查MongoDB扩展是否安装
if (!class_exists('MongoDB\\Client')) {
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 0);
$metrics[] = $this->formatGauge('webman_mongodb_error', 'MongoDB error', [
'error' => 'extension_not_installed'
], 1);
return implode("\n", $metrics);
}
// 尝试连接MongoDB
$mongoClient = new \MongoDB\Client('mongodb://localhost:27017');
// MongoDB连接状态
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 1);
// 获取服务器状态
$serverStatus = $mongoClient->selectDatabase('admin')->command(['serverStatus' => 1]);
$status = $serverStatus->toArray()[0];
// 版本信息
if (isset($status['version'])) {
$metrics[] = $this->formatGauge('webman_mongodb_version_info', 'MongoDB version information', [
'version' => $status['version']
], 1);
}
// 连接数
if (isset($status['connections'])) {
$metrics[] = $this->formatGauge('webman_mongodb_connections_current', 'Current number of connections', [], (int)$status['connections']['current']);
$metrics[] = $this->formatGauge('webman_mongodb_connections_available', 'Available number of connections', [], (int)$status['connections']['available']);
}
// 内存使用
if (isset($status['mem'])) {
if (isset($status['mem']['resident'])) {
$metrics[] = $this->formatGauge('webman_mongodb_memory_resident_bytes', 'Resident memory usage in bytes', [], (int)$status['mem']['resident'] * 1024 * 1024);
}
if (isset($status['mem']['virtual'])) {
$metrics[] = $this->formatGauge('webman_mongodb_memory_virtual_bytes', 'Virtual memory usage in bytes', [], (int)$status['mem']['virtual'] * 1024 * 1024);
}
}
// 操作统计
if (isset($status['opcounters'])) {
if (isset($status['opcounters']['insert'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_insert_total', 'Total insert operations', [], (int)$status['opcounters']['insert']);
}
if (isset($status['opcounters']['query'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_query_total', 'Total query operations', [], (int)$status['opcounters']['query']);
}
if (isset($status['opcounters']['update'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_update_total', 'Total update operations', [], (int)$status['opcounters']['update']);
}
if (isset($status['opcounters']['delete'])) {
$metrics[] = $this->formatCounter('webman_mongodb_ops_delete_total', 'Total delete operations', [], (int)$status['opcounters']['delete']);
}
}
// 集合和数据库统计
$databases = $mongoClient->listDatabases();
foreach ($databases as $dbInfo) {
$dbName = $dbInfo->getName();
if (!in_array($dbName, ['admin', 'local', 'config'])) {
$db = $mongoClient->selectDatabase($dbName);
$collections = $db->listCollections();
$collectionCount = 0;
foreach ($collections as $collection) {
$collectionCount++;
$collectionName = $collection->getName();
$stats = $db->command(['collStats' => $collectionName]);
$collStats = $stats->toArray()[0];
if (isset($collStats['count'])) {
$metrics[] = $this->formatGauge('webman_mongodb_collection_documents', 'Number of documents in collection', [
'database' => $dbName,
'collection' => $collectionName
], (int)$collStats['count']);
}
if (isset($collStats['size'])) {
$metrics[] = $this->formatGauge('webman_mongodb_collection_size_bytes', 'Size of collection in bytes', [
'database' => $dbName,
'collection' => $collectionName
], (int)$collStats['size']);
}
}
$metrics[] = $this->formatGauge('webman_mongodb_database_collections', 'Number of collections in database', [
'database' => $dbName
], $collectionCount);
}
}
} catch (\Exception $e) {
$metrics[] = $this->formatGauge('webman_mongodb_up', 'MongoDB connection status', [], 0);
$metrics[] = $this->formatGauge('webman_mongodb_error', 'MongoDB error', [
'error' => 'connection_failed'
], 1);
}
return implode("\n", $metrics);
}
/**
* 获取HTTP请求指标
*/
protected function getHttpMetrics(): string
{
$metrics = [];
// 请求计数器(这里需要从中间件或日志中统计,示例使用静态数据)
$metrics[] = $this->formatCounter('webman_http_requests_total', 'Total HTTP requests', [
'method' => 'GET',
'status' => '200'
], 0);
$metrics[] = $this->formatCounter('webman_http_requests_total', 'Total HTTP requests', [
'method' => 'POST',
'status' => '200'
], 0);
// 请求处理时间(使用Gauge类型)
$metrics[] = $this->formatGauge('webman_http_request_duration_seconds', 'HTTP request duration in seconds', [
'method' => 'GET',
'path' => '/metrics'
], 0);
// 响应大小
$metrics[] = $this->formatCounter('webman_http_response_size_bytes_total', 'Total HTTP response size in bytes', [], 0);
// 请求大小
$metrics[] = $this->formatCounter('webman_http_request_size_bytes_total', 'Total HTTP request size in bytes', [], 0);
return implode("\n", $metrics);
}
/**
* 格式化Counter类型指标
*/
protected function formatCounter(string $name, string $help, array $labels, float $value): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} counter";
$labelStr = $this->formatLabels($labels);
$output[] = $name . $labelStr . ' ' . $value;
return implode("\n", $output);
}
/**
* 格式化Gauge类型指标
*/
protected function formatGauge(string $name, string $help, array $labels, float $value): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} gauge";
$labelStr = $this->formatLabels($labels);
$output[] = $name . $labelStr . ' ' . $value;
return implode("\n", $output);
}
/**
* 格式化Histogram类型指标
*/
protected function formatHistogram(string $name, string $help, array $labels, array $buckets): string
{
$output = [];
$output[] = "# HELP {$name} {$help}";
$output[] = "# TYPE {$name} histogram";
// 如果没有提供桶数据,返回空直方图
if (empty($buckets)) {
$labelStr = $this->formatLabels($labels);
$output[] = $name . '_bucket' . $labelStr . ',le="+Inf" 0';
$output[] = $name . '_sum' . $labelStr . ' 0';
$output[] = $name . '_count' . $labelStr . ' 0';
}
return implode("\n", $output);
}
/**
* 格式化标签
*/
protected function formatLabels(array $labels): string
{
if (empty($labels)) {
return '';
}
$pairs = [];
foreach ($labels as $key => $value) {
$pairs[] = $key . '="' . $this->escapeLabel($value) . '"';
}
return '{' . implode(',', $pairs) . '}';
}
/**
* 转义标签值
*/
protected function escapeLabel(string $value): string
{
return str_replace(['\\', '"', "\n"], ['\\\\', '\\"', '\\n'], $value);
}
/**
* 将PHP内存限制字符串转换为字节数
*/
protected function getBytes(string $val): int
{
$val = trim($val);
$last = strtolower($val[strlen($val) - 1]);
$val = (int) $val;
switch ($last) {
case 'g':
$val *= 1024;
case 'm':
$val *= 1024;
case 'k':
$val *= 1024;
}
return $val;
}
/**
* 增加计数器值(供其他控制器调用)
*
* @param string $name
* @param array $labels
* @param float $value
*/
public static function incCounter(string $name, array $labels = [], float $value = 1): void
{
$key = $name . json_encode($labels);
if (!isset(self::$metrics[$key])) {
self::$metrics[$key] = [
'type' => 'counter',
'name' => $name,
'labels' => $labels,
'value' => 0
];
}
self::$metrics[$key]['value'] += $value;
}
/**
* 设置Gauge值(供其他控制器调用)
*
* @param string $name
* @param float $value
* @param array $labels
*/
public static function setGauge(string $name, float $value, array $labels = []): void
{
$key = $name . json_encode($labels);
self::$metrics[$key] = [
'type' => 'gauge',
'name' => $name,
'labels' => $labels,
'value' => $value
];
}
/**
* 记录HTTP请求指标(供中间件调用)
*
* @param string $method
* @param string $path
* @param int $status
* @param float $duration
* @param int $responseSize
*/
public static function recordHttpRequest(string $method, string $path, int $status, float $duration, int $responseSize = 0): void
{
$labels = [
'method' => $method,
'path' => $path,
'status' => (string)$status
];
// 请求总数
self::incCounter('webman_http_requests_total', $labels);
// 响应大小
self::incCounter('webman_http_response_size_bytes_total', $labels, $responseSize);
}
}