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 ceedadc3bd Add service overview live proof
ceedadc3bd is described below
commit ceedadc3bd9bf9ee75e96e2d53778dac217d796d
Author: Logic <[email protected]>
AuthorDate: Mon Jun 8 09:39:08 2026 +0800
Add service overview live proof
---
script/dev/run-three-signal-live-proof.sh | 2 +-
...ashboard-source-edit-live-browser-smoke.spec.ts | 133 +++++++++++++++++++++
...ashboard-source-edit-live-browser-smoke.test.ts | 21 ++++
.../three-signal-live-proof-contract.test.ts | 2 +-
4 files changed, 156 insertions(+), 2 deletions(-)
diff --git a/script/dev/run-three-signal-live-proof.sh
b/script/dev/run-three-signal-live-proof.sh
index 9fb35c3310..5976d884e4 100755
--- a/script/dev/run-three-signal-live-proof.sh
+++ b/script/dev/run-three-signal-live-proof.sh
@@ -15,7 +15,7 @@ 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}"
+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}"
BACKEND_READY_PATH="${BACKEND_READY_PATH:-/actuator/health}"
READY_ATTEMPTS="${READY_ATTEMPTS:-90}"
READY_SLEEP_SECONDS="${READY_SLEEP_SECONDS:-2}"
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 f922c18fb0..a214d54b57 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,
+ buildSignalServiceOverviewDashboard,
createSignalDashboardPanelDraftFromRuntimeBreakout,
createSignalDashboardPanelDraftFromRuntimeEvidence,
type SignalDashboardRuntimeSyncTooltipRow
@@ -156,6 +157,23 @@ function uniqueSavedViewReplayDashboardKey() {
return
`${DASHBOARD_KEY_PREFIX}-saved-view-replay-${randomBytes(4).toString('hex')}`;
}
+function serviceOverviewRoute() {
+ return [
+ '/dashboard?serviceName=checkout',
+ 'serviceNamespace=payments',
+ 'environment=prod',
+ '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';
@@ -467,6 +485,121 @@ function assertPlanMatchesReplayExpectation(
}
test.describe('live dashboard source edit browser smoke', () => {
+ test('saves a service overview dashboard from URL service and entity
context', async ({ page }) => {
+ test.setTimeout(BROWSER_SMOKE_TIMEOUT);
+
+ const expectedDashboard = buildSignalServiceOverviewDashboard({
+ serviceName: 'checkout',
+ serviceNamespace: 'payments',
+ environment: 'prod',
+ 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}${serviceOverviewRoute()}`, {
+ timeout: BROWSER_SMOKE_TIMEOUT,
+ waitUntil: 'domcontentloaded'
+ });
+
+ const serviceOverviewAction =
page.locator('[data-dashboard-service-overview-action="save"]').first();
+ await
expect(page.locator('[data-dashboard-service-overview-context="ready"]')).toHaveAttribute(
+ 'data-dashboard-service-overview-service',
+ 'checkout',
+ { timeout: WORKBENCH_READY_TIMEOUT }
+ );
+ await
expect(serviceOverviewAction).toHaveAttribute('data-dashboard-service-overview-action-state',
'ready');
+ await
expect(serviceOverviewAction).toHaveAttribute('data-dashboard-service-overview-action-service',
'checkout');
+ await serviceOverviewAction.click();
+
+ await expect(page).toHaveURL(/dashboard=service-checkout-overview/, {
+ timeout: WORKBENCH_READY_TIMEOUT
+ });
+ await
expect(page.locator('[data-dashboard-composition-preview-panel]')).toHaveCount(18,
{
+ 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(18);
+
+ const persistedDashboard = await loadDashboard(page.context().request,
dashboardKey);
+ const persistedWidgets = parseWidgets(persistedDashboard);
+ const persistedVariables = parseVariables(persistedDashboard);
+ expect(persistedDashboard).toEqual(expect.objectContaining({
+ dashboardKey: 'service-checkout-overview',
+ title: 'Checkout API service overview',
+ tags: 'service,apm,metrics,logs,traces,alerts'
+ }));
+ expect(persistedWidgets.map(widget =>
widget.title)).toEqual(expect.arrayContaining([
+ 'Service overview request rate: service.name=checkout',
+ 'Service overview error rate: service.name=checkout',
+ 'Service overview apdex: service.name=checkout',
+ 'Service overview log errors: service.name=checkout',
+ 'Service overview exceptions: service.name=checkout',
+ 'Service overview firing alerts: service.name=checkout'
+ ]));
+ expect(persistedVariables).toEqual(expect.arrayContaining([
+ expect.objectContaining({ name: 'service.name', type: 'query', value:
'checkout' }),
+ expect.objectContaining({ name: 'service.namespace', type: 'query',
value: 'payments' }),
+ expect.objectContaining({ name: 'deployment.environment.name', type:
'query', value: 'prod' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_id', type:
'dynamic', value: '4200' }),
+ expect.objectContaining({ name: 'hertzbeat.entity_type', type:
'dynamic', value: 'service' }),
+ expect.objectContaining({ name: 'hertzbeat.template', type: 'dynamic',
value: 'spring-boot' })
+ ]));
+
+ const plans = buildSignalDashboardExecutionPlans(persistedDashboard);
+ const planForTitle = (titleNeedle: string) => {
+ const widget = persistedWidgets.find(item =>
item.title.includes(titleNeedle));
+ expect(widget, `service overview 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',
'alerts']));
+ expect(planForTitle('request rate')).toEqual(expect.objectContaining({
+ signal: 'metrics',
+ state: 'ready',
+ primaryUrl: expect.stringContaining('/ingestion/otlp/metrics/console')
+ }));
+ expect(planForTitle('request
rate')?.primaryUrl).toEqual(expect.stringContaining('entityType=service'));
+
expect(planForTitle('apdex')?.primaryUrl).toEqual(expect.stringContaining('template=service-apdex'));
+ expect(planForTitle('log
errors')?.primaryUrl).toEqual(expect.stringContaining('/logs/list'));
+ expect(planForTitle('log
errors')?.primaryUrl).toEqual(expect.stringContaining('severityText=ERROR'));
+
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('/traces/stats/group-by'));
+
expect(planForTitle('exceptions')?.primaryUrl).toEqual(expect.stringContaining('groupBy=exception.type'));
+ expect(planForTitle('firing
alerts')?.primaryUrl).toEqual(expect.stringContaining('/alerts/group'));
+ expect(planForTitle('firing
alerts')?.primaryUrl).toEqual(expect.stringContaining('status=firing'));
+
+ const requestRatePanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Service overview request rate' }).first();
+ await
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'metrics');
+ await
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/entityType=service/);
+ await
expect(requestRatePanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/temporalAggregation=rate/);
+
+ const alertPanel =
page.locator('[data-dashboard-composition-preview-panel]').filter({ hasText:
'Service overview firing alerts' }).first();
+ await
expect(alertPanel).toHaveAttribute('data-dashboard-composition-preview-signal',
'alerts');
+ await
expect(alertPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/\/alerts\/group\?/);
+ await
expect(alertPanel).toHaveAttribute('data-dashboard-composition-execution-primary-url',
/status=firing/);
+ } 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 e7d78a005c..962684418c 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
@@ -24,6 +24,27 @@ describe('live dashboard source edit browser smoke
contract', () => {
expect(source).toContain("expect(payload.dashboardPanelEdit).toEqual(expect.objectContaining({");
expect(source).toContain("promotes a saved query view through real
saved-view and panel-draft APIs");
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("data-dashboard-service-overview-action=\"save\"");
+
expect(source).toContain("data-dashboard-service-overview-context=\"ready\"");
+ expect(source).toContain("Checkout API service overview");
+ expect(source).toContain("expect(persistedWidgets.map(widget =>
widget.title)).toEqual(expect.arrayContaining([");
+ expect(source).toContain("'Service overview request rate:
service.name=checkout'");
+ expect(source).toContain("'Service overview error rate:
service.name=checkout'");
+ expect(source).toContain("'Service overview apdex:
service.name=checkout'");
+ 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("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)");
+ expect(source).toContain("expect(plans.map(plan =>
plan.signal)).toEqual(expect.arrayContaining(['metrics', 'logs', 'traces',
'alerts']))");
+ expect(source).toContain("entityType=service");
+ expect(source).toContain("template=service-apdex");
+ expect(source).toContain("/logs/list");
+ expect(source).toContain("/traces/stats/group-by");
+ expect(source).toContain("/alerts/group");
expect(source).toContain("buildThreeSignalWorkbenchDashboardReplayExpectations");
expect(source).toContain("buildThreeSignalWorkbenchExpectedDashboardVariables");
expect(source).toContain("seedReplaySavedView(page.context().request,
expectation, proofKey)");
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 6eecc69aa5..ee9c745f7f 100644
--- a/web-next/scripts/three-signal-live-proof-contract.test.ts
+++ b/web-next/scripts/three-signal-live-proof-contract.test.ts
@@ -17,7 +17,7 @@ describe('three-signal live proof script contract', () => {
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');
+ 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('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]