grafana/public/app/features/logs/components/panel/processing.test.ts

391 lines
15 KiB
TypeScript
Raw Normal View History

import { createTheme, Field, FieldType, LogLevel, LogRowModel, LogsSortOrder, toDataFrame } from '@grafana/data';
import { config } from '@grafana/runtime';
import { LOG_LINE_BODY_FIELD_NAME } from '../LogDetailsBody';
import { createLogLine, createLogRow } from '../mocks/logRow';
New Logs Panel: font size selector and Log Details size improvments (#106376) * LogList: create font size option * LogList: prevent option fontSize bouncing * LogListContext: fix stored container size bigger than container * LogList: render smaller font size * virtualization: adjust to variable font size * virtualization: strip white characters of at the start successive long lines * LogList: add font size to log size cache * LogList: use getters instead of fixed constants * LogLine: prevent unnecessary overflow calls * virtualization: strip ansi color codes before measuring * LogListDetails: adjust size on resize and give logs panel a min width * LogsPanel: add showControls as a dashboard option * virtualization: update test * virtualization: add small test case * processing: update font size * LogListControls: update test * Extract translations * Logs Panel: enable controls by default * LogListContext: update mock * ControlledLogRows: add missing prop * LogLine: remove height ref * LogList: dont touch the debounced function on successive calls * LogLine: update test * LogsPanel: make controls default to false again * LogsPanel: make controls default to false again * LogLineDetails: fix height resizing and make close button sticky * LogLine: memo log component * LogLineDetails: fix close button position * New Logs Panel: Add Popover Menu support (#106394) * LogList: add popover menu support * LogList: test popover menu * Chore: remove unnecessary optional chain op * LogLinedDetails: fix close button position with and without scroll
2025-06-10 17:59:01 +08:00
import { LogListFontSize } from './LogList';
import { LogListModel, preProcessLogs } from './processing';
import { LogLineVirtualization } from './virtualization';
describe('preProcessLogs', () => {
let logFmtLog: LogRowModel, nginxLog: LogRowModel, jsonLog: LogRowModel;
let processedLogs: LogListModel[];
New Logs Panel: font size selector and Log Details size improvments (#106376) * LogList: create font size option * LogList: prevent option fontSize bouncing * LogListContext: fix stored container size bigger than container * LogList: render smaller font size * virtualization: adjust to variable font size * virtualization: strip white characters of at the start successive long lines * LogList: add font size to log size cache * LogList: use getters instead of fixed constants * LogLine: prevent unnecessary overflow calls * virtualization: strip ansi color codes before measuring * LogListDetails: adjust size on resize and give logs panel a min width * LogsPanel: add showControls as a dashboard option * virtualization: update test * virtualization: add small test case * processing: update font size * LogListControls: update test * Extract translations * Logs Panel: enable controls by default * LogListContext: update mock * ControlledLogRows: add missing prop * LogLine: remove height ref * LogList: dont touch the debounced function on successive calls * LogLine: update test * LogsPanel: make controls default to false again * LogsPanel: make controls default to false again * LogLineDetails: fix height resizing and make close button sticky * LogLine: memo log component * LogLineDetails: fix close button position * New Logs Panel: Add Popover Menu support (#106394) * LogList: add popover menu support * LogList: test popover menu * Chore: remove unnecessary optional chain op * LogLinedDetails: fix close button position with and without scroll
2025-06-10 17:59:01 +08:00
const fontSizes: LogListFontSize[] = ['default', 'small'];
beforeEach(() => {
const getFieldLinks = jest.fn().mockImplementationOnce((field: Field) => ({
href: '/link',
title: 'link',
target: '_blank',
origin: field,
}));
logFmtLog = createLogRow({
uid: '1',
timeEpochMs: 3,
labels: { level: 'warn', logger: 'interceptor' },
entry: `logger=interceptor t=2025-03-18T08:58:34.820119602Z level=warn msg="calling resource store as the service without id token or marking it as the service identity" subject=:0 uid=43eb4c92-18a0-4060-be96-37af854f0830`,
logLevel: LogLevel.warning,
rowIndex: 0,
dataFrame: toDataFrame({
refId: 'A',
fields: [
{ name: 'Time', type: FieldType.time, values: [3, 2, 1] },
{
name: 'Line',
type: FieldType.string,
values: ['log message 1', 'log message 2', 'log message 3'],
},
{
name: 'labels',
type: FieldType.other,
values: [
{ level: 'warn', logger: 'interceptor' },
{ method: 'POST', status: '200' },
{ kind: 'Event', stage: 'ResponseComplete' },
],
},
{
name: 'link',
type: FieldType.string,
config: {
links: [
{
title: 'link1',
url: 'https://example.com',
},
],
},
values: ['link'],
},
],
}),
});
nginxLog = createLogRow({
uid: '2',
timeEpochMs: 2,
labels: { method: 'POST', status: '200' },
entry: `35.191.12.195 - accounts.google.com:test@grafana.com [18/Mar/2025:08:58:38 +0000] 200 "POST /grafana/api/ds/query?ds_type=prometheus&requestId=SQR461 HTTP/1.1" 59460 "https://test.example.com/?orgId=1" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "95.91.240.90, 34.107.247.24"`,
logLevel: LogLevel.critical,
});
jsonLog = createLogRow({
uid: '3',
timeEpochMs: 1,
labels: { kind: 'Event', stage: 'ResponseComplete' },
entry: `{"kind":"Event","apiVersion":"audit.k8s.io/v1","level":"Request","auditID":"2052d577-3391-4fe7-9fe2-4d6c8cbe398f","stage":"ResponseComplete","requestURI":"/api/v1/test","verb":"list","user":{"username":"system:apiserver","uid":"6f35feec-4522-4f21-8289-668e336967b5","groups":["system:authenticated","system:masters"]},"sourceIPs":["::1"],"userAgent":"kube-apiserver/v1.31.5 (linux/amd64) kubernetes/test","objectRef":{"resource":"resourcequotas","namespace":"test","apiVersion":"v1"},"responseStatus":{"metadata":{},"code":200},"requestReceivedTimestamp":"2025-03-18T08:58:34.940093Z"}`,
logLevel: LogLevel.error,
});
processedLogs = preProcessLogs([logFmtLog, nginxLog, jsonLog], {
escape: false,
getFieldLinks,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: true,
});
});
describe('LogListModel', () => {
test('Extends a LogRowModel', () => {
const logRowModel = createLogRow({
uid: '2',
datasourceUid: 'test',
timeEpochMs: 2,
labels: { method: 'POST', status: '200' },
entry: `35.191.12.195 - accounts.google.com:test@grafana.com [18/Mar/2025:08:58:38 +0000] 200 "POST /grafana/api/ds/query?ds_type=prometheus&requestId=SQR461 HTTP/1.1" 59460 "https://test.example.com/?orgId=1" "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36" "95.91.240.90, 34.107.247.24"`,
logLevel: LogLevel.critical,
});
const logListModel = new LogListModel(logRowModel, { escape: false, timeZone: 'browser ', wrapLogMessage: true });
expect(logListModel).toMatchObject(logRowModel);
});
test('Unwrapped log lines strip new lines', () => {
const logListModel = createLogLine(
{ labels: { place: `lu\nna` }, entry: `log\n message\n 1` },
{
escape: false,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: false, // unwrapped
}
);
expect(logListModel.getDisplayedFieldValue('place')).toBe('luna');
expect(logListModel.body).toBe('log message 1');
});
test('Wrapped log lines do not modify new lines', () => {
const logListModel = createLogLine(
{ labels: { place: `lu\nna` }, entry: `log\n message\n 1` },
{
escape: false,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: true, // wrapped
}
);
expect(logListModel.getDisplayedFieldValue('place')).toBe(logListModel.labels['place']);
expect(logListModel.body).toBe(logListModel.raw);
});
test('Strips ansi colors for measurement', () => {
const logListModel = createLogLine(
{ entry: `log \u001B[31mmessage\u001B[0m 1` },
{
escape: false,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: true,
}
);
expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, false)).toBe(
`log \u001B[31mmessage\u001B[0m 1`
);
expect(logListModel.getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME, true)).toBe('log message 1');
});
test('Prettifies JSON', () => {
const entry = '{"key": "value", "otherKey": "otherValue"}';
const logListModel = createLogLine(
{ entry },
{
escape: false,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: false, // unwrapped
}
);
expect(logListModel.entry).toBe(entry);
expect(logListModel.body).not.toBe(entry);
});
test('Uses lossless parsing', () => {
const entry = '{"number": 90071992547409911}';
const logListModel = createLogLine(
{ entry },
{
escape: false,
order: LogsSortOrder.Descending,
timeZone: 'browser',
wrapLogMessage: false, // unwrapped
}
);
expect(logListModel.entry).toBe(entry);
expect(logListModel.body).toContain('90071992547409911');
});
});
test('Orders logs', () => {
expect(processedLogs[0].uid).toBe('1');
expect(processedLogs[1].uid).toBe('2');
expect(processedLogs[2].uid).toBe('3');
});
test('Sets the display level level', () => {
expect(processedLogs[0].displayLevel).toBe('warn');
expect(processedLogs[1].displayLevel).toBe('crit');
expect(processedLogs[2].displayLevel).toBe('error');
});
test('Sets the log fields links', () => {
expect(processedLogs[0].fields).toEqual([
{
fieldIndex: 3,
keys: ['link'],
links: {
href: '/link',
origin: {
config: {
links: [
{
title: 'link1',
url: 'https://example.com',
},
],
},
index: 3,
name: 'link',
type: 'string',
values: ['link'],
},
target: '_blank',
title: 'link',
},
values: ['link'],
},
]);
expect(processedLogs[1].fields).toEqual([]);
expect(processedLogs[2].fields).toEqual([]);
});
test('Highlights tokens in log lines', () => {
expect(processedLogs[0].highlightedBody).toContain('log-token-label');
expect(processedLogs[0].highlightedBody).toContain('log-token-key');
expect(processedLogs[0].highlightedBody).toContain('log-token-string');
expect(processedLogs[0].highlightedBody).toContain('log-token-uuid');
expect(processedLogs[0].highlightedBody).not.toContain('log-token-method');
expect(processedLogs[0].highlightedBody).not.toContain('log-token-json-key');
expect(processedLogs[1].highlightedBody).toContain('log-token-method');
expect(processedLogs[1].highlightedBody).toContain('log-token-key');
expect(processedLogs[1].highlightedBody).toContain('log-token-string');
expect(processedLogs[1].highlightedBody).not.toContain('log-token-json-key');
expect(processedLogs[2].highlightedBody).toContain('log-token-json-key');
expect(processedLogs[2].highlightedBody).toContain('log-token-string');
expect(processedLogs[2].highlightedBody).not.toContain('log-token-method');
});
test('Returns displayed field values', () => {
expect(processedLogs[0].getDisplayedFieldValue('logger')).toBe('interceptor');
expect(processedLogs[1].getDisplayedFieldValue('method')).toBe('POST');
expect(processedLogs[2].getDisplayedFieldValue('kind')).toBe('Event');
expect(processedLogs[0].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[0].body);
expect(processedLogs[1].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[1].body);
expect(processedLogs[2].getDisplayedFieldValue(LOG_LINE_BODY_FIELD_NAME)).toBe(processedLogs[2].body);
});
New Logs Panel: font size selector and Log Details size improvments (#106376) * LogList: create font size option * LogList: prevent option fontSize bouncing * LogListContext: fix stored container size bigger than container * LogList: render smaller font size * virtualization: adjust to variable font size * virtualization: strip white characters of at the start successive long lines * LogList: add font size to log size cache * LogList: use getters instead of fixed constants * LogLine: prevent unnecessary overflow calls * virtualization: strip ansi color codes before measuring * LogListDetails: adjust size on resize and give logs panel a min width * LogsPanel: add showControls as a dashboard option * virtualization: update test * virtualization: add small test case * processing: update font size * LogListControls: update test * Extract translations * Logs Panel: enable controls by default * LogListContext: update mock * ControlledLogRows: add missing prop * LogLine: remove height ref * LogList: dont touch the debounced function on successive calls * LogLine: update test * LogsPanel: make controls default to false again * LogsPanel: make controls default to false again * LogLineDetails: fix height resizing and make close button sticky * LogLine: memo log component * LogLineDetails: fix close button position * New Logs Panel: Add Popover Menu support (#106394) * LogList: add popover menu support * LogList: test popover menu * Chore: remove unnecessary optional chain op * LogLinedDetails: fix close button position with and without scroll
2025-06-10 17:59:01 +08:00
describe.each(fontSizes)('Collapsible log lines', (fontSize: LogListFontSize) => {
let longLog: LogListModel, entry: string, container: HTMLDivElement, virtualization: LogLineVirtualization;
beforeEach(() => {
virtualization = new LogLineVirtualization(createTheme(), fontSize);
container = document.createElement('div');
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(200);
entry = new Array(2 * virtualization.getTruncationLength(null)).fill('e').join('');
longLog = createLogLine(
{ entry, labels: { field: 'value' } },
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true }
);
});
test('Long lines that are not truncated are not modified', () => {
expect(longLog.body).toBe(entry);
expect(longLog.highlightedBody).toBe(entry);
});
test('Sets the collapsed state based on the container size', () => {
// Make container half of the size
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(100);
expect(longLog.collapsed).toBeUndefined();
longLog.updateCollapsedState([], container);
expect(longLog.collapsed).toBe(true);
expect(longLog.body).not.toBe(entry);
expect(entry).toContain(longLog.body);
});
test('Sets the collapsed state based on the new lines count', () => {
const entry = new Array(virtualization.getTruncationLineCount()).fill('test\n').join('');
const multilineLog = createLogLine(
{ entry, labels: { field: 'value' } },
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true }
);
expect(multilineLog.collapsed).toBeUndefined();
multilineLog.updateCollapsedState([], container);
expect(multilineLog.collapsed).toBe(true);
expect(entry).toContain(multilineLog.body);
});
test('Correctly counts new lines', () => {
const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join('');
const multilineLog = createLogLine(
{ entry, labels: { field: 'value' } },
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true }
);
expect(multilineLog.collapsed).toBeUndefined();
multilineLog.updateCollapsedState([], container);
expect(multilineLog.collapsed).toBeUndefined();
});
New Logs Panel: Add Log Details support (#105609) * Log list: add onclick listener * LogListContext: add basic details support * LogLineDetails: create component * Address lint issues * Log Details: make resizable and store size * LogListModel: add sampled and error support * LogDetails: pass more required props * LogLineContext: add interactive callbacks support * LogLineDetails: pass interactive callbacks * LogList: pass displayedFields callbacks * LogLine: move click listener * LogLineMenu: support showing details * LogLine: move onclick listener * LogListContext: remove displayedFields intermediation * i18n * LogListContext: abstract details shown function * LogLine: visually show expanded lines * LogDetails: remove min width for labels * LogLineDetails: add close button * LogList: add extra wrapper to get width * LogLineDetails: update logs size on resize * virtualization: update to new width reference * LogLine: check overflow on every re-render * LogList: debug virtualization when resizing * LogLineDetails: make it scrollable * LogListContext: make detailsWidth not undefined * Update tests with new attributes * LogLine: update collapsed state with container changes * LogLine: move cursor property to clickable styles * LogList: fix height recalculation when display options change * Logs: fix feature toggles support * Logs: more feature toggles adjustments * Lint * LogLine: support duplicates, hasError, and isSampled * Logs: debug feature flag combinations * i18n * Prettier * New Logs Panel: generate storage key for dashboards * Explore Logs: fix filtered levels * Logs Sample: integrate new panel * LogLine: fix unwrapped logs * Fix test * Update test * Logs panel: update test * Prettier * LogLine: update tests * LogLineMenu: update test * LogList: update unit test * processing: update test * virtualization: update unit test
2025-05-21 01:28:35 +08:00
test('Considers the displayed fields to set the collapsed state', () => {
// Make container half of the size
jest.spyOn(container, 'clientWidth', 'get').mockReturnValue(100);
expect(longLog.collapsed).toBeUndefined();
// Log line body is not included in the displayed fields, so it fits in the container
longLog.updateCollapsedState(['field'], container);
expect(longLog.collapsed).toBeUndefined();
});
test('Considers new lines in displayed fields to set the collapsed state', () => {
const entry = new Array(virtualization.getTruncationLineCount() - 1).fill('test\n').join('');
const field = 'test\n';
const multilineLog = createLogLine(
{ entry, labels: { field } },
{ escape: false, order: LogsSortOrder.Descending, timeZone: 'browser', virtualization, wrapLogMessage: true }
);
expect(multilineLog.collapsed).toBeUndefined();
multilineLog.updateCollapsedState(['field', LOG_LINE_BODY_FIELD_NAME], container);
expect(multilineLog.collapsed).toBe(true);
expect(entry).toContain(multilineLog.body);
});
test('Updates the body based on the collapsed state', () => {
expect(longLog.collapsed).toBeUndefined();
expect(longLog.body).toBe(entry);
longLog.setCollapsedState(true);
expect(longLog.collapsed).toBe(true);
expect(longLog.body).not.toBe(entry);
expect(entry).toContain(longLog.body);
longLog.setCollapsedState(false);
expect(longLog.collapsed).toBe(false);
expect(longLog.body).toBe(entry);
});
});
});
describe('OTel logs', () => {
let originalOtelLogsFormatting = config.featureToggles.otelLogsFormatting;
afterAll(() => {
config.featureToggles.otelLogsFormatting = originalOtelLogsFormatting;
});
test('Requires a feature flag', () => {
const log = createLogLine({
labels: {
severity_number: '1',
telemetry_sdk_language: 'php',
scope_name: 'scope',
aws_ignore: 'ignored',
key: 'value',
otel: 'otel',
},
entry: `place="luna" 1ms 3 KB`,
});
expect(log.otelLanguage).toBeDefined();
expect(log.body).toEqual(`place="luna" 1ms 3 KB`);
});
test('Augments OTel log lines', () => {
config.featureToggles.otelLogsFormatting = true;
const log = createLogLine({
labels: {
severity_number: '1',
telemetry_sdk_language: 'php',
scope_name: 'scope',
aws_ignore: 'ignored',
key: 'value',
otel: 'otel',
},
entry: `place="luna" 1ms 3 KB`,
});
expect(log.otelLanguage).toBeDefined();
expect(log.body).toEqual(`place="luna" 1ms 3 KB key=value otel=otel`);
});
});