| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564 |
- import {
- DataFrame,
- FieldType,
- LogsModel,
- LogsMetaKind,
- LogsDedupStrategy,
- LogLevel,
- DataFrameHelper,
- toDataFrame,
- } from '@grafana/data';
- import {
- dedupLogRows,
- calculateFieldStats,
- calculateLogsLabelStats,
- getParser,
- LogsParsers,
- dataFrameToLogsModel,
- } from '../logs_model';
- describe('dedupLogRows()', () => {
- test('should return rows as is when dedup is set to none', () => {
- const logs = {
- rows: [
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- ],
- };
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toMatchObject(logs.rows);
- });
- test('should dedup on exact matches', () => {
- const logs = {
- rows: [
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- entry: 'INFO test 2.44 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- ],
- };
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
- {
- duplicates: 1,
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- duplicates: 0,
- entry: 'INFO test 2.44 on [xxx]',
- },
- {
- duplicates: 0,
- entry: 'WARN test 1.23 on [xxx]',
- },
- ]);
- });
- test('should dedup on number matches', () => {
- const logs = {
- rows: [
- {
- entry: 'WARN test 1.2323423 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- entry: 'INFO test 2.44 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- ],
- };
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.numbers).rows).toEqual([
- {
- duplicates: 1,
- entry: 'WARN test 1.2323423 on [xxx]',
- },
- {
- duplicates: 0,
- entry: 'INFO test 2.44 on [xxx]',
- },
- {
- duplicates: 0,
- entry: 'WARN test 1.23 on [xxx]',
- },
- ]);
- });
- test('should dedup on signature matches', () => {
- const logs = {
- rows: [
- {
- entry: 'WARN test 1.2323423 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- {
- entry: 'INFO test 2.44 on [xxx]',
- },
- {
- entry: 'WARN test 1.23 on [xxx]',
- },
- ],
- };
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.signature).rows).toEqual([
- {
- duplicates: 3,
- entry: 'WARN test 1.2323423 on [xxx]',
- },
- ]);
- });
- test('should return to non-deduped state on same log result', () => {
- const logs = {
- rows: [
- {
- entry: 'INFO 123',
- },
- {
- entry: 'WARN 123',
- },
- {
- entry: 'WARN 123',
- },
- ],
- };
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.exact).rows).toEqual([
- {
- duplicates: 0,
- entry: 'INFO 123',
- },
- {
- duplicates: 1,
- entry: 'WARN 123',
- },
- ]);
- expect(dedupLogRows(logs as LogsModel, LogsDedupStrategy.none).rows).toEqual(logs.rows);
- });
- });
- describe('calculateFieldStats()', () => {
- test('should return no stats for empty rows', () => {
- expect(calculateFieldStats([], /foo=(.*)/)).toEqual([]);
- });
- test('should return no stats if extractor does not match', () => {
- const rows = [
- {
- entry: 'foo=bar',
- },
- ];
- expect(calculateFieldStats(rows as any, /baz=(.*)/)).toEqual([]);
- });
- test('should return stats for found field', () => {
- const rows = [
- {
- entry: 'foo="42 + 1"',
- },
- {
- entry: 'foo=503 baz=foo',
- },
- {
- entry: 'foo="42 + 1"',
- },
- {
- entry: 't=2018-12-05T07:44:59+0000 foo=503',
- },
- ];
- expect(calculateFieldStats(rows as any, /foo=("[^"]*"|\S+)/)).toMatchObject([
- {
- value: '"42 + 1"',
- count: 2,
- },
- {
- value: '503',
- count: 2,
- },
- ]);
- });
- });
- describe('calculateLogsLabelStats()', () => {
- test('should return no stats for empty rows', () => {
- expect(calculateLogsLabelStats([], '')).toEqual([]);
- });
- test('should return no stats of label is not found', () => {
- const rows = [
- {
- entry: 'foo 1',
- labels: {
- foo: 'bar',
- },
- },
- ];
- expect(calculateLogsLabelStats(rows as any, 'baz')).toEqual([]);
- });
- test('should return stats for found labels', () => {
- const rows = [
- {
- entry: 'foo 1',
- labels: {
- foo: 'bar',
- },
- },
- {
- entry: 'foo 0',
- labels: {
- foo: 'xxx',
- },
- },
- {
- entry: 'foo 2',
- labels: {
- foo: 'bar',
- },
- },
- ];
- expect(calculateLogsLabelStats(rows as any, 'foo')).toMatchObject([
- {
- value: 'bar',
- count: 2,
- },
- {
- value: 'xxx',
- count: 1,
- },
- ]);
- });
- });
- describe('getParser()', () => {
- test('should return no parser on empty line', () => {
- expect(getParser('')).toBeUndefined();
- });
- test('should return no parser on unknown line pattern', () => {
- expect(getParser('To Be or not to be')).toBeUndefined();
- });
- test('should return logfmt parser on key value patterns', () => {
- expect(getParser('foo=bar baz="41 + 1')).toEqual(LogsParsers.logfmt);
- });
- test('should return JSON parser on JSON log lines', () => {
- // TODO implement other JSON value types than string
- expect(getParser('{"foo": "bar", "baz": "41 + 1"}')).toEqual(LogsParsers.JSON);
- });
- });
- describe('LogsParsers', () => {
- describe('logfmt', () => {
- const parser = LogsParsers.logfmt;
- test('should detect format', () => {
- expect(parser.test('foo')).toBeFalsy();
- expect(parser.test('foo=bar')).toBeTruthy();
- });
- test('should return parsed fields', () => {
- expect(parser.getFields('foo=bar baz="42 + 1"')).toEqual(['foo=bar', 'baz="42 + 1"']);
- });
- test('should return label for field', () => {
- expect(parser.getLabelFromField('foo=bar')).toBe('foo');
- });
- test('should return value for field', () => {
- expect(parser.getValueFromField('foo=bar')).toBe('bar');
- });
- test('should build a valid value matcher', () => {
- const matcher = parser.buildMatcher('foo');
- const match = 'foo=bar'.match(matcher);
- expect(match).toBeDefined();
- expect(match[1]).toBe('bar');
- });
- });
- describe('JSON', () => {
- const parser = LogsParsers.JSON;
- test('should detect format', () => {
- expect(parser.test('foo')).toBeFalsy();
- expect(parser.test('{"foo":"bar"}')).toBeTruthy();
- });
- test('should return parsed fields', () => {
- expect(parser.getFields('{ "foo" : "bar", "baz" : 42 }')).toEqual(['"foo" : "bar"', '"baz" : 42']);
- });
- test('should return parsed fields for nested quotes', () => {
- expect(parser.getFields(`{"foo":"bar: '[value=\\"42\\"]'"}`)).toEqual([`"foo":"bar: '[value=\\"42\\"]'"`]);
- });
- test('should return label for field', () => {
- expect(parser.getLabelFromField('"foo" : "bar"')).toBe('foo');
- });
- test('should return value for field', () => {
- expect(parser.getValueFromField('"foo" : "bar"')).toBe('"bar"');
- expect(parser.getValueFromField('"foo" : 42')).toBe('42');
- expect(parser.getValueFromField('"foo" : 42.1')).toBe('42.1');
- });
- test('should build a valid value matcher for strings', () => {
- const matcher = parser.buildMatcher('foo');
- const match = '{"foo":"bar"}'.match(matcher);
- expect(match).toBeDefined();
- expect(match[1]).toBe('bar');
- });
- test('should build a valid value matcher for integers', () => {
- const matcher = parser.buildMatcher('foo');
- const match = '{"foo":42.1}'.match(matcher);
- expect(match).toBeDefined();
- expect(match[1]).toBe('42.1');
- });
- });
- });
- const emptyLogsModel: any = {
- hasUniqueLabels: false,
- rows: [],
- meta: [],
- series: [],
- };
- describe('dataFrameToLogsModel', () => {
- it('given empty series should return empty logs model', () => {
- expect(dataFrameToLogsModel([] as DataFrame[], 0)).toMatchObject(emptyLogsModel);
- });
- it('given series without correct series name should return empty logs model', () => {
- const series: DataFrame[] = [
- toDataFrame({
- fields: [],
- }),
- ];
- expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
- });
- it('given series without a time field should return empty logs model', () => {
- const series: DataFrame[] = [
- new DataFrameHelper({
- fields: [
- {
- name: 'message',
- type: FieldType.string,
- values: [],
- },
- ],
- }),
- ];
- expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
- });
- it('given series without a string field should return empty logs model', () => {
- const series: DataFrame[] = [
- new DataFrameHelper({
- fields: [
- {
- name: 'time',
- type: FieldType.time,
- values: [],
- },
- ],
- }),
- ];
- expect(dataFrameToLogsModel(series, 0)).toMatchObject(emptyLogsModel);
- });
- it('given one series should return expected logs model', () => {
- const series: DataFrame[] = [
- new DataFrameHelper({
- labels: {
- filename: '/var/log/grafana/grafana.log',
- job: 'grafana',
- },
- fields: [
- {
- name: 'time',
- type: FieldType.time,
- values: ['2019-04-26T09:28:11.352440161Z', '2019-04-26T14:42:50.991981292Z'],
- },
- {
- name: 'message',
- type: FieldType.string,
- values: [
- 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
- 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
- ],
- },
- ],
- meta: {
- limit: 1000,
- },
- }),
- ];
- const logsModel = dataFrameToLogsModel(series, 0);
- expect(logsModel.hasUniqueLabels).toBeFalsy();
- expect(logsModel.rows).toHaveLength(2);
- expect(logsModel.rows).toMatchObject([
- {
- timestamp: '2019-04-26T09:28:11.352440161Z',
- entry: 't=2019-04-26T11:05:28+0200 lvl=info msg="Initializing DatasourceCacheService" logger=server',
- labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
- logLevel: 'info',
- uniqueLabels: {},
- },
- {
- timestamp: '2019-04-26T14:42:50.991981292Z',
- entry: 't=2019-04-26T16:42:50+0200 lvl=eror msg="new token…t unhashed token=56d9fdc5c8b7400bd51b060eea8ca9d7',
- labels: { filename: '/var/log/grafana/grafana.log', job: 'grafana' },
- logLevel: 'error',
- uniqueLabels: {},
- },
- ]);
- expect(logsModel.series).toHaveLength(2);
- expect(logsModel.meta).toHaveLength(2);
- expect(logsModel.meta[0]).toMatchObject({
- label: 'Common labels',
- value: series[0].labels,
- kind: LogsMetaKind.LabelsMap,
- });
- expect(logsModel.meta[1]).toMatchObject({
- label: 'Limit',
- value: `1000 (2 returned)`,
- kind: LogsMetaKind.String,
- });
- });
- it('given one series without labels should return expected logs model', () => {
- const series: DataFrame[] = [
- new DataFrameHelper({
- fields: [
- {
- name: 'time',
- type: FieldType.time,
- values: ['1970-01-01T00:00:01Z'],
- },
- {
- name: 'message',
- type: FieldType.string,
- values: ['WARN boooo'],
- },
- {
- name: 'level',
- type: FieldType.string,
- values: ['dbug'],
- },
- ],
- }),
- ];
- const logsModel = dataFrameToLogsModel(series, 0);
- expect(logsModel.rows).toHaveLength(1);
- expect(logsModel.rows).toMatchObject([
- {
- entry: 'WARN boooo',
- labels: undefined,
- logLevel: LogLevel.debug,
- uniqueLabels: {},
- },
- ]);
- });
- it('given multiple series should return expected logs model', () => {
- const series: DataFrame[] = [
- toDataFrame({
- labels: {
- foo: 'bar',
- baz: '1',
- level: 'dbug',
- },
- fields: [
- {
- name: 'ts',
- type: FieldType.time,
- values: ['1970-01-01T00:00:01Z'],
- },
- {
- name: 'line',
- type: FieldType.string,
- values: ['WARN boooo'],
- },
- ],
- }),
- toDataFrame({
- name: 'logs',
- labels: {
- foo: 'bar',
- baz: '2',
- level: 'err',
- },
- fields: [
- {
- name: 'time',
- type: FieldType.time,
- values: ['1970-01-01T00:00:00Z', '1970-01-01T00:00:02Z'],
- },
- {
- name: 'message',
- type: FieldType.string,
- values: ['INFO 1', 'INFO 2'],
- },
- ],
- }),
- ];
- const logsModel = dataFrameToLogsModel(series, 0);
- expect(logsModel.hasUniqueLabels).toBeTruthy();
- expect(logsModel.rows).toHaveLength(3);
- expect(logsModel.rows).toMatchObject([
- {
- entry: 'WARN boooo',
- labels: { foo: 'bar', baz: '1' },
- logLevel: LogLevel.debug,
- uniqueLabels: { baz: '1' },
- },
- {
- entry: 'INFO 1',
- labels: { foo: 'bar', baz: '2' },
- logLevel: LogLevel.error,
- uniqueLabels: { baz: '2' },
- },
- {
- entry: 'INFO 2',
- labels: { foo: 'bar', baz: '2' },
- logLevel: LogLevel.error,
- uniqueLabels: { baz: '2' },
- },
- ]);
- expect(logsModel.series).toHaveLength(2);
- expect(logsModel.meta).toHaveLength(1);
- expect(logsModel.meta[0]).toMatchObject({
- label: 'Common labels',
- value: {
- foo: 'bar',
- },
- kind: LogsMetaKind.LabelsMap,
- });
- });
- });
|