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 クエリーの正確性 | データソース | 高速 |
| ダッシュボードの精度 | 表示がソースデータと一致すること | フルスタック | 中速 |
| アラートのしきい値 | 発火と通知のテスト | アラートスタック | 中速 |
| パイプラインの完全性 | メトリクスフローのエンドツーエンド検証 | フルパイプライン | 低速 |
| パフォーマンス | ダッシュボードのレンダリング時間、クエリーのレイテンシ | フルスタック | 低速 |
重要なテストシナリオ
| シナリオ | 必ずテストする項目 | 例 |
|---|---|---|
| データ精度 | ダッシュボード = ソースの真実 | ダッシュボード上の注文数 = DB の件数 |
| アラート発火 | しきい値によるアラートの発火 | エラー率 > 5% で PagerDuty が発火 |
| アラート復旧 | 復旧時の自動解決 | エラー率が 5% を下回るとアラートが解除される |
| ログの完全性 | すべてのサービスがログを出力すること | 10 個のマイクロサービスすべてのログが Kibana にある |
| トレースの完全性 | リクエストの全経路が表示されること | Auth -> API -> DB -> Cache のスパン |
| 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
エージェントの連携
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、メール)をテストするのを忘れる
エージェント支援によるオブザーバビリティテスト
// 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");
エージェント連携のヒント
メモリ名前空間
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 - ダッシュボードの視覚的リグレッション
覚えておくこと
オブザーバビリティテストとは、監視が真実を伝えていることを証明することです。システムが炎上しているときに緑色を表示するダッシュボードは、ダッシュボードがないよりも悪いです。情報源のデータベースと比較してデータの正確性を検証し、制御されたデータ注入でアラートしきい値をテストし、既知のイベントをパイプライン全体で追跡してログの完全性を検証してください。
エージェントを活用する場合: エージェントは、ダッシュボードの値と情報源のデータベースとの面倒な比較を自動化し、合成負荷でアラートしきい値を体系的にテストし、すべてのサービスにわたってログパイプラインの完全性を検証します。エージェントを使用して、オブザーバビリティスタックが信頼できることを継続的に検証してください。