Commit 4d722b21 by David Committed by GitHub

Merge pull request #12675 from grafana/davkal/logging-datasource

Datasource for Grafana logging platform
parents 6b071054 3297ae46
...@@ -17,12 +17,14 @@ import ( ...@@ -17,12 +17,14 @@ import (
plugin "github.com/hashicorp/go-plugin" plugin "github.com/hashicorp/go-plugin"
) )
// DataSourcePlugin contains all metadata about a datasource plugin
type DataSourcePlugin struct { type DataSourcePlugin struct {
FrontendPluginBase FrontendPluginBase
Annotations bool `json:"annotations"` Annotations bool `json:"annotations"`
Metrics bool `json:"metrics"` Metrics bool `json:"metrics"`
Alerting bool `json:"alerting"` Alerting bool `json:"alerting"`
Explore bool `json:"explore"` Explore bool `json:"explore"`
Logs bool `json:"logs"`
QueryOptions map[string]bool `json:"queryOptions,omitempty"` QueryOptions map[string]bool `json:"queryOptions,omitempty"`
BuiltIn bool `json:"builtIn,omitempty"` BuiltIn bool `json:"builtIn,omitempty"`
Mixed bool `json:"mixed,omitempty"` Mixed bool `json:"mixed,omitempty"`
......
...@@ -11,6 +11,7 @@ import { parse as parseDate } from 'app/core/utils/datemath'; ...@@ -11,6 +11,7 @@ import { parse as parseDate } from 'app/core/utils/datemath';
import ElapsedTime from './ElapsedTime'; import ElapsedTime from './ElapsedTime';
import QueryRows from './QueryRows'; import QueryRows from './QueryRows';
import Graph from './Graph'; import Graph from './Graph';
import Logs from './Logs';
import Table from './Table'; import Table from './Table';
import TimePicker, { DEFAULT_RANGE } from './TimePicker'; import TimePicker, { DEFAULT_RANGE } from './TimePicker';
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query'; import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
...@@ -58,12 +59,17 @@ interface IExploreState { ...@@ -58,12 +59,17 @@ interface IExploreState {
initialDatasource?: string; initialDatasource?: string;
latency: number; latency: number;
loading: any; loading: any;
logsResult: any;
queries: any; queries: any;
queryError: any; queryError: any;
range: any; range: any;
requestOptions: any; requestOptions: any;
showingGraph: boolean; showingGraph: boolean;
showingLogs: boolean;
showingTable: boolean; showingTable: boolean;
supportsGraph: boolean | null;
supportsLogs: boolean | null;
supportsTable: boolean | null;
tableResult: any; tableResult: any;
} }
...@@ -82,12 +88,17 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -82,12 +88,17 @@ export class Explore extends React.Component<any, IExploreState> {
initialDatasource: datasource, initialDatasource: datasource,
latency: 0, latency: 0,
loading: false, loading: false,
logsResult: null,
queries: ensureQueries(queries), queries: ensureQueries(queries),
queryError: null, queryError: null,
range: range || { ...DEFAULT_RANGE }, range: range || { ...DEFAULT_RANGE },
requestOptions: null, requestOptions: null,
showingGraph: true, showingGraph: true,
showingLogs: true,
showingTable: true, showingTable: true,
supportsGraph: null,
supportsLogs: null,
supportsTable: null,
tableResult: null, tableResult: null,
...props.initialState, ...props.initialState,
}; };
...@@ -124,17 +135,29 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -124,17 +135,29 @@ export class Explore extends React.Component<any, IExploreState> {
} }
async setDatasource(datasource) { async setDatasource(datasource) {
const supportsGraph = datasource.meta.metrics;
const supportsLogs = datasource.meta.logs;
const supportsTable = datasource.meta.metrics;
let datasourceError = null;
try { try {
const testResult = await datasource.testDatasource(); const testResult = await datasource.testDatasource();
if (testResult.status === 'success') { datasourceError = testResult.status === 'success' ? null : testResult.message;
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
} else {
this.setState({ datasource: datasource, datasourceError: testResult.message, datasourceLoading: false });
}
} catch (error) { } catch (error) {
const message = (error && error.statusText) || error; datasourceError = (error && error.statusText) || error;
this.setState({ datasource: datasource, datasourceError: message, datasourceLoading: false });
} }
this.setState(
{
datasource,
datasourceError,
supportsGraph,
supportsLogs,
supportsTable,
datasourceLoading: false,
},
() => datasourceError === null && this.handleSubmit()
);
} }
getRef = el => { getRef = el => {
...@@ -157,6 +180,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -157,6 +180,7 @@ export class Explore extends React.Component<any, IExploreState> {
datasourceError: null, datasourceError: null,
datasourceLoading: true, datasourceLoading: true,
graphResult: null, graphResult: null,
logsResult: null,
tableResult: null, tableResult: null,
}); });
const datasource = await this.props.datasourceSrv.get(option.value); const datasource = await this.props.datasourceSrv.get(option.value);
...@@ -193,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -193,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
this.setState(state => ({ showingGraph: !state.showingGraph })); this.setState(state => ({ showingGraph: !state.showingGraph }));
}; };
handleClickLogsButton = () => {
this.setState(state => ({ showingLogs: !state.showingLogs }));
};
handleClickSplit = () => { handleClickSplit = () => {
const { onChangeSplit } = this.props; const { onChangeSplit } = this.props;
if (onChangeSplit) { if (onChangeSplit) {
...@@ -214,16 +242,19 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -214,16 +242,19 @@ export class Explore extends React.Component<any, IExploreState> {
}; };
handleSubmit = () => { handleSubmit = () => {
const { showingGraph, showingTable } = this.state; const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
if (showingTable) { if (showingTable && supportsTable) {
this.runTableQuery(); this.runTableQuery();
} }
if (showingGraph) { if (showingGraph && supportsGraph) {
this.runGraphQuery(); this.runGraphQuery();
} }
if (showingLogs && supportsLogs) {
this.runLogsQuery();
}
}; };
buildQueryOptions(targetOptions: { format: string; instant: boolean }) { buildQueryOptions(targetOptions: { format: string; instant?: boolean }) {
const { datasource, queries, range } = this.state; const { datasource, queries, range } = this.state;
const resolution = this.el.offsetWidth; const resolution = this.el.offsetWidth;
const absoluteRange = { const absoluteRange = {
...@@ -285,6 +316,29 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -285,6 +316,29 @@ export class Explore extends React.Component<any, IExploreState> {
} }
} }
async runLogsQuery() {
const { datasource, queries } = this.state;
if (!hasQuery(queries)) {
return;
}
this.setState({ latency: 0, loading: true, queryError: null, logsResult: null });
const now = Date.now();
const options = this.buildQueryOptions({
format: 'logs',
});
try {
const res = await datasource.query(options);
const logsData = res.data;
const latency = Date.now() - now;
this.setState({ latency, loading: false, logsResult: logsData, requestOptions: options });
} catch (response) {
console.error(response);
const queryError = response.data ? response.data.error : response;
this.setState({ loading: false, queryError });
}
}
request = url => { request = url => {
const { datasource } = this.state; const { datasource } = this.state;
return datasource.metadataRequest(url); return datasource.metadataRequest(url);
...@@ -300,17 +354,23 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -300,17 +354,23 @@ export class Explore extends React.Component<any, IExploreState> {
graphResult, graphResult,
latency, latency,
loading, loading,
logsResult,
queries, queries,
queryError, queryError,
range, range,
requestOptions, requestOptions,
showingGraph, showingGraph,
showingLogs,
showingTable, showingTable,
supportsGraph,
supportsLogs,
supportsTable,
tableResult, tableResult,
} = this.state; } = this.state;
const showingBoth = showingGraph && showingTable; const showingBoth = showingGraph && showingTable;
const graphHeight = showingBoth ? '200px' : '400px'; const graphHeight = showingBoth ? '200px' : '400px';
const graphButtonActive = showingBoth || showingGraph ? 'active' : ''; const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
const logsButtonActive = showingLogs ? 'active' : '';
const tableButtonActive = showingBoth || showingTable ? 'active' : ''; const tableButtonActive = showingBoth || showingTable ? 'active' : '';
const exploreClass = split ? 'explore explore-split' : 'explore'; const exploreClass = split ? 'explore explore-split' : 'explore';
const datasources = datasourceSrv.getExploreSources().map(ds => ({ const datasources = datasourceSrv.getExploreSources().map(ds => ({
...@@ -357,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -357,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
</div> </div>
) : null} ) : null}
<div className="navbar-buttons"> <div className="navbar-buttons">
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}> {supportsGraph ? (
Graph <button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
</button> Graph
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}> </button>
Table ) : null}
</button> {supportsTable ? (
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
Table
</button>
) : null}
{supportsLogs ? (
<button className={`btn navbar-button ${logsButtonActive}`} onClick={this.handleClickLogsButton}>
Logs
</button>
) : null}
</div> </div>
<TimePicker range={range} onChangeTime={this.handleChangeTime} /> <TimePicker range={range} onChangeTime={this.handleChangeTime} />
<div className="navbar-buttons relative"> <div className="navbar-buttons relative">
...@@ -395,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -395,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
/> />
{queryError ? <div className="text-warning m-a-2">{queryError}</div> : null} {queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
<main className="m-t-2"> <main className="m-t-2">
{showingGraph ? ( {supportsGraph && showingGraph ? (
<Graph <Graph
data={graphResult} data={graphResult}
id={`explore-graph-${position}`} id={`explore-graph-${position}`}
...@@ -404,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> { ...@@ -404,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
split={split} split={split}
/> />
) : null} ) : null}
{showingTable ? <Table data={tableResult} className="m-t-3" /> : null} {supportsTable && showingTable ? <Table data={tableResult} className="m-t-3" /> : null}
{supportsLogs && showingLogs ? <Logs data={logsResult} /> : null}
</main> </main>
</div> </div>
) : null} ) : null}
......
import React from 'react';
export default function({ value }) {
return (
<div>
<pre>{JSON.stringify(value, undefined, 2)}</pre>
</div>
);
}
import React, { Fragment, PureComponent } from 'react';
import { LogsModel, LogRow } from 'app/core/logs_model';
interface LogsProps {
className?: string;
data: LogsModel;
}
const EXAMPLE_QUERY = '{job="default/prometheus"}';
const Entry: React.SFC<LogRow> = props => {
const { entry, searchMatches } = props;
if (searchMatches && searchMatches.length > 0) {
let lastMatchEnd = 0;
const spans = searchMatches.reduce((acc, match, i) => {
// Insert non-match
if (match.start !== lastMatchEnd) {
acc.push(<>{entry.slice(lastMatchEnd, match.start)}</>);
}
// Match
acc.push(
<span className="logs-row-match-highlight" title={`Matching expression: ${match.text}`}>
{entry.substr(match.start, match.length)}
</span>
);
lastMatchEnd = match.start + match.length;
// Non-matching end
if (i === searchMatches.length - 1) {
acc.push(<>{entry.slice(lastMatchEnd)}</>);
}
return acc;
}, []);
return <>{spans}</>;
}
return <>{props.entry}</>;
};
export default class Logs extends PureComponent<LogsProps, any> {
render() {
const { className = '', data } = this.props;
const hasData = data && data.rows && data.rows.length > 0;
return (
<div className={`${className} logs`}>
{hasData ? (
<div className="logs-entries panel-container">
{data.rows.map(row => (
<Fragment key={row.key}>
<div className={row.logLevel ? `logs-row-level logs-row-level-${row.logLevel}` : ''} />
<div title={`${row.timestamp} (${row.timeFromNow})`}>{row.timeLocal}</div>
<div>
<Entry {...row} />
</div>
</Fragment>
))}
</div>
) : null}
{!hasData ? (
<div className="panel-container">
Enter a query like <code>{EXAMPLE_QUERY}</code>
</div>
) : null}
</div>
);
}
}
...@@ -417,6 +417,7 @@ class QueryField extends React.Component<any, any> { ...@@ -417,6 +417,7 @@ class QueryField extends React.Component<any, any> {
const url = `/api/v1/label/${key}/values`; const url = `/api/v1/label/${key}/values`;
try { try {
const res = await this.request(url); const res = await this.request(url);
console.log(res);
const body = await (res.data || res.json()); const body = await (res.data || res.json());
const pairs = this.state.labelValues[EMPTY_METRIC]; const pairs = this.state.labelValues[EMPTY_METRIC];
const values = { const values = {
......
export enum LogLevel {
crit = 'crit',
warn = 'warn',
err = 'error',
error = 'error',
info = 'info',
debug = 'debug',
trace = 'trace',
}
export interface LogSearchMatch {
start: number;
length: number;
text?: string;
}
export interface LogRow {
key: string;
entry: string;
logLevel: LogLevel;
timestamp: string;
timeFromNow: string;
timeLocal: string;
searchMatches?: LogSearchMatch[];
}
export interface LogsModel {
rows: LogRow[];
}
...@@ -4,6 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul ...@@ -4,6 +4,7 @@ import * as elasticsearchPlugin from 'app/plugins/datasource/elasticsearch/modul
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module'; import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
import * as grafanaPlugin from 'app/plugins/datasource/grafana/module'; import * as grafanaPlugin from 'app/plugins/datasource/grafana/module';
import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module'; import * as influxdbPlugin from 'app/plugins/datasource/influxdb/module';
import * as loggingPlugin from 'app/plugins/datasource/logging/module';
import * as mixedPlugin from 'app/plugins/datasource/mixed/module'; import * as mixedPlugin from 'app/plugins/datasource/mixed/module';
import * as mysqlPlugin from 'app/plugins/datasource/mysql/module'; import * as mysqlPlugin from 'app/plugins/datasource/mysql/module';
import * as postgresPlugin from 'app/plugins/datasource/postgres/module'; import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
...@@ -28,6 +29,7 @@ const builtInPlugins = { ...@@ -28,6 +29,7 @@ const builtInPlugins = {
'app/plugins/datasource/opentsdb/module': opentsdbPlugin, 'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
'app/plugins/datasource/grafana/module': grafanaPlugin, 'app/plugins/datasource/grafana/module': grafanaPlugin,
'app/plugins/datasource/influxdb/module': influxdbPlugin, 'app/plugins/datasource/influxdb/module': influxdbPlugin,
'app/plugins/datasource/logging/module': loggingPlugin,
'app/plugins/datasource/mixed/module': mixedPlugin, 'app/plugins/datasource/mixed/module': mixedPlugin,
'app/plugins/datasource/mysql/module': mysqlPlugin, 'app/plugins/datasource/mysql/module': mysqlPlugin,
'app/plugins/datasource/postgres/module': postgresPlugin, 'app/plugins/datasource/postgres/module': postgresPlugin,
......
# Grafana Logging Datasource - Native Plugin
This is a **built in** datasource that allows you to connect to Grafana's logging service.
\ No newline at end of file
import { parseQuery } from './datasource';
describe('parseQuery', () => {
it('returns empty for empty string', () => {
expect(parseQuery('')).toEqual({
query: '',
regexp: '',
});
});
it('returns regexp for strings without query', () => {
expect(parseQuery('test')).toEqual({
query: '',
regexp: 'test',
});
});
it('returns query for strings without regexp', () => {
expect(parseQuery('{foo="bar"}')).toEqual({
query: '{foo="bar"}',
regexp: '',
});
});
it('returns query for strings with query and search string', () => {
expect(parseQuery('x {foo="bar"}')).toEqual({
query: '{foo="bar"}',
regexp: 'x',
});
});
it('returns query for strings with query and regexp', () => {
expect(parseQuery('{foo="bar"} x|y')).toEqual({
query: '{foo="bar"}',
regexp: 'x|y',
});
});
});
import _ from 'lodash';
import * as dateMath from 'app/core/utils/datemath';
import { processStreams } from './result_transformer';
const DEFAULT_LIMIT = 100;
const DEFAULT_QUERY_PARAMS = {
direction: 'BACKWARD',
limit: DEFAULT_LIMIT,
regexp: '',
query: '',
};
const QUERY_REGEXP = /({\w+="[^"]+"})?\s*(\w[^{]+)?\s*({\w+="[^"]+"})?/;
export function parseQuery(input: string) {
const match = input.match(QUERY_REGEXP);
let query = '';
let regexp = '';
if (match) {
if (match[1]) {
query = match[1];
}
if (match[2]) {
regexp = match[2].trim();
}
if (match[3]) {
if (match[1]) {
query = `${match[1].slice(0, -1)},${match[3].slice(1)}`;
} else {
query = match[3];
}
}
}
return { query, regexp };
}
function serializeParams(data: any) {
return Object.keys(data)
.map(k => {
const v = data[k];
return encodeURIComponent(k) + '=' + encodeURIComponent(v);
})
.join('&');
}
export default class LoggingDatasource {
/** @ngInject */
constructor(private instanceSettings, private backendSrv, private templateSrv) {}
_request(apiUrl: string, data?, options?: any) {
const baseUrl = this.instanceSettings.url;
const params = data ? serializeParams(data) : '';
const url = `${baseUrl}${apiUrl}?${params}`;
const req = {
...options,
url,
};
return this.backendSrv.datasourceRequest(req);
}
prepareQueryTarget(target, options) {
const interpolated = this.templateSrv.replace(target.expr);
const start = this.getTime(options.range.from, false);
const end = this.getTime(options.range.to, true);
return {
...DEFAULT_QUERY_PARAMS,
...parseQuery(interpolated),
start,
end,
};
}
query(options) {
const queryTargets = options.targets
.filter(target => target.expr)
.map(target => this.prepareQueryTarget(target, options));
if (queryTargets.length === 0) {
return Promise.resolve({ data: [] });
}
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 = results.reduce((acc, response, i) => {
const streams = response.data.streams || [];
// Inject search for match highlighting
const search = queryTargets[i].regexp;
streams.forEach(s => {
s.search = search;
});
return [...acc, ...streams];
}, []);
const model = processStreams(allStreams, DEFAULT_LIMIT);
return { data: model };
});
}
metadataRequest(url) {
// HACK to get label values for {job=|}, will be replaced when implementing LoggingQueryField
const apiUrl = url.replace('v1', 'prom');
return this._request(apiUrl, { silent: true }).then(res => {
const data = { data: { data: res.data.values || [] } };
return data;
});
}
getTime(date, roundUp) {
if (_.isString(date)) {
date = dateMath.parse(date, roundUp);
}
return Math.ceil(date.valueOf() * 1e6);
}
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 logging is configured properly.',
};
})
.catch(err => {
return { status: 'error', message: err.message };
});
}
}
<?xml version="1.0" encoding="utf-8"?>
<!-- Generator: Adobe Illustrator 20.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
width="351px" height="365px" viewBox="0 0 351 365" style="enable-background:new 0 0 351 365;" xml:space="preserve">
<style type="text/css">
.st0{fill:url(#SVGID_1_);}
</style>
<g id="Layer_1_1_">
</g>
<linearGradient id="SVGID_1_" gradientUnits="userSpaceOnUse" x1="175.5" y1="445.4948" x2="175.5" y2="114.0346">
<stop offset="0" style="stop-color:#FFF100"/>
<stop offset="1" style="stop-color:#F05A28"/>
</linearGradient>
<path class="st0" d="M342,161.2c-0.6-6.1-1.6-13.1-3.6-20.9c-2-7.7-5-16.2-9.4-25c-4.4-8.8-10.1-17.9-17.5-26.8
c-2.9-3.5-6.1-6.9-9.5-10.2c5.1-20.3-6.2-37.9-6.2-37.9c-19.5-1.2-31.9,6.1-36.5,9.4c-0.8-0.3-1.5-0.7-2.3-1
c-3.3-1.3-6.7-2.6-10.3-3.7c-3.5-1.1-7.1-2.1-10.8-3c-3.7-0.9-7.4-1.6-11.2-2.2c-0.7-0.1-1.3-0.2-2-0.3
c-8.5-27.2-32.9-38.6-32.9-38.6c-27.3,17.3-32.4,41.5-32.4,41.5s-0.1,0.5-0.3,1.4c-1.5,0.4-3,0.9-4.5,1.3c-2.1,0.6-4.2,1.4-6.2,2.2
c-2.1,0.8-4.1,1.6-6.2,2.5c-4.1,1.8-8.2,3.8-12.2,6c-3.9,2.2-7.7,4.6-11.4,7.1c-0.5-0.2-1-0.4-1-0.4c-37.8-14.4-71.3,2.9-71.3,2.9
c-3.1,40.2,15.1,65.5,18.7,70.1c-0.9,2.5-1.7,5-2.5,7.5c-2.8,9.1-4.9,18.4-6.2,28.1c-0.2,1.4-0.4,2.8-0.5,4.2
C18.8,192.7,8.5,228,8.5,228c29.1,33.5,63.1,35.6,63.1,35.6c0,0,0.1-0.1,0.1-0.1c4.3,7.7,9.3,15,14.9,21.9c2.4,2.9,4.8,5.6,7.4,8.3
c-10.6,30.4,1.5,55.6,1.5,55.6c32.4,1.2,53.7-14.2,58.2-17.7c3.2,1.1,6.5,2.1,9.8,2.9c10,2.6,20.2,4.1,30.4,4.5
c2.5,0.1,5.1,0.2,7.6,0.1l1.2,0l0.8,0l1.6,0l1.6-0.1l0,0.1c15.3,21.8,42.1,24.9,42.1,24.9c19.1-20.1,20.2-40.1,20.2-44.4l0,0
c0,0,0-0.1,0-0.3c0-0.4,0-0.6,0-0.6l0,0c0-0.3,0-0.6,0-0.9c4-2.8,7.8-5.8,11.4-9.1c7.6-6.9,14.3-14.8,19.9-23.3
c0.5-0.8,1-1.6,1.5-2.4c21.6,1.2,36.9-13.4,36.9-13.4c-3.6-22.5-16.4-33.5-19.1-35.6l0,0c0,0-0.1-0.1-0.3-0.2
c-0.2-0.1-0.2-0.2-0.2-0.2c0,0,0,0,0,0c-0.1-0.1-0.3-0.2-0.5-0.3c0.1-1.4,0.2-2.7,0.3-4.1c0.2-2.4,0.2-4.9,0.2-7.3l0-1.8l0-0.9
l0-0.5c0-0.6,0-0.4,0-0.6l-0.1-1.5l-0.1-2c0-0.7-0.1-1.3-0.2-1.9c-0.1-0.6-0.1-1.3-0.2-1.9l-0.2-1.9l-0.3-1.9
c-0.4-2.5-0.8-4.9-1.4-7.4c-2.3-9.7-6.1-18.9-11-27.2c-5-8.3-11.2-15.6-18.3-21.8c-7-6.2-14.9-11.2-23.1-14.9
c-8.3-3.7-16.9-6.1-25.5-7.2c-4.3-0.6-8.6-0.8-12.9-0.7l-1.6,0l-0.4,0c-0.1,0-0.6,0-0.5,0l-0.7,0l-1.6,0.1c-0.6,0-1.2,0.1-1.7,0.1
c-2.2,0.2-4.4,0.5-6.5,0.9c-8.6,1.6-16.7,4.7-23.8,9c-7.1,4.3-13.3,9.6-18.3,15.6c-5,6-8.9,12.7-11.6,19.6c-2.7,6.9-4.2,14.1-4.6,21
c-0.1,1.7-0.1,3.5-0.1,5.2c0,0.4,0,0.9,0,1.3l0.1,1.4c0.1,0.8,0.1,1.7,0.2,2.5c0.3,3.5,1,6.9,1.9,10.1c1.9,6.5,4.9,12.4,8.6,17.4
c3.7,5,8.2,9.1,12.9,12.4c4.7,3.2,9.8,5.5,14.8,7c5,1.5,10,2.1,14.7,2.1c0.6,0,1.2,0,1.7,0c0.3,0,0.6,0,0.9,0c0.3,0,0.6,0,0.9-0.1
c0.5,0,1-0.1,1.5-0.1c0.1,0,0.3,0,0.4-0.1l0.5-0.1c0.3,0,0.6-0.1,0.9-0.1c0.6-0.1,1.1-0.2,1.7-0.3c0.6-0.1,1.1-0.2,1.6-0.4
c1.1-0.2,2.1-0.6,3.1-0.9c2-0.7,4-1.5,5.7-2.4c1.8-0.9,3.4-2,5-3c0.4-0.3,0.9-0.6,1.3-1c1.6-1.3,1.9-3.7,0.6-5.3
c-1.1-1.4-3.1-1.8-4.7-0.9c-0.4,0.2-0.8,0.4-1.2,0.6c-1.4,0.7-2.8,1.3-4.3,1.8c-1.5,0.5-3.1,0.9-4.7,1.2c-0.8,0.1-1.6,0.2-2.5,0.3
c-0.4,0-0.8,0.1-1.3,0.1c-0.4,0-0.9,0-1.2,0c-0.4,0-0.8,0-1.2,0c-0.5,0-1,0-1.5-0.1c0,0-0.3,0-0.1,0l-0.2,0l-0.3,0
c-0.2,0-0.5,0-0.7-0.1c-0.5-0.1-0.9-0.1-1.4-0.2c-3.7-0.5-7.4-1.6-10.9-3.2c-3.6-1.6-7-3.8-10.1-6.6c-3.1-2.8-5.8-6.1-7.9-9.9
c-2.1-3.8-3.6-8-4.3-12.4c-0.3-2.2-0.5-4.5-0.4-6.7c0-0.6,0.1-1.2,0.1-1.8c0,0.2,0-0.1,0-0.1l0-0.2l0-0.5c0-0.3,0.1-0.6,0.1-0.9
c0.1-1.2,0.3-2.4,0.5-3.6c1.7-9.6,6.5-19,13.9-26.1c1.9-1.8,3.9-3.4,6-4.9c2.1-1.5,4.4-2.8,6.8-3.9c2.4-1.1,4.8-2,7.4-2.7
c2.5-0.7,5.1-1.1,7.8-1.4c1.3-0.1,2.6-0.2,4-0.2c0.4,0,0.6,0,0.9,0l1.1,0l0.7,0c0.3,0,0,0,0.1,0l0.3,0l1.1,0.1
c2.9,0.2,5.7,0.6,8.5,1.3c5.6,1.2,11.1,3.3,16.2,6.1c10.2,5.7,18.9,14.5,24.2,25.1c2.7,5.3,4.6,11,5.5,16.9c0.2,1.5,0.4,3,0.5,4.5
l0.1,1.1l0.1,1.1c0,0.4,0,0.8,0,1.1c0,0.4,0,0.8,0,1.1l0,1l0,1.1c0,0.7-0.1,1.9-0.1,2.6c-0.1,1.6-0.3,3.3-0.5,4.9
c-0.2,1.6-0.5,3.2-0.8,4.8c-0.3,1.6-0.7,3.2-1.1,4.7c-0.8,3.1-1.8,6.2-3,9.3c-2.4,6-5.6,11.8-9.4,17.1
c-7.7,10.6-18.2,19.2-30.2,24.7c-6,2.7-12.3,4.7-18.8,5.7c-3.2,0.6-6.5,0.9-9.8,1l-0.6,0l-0.5,0l-1.1,0l-1.6,0l-0.8,0
c0.4,0-0.1,0-0.1,0l-0.3,0c-1.8,0-3.5-0.1-5.3-0.3c-7-0.5-13.9-1.8-20.7-3.7c-6.7-1.9-13.2-4.6-19.4-7.8
c-12.3-6.6-23.4-15.6-32-26.5c-4.3-5.4-8.1-11.3-11.2-17.4c-3.1-6.1-5.6-12.6-7.4-19.1c-1.8-6.6-2.9-13.3-3.4-20.1l-0.1-1.3l0-0.3
l0-0.3l0-0.6l0-1.1l0-0.3l0-0.4l0-0.8l0-1.6l0-0.3c0,0,0,0.1,0-0.1l0-0.6c0-0.8,0-1.7,0-2.5c0.1-3.3,0.4-6.8,0.8-10.2
c0.4-3.4,1-6.9,1.7-10.3c0.7-3.4,1.5-6.8,2.5-10.2c1.9-6.7,4.3-13.2,7.1-19.3c5.7-12.2,13.1-23.1,22-31.8c2.2-2.2,4.5-4.2,6.9-6.2
c2.4-1.9,4.9-3.7,7.5-5.4c2.5-1.7,5.2-3.2,7.9-4.6c1.3-0.7,2.7-1.4,4.1-2c0.7-0.3,1.4-0.6,2.1-0.9c0.7-0.3,1.4-0.6,2.1-0.9
c2.8-1.2,5.7-2.2,8.7-3.1c0.7-0.2,1.5-0.4,2.2-0.7c0.7-0.2,1.5-0.4,2.2-0.6c1.5-0.4,3-0.8,4.5-1.1c0.7-0.2,1.5-0.3,2.3-0.5
c0.8-0.2,1.5-0.3,2.3-0.5c0.8-0.1,1.5-0.3,2.3-0.4l1.1-0.2l1.2-0.2c0.8-0.1,1.5-0.2,2.3-0.3c0.9-0.1,1.7-0.2,2.6-0.3
c0.7-0.1,1.9-0.2,2.6-0.3c0.5-0.1,1.1-0.1,1.6-0.2l1.1-0.1l0.5-0.1l0.6,0c0.9-0.1,1.7-0.1,2.6-0.2l1.3-0.1c0,0,0.5,0,0.1,0l0.3,0
l0.6,0c0.7,0,1.5-0.1,2.2-0.1c2.9-0.1,5.9-0.1,8.8,0c5.8,0.2,11.5,0.9,17,1.9c11.1,2.1,21.5,5.6,31,10.3
c9.5,4.6,17.9,10.3,25.3,16.5c0.5,0.4,0.9,0.8,1.4,1.2c0.4,0.4,0.9,0.8,1.3,1.2c0.9,0.8,1.7,1.6,2.6,2.4c0.9,0.8,1.7,1.6,2.5,2.4
c0.8,0.8,1.6,1.6,2.4,2.5c3.1,3.3,6,6.6,8.6,10c5.2,6.7,9.4,13.5,12.7,19.9c0.2,0.4,0.4,0.8,0.6,1.2c0.2,0.4,0.4,0.8,0.6,1.2
c0.4,0.8,0.8,1.6,1.1,2.4c0.4,0.8,0.7,1.5,1.1,2.3c0.3,0.8,0.7,1.5,1,2.3c1.2,3,2.4,5.9,3.3,8.6c1.5,4.4,2.6,8.3,3.5,11.7
c0.3,1.4,1.6,2.3,3,2.1c1.5-0.1,2.6-1.3,2.6-2.8C342.6,170.4,342.5,166.1,342,161.2z"/>
</svg>
import Datasource from './datasource';
export class LoggingConfigCtrl {
static templateUrl = 'partials/config.html';
}
export { Datasource, LoggingConfigCtrl as ConfigCtrl };
<datasource-http-settings current="ctrl.current" no-direct-access="true">
</datasource-http-settings>
\ No newline at end of file
{
"type": "datasource",
"name": "Grafana Logging",
"id": "logging",
"metrics": false,
"alerting": false,
"annotations": false,
"logs": true,
"explore": true,
"info": {
"description": "Grafana Logging Data Source for Grafana",
"author": {
"name": "Grafana Project",
"url": "https://grafana.com"
},
"logos": {
"small": "img/grafana_icon.svg",
"large": "img/grafana_icon.svg"
},
"links": [
{
"name": "Grafana Logging",
"url": "https://grafana.com/"
}
],
"version": "5.3.0"
}
}
\ No newline at end of file
import { LogLevel } from 'app/core/logs_model';
import { getLogLevel, getSearchMatches } from './result_transformer';
describe('getSearchMatches()', () => {
it('gets no matches for when search and or line are empty', () => {
expect(getSearchMatches('', '')).toEqual([]);
expect(getSearchMatches('foo', '')).toEqual([]);
expect(getSearchMatches('', 'foo')).toEqual([]);
});
it('gets no matches for unmatched search string', () => {
expect(getSearchMatches('foo', 'bar')).toEqual([]);
});
it('gets matches for matched search string', () => {
expect(getSearchMatches('foo', 'foo')).toEqual([{ length: 3, start: 0, text: 'foo' }]);
expect(getSearchMatches(' foo ', 'foo')).toEqual([{ length: 3, start: 1, text: 'foo' }]);
});
expect(getSearchMatches(' foo foo bar ', 'foo|bar')).toEqual([
{ length: 3, start: 1, text: 'foo' },
{ length: 3, start: 5, text: 'foo' },
{ length: 3, start: 9, text: 'bar' },
]);
});
describe('getLoglevel()', () => {
it('returns no log level on empty line', () => {
expect(getLogLevel('')).toBe(undefined);
});
it('returns no log level on when level is part of a word', () => {
expect(getLogLevel('this is a warning')).toBe(undefined);
});
it('returns log level on line contains a log level', () => {
expect(getLogLevel('warn: it is looking bad')).toBe(LogLevel.warn);
expect(getLogLevel('2007-12-12 12:12:12 [WARN]: it is looking bad')).toBe(LogLevel.warn);
});
it('returns first log level found', () => {
expect(getLogLevel('WARN this could be a debug message')).toBe(LogLevel.warn);
});
});
import _ from 'lodash';
import moment from 'moment';
import { LogLevel, LogsModel, LogRow } from 'app/core/logs_model';
export function getLogLevel(line: string): LogLevel {
if (!line) {
return undefined;
}
let level: LogLevel;
Object.keys(LogLevel).forEach(key => {
if (!level) {
const regexp = new RegExp(`\\b${key}\\b`, 'i');
if (regexp.test(line)) {
level = LogLevel[key];
}
}
});
return level;
}
export function getSearchMatches(line: string, search: string) {
// Empty search can send re.exec() into infinite loop, exit early
if (!line || !search) {
return [];
}
const regexp = new RegExp(`(?:${search})`, 'g');
const matches = [];
let match;
while ((match = regexp.exec(line))) {
matches.push({
text: match[0],
start: match.index,
length: match[0].length,
});
}
return matches;
}
export function processEntry(entry: { line: string; timestamp: string }, stream): LogRow {
const { line, timestamp } = entry;
const { labels } = stream;
const key = `EK${timestamp}${labels}`;
const time = moment(timestamp);
const timeFromNow = time.fromNow();
const timeLocal = time.format('YYYY-MM-DD HH:mm:ss');
const searchMatches = getSearchMatches(line, stream.search);
const logLevel = getLogLevel(line);
return {
key,
logLevel,
searchMatches,
timeFromNow,
timeLocal,
entry: line,
timestamp: timestamp,
};
}
export function processStreams(streams, limit?: number): LogsModel {
const combinedEntries = streams.reduce((acc, stream) => {
return [...acc, ...stream.entries.map(entry => processEntry(entry, stream))];
}, []);
const sortedEntries = _.chain(combinedEntries)
.sortBy('timestamp')
.reverse()
.slice(0, limit || combinedEntries.length)
.value();
return { rows: sortedEntries };
}
...@@ -97,3 +97,40 @@ ...@@ -97,3 +97,40 @@
.query-row-tools { .query-row-tools {
width: 4rem; width: 4rem;
} }
.explore {
.logs {
.logs-entries {
display: grid;
grid-column-gap: 1rem;
grid-row-gap: 0.1rem;
grid-template-columns: 4px minmax(100px, max-content) 1fr;
font-family: $font-family-monospace;
}
.logs-row-match-highlight {
background-color: lighten($blue, 20%);
}
.logs-row-level {
background-color: transparent;
margin: 6px 0;
border-radius: 2px;
opacity: 0.8;
}
.logs-row-level-crit,
.logs-row-level-error,
.logs-row-level-err {
background-color: $red;
}
.logs-row-level-warn {
background-color: $orange;
}
.logs-row-level-info {
background-color: $green;
}
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment