Skip to content

Commit

Permalink
Merge pull request grafana#12675 from grafana/davkal/logging-datasource
Browse files Browse the repository at this point in the history
Datasource for Grafana logging platform
  • Loading branch information
davkal authored Jul 23, 2018
2 parents 6b07105 + 3297ae4 commit 4d722b2
Show file tree
Hide file tree
Showing 17 changed files with 620 additions and 19 deletions.
2 changes: 2 additions & 0 deletions pkg/plugins/datasource_plugin.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"`
Expand Down
108 changes: 89 additions & 19 deletions public/app/containers/Explore/Explore.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down Expand Up @@ -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;
}

Expand All @@ -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,
};
Expand Down Expand Up @@ -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 => {
Expand All @@ -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);
Expand Down Expand Up @@ -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) {
Expand All @@ -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 = {
Expand Down Expand Up @@ -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);
Expand All @@ -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 => ({
Expand Down Expand Up @@ -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">
Expand Down Expand Up @@ -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}`}
Expand All @@ -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}
Expand Down
9 changes: 9 additions & 0 deletions public/app/containers/Explore/JSONViewer.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React from 'react';

export default function({ value }) {
return (
<div>
<pre>{JSON.stringify(value, undefined, 2)}</pre>
</div>
);
}
66 changes: 66 additions & 0 deletions public/app/containers/Explore/Logs.tsx
Original file line number Diff line number Diff line change
@@ -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>
);
}
}
1 change: 1 addition & 0 deletions public/app/containers/Explore/QueryField.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand Down
29 changes: 29 additions & 0 deletions public/app/core/logs_model.ts
Original file line number Diff line number Diff line change
@@ -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[];
}
2 changes: 2 additions & 0 deletions public/app/features/plugins/built_in_plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions public/app/plugins/datasource/logging/README.md
Original file line number Diff line number Diff line change
@@ -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.
Loading

0 comments on commit 4d722b2

Please sign in to comment.