import { APIRequestContext } from '@playwright/test'; import { testLogger } from './test-logger'; export interface MetricsData { jvmMemoryUsed: number; jvmMemoryMax: number; jvmGcPause: number; responseTime: number; requestCount: number; errorCount: number; } export interface JvmInfo { memory: { heap: { used: number; max: number; committed: number; }; nonHeap: { used: number; max: number; committed: number; }; }; gc: { pauseCount: number; pauseTime: number; }; threads: { live: number; peak: number; daemon: number; }; } export interface EnvInfo { activeProfiles: string[]; properties: Record; } export interface AppInfo { name: string; version: string; description: string; } export interface TestMetrics { testName: string; duration: number; status: 'passed' | 'failed'; memoryUsage: number; responseTime: number; timestamp: string; } export class ActuatorMonitor { private request: APIRequestContext; private baseUrl: string; private authToken?: string; constructor(request: APIRequestContext, baseUrl: string, authToken?: string) { this.request = request; this.baseUrl = baseUrl.replace(/\/$/, ''); this.authToken = authToken; } private async getEndpoint(endpoint: string): Promise { try { const headers: Record = { 'Accept': 'application/json', }; if (this.authToken) { headers['Authorization'] = `Bearer ${this.authToken}`; } const response = await this.request.get(`${this.baseUrl}${endpoint}`, { headers, timeout: 5000, }); if (!response.ok()) { throw new Error(`Actuator endpoint failed: ${response.status()} ${response.statusText()}`); } return await response.json(); } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error(`获取Actuator端点失败: ${endpoint}`, errorObj); throw errorObj; } } async checkHealth(): Promise { try { testLogger.debug('检查应用健康状态'); const healthData = await this.getEndpoint('/actuator/health'); const status = healthData.status; // 服务可访问即可,不严格要求 UP 状态 // 因为某些组件(如 CPU 负载)可能导致整体状态为 DOWN const isAccessible = status === 'UP' || status === 'DOWN' || status === 'WARNING'; if (status !== 'UP') { testLogger.warn(`应用健康状态非UP: ${status}`); if (healthData.components) { for (const [name, component] of Object.entries(healthData.components)) { if ((component as { status: string }).status !== 'UP') { testLogger.warn(`组件 ${name} 状态: ${(component as { status: string }).status}`); } } } } testLogger.info(`应用健康状态: ${status}, 可访问: ${isAccessible}`); return isAccessible; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('健康检查失败', errorObj); return false; } } async getMetrics(): Promise { try { testLogger.debug('获取性能指标'); const jvmMemoryUsed = await this.getMetricValue('jvm.memory.used', 'area=heap'); const jvmMemoryMax = await this.getMetricValue('jvm.memory.max', 'area=heap'); const jvmGcPause = await this.getMetricValue('jvm.gc.pause', 'count'); const metrics: MetricsData = { jvmMemoryUsed: Math.round(jvmMemoryUsed / 1024 / 1024), jvmMemoryMax: Math.round(jvmMemoryMax / 1024 / 1024), jvmGcPause: Math.round(jvmGcPause), responseTime: 0, requestCount: 0, errorCount: 0, }; testLogger.debug(`性能指标: ${JSON.stringify(metrics)}`); return metrics; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('获取性能指标失败', errorObj); throw errorObj; } } private async getMetricValue(metricName: string, tags: string = ''): Promise { try { const endpoint = tags ? `/actuator/metrics/${metricName}?tag=${tags}` : `/actuator/metrics/${metricName}`; const data = await this.getEndpoint(endpoint); return data.measurements?.[0]?.value || 0; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.warn(`获取指标值失败: ${metricName}`, errorObj); return 0; } } async getJvmInfo(): Promise { try { testLogger.debug('获取JVM信息'); const heapUsed = await this.getMetricValue('jvm.memory.used', 'area=heap'); const heapMax = await this.getMetricValue('jvm.memory.max', 'area=heap'); const heapCommitted = await this.getMetricValue('jvm.memory.committed', 'area=heap'); const nonHeapUsed = await this.getMetricValue('jvm.memory.used', 'area=nonheap'); const nonHeapMax = await this.getMetricValue('jvm.memory.max', 'area=nonheap'); const nonHeapCommitted = await this.getMetricValue('jvm.memory.committed', 'area=nonheap'); const gcPauseCount = await this.getMetricValue('jvm.gc.pause.count'); const gcPauseTime = await this.getMetricValue('jvm.gc.pause.total'); const threadsLive = await this.getMetricValue('jvm.threads.live'); const threadsPeak = await this.getMetricValue('jvm.threads.peak'); const threadsDaemon = await this.getMetricValue('jvm.threads.daemon'); const jvmInfo: JvmInfo = { memory: { heap: { used: Math.round(heapUsed / 1024 / 1024), max: Math.round(heapMax / 1024 / 1024), committed: Math.round(heapCommitted / 1024 / 1024), }, nonHeap: { used: Math.round(nonHeapUsed / 1024 / 1024), max: Math.round(nonHeapMax / 1024 / 1024), committed: Math.round(nonHeapCommitted / 1024 / 1024), }, }, gc: { pauseCount: Math.round(gcPauseCount), pauseTime: Math.round(gcPauseTime / 1000), }, threads: { live: Math.round(threadsLive), peak: Math.round(threadsPeak), daemon: Math.round(threadsDaemon), }, }; testLogger.debug(`JVM信息: ${JSON.stringify(jvmInfo)}`); return jvmInfo; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('获取JVM信息失败', errorObj); throw errorObj; } } async getEnvInfo(): Promise { try { testLogger.debug('获取环境信息'); const envData = await this.getEndpoint('/actuator/env'); const envInfo: EnvInfo = { activeProfiles: envData.profiles?.active || [], properties: {}, }; if (envData.propertySources) { for (const source of envData.propertySources) { if (source.properties) { for (const [key, value] of Object.entries(source.properties)) { if (value && typeof value === 'object' && 'value' in value) { envInfo.properties[key] = (value as any).value; } } } } } testLogger.debug(`环境信息: 激活的配置文件 [${envInfo.activeProfiles.join(', ')}]`); return envInfo; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('获取环境信息失败', errorObj); throw errorObj; } } async getAppInfo(): Promise { try { testLogger.debug('获取应用信息'); const appData = await this.getEndpoint('/actuator/info'); const appInfo: AppInfo = { name: appData.app?.name || 'Unknown', version: appData.app?.version || 'Unknown', description: appData.app?.description || 'Unknown', }; testLogger.debug(`应用信息: ${appInfo.name} v${appInfo.version}`); return appInfo; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('获取应用信息失败', errorObj); throw errorObj; } } async pushTestMetrics(metrics: TestMetrics): Promise { try { testLogger.debug(`推送测试指标: ${metrics.testName}`); const response = await this.request.post(`${this.baseUrl}/actuator/metrics/test`, { headers: { 'Content-Type': 'application/json', 'Authorization': this.authToken ? `Bearer ${this.authToken}` : '', }, data: metrics, timeout: 5000, }); if (!response.ok()) { throw new Error(`推送测试指标失败: ${response.status()} ${response.statusText()}`); } testLogger.debug(`测试指标推送成功: ${metrics.testName}`); } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.warn('推送测试指标失败(可能不支持自定义指标)', errorObj); } } async waitForHealth(maxRetries: number = 30, retryInterval: number = 2000): Promise { testLogger.info(`等待应用健康状态,最大重试次数: ${maxRetries}`); for (let i = 0; i < maxRetries; i++) { const isHealthy = await this.checkHealth(); if (isHealthy) { testLogger.info('应用健康状态检查通过'); return true; } testLogger.debug(`应用未就绪,等待 ${retryInterval}ms 后重试 (${i + 1}/${maxRetries})`); await new Promise(resolve => setTimeout(resolve, retryInterval)); } testLogger.error('应用健康状态检查超时'); return false; } async getFullHealthInfo(): Promise { try { testLogger.debug('获取完整健康信息'); const healthData = await this.getEndpoint('/actuator/health'); testLogger.debug(`完整健康信息: ${JSON.stringify(healthData)}`); return healthData; } catch (error) { const errorObj = error instanceof Error ? error : new Error(String(error)); testLogger.error('获取完整健康信息失败', errorObj); throw errorObj; } } }