mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-04 16:09:05 +00:00
362 lines
14 KiB
JavaScript
362 lines
14 KiB
JavaScript
const testLogger = require('./test-logger.js');
|
|
const logsdata = require("../../../test-data/logs_data.json");
|
|
const { getAuthHeaders, getOrgIdentifier } = require('./cloud-auth.js');
|
|
const { ingestBase } = require('./ingest-url.js');
|
|
|
|
// Rows are stamped at ingest, so a 5-min refresh keeps them inside every 15-min test window.
|
|
const SHARED_STREAM = 'e2e_automate';
|
|
const SHARED_STREAM_REFRESH_MS = 5 * 60 * 1000;
|
|
const lastSharedIngest = new Map();
|
|
|
|
/**
|
|
* Ingest test data into a stream via API using Node.js context (secure)
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} streamName - Name of the stream to ingest data into (default: "e2e_automate")
|
|
* @param {{ force?: boolean }} [options] - `force` posts even if the shared stream was refreshed recently (use after a stream settings change)
|
|
* @returns {Promise<object>} - API response, or `{ skipped: true }` when this worker refreshed the shared stream within the last 5 min
|
|
*/
|
|
async function ingestTestData(page, streamName = "e2e_automate", { force = false } = {}) {
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
const baseUrl = process.env.INGESTION_URL.endsWith('/')
|
|
? process.env.INGESTION_URL.slice(0, -1)
|
|
: process.env.INGESTION_URL;
|
|
|
|
const sharedKey = `${orgId}:${streamName}`;
|
|
const lastIngest = streamName === SHARED_STREAM ? lastSharedIngest.get(sharedKey) : undefined;
|
|
if (!force && lastIngest && Date.now() - lastIngest < SHARED_STREAM_REFRESH_MS) {
|
|
testLogger.info('Test data ingestion skipped — this worker refreshed the shared stream recently', { streamName, ageMs: Date.now() - lastIngest });
|
|
return { skipped: true };
|
|
}
|
|
|
|
try {
|
|
const response = await page.request.post(`${ingestBase(baseUrl)}/api/${orgId}/${streamName}/_json`, {
|
|
headers: headers,
|
|
data: logsdata
|
|
});
|
|
if (response.ok() && streamName === SHARED_STREAM) lastSharedIngest.set(sharedKey, Date.now());
|
|
|
|
const responseData = await response.json().catch(() => ({ error: 'Failed to parse JSON' }));
|
|
testLogger.debug('Test data ingestion response', { response: responseData, streamName });
|
|
return responseData;
|
|
} catch (e) {
|
|
testLogger.debug('Test data ingestion error', { error: e.message, streamName });
|
|
return { error: e.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generates authentication headers for API requests.
|
|
* Delegates to cloud-auth.js which handles both cloud (cookie-based) and
|
|
* self-hosted (Basic Auth) environments.
|
|
*
|
|
* @returns {Object} Headers object with appropriate auth for the environment
|
|
*/
|
|
function getHeaders() {
|
|
return getAuthHeaders();
|
|
}
|
|
|
|
/**
|
|
* Constructs the ingestion URL for a specific stream.
|
|
* @param {string} orgId - The organization ID
|
|
* @param {string} streamName - The name of the stream to ingest data into
|
|
* @returns {string} The complete ingestion URL
|
|
* @example
|
|
* const url = getIngestionUrl("default", "my_stream");
|
|
* // Returns: "http://localhost:5080/api/default/my_stream/_json"
|
|
*/
|
|
function getIngestionUrl(orgId, streamName) {
|
|
return `${ingestBase(process.env.INGESTION_URL)}/api/${orgId}/${streamName}/_json`;
|
|
}
|
|
|
|
/**
|
|
* Sends an ingestion request using page.request API (secure, keeps credentials in Node.js context).
|
|
* @param {Page} page - Playwright page object
|
|
* @param {string} url - The ingestion URL
|
|
* @param {Object} payload - The data payload to ingest
|
|
* @param {Object} headers - Request headers including authentication
|
|
* @returns {Promise<Object>} The API response
|
|
* @example
|
|
* const response = await sendRequest(page, url, { log: "test" }, headers);
|
|
* // Returns: { code: 200, status: [{ name: "stream", successful: 1 }] }
|
|
*/
|
|
async function sendRequest(page, url, payload, headers) {
|
|
try {
|
|
const response = await page.request.post(url, {
|
|
headers: headers,
|
|
data: payload
|
|
});
|
|
return await response.json().catch(() => ({ error: 'Failed to parse JSON' }));
|
|
} catch (e) {
|
|
testLogger.debug('sendRequest error', { error: e.message, url });
|
|
return { error: e.message };
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Ingest custom data into a stream via API using Node.js context (secure)
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} streamName - Name of the stream to ingest data into
|
|
* @param {Array} data - Array of log records to ingest
|
|
* @returns {Promise<object>} - API response with status and data
|
|
*/
|
|
async function ingestCustomData(page, streamName, data) {
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
const baseUrl = process.env.INGESTION_URL.endsWith('/')
|
|
? process.env.INGESTION_URL.slice(0, -1)
|
|
: process.env.INGESTION_URL;
|
|
|
|
try {
|
|
// Use Playwright's request API to keep credentials in Node.js context
|
|
const response = await page.request.post(`${ingestBase(baseUrl)}/api/${orgId}/${streamName}/_json`, {
|
|
headers: headers,
|
|
data: data
|
|
});
|
|
|
|
let responseData = null;
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (e) {
|
|
responseData = { error: 'Failed to parse JSON response' };
|
|
}
|
|
|
|
testLogger.debug('Custom data ingestion response', { status: response.status(), streamName });
|
|
return {
|
|
status: response.status(),
|
|
data: responseData
|
|
};
|
|
} catch (e) {
|
|
testLogger.debug('Custom data ingestion error', { error: e.message, streamName });
|
|
return {
|
|
status: 500,
|
|
data: { error: e.message }
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Enable log patterns extraction on a stream via API using Node.js context (secure)
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} streamName - Name of the stream
|
|
* @returns {Promise<object>} - API response with status and data
|
|
*/
|
|
async function enableLogPatternsExtraction(page, streamName) {
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
const baseUrl = process.env.INGESTION_URL.endsWith('/')
|
|
? process.env.INGESTION_URL.slice(0, -1)
|
|
: process.env.INGESTION_URL;
|
|
|
|
const settingsPayload = {
|
|
enable_log_patterns_extraction: true
|
|
};
|
|
|
|
try {
|
|
// Use Playwright's request API to keep credentials in Node.js context
|
|
const response = await page.request.put(`${baseUrl}/api/${orgId}/streams/${streamName}/settings?type=logs`, {
|
|
headers: headers,
|
|
data: settingsPayload
|
|
});
|
|
|
|
let responseData = null;
|
|
try {
|
|
responseData = await response.json();
|
|
} catch (e) {
|
|
responseData = { error: 'Failed to parse JSON response' };
|
|
}
|
|
|
|
testLogger.debug('Enable log patterns extraction response', { status: response.status(), streamName, data: responseData });
|
|
return {
|
|
status: response.status(),
|
|
data: responseData
|
|
};
|
|
} catch (e) {
|
|
testLogger.debug('Enable log patterns extraction error', { error: e.message, streamName });
|
|
return {
|
|
status: 500,
|
|
data: { error: e.message }
|
|
};
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for stream data to be indexed by polling the search API
|
|
* Replaces arbitrary waitForTimeout with explicit condition check
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} streamName - Name of the stream to check
|
|
* @param {number} expectedMinCount - Minimum expected record count (default: 1)
|
|
* @param {number} maxWaitMs - Maximum wait time in ms (default: 30000)
|
|
* @param {number} pollIntervalMs - Polling interval in ms (default: 2000)
|
|
* @returns {Promise<boolean>} - True if data found, false if timed out
|
|
*/
|
|
async function waitForStreamData(page, streamName, expectedMinCount = 1, maxWaitMs = 30000, pollIntervalMs = 2000) {
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
const baseUrl = process.env.INGESTION_URL.endsWith('/')
|
|
? process.env.INGESTION_URL.slice(0, -1)
|
|
: process.env.INGESTION_URL;
|
|
|
|
const startTime = Date.now();
|
|
const endTime = Date.now() * 1000; // microseconds
|
|
const startTimeUs = (Date.now() - 3600000) * 1000; // 1 hour ago in microseconds
|
|
|
|
const searchPayload = {
|
|
query: {
|
|
sql: `SELECT COUNT(*) as count FROM "${streamName}"`,
|
|
start_time: startTimeUs,
|
|
end_time: endTime,
|
|
from: 0,
|
|
size: 1
|
|
}
|
|
};
|
|
|
|
while (Date.now() - startTime < maxWaitMs) {
|
|
try {
|
|
const response = await page.request.post(`${baseUrl}/api/${orgId}/_search?type=logs`, {
|
|
headers: headers,
|
|
data: searchPayload
|
|
});
|
|
|
|
if (response.status() === 200) {
|
|
const data = await response.json().catch(() => null);
|
|
const count = data?.hits?.[0]?.count || data?.total || 0;
|
|
|
|
if (count >= expectedMinCount) {
|
|
testLogger.debug('Stream data indexed', { streamName, count, waitedMs: Date.now() - startTime });
|
|
return true;
|
|
}
|
|
testLogger.debug('Polling for stream data', { streamName, currentCount: count, expectedMinCount });
|
|
}
|
|
} catch (e) {
|
|
testLogger.debug('Stream data poll error', { error: e.message, streamName });
|
|
}
|
|
|
|
// Wait before next poll
|
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
}
|
|
|
|
testLogger.warn('Stream data poll timed out', { streamName, maxWaitMs });
|
|
return false;
|
|
}
|
|
|
|
/**
|
|
* Polls the search API until a specific field value is searchable in a stream.
|
|
* Use this as a readiness gate after ingesting a unique value into a SHARED stream
|
|
* (e.g. e2e_automate) before driving the UI: freshly-ingested data is not queryable
|
|
* immediately (WAL -> index lag), so the field-values panel can return stale/other
|
|
* values and miss the just-ingested one. Waiting on the actual value (not just a row
|
|
* count) guarantees the value is present before the UI assertion runs.
|
|
* @param {import('@playwright/test').Page} page - Playwright page object
|
|
* @param {string} streamName - Name of the stream to check
|
|
* @param {string} fieldName - Field to filter on (e.g. "service_name")
|
|
* @param {string} fieldValue - Exact value that must be searchable
|
|
* @param {number} maxWaitMs - Maximum wait time in ms (default: 30000)
|
|
* @param {number} pollIntervalMs - Polling interval in ms (default: 2000)
|
|
* @returns {Promise<boolean>} - True if the value became searchable, false if timed out
|
|
*/
|
|
async function waitForFieldValueSearchable(page, streamName, fieldName, fieldValue, maxWaitMs = 30000, pollIntervalMs = 2000) {
|
|
// fieldName is interpolated unescaped into the SQL identifier position, so restrict it
|
|
// to a plain column identifier (callers pass hardcoded names like "service_name").
|
|
if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(fieldName)) {
|
|
throw new Error(`waitForFieldValueSearchable: invalid fieldName "${fieldName}" (expected a column identifier)`);
|
|
}
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
const baseUrl = process.env.INGESTION_URL.endsWith('/')
|
|
? process.env.INGESTION_URL.slice(0, -1)
|
|
: process.env.INGESTION_URL;
|
|
|
|
const startTime = Date.now();
|
|
// Escape single quotes to keep the SQL literal well-formed.
|
|
const safeValue = String(fieldValue).replace(/'/g, "''");
|
|
const searchPayload = {
|
|
query: {
|
|
sql: `SELECT COUNT(*) as count FROM "${streamName}" WHERE ${fieldName} = '${safeValue}'`,
|
|
start_time: (Date.now() - 3600000) * 1000, // 1 hour ago in microseconds
|
|
end_time: Date.now() * 1000,
|
|
from: 0,
|
|
size: 1
|
|
}
|
|
};
|
|
|
|
while (Date.now() - startTime < maxWaitMs) {
|
|
try {
|
|
// end_time must advance each poll so newly-ingested records fall inside the window.
|
|
searchPayload.query.end_time = Date.now() * 1000;
|
|
const response = await page.request.post(`${baseUrl}/api/${orgId}/_search?type=logs`, {
|
|
headers: headers,
|
|
data: searchPayload
|
|
});
|
|
|
|
if (response.status() === 200) {
|
|
const data = await response.json().catch(() => null);
|
|
const count = data?.hits?.[0]?.count || 0;
|
|
if (count >= 1) {
|
|
testLogger.debug('Field value searchable', { streamName, fieldName, fieldValue, count, waitedMs: Date.now() - startTime });
|
|
return true;
|
|
}
|
|
testLogger.debug('Polling for field value', { streamName, fieldName, fieldValue });
|
|
} else {
|
|
// Surface non-200s (400/401/403/5xx) so a persistent failure is visible in logs
|
|
// rather than silently burning the full timeout as "not found yet".
|
|
testLogger.warn('Field value poll got non-200', { status: response.status(), streamName, fieldName });
|
|
}
|
|
} catch (e) {
|
|
testLogger.warn('Field value poll error', { error: e.message, streamName, fieldName });
|
|
}
|
|
|
|
await new Promise(resolve => setTimeout(resolve, pollIntervalMs));
|
|
}
|
|
|
|
testLogger.warn('Field value poll timed out', { streamName, fieldName, fieldValue, maxWaitMs });
|
|
return false;
|
|
}
|
|
|
|
/** Polls /streams until the stream is enumerated — distinct from waitForStreamData (which polls _search), because a new stream is queryable before it is listed and every stream PICKER is built from this list, not from search. */
|
|
async function waitForStreamListed(page, streamName, streamType = 'logs', maxWaitMs = 90000, pollIntervalMs = 2000) {
|
|
const orgId = getOrgIdentifier();
|
|
const headers = getHeaders();
|
|
// INGESTION_URL first, then ZO_BASE_URL: on cloud the two hosts differ and only the former feeds the picker's list.
|
|
const rawBase = process.env.INGESTION_URL || process.env.ZO_BASE_URL || '';
|
|
const baseUrl = rawBase.endsWith('/') ? rawBase.slice(0, -1) : rawBase;
|
|
|
|
const startTime = Date.now();
|
|
while (Date.now() - startTime < maxWaitMs) {
|
|
try {
|
|
const response = await page.request.get(
|
|
`${baseUrl}/api/${orgId}/streams?type=${encodeURIComponent(streamType)}`,
|
|
{ headers }
|
|
);
|
|
if (response.status() === 200) {
|
|
const data = await response.json().catch(() => null);
|
|
const list = Array.isArray(data?.list) ? data.list : [];
|
|
if (list.some((s) => s?.name === streamName)) {
|
|
testLogger.debug('Stream listed', { streamName, waitedMs: Date.now() - startTime });
|
|
return true;
|
|
}
|
|
} else {
|
|
testLogger.warn('Stream list poll got non-200', { status: response.status(), streamName });
|
|
}
|
|
} catch (e) {
|
|
testLogger.warn('Stream list poll error', { error: e.message, streamName });
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, pollIntervalMs));
|
|
}
|
|
|
|
testLogger.warn('Stream list poll timed out', { streamName, streamType, maxWaitMs });
|
|
return false;
|
|
}
|
|
|
|
module.exports = {
|
|
ingestTestData,
|
|
getHeaders,
|
|
getIngestionUrl,
|
|
sendRequest,
|
|
ingestCustomData,
|
|
enableLogPatternsExtraction,
|
|
waitForStreamData,
|
|
waitForFieldValueSearchable,
|
|
waitForStreamListed
|
|
};
|