From 740e1a05402ad452889c2b7706aebe22452f5b2b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 1 Feb 2019 13:30:15 +0100 Subject: [PATCH] Made really good progress on loki support in dashboards --- public/app/core/logs_model.ts | 5 +- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Logs.tsx | 5 +- .../datasource/loki/datasource.test.ts | 43 ++++++++- .../app/plugins/datasource/loki/datasource.ts | 94 ++++++++++--------- 5 files changed, 100 insertions(+), 49 deletions(-) diff --git a/public/app/core/logs_model.ts b/public/app/core/logs_model.ts index a3f78e7152a..abcd5563bd0 100644 --- a/public/app/core/logs_model.ts +++ b/public/app/core/logs_model.ts @@ -1,7 +1,6 @@ import _ from 'lodash'; -import { colors } from '@grafana/ui'; -import { TimeSeries } from 'app/core/core'; +import { colors, TimeSeries } from '@grafana/ui'; import { getThemeColor } from 'app/core/utils/colors'; /** @@ -341,6 +340,6 @@ export function makeSeriesForLogs(rows: LogRowModel[], intervalMs: number): Time return a[1] - b[1]; }); - return new TimeSeries(series); + return { datapoints: series.datapoints, target: series.alias, color: series.color }; }); } diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index 909c4e81b8b..06a6ae24cac 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -216,7 +216,7 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && } + {supportsGraph && !supportsLogs && } {supportsTable && } {supportsLogs && ( { // React profiler becomes unusable if we pass all rows to all rows and their labels, using getter instead const getRows = () => processedRows; + const timeSeries = data.series.map(series => new TimeSeries(series)); return (
{ url: 'myloggingurl', }; + const testResp = { + data: { + streams: [ + { + entries: [{ ts: '2019-02-01T10:27:37.498180581Z', line: 'hello' }], + labels: '{}', + }, + ], + }, + }; + describe('when querying', () => { const backendSrvMock = { datasourceRequest: jest.fn() }; @@ -17,7 +28,7 @@ describe('LokiDatasource', () => { test('should use default max lines when no limit given', () => { const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); - backendSrvMock.datasourceRequest = jest.fn(); + backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); ds.query(options); @@ -30,7 +41,7 @@ describe('LokiDatasource', () => { const customData = { ...(instanceSettings.jsonData || {}), maxLines: 20 }; const customSettings = { ...instanceSettings, jsonData: customData }; const ds = new LokiDatasource(customSettings, backendSrvMock, templateSrvMock); - backendSrvMock.datasourceRequest = jest.fn(); + backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); const options = getQueryOptions({ targets: [{ expr: 'foo', refId: 'B' }] }); ds.query(options); @@ -38,6 +49,34 @@ describe('LokiDatasource', () => { expect(backendSrvMock.datasourceRequest.mock.calls.length).toBe(1); expect(backendSrvMock.datasourceRequest.mock.calls[0][0].url).toContain('limit=20'); }); + + test('should return log streams when resultFormat is undefined', async done => { + const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); + backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); + + const options = getQueryOptions({ + targets: [{ expr: 'foo', refId: 'B' }], + }); + + const res = await ds.query(options); + + expect(res.data[0].entries[0].line).toBe('hello'); + done(); + }); + + test('should return time series when resultFormat is time_series', async done => { + const ds = new LokiDatasource(instanceSettings, backendSrvMock, templateSrvMock); + backendSrvMock.datasourceRequest = jest.fn(() => Promise.resolve(testResp)); + + const options = getQueryOptions({ + targets: [{ expr: 'foo', refId: 'B', resultFormat: 'time_series' }], + }); + + const res = await ds.query(options); + + expect(res.data[0].datapoints).toBeDefined(); + done(); + }); }); describe('when performing testDataSource', () => { diff --git a/public/app/plugins/datasource/loki/datasource.ts b/public/app/plugins/datasource/loki/datasource.ts index b57586f8f2c..282a62f9875 100644 --- a/public/app/plugins/datasource/loki/datasource.ts +++ b/public/app/plugins/datasource/loki/datasource.ts @@ -73,10 +73,11 @@ export class LokiDatasource { }; } - query(options: DataQueryOptions): Promise<{ data: LogsStream[] }> { + async query(options: DataQueryOptions) { const queryTargets = options.targets - .filter(target => target.expr) + .filter(target => target.expr && !target.hide) .map(target => this.prepareQueryTarget(target, options)); + if (queryTargets.length === 0) { return Promise.resolve({ data: [] }); } @@ -84,20 +85,29 @@ export class LokiDatasource { const queries = queryTargets.map(target => this._request('/api/prom/query', target)); return Promise.all(queries).then((results: any[]) => { - // Flatten streams from multiple queries - const allStreams: LogsStream[] = results.reduce((acc, response, i) => { - if (!response) { - return acc; + const allStreams: LogsStream[] = []; + + for (let i = 0; i < results.length; i++) { + const result = results[i]; + const query = queryTargets[i]; + + // add search term to stream & add to array + if (result.data) { + for (const stream of (result.data.streams || [])) { + stream.search = query.regexp; + allStreams.push(stream); + } } - const streams: LogsStream[] = response.data.streams || []; - // Inject search for match highlighting - const search: string = queryTargets[i].regexp; - streams.forEach(s => { - s.search = search; - }); - return [...acc, ...streams]; - }, []); - return { data: allStreams }; + } + + // check resultType + if (options.targets[0].resultFormat === 'time_series') { + const logs = mergeStreamsToLogs(allStreams, this.maxLines); + logs.series = makeSeriesForLogs(logs.rows, options.intervalMs); + return { data: logs.series }; + } else { + return { data: allStreams }; + } }); } @@ -142,35 +152,35 @@ export class LokiDatasource { testDatasource() { return this._request('/api/prom/label') - .then(res => { - if (res && res.data && res.data.values && res.data.values.length > 0) { - return { status: 'success', message: 'Data source connected and labels found.' }; - } - return { - status: 'error', - message: - 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', - }; - }) - .catch(err => { - let message = 'Loki: '; - if (err.statusText) { - message += err.statusText; - } else { - message += 'Cannot connect to Loki'; - } + .then(res => { + if (res && res.data && res.data.values && res.data.values.length > 0) { + return { status: 'success', message: 'Data source connected and labels found.' }; + } + return { + status: 'error', + message: + 'Data source connected, but no labels received. Verify that Loki and Promtail is configured properly.', + }; + }) + .catch(err => { + let message = 'Loki: '; + if (err.statusText) { + message += err.statusText; + } else { + message += 'Cannot connect to Loki'; + } - if (err.status) { - message += `. ${err.status}`; - } + if (err.status) { + message += `. ${err.status}`; + } - if (err.data && err.data.message) { - message += `. ${err.data.message}`; - } else if (err.data) { - message += `. ${err.data}`; - } - return { status: 'error', message: message }; - }); + if (err.data && err.data.message) { + message += `. ${err.data.message}`; + } else if (err.data) { + message += `. ${err.data}`; + } + return { status: 'error', message: message }; + }); } }