Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -305,4 +305,80 @@ describe('SampleLogsInput', () => {
});
});
});

describe('when the file is too large', () => {
const type = 'text/plain';
let jsonParseSpy: jest.SpyInstance;

beforeEach(async () => {
// Simulate large log content that would cause a RangeError
jsonParseSpy = jest.spyOn(JSON, 'parse').mockImplementation(() => {
throw new RangeError();
});

await changeFile(input, new File(['...'], 'test.json', { type }));
});

afterAll(() => {
// Restore the original implementation after all tests
jsonParseSpy.mockRestore();
});

it('should raise an appropriate error', () => {
expect(result.queryByText('This logs sample file is too large to parse')).toBeInTheDocument();
});
});

describe('when the file is neither a valid json nor ndjson', () => {
const plainTextFile = 'test message 1\ntest message 2';
const type = 'text/plain';

beforeEach(async () => {
await changeFile(input, new File([plainTextFile], 'test.txt', { type }));
});

it('should set the integrationSetting correctly', () => {
expect(mockActions.setIntegrationSettings).toBeCalledWith({
logSamples: plainTextFile.split('\n'),
samplesFormat: undefined,
});
});
});

describe('when the file reader fails', () => {
const mockedMessage = 'Mocked error';
let myFileReader: FileReader;
let fileReaderSpy: jest.SpyInstance;

beforeEach(async () => {
myFileReader = new FileReader();
fileReaderSpy = jest.spyOn(global, 'FileReader').mockImplementation(() => myFileReader);

// We need to mock the error property
Object.defineProperty(myFileReader, 'error', {
value: new Error(mockedMessage),
writable: false,
});

jest.spyOn(myFileReader, 'readAsText').mockImplementation(() => {
const errorEvent = new ProgressEvent('error');
myFileReader.dispatchEvent(errorEvent);
});

const file = new File([`...`], 'test.json', { type: 'application/json' });
act(() => {
fireEvent.change(input, { target: { files: [file] } });
});
});

afterEach(() => {
fileReaderSpy.mockRestore();
});

it('should set the error message accordingly', () => {
expect(
result.queryByText(`An error occurred when reading logs sample: ${mockedMessage}`)
).toBeInTheDocument();
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -64,16 +64,32 @@ export const parseJSONArray = (
return { errorNoArrayFound: true, entries: [], pathToEntries: [] };
};

interface ParseLogsErrorResult {
error: string;
}

interface ParseLogsSuccessResult {
// Format of the samples, if able to be determined.
samplesFormat?: SamplesFormat;
// The parsed log samples. If samplesFormat is (ND)JSON, these are JSON strings.
logSamples: string[];
}

type ParseLogsResult = ParseLogsErrorResult | ParseLogsSuccessResult;

/**
* Parse the logs sample file content (json or ndjson) and return the parsed logs sample
* Parse the logs sample file content and return the parsed logs sample.
*
* This function will return an error message if the file content is not valid, that is:
* - it is too large to parse (the memory required is 2-3x of the file size); or
* - it looks like a JSON format, but there is no array; or
* - it looks like (ND)JSON format, but the items are not JSON dictionaries.
*
* Otherwise it is guaranteed to parse and return (possibly empty) `logSamples` array.
* If the file content is (ND)JSON, it will additionally fill out the `samplesFormat`
* field with name 'json' or 'ndjson'; otherwise it will be undefined.
*/
const parseLogsContent = (
fileContent: string
): {
error?: string;
logSamples: string[];
samplesFormat?: SamplesFormat;
} => {
const parseLogsContent = (fileContent: string): ParseLogsResult => {
let parsedContent: unknown[];
let samplesFormat: SamplesFormat;

Expand All @@ -90,31 +106,37 @@ const parseLogsContent = (
samplesFormat = { name: 'ndjson', multiline: false };
}
} catch (parseNDJSONError) {
if (parseNDJSONError instanceof RangeError) {
return { error: i18n.LOGS_SAMPLE_ERROR.TOO_LARGE_TO_PARSE };
}
try {
const { entries, pathToEntries, errorNoArrayFound } = parseJSONArray(fileContent);
if (errorNoArrayFound) {
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_ARRAY, logSamples: [] };
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_ARRAY };
}
parsedContent = entries;
samplesFormat = { name: 'json', json_path: pathToEntries };
} catch (parseJSONError) {
if (parseJSONError instanceof RangeError) {
return { error: i18n.LOGS_SAMPLE_ERROR.TOO_LARGE_TO_PARSE };
}
try {
parsedContent = parseNDJSON(fileContent, true);
samplesFormat = { name: 'ndjson', multiline: true };
} catch (parseMultilineNDJSONError) {
if (parseMultilineNDJSONError instanceof RangeError) {
return { error: i18n.LOGS_SAMPLE_ERROR.TOO_LARGE_TO_PARSE };
}
return {
logSamples: fileContent.split('\n').filter((line) => line.trim() !== ''),
samplesFormat: undefined, // Signifies that the format is unknown.
};
}
}
}

if (parsedContent.length === 0) {
return { error: i18n.LOGS_SAMPLE_ERROR.EMPTY, logSamples: [] };
}

if (parsedContent.some((log) => !isPlainObject(log))) {
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_OBJECT, logSamples: [] };
return { error: i18n.LOGS_SAMPLE_ERROR.NOT_OBJECT };
}

const logSamples = parsedContent.map((log) => JSON.stringify(log));
Expand All @@ -133,50 +155,84 @@ export const SampleLogsInput = React.memo<SampleLogsInputProps>(({ integrationSe

const onChangeLogsSample = useCallback(
(files: FileList | null) => {
const logsSampleFile = files?.[0];
if (logsSampleFile == null) {
setSampleFileError(undefined);
setIntegrationSettings({
...integrationSettings,
logSamples: undefined,
samplesFormat: undefined,
});
if (!files) {
return;
}

setSampleFileError(undefined);
setIntegrationSettings({
...integrationSettings,
logSamples: undefined,
samplesFormat: undefined,
});

const logsSampleFile = files[0];
const reader = new FileReader();

reader.onloadstart = function () {
setIsParsing(true);
};

reader.onloadend = function () {
setIsParsing(false);
};

reader.onload = function (e) {
const fileContent = e.target?.result as string | undefined; // We can safely cast to string since we call `readAsText` to load the file.

if (fileContent == null) {
return { error: i18n.LOGS_SAMPLE_ERROR.CAN_NOT_READ };
setSampleFileError(i18n.LOGS_SAMPLE_ERROR.CAN_NOT_READ);
return;
}
let samples;
const { error, logSamples, samplesFormat } = parseLogsContent(fileContent);
setIsParsing(false);
samples = logSamples;

if (error) {
setSampleFileError(error);
setIntegrationSettings({
...integrationSettings,
logSamples: undefined,
samplesFormat: undefined,
});

if (fileContent === '' && e.loaded > 100000) {
// V8-based browsers can't handle large files and return an empty string
// instead of an error; see https://stackoverflow.com/a/61316641
setSampleFileError(i18n.LOGS_SAMPLE_ERROR.TOO_LARGE_TO_PARSE);
return;
}

const result = parseLogsContent(fileContent);

if ('error' in result) {
setSampleFileError(result.error);
return;
}

const { logSamples: possiblyLargeLogSamples, samplesFormat } = result;

if (possiblyLargeLogSamples.length === 0) {
setSampleFileError(i18n.LOGS_SAMPLE_ERROR.EMPTY);
return;
}

if (samples.length > MaxLogsSampleRows) {
samples = samples.slice(0, MaxLogsSampleRows);
let logSamples;
if (possiblyLargeLogSamples.length > MaxLogsSampleRows) {
logSamples = possiblyLargeLogSamples.slice(0, MaxLogsSampleRows);
notifications?.toasts.addInfo(i18n.LOGS_SAMPLE_TRUNCATED(MaxLogsSampleRows));
} else {
logSamples = possiblyLargeLogSamples;
}

setIntegrationSettings({
...integrationSettings,
logSamples: samples,
logSamples,
samplesFormat,
});
};
setIsParsing(true);

const handleReaderError = function () {
const message = reader.error?.message;
if (message) {
setSampleFileError(i18n.LOGS_SAMPLE_ERROR.CAN_NOT_READ_WITH_REASON(message));
} else {
setSampleFileError(i18n.LOGS_SAMPLE_ERROR.CAN_NOT_READ);
}
};

reader.onerror = handleReaderError;
reader.onabort = handleReaderError;

reader.readAsText(logsSampleFile);
},
[integrationSettings, setIntegrationSettings, notifications?.toasts, setIsParsing]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -126,12 +126,26 @@ export const LOGS_SAMPLE_ERROR = {
defaultMessage: 'Failed to read the logs sample file',
}
),
CAN_NOT_READ_WITH_REASON: (reason: string) =>
i18n.translate(
'xpack.integrationAssistant.step.dataStream.logsSample.errorCanNotReadWithReason',
{
values: { reason },
defaultMessage: 'An error occurred when reading logs sample: {reason}',
}
),
CAN_NOT_PARSE: i18n.translate(
'xpack.integrationAssistant.step.dataStream.logsSample.errorCanNotParse',
{
defaultMessage: 'Cannot parse the logs sample file as either a JSON or NDJSON file',
}
),
TOO_LARGE_TO_PARSE: i18n.translate(
'xpack.integrationAssistant.step.dataStream.logsSample.errorTooLargeToParse',
{
defaultMessage: 'This logs sample file is too large to parse',
}
),
NOT_ARRAY: i18n.translate('xpack.integrationAssistant.step.dataStream.logsSample.errorNotArray', {
defaultMessage: 'The logs sample file is not an array',
}),
Expand Down