Files
c99cb0d516 fix(logs): logs finesse phase 1 — severity inference, auto-run, free-text FTS search, J/K, line permalinks (#15234)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
Co-authored-by: Neha P <neha@openobserve.ai>
2026-10-12 16:50:32 +00:00

214 lines
9.2 KiB
JavaScript

// Copyright 2026 OpenObserve Inc.
//
// This program is free software: you can redistribute it and/or modify
// it under the terms of the GNU Affero General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// This program is distributed in the hope that it will be useful
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Affero General Public License for more details.
//
// You should have received a copy of the GNU Affero General Public License
// along with this program. If not, see <http://www.gnu.org/licenses/>.
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
const testLogger = require('../utils/test-logger.js');
const {
MINUTE_US,
trackSearches,
ingestRows,
waitForStats,
estimateMb,
readConfig,
} = require('../utils/auto-run-helpers.js');
const ORG = `autorunguard${Date.now()}`;
const STREAM = 'guarded_24h';
const HOUR_US = 60 * MINUTE_US;
const guard = '[data-test="logs-auto-run-guard"]';
const banner = '[data-test="logs-auto-run-guard-banner"]';
const runAnyway = '[data-test="logs-auto-run-guard-run-btn"]';
const narrow = '[data-test="logs-auto-run-guard-narrow-btn"]';
const rows = '[data-test="logs-search-result-logs-table"] tbody tr';
const runInFlight = '[data-test="logs-search-bar-refresh-btn"][class*="bg-cancel-query-bg"]';
const urlFor = (period) =>
`/web/logs?org_identifier=${ORG}&stream=${STREAM}&stream_type=logs&period=${period}` +
'&refresh=0&sql_mode=false&quick_mode=false&show_histogram=true';
async function pickRelative(page, value, unit) {
await page.locator('[data-test="date-time-btn"]').click({ force: true });
await page.locator(`[data-test="date-time-relative-${value}-${unit}-btn"]`).click();
await page.keyboard.press('Escape');
}
// No run starts while the timers fire: a dispatched hits run flips the Run button to Cancel at once.
async function runTimersExpectingNoRun(page, ms) {
await page.clock.runFor(ms);
await settlePageWork(page);
await expect(page.locator(runInFlight)).toHaveCount(0);
}
// networkidle resolves at once after the first load, so a fired timer's async chain gets a bounded real wait instead.
async function settlePageWork(page) {
await page.evaluate(() => new Promise((resolve) => queueMicrotask(resolve)));
await page.clock.runFor(0);
await new Promise((resolve) => setTimeout(resolve, 300));
await page.clock.runFor(0);
}
async function setInterval5s(page) {
await page.locator('[data-test="logs-search-bar-refresh-interval-btn"]').click();
await page.locator('[data-test="logs-search-bar-refresh-time-5"]').click();
await page.keyboard.press('Escape');
}
// Natural time until a test calls clock.runFor, which then fires every due app timer at once.
async function open(page, period) {
await page.clock.install();
await navigateToBase(page);
await page.evaluate(() => {
for (const key of Object.keys(localStorage)) {
if (/^oo_(toggle_auto_run|logs_|selected_stream_)/.test(key)) localStorage.removeItem(key);
}
});
const searches = trackSearches(page);
await page.goto(urlFor(period));
return searches;
}
test.describe.configure({ mode: 'serial' });
test.describe('Logs Auto Run cost guard', () => {
test.beforeAll(async ({ request }) => {
const config = await readConfig(request);
test.skip(!Number.isFinite(Number(config.auto_query_max_scan_mb)), 'The backend phase adds auto_query_max_scan_mb.');
const now = Date.now() * 1000;
const filler = 'x'.repeat(1800);
const rows = Array.from({ length: 7200 }, (_, i) => ({
_timestamp: now - 24 * HOUR_US + i * 12_000_000 + 1_000_000,
level: i % 7 === 0 ? 'error' : 'info',
message: `guard row ${i} ${filler}`,
}));
await ingestRows(request, ORG, STREAM, rows);
const entry = await waitForStats(request, ORG, STREAM);
expect(config.auto_query_enabled).toBe(true);
expect(Number(config.auto_query_max_scan_mb)).toBe(1);
expect(estimateMb(entry, 15 * MINUTE_US)).toBeLessThanOrEqual(1);
expect(estimateMb(entry, 24 * HOUR_US)).toBeGreaterThan(1);
});
test('a 24 h link is guarded: nothing is sent and the guard shows', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '24h');
await expect(page.locator(guard)).toBeVisible({ timeout: 60000 });
await expect(page.locator('[data-test="logs-auto-run-guard-estimate"]')).toContainText(STREAM);
await runTimersExpectingNoRun(page, 2000);
expect(searches.all()).toHaveLength(0);
testLogger.info('Run anyway sends exactly one hits request');
await page.locator(runAnyway).click();
await expect(page.locator(rows).first()).toBeVisible({ timeout: 60000 });
expect(searches.hits()).toHaveLength(1);
});
test('Narrow to changes the picker and runs once; widening back shows the banner over the rows', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '24h');
await expect(page.locator(narrow)).toBeVisible({ timeout: 60000 });
await page.locator(narrow).click();
await expect(page.locator(rows).first()).toBeVisible({ timeout: 60000 });
expect(searches.hits()).toHaveLength(1);
await expect(page.locator('[data-test="date-time-btn"]')).not.toContainText('24');
await pickRelative(page, 1, 'd');
await expect(page.locator(banner)).toBeVisible({ timeout: 15000 });
await expect(page.locator('[data-test="logs-search-bar-refresh-btn"][data-run-pending="true"]')).toBeVisible();
await expect(page.locator('[data-test="logs-search-result-pagination"]')).toHaveAttribute(
'data-locked',
'true',
);
await settlePageWork(page);
expect(searches.hits()).toHaveLength(1);
});
test('a consented 24 h wallboard keeps refreshing; a facet runs, a wider window re-blocks', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '24h');
await page.locator(runAnyway).click();
await expect(page.locator(rows).first()).toBeVisible({ timeout: 60000 });
await setInterval5s(page);
const before = searches.hits().length;
await expect
.poll(() => searches.hits().length - before, { timeout: 30000 })
.toBeGreaterThanOrEqual(3);
await expect(page.locator(banner)).toHaveCount(0);
await page.locator('[data-test="logs-search-bar-refresh-interval-btn"]').click();
await page.locator('[data-test="logs-search-off-refresh-interval"]').first().click();
await page.keyboard.press('Escape');
testLogger.info('A facet narrows the consented scope: it runs with no banner');
await expect(page.locator(runInFlight)).toHaveCount(0, { timeout: 30000 });
const beforeFacet = searches.hits().length;
await page.locator('[data-test="log-search-expand-level-field-btn"]').click();
const value = page.locator('[data-test="logs-search-subfield-add-level-error"]');
await value.waitFor({ timeout: 30000 });
await value.locator('[role="checkbox"], button, input').first().click();
await expect.poll(() => searches.hits().length, { timeout: 15000 }).toBe(beforeFacet + 1);
await expect(page.locator(banner)).toHaveCount(0);
testLogger.info('Widening to 2 days is a new scope: the banner shows');
await pickRelative(page, 2, 'd');
await expect(page.locator(banner)).toBeVisible({ timeout: 15000 });
await settlePageWork(page);
expect(searches.hits().length).toBe(beforeFacet + 1);
});
test('an interval on a never-run over-budget scope sends nothing', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '24h');
await expect(page.locator(guard)).toBeVisible({ timeout: 60000 });
await setInterval5s(page);
await runTimersExpectingNoRun(page, 7000);
expect(searches.all()).toHaveLength(0);
await expect(page.locator(guard)).toBeVisible();
});
test('Run anyway is withdrawn once the editor turns dirty', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '24h');
await expect(page.locator(runAnyway)).toBeVisible({ timeout: 60000 });
const editor = page.locator('[data-test="logs-search-bar-query-editor"] .monaco-editor').first();
await editor.click();
await page.keyboard.type("level='error'", { delay: 20 });
await expect(page.locator(runAnyway)).toHaveCount(0);
await expect(page.locator('[data-test="logs-search-bar-refresh-btn"][data-run-pending="true"]')).toBeVisible();
await runTimersExpectingNoRun(page, 2000);
expect(searches.all()).toHaveLength(0);
});
test('a widened window under auto-refresh is not refreshed until it is run', {
tag: ['@guard', '@logs'],
}, async ({ page }) => {
const searches = await open(page, '15m');
await expect(page.locator(rows).first()).toBeVisible({ timeout: 60000 });
const firstRun = searches.hits().length;
await setInterval5s(page);
await expect.poll(() => searches.hits().length, { timeout: 30000 }).toBeGreaterThan(firstRun);
await pickRelative(page, 1, 'd');
await expect(page.locator(banner)).toBeVisible({ timeout: 15000 });
const paused = searches.hits().length;
await runTimersExpectingNoRun(page, 12000);
expect(searches.hits()).toHaveLength(paused);
});
});