关于 observability-testing-patterns
面向测试与运维团队的可观测性测试模式指南,涵盖仪表板、告警、日志、追踪和指标管道验证,并提供浏览器断言示例。
主要功能
- 01仪表板数据准确性验证
- 02告警触发与恢复测试
- 03日志聚合完整性检查
- 04分布式追踪与 SLO 验证
- 05指标管道及性能测试
适用场景
- 01核对仪表板数据与来源数据
- 02验证告警阈值和通知送达
- 03排查跨服务日志缺失
- 04评估仪表板延迟和 SLO 合规性
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ proffesor-for-testing/ agentic-qe --skill observability-testing-patterns安装后在 Claude Code 中输入/observability-testing-patterns确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
evals/observability-testing-patterns.yamlschemas/output.jsonscripts/validate-config.json详细介绍
可观测性测试模式
浏览器引擎
仪表板截图验证和告警 UI 验证通过 qe-browser 设备组技能(.claude/skills/qe-browser/)进行。Vibium 由 aqe init 安装。典型的仪表板回归工作流:
vibium go "$GRAFANA_URL/d/api-latency"
vibium wait load
node .claude/skills/qe-browser/scripts/assert.js --checks '[
{"kind": "selector_visible", "selector": ".panel-title"},
{"kind": "no_console_errors"},
{"kind": "no_failed_requests"},
{"kind": "element_count", "selector": ".panel", "op": ">=", "count": 4}
]'
node .claude/skills/qe-browser/scripts/visual-diff.js --name "grafana-api-latency"
<default_to_action> 测试可观测性基础设施、仪表板或监控时:
- 验证数据准确性(源数据与仪表板显示的内容相符)
- 测试告警规则是否在定义的阈值下正确触发
- 验证日志聚合完整性(各服务均无日志缺失)
- 通过 APM 端到端追踪分布式请求
- 测量仪表板性能(渲染时间、查询延迟)
- 通过合成监控确认 SLA/SLO 合规性
- 测试从采集到显示的指标管道完整性
快速模式选择:
- 仪表板显示的数字错误 -> 数据准确性验证
- 告警未触发 -> 告警规则阈值测试
- Kibana 中缺少日志 -> 日志聚合完整性
- 仪表板缓慢 -> 仪表板性能测试
- 追踪损坏 -> APM 追踪验证
- SLA 争议 -> SLO 合规性验证
关键成功因素:
- 可观测性的质量取决于其显示的数据
- 会误导人的仪表板比没有仪表板更糟
- 告警疲劳会拖慢响应速度;请谨慎测试阈值 </default_to_action>
快速参考卡
适用场景
- 验证仪表板数据准确性(Kibana、Grafana、Datadog)
- 测试告警规则阈值和通知送达情况
- 验证微服务间的日志聚合完整性
- 验证分布式追踪(APM)的正确性
- 测量 SLA/SLO 合规性
- 测试指标管道完整性(采集 -> 聚合 -> 显示)
测试级别
| 级别 | 目的 | 依赖项 | 速度 |
|---|---|---|---|
| 查询验证 | Elasticsearch/PromQL 查询准确性 | 数据源 | 快 |
| 仪表板准确性 | 视觉呈现与源数据相符 | 完整技术栈 | 中 |
| 告警阈值 | 触发和通知测试 | 告警技术栈 | 中 |
| 管道完整性 | 端到端指标流 | 完整管道 | 较慢 |
| 性能 | 仪表板渲染时间、查询延迟 | 完整技术栈 | 较慢 |
关键测试场景
| 场景 | 必须测试 | 示例 |
|---|---|---|
| 数据准确性 | 仪表板 = 源数据真值 | 仪表板中的订单数 = 数据库中的订单数 |
| 告警触发 | 阈值触发告警 | 错误率 > 5% 时触发 PagerDuty |
| 告警恢复 | 恢复后自动解决 | 错误率降至 5% 以下时清除告警 |
| 日志完整性 | 所有服务均产生日志 | 10 个微服务的所有日志均出现在 Kibana 中 |
| 追踪完整性 | 可查看完整请求路径 | Auth -> API -> DB -> Cache spans |
| SLO 合规性 | 错误预算跟踪 | 30 天内可用性为 99.9% |
| 时间准确性 | 时间戳一致 | 日志时间戳与事件时间相符 |
工具
- 仪表板:Kibana、Grafana、Datadog、New Relic
- 搜索:Elasticsearch、OpenSearch、Loki
- 指标:Prometheus、InfluxDB、CloudWatch
- 追踪:Jaeger、Zipkin、Datadog APM、OpenTelemetry
- 告警:PagerDuty、OpsGenie、Alertmanager
- 合成监控:Datadog Synthetics、Checkly、Playwright
Agent 协作
qe-integration-tester:验证数据管道、查询准确性和日志完整性qe-performance-tester:仪表板渲染性能、查询延迟qe-visual-tester:仪表板视觉回归、布局准确性
仪表板数据准确性验证
对比源数据与仪表板
describe('Dashboard Data Accuracy', () => {
it('order count on dashboard matches database', async () => {
// Step 1: Get ground truth from source database
const dbResult = await db.query(
"SELECT COUNT(*) as count FROM orders WHERE created_at >= NOW() - INTERVAL '24 HOURS'"
);
const dbCount = parseInt(dbResult.rows[0].count);
// Step 2: Query Elasticsearch (same data source as dashboard)
const esResult = await esClient.search({
index: 'orders-*',
body: {
query: {
range: { created_at: { gte: 'now-24h' } }
},
size: 0,
track_total_hits: true
}
});
const esCount = esResult.hits.total.value;
// Step 3: Compare
expect(esCount).toBe(dbCount);
});
it('revenue metric on dashboard matches transaction totals', async () => {
const dbRevenue = await db.query(
"SELECT SUM(total) as revenue FROM orders WHERE status = 'COMPLETED' AND created_at >= NOW() - INTERVAL '24 HOURS'"
);
const expectedRevenue = parseFloat(dbRevenue.rows[0].revenue);
const esResult = await esClient.search({
index: 'orders-*',
body: {
query: {
bool: {
must: [
{ term: { status: 'COMPLETED' } },
{ range: { created_at: { gte: 'now-24h' } } }
]
}
},
aggs: {
total_revenue: { sum: { field: 'total' } }
},
size: 0
}
});
const dashboardRevenue = esResult.aggregations.total_revenue.value;
// Allow small floating point tolerance
expect(Math.abs(dashboardRevenue - expectedRevenue)).toBeLessThan(0.01);
});
it('error rate percentage is calculated correctly', async () => {
const esResult = await esClient.search({
index: 'logs-*',
body: {
query: { range: { '@timestamp': { gte: 'now-1h' } } },
aggs: {
total: { value_count: { field: 'status_code' } },
errors: {
filter: { range: { status_code: { gte: 500 } } },
aggs: { count: { value_count: { field: 'status_code' } } }
}
},
size: 0
}
});
const total = esResult.aggregations.total.value;
const errors = esResult.aggregations.errors.count.value;
const expectedErrorRate = (errors / total) * 100;
// Fetch what the dashboard shows via Kibana API
const dashboardPanel = await kibanaApi.get('/api/saved_objects/visualization/error-rate-gauge');
const displayedErrorRate = await evaluateKibanaVisualization(dashboardPanel);
expect(Math.abs(displayedErrorRate - expectedErrorRate)).toBeLessThan(0.1);
});
});
Elasticsearch 查询结果验证
describe('Elasticsearch Query Validation', () => {
it('validates date histogram aggregation returns correct buckets', async () => {
// Insert known test data
const testDocs = [];
for (let hour = 0; hour < 24; hour++) {
const timestamp = new Date();
timestamp.setHours(hour, 0, 0, 0);
testDocs.push({
'@timestamp': timestamp.toISOString(),
service: 'order-api',
status_code: hour % 5 === 0 ? 500 : 200,
response_time: 100 + (hour * 10)
});
}
await esClient.bulk({
index: 'test-logs',
body: testDocs.flatMap(doc => [{ index: {} }, doc])
});
await esClient.indices.refresh({ index: 'test-logs' });
// Run the same query the dashboard uses
const result = await esClient.search({
index: 'test-logs',
body: {
query: { match_all: {} },
aggs: {
requests_over_time: {
date_histogram: { field: '@timestamp', fixed_interval: '1h' },
aggs: {
avg_response: { avg: { field: 'response_time' } },
error_count: {
filter: { range: { status_code: { gte: 500 } } }
}
}
}
},
size: 0
}
});
const buckets = result.aggregations.requests_over_time.buckets;
expect(buckets.length).toBe(24);
// Verify specific bucket values
const errorBuckets = buckets.filter(b => b.error_count.doc_count > 0);
expect(errorBuckets.length).toBe(5); // Hours 0, 5, 10, 15, 20
});
it('validates term aggregation for top services', async () => {
const result = await esClient.search({
index: 'logs-*',
body: {
query: { range: { '@timestamp': { gte: 'now-1h' } } },
aggs: {
top_services: {
terms: { field: 'service.keyword', size: 10 }
}
},
size: 0
}
});
const services = result.aggregations.top_services.buckets;
expect(services.length).toBeGreaterThan(0);
// Each bucket should have reasonable doc counts
for (const bucket of services) {
expect(bucket.key).toBeDefined();
expect(bucket.doc_count).toBeGreaterThan(0);
}
});
});
Kibana 仪表板元素断言
describe('Kibana Dashboard Visual Validation', () => {
it('validates dashboard panels render without errors', async () => {
await page.goto(`${kibanaUrl}/app/dashboards#/view/operations-overview`);
// Wait for all panels to finish loading
await page.waitForSelector('.embPanel__content', { state: 'visible' });
await page.waitForFunction(() => {
const loaders = document.querySelectorAll('.euiLoadingSpinner');
return loaders.length === 0;
}, { timeout: 30000 });
// Check no error icons on any panel
const errorPanels = await page.locator('.embPanel--error').count();
expect(errorPanels).toBe(0);
// Check no "No results found" where data is expected
const noResultPanels = await page.locator('text="No results found"').count();
expect(noResultPanels).toBe(0);
});
it('validates metric visualization shows correct value', async () => {
await page.goto(`${kibanaUrl}/app/dashboards#/view/operations-overview`);
await page.waitForLoadState('networkidle');
// Get the displayed metric value
const metricValue = await page.locator('[data-test-subj="metricVis-total-orders"] .mtrVis__value').textContent();
const displayedCount = parseInt(metricValue.replace(/,/g, ''));
// Compare with direct ES query
const esResult = await esClient.count({ index: 'orders-*' });
expect(displayedCount).toBe(esResult.count);
});
it('validates table visualization columns and sorting', async () => {
await page.goto(`${kibanaUrl}/app/dashboards#/view/operations-overview`);
await page.waitForLoadState('networkidle');
// Verify expected columns exist
const headers = await page.locator('.euiTable th').allTextContents();
expect(headers).toContain('Service');
expect(headers).toContain('Error Rate');
expect(headers).toContain('P95 Latency');
// Verify sorting works
await page.click('th:has-text("Error Rate")');
const firstRow = await page.locator('.euiTable tbody tr:first-child td').allTextContents();
const secondRow = await page.locator('.euiTable tbody tr:nth-child(2) td').allTextContents();
const firstErrorRate = parseFloat(firstRow[1]);
const secondErrorRate = parseFloat(secondRow[1]);
expect(firstErrorRate).toBeGreaterThanOrEqual(secondErrorRate);
});
});
告警规则测试
describe('Alert Rule Validation', () => {
it('fires alert when error rate exceeds threshold', async () => {
// Generate errors to exceed the 5% threshold
const requests = [];
for (let i = 0; i < 100; i++) {
requests.push({
'@timestamp': new Date().toISOString(),
service: 'payment-api',
status_code: i < 10 ? 500 : 200, // 10% error rate > 5% threshold
response_time: 200
});
}
await esClient.bulk({
index: 'logs-payment',
body: requests.flatMap(doc => [{ index: {} }, doc])
});
await esClient.indices.refresh({ index: 'logs-payment' });
// Wait for alert evaluation cycle (typically 1 minute)
await sleep(90000);
// Check alert was fired
const alerts = await alertManager.getActiveAlerts({
filter: 'alertname="HighErrorRate" AND service="payment-api"'
});
expect(alerts.length).toBeGreaterThan(0);
expect(alerts[0].labels.severity).toBe('critical');
});
it('alert auto-resolves when condition clears', async () => {
// First trigger the alert
await injectErrors('payment-api', { count: 50, total: 100 });
await sleep(90000);
let alerts = await alertManager.getActiveAlerts({ filter: 'alertname="HighErrorRate"' });
expect(alerts.length).toBeGreaterThan(0);
// Now inject healthy traffic to bring error rate below threshold
await injectSuccessRequests('payment-api', { count: 1000 });
await sleep(90000);
// Alert should auto-resolve
alerts = await alertManager.getActiveAlerts({ filter: 'alertname="HighErrorRate"' });
expect(alerts.length).toBe(0);
});
it('alert notification reaches correct channel', async () => {
// Subscribe to notification channel
const notifications = [];
const subscription = pagerDutyMock.onIncident((incident) => {
notifications.push(incident);
});
// Trigger alert condition
await injectErrors('critical-service', { count: 50, total: 100 });
await sleep(120000);
expect(notifications.length).toBeGreaterThan(0);
expect(notifications[0].service.name).toBe('critical-service');
expect(notifications[0].urgency).toBe('high');
subscription.unsubscribe();
});
it('alert does not fire for brief transient spikes', async () => {
// Inject a brief 30-second spike (alert requires 5 minutes sustained)
await injectErrors('api-service', { count: 20, total: 50, duration: 30000 });
await sleep(120000);
const alerts = await alertManager.getActiveAlerts({ filter: 'alertname="HighErrorRate"' });
expect(alerts.length).toBe(0); // Should NOT fire for transient spike
});
});
日志聚合完整性
describe('Log Aggregation Completeness', () => {
it('all microservice logs appear in centralized index', async () => {
const traceId = uuid();
const services = ['api-gateway', 'auth-service', 'order-service', 'payment-service', 'notification-service'];
// Generate a log entry with known traceId in each service
for (const service of services) {
await serviceLogEmitter.emit(service, {
level: 'INFO',
message: `Completeness test - ${traceId}`,
traceId,
timestamp: new Date().toISOString()
});
}
// Wait for log pipeline to process (Filebeat -> Logstash -> Elasticsearch)
await sleep(15000);
// Query Elasticsearch for the trace ID
const result = await esClient.search({
index: 'logs-*',
body: {
query: { term: { 'traceId.keyword': traceId } },
size: 100
}
});
const foundServices = result.hits.hits.map(h => h._source.service);
// All services should have their log entry in Elasticsearch
for (const service of services) {
expect(foundServices).toContain(service);
}
expect(foundServices.length).toBe(services.length);
});
it('logs retain correct structure after pipeline processing', async () => {
const testLog = {
level: 'ERROR',
message: 'Payment declined',
traceId: uuid(),
userId: 'user-123',
orderId: 'order-456',
errorCode: 'INSUFFICIENT_FUNDS',
timestamp: new Date().toISOString()
};
await serviceLogEmitter.emit('payment-service', testLog);
await sleep(10000);
const result = await esClient.search({
index: 'logs-*',
body: { query: { term: { 'traceId.keyword': testLog.traceId } } }
});
expect(result.hits.hits.length).toBe(1);
const indexed = result.hits.hits[0]._source;
// Verify all fields survived the pipeline
expect(indexed.level).toBe('ERROR');
expect(indexed.message).toBe('Payment declined');
expect(indexed.userId).toBe('user-123');
expect(indexed.orderId).toBe('order-456');
expect(indexed.errorCode).toBe('INSUFFICIENT_FUNDS');
});
it('detects log volume drops indicating pipeline issues', async () => {
// Get baseline log volume for the past hour
const baseline = await esClient.count({
index: 'logs-*',
body: { query: { range: { '@timestamp': { gte: 'now-2h', lt: 'now-1h' } } } }
});
const current = await esClient.count({
index: 'logs-*',
body: { query: { range: { '@timestamp': { gte: 'now-1h' } } } }
});
// Current volume should be at least 50% of baseline (not a sudden drop)
const ratio = current.count / baseline.count;
expect(ratio).toBeGreaterThan(0.5);
});
});
APM 追踪验证
describe('Distributed Trace Validation', () => {
it('captures complete trace across all services', async () => {
// Make a request that traverses multiple services
const response = await httpClient.post('/api/orders', {
customerId: 'CUST-TRACE',
items: [{ sku: 'ITEM-1', qty: 1 }]
});
const traceId = response.headers['x-trace-id'];
expect(traceId).toBeDefined();
// Wait for trace to be indexed
await sleep(10000);
// Query Jaeger/APM for the trace
const trace = await jaegerClient.getTrace(traceId);
// Verify all expected spans exist
const spanNames = trace.spans.map(s => s.operationName);
expect(spanNames).toContain('POST /api/orders');
expect(spanNames).toContain('auth.validateToken');
expect(spanNames).toContain('order.create');
expect(spanNames).toContain('payment.authorize');
expect(spanNames).toContain('inventory.reserve');
expect(spanNames).toContain('db.insert orders');
// Verify parent-child relationships
const apiSpan = trace.spans.find(s => s.operationName === 'POST /api/orders');
const authSpan = trace.spans.find(s => s.operationName === 'auth.validateToken');
expect(authSpan.references[0].refType).toBe('CHILD_OF');
expect(authSpan.references[0].spanID).toBe(apiSpan.spanID);
});
it('traces capture error spans correctly', async () => {
// Trigger a known error
const response = await httpClient.post('/api/orders', {
customerId: 'INVALID-CUSTOMER',
items: [{ sku: 'ITEM-1', qty: 1 }]
});
const traceId = response.headers['x-trace-id'];
await sleep(10000);
const trace = await jaegerClient.getTrace(traceId);
// Find error span
const errorSpan = trace.spans.find(s => s.tags.some(t => t.key === 'error' && t.value === true));
expect(errorSpan).toBeDefined();
expect(errorSpan.logs).toContainEqual(
expect.objectContaining({
fields: expect.arrayContaining([
expect.objectContaining({ key: 'error.message' })
])
})
);
});
it('validates trace sampling rate', async () => {
const requestCount = 100;
const traceIds = [];
for (let i = 0; i < requestCount; i++) {
const resp = await httpClient.get('/api/health');
if (resp.headers['x-trace-id']) {
traceIds.push(resp.headers['x-trace-id']);
}
}
await sleep(15000);
let tracesFound = 0;
for (const traceId of traceIds) {
try {
await jaegerClient.getTrace(traceId);
tracesFound++;
} catch (e) {
// Trace not sampled
}
}
// With 10% sampling rate, expect roughly 10 traces (allow variance)
const samplingRate = tracesFound / requestCount;
expect(samplingRate).toBeGreaterThan(0.05);
expect(samplingRate).toBeLessThan(0.20);
});
});
SLA/SLO 验证
describe('SLA/SLO Compliance Validation', () => {
it('validates 99.9% availability SLO over 30 days', async () => {
const result = await prometheusClient.query(
'avg_over_time(up{job="api-service"}[30d])'
);
const availability = parseFloat(result.data.result[0].value[1]) * 100;
expect(availability).toBeGreaterThanOrEqual(99.9);
// Calculate error budget remaining
const totalMinutes = 30 * 24 * 60;
const allowedDowntime = totalMinutes * 0.001; // 43.2 minutes
const actualDowntime = totalMinutes * (1 - availability / 100);
const errorBudgetRemaining = ((allowedDowntime - actualDowntime) / allowedDowntime) * 100;
expect(errorBudgetRemaining).toBeGreaterThan(0);
console.log(`Error budget remaining: ${errorBudgetRemaining.toFixed(1)}%`);
});
it('validates P95 latency SLO', async () => {
const result = await prometheusClient.query(
'histogram_quantile(0.95, sum(rate(http_request_duration_seconds_bucket{service="api-service"}[24h])) by (le))'
);
const p95Latency = parseFloat(result.data.result[0].value[1]) * 1000; // Convert to ms
expect(p95Latency).toBeLessThan(500); // SLO: P95 < 500ms
});
it('runs synthetic monitoring check for uptime', async () => {
const endpoints = [
{ url: '/api/health', expectedStatus: 200, maxLatency: 200 },
{ url: '/api/orders', expectedStatus: 401, maxLatency: 300 }, // Auth required
{ url: '/api/products', expectedStatus: 200, maxLatency: 500 }
];
const results = [];
for (const endpoint of endpoints) {
const start = Date.now();
const response = await httpClient.get(endpoint.url);
const latency = Date.now() - start;
results.push({
url: endpoint.url,
status: response.status,
latency,
statusMatch: response.status === endpoint.expectedStatus,
latencyOk: latency <= endpoint.maxLatency
});
}
// All checks should pass
for (const result of results) {
expect(result.statusMatch).toBe(true);
expect(result.latencyOk).toBe(true);
}
});
});
指标管道完整性
describe('Metric Pipeline - Collection to Display', () => {
it('validates custom metric flows from app to Prometheus to Grafana', async () => {
// Step 1: Emit a known custom metric from application
const metricName = 'test_orders_processed_total';
const expectedValue = 42;
await appMetrics.set(metricName, expectedValue, { service: 'test' });
// Step 2: Wait for scrape interval (15s default)
await sleep(20000);
// Step 3: Query Prometheus directly
const promResult = await prometheusClient.query(`${metricName}{service="test"}`);
const promValue = parseFloat(promResult.data.result[0].value[1]);
expect(promValue).toBe(expectedValue);
// Step 4: Query Grafana datasource API (same as dashboard would)
const grafanaResult = await grafanaApi.post('/api/ds/query', {
queries: [{
datasource: { type: 'prometheus' },
expr: `${metricName}{service="test"}`,
refId: 'A'
}]
});
const grafanaValue = parseFloat(grafanaResult.body.results.A.frames[0].data.values[1][0]);
expect(grafanaValue).toBe(expectedValue);
});
it('validates histogram metric percentile accuracy', async () => {
// Generate known latency distribution
const latencies = [10, 20, 30, 50, 100, 200, 300, 500, 1000, 2000]; // ms
for (const latency of latencies) {
await appMetrics.observe('http_request_duration_ms', latency, { endpoint: '/test' });
}
await sleep(20000);
// Verify P50 and P99
const p50 = await prometheusClient.query(
'histogram_quantile(0.5, rate(http_request_duration_ms_bucket{endpoint="/test"}[5m]))'
);
const p99 = await prometheusClient.query(
'histogram_quantile(0.99, rate(http_request_duration_ms_bucket{endpoint="/test"}[5m]))'
);
const p50Value = parseFloat(p50.data.result[0].value[1]);
const p99Value = parseFloat(p99.data.result[0].value[1]);
// P50 should be around 100ms (median of our distribution)
expect(p50Value).toBeGreaterThan(50);
expect(p50Value).toBeLessThan(300);
// P99 should be around 2000ms
expect(p99Value).toBeGreaterThan(1000);
});
});
仪表板性能测试
describe('Dashboard Performance', () => {
it('dashboard loads within acceptable time', async () => {
const start = Date.now();
await page.goto(`${kibanaUrl}/app/dashboards#/view/operations-overview`);
// Wait for all panels to finish loading
await page.waitForFunction(() => {
const spinners = document.querySelectorAll('.euiLoadingSpinner');
return spinners.length === 0;
}, { timeout: 30000 });
const loadTime = Date.now() - start;
expect(loadTime).toBeLessThan(10000); // Dashboard should load in under 10s
});
it('dashboard handles large time range without timeout', async () => {
// Set time range to 30 days
await page.goto(`${kibanaUrl}/app/dashboards#/view/operations-overview?_g=(time:(from:now-30d,to:now))`);
// Should complete without timeout error
await page.waitForFunction(() => {
const errors = document.querySelectorAll('.embPanel--error');
const spinners = document.querySelectorAll('.euiLoadingSpinner');
return errors.length === 0 && spinners.length === 0;
}, { timeout: 60000 });
const errorPanels = await page.locator('.embPanel--error').count();
expect(errorPanels).toBe(0);
});
it('Elasticsearch query performance is within bounds', async () => {
const queries = [
{ name: 'date_histogram', body: { aggs: { over_time: { date_histogram: { field: '@timestamp', fixed_interval: '1h' } } }, size: 0 } },
{ name: 'terms_agg', body: { aggs: { top_services: { terms: { field: 'service.keyword', size: 20 } } }, size: 0 } },
{ name: 'percentiles', body: { aggs: { latency: { percentiles: { field: 'response_time', percents: [50, 90, 95, 99] } } }, size: 0 } }
];
for (const query of queries) {
const start = Date.now();
await esClient.search({ index: 'logs-*', body: { query: { range: { '@timestamp': { gte: 'now-24h' } } }, ...query.body } });
const elapsed = Date.now() - start;
expect(elapsed).toBeLessThan(5000); // Each query under 5s
}
});
});
时间序列数据准确性
describe('Time-Series Data Accuracy', () => {
it('validates no data gaps in time-series metrics', async () => {
const result = await prometheusClient.queryRange(
'up{job="api-service"}',
{ start: 'now-24h', end: 'now', step: '5m' }
);
const values = result.data.result[0].values;
const expectedPoints = (24 * 60) / 5; // 288 data points for 24h at 5m intervals
// Allow up to 5% missing data points
expect(values.length).toBeGreaterThan(expectedPoints * 0.95);
// Check for gaps longer than 15 minutes (3 consecutive missing points)
for (let i = 1; i < values.length; i++) {
const gap = values[i][0] - values[i - 1][0]; // timestamp difference
expect(gap).toBeLessThanOrEqual(900); // No gap > 15 minutes
}
});
it('validates timestamp alignment across sources', async () => {
// Generate event with precise timestamp
const eventTime = new Date();
const traceId = uuid();
await httpClient.post('/api/test-event', { traceId });
await sleep(15000);
// Check timestamp in logs
const logResult = await esClient.search({
index: 'logs-*',
body: { query: { term: { 'traceId.keyword': traceId } } }
});
const logTimestamp = new Date(logResult.hits.hits[0]._source['@timestamp']);
// Check timestamp in metrics (approximate)
// Timestamps should be within 5 seconds of each other
const diff = Math.abs(logTimestamp.getTime() - eventTime.getTime());
expect(diff).toBeLessThan(5000);
});
});
最佳实践
应做事项
- 将仪表盘数值与权威数据源数据库进行比对
- 使用已知数据测试告警阈值,验证确切的触发条件
- 注入可追踪的测试事件,验证日志完整性
- 测试告警恢复(自动解除),而不只是测试告警触发
- 将日志量作为管道健康状况的参考指标进行监控
- 验证 APM 跟踪的采样率
- 在真实的时间范围内测试仪表盘(而不只是最近 15 分钟)
避免事项
- 未经数据源验证就相信仪表盘数值
- 只在 UI 中手动检查阈值来测试告警
- 忽略日志管道延迟(日志可能需要几秒到几分钟才会出现)
- 跳过告警疲劳测试(误报过多)
- 未经端到端管道验证就假定指标准确
- 只使用少量数据进行测试(性能问题会在规模增大时出现)
- 忘记测试告警通知的送达情况(PagerDuty、Slack、电子邮件)
Agent 辅助的可观测性测试
// Data accuracy validation
await Task("Dashboard Data Accuracy Validation", {
dashboard: 'operations-overview',
panels: ['order-count', 'revenue-total', 'error-rate'],
sourceDatabase: 'orders-db',
compareFields: ['count', 'sum(total)', 'error_percentage'],
tolerance: 0.01
}, "qe-integration-tester");
// Dashboard performance testing
await Task("Dashboard Performance Benchmark", {
dashboardUrl: 'http://kibana:5601/app/dashboards#/view/operations-overview',
timeRanges: ['15m', '1h', '24h', '7d', '30d'],
maxLoadTime: 10000,
maxQueryTime: 5000,
captureScreenshots: true
}, "qe-performance-tester");
// Dashboard visual regression
await Task("Dashboard Visual Regression", {
dashboardUrl: 'http://kibana:5601/app/dashboards#/view/operations-overview',
baselineScreenshots: 'baseline/dashboards/',
threshold: 0.05,
ignoreRegions: ['timestamp-header', 'dynamic-counters']
}, "qe-visual-tester");
// Alert rule validation
await Task("Alert Rule Comprehensive Test", {
alertRules: ['HighErrorRate', 'HighLatency', 'ServiceDown'],
testFiring: true,
testRecovery: true,
testNotificationChannel: true,
validateSilencing: true
}, "qe-integration-tester");
Agent 协调提示
记忆命名空间
aqe/observability-testing/
dashboards/ - Dashboard test results and screenshots
alerts/ - Alert rule test outcomes
logs/ - Log completeness validation results
traces/ - APM trace validation results
slo/ - SLA/SLO compliance metrics
pipelines/ - Metric pipeline integrity checks
performance/ - Dashboard and query performance benchmarks
集群协调
const observabilityFleet = await FleetManager.coordinate({
strategy: 'observability-testing',
agents: [
'qe-integration-tester', // Data accuracy, log completeness, alert rules
'qe-performance-tester', // Dashboard load time, query latency
'qe-visual-tester' // Dashboard visual regression
],
topology: 'mesh'
});
await observabilityFleet.execute({
targets: [
{ type: 'dashboard', id: 'operations-overview', checks: ['accuracy', 'performance', 'visual'] },
{ type: 'alerts', rules: ['HighErrorRate', 'HighLatency'], checks: ['fire', 'resolve', 'notify'] },
{ type: 'logs', services: ['api', 'auth', 'payment'], checks: ['completeness', 'structure'] },
{ type: 'traces', endpoints: ['/api/orders'], checks: ['spans', 'errors', 'sampling'] }
]
});
相关技能
- api-testing-patterns - 为提供可观测性数据的服务进行 API 测试
- shift-right-testing - 生产环境监控与可观测性
- performance-testing - 性能基线与 SLO 定义
- chaos-engineering-resilience - 通过故障注入验证告警
- visual-testing-advanced - 仪表盘视觉回归测试
请记住
可观测性测试旨在证明监控能够反映真实情况。系统已经发生严重故障,仪表盘却仍显示绿色,这比没有仪表盘更糟。将数据与源数据库进行比对以验证准确性,使用受控数据注入测试告警阈值,并通过追踪已知事件在整个管道中的流转来验证日志完整性。
使用 Agent: Agent 可以自动完成将仪表盘数值与源数据库比对这项繁琐工作,使用合成负载系统地测试告警阈值,并验证所有服务的日志管道完整性。使用 Agent 持续验证可观测性技术栈是否值得信赖。
评论
支持 Markdown