mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
143 lines
5.7 KiB
JavaScript
143 lines
5.7 KiB
JavaScript
// databaseMonitoringPlans.spec.js
|
|
//
|
|
// EXECUTION PLANS MUST REACH THE SCREEN, not merely the stream.
|
|
//
|
|
// The receivers (mysqlreceiver, postgresqlreceiver, sqlserverreceiver) run
|
|
// EXPLAIN themselves and ship the plan as an attribute on `db.server.top_query`.
|
|
// That is three hops from a reader seeing it: the receiver must emit it, the
|
|
// backend must KEEP it (SQL Server's was dropped for want of one entry in a
|
|
// lookup list), and the detail page must RENDER it. A test that stops at the
|
|
// stream proves only the first two.
|
|
//
|
|
// Engine-agnostic and volume-agnostic by construction: the fingerprint under
|
|
// test is DISCOVERED from the org's own data, never hardcoded, so this suite
|
|
// says something true on whichever org ORGNAME points at rather than passing
|
|
// vacuously on one and fabricating failures on another.
|
|
|
|
const { test, expect } = require('../utils/enhanced-baseFixtures.js');
|
|
const testLogger = require('../utils/test-logger.js');
|
|
const PageManager = require('../../pages/page-manager.js');
|
|
|
|
/** Ask the stream for a statement this org actually holds a plan for. */
|
|
async function planBearingQuery(page, { periodSeconds = 3600 } = {}) {
|
|
const org = process.env['ORGNAME'] || 'default';
|
|
const baseUrl = (process.env['ZO_BASE_URL'] || '').replace(/\/+$/, '');
|
|
const now = Date.now() * 1000;
|
|
const res = await page.request.post(
|
|
`${baseUrl}/api/${org}/_search?type=logs`,
|
|
{
|
|
data: {
|
|
query: {
|
|
sql:
|
|
"SELECT * FROM _o2_dbm_server WHERE o2_dbm_plan IS NOT NULL LIMIT 5",
|
|
start_time: now - periodSeconds * 1_000_000,
|
|
end_time: now,
|
|
from: 0,
|
|
size: 5,
|
|
},
|
|
},
|
|
},
|
|
);
|
|
if (!res.ok()) return null;
|
|
const body = await res.json().catch(() => null);
|
|
const hits = body?.hits ?? [];
|
|
if (!hits.length) return null;
|
|
return {
|
|
fingerprint: hits[0].o2_dbm_fingerprint,
|
|
engine: hits[0].o2_dbm_engine,
|
|
planHash: hits[0].o2_dbm_plan_hash,
|
|
planSource: hits[0].o2_dbm_plan_source,
|
|
plan: hits[0].o2_dbm_plan,
|
|
count: hits.length,
|
|
};
|
|
}
|
|
|
|
test.describe('Database Monitoring — execution plans', () => {
|
|
let pm;
|
|
let dbm;
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
pm = new PageManager(page);
|
|
dbm = pm.databaseMonitoringPage;
|
|
});
|
|
|
|
test.afterEach(async ({}, testInfo) => {
|
|
testLogger.testEnd(testInfo.title, testInfo.status);
|
|
});
|
|
|
|
test('P0: a query the database planned renders its plan on the detail page', {
|
|
tag: ['@dbm', '@infra', '@P0', '@plans', '@all'],
|
|
}, async ({ page }) => {
|
|
const found = await planBearingQuery(page);
|
|
test.skip(!found?.fingerprint, 'this org holds no execution plans in this window');
|
|
testLogger.info(
|
|
`plan-bearing query: engine=${found.engine} fp=${found.fingerprint} ` +
|
|
`hash=${String(found.planHash).slice(0, 12)} source=${found.planSource}`,
|
|
);
|
|
|
|
// `?tab=plans` is REQUIRED, not cosmetic: the detail page's panels use
|
|
// `OTabPanel`, which defaults to `v-if` rather than `v-show`, so the plans
|
|
// section does not exist in the DOM at all while the Overview tab is
|
|
// active. Landing on the default tab and asserting on the section is a
|
|
// test bug that reads exactly like a missing feature.
|
|
await dbm.openQueryDetailTab(found.fingerprint, 'plans', { system: found.engine });
|
|
|
|
// The plans SECTION must exist. Its absence means the page did not even
|
|
// offer the reader a place to look, which is a different and worse failure
|
|
// than an empty one.
|
|
const section = dbm.detailPlansSection;
|
|
await expect(
|
|
section,
|
|
'the detail page rendered no plans section for a query that HAS a plan',
|
|
).toBeVisible({ timeout: 30000 });
|
|
|
|
// THE INVARIANT. A plan is stored for this statement, so the section must
|
|
// show one — not the "no plans captured" state that a reader would take as
|
|
// "this engine cannot do plans".
|
|
const planCards = dbm.detailPlanCards;
|
|
await expect
|
|
.poll(async () => planCards.count(), { timeout: 30000 })
|
|
.toBeGreaterThan(0);
|
|
|
|
const rendered = await section.innerText();
|
|
testLogger.info(`plans section (first 160): ${rendered.replace(/\s+/g, ' ').slice(0, 160)}`);
|
|
expect(
|
|
rendered.trim().length,
|
|
'the plans section rendered but carried no text',
|
|
).toBeGreaterThan(0);
|
|
});
|
|
|
|
test('P1: the rendered plan is the engine\'s own, not a placeholder', {
|
|
tag: ['@dbm', '@infra', '@P1', '@plans', '@all'],
|
|
}, async ({ page }) => {
|
|
const found = await planBearingQuery(page);
|
|
test.skip(!found?.fingerprint, 'this org holds no execution plans in this window');
|
|
|
|
await dbm.openQueryDetailTab(found.fingerprint, 'plans', { system: found.engine });
|
|
const section = dbm.detailPlansSection;
|
|
await expect(section).toBeVisible({ timeout: 30000 });
|
|
await page.waitForTimeout(2000);
|
|
|
|
const rendered = (await section.innerText()).toLowerCase();
|
|
|
|
// A plan document has recognisable structure per engine. Asserting on the
|
|
// ENGINE'S OWN vocabulary rather than a fixed string keeps this honest
|
|
// across Postgres (JSON "Node Type"), MySQL (JSON "query_block") and SQL
|
|
// Server (XML ShowPlanXML) — all three of which reach this page.
|
|
const markers = {
|
|
postgresql: ['node type', 'plan', 'cost'],
|
|
mysql: ['query_block', 'cost', 'table'],
|
|
mariadb: ['query_block', 'cost', 'table'],
|
|
mssql: ['showplan', 'stmt', 'plan'],
|
|
};
|
|
const expected = markers[found.engine] ?? ['plan'];
|
|
const hit = expected.some((m) => rendered.includes(m));
|
|
expect(
|
|
hit,
|
|
`the plans section for ${found.engine} shows none of ${JSON.stringify(expected)} — ` +
|
|
`rendered: ${rendered.slice(0, 200)}`,
|
|
).toBeTruthy();
|
|
});
|
|
});
|