08ea5fbe98
添加用户管理视图、API和状态管理文件
333 lines
10 KiB
TypeScript
333 lines
10 KiB
TypeScript
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<string, string>;
|
|
}
|
|
|
|
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<any> {
|
|
try {
|
|
const headers: Record<string, string> = {
|
|
'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<boolean> {
|
|
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<MetricsData> {
|
|
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<number> {
|
|
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<JvmInfo> {
|
|
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<EnvInfo> {
|
|
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<AppInfo> {
|
|
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<void> {
|
|
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<boolean> {
|
|
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<any> {
|
|
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;
|
|
}
|
|
}
|
|
}
|