diff --git a/loki/src/queries/loki-time-series-query/get-loki-time-series-data.ts b/loki/src/queries/loki-time-series-query/get-loki-time-series-data.ts index d005279b6..3a45feed6 100644 --- a/loki/src/queries/loki-time-series-query/get-loki-time-series-data.ts +++ b/loki/src/queries/loki-time-series-query/get-loki-time-series-data.ts @@ -14,8 +14,8 @@ import { TimeSeriesQueryPlugin, replaceVariables } from '@perses-dev/plugin-system'; import { milliseconds } from 'date-fns'; import { DurationString, parseDurationString, TimeSeries } from '@perses-dev/spec'; -import { LokiClient } from '../../model/loki-client'; -import { LokiMatrixResult } from '../../model/loki-client-types'; +import { LokiClient, toUnixSeconds } from '../../model/loki-client'; +import { LokiMatrixResult, LokiVectorResult } from '../../model/loki-client-types'; import { DEFAULT_DATASOURCE } from '../constants'; import { LokiTimeSeriesQuerySpec } from './loki-time-series-query-types'; @@ -88,6 +88,19 @@ function convertMatrixToTimeSeries(matrix: LokiMatrixResult[]): TimeSeries[] { }); } +function convertVectorToTimeSeries(vector: LokiVectorResult[]): TimeSeries[] { + return vector.map((series) => { + const name = Object.entries(series.metric) + .map(([k, v]) => `${k}=${v}`) + .join(', '); + return { + name, + values: [[Number(series.value[0]) * 1000, Number(series.value[1])]], + labels: series.metric, + }; + }); +} + export const getLokiTimeSeriesData: TimeSeriesQueryPlugin['getTimeSeriesData'] = async ( spec, context @@ -107,7 +120,27 @@ export const getLokiTimeSeriesData: TimeSeriesQueryPlugin { return stubResponse as LokiQueryRangeResponse; }); +// Mock instant query +lokiStubClient.query = jest.fn(async () => { + const stubResponse: LokiQueryResponse = { + status: 'success', + data: { + resultType: 'vector', + result: [ + { + metric: { __name__: 'loki_count', service: 'api' }, + value: [1686141338, '99'], + }, + ], + }, + }; + return stubResponse; +}); + const getDatasourceClient: jest.Mock = jest.fn(() => { return lokiStubClient; }); @@ -62,7 +79,7 @@ const getDatasource: jest.Mock = jest.fn((): DatasourceSpec }; }); -const createStubContext = (): TimeSeriesQueryContext => { +const createStubContext = (overrides?: Partial): TimeSeriesQueryContext => { const stubTimeSeriesContext: TimeSeriesQueryContext = { datasourceStore: { getDatasource: getDatasource, @@ -74,10 +91,11 @@ const createStubContext = (): TimeSeriesQueryContext => { setSavedDatasources: jest.fn(), }, timeRange: { - end: new Date('01-01-2025'), - start: new Date('01-02-2025'), + end: new Date('2025-01-01T00:00:00Z'), + start: new Date('2024-12-25T00:00:00Z'), }, variableState: {}, + ...overrides, }; return stubTimeSeriesContext; }; @@ -98,4 +116,51 @@ describe('LokiTimeSeriesQuery', () => { const initialOptions = LokiTimeSeriesQuery.createInitialOptions(); expect(initialOptions).toEqual({ query: '' }); }); + + describe('instant mode', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('should call client.query() with end time as unix seconds and return vector series', async () => { + const context = createStubContext({ mode: 'instant' }); + const result = await LokiTimeSeriesQuery.getTimeSeriesData( + { query: 'count_over_time({service="api"} [1h])' }, + context + ); + + expect(lokiStubClient.query).toHaveBeenCalledTimes(1); + expect(lokiStubClient.queryRange).not.toHaveBeenCalled(); + const callArgs = (lokiStubClient.query as jest.Mock).mock.calls[0][0]; + expect(callArgs.time).toBe(Math.floor(context.timeRange.end.getTime() / 1000).toString()); + expect(result.series).toHaveLength(1); + expect(result.series[0]?.values[0]?.[1]).toBe(99); + }); + + it('should return empty series with warning notice for streams resultType', async () => { + (lokiStubClient.query as jest.Mock).mockResolvedValueOnce({ + status: 'success', + data: { + resultType: 'streams', + result: [{ stream: { service: 'api' }, values: [['1686141338000000000', 'log line']] }], + }, + } as LokiQueryResponse); + + const context = createStubContext({ mode: 'instant' }); + const result = await LokiTimeSeriesQuery.getTimeSeriesData({ query: '{service="api"}' }, context); + + expect(result.series).toHaveLength(0); + expect(result.metadata?.notices?.[0]?.type).toBe('warning'); + expect(result.metadata?.notices?.[0]?.message).toBe("log streams are not supported in 'instant' mode"); + }); + }); + + it('should use queryRange when mode is not set', async () => { + jest.clearAllMocks(); + const context = createStubContext(); + await LokiTimeSeriesQuery.getTimeSeriesData({ query: 'count_over_time({service="api"} [1h])' }, context); + + expect(lokiStubClient.queryRange).toHaveBeenCalledTimes(1); + expect(lokiStubClient.query).not.toHaveBeenCalled(); + }); }); diff --git a/loki/src/queries/loki-time-series-query/loki-time-series-query-types.ts b/loki/src/queries/loki-time-series-query/loki-time-series-query-types.ts index 918d9a56b..2b57a7f24 100644 --- a/loki/src/queries/loki-time-series-query/loki-time-series-query-types.ts +++ b/loki/src/queries/loki-time-series-query/loki-time-series-query-types.ts @@ -12,7 +12,7 @@ // limitations under the License. import { DatasourceSelector } from '@perses-dev/spec'; -import { LokiQueryRangeMatrixResponse } from '../../model/loki-client-types'; +import { LokiQueryRangeMatrixResponse, LokiQueryResponse } from '../../model/loki-client-types'; export interface LokiTimeSeriesQuerySpec { query: string; @@ -20,4 +20,4 @@ export interface LokiTimeSeriesQuerySpec { step?: string; } -export type LokiTimeSeriesQueryResponse = LokiQueryRangeMatrixResponse; +export type LokiTimeSeriesQueryResponse = LokiQueryRangeMatrixResponse | LokiQueryResponse;