grafana/public/app/plugins/datasource/graphite/query_ctrl.ts
Piotr Jamróz 04a85b1a2a
Explore: Map Graphite queries to Loki (#33405)
* Create basic prototype for Loki integration

* Simplify importing queries

* Code clean-up

* Add test coverage and info box

* Remove test data script

* Update help

* Use less space for mappings info

* Make help screen dismissable

* Make mappings help more generic

* Convert learn more to a link

* Remove unused param

* Use a link Button for help section

* Add an extra line for better formatting

* Update public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Update public/app/plugins/datasource/graphite/configuration/MappingsHelp.tsx

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>

* Re-arrange lines

Co-authored-by: achatterjee-grafana <70489351+achatterjee-grafana@users.noreply.github.com>
2021-05-06 09:26:26 +02:00

461 lines
13 KiB
TypeScript

import './add_graphite_func';
import './func_editor';
import { each, eachRight, map, remove } from 'lodash';
import GraphiteQuery, { GraphiteTagOperator } from './graphite_query';
import { QueryCtrl } from 'app/plugins/sdk';
import { promiseToDigest } from 'app/core/utils/promiseToDigest';
import { auto } from 'angular';
import { TemplateSrv } from '@grafana/runtime';
import { dispatch } from 'app/store/store';
import { notifyApp } from 'app/core/actions';
import { createErrorNotification } from 'app/core/copy/appNotification';
const GRAPHITE_TAG_OPERATORS = ['=', '!=', '=~', '!=~'];
const TAG_PREFIX = 'tag: ';
export class GraphiteQueryCtrl extends QueryCtrl {
static templateUrl = 'partials/query.editor.html';
queryModel: GraphiteQuery;
segments: any[] = [];
addTagSegments: any[] = [];
removeTagValue: string;
supportsTags = false;
paused = false;
// to avoid error flooding, these errors are shown only once per session
private _tagsAutoCompleteErrorShown = false;
private _metricAutoCompleteErrorShown = false;
/** @ngInject */
constructor(
$scope: any,
$injector: auto.IInjectorService,
private uiSegmentSrv: any,
private templateSrv: TemplateSrv,
$timeout: any
) {
super($scope, $injector);
this.supportsTags = this.datasource.supportsTags;
this.paused = false;
this.target.target = this.target.target || '';
this.datasource.waitForFuncDefsLoaded().then(() => {
this.queryModel = new GraphiteQuery(this.datasource, this.target, templateSrv);
this.buildSegments(false);
});
this.removeTagValue = '-- remove tag --';
}
parseTarget() {
this.queryModel.parseTarget();
this.buildSegments();
}
toggleEditorMode() {
this.target.textEditor = !this.target.textEditor;
this.parseTarget();
}
buildSegments(modifyLastSegment = true) {
this.segments = map(this.queryModel.segments, (segment) => {
return this.uiSegmentSrv.newSegment(segment);
});
const checkOtherSegmentsIndex = this.queryModel.checkOtherSegmentsIndex || 0;
promiseToDigest(this.$scope)(this.checkOtherSegments(checkOtherSegmentsIndex, modifyLastSegment));
if (this.queryModel.seriesByTagUsed) {
this.fixTagSegments();
}
}
addSelectMetricSegment() {
this.queryModel.addSelectMetricSegment();
this.segments.push(this.uiSegmentSrv.newSelectMetric());
}
checkOtherSegments(fromIndex: number, modifyLastSegment = true) {
if (this.queryModel.segments.length === 1 && this.queryModel.segments[0].type === 'series-ref') {
return Promise.resolve();
}
if (fromIndex === 0) {
this.addSelectMetricSegment();
return Promise.resolve();
}
const path = this.queryModel.getSegmentPathUpTo(fromIndex + 1);
if (path === '') {
return Promise.resolve();
}
return this.datasource
.metricFindQuery(path)
.then((segments: any) => {
if (segments.length === 0) {
if (path !== '' && modifyLastSegment) {
this.queryModel.segments = this.queryModel.segments.splice(0, fromIndex);
this.segments = this.segments.splice(0, fromIndex);
this.addSelectMetricSegment();
}
} else if (segments[0].expandable) {
if (this.segments.length === fromIndex) {
this.addSelectMetricSegment();
} else {
return this.checkOtherSegments(fromIndex + 1);
}
}
})
.catch((err: any) => {
this.handleMetricsAutoCompleteError(err);
});
}
setSegmentFocus(segmentIndex: any) {
each(this.segments, (segment, index) => {
segment.focus = segmentIndex === index;
});
}
getAltSegments(index: number, prefix: string) {
let query = prefix && prefix.length > 0 ? '*' + prefix + '*' : '*';
if (index > 0) {
query = this.queryModel.getSegmentPathUpTo(index) + '.' + query;
}
const options = {
range: this.panelCtrl.range,
requestId: 'get-alt-segments',
};
return this.datasource
.metricFindQuery(query, options)
.then((segments: any[]) => {
const altSegments = map(segments, (segment) => {
return this.uiSegmentSrv.newSegment({
value: segment.text,
expandable: segment.expandable,
});
});
if (index > 0 && altSegments.length === 0) {
return altSegments;
}
// add query references
if (index === 0) {
eachRight(this.panelCtrl.panel.targets, (target) => {
if (target.refId === this.queryModel.target.refId) {
return;
}
altSegments.unshift(
this.uiSegmentSrv.newSegment({
type: 'series-ref',
value: '#' + target.refId,
expandable: false,
})
);
});
}
// add template variables
eachRight(this.templateSrv.getVariables(), (variable) => {
altSegments.unshift(
this.uiSegmentSrv.newSegment({
type: 'template',
value: '$' + variable.name,
expandable: true,
})
);
});
// add wildcard option
altSegments.unshift(this.uiSegmentSrv.newSegment('*'));
if (this.supportsTags && index === 0) {
this.removeTaggedEntry(altSegments);
return this.addAltTagSegments(prefix, altSegments);
} else {
return altSegments;
}
})
.catch((err: any): any[] => {
this.handleMetricsAutoCompleteError(err);
return [];
});
}
addAltTagSegments(prefix: string, altSegments: any[]) {
return this.getTagsAsSegments(prefix).then((tagSegments: any[]) => {
tagSegments = map(tagSegments, (segment) => {
segment.value = TAG_PREFIX + segment.value;
return segment;
});
return altSegments.concat(...tagSegments);
});
}
removeTaggedEntry(altSegments: any[]) {
altSegments = remove(altSegments, (s) => s.value === '_tagged');
}
segmentValueChanged(segment: { type: string; value: string; expandable: any }, segmentIndex: number) {
this.error = null;
this.queryModel.updateSegmentValue(segment, segmentIndex);
if (this.queryModel.functions.length > 0 && this.queryModel.functions[0].def.fake) {
this.queryModel.functions = [];
}
if (segment.type === 'tag') {
const tag = removeTagPrefix(segment.value);
this.pause();
this.addSeriesByTagFunc(tag);
return null;
}
if (segment.expandable) {
return promiseToDigest(this.$scope)(
this.checkOtherSegments(segmentIndex + 1).then(() => {
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
})
);
} else {
this.spliceSegments(segmentIndex + 1);
}
this.setSegmentFocus(segmentIndex + 1);
this.targetChanged();
return null;
}
spliceSegments(index: any) {
this.segments = this.segments.splice(0, index);
this.queryModel.segments = this.queryModel.segments.splice(0, index);
}
emptySegments() {
this.queryModel.segments = [];
this.segments = [];
}
targetTextChanged() {
this.updateModelTarget();
this.refresh();
}
updateModelTarget() {
this.queryModel.updateModelTarget(this.panelCtrl.panel.targets);
}
targetChanged() {
if (this.queryModel.error) {
return;
}
const oldTarget = this.queryModel.target.target;
this.updateModelTarget();
if (this.queryModel.target !== oldTarget && !this.paused) {
this.panelCtrl.refresh();
}
}
addFunction(funcDef: any) {
const newFunc = this.datasource.createFuncInstance(funcDef, {
withDefaultParams: true,
});
newFunc.added = true;
this.queryModel.addFunction(newFunc);
this.smartlyHandleNewAliasByNode(newFunc);
if (this.segments.length === 1 && this.segments[0].fake) {
this.emptySegments();
}
if (!newFunc.params.length && newFunc.added) {
this.targetChanged();
}
if (newFunc.def.name === 'seriesByTag') {
this.parseTarget();
}
}
removeFunction(func: any) {
this.queryModel.removeFunction(func);
this.targetChanged();
}
moveFunction(func: any, offset: any) {
this.queryModel.moveFunction(func, offset);
this.targetChanged();
}
addSeriesByTagFunc(tag: string) {
const newFunc = this.datasource.createFuncInstance('seriesByTag', {
withDefaultParams: false,
});
const tagParam = `${tag}=`;
newFunc.params = [tagParam];
this.queryModel.addFunction(newFunc);
newFunc.added = true;
this.emptySegments();
this.targetChanged();
this.parseTarget();
}
smartlyHandleNewAliasByNode(func: { def: { name: string }; params: number[]; added: boolean }) {
if (func.def.name !== 'aliasByNode') {
return;
}
for (let i = 0; i < this.segments.length; i++) {
if (this.segments[i].value.indexOf('*') >= 0) {
func.params[0] = i;
func.added = false;
this.targetChanged();
return;
}
}
}
getAllTags() {
return this.datasource.getTags().then((values: any[]) => {
const altTags = map(values, 'text');
altTags.splice(0, 0, this.removeTagValue);
return mapToDropdownOptions(altTags);
});
}
getTags(index: number, tagPrefix: any) {
const tagExpressions = this.queryModel.renderTagExpressions(index);
return this.datasource
.getTagsAutoComplete(tagExpressions, tagPrefix)
.then((values: any) => {
const altTags = map(values, 'text');
altTags.splice(0, 0, this.removeTagValue);
return mapToDropdownOptions(altTags);
})
.catch((err: any) => {
this.handleTagsAutoCompleteError(err);
});
}
getTagsAsSegments(tagPrefix: string) {
const tagExpressions = this.queryModel.renderTagExpressions();
return this.datasource
.getTagsAutoComplete(tagExpressions, tagPrefix)
.then((values: any) => {
return map(values, (val) => {
return this.uiSegmentSrv.newSegment({
value: val.text,
type: 'tag',
expandable: false,
});
});
})
.catch((err: any) => {
this.handleTagsAutoCompleteError(err);
});
}
getTagOperators() {
return mapToDropdownOptions(GRAPHITE_TAG_OPERATORS);
}
getAllTagValues(tag: { key: any }) {
const tagKey = tag.key;
return this.datasource.getTagValues(tagKey).then((values: any[]) => {
const altValues = map(values, 'text');
return mapToDropdownOptions(altValues);
});
}
getTagValues(tag: { key: any }, index: number, valuePrefix: any) {
const tagExpressions = this.queryModel.renderTagExpressions(index);
const tagKey = tag.key;
return this.datasource.getTagValuesAutoComplete(tagExpressions, tagKey, valuePrefix).then((values: any[]) => {
const altValues = map(values, 'text');
// Add template variables as additional values
eachRight(this.templateSrv.getVariables(), (variable) => {
altValues.push('${' + variable.name + ':regex}');
});
return mapToDropdownOptions(altValues);
});
}
tagChanged(tag: any, tagIndex: any) {
this.queryModel.updateTag(tag, tagIndex);
this.targetChanged();
}
addNewTag(segment: { value: any }) {
const newTagKey = segment.value;
const newTag = { key: newTagKey, operator: '=' as GraphiteTagOperator, value: '' };
this.queryModel.addTag(newTag);
this.targetChanged();
this.fixTagSegments();
}
removeTag(index: any) {
this.queryModel.removeTag(index);
this.targetChanged();
}
fixTagSegments() {
// Adding tag with the same name as just removed works incorrectly if single segment is used (instead of array)
this.addTagSegments = [this.uiSegmentSrv.newPlusButton()];
}
showDelimiter(index: number) {
return index !== this.queryModel.tags.length - 1;
}
pause() {
this.paused = true;
}
unpause() {
this.paused = false;
this.panelCtrl.refresh();
}
getCollapsedText() {
return this.target.target;
}
private handleTagsAutoCompleteError(error: Error): void {
console.error(error);
if (!this._tagsAutoCompleteErrorShown) {
this._tagsAutoCompleteErrorShown = true;
dispatch(notifyApp(createErrorNotification(`Fetching tags failed: ${error.message}.`)));
}
}
private handleMetricsAutoCompleteError(error: Error): void {
console.error(error);
if (!this._metricAutoCompleteErrorShown) {
this._metricAutoCompleteErrorShown = true;
dispatch(notifyApp(createErrorNotification(`Fetching metrics failed: ${error.message}.`)));
}
}
}
function mapToDropdownOptions(results: any[]) {
return map(results, (value) => {
return { text: value, value: value };
});
}
function removeTagPrefix(value: string): string {
return value.replace(TAG_PREFIX, '');
}