Made really good progress on loki support in dashboards

pull/15012/head
Torkel Ödegaard 6 years ago
parent 6e0b873739
commit 740e1a0540
  1. 5
      public/app/core/logs_model.ts
  2. 2
      public/app/features/explore/Explore.tsx
  3. 5
      public/app/features/explore/Logs.tsx
  4. 43
      public/app/plugins/datasource/loki/datasource.test.ts
  5. 94
      public/app/plugins/datasource/loki/datasource.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 };
});
}

@ -216,7 +216,7 @@ export class Explore extends React.PureComponent<ExploreProps> {
{showingStartPage && <StartPage onClickExample={this.onClickExample} />}
{!showingStartPage && (
<>
{supportsGraph && <GraphContainer exploreId={exploreId} />}
{supportsGraph && !supportsLogs && <GraphContainer exploreId={exploreId} />}
{supportsTable && <TableContainer exploreId={exploreId} onClickCell={this.onClickLabel} />}
{supportsLogs && (
<LogsContainer

@ -3,6 +3,8 @@ import React, { PureComponent } from 'react';
import * as rangeUtil from 'app/core/utils/rangeutil';
import { RawTimeRange, Switch } from '@grafana/ui';
import TimeSeries from 'app/core/time_series2';
import {
LogsDedupDescription,
LogsDedupStrategy,
@ -205,12 +207,13 @@ export default class Logs extends PureComponent<Props, State> {
// 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 (
<div className="logs-panel">
<div className="logs-panel-graph">
<Graph
data={data.series}
data={timeSeries}
height="100px"
range={range}
id={`explore-logs-graph-${exploreId}`}

@ -7,6 +7,17 @@ describe('LokiDatasource', () => {
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<LokiQuery>({ 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<LokiQuery>({ 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<LokiQuery>({
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<LokiQuery>({
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', () => {

@ -73,10 +73,11 @@ export class LokiDatasource {
};
}
query(options: DataQueryOptions<LokiQuery>): Promise<{ data: LogsStream[] }> {
async query(options: DataQueryOptions<LokiQuery>) {
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 };
});
}
}

Loading…
Cancel
Save