mirror of
https://github.com/openobserve/openobserve.git
synced 2026-11-06 00:09:22 +00:00
Co-authored-by: Yashodhan Joshi <yashodhan@openobserve.ai>
454 lines
20 KiB
JavaScript
454 lines
20 KiB
JavaScript
const { test, expect, navigateToBase } = require('../utils/enhanced-baseFixtures.js');
|
|
const testLogger = require('../utils/test-logger.js');
|
|
const PageManager = require('../../pages/page-manager.js');
|
|
const path = require('path');
|
|
|
|
// Helper function to ingest a SINGLE log entry
|
|
async function ingestSingleLog(page, streamName, fieldName, fieldValue) {
|
|
const orgId = process.env["ORGNAME"];
|
|
const basicAuthCredentials = Buffer.from(
|
|
`${process.env["ZO_ROOT_USER_EMAIL"]}:${process.env["ZO_ROOT_USER_PASSWORD"]}`
|
|
).toString('base64');
|
|
|
|
const headers = {
|
|
"Authorization": `Basic ${basicAuthCredentials}`,
|
|
"Content-Type": "application/json",
|
|
};
|
|
|
|
const logEntry = {
|
|
level: "info",
|
|
[fieldName]: fieldValue,
|
|
log: `Test log with ${fieldName} = ${fieldValue}`,
|
|
_timestamp: Date.now() * 1000
|
|
};
|
|
|
|
testLogger.info(`Ingesting single log with ${fieldName}: ${fieldValue}`);
|
|
|
|
const response = await page.evaluate(async ({ url, headers, orgId, streamName, logData }) => {
|
|
const fetchResponse = await fetch(`${url}/api/${orgId}/${streamName}/_json`, {
|
|
method: 'POST',
|
|
headers: headers,
|
|
body: JSON.stringify([logData])
|
|
});
|
|
const responseJson = await fetchResponse.json();
|
|
return {
|
|
status: fetchResponse.status,
|
|
statusText: fetchResponse.statusText,
|
|
body: responseJson
|
|
};
|
|
}, {
|
|
url: process.env.INGESTION_URL,
|
|
headers: headers,
|
|
orgId: orgId,
|
|
streamName: streamName,
|
|
logData: logEntry
|
|
});
|
|
|
|
testLogger.info(`Ingestion API response - Status: ${response.status}, Body:`, response.body);
|
|
|
|
if (response.status !== 200) {
|
|
testLogger.error(`Ingestion failed! Status: ${response.status}, Response:`, response.body);
|
|
}
|
|
|
|
await page.waitForTimeout(2000);
|
|
}
|
|
|
|
async function closeStreamDetailSidebar(page) {
|
|
const cancelButton = page.getByRole('button', { name: 'Cancel' });
|
|
const cancelVisible = await cancelButton.isVisible({ timeout: 1000 }).catch(() => false);
|
|
if (cancelVisible) {
|
|
await cancelButton.click();
|
|
testLogger.info('Closed stream detail sidebar');
|
|
await page.waitForTimeout(500);
|
|
}
|
|
}
|
|
|
|
async function navigateToLogsQuick(page) {
|
|
await page.locator('[data-test="menu-link-\\/logs-item"]').click();
|
|
await page.waitForLoadState('networkidle');
|
|
testLogger.info('Navigated to Logs (fast - no VRL wait)');
|
|
}
|
|
|
|
// Verify a single field in the most recent log entry
|
|
async function verifySingleFieldInLatestLog(page, pm, streamName, fieldName, shouldBeDropped, shouldBeRedacted) {
|
|
testLogger.info(`Verifying field: ${fieldName}, shouldBeDropped: ${shouldBeDropped}, shouldBeRedacted: ${shouldBeRedacted}`);
|
|
|
|
await closeStreamDetailSidebar(page);
|
|
await navigateToLogsQuick(page);
|
|
await pm.logsPage.selectStream(streamName);
|
|
await page.waitForTimeout(1000);
|
|
await pm.logsPage.clickRefreshButton();
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Get log entries
|
|
let logTableCell = page.locator('[data-test="log-table-column-0-source"]');
|
|
let logCount = await logTableCell.count();
|
|
|
|
if (logCount === 0) {
|
|
logTableCell = page.locator('tbody tr');
|
|
logCount = await logTableCell.count();
|
|
}
|
|
|
|
testLogger.info(`Found ${logCount} log entries in the UI`);
|
|
|
|
if (logCount === 0) {
|
|
throw new Error('No logs found in the stream');
|
|
}
|
|
|
|
// Get the first log entry (most recent)
|
|
const logText = await logTableCell.nth(0).textContent();
|
|
testLogger.info(`Latest log text (first 300 chars): ${logText.substring(0, 300)}...`);
|
|
|
|
// Check if field exists in the log
|
|
const fieldAsJsonKey = `"${fieldName}":`;
|
|
const fieldAsKey = `${fieldName}:`;
|
|
const fieldFound = logText.includes(fieldAsJsonKey) || logText.includes(fieldAsKey);
|
|
|
|
if (shouldBeDropped) {
|
|
// Field should NOT be present at all
|
|
if (fieldFound) {
|
|
testLogger.error(`Field ${fieldName} should have been DROPPED but was found in log!`);
|
|
throw new Error(`Field ${fieldName} was not dropped - still present in logs`);
|
|
}
|
|
testLogger.info(`✓ Field ${fieldName} is correctly DROPPED (not present)`);
|
|
} else if (shouldBeRedacted) {
|
|
// Field should be present with [REDACTED]
|
|
if (!fieldFound) {
|
|
testLogger.error(`Field ${fieldName} should be present but was not found in log!`);
|
|
throw new Error(`Field ${fieldName} not found in log`);
|
|
}
|
|
if (!logText.includes('[REDACTED]')) {
|
|
testLogger.error(`Field ${fieldName} should be REDACTED but [REDACTED] marker not found!`);
|
|
throw new Error(`Field ${fieldName} was not redacted`);
|
|
}
|
|
testLogger.info(`✓ Field ${fieldName} is correctly REDACTED`);
|
|
} else {
|
|
// Field should be visible with actual value
|
|
if (!fieldFound) {
|
|
testLogger.error(`Field ${fieldName} should be visible but was not found in log!`);
|
|
throw new Error(`Field ${fieldName} not found in log`);
|
|
}
|
|
if (logText.includes('[REDACTED]')) {
|
|
testLogger.error(`Field ${fieldName} should be visible but is REDACTED!`);
|
|
throw new Error(`Field ${fieldName} is unexpectedly redacted`);
|
|
}
|
|
testLogger.info(`✓ Field ${fieldName} is visible with actual value (as expected)`);
|
|
}
|
|
}
|
|
|
|
test.describe("Import POC - Multiple Patterns on One Field", { tag: '@enterprise' }, () => {
|
|
test.describe.configure({ mode: 'serial' });
|
|
let pm;
|
|
let cleanupPerformedInSetup = false;
|
|
|
|
// All 6 patterns from import file (for cleanup purposes)
|
|
const allImportedPatterns = [
|
|
'ifsc_code',
|
|
'date_dd_mm_yyyy',
|
|
'log_filename',
|
|
'time_hh_mm_ss',
|
|
'log_level_keyword',
|
|
'hex_number'
|
|
];
|
|
|
|
// Query-time patterns (2) - tested first, no re-ingestion needed
|
|
const queryTimePatterns = [
|
|
{ name: 'log_filename', value: 'application.log', action: 'drop', timeType: 'query', scenario: 'query_drop' },
|
|
{ name: 'time_hh_mm_ss', value: '14:30:45', action: 'redact', timeType: 'query', scenario: 'query_redact' }
|
|
];
|
|
|
|
// Ingestion-time patterns (2) - tested second, requires re-ingestion
|
|
const ingestionTimePatterns = [
|
|
{ name: 'ifsc_code', value: 'AB12CDEF1234567890', action: 'drop', timeType: 'ingestion', scenario: 'ingestion_drop' },
|
|
{ name: 'date_dd_mm_yyyy', value: '25/12/2024', action: 'redact', timeType: 'ingestion', scenario: 'ingestion_redact' }
|
|
];
|
|
|
|
const fieldName = "multi_data"; // Single field for all patterns
|
|
const testStreamName = "sdr_poc_multi_pattern_test";
|
|
const importFilePath = path.resolve(__dirname, '../../../test-data/regex_patterns_import.json');
|
|
|
|
test.beforeEach(async ({ page }, testInfo) => {
|
|
testLogger.testStart(testInfo.title, testInfo.file);
|
|
await navigateToBase(page);
|
|
pm = new PageManager(page);
|
|
await page.waitForLoadState('networkidle');
|
|
testLogger.info('Import POC test setup completed');
|
|
});
|
|
|
|
// TEST 1: Setup cleanup
|
|
test('setup: cleanup imported patterns', {
|
|
tag: ['@sdr', '@cleanup', '@import', '@poc']
|
|
}, async ({ page }) => {
|
|
testLogger.info('=== SETUP CLEANUP: Cleaning up ALL 6 imported patterns ===');
|
|
|
|
// Our 4 test patterns that are linked to the same field
|
|
const testPatterns = [...queryTimePatterns.map(p => p.name), ...ingestionTimePatterns.map(p => p.name)];
|
|
let needsUnlinking = false;
|
|
|
|
// STEP 1: Try to delete all patterns first
|
|
testLogger.info('STEP 1: Attempting to delete all patterns');
|
|
for (const patternName of allImportedPatterns) {
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
|
|
|
|
if (exists) {
|
|
cleanupPerformedInSetup = true;
|
|
testLogger.info(`Pattern ${patternName} exists, attempting delete...`);
|
|
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
|
|
if (deleteResult.success) {
|
|
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
|
|
} else if (deleteResult.reason === 'in_use') {
|
|
testLogger.info(`Pattern ${patternName} is in use, will need to unlink`);
|
|
needsUnlinking = true;
|
|
// Don't try to unlink yet, just mark that we need to
|
|
}
|
|
} else {
|
|
testLogger.info(`✓ Pattern ${patternName} does not exist`);
|
|
}
|
|
}
|
|
|
|
// STEP 2: If any patterns are in use, unlink all test patterns at once
|
|
if (needsUnlinking) {
|
|
testLogger.info('STEP 2: Some patterns in use, unlinking our 4 test patterns from field');
|
|
await pm.streamAssociationPage.unlinkAllPatternsFromField(
|
|
testStreamName,
|
|
fieldName,
|
|
testPatterns
|
|
);
|
|
testLogger.info('✓ Unlinking complete');
|
|
|
|
// STEP 3: Now delete the patterns that were in use
|
|
testLogger.info('STEP 3: Deleting patterns after unlinking');
|
|
for (const patternName of allImportedPatterns) {
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
|
|
|
|
if (exists) {
|
|
testLogger.info(`Deleting pattern: ${patternName}`);
|
|
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
|
|
if (deleteResult.success) {
|
|
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
|
|
} else if (deleteResult.reason === 'in_use') {
|
|
// Handle non-test patterns that might still be linked elsewhere
|
|
testLogger.warn(`Pattern ${patternName} still in use (non-test pattern), unlinking individually`);
|
|
for (const association of deleteResult.associations) {
|
|
await pm.streamAssociationPage.unlinkPatternFromField(
|
|
association.streamName,
|
|
association.fieldName,
|
|
patternName
|
|
);
|
|
}
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const retryDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
expect(retryDelete.success).toBeTruthy();
|
|
testLogger.info(`✓ Pattern ${patternName} deleted after unlinking`);
|
|
} else {
|
|
throw new Error(`Failed to delete pattern ${patternName}. Reason: ${deleteResult.reason}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
testLogger.info(`=== SETUP CLEANUP COMPLETE (Cleanup performed: ${cleanupPerformedInSetup}) ===`);
|
|
});
|
|
|
|
// TEST 2: Import patterns and verify
|
|
test('should import 4 patterns and verify they exist', {
|
|
tag: ['@sdr', '@import', '@poc']
|
|
}, async ({ page }, testInfo) => {
|
|
testInfo.annotations.push({ type: 'dependency', description: 'setup: cleanup imported patterns' });
|
|
testLogger.info('=== Testing pattern import functionality ===');
|
|
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
|
|
// Import patterns
|
|
testLogger.info('Importing patterns from file');
|
|
const importSuccess = await pm.sdrPatternsPage.importPatternsFromFile(importFilePath);
|
|
expect(importSuccess).toBeTruthy();
|
|
|
|
// Verify each imported pattern exists by searching for it
|
|
testLogger.info('Verifying all 4 patterns exist by searching for them');
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
await page.waitForTimeout(1000);
|
|
|
|
// Combine both pattern arrays to verify all 4
|
|
const allPatternsToVerify = [...queryTimePatterns, ...ingestionTimePatterns];
|
|
|
|
for (const pattern of allPatternsToVerify) {
|
|
testLogger.info(`Searching for pattern: ${pattern.name}`);
|
|
const exists = await pm.sdrPatternsPage.verifyPatternExistsInList(pattern.name);
|
|
expect(exists).toBeTruthy();
|
|
testLogger.info(`✓ Pattern ${pattern.name} found in list`);
|
|
}
|
|
|
|
testLogger.info('=== ✓ IMPORT TEST COMPLETED SUCCESSFULLY ===');
|
|
});
|
|
|
|
// TEST 3: Multiple patterns on ONE field (SEQUENTIAL FLOW)
|
|
test('should link 4 patterns to one field and verify with sequential ingestion', {
|
|
tag: ['@sdr', '@import', '@poc', '@multiPattern']
|
|
}, async ({ page }, testInfo) => {
|
|
testInfo.annotations.push({ type: 'dependency', description: 'should import 4 patterns and verify they exist' });
|
|
testLogger.info('=== Testing multiple patterns on ONE field - SEQUENTIAL FLOW ===');
|
|
testLogger.info(`Field: ${fieldName}`);
|
|
testLogger.info('Strategy: Ingest one log → verify → ingest next → verify (repeating pattern)');
|
|
|
|
// ==================== STEP 1: Ingest log #1 for query-time drop pattern ====================
|
|
testLogger.info('========== STEP 1: Ingest log with value "application.log" ==========');
|
|
await ingestSingleLog(page, testStreamName, fieldName, queryTimePatterns[0].value); // "application.log"
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, false);
|
|
testLogger.info('✓ STEP 1 PASSED: Log #1 ingested, field visible (no patterns yet)');
|
|
|
|
// ==================== STEP 2: Link query-time DROP pattern ====================
|
|
testLogger.info('========== STEP 2: Link query-time DROP pattern (log_filename) ==========');
|
|
await pm.streamAssociationPage.associatePatternWithStream(
|
|
testStreamName,
|
|
queryTimePatterns[0].name, // 'log_filename'
|
|
queryTimePatterns[0].action, // 'drop'
|
|
queryTimePatterns[0].timeType, // 'query'
|
|
fieldName
|
|
);
|
|
testLogger.info('✓ STEP 2 PASSED: Query-time DROP pattern linked');
|
|
|
|
// ==================== STEP 3: Verify query-time DROP on existing log ====================
|
|
testLogger.info('========== STEP 3: Verify query-time DROP works on EXISTING log ==========');
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, true, false);
|
|
testLogger.info('✓ STEP 3 PASSED: Field DROPPED at query time (no re-ingestion needed)');
|
|
|
|
// ==================== STEP 4: Ingest log #2 for query-time redact pattern ====================
|
|
testLogger.info('========== STEP 4: Ingest log with value "14:30:45" ==========');
|
|
await ingestSingleLog(page, testStreamName, fieldName, queryTimePatterns[1].value); // "14:30:45"
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, false);
|
|
testLogger.info('✓ STEP 4 PASSED: Log #2 ingested, field visible (pattern not yet linked)');
|
|
|
|
// ==================== STEP 5: Link query-time REDACT pattern ====================
|
|
testLogger.info('========== STEP 5: Link query-time REDACT pattern (time_hh_mm_ss) ==========');
|
|
await pm.streamAssociationPage.addAdditionalPatternToField(
|
|
testStreamName,
|
|
fieldName,
|
|
queryTimePatterns[1].name, // 'time_hh_mm_ss'
|
|
queryTimePatterns[1].action, // 'redact'
|
|
queryTimePatterns[1].timeType // 'query'
|
|
);
|
|
testLogger.info('✓ STEP 5 PASSED: Query-time REDACT pattern linked (now 2 patterns total)');
|
|
|
|
// ==================== STEP 6: Verify query-time REDACT on existing log ====================
|
|
testLogger.info('========== STEP 6: Verify query-time REDACT works on EXISTING log ==========');
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, true);
|
|
testLogger.info('✓ STEP 6 PASSED: Field REDACTED at query time (no re-ingestion needed)');
|
|
|
|
// ==================== STEP 7: Link ingestion-time DROP pattern ====================
|
|
testLogger.info('========== STEP 7: Link ingestion-time DROP pattern (ifsc_code) ==========');
|
|
await pm.streamAssociationPage.addAdditionalPatternToField(
|
|
testStreamName,
|
|
fieldName,
|
|
ingestionTimePatterns[0].name, // 'ifsc_code'
|
|
ingestionTimePatterns[0].action, // 'drop'
|
|
ingestionTimePatterns[0].timeType // 'ingestion'
|
|
);
|
|
testLogger.info('✓ STEP 7 PASSED: Ingestion-time DROP pattern linked (now 3 patterns total)');
|
|
|
|
// ==================== STEP 8: Ingest log #3 and verify ingestion-time DROP ====================
|
|
testLogger.info('========== STEP 8: Ingest log with value "AB12CDEF1234567890" ==========');
|
|
await ingestSingleLog(page, testStreamName, fieldName, ingestionTimePatterns[0].value); // IFSC code
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, true, false);
|
|
testLogger.info('✓ STEP 8 PASSED: Field DROPPED at ingestion time');
|
|
|
|
// ==================== STEP 9: Link ingestion-time REDACT pattern ====================
|
|
testLogger.info('========== STEP 9: Link ingestion-time REDACT pattern (date_dd_mm_yyyy) ==========');
|
|
await pm.streamAssociationPage.addAdditionalPatternToField(
|
|
testStreamName,
|
|
fieldName,
|
|
ingestionTimePatterns[1].name, // 'date_dd_mm_yyyy'
|
|
ingestionTimePatterns[1].action, // 'redact'
|
|
ingestionTimePatterns[1].timeType // 'ingestion'
|
|
);
|
|
testLogger.info('✓ STEP 9 PASSED: Ingestion-time REDACT pattern linked (now 4 patterns total on ONE field)');
|
|
|
|
// ==================== STEP 10: Ingest log #4 and verify ingestion-time REDACT ====================
|
|
testLogger.info('========== STEP 10: Ingest log with value "25/12/2024" ==========');
|
|
await ingestSingleLog(page, testStreamName, fieldName, ingestionTimePatterns[1].value); // Date
|
|
await verifySingleFieldInLatestLog(page, pm, testStreamName, fieldName, false, true);
|
|
testLogger.info('✓ STEP 10 PASSED: Field REDACTED at ingestion time');
|
|
|
|
testLogger.info('=== ✓ ALL 4 PATTERNS ON ONE FIELD TEST COMPLETED SUCCESSFULLY ===');
|
|
testLogger.info(' - Field: multi_data has 4 patterns');
|
|
testLogger.info(' - Query-time: 1 drop + 1 redact (work on existing data)');
|
|
testLogger.info(' - Ingestion-time: 1 drop + 1 redact (require re-ingestion)');
|
|
testLogger.info(' - Each pattern tested independently with sequential ingestion');
|
|
});
|
|
|
|
// TEST 4: End cleanup
|
|
test('cleanup: unlink and delete patterns', {
|
|
tag: ['@sdr', '@cleanup', '@import', '@poc']
|
|
}, async ({ page }, testInfo) => {
|
|
testInfo.annotations.push({ type: 'dependency', description: 'should link 4 patterns to one field and verify with sequential ingestion' });
|
|
testLogger.info('=== END CLEANUP: Unlinking and deleting ALL 6 patterns ===');
|
|
|
|
// First, unlink ALL patterns from the field in one go (since all 4 test patterns are on the same field)
|
|
testLogger.info(`Unlinking all patterns from field ${fieldName} in stream ${testStreamName}`);
|
|
await pm.streamAssociationPage.unlinkAllPatternsFromField(testStreamName, fieldName);
|
|
testLogger.info('✓ All patterns unlinked from field');
|
|
|
|
// Now delete each pattern
|
|
for (const patternName of allImportedPatterns) {
|
|
testLogger.info(`Checking and deleting pattern: ${patternName}`);
|
|
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const exists = await pm.sdrPatternsPage.checkPatternExists(patternName);
|
|
|
|
if (exists) {
|
|
testLogger.info(`Pattern ${patternName} exists. Deleting...`);
|
|
const deleteResult = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
|
|
if (deleteResult.success) {
|
|
testLogger.info(`✓ Pattern ${patternName} deleted successfully`);
|
|
} else if (deleteResult.reason === 'in_use') {
|
|
// This shouldn't happen since we already unlinked, but handle it just in case
|
|
testLogger.warn(`Pattern ${patternName} still in use. Checking associations...`);
|
|
for (const association of deleteResult.associations) {
|
|
testLogger.info(`Unlinking from stream: ${association.streamName}, field: ${association.fieldName}`);
|
|
await pm.streamAssociationPage.unlinkAllPatternsFromField(
|
|
association.streamName,
|
|
association.fieldName
|
|
);
|
|
}
|
|
// Try deleting again with retry logic
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const finalDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
|
|
if (!finalDelete.success) {
|
|
testLogger.warn(`Pattern ${patternName} still in use after unlinking, waiting and retrying...`);
|
|
await page.waitForTimeout(2000);
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
const retryDelete = await pm.sdrPatternsPage.deletePatternByName(patternName);
|
|
expect(retryDelete.success).toBeTruthy();
|
|
testLogger.info(`✓ Pattern ${patternName} deleted successfully on retry`);
|
|
} else {
|
|
testLogger.info(`✓ Pattern ${patternName} deleted successfully after unlinking`);
|
|
}
|
|
} else {
|
|
throw new Error(`Failed to delete pattern ${patternName}. Reason: ${deleteResult.reason}`);
|
|
}
|
|
} else {
|
|
testLogger.info(`✓ Pattern ${patternName} does not exist, no cleanup needed`);
|
|
}
|
|
}
|
|
|
|
// Verify all patterns have been removed
|
|
testLogger.info('Verifying all patterns have been removed');
|
|
await pm.sdrPatternsPage.navigateToRegexPatterns();
|
|
await page.waitForTimeout(1000);
|
|
|
|
for (const patternName of allImportedPatterns) {
|
|
const exists = await pm.sdrPatternsPage.verifyPatternExistsInList(patternName);
|
|
expect(exists).toBeFalsy();
|
|
testLogger.info(`✓ Verified pattern ${patternName} is removed`);
|
|
}
|
|
|
|
testLogger.info('=== END CLEANUP COMPLETE ===');
|
|
});
|
|
});
|