This is an automated email from the ASF dual-hosted git repository.
zqr10159 pushed a commit to branch 2.0.0
in repository https://gitbox.apache.org/repos/asf/hertzbeat.git
The following commit(s) were added to refs/heads/2.0.0 by this push:
new 62941f3322 Add operation drilldown live proof
62941f3322 is described below
commit 62941f3322a58abce4de883ee5c0a336881d548d
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 10:56:30 2026 +0800
Add operation drilldown live proof
---
script/dev/run-three-signal-live-proof.sh | 4 +-
...ashboard-source-edit-live-browser-smoke.spec.ts | 134 +++++++++++++++++++++
...ashboard-source-edit-live-browser-smoke.test.ts | 11 ++
.../three-signal-live-proof-contract.test.ts | 3 +-
4 files changed, 149 insertions(+), 3 deletions(-)
diff --git a/script/dev/run-three-signal-live-proof.sh
b/script/dev/run-three-signal-live-proof.sh
index 5976d884e4..2b0861f9f1 100755
--- a/script/dev/run-three-signal-live-proof.sh
+++ b/script/dev/run-three-signal-live-proof.sh
@@ -15,9 +15,9 @@ HERTZBEAT_PASSWORD="${HERTZBEAT_PASSWORD:-hertzbeat}"
SPRING_DATASOURCE_URL="${SPRING_DATASOURCE_URL:-jdbc:h2:mem:hb_live_smoke;MODE=MYSQL;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=FALSE}"
BACKEND_LOG="${BACKEND_LOG:-/tmp/hb-three-signal-live-backend.log}"
FRONTEND_LOG="${FRONTEND_LOG:-/tmp/hb-three-signal-live-frontend.log}"
-PLAYWRIGHT_GREP="${PLAYWRIGHT_GREP:-promotes logs, traces, and metrics saved
views into a persisted replay dashboard|saves a service overview dashboard from
URL service and entity context}"
+PLAYWRIGHT_GREP="${PLAYWRIGHT_GREP:-promotes logs, traces, and metrics saved
views into a persisted replay dashboard|saves a service overview dashboard from
URL service and entity context|saves an operation drilldown dashboard from URL
operation context}"
BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"
-READY_ATTEMPTS="${READY_ATTEMPTS:-90}"
+READY_ATTEMPTS="${READY_ATTEMPTS:-300}"
READY_SLEEP_SECONDS="${READY_SLEEP_SECONDS:-2}"
DRY_RUN=false
diff --git a/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
b/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
index a214d54b57..bdb9eed634 100644
--- a/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
+++ b/web-next/scripts/dashboard-source-edit-live-browser-smoke.spec.ts
@@ -3,6 +3,7 @@ import { expect, test, type APIRequestContext, type Page } from
'playwright/test
import {
buildSignalDashboardCompositionFromDrafts,
buildSignalDashboardExecutionPlans,
+ buildSignalOperationDrilldownDashboard,
buildSignalServiceOverviewDashboard,
createSignalDashboardPanelDraftFromRuntimeBreakout,
createSignalDashboardPanelDraftFromRuntimeEvidence,
@@ -174,6 +175,24 @@ function serviceOverviewRoute() {
].join('&');
}
+function operationDrilldownRoute() {
+ return [
+ '/dashboard?serviceName=checkout',
+ 'serviceNamespace=payments',
+ 'environment=prod',
+ 'operationName=POST%20%2Fcheckout',
+ 'entityId=4200',
+ 'entityType=service',
+ 'entityName=Checkout%20API',
+ 'source=otlp',
+ 'collector=collector-a',
+ 'template=spring-boot',
+ 'timeRange=last-1h',
+ 'refresh=30',
+ 'live=true'
+ ].join('&');
+}
+
function titleFromRoute(signalCase: SignalCase, route: string) {
if (signalCase.signal === 'logs') return route.includes('latency') ?
'latency' : 'timeout';
if (signalCase.signal === 'traces') return route.includes('GET+%2Fbilling')
? 'GET /billing' : 'POST /checkout';
@@ -600,6 +619,121 @@ test.describe('live dashboard source edit browser smoke',
() => {
}
});
+ test('saves an operation drilldown dashboard from URL operation context',
async ({ page }) => {
+ test.setTimeout(BROWSER_SMOKE_TIMEOUT);
+
+ const expectedDashboard = buildSignalOperationDrilldownDashboard({
+ serviceName: 'checkout',
+ serviceNamespace: 'payments',
+ environment: 'prod',
+ operationName: 'POST /checkout',
+ entityId: '4200',
+ entityType: 'service',
+ entityName: 'Checkout API',
+ source: 'otlp',
+ collector: 'collector-a',
+ template: 'spring-boot',
+ timeRange: 'last-1h',
+ refresh: '30',
+ live: 'true'
+ });
+ const dashboardKey = expectedDashboard.dashboardKey;
+ await authenticate(page);
+ await
page.context().request.delete(`${baseUrl}/api/signal/dashboard/${encodeURIComponent(dashboardKey)}`,
{
+ failOnStatusCode: false
+ });
+
+ try {
+ await page.goto(`${baseUrl}${operationDrilldownRoute()}`, {
+ timeout: BROWSER_SMOKE_TIMEOUT,
+ waitUntil: 'domcontentloaded'
+ });
+
+ const operationDrilldownAction =
page.locator('[data-dashboard-operation-drilldown-action="save"]').first();
+ await
expect(page.locator('[data-dashboard-operation-drilldown-context="ready"]')).toHaveAttribute(
+ 'data-dashboard-operation-drilldown-operation',
+ 'POST /checkout',
+ { timeout: WORKBENCH_READY_TIMEOUT }
+ );
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-state',
'ready');
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-service',
'checkout');
+ await
expect(operationDrilldownAction).toHaveAttribute('data-dashboard-operation-drilldown-action-operation',
'POST /checkout');
+ await operationDrilldownAction.click();
+
+ await
expect(page).toHaveURL(/dashboard=service-checkout-operation-post-checkout-drilldown/,
{
+ timeout: WORKBENCH_READY_TIMEOUT
+ });
+ await
expect(page.locator('[data-dashboard-composition-preview-panel]')).toHaveCount(8,
{
+ timeout: WORKBENCH_READY_TIMEOUT
+ });
+
+ await expect.poll(async () => {
+ const persisted = await loadDashboard(page.context().request,
dashboardKey);
+ return parseWidgets(persisted).length;
+ }, {
+ timeout: WORKBENCH_READY_TIMEOUT
+ }).toBe(8);
+
+ const persistedDashboard = await loadDashboard(page.context().request,
dashboardKey);
+ const persistedWidgets = parseWidgets(persistedDashboard);
+ const persistedVariables = parseVariables(persistedDashboard);
+ expect(persistedDashboard).toEqual(expect.objectContaining({
+ dashboardKey: 'service-checkout-operation-post-checkout-drilldown',
+ title: 'Checkout API POST /checkout operation drilldown',
+ tags: 'service,operation,apm,metrics,logs,traces'
+ }));
+ expect(persistedWidgets.map(widget => widget.title)).toEqual([
+ 'Operation drilldown latency p95: operation.name=POST /checkout',
+ 'Operation drilldown request rate: operation.name=POST /checkout',
+ 'Operation drilldown error rate: operation.name=POST /checkout',
+ 'Operation drilldown logs: operation.name=POST /checkout',
+ 'Operation drilldown log errors: operation.name=POST /checkout',
+ 'Operation drilldown traces: operation.name=POST /checkout',
+ 'Operation drilldown trace errors: operation.name=POST /checkout',
+ 'Operation drilldown exceptions: operation.name=POST /checkout'
+ ]);
+ expect(persistedVariables).toEqual(expect.arrayContaining([
+ expect.objectContaining({ name: 'operation.name', type: 'query',
value: 'POST /checkout' }),
+ expect.objectContaining({ name: 'service.name', type: 'query', value:
'checkout' }),
+ expect.objectContaining({ name: 'service.namespace', type: 'query',
value: 'payments' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_id', type:
'dynamic', value: '4200' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_type', type:
'dynamic', value: 'service' })
+ ]));
+
+ const plans = buildSignalDashboardExecutionPlans(persistedDashboard);
+ const planForTitle = (titleNeedle: string) => {
+ const widget = persistedWidgets.find(item =>
item.title.includes(titleNeedle));
+ expect(widget, `operation drilldown widget ${titleNeedle} should
exist`).toBeTruthy();
+ return plans.find(plan => plan.panelId === widget?.id);
+ };
+ expect(plans.map(plan =>
plan.signal)).toEqual(expect.arrayContaining(['metrics', 'logs', 'traces']));
+ expect(planForTitle('latency p95')).toEqual(expect.objectContaining({
+ signal: 'metrics',
+ state: 'ready',
+ primaryUrl:
expect.stringContaining('operation%3D%22POST+%2Fcheckout%22')
+ }));
+
expect(planForTitle('logs')?.primaryUrl).toEqual(expect.stringContaining('/logs/list'));
+
expect(planForTitle('logs')?.primaryUrl).toEqual(expect.stringContaining('attributeFilter=http.route%3APOST+%2Fcheckout'));
+
expect(planForTitle('traces')?.primaryUrl).toEqual(expect.stringContaining('/traces/list'));
+
expect(planForTitle('traces')?.primaryUrl).toEqual(expect.stringContaining('operationName=POST+%2Fcheckout'));
+
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('/traces/stats/group-by'));
+
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('groupBy=exception.type'));
+
+ const latencyPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown latency p95' }).first();
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'metrics');
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/operation%3D%22POST\+%2Fcheckout%22/);
+ await
expect(latencyPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/entityType=service/);
+
+ const tracesPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Operation drilldown traces' }).first();
+ await
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'traces');
+ await
expect(tracesPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/operationName=POST\+%2Fcheckout/);
+ } finally {
+ await
page.context().request.delete(`${baseUrl}/api/signal/dashboard/${encodeURIComponent(dashboardKey)}`,
{
+ failOnStatusCode: false
+ });
+ }
+ });
+
test('promotes logs, traces, and metrics saved views into a persisted replay
dashboard', async ({ page }) => {
test.setTimeout(BROWSER_SMOKE_TIMEOUT);
diff --git a/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
b/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
index 962684418c..e89f43012c 100644
--- a/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
+++ b/web-next/scripts/dashboard-source-edit-live-browser-smoke.test.ts
@@ -26,6 +26,8 @@ describe('live dashboard source edit browser smoke contract',
() => {
expect(source).toContain("promotes logs, traces, and metrics saved views
into a persisted replay dashboard");
expect(source).toContain("saves a service overview dashboard from URL
service and entity context");
expect(source).toContain("buildSignalServiceOverviewDashboard");
+ expect(source).toContain("saves an operation drilldown dashboard from URL
operation context");
+ expect(source).toContain("buildSignalOperationDrilldownDashboard");
expect(source).toContain("data-dashboard-service-overview-action=\"save\"");
expect(source).toContain("data-dashboard-service-overview-context=\"ready\"");
expect(source).toContain("Checkout API service overview");
@@ -36,6 +38,15 @@ describe('live dashboard source edit browser smoke
contract', () => {
expect(source).toContain("'Service overview log errors:
service.name=checkout'");
expect(source).toContain("'Service overview exceptions:
service.name=checkout'");
expect(source).toContain("'Service overview firing alerts:
service.name=checkout'");
+
expect(source).toContain("data-dashboard-operation-drilldown-action=\"save\"");
+
expect(source).toContain("data-dashboard-operation-drilldown-context=\"ready\"");
+
expect(source).toContain("service-checkout-operation-post-checkout-drilldown");
+ expect(source).toContain("Checkout API POST /checkout operation
drilldown");
+ expect(source).toContain("'Operation drilldown latency p95:
operation.name=POST /checkout'");
+ expect(source).toContain("'Operation drilldown traces: operation.name=POST
/checkout'");
+ expect(source).toContain("operation%3D%22POST+%2Fcheckout%22");
+ expect(source).toContain("attributeFilter=http.route%3APOST+%2Fcheckout");
+ expect(source).toContain("operationName=POST+%2Fcheckout");
expect(source).toContain("expect.objectContaining({ name:
'hertzbeat.entity_id', type: 'dynamic', value: '4200' })");
expect(source).toContain("expect.objectContaining({ name:
'hertzbeat.entity_type', type: 'dynamic', value: 'service' })");
expect(source).toContain("buildSignalDashboardExecutionPlans(persistedDashboard)");
diff --git a/web-next/scripts/three-signal-live-proof-contract.test.ts
b/web-next/scripts/three-signal-live-proof-contract.test.ts
index ee9c745f7f..ca84696d82 100644
--- a/web-next/scripts/three-signal-live-proof-contract.test.ts
+++ b/web-next/scripts/three-signal-live-proof-contract.test.ts
@@ -11,13 +11,14 @@ describe('three-signal live proof script contract', () => {
expect(source).toContain('script/dev/start-workspace-backend.sh');
expect(source).toContain('script/dev/start-mixed-frontend.sh');
expect(source).toContain('BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"');
+ expect(source).toContain('READY_ATTEMPTS="${READY_ATTEMPTS:-300}"');
expect(source).toContain('"readyPath": "${BACKEND_READY_PATH}"');
expect(source).toContain('wait_for_http
"${HERTZBEAT_BASE}${BACKEND_READY_PATH}"');
expect(source).toContain('|| "${status}" == "401"');
expect(source).toContain('TRACE_ID="${TRACE_ID}"
HERTZBEAT_BASE="${HERTZBEAT_BASE}" bash
script/dev/verify-otlp-three-signal-demo.sh');
expect(source).toContain('DASHBOARD_SOURCE_EDIT_LIVE_BROWSER_BASE_URL="${FRONTEND_BASE}"');
expect(source).toContain('npm exec -- playwright test
scripts/dashboard-source-edit-live-browser-smoke.spec.ts -g
"${PLAYWRIGHT_GREP}"');
- expect(source).toContain('promotes logs, traces, and metrics saved views
into a persisted replay dashboard|saves a service overview dashboard from URL
service and entity context');
+ expect(source).toContain('promotes logs, traces, and metrics saved views
into a persisted replay dashboard|saves a service overview dashboard from URL
service and entity context|saves an operation drilldown dashboard from URL
operation context');
expect(source).toContain('trap cleanup EXIT');
expect(source).toContain('kill "${BACKEND_PID}"');
expect(source).toContain('kill "${FRONTEND_WRAPPER_PID}"');
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]