import { parse } from '../parser/parser'; import type { LokiQuery, StreamSelector, LineFilter, LabelFilter, MetricExpr, JsonParser, LogfmtParser, RegexpParser } from '../parser/types'; // ─── Helper ─────────────────────────────────────────────────────── function findStage(query: LokiQuery, type: string): S | undefined { return query.pipeline.find((s) => s.type === type) as S | undefined; } describe('Loki parser', () => { // ── Stream selector ── it('parses empty stream selector', () => { const result = parse('{}'); expect(result.streamSelector).not.toBeNull(); expect(result.streamSelector!.matchers).toEqual([]); expect(result.errors.length).toBe(0); }); it('parses single matcher', () => { const result = parse('{job="nginx"}'); expect(result.streamSelector!.matchers).toHaveLength(1); expect(result.streamSelector!.matchers[0]).toEqual({ label: 'job', operator: '=', value: '"nginx"', }); }); it('parses multiple matchers', () => { const result = parse('{job="nginx", app="api"}'); expect(result.streamSelector!.matchers).toHaveLength(2); expect(result.streamSelector!.matchers[0].label).toBe('job'); expect(result.streamSelector!.matchers[1].label).toBe('app'); }); it('parses != and =~ and !~ operators', () => { const r1 = parse('{job!="nginx"}'); expect(r1.streamSelector!.matchers[0].operator).toBe('!='); const r2 = parse('{job=~"nginx|api"}'); expect(r2.streamSelector!.matchers[0].operator).toBe('=~'); const r3 = parse('{job!~"nginx"}'); expect(r3.streamSelector!.matchers[0].operator).toBe('!~'); }); // ── Line filters ── it('parses line filter |=', () => { const result = parse('{job="nginx"} |= "error"'); const stage = findStage(result, 'lineFilter'); expect(stage).toBeDefined(); expect(stage!.operator).toBe('|='); expect(stage!.value).toBe('"error"'); }); it('parses line filter !=', () => { const result = parse('{job="nginx"} | != "error"'); const stage = findStage(result, 'lineFilter'); expect(stage).toBeDefined(); expect(stage!.operator).toBe('!='); }); it('parses line filter !~', () => { const result = parse('{job="nginx"} | !~ "5[0-9]+"'); const stage = findStage(result, 'lineFilter'); expect(stage).toBeDefined(); expect(stage!.operator).toBe('!~'); }); // ── Parsers ── it('parses | json', () => { const result = parse('{job="nginx"} | json'); const stage = findStage(result, 'jsonParser'); expect(stage).toBeDefined(); }); it('parses | logfmt', () => { const result = parse('{job="nginx"} | logfmt'); const stage = findStage(result, 'logfmtParser'); expect(stage).toBeDefined(); }); it('parses | regexp', () => { const result = parse('{job="nginx"} | regexp "(?P\\\\w+)"'); const stage = findStage(result, 'regexpParser'); expect(stage).toBeDefined(); expect(stage!.pattern).toBe('"(?P\\\\w+)"'); }); it('parses | pattern', () => { const result = parse('{job="nginx"} | pattern ""'); const stage = findStage(result, 'regexpParser'); expect(stage).toBeDefined(); }); it('parses | unpack', () => { const result = parse('{job="nginx"} | unpack'); const stage = findStage(result, 'jsonParser'); expect(stage).toBeDefined(); }); // ── Label filters ── it('parses label filter with >=', () => { const result = parse('{job="nginx"} | status >= 400'); const stage = findStage(result, 'labelFilter'); expect(stage).toBeDefined(); expect(stage!.conditions).toHaveLength(1); expect(stage!.conditions[0]).toEqual({ label: 'status', operator: '>=', value: '400', }); }); it('parses label filter with string equals', () => { const result = parse('{job="nginx"} | level = "error"'); const stage = findStage(result, 'labelFilter'); expect(stage).toBeDefined(); expect(stage!.conditions[0].operator).toBe('='); expect(stage!.conditions[0].value).toBe('"error"'); }); it('parses label filter with and', () => { const result = parse('{job="nginx"} | status >= 400 and level = "error"'); const stage = findStage(result, 'labelFilter'); expect(stage!.conditions).toHaveLength(2); }); // ── Metric expressions ── it('parses rate(...[5m])', () => { const result = parse('rate({job="nginx"}[5m])'); expect(result.metricExpr).not.toBeNull(); expect(result.metricExpr!.func).toBe('rate'); expect(result.metricExpr!.range).toBe('5m'); }); it('parses count_over_time', () => { const result = parse('count_over_time({job="api"}[1h])'); expect(result.metricExpr!.func).toBe('count_over_time'); expect(result.metricExpr!.range).toBe('1h'); }); it('parses sum', () => { const result = parse('sum(rate({job="nginx"}[5m]))'); expect(result.metricExpr).not.toBeNull(); expect(result.metricExpr!.func).toBe('sum'); }); // ── Multiple pipeline stages ── it('parses multiple pipeline stages', () => { const result = parse('{job="nginx"} |= "error" | json | status >= 400'); expect(result.pipeline).toHaveLength(3); expect(result.pipeline[0].type).toBe('lineFilter'); expect(result.pipeline[1].type).toBe('jsonParser'); expect(result.pipeline[2].type).toBe('labelFilter'); }); // ── Partial / error recovery ── it('recovers from incomplete matcher', () => { const result = parse('{job}'); // Should have an incomplete matcher but not crash expect(result.streamSelector).not.toBeNull(); }); it('recovers from empty input', () => { const result = parse(''); expect(result.streamSelector).toBeNull(); expect(result.pipeline).toEqual([]); expect(result.metricExpr).toBeNull(); }); });