Elastic: Internal data links (#25942)

* Allow internal datalinks for elastic

* Add docs

* Update docs/sources/features/datasources/elasticsearch.md

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

* Apply suggestions from code review

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>

Co-authored-by: Diana Payton <52059945+oddlittlebird@users.noreply.github.com>
This commit is contained in:
Andrej Ocenas 2020-07-01 09:45:21 +02:00 committed by GitHub
parent 1716b706da
commit 2ca6df814e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 188 additions and 26 deletions

View File

@ -91,6 +91,15 @@ For example, if you're using a default setup of Filebeat for shipping logs to El
- **Message field name:** message
- **Level field name:** fields.level
### Data links
Data links create a link from a specified field that can be accessed in logs view in Explore.
Each data link configuration consists of:
- **Field -** Name of the field used by the data link.
- **URL/query -** If the link is external, then enter the full link URL. If the link is internal link, then this input serves as query for the target data source. In both cases, you can interpolate the value from the field with `${__value.raw }` macro.
- **Internal link -** Select if the link is internal or external. In case of internal link, a data source selectorallows you to select the target data source. Only tracing data sources are supported.
## Metric Query editor
![Elasticsearch Query Editor](/img/docs/elasticsearch/query_editor.png)

View File

@ -1,9 +1,12 @@
import React from 'react';
import React, { Dispatch, SetStateAction, useEffect, useState } from 'react';
import { css } from 'emotion';
import { VariableSuggestion } from '@grafana/data';
import { DataSourceSelectItem, VariableSuggestion } from '@grafana/data';
import { Button, LegacyForms, DataLinkInput, stylesFactory } from '@grafana/ui';
const { FormField } = LegacyForms;
const { FormField, Switch } = LegacyForms;
import { DataLinkConfig } from '../types';
import { usePrevious } from 'react-use';
import { getDatasourceSrv } from '../../../../features/plugins/datasource_srv';
import DataSourcePicker from '../../../../core/components/Select/DataSourcePicker';
const getStyles = stylesFactory(() => ({
firstRow: css`
@ -15,6 +18,10 @@ const getStyles = stylesFactory(() => ({
regexField: css`
flex: 3;
`,
row: css`
display: flex;
align-items: baseline;
`,
}));
type Props = {
@ -27,6 +34,7 @@ type Props = {
export const DataLink = (props: Props) => {
const { value, onChange, onDelete, suggestions, className } = props;
const styles = getStyles();
const [showInternalLink, setShowInternalLink] = useInternalLink(value.datasourceUid);
const handleChange = (field: keyof typeof value) => (event: React.ChangeEvent<HTMLInputElement>) => {
onChange({
@ -61,11 +69,11 @@ export const DataLink = (props: Props) => {
</div>
<div className="gf-form">
<FormField
label="URL"
label={showInternalLink ? 'Query' : 'URL'}
labelWidth={6}
inputEl={
<DataLinkInput
placeholder={'http://example.com/${__value.raw}'}
placeholder={showInternalLink ? '${__value.raw}' : 'http://example.com/${__value.raw}'}
value={value.url || ''}
onChange={newValue =>
onChange({
@ -81,6 +89,82 @@ export const DataLink = (props: Props) => {
`}
/>
</div>
<div className={styles.row}>
<Switch
label="Internal link"
checked={showInternalLink}
onChange={() => {
if (showInternalLink) {
onChange({
...value,
datasourceUid: undefined,
});
}
setShowInternalLink(!showInternalLink);
}}
/>
{showInternalLink && (
<DataSourceSection
onChange={datasourceUid => {
onChange({
...value,
datasourceUid,
});
}}
datasourceUid={value.datasourceUid}
/>
)}
</div>
</div>
);
};
type DataSourceSectionProps = {
datasourceUid?: string;
onChange: (uid: string) => void;
};
const DataSourceSection = (props: DataSourceSectionProps) => {
const { datasourceUid, onChange } = props;
const datasources: DataSourceSelectItem[] = getDatasourceSrv()
.getExternal()
// At this moment only Jaeger and Zipkin datasource is supported as the link target.
.filter(ds => ds.meta.tracing)
.map(
ds =>
({
value: ds.uid,
name: ds.name,
meta: ds.meta,
} as DataSourceSelectItem)
);
let selectedDatasource = datasourceUid && datasources.find(d => d.value === datasourceUid);
return (
<DataSourcePicker
// Uid and value should be always set in the db and so in the items.
onChange={ds => onChange(ds.value!)}
datasources={datasources}
current={selectedDatasource || undefined}
/>
);
};
function useInternalLink(datasourceUid: string): [boolean, Dispatch<SetStateAction<boolean>>] {
const [showInternalLink, setShowInternalLink] = useState<boolean>(!!datasourceUid);
const previousUid = usePrevious(datasourceUid);
// Force internal link visibility change if uid changed outside of this component.
useEffect(() => {
if (!previousUid && datasourceUid && !showInternalLink) {
setShowInternalLink(true);
}
if (previousUid && !datasourceUid && showInternalLink) {
setShowInternalLink(false);
}
}, [previousUid, datasourceUid, showInternalLink]);
return [showInternalLink, setShowInternalLink];
}

View File

@ -1,7 +1,17 @@
import angular from 'angular';
import { CoreApp, DataQueryRequest, DataSourceInstanceSettings, dateMath, dateTime, Field, toUtc } from '@grafana/data';
import {
ArrayVector,
CoreApp,
DataQueryRequest,
DataSourceInstanceSettings,
dateMath,
dateTime,
Field,
MutableDataFrame,
toUtc,
} from '@grafana/data';
import _ from 'lodash';
import { ElasticDatasource } from './datasource';
import { ElasticDatasource, enhanceDataFrame } from './datasource';
import { backendSrv } from 'app/core/services/backend_srv'; // will use the version in __mocks__
import { TimeSrv } from 'app/features/dashboard/services/TimeSrv';
import { TemplateSrv } from 'app/features/templating/template_srv';
@ -849,6 +859,49 @@ describe('ElasticDatasource', function(this: any) {
});
});
describe('enhanceDataFrame', () => {
it('adds links to dataframe', () => {
const df = new MutableDataFrame({
fields: [
{
name: 'urlField',
values: new ArrayVector([]),
},
{
name: 'traceField',
values: new ArrayVector([]),
},
],
});
enhanceDataFrame(df, [
{
field: 'urlField',
url: 'someUrl',
},
{
field: 'traceField',
url: 'query',
datasourceUid: 'dsUid',
},
]);
expect(df.fields[0].config.links.length).toBe(1);
expect(df.fields[0].config.links[0]).toEqual({
title: '',
url: 'someUrl',
});
expect(df.fields[1].config.links.length).toBe(1);
expect(df.fields[1].config.links[0]).toEqual({
title: '',
url: '',
internal: {
query: { query: 'query' },
datasourceUid: 'dsUid',
},
});
});
});
const createElasticQuery = (): DataQueryRequest<ElasticsearchQuery> => {
return {
requestId: '',

View File

@ -7,6 +7,7 @@ import {
DataQueryResponse,
DataFrame,
ScopedVars,
DataLink,
} from '@grafana/data';
import { ElasticResponse } from './elastic_response';
import { IndexPattern } from './index_pattern';
@ -404,7 +405,7 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
if (sentTargets.some(target => target.isLogsQuery)) {
const response = er.getLogs(this.logMessageField, this.logLevelField);
for (const dataFrame of response.data) {
this.enhanceDataFrame(dataFrame);
enhanceDataFrame(dataFrame, this.dataLinks);
}
return response;
}
@ -584,24 +585,6 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
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) {
if (obj === null || obj === undefined) {
return true;
@ -639,3 +622,35 @@ export class ElasticDatasource extends DataSourceApi<ElasticsearchQuery, Elastic
return false;
}
}
/**
* Modifies dataframe and adds dataLinks from the config.
* Exported for tests.
*/
export function enhanceDataFrame(dataFrame: DataFrame, dataLinks: DataLinkConfig[]) {
if (dataLinks.length) {
for (const field of dataFrame.fields) {
const dataLinkConfig = dataLinks.find(dataLink => field.name && field.name.match(dataLink.field));
if (dataLinkConfig) {
let link: DataLink;
if (dataLinkConfig.datasourceUid) {
link = {
title: '',
url: '',
internal: {
query: { query: dataLinkConfig.url },
datasourceUid: dataLinkConfig.datasourceUid,
},
};
} else {
link = {
title: '',
url: dataLinkConfig.url,
};
}
field.config = field.config || {};
field.config.links = [...(field.config.links || []), link];
}
}
}
}

View File

@ -29,4 +29,5 @@ export interface ElasticsearchQuery extends DataQuery {
export type DataLinkConfig = {
field: string;
url: string;
datasourceUid?: string;
};