mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Datasource for Grafana logging platform
- new builtin datasource plugin "Logging" (likely going to be renamed) - plugin implements no panel ctrls yet, only ships datasource - new models for logging data as first class citizen (aside from table and time_series model) - Logs as new view for Explore - JSON view for development Testable only against existing logish deployment. Then test with queries like `{job="..."} regexp`.
This commit is contained in:
@@ -17,12 +17,14 @@ import (
|
||||
plugin "github.com/hashicorp/go-plugin"
|
||||
)
|
||||
|
||||
// DataSourcePlugin contains all metadata about a datasource plugin
|
||||
type DataSourcePlugin struct {
|
||||
FrontendPluginBase
|
||||
Annotations bool `json:"annotations"`
|
||||
Metrics bool `json:"metrics"`
|
||||
Alerting bool `json:"alerting"`
|
||||
Explore bool `json:"explore"`
|
||||
Logs bool `json:"logs"`
|
||||
QueryOptions map[string]bool `json:"queryOptions,omitempty"`
|
||||
BuiltIn bool `json:"builtIn,omitempty"`
|
||||
Mixed bool `json:"mixed,omitempty"`
|
||||
|
@@ -11,6 +11,7 @@ import { parse as parseDate } from 'app/core/utils/datemath';
|
||||
import ElapsedTime from './ElapsedTime';
|
||||
import QueryRows from './QueryRows';
|
||||
import Graph from './Graph';
|
||||
import Logs from './Logs';
|
||||
import Table from './Table';
|
||||
import TimePicker, { DEFAULT_RANGE } from './TimePicker';
|
||||
import { ensureQueries, generateQueryKey, hasQuery } from './utils/query';
|
||||
@@ -58,12 +59,17 @@ interface IExploreState {
|
||||
initialDatasource?: string;
|
||||
latency: number;
|
||||
loading: any;
|
||||
logsResult: any;
|
||||
queries: any;
|
||||
queryError: any;
|
||||
range: any;
|
||||
requestOptions: any;
|
||||
showingGraph: boolean;
|
||||
showingLogs: boolean;
|
||||
showingTable: boolean;
|
||||
supportsGraph: boolean | null;
|
||||
supportsLogs: boolean | null;
|
||||
supportsTable: boolean | null;
|
||||
tableResult: any;
|
||||
}
|
||||
|
||||
@@ -82,12 +88,17 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
initialDatasource: datasource,
|
||||
latency: 0,
|
||||
loading: false,
|
||||
logsResult: null,
|
||||
queries: ensureQueries(queries),
|
||||
queryError: null,
|
||||
range: range || { ...DEFAULT_RANGE },
|
||||
requestOptions: null,
|
||||
showingGraph: true,
|
||||
showingLogs: true,
|
||||
showingTable: true,
|
||||
supportsGraph: null,
|
||||
supportsLogs: null,
|
||||
supportsTable: null,
|
||||
tableResult: null,
|
||||
...props.initialState,
|
||||
};
|
||||
@@ -124,17 +135,29 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
}
|
||||
|
||||
async setDatasource(datasource) {
|
||||
const supportsGraph = datasource.meta.metrics;
|
||||
const supportsLogs = datasource.meta.logs;
|
||||
const supportsTable = datasource.meta.metrics;
|
||||
let datasourceError = null;
|
||||
|
||||
try {
|
||||
const testResult = await datasource.testDatasource();
|
||||
if (testResult.status === 'success') {
|
||||
this.setState({ datasource, datasourceError: null, datasourceLoading: false }, () => this.handleSubmit());
|
||||
} else {
|
||||
this.setState({ datasource: datasource, datasourceError: testResult.message, datasourceLoading: false });
|
||||
}
|
||||
datasourceError = testResult.status === 'success' ? null : testResult.message;
|
||||
} catch (error) {
|
||||
const message = (error && error.statusText) || error;
|
||||
this.setState({ datasource: datasource, datasourceError: message, datasourceLoading: false });
|
||||
datasourceError = (error && error.statusText) || error;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
{
|
||||
datasource,
|
||||
datasourceError,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
datasourceLoading: false,
|
||||
},
|
||||
() => datasourceError === null && this.handleSubmit()
|
||||
);
|
||||
}
|
||||
|
||||
getRef = el => {
|
||||
@@ -157,6 +180,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
datasourceError: null,
|
||||
datasourceLoading: true,
|
||||
graphResult: null,
|
||||
logsResult: null,
|
||||
tableResult: null,
|
||||
});
|
||||
const datasource = await this.props.datasourceSrv.get(option.value);
|
||||
@@ -193,6 +217,10 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
this.setState(state => ({ showingGraph: !state.showingGraph }));
|
||||
};
|
||||
|
||||
handleClickLogsButton = () => {
|
||||
this.setState(state => ({ showingLogs: !state.showingLogs }));
|
||||
};
|
||||
|
||||
handleClickSplit = () => {
|
||||
const { onChangeSplit } = this.props;
|
||||
if (onChangeSplit) {
|
||||
@@ -214,16 +242,19 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
};
|
||||
|
||||
handleSubmit = () => {
|
||||
const { showingGraph, showingTable } = this.state;
|
||||
if (showingTable) {
|
||||
const { showingLogs, showingGraph, showingTable, supportsGraph, supportsLogs, supportsTable } = this.state;
|
||||
if (showingTable && supportsTable) {
|
||||
this.runTableQuery();
|
||||
}
|
||||
if (showingGraph) {
|
||||
if (showingGraph && supportsGraph) {
|
||||
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 resolution = this.el.offsetWidth;
|
||||
const absoluteRange = {
|
||||
@@ -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 => {
|
||||
const { datasource } = this.state;
|
||||
return datasource.metadataRequest(url);
|
||||
@@ -300,17 +354,23 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
graphResult,
|
||||
latency,
|
||||
loading,
|
||||
logsResult,
|
||||
queries,
|
||||
queryError,
|
||||
range,
|
||||
requestOptions,
|
||||
showingGraph,
|
||||
showingLogs,
|
||||
showingTable,
|
||||
supportsGraph,
|
||||
supportsLogs,
|
||||
supportsTable,
|
||||
tableResult,
|
||||
} = this.state;
|
||||
const showingBoth = showingGraph && showingTable;
|
||||
const graphHeight = showingBoth ? '200px' : '400px';
|
||||
const graphButtonActive = showingBoth || showingGraph ? 'active' : '';
|
||||
const logsButtonActive = showingLogs ? 'active' : '';
|
||||
const tableButtonActive = showingBoth || showingTable ? 'active' : '';
|
||||
const exploreClass = split ? 'explore explore-split' : 'explore';
|
||||
const datasources = datasourceSrv.getExploreSources().map(ds => ({
|
||||
@@ -357,12 +417,21 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
</div>
|
||||
) : null}
|
||||
<div className="navbar-buttons">
|
||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
<button className={`btn navbar-button ${tableButtonActive}`} onClick={this.handleClickTableButton}>
|
||||
Table
|
||||
</button>
|
||||
{supportsGraph ? (
|
||||
<button className={`btn navbar-button ${graphButtonActive}`} onClick={this.handleClickGraphButton}>
|
||||
Graph
|
||||
</button>
|
||||
) : null}
|
||||
{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>
|
||||
<TimePicker range={range} onChangeTime={this.handleChangeTime} />
|
||||
<div className="navbar-buttons relative">
|
||||
@@ -395,7 +464,7 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
/>
|
||||
{queryError ? <div className="text-warning m-a-2">{queryError}</div> : null}
|
||||
<main className="m-t-2">
|
||||
{showingGraph ? (
|
||||
{supportsGraph && showingGraph ? (
|
||||
<Graph
|
||||
data={graphResult}
|
||||
id={`explore-graph-${position}`}
|
||||
@@ -404,7 +473,8 @@ export class Explore extends React.Component<any, IExploreState> {
|
||||
split={split}
|
||||
/>
|
||||
) : 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>
|
||||
</div>
|
||||
) : null}
|
||||
|
9
public/app/containers/Explore/JSONViewer.tsx
Normal file
9
public/app/containers/Explore/JSONViewer.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
|
||||
export default function({ value }) {
|
||||
return (
|
||||
<div>
|
||||
<pre>{JSON.stringify(value, undefined, 2)}</pre>
|
||||
</div>
|
||||
);
|
||||
}
|
66
public/app/containers/Explore/Logs.tsx
Normal file
66
public/app/containers/Explore/Logs.tsx
Normal file
@@ -0,0 +1,66 @@
|
||||
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> {
|
||||
const url = `/api/v1/label/${key}/values`;
|
||||
try {
|
||||
const res = await this.request(url);
|
||||
console.log(res);
|
||||
const body = await (res.data || res.json());
|
||||
const pairs = this.state.labelValues[EMPTY_METRIC];
|
||||
const values = {
|
||||
|
29
public/app/core/logs_model.ts
Normal file
29
public/app/core/logs_model.ts
Normal file
@@ -0,0 +1,29 @@
|
||||
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
|
||||
import * as opentsdbPlugin from 'app/plugins/datasource/opentsdb/module';
|
||||
import * as grafanaPlugin from 'app/plugins/datasource/grafana/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 mysqlPlugin from 'app/plugins/datasource/mysql/module';
|
||||
import * as postgresPlugin from 'app/plugins/datasource/postgres/module';
|
||||
@@ -28,6 +29,7 @@ const builtInPlugins = {
|
||||
'app/plugins/datasource/opentsdb/module': opentsdbPlugin,
|
||||
'app/plugins/datasource/grafana/module': grafanaPlugin,
|
||||
'app/plugins/datasource/influxdb/module': influxdbPlugin,
|
||||
'app/plugins/datasource/logging/module': loggingPlugin,
|
||||
'app/plugins/datasource/mixed/module': mixedPlugin,
|
||||
'app/plugins/datasource/mysql/module': mysqlPlugin,
|
||||
'app/plugins/datasource/postgres/module': postgresPlugin,
|
||||
|
3
public/app/plugins/datasource/logging/README.md
Normal file
3
public/app/plugins/datasource/logging/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# Grafana Logging Datasource - Native Plugin
|
||||
|
||||
This is a **built in** datasource that allows you to connect to Grafana's logging service.
|
38
public/app/plugins/datasource/logging/datasource.jest.ts
Normal file
38
public/app/plugins/datasource/logging/datasource.jest.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
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',
|
||||
});
|
||||
});
|
||||
});
|
134
public/app/plugins/datasource/logging/datasource.ts
Normal file
134
public/app/plugins/datasource/logging/datasource.ts
Normal file
@@ -0,0 +1,134 @@
|
||||
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 };
|
||||
});
|
||||
}
|
||||
}
|
57
public/app/plugins/datasource/logging/img/grafana_icon.svg
Normal file
57
public/app/plugins/datasource/logging/img/grafana_icon.svg
Normal file
@@ -0,0 +1,57 @@
|
||||
<?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>
|
After Width: | Height: | Size: 5.6 KiB |
7
public/app/plugins/datasource/logging/module.ts
Normal file
7
public/app/plugins/datasource/logging/module.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import Datasource from './datasource';
|
||||
|
||||
export class LoggingConfigCtrl {
|
||||
static templateUrl = 'partials/config.html';
|
||||
}
|
||||
|
||||
export { Datasource, LoggingConfigCtrl as ConfigCtrl };
|
@@ -0,0 +1,2 @@
|
||||
<datasource-http-settings current="ctrl.current" no-direct-access="true">
|
||||
</datasource-http-settings>
|
28
public/app/plugins/datasource/logging/plugin.json
Normal file
28
public/app/plugins/datasource/logging/plugin.json
Normal file
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"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"
|
||||
}
|
||||
}
|
@@ -0,0 +1,45 @@
|
||||
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);
|
||||
});
|
||||
});
|
71
public/app/plugins/datasource/logging/result_transformer.ts
Normal file
71
public/app/plugins/datasource/logging/result_transformer.ts
Normal file
@@ -0,0 +1,71 @@
|
||||
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 @@
|
||||
.query-row-tools {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user