mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Elastic: Add data links in datasource config (#20186)
This commit is contained in:
@@ -202,6 +202,7 @@ export function dataFrameToLogsModel(dataFrame: DataFrame[], intervalMs: number,
|
|||||||
// Create metrics from logs
|
// Create metrics from logs
|
||||||
logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs, timeZone);
|
logsModel.series = makeSeriesForLogs(logsModel.rows, intervalMs, timeZone);
|
||||||
} else {
|
} else {
|
||||||
|
// We got metrics in the dataFrame so process those
|
||||||
logsModel.series = getGraphSeriesModel(
|
logsModel.series = getGraphSeriesModel(
|
||||||
metricSeries,
|
metricSeries,
|
||||||
timeZone,
|
timeZone,
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { DataSourcePluginOptionsEditorProps } from '@grafana/data';
|
|||||||
import { ElasticsearchOptions } from '../types';
|
import { ElasticsearchOptions } from '../types';
|
||||||
import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails';
|
import { defaultMaxConcurrentShardRequests, ElasticDetails } from './ElasticDetails';
|
||||||
import { LogsConfig } from './LogsConfig';
|
import { LogsConfig } from './LogsConfig';
|
||||||
|
import { DataLinks } from './DataLinks';
|
||||||
|
|
||||||
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
|
export type Props = DataSourcePluginOptionsEditorProps<ElasticsearchOptions>;
|
||||||
export const ConfigEditor = (props: Props) => {
|
export const ConfigEditor = (props: Props) => {
|
||||||
@@ -46,6 +47,19 @@ export const ConfigEditor = (props: Props) => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<DataLinks
|
||||||
|
value={options.jsonData.dataLinks}
|
||||||
|
onChange={newValue => {
|
||||||
|
onOptionsChange({
|
||||||
|
...options,
|
||||||
|
jsonData: {
|
||||||
|
...options.jsonData,
|
||||||
|
dataLinks: newValue,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { Button, FormField, VariableSuggestion, DataLinkInput, stylesFactory } from '@grafana/ui';
|
||||||
|
import { DataLinkConfig } from '../types';
|
||||||
|
|
||||||
|
const getStyles = stylesFactory(() => ({
|
||||||
|
firstRow: css`
|
||||||
|
display: flex;
|
||||||
|
`,
|
||||||
|
nameField: css`
|
||||||
|
flex: 2;
|
||||||
|
`,
|
||||||
|
regexField: css`
|
||||||
|
flex: 3;
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value: DataLinkConfig;
|
||||||
|
onChange: (value: DataLinkConfig) => void;
|
||||||
|
onDelete: () => void;
|
||||||
|
suggestions: VariableSuggestion[];
|
||||||
|
className?: string;
|
||||||
|
};
|
||||||
|
export const DataLink = (props: Props) => {
|
||||||
|
const { value, onChange, onDelete, suggestions, className } = props;
|
||||||
|
const styles = getStyles();
|
||||||
|
|
||||||
|
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
[field]: event.currentTarget.value,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={className}>
|
||||||
|
<div className={styles.firstRow}>
|
||||||
|
<FormField
|
||||||
|
className={styles.nameField}
|
||||||
|
labelWidth={6}
|
||||||
|
// A bit of a hack to prevent using default value for the width from FormField
|
||||||
|
inputWidth={null}
|
||||||
|
label="Field"
|
||||||
|
type="text"
|
||||||
|
value={value.field}
|
||||||
|
tooltip={'Can be exact field name or a regex pattern that will match on the field name.'}
|
||||||
|
onChange={handleChange('field')}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
variant={'inverse'}
|
||||||
|
title="Remove field"
|
||||||
|
icon={'fa fa-times'}
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
onDelete();
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<FormField
|
||||||
|
label="URL"
|
||||||
|
labelWidth={6}
|
||||||
|
inputEl={
|
||||||
|
<DataLinkInput
|
||||||
|
placeholder={'http://example.com/${__value.raw}'}
|
||||||
|
value={value.url || ''}
|
||||||
|
onChange={newValue =>
|
||||||
|
onChange({
|
||||||
|
...value,
|
||||||
|
url: newValue,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
suggestions={suggestions}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
className={css`
|
||||||
|
width: 100%;
|
||||||
|
`}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { mount } from 'enzyme';
|
||||||
|
import { DataLinks } from './DataLinks';
|
||||||
|
import { Button } from '@grafana/ui';
|
||||||
|
import { DataLink } from './DataLink';
|
||||||
|
|
||||||
|
describe('DataLinks', () => {
|
||||||
|
let originalGetSelection: typeof window.getSelection;
|
||||||
|
beforeAll(() => {
|
||||||
|
originalGetSelection = window.getSelection;
|
||||||
|
window.getSelection = () => null;
|
||||||
|
});
|
||||||
|
|
||||||
|
afterAll(() => {
|
||||||
|
window.getSelection = originalGetSelection;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly when no fields', () => {
|
||||||
|
const wrapper = mount(<DataLinks onChange={() => {}} />);
|
||||||
|
expect(wrapper.find(Button).length).toBe(1);
|
||||||
|
expect(wrapper.find(Button).contains('Add')).toBeTruthy();
|
||||||
|
expect(wrapper.find(DataLink).length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders correctly when there are fields', () => {
|
||||||
|
const wrapper = mount(<DataLinks value={testValue} onChange={() => {}} />);
|
||||||
|
|
||||||
|
expect(wrapper.find(Button).filterWhere(button => button.contains('Add')).length).toBe(1);
|
||||||
|
expect(wrapper.find(DataLink).length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('adds new field', () => {
|
||||||
|
const onChangeMock = jest.fn();
|
||||||
|
const wrapper = mount(<DataLinks onChange={onChangeMock} />);
|
||||||
|
const addButton = wrapper.find(Button).filterWhere(button => button.contains('Add'));
|
||||||
|
addButton.simulate('click');
|
||||||
|
expect(onChangeMock.mock.calls[0][0].length).toBe(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('removes field', () => {
|
||||||
|
const onChangeMock = jest.fn();
|
||||||
|
const wrapper = mount(<DataLinks value={testValue} onChange={onChangeMock} />);
|
||||||
|
const removeButton = wrapper
|
||||||
|
.find(DataLink)
|
||||||
|
.at(0)
|
||||||
|
.find(Button);
|
||||||
|
removeButton.simulate('click');
|
||||||
|
const newValue = onChangeMock.mock.calls[0][0];
|
||||||
|
expect(newValue.length).toBe(1);
|
||||||
|
expect(newValue[0]).toMatchObject({
|
||||||
|
field: 'regex2',
|
||||||
|
url: 'localhost2',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const testValue = [
|
||||||
|
{
|
||||||
|
field: 'regex1',
|
||||||
|
url: 'localhost1',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
field: 'regex2',
|
||||||
|
url: 'localhost2',
|
||||||
|
},
|
||||||
|
];
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
import React from 'react';
|
||||||
|
import { css } from 'emotion';
|
||||||
|
import { Button, DataLinkBuiltInVars, stylesFactory, useTheme, VariableOrigin } from '@grafana/ui';
|
||||||
|
import { GrafanaTheme } from '@grafana/data';
|
||||||
|
import { DataLinkConfig } from '../types';
|
||||||
|
import { DataLink } from './DataLink';
|
||||||
|
|
||||||
|
const getStyles = stylesFactory((theme: GrafanaTheme) => ({
|
||||||
|
infoText: css`
|
||||||
|
padding-bottom: ${theme.spacing.md};
|
||||||
|
color: ${theme.colors.textWeak};
|
||||||
|
`,
|
||||||
|
dataLink: css`
|
||||||
|
margin-bottom: ${theme.spacing.sm};
|
||||||
|
`,
|
||||||
|
}));
|
||||||
|
|
||||||
|
type Props = {
|
||||||
|
value?: DataLinkConfig[];
|
||||||
|
onChange: (value: DataLinkConfig[]) => void;
|
||||||
|
};
|
||||||
|
export const DataLinks = (props: Props) => {
|
||||||
|
const { value, onChange } = props;
|
||||||
|
const theme = useTheme();
|
||||||
|
const styles = getStyles(theme);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<h3 className="page-heading">Data links</h3>
|
||||||
|
|
||||||
|
<div className={styles.infoText}>
|
||||||
|
Add links to existing fields. Links will be shown in log row details next to the field value.
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="gf-form-group">
|
||||||
|
{value &&
|
||||||
|
value.map((field, index) => {
|
||||||
|
return (
|
||||||
|
<DataLink
|
||||||
|
className={styles.dataLink}
|
||||||
|
key={index}
|
||||||
|
value={field}
|
||||||
|
onChange={newField => {
|
||||||
|
const newDataLinks = [...value];
|
||||||
|
newDataLinks.splice(index, 1, newField);
|
||||||
|
onChange(newDataLinks);
|
||||||
|
}}
|
||||||
|
onDelete={() => {
|
||||||
|
const newDataLinks = [...value];
|
||||||
|
newDataLinks.splice(index, 1);
|
||||||
|
onChange(newDataLinks);
|
||||||
|
}}
|
||||||
|
suggestions={[
|
||||||
|
{
|
||||||
|
value: DataLinkBuiltInVars.valueRaw,
|
||||||
|
label: 'Raw value',
|
||||||
|
documentation: 'Raw value of the field',
|
||||||
|
origin: VariableOrigin.Value,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
<div>
|
||||||
|
<Button
|
||||||
|
variant={'inverse'}
|
||||||
|
className={css`
|
||||||
|
margin-right: 10px;
|
||||||
|
`}
|
||||||
|
icon="fa fa-plus"
|
||||||
|
onClick={event => {
|
||||||
|
event.preventDefault();
|
||||||
|
const newDataLinks = [...(value || []), { field: '', url: '' }];
|
||||||
|
onChange(newDataLinks);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Add
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,13 +1,13 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import { dateMath } from '@grafana/data';
|
import { dateMath, Field } from '@grafana/data';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { ElasticDatasource } from '../datasource';
|
import { ElasticDatasource } from './datasource';
|
||||||
import { toUtc, dateTime } from '@grafana/data';
|
import { toUtc, dateTime } from '@grafana/data';
|
||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { DataSourceInstanceSettings } from '@grafana/data';
|
import { DataSourceInstanceSettings } from '@grafana/data';
|
||||||
import { ElasticsearchOptions } from '../types';
|
import { ElasticsearchOptions } from './types';
|
||||||
|
|
||||||
describe('ElasticDatasource', function(this: any) {
|
describe('ElasticDatasource', function(this: any) {
|
||||||
const backendSrv: any = {
|
const backendSrv: any = {
|
||||||
@@ -153,73 +153,23 @@ describe('ElasticDatasource', function(this: any) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('When issuing logs query with interval pattern', () => {
|
describe('When issuing logs query with interval pattern', () => {
|
||||||
let query, queryBuilderSpy: any;
|
async function setupDataSource(jsonData?: Partial<ElasticsearchOptions>) {
|
||||||
|
|
||||||
beforeEach(async () => {
|
|
||||||
createDatasource({
|
createDatasource({
|
||||||
url: 'http://es.com',
|
url: 'http://es.com',
|
||||||
database: 'mock-index',
|
database: 'mock-index',
|
||||||
jsonData: { interval: 'Daily', esVersion: 2, timeField: '@timestamp' } as ElasticsearchOptions,
|
jsonData: {
|
||||||
|
interval: 'Daily',
|
||||||
|
esVersion: 2,
|
||||||
|
timeField: '@timestamp',
|
||||||
|
...(jsonData || {}),
|
||||||
|
} as ElasticsearchOptions,
|
||||||
} as DataSourceInstanceSettings<ElasticsearchOptions>);
|
} as DataSourceInstanceSettings<ElasticsearchOptions>);
|
||||||
|
|
||||||
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
ctx.backendSrv.datasourceRequest = jest.fn(options => {
|
||||||
return Promise.resolve({
|
return Promise.resolve(logsResponse);
|
||||||
data: {
|
|
||||||
responses: [
|
|
||||||
{
|
|
||||||
aggregations: {
|
|
||||||
'2': {
|
|
||||||
buckets: [
|
|
||||||
{
|
|
||||||
doc_count: 10,
|
|
||||||
key: 1000,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
doc_count: 15,
|
|
||||||
key: 2000,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
hits: {
|
|
||||||
hits: [
|
|
||||||
{
|
|
||||||
'@timestamp': ['2019-06-24T09:51:19.765Z'],
|
|
||||||
_id: 'fdsfs',
|
|
||||||
_type: '_doc',
|
|
||||||
_index: 'mock-index',
|
|
||||||
_source: {
|
|
||||||
'@timestamp': '2019-06-24T09:51:19.765Z',
|
|
||||||
host: 'djisaodjsoad',
|
|
||||||
message: 'hello, i am a message',
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
'@timestamp': ['2019-06-24T09:51:19.765Z'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'@timestamp': ['2019-06-24T09:52:19.765Z'],
|
|
||||||
_id: 'kdospaidopa',
|
|
||||||
_type: '_doc',
|
|
||||||
_index: 'mock-index',
|
|
||||||
_source: {
|
|
||||||
'@timestamp': '2019-06-24T09:52:19.765Z',
|
|
||||||
host: 'dsalkdakdop',
|
|
||||||
message: 'hello, i am also message',
|
|
||||||
},
|
|
||||||
fields: {
|
|
||||||
'@timestamp': ['2019-06-24T09:52:19.765Z'],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
query = {
|
const query = {
|
||||||
range: {
|
range: {
|
||||||
from: toUtc([2015, 4, 30, 10]),
|
from: toUtc([2015, 4, 30, 10]),
|
||||||
to: toUtc([2019, 7, 1, 10]),
|
to: toUtc([2019, 7, 1, 10]),
|
||||||
@@ -238,12 +188,30 @@ describe('ElasticDatasource', function(this: any) {
|
|||||||
],
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
queryBuilderSpy = jest.spyOn(ctx.ds.queryBuilder, 'getLogsQuery');
|
const queryBuilderSpy = jest.spyOn(ctx.ds.queryBuilder, 'getLogsQuery');
|
||||||
await ctx.ds.query(query);
|
const response = await ctx.ds.query(query);
|
||||||
|
return { queryBuilderSpy, response };
|
||||||
|
}
|
||||||
|
|
||||||
|
it('should call getLogsQuery()', async () => {
|
||||||
|
const { queryBuilderSpy } = await setupDataSource();
|
||||||
|
expect(queryBuilderSpy).toHaveBeenCalled();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should call getLogsQuery()', () => {
|
it('should enhance fields with links', async () => {
|
||||||
expect(queryBuilderSpy).toHaveBeenCalled();
|
const { response } = await setupDataSource({
|
||||||
|
dataLinks: [
|
||||||
|
{
|
||||||
|
field: 'host',
|
||||||
|
url: 'http://localhost:3000/${__value.raw}',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
// 1 for logs and 1 for counts.
|
||||||
|
expect(response.data.length).toBe(2);
|
||||||
|
const links = response.data[0].fields.find((field: Field) => field.name === 'host').config.links;
|
||||||
|
expect(links.length).toBe(1);
|
||||||
|
expect(links[0].url).toBe('http://localhost:3000/${__value.raw}');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -645,3 +613,58 @@ describe('ElasticDatasource', function(this: any) {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const logsResponse = {
|
||||||
|
data: {
|
||||||
|
responses: [
|
||||||
|
{
|
||||||
|
aggregations: {
|
||||||
|
'2': {
|
||||||
|
buckets: [
|
||||||
|
{
|
||||||
|
doc_count: 10,
|
||||||
|
key: 1000,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
doc_count: 15,
|
||||||
|
key: 2000,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
hits: {
|
||||||
|
hits: [
|
||||||
|
{
|
||||||
|
'@timestamp': ['2019-06-24T09:51:19.765Z'],
|
||||||
|
_id: 'fdsfs',
|
||||||
|
_type: '_doc',
|
||||||
|
_index: 'mock-index',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2019-06-24T09:51:19.765Z',
|
||||||
|
host: 'djisaodjsoad',
|
||||||
|
message: 'hello, i am a message',
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
'@timestamp': ['2019-06-24T09:51:19.765Z'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
'@timestamp': ['2019-06-24T09:52:19.765Z'],
|
||||||
|
_id: 'kdospaidopa',
|
||||||
|
_type: '_doc',
|
||||||
|
_index: 'mock-index',
|
||||||
|
_source: {
|
||||||
|
'@timestamp': '2019-06-24T09:52:19.765Z',
|
||||||
|
host: 'dsalkdakdop',
|
||||||
|
message: 'hello, i am also message',
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
'@timestamp': ['2019-06-24T09:52:19.765Z'],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,6 +1,12 @@
|
|||||||
import angular from 'angular';
|
import angular from 'angular';
|
||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import { DataSourceApi, DataSourceInstanceSettings, DataQueryRequest, DataQueryResponse } from '@grafana/data';
|
import {
|
||||||
|
DataSourceApi,
|
||||||
|
DataSourceInstanceSettings,
|
||||||
|
DataQueryRequest,
|
||||||
|
DataQueryResponse,
|
||||||
|
DataFrame,
|
||||||
|
} from '@grafana/data';
|
||||||
import { ElasticResponse } from './elastic_response';
|
import { ElasticResponse } from './elastic_response';
|
||||||
import { IndexPattern } from './index_pattern';
|
import { IndexPattern } from './index_pattern';
|
||||||
import { ElasticQueryBuilder } from './query_builder';
|
import { ElasticQueryBuilder } from './query_builder';
|
||||||
@@ -9,7 +15,7 @@ import * as queryDef from './query_def';
|
|||||||
import { BackendSrv } from 'app/core/services/backend_srv';
|
import { BackendSrv } from 'app/core/services/backend_srv';
|
||||||
import { TemplateSrv } from 'app/features/templating/template_srv';
|
import { TemplateSrv } from 'app/features/templating/template_srv';
|
||||||
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
|
||||||
import { ElasticsearchOptions, ElasticsearchQuery } from './types';
|
import { DataLinkConfig, ElasticsearchOptions, ElasticsearchQuery } from './types';
|
||||||
|
|
||||||
export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, ElasticsearchOptions> {
|
export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, ElasticsearchOptions> {
|
||||||
basicAuth: string;
|
basicAuth: string;
|
||||||
@@ -25,6 +31,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
indexPattern: IndexPattern;
|
indexPattern: IndexPattern;
|
||||||
logMessageField?: string;
|
logMessageField?: string;
|
||||||
logLevelField?: string;
|
logLevelField?: string;
|
||||||
|
dataLinks: DataLinkConfig[];
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(
|
constructor(
|
||||||
@@ -52,6 +59,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
});
|
});
|
||||||
this.logMessageField = settingsData.logMessageField || '';
|
this.logMessageField = settingsData.logMessageField || '';
|
||||||
this.logLevelField = settingsData.logLevelField || '';
|
this.logLevelField = settingsData.logLevelField || '';
|
||||||
|
this.dataLinks = settingsData.dataLinks || [];
|
||||||
|
|
||||||
if (this.logMessageField === '') {
|
if (this.logMessageField === '') {
|
||||||
this.logMessageField = null;
|
this.logMessageField = null;
|
||||||
@@ -369,7 +377,11 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
return this.post(url, payload).then((res: any) => {
|
return this.post(url, payload).then((res: any) => {
|
||||||
const er = new ElasticResponse(sentTargets, res);
|
const er = new ElasticResponse(sentTargets, res);
|
||||||
if (sentTargets.some(target => target.isLogsQuery)) {
|
if (sentTargets.some(target => target.isLogsQuery)) {
|
||||||
return er.getLogs(this.logMessageField, this.logLevelField);
|
const response = er.getLogs(this.logMessageField, this.logLevelField);
|
||||||
|
for (const dataFrame of response.data) {
|
||||||
|
this.enhanceDataFrame(dataFrame);
|
||||||
|
}
|
||||||
|
return response;
|
||||||
}
|
}
|
||||||
|
|
||||||
return er.getTimeSeries();
|
return er.getTimeSeries();
|
||||||
@@ -547,6 +559,24 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enhanceDataFrame(dataFrame: DataFrame) {
|
||||||
|
if (this.dataLinks.length) {
|
||||||
|
for (const field of dataFrame.fields) {
|
||||||
|
const dataLink = this.dataLinks.find(dataLink => field.name && field.name.match(dataLink.field));
|
||||||
|
if (dataLink) {
|
||||||
|
field.config = field.config || {};
|
||||||
|
field.config.links = [
|
||||||
|
...(field.config.links || []),
|
||||||
|
{
|
||||||
|
url: dataLink.url,
|
||||||
|
title: '',
|
||||||
|
},
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private isPrimitive(obj: any) {
|
private isPrimitive(obj: any) {
|
||||||
if (obj === null || obj === undefined) {
|
if (obj === null || obj === undefined) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ export interface ElasticsearchOptions extends DataSourceJsonData {
|
|||||||
maxConcurrentShardRequests?: number;
|
maxConcurrentShardRequests?: number;
|
||||||
logMessageField?: string;
|
logMessageField?: string;
|
||||||
logLevelField?: string;
|
logLevelField?: string;
|
||||||
|
dataLinks?: DataLinkConfig[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ElasticsearchAggregation {
|
export interface ElasticsearchAggregation {
|
||||||
@@ -24,3 +25,8 @@ export interface ElasticsearchQuery extends DataQuery {
|
|||||||
bucketAggs?: ElasticsearchAggregation[];
|
bucketAggs?: ElasticsearchAggregation[];
|
||||||
metrics?: ElasticsearchAggregation[];
|
metrics?: ElasticsearchAggregation[];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type DataLinkConfig = {
|
||||||
|
field: string;
|
||||||
|
url: string;
|
||||||
|
};
|
||||||
|
|||||||
@@ -25,9 +25,7 @@
|
|||||||
"limit": 100,
|
"limit": 100,
|
||||||
"name": "Annotations & Alerts",
|
"name": "Annotations & Alerts",
|
||||||
"showIn": 0,
|
"showIn": 0,
|
||||||
"tags": [
|
"tags": ["metrictank"],
|
||||||
"metrictank"
|
|
||||||
],
|
|
||||||
"type": "tags"
|
"type": "tags"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
@@ -3258,11 +3256,11 @@
|
|||||||
"target": "groupByNodes(perSecond(metrictank.stats.$environment.$instance.idx.*.ops.*.counter32), 'sum', 5, 7)",
|
"target": "groupByNodes(perSecond(metrictank.stats.$environment.$instance.idx.*.ops.*.counter32), 'sum', 5, 7)",
|
||||||
"textEditor": false
|
"textEditor": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"refId": "B",
|
"refId": "B",
|
||||||
"target": "aliasByNode(sumSeriesWithWildcards(metrictank.stats.$environment.$instance.idx.*.add.values.rate32, 2, 3), 3, 4)",
|
"target": "aliasByNode(sumSeriesWithWildcards(metrictank.stats.$environment.$instance.idx.*.add.values.rate32, 2, 3), 3, 4)",
|
||||||
"textEditor": false
|
"textEditor": false
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"refId": "C",
|
"refId": "C",
|
||||||
"target": "aliasByNode(sumSeriesWithWildcards(metrictank.stats.$environment.$instance.idx.*.query-insert.exec.values.rate32, 2, 3), 3, 4)",
|
"target": "aliasByNode(sumSeriesWithWildcards(metrictank.stats.$environment.$instance.idx.*.query-insert.exec.values.rate32, 2, 3), 3, 4)",
|
||||||
@@ -4783,30 +4781,9 @@
|
|||||||
"enable": true,
|
"enable": true,
|
||||||
"notice": false,
|
"notice": false,
|
||||||
"now": true,
|
"now": true,
|
||||||
"refresh_intervals": [
|
"refresh_intervals": ["5s", "10s", "30s", "1m", "5m", "15m", "30m", "1h", "2h", "1d"],
|
||||||
"5s",
|
|
||||||
"10s",
|
|
||||||
"30s",
|
|
||||||
"1m",
|
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"30m",
|
|
||||||
"1h",
|
|
||||||
"2h",
|
|
||||||
"1d"
|
|
||||||
],
|
|
||||||
"status": "Stable",
|
"status": "Stable",
|
||||||
"time_options": [
|
"time_options": ["5m", "15m", "1h", "6h", "12h", "24h", "2d", "7d", "30d"],
|
||||||
"5m",
|
|
||||||
"15m",
|
|
||||||
"1h",
|
|
||||||
"6h",
|
|
||||||
"12h",
|
|
||||||
"24h",
|
|
||||||
"2d",
|
|
||||||
"7d",
|
|
||||||
"30d"
|
|
||||||
],
|
|
||||||
"type": "timepicker"
|
"type": "timepicker"
|
||||||
},
|
},
|
||||||
"timezone": "utc",
|
"timezone": "utc",
|
||||||
|
|||||||
@@ -5,8 +5,8 @@
|
|||||||
"category": "tsdb",
|
"category": "tsdb",
|
||||||
|
|
||||||
"includes": [
|
"includes": [
|
||||||
{ "type": "dashboard", "name": "Graphite Carbon Metrics", "path": "dashboards/carbon_metrics.json" },
|
{ "type": "dashboard", "name": "Graphite Carbon Metrics", "path": "dashboards/carbon_metrics.json" },
|
||||||
{ "type": "dashboard", "name": "Metrictank (Graphite alternative)", "path": "dashboards/metrictank.json" }
|
{ "type": "dashboard", "name": "Metrictank (Graphite alternative)", "path": "dashboards/metrictank.json" }
|
||||||
],
|
],
|
||||||
|
|
||||||
"metrics": true,
|
"metrics": true,
|
||||||
|
|||||||
Reference in New Issue
Block a user