mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-05 08:09:22 +00:00
Merge remote-tracking branch 'origin/main' into feat/unified-notification-templates
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import { expect } from '@playwright/test';
|
||||
const testLogger = require('../../playwright-tests/utils/test-logger.js');
|
||||
|
||||
export class IngestionConfigPage {
|
||||
constructor(page) {
|
||||
@@ -77,6 +78,7 @@ export class IngestionConfigPage {
|
||||
this.rumContentText = this.legacyCodeBlockPre.first();
|
||||
// Shared by every link in the test so 429 waits stay well under the test timeout
|
||||
this.rateLimitRetryBudgetMs = 60000;
|
||||
this.networkErrorRetryBudgetMs = 60000;
|
||||
}
|
||||
|
||||
// ==================== Navigation ====================
|
||||
@@ -460,8 +462,7 @@ export class IngestionConfigPage {
|
||||
}
|
||||
|
||||
async headWithRateLimitRetry(href, maxRetries = 2) {
|
||||
// 5 second timeout to prevent hanging
|
||||
let response = await this.page.request.head(href, { timeout: 5000 });
|
||||
let response = await this.headWithNetworkErrorRetry(href);
|
||||
for (let attempt = 0; attempt < maxRetries && response.status() === 429; attempt++) {
|
||||
const retryAfter = Number(response.headers()['retry-after']);
|
||||
const waitMs = Number.isFinite(retryAfter) && retryAfter > 0 ? retryAfter * 1000 : 2000;
|
||||
@@ -478,4 +479,20 @@ export class IngestionConfigPage {
|
||||
}
|
||||
return response;
|
||||
}
|
||||
|
||||
async headWithNetworkErrorRetry(href, maxRetries = 2) {
|
||||
for (let attempt = 0; ; attempt++) {
|
||||
try {
|
||||
// 5 second timeout to prevent hanging
|
||||
return await this.page.request.head(href, { timeout: 5000 });
|
||||
} catch (error) {
|
||||
const retryCostMs = 1000 * (attempt + 1) + 5000;
|
||||
// A spent budget would push the whole test past its timeout, so the real error is reported instead
|
||||
if (attempt >= maxRetries || retryCostMs > this.networkErrorRetryBudgetMs) throw error;
|
||||
testLogger.warn(`[doc-links] HEAD failed (${error.message}), retry ${attempt + 1}/${maxRetries}: ${href}`);
|
||||
this.networkErrorRetryBudgetMs -= retryCostMs;
|
||||
await this.page.waitForTimeout(1000 * (attempt + 1));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,10 @@ import { expect } from '@playwright/test';
|
||||
import testLogger from '../../playwright-tests/utils/test-logger.js';
|
||||
const { getAuthHeaders, getOrgIdentifier } = require('../../playwright-tests/utils/cloud-auth.js');
|
||||
|
||||
// The search-job worker starts one queued job per 10 s tick, and parallel tests queue several jobs.
|
||||
const SEARCH_JOB_START_TIMEOUT_MS = 120000;
|
||||
const JOB_STATUS = { PENDING: 0, RUNNING: 1, FINISHED: 2 };
|
||||
|
||||
export class JobSchedulerPage {
|
||||
constructor(page) {
|
||||
this.page = page;
|
||||
@@ -96,6 +100,16 @@ export class JobSchedulerPage {
|
||||
return jobs.findIndex(job => job.trace_id === trace_id);
|
||||
}
|
||||
|
||||
async _getJobStatus(trace_id) {
|
||||
const orgId = getOrgIdentifier();
|
||||
const url = `${process.env["ZO_BASE_URL"]}/api/${orgId}/search_jobs?type=logs&search_type=UI&use_cache=true`;
|
||||
const response = await this.page.request.get(url, { headers: getAuthHeaders() });
|
||||
expect(response.status(), `GET search_jobs failed: ${await response.text()}`).toBe(200);
|
||||
const job = (await response.json()).find(j => j.trace_id === trace_id);
|
||||
expect(job, `Job with trace ID ${trace_id} not found in search_jobs`).toBeTruthy();
|
||||
return job.status;
|
||||
}
|
||||
|
||||
|
||||
async deleteJobSearch(trace_id) {
|
||||
const rowIndex = await this._getJobRowIndex(trace_id);
|
||||
@@ -165,33 +179,27 @@ async cancelJobSearch(trace_id) {
|
||||
}
|
||||
|
||||
async exploreJob(trace_id) {
|
||||
// The explore button is disabled while the job is pending (status=0) or failed (status=3).
|
||||
// Retry: each attempt re-fetches the list and checks whether the button is enabled.
|
||||
const maxRetries = 5;
|
||||
let clicked = false;
|
||||
// Explore is enabled only once the job has left pending (0) and was not cancelled (3).
|
||||
let status;
|
||||
await expect.poll(async () => {
|
||||
status = await this._getJobStatus(trace_id);
|
||||
return status;
|
||||
}, {
|
||||
message: `Search job ${trace_id} never left the pending queue`,
|
||||
timeout: SEARCH_JOB_START_TIMEOUT_MS,
|
||||
intervals: [2000, 5000],
|
||||
}).not.toBe(JOB_STATUS.PENDING);
|
||||
expect([JOB_STATUS.RUNNING, JOB_STATUS.FINISHED], `Search job ${trace_id} ended in status ${status}`).toContain(status);
|
||||
|
||||
for (let attempt = 0; attempt < maxRetries; attempt++) {
|
||||
const rowIndex = await this._getJobRowIndex(trace_id, 15000);
|
||||
if (rowIndex === -1) throw new Error(`Job with trace ID ${trace_id} not found in scheduler list`);
|
||||
const rowIndex = await this._getJobRowIndex(trace_id, 15000);
|
||||
if (rowIndex === -1) throw new Error(`Job with trace ID ${trace_id} not found in scheduler list`);
|
||||
|
||||
const row = this.page.locator(`[data-test="search-scheduler-table"] [data-test="o2-table-row-${rowIndex}"]`);
|
||||
await row.waitFor({ state: 'visible', timeout: 10000 });
|
||||
const row = this.page.locator(`[data-test="search-scheduler-table"] [data-test="o2-table-row-${rowIndex}"]`);
|
||||
await row.waitFor({ state: 'visible', timeout: 10000 });
|
||||
|
||||
const exploreButton = row.locator('[data-test="search-scheduler-explore-btn"]');
|
||||
const isEnabled = await exploreButton.isEnabled();
|
||||
if (isEnabled) {
|
||||
await exploreButton.click();
|
||||
clicked = true;
|
||||
break;
|
||||
}
|
||||
|
||||
testLogger.warn(`Explore button for trace ID ${trace_id} is not enabled (attempt ${attempt + 1}/${maxRetries}).`);
|
||||
if (attempt < maxRetries - 1) await this.page.waitForTimeout(5000);
|
||||
}
|
||||
|
||||
if (!clicked) {
|
||||
throw new Error(`Explore button for trace ID ${trace_id} remained disabled after ${maxRetries} attempts.`);
|
||||
}
|
||||
const exploreButton = row.locator('[data-test="search-scheduler-explore-btn"]');
|
||||
await expect(exploreButton, `Explore button for trace ID ${trace_id} is disabled`).toBeEnabled({ timeout: 10000 });
|
||||
await exploreButton.click();
|
||||
|
||||
await expect(
|
||||
this.page.locator('[data-test="o-toast-message"]').filter({ hasText: 'Search Job have been applied successfully' }).first()
|
||||
|
||||
@@ -4819,11 +4819,7 @@ export class LogsPage {
|
||||
// When the show-query toggle is clicked, it switches to results view
|
||||
// Check if we're in results view by looking for the logs table
|
||||
const logsTable = this.page.locator('[data-test="logs-search-result-logs-table"]');
|
||||
const isResultsVisible = await logsTable.isVisible();
|
||||
|
||||
if (!isResultsVisible) {
|
||||
throw new Error('Expected results view to be visible after toggling query editor off');
|
||||
}
|
||||
await expect(logsTable, 'Expected results view to be visible after toggling query editor off').toBeVisible({ timeout: 15000 });
|
||||
|
||||
testLogger.info('Results view is visible - query editor toggled off successfully');
|
||||
return true;
|
||||
|
||||
@@ -118,7 +118,8 @@ test.describe("Create Alert from Dashboard Panel testcases", () => {
|
||||
// is a span, not an <input>, so read the committed value via the helper.
|
||||
const alertNameValue = pm.alertsPage.getAlertNameValueLocator();
|
||||
await expect(alertNameValue).toBeVisible({ timeout: 30000 });
|
||||
expect((await alertNameValue.textContent()) ?? "").toContain("Alert_from_");
|
||||
// The temporary auto-name shows first until the prefill resetForm applies the panel name
|
||||
await expect(alertNameValue).toContainText("Alert_from_", { timeout: 30000 });
|
||||
testLogger.info("Landed on the alert form pre-filled from the panel");
|
||||
|
||||
// Navigate back to the dashboard folder and clean up.
|
||||
|
||||
@@ -107,6 +107,8 @@ async function openResults(page) {
|
||||
).toHaveCount(0, {
|
||||
timeout: 60000,
|
||||
});
|
||||
// Stale rows stay on screen during the re-run and the app ignores J/K until the hits settle.
|
||||
expect(await logs.getResultHitsCount()).toBe(50);
|
||||
await page.evaluate(
|
||||
() =>
|
||||
document.activeElement instanceof HTMLElement &&
|
||||
|
||||
@@ -2,11 +2,16 @@ const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures
|
||||
const testLogger = require('../utils/test-logger.js');
|
||||
const PageManager = require('../../pages/page-manager.js');
|
||||
const { ensureMetricsIngested } = require('../utils/shared-metrics-setup.js');
|
||||
const metricsIngestion = require('../utils/metrics-ingestion.js');
|
||||
const { verifyDataOnUIWithValues: verifyDataOnUI } = require('../utils/metrics-assertions.js');
|
||||
|
||||
test.describe("Metrics Aggregation and Grouping Tests", () => {
|
||||
test.beforeAll(async () => {
|
||||
await ensureMetricsIngested();
|
||||
// The random seed puts ~1 point per series, and rate() needs 2+ points in one series.
|
||||
const seeded = await metricsIngestion.ingestFixedRequestCountSeries();
|
||||
expect(seeded.success, `fixed request_count seed failed: ${seeded.error}`).toBe(true);
|
||||
expect(await metricsIngestion.waitForMetricQueryable('sum by (service) (rate(request_count[5m]))'), 'rate(request_count[5m]) never became queryable').toBe(true);
|
||||
});
|
||||
|
||||
/** Create fresh PageManager per test — avoids data races in parallel workers. */
|
||||
|
||||
@@ -328,7 +328,7 @@ test.describe("Ingestion Configuration Tests", () => {
|
||||
testLogger.info(` Path: ${item.path}`);
|
||||
for (const link of item.brokenLinks) {
|
||||
testLogger.info(` ✗ ${link.url}`);
|
||||
testLogger.info(` Status: ${link.status || link.error}`);
|
||||
testLogger.info(` Status: ${link.error ? `${link.status} (${link.error})` : link.status}`);
|
||||
}
|
||||
}
|
||||
testLogger.info('\n' + '='.repeat(80));
|
||||
|
||||
@@ -585,6 +585,49 @@ class MetricsIngestion {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Send several request_count points into one fixed series so rate() over it is never empty.
|
||||
*/
|
||||
async ingestFixedRequestCountSeries(points = 5, spacingMs = 15000) {
|
||||
const now = Date.now();
|
||||
const dataPoints = [];
|
||||
for (let i = 0; i < points; i++) {
|
||||
dataPoints.push({
|
||||
timeUnixNano: ((now - (points - 1 - i) * spacingMs) * 1000000).toString(),
|
||||
asDouble: 100 + i * 10,
|
||||
attributes: [
|
||||
{ key: "node", value: { stringValue: "server-01" } },
|
||||
{ key: "service", value: { stringValue: "api-gateway" } },
|
||||
{ key: "region", value: { stringValue: "us-east-1" } }
|
||||
]
|
||||
});
|
||||
}
|
||||
const metricsData = {
|
||||
resourceMetrics: [{
|
||||
resource: {
|
||||
attributes: [
|
||||
{ key: "service.name", value: { stringValue: "api-gateway" } },
|
||||
{ key: "service.version", value: { stringValue: "1.0.0" } },
|
||||
{ key: "environment", value: { stringValue: "production" } },
|
||||
{ key: "host.name", value: { stringValue: "server-01" } },
|
||||
{ key: "region", value: { stringValue: "us-east-1" } }
|
||||
]
|
||||
},
|
||||
scopeMetrics: [{
|
||||
scope: { name: "opentelemetry-instrumentation", version: "1.0.0" },
|
||||
metrics: [{ name: "request_count", unit: "count", gauge: { dataPoints } }]
|
||||
}]
|
||||
}]
|
||||
};
|
||||
const result = await this.sendMetrics(metricsData);
|
||||
if (result.success) {
|
||||
testLogger.info('Sent fixed request_count series', { points });
|
||||
} else {
|
||||
testLogger.error('Failed to send fixed request_count series', { error: result.error });
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Continuous metrics ingestion
|
||||
* Uses while loop with await to prevent overlapping executions
|
||||
|
||||
@@ -216,7 +216,7 @@ describe("Logs Index — opening a line link (4c C5)", { timeout: 30000 }, () =>
|
||||
} as any);
|
||||
gridRuns.length = 0;
|
||||
await land(linkQuery(), {}, { realExecutor: true });
|
||||
await vi.waitFor(() => expect(gridRuns.length).toBeGreaterThan(0));
|
||||
expect(gridRuns.length).toBeGreaterThan(0);
|
||||
expect(gridRuns[0][2]).toMatch(/^permalink-init:/);
|
||||
});
|
||||
|
||||
@@ -239,11 +239,7 @@ describe("Logs Index — opening a line link (4c C5)", { timeout: 30000 }, () =>
|
||||
await vi.waitFor(() =>
|
||||
expect(document.querySelector('[data-test="logs-permalink-detail-dialog"]')).not.toBeNull(),
|
||||
);
|
||||
await vi.waitFor(() =>
|
||||
expect(
|
||||
document.querySelector('[data-test="log-detail-json-content"]')?.textContent,
|
||||
).toContain("payment declined"),
|
||||
);
|
||||
await vi.waitFor(() => expect(sharedLineRecord.value).toEqual(line));
|
||||
logs.mockClear();
|
||||
wrapper.vm.onGuardRunAnyway();
|
||||
expect(logs).toHaveBeenCalledTimes(1);
|
||||
|
||||
Reference in New Issue
Block a user