|
|
@@ -0,0 +1,426 @@
|
|
|
+<?php
|
|
|
+
|
|
|
+namespace App\Module\ThirdParty\Services;
|
|
|
+
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyService;
|
|
|
+use App\Module\ThirdParty\Models\ThirdPartyMonitor;
|
|
|
+use App\Module\ThirdParty\Logics\ServiceLogic;
|
|
|
+use Illuminate\Support\Collection;
|
|
|
+use Illuminate\Support\Facades\Http;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 第三方服务监控服务类
|
|
|
+ */
|
|
|
+class MonitorService
|
|
|
+{
|
|
|
+ /**
|
|
|
+ * 执行服务健康检查
|
|
|
+ *
|
|
|
+ * @param int|null $serviceId 服务ID,为null时检查所有服务
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function performHealthCheck(?int $serviceId = null): array
|
|
|
+ {
|
|
|
+ $results = [];
|
|
|
+
|
|
|
+ if ($serviceId) {
|
|
|
+ $services = [ThirdPartyService::findOrFail($serviceId)];
|
|
|
+ } else {
|
|
|
+ $services = ThirdPartyService::where('status', 'ACTIVE')
|
|
|
+ ->whereNotNull('health_check_url')
|
|
|
+ ->get();
|
|
|
+ }
|
|
|
+
|
|
|
+ foreach ($services as $service) {
|
|
|
+ $result = static::checkSingleService($service);
|
|
|
+ $results[] = $result;
|
|
|
+
|
|
|
+ // 记录监控结果
|
|
|
+ static::recordMonitorResult($service, 'health', $result);
|
|
|
+
|
|
|
+ // 更新服务健康状态
|
|
|
+ $service->updateHealthStatus($result['status']);
|
|
|
+ }
|
|
|
+
|
|
|
+ return $results;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 检查单个服务
|
|
|
+ *
|
|
|
+ * @param ThirdPartyService $service
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected static function checkSingleService(ThirdPartyService $service): array
|
|
|
+ {
|
|
|
+ $result = [
|
|
|
+ 'service_id' => $service->id,
|
|
|
+ 'service_code' => $service->code,
|
|
|
+ 'service_name' => $service->name,
|
|
|
+ 'status' => ThirdPartyMonitor::STATUS_UNKNOWN,
|
|
|
+ 'response_time' => null,
|
|
|
+ 'status_code' => null,
|
|
|
+ 'error_message' => null,
|
|
|
+ 'details' => [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 检查服务基本状态
|
|
|
+ $healthCheck = ServiceLogic::checkServiceHealth($service);
|
|
|
+ if ($healthCheck['status'] !== 'healthy') {
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_ERROR;
|
|
|
+ $result['error_message'] = $healthCheck['message'];
|
|
|
+ $result['details'] = $healthCheck['details'];
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果有健康检查URL,执行HTTP检查
|
|
|
+ if ($service->health_check_url) {
|
|
|
+ $httpResult = static::performHttpCheck($service);
|
|
|
+ $result = array_merge($result, $httpResult);
|
|
|
+ } else {
|
|
|
+ // 没有健康检查URL,基于服务状态判断
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_SUCCESS;
|
|
|
+ $result['details']['message'] = '服务状态正常,无HTTP健康检查';
|
|
|
+ }
|
|
|
+
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_ERROR;
|
|
|
+ $result['error_message'] = $e->getMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 执行HTTP健康检查
|
|
|
+ *
|
|
|
+ * @param ThirdPartyService $service
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ protected static function performHttpCheck(ThirdPartyService $service): array
|
|
|
+ {
|
|
|
+ $result = [
|
|
|
+ 'status' => ThirdPartyMonitor::STATUS_UNKNOWN,
|
|
|
+ 'response_time' => null,
|
|
|
+ 'status_code' => null,
|
|
|
+ 'error_message' => null,
|
|
|
+ 'details' => [],
|
|
|
+ ];
|
|
|
+
|
|
|
+ $startTime = microtime(true);
|
|
|
+
|
|
|
+ try {
|
|
|
+ $timeout = config('thirdparty.monitoring.health_check.timeout', 10);
|
|
|
+
|
|
|
+ $response = Http::timeout($timeout)
|
|
|
+ ->get($service->health_check_url);
|
|
|
+
|
|
|
+ $responseTime = (int)((microtime(true) - $startTime) * 1000);
|
|
|
+ $result['response_time'] = $responseTime;
|
|
|
+ $result['status_code'] = $response->status();
|
|
|
+
|
|
|
+ // 判断响应状态
|
|
|
+ if ($response->successful()) {
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_SUCCESS;
|
|
|
+
|
|
|
+ // 检查响应时间
|
|
|
+ $slowThreshold = config('thirdparty.monitoring.performance.slow_threshold', 2000);
|
|
|
+ if ($responseTime > $slowThreshold) {
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_WARNING;
|
|
|
+ $result['details']['warning'] = "响应时间 {$responseTime}ms 超过阈值 {$slowThreshold}ms";
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_ERROR;
|
|
|
+ $result['error_message'] = "HTTP状态码: {$response->status()}";
|
|
|
+ }
|
|
|
+
|
|
|
+ $result['details']['response_body'] = $response->body();
|
|
|
+
|
|
|
+ } catch (\Illuminate\Http\Client\ConnectionException $e) {
|
|
|
+ $responseTime = (int)((microtime(true) - $startTime) * 1000);
|
|
|
+ $result['response_time'] = $responseTime;
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_TIMEOUT;
|
|
|
+ $result['error_message'] = '连接超时: ' . $e->getMessage();
|
|
|
+ } catch (\Exception $e) {
|
|
|
+ $responseTime = (int)((microtime(true) - $startTime) * 1000);
|
|
|
+ $result['response_time'] = $responseTime;
|
|
|
+ $result['status'] = ThirdPartyMonitor::STATUS_ERROR;
|
|
|
+ $result['error_message'] = $e->getMessage();
|
|
|
+ }
|
|
|
+
|
|
|
+ return $result;
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 记录监控结果
|
|
|
+ *
|
|
|
+ * @param ThirdPartyService $service
|
|
|
+ * @param string $checkType
|
|
|
+ * @param array $result
|
|
|
+ * @return ThirdPartyMonitor
|
|
|
+ */
|
|
|
+ protected static function recordMonitorResult(ThirdPartyService $service, string $checkType, array $result): ThirdPartyMonitor
|
|
|
+ {
|
|
|
+ return ThirdPartyMonitor::createMonitor([
|
|
|
+ 'service_id' => $service->id,
|
|
|
+ 'check_type' => $checkType,
|
|
|
+ 'status' => $result['status'],
|
|
|
+ 'response_time' => $result['response_time'],
|
|
|
+ 'status_code' => $result['status_code'],
|
|
|
+ 'error_message' => $result['error_message'],
|
|
|
+ 'details' => $result['details'] ?? [],
|
|
|
+ ]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取服务监控历史
|
|
|
+ *
|
|
|
+ * @param int $serviceId
|
|
|
+ * @param array $options
|
|
|
+ * @return Collection
|
|
|
+ */
|
|
|
+ public static function getServiceMonitorHistory(int $serviceId, array $options = []): Collection
|
|
|
+ {
|
|
|
+ $query = ThirdPartyMonitor::where('service_id', $serviceId);
|
|
|
+
|
|
|
+ // 时间范围过滤
|
|
|
+ if (isset($options['start_date'])) {
|
|
|
+ $query->where('checked_at', '>=', $options['start_date']);
|
|
|
+ }
|
|
|
+
|
|
|
+ if (isset($options['end_date'])) {
|
|
|
+ $query->where('checked_at', '<=', $options['end_date']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查类型过滤
|
|
|
+ if (isset($options['check_type'])) {
|
|
|
+ $query->where('check_type', $options['check_type']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 状态过滤
|
|
|
+ if (isset($options['status'])) {
|
|
|
+ $query->where('status', $options['status']);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 排序和限制
|
|
|
+ $limit = $options['limit'] ?? 100;
|
|
|
+ $query->orderBy('checked_at', 'desc')->limit($limit);
|
|
|
+
|
|
|
+ return $query->get();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取监控统计信息
|
|
|
+ *
|
|
|
+ * @param int|null $serviceId
|
|
|
+ * @param array $options
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function getMonitorStats(?int $serviceId = null, array $options = []): array
|
|
|
+ {
|
|
|
+ $query = ThirdPartyMonitor::query();
|
|
|
+
|
|
|
+ if ($serviceId) {
|
|
|
+ $query->where('service_id', $serviceId);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 时间范围
|
|
|
+ $startDate = $options['start_date'] ?? now()->subDays(7);
|
|
|
+ $endDate = $options['end_date'] ?? now();
|
|
|
+ $query->whereBetween('checked_at', [$startDate, $endDate]);
|
|
|
+
|
|
|
+ $total = $query->count();
|
|
|
+ $successful = $query->where('status', ThirdPartyMonitor::STATUS_SUCCESS)->count();
|
|
|
+ $warnings = $query->where('status', ThirdPartyMonitor::STATUS_WARNING)->count();
|
|
|
+ $errors = $query->whereIn('status', [
|
|
|
+ ThirdPartyMonitor::STATUS_ERROR,
|
|
|
+ ThirdPartyMonitor::STATUS_TIMEOUT
|
|
|
+ ])->count();
|
|
|
+
|
|
|
+ // 平均响应时间
|
|
|
+ $avgResponseTime = $query->whereNotNull('response_time')
|
|
|
+ ->avg('response_time');
|
|
|
+
|
|
|
+ // 可用性计算
|
|
|
+ $availability = $total > 0 ? round(($successful / $total) * 100, 2) : 0;
|
|
|
+
|
|
|
+ // 按状态统计
|
|
|
+ $statusStats = $query->selectRaw('status, COUNT(*) as count')
|
|
|
+ ->groupBy('status')
|
|
|
+ ->pluck('count', 'status')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ // 按检查类型统计
|
|
|
+ $typeStats = $query->selectRaw('check_type, COUNT(*) as count')
|
|
|
+ ->groupBy('check_type')
|
|
|
+ ->pluck('count', 'check_type')
|
|
|
+ ->toArray();
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'total_checks' => $total,
|
|
|
+ 'successful' => $successful,
|
|
|
+ 'warnings' => $warnings,
|
|
|
+ 'errors' => $errors,
|
|
|
+ 'availability' => $availability,
|
|
|
+ 'avg_response_time' => $avgResponseTime ? round($avgResponseTime, 2) : null,
|
|
|
+ 'by_status' => $statusStats,
|
|
|
+ 'by_type' => $typeStats,
|
|
|
+ 'period' => [
|
|
|
+ 'start' => $startDate->toDateTimeString(),
|
|
|
+ 'end' => $endDate->toDateTimeString(),
|
|
|
+ ],
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取服务可用性报告
|
|
|
+ *
|
|
|
+ * @param int $serviceId
|
|
|
+ * @param int $days
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function getAvailabilityReport(int $serviceId, int $days = 30): array
|
|
|
+ {
|
|
|
+ $service = ThirdPartyService::findOrFail($serviceId);
|
|
|
+ $startDate = now()->subDays($days);
|
|
|
+
|
|
|
+ $monitors = ThirdPartyMonitor::where('service_id', $serviceId)
|
|
|
+ ->where('checked_at', '>=', $startDate)
|
|
|
+ ->orderBy('checked_at')
|
|
|
+ ->get();
|
|
|
+
|
|
|
+ $totalChecks = $monitors->count();
|
|
|
+ $successfulChecks = $monitors->where('status', ThirdPartyMonitor::STATUS_SUCCESS)->count();
|
|
|
+ $availability = $totalChecks > 0 ? round(($successfulChecks / $totalChecks) * 100, 2) : 0;
|
|
|
+
|
|
|
+ // 按天统计
|
|
|
+ $dailyStats = $monitors->groupBy(function ($monitor) {
|
|
|
+ return $monitor->checked_at->format('Y-m-d');
|
|
|
+ })->map(function ($dayMonitors) {
|
|
|
+ $total = $dayMonitors->count();
|
|
|
+ $successful = $dayMonitors->where('status', ThirdPartyMonitor::STATUS_SUCCESS)->count();
|
|
|
+ $avgResponseTime = $dayMonitors->whereNotNull('response_time')->avg('response_time');
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'total_checks' => $total,
|
|
|
+ 'successful_checks' => $successful,
|
|
|
+ 'availability' => $total > 0 ? round(($successful / $total) * 100, 2) : 0,
|
|
|
+ 'avg_response_time' => $avgResponseTime ? round($avgResponseTime, 2) : null,
|
|
|
+ ];
|
|
|
+ });
|
|
|
+
|
|
|
+ // 故障时间段
|
|
|
+ $downtimes = [];
|
|
|
+ $currentDowntime = null;
|
|
|
+
|
|
|
+ foreach ($monitors as $monitor) {
|
|
|
+ if ($monitor->status !== ThirdPartyMonitor::STATUS_SUCCESS) {
|
|
|
+ if (!$currentDowntime) {
|
|
|
+ $currentDowntime = [
|
|
|
+ 'start' => $monitor->checked_at,
|
|
|
+ 'end' => $monitor->checked_at,
|
|
|
+ 'duration' => 0,
|
|
|
+ 'reason' => $monitor->error_message,
|
|
|
+ ];
|
|
|
+ } else {
|
|
|
+ $currentDowntime['end'] = $monitor->checked_at;
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ if ($currentDowntime) {
|
|
|
+ $currentDowntime['duration'] = $currentDowntime['end']->diffInMinutes($currentDowntime['start']);
|
|
|
+ $downtimes[] = $currentDowntime;
|
|
|
+ $currentDowntime = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果最后还有未结束的故障
|
|
|
+ if ($currentDowntime) {
|
|
|
+ $currentDowntime['end'] = now();
|
|
|
+ $currentDowntime['duration'] = $currentDowntime['end']->diffInMinutes($currentDowntime['start']);
|
|
|
+ $downtimes[] = $currentDowntime;
|
|
|
+ }
|
|
|
+
|
|
|
+ return [
|
|
|
+ 'service' => [
|
|
|
+ 'id' => $service->id,
|
|
|
+ 'name' => $service->name,
|
|
|
+ 'code' => $service->code,
|
|
|
+ ],
|
|
|
+ 'period' => [
|
|
|
+ 'days' => $days,
|
|
|
+ 'start' => $startDate->toDateTimeString(),
|
|
|
+ 'end' => now()->toDateTimeString(),
|
|
|
+ ],
|
|
|
+ 'summary' => [
|
|
|
+ 'total_checks' => $totalChecks,
|
|
|
+ 'successful_checks' => $successfulChecks,
|
|
|
+ 'availability' => $availability,
|
|
|
+ 'downtime_count' => count($downtimes),
|
|
|
+ 'total_downtime_minutes' => array_sum(array_column($downtimes, 'duration')),
|
|
|
+ ],
|
|
|
+ 'daily_stats' => $dailyStats,
|
|
|
+ 'downtimes' => $downtimes,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 清理旧的监控记录
|
|
|
+ *
|
|
|
+ * @param int $days
|
|
|
+ * @return int
|
|
|
+ */
|
|
|
+ public static function cleanupOldMonitorRecords(int $days = 90): int
|
|
|
+ {
|
|
|
+ return ThirdPartyMonitor::where('checked_at', '<', now()->subDays($days))->delete();
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 获取需要关注的服务
|
|
|
+ *
|
|
|
+ * @return array
|
|
|
+ */
|
|
|
+ public static function getServicesNeedingAttention(): array
|
|
|
+ {
|
|
|
+ $services = ThirdPartyService::where('status', 'ACTIVE')->get();
|
|
|
+ $needingAttention = [];
|
|
|
+
|
|
|
+ foreach ($services as $service) {
|
|
|
+ $issues = [];
|
|
|
+
|
|
|
+ // 检查最近的监控记录
|
|
|
+ $recentMonitor = ThirdPartyMonitor::where('service_id', $service->id)
|
|
|
+ ->orderBy('checked_at', 'desc')
|
|
|
+ ->first();
|
|
|
+
|
|
|
+ if (!$recentMonitor) {
|
|
|
+ $issues[] = '缺少监控数据';
|
|
|
+ } elseif ($recentMonitor->status !== ThirdPartyMonitor::STATUS_SUCCESS) {
|
|
|
+ $issues[] = '最近检查失败: ' . $recentMonitor->error_message;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查健康检查间隔
|
|
|
+ if ($service->needsHealthCheck()) {
|
|
|
+ $issues[] = '需要健康检查';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 检查凭证状态
|
|
|
+ $credential = $service->getActiveCredential();
|
|
|
+ if (!$credential) {
|
|
|
+ $issues[] = '缺少活跃凭证';
|
|
|
+ } elseif ($credential->isExpiringSoon()) {
|
|
|
+ $issues[] = '凭证即将过期';
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!empty($issues)) {
|
|
|
+ $needingAttention[] = [
|
|
|
+ 'service' => $service,
|
|
|
+ 'issues' => $issues,
|
|
|
+ ];
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return $needingAttention;
|
|
|
+ }
|
|
|
+}
|