468 lines
16 KiB
PHP
468 lines
16 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();
|
|
|
|
// 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();
|
|
$status = $db->query('SHOW 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']);
|
|
}
|
|
|
|
} 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 = \Bilulanlv\ThinkCache\facade\ThinkCache::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['connected_clients'])) {
|
|
$metrics[] = $this->formatGauge('webman_redis_connected_clients', 'Number of connected clients', [], (int)$info['connected_clients']);
|
|
}
|
|
|
|
// 命令统计
|
|
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['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']);
|
|
}
|
|
|
|
} catch (\Exception $e) {
|
|
$metrics[] = $this->formatGauge('webman_redis_up', 'Redis connection status', [], 0);
|
|
}
|
|
|
|
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);
|
|
}
|
|
}
|