Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Metric labels do not include hostname when two queries are used #225

Merged
merged 1 commit into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@
"@vue/compiler-sfc": "3.2.47",
"copy-webpack-plugin": "^11.0.0",
"css-loader": "^6.7.3",
"cypress": "13.6.0",
"eslint": "^8.39.0",
"eslint-plugin-jsdoc": "^43.0.7",
"eslint-plugin-react": "^7.32.2",
Expand Down Expand Up @@ -78,5 +79,5 @@
"react-dom": "17.0.2",
"tslib": "2.5.3"
},
"packageManager": "yarn@"
"packageManager": "yarn@1.22.21"
}
2 changes: 2 additions & 0 deletions src/RequestSpec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export interface RequestSpec {
service_in_group: NegatableOption | undefined;

graph: string | undefined;

label: string | undefined;
}

// subset of RequestSpec used with the Filters Component
Expand Down
23 changes: 14 additions & 9 deletions src/backend/rest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import {
FieldType,
MetricFindValue,
MutableDataFrame,
ScopedVars,
TimeRange,
dateTime,
} from '@grafana/data';
Expand All @@ -16,7 +17,7 @@ import { Aggregation, GraphType, MetricFindQuery } from 'RequestSpec';
import * as process from 'process';

import { CmkQuery } from '../types';
import { createCmkContext, replaceVariables, toLiveStatusQuery, updateQuery } from '../utils';
import { createCmkContext, replaceVariables, toLiveStatusQuery, updateMetricTitles, updateQuery } from '../utils';
import { Backend, DatasourceOptions } from './types';
import { validateRequestSpec } from './validate';

Expand All @@ -26,18 +27,20 @@ type RestApiError = {
title: string;
};

export type MetricResponse = {
color: string;
data_points: number[];
line_type: string;
title: string;
};

type RestApiGraphResponse = {
time_range: {
start: string;
end: string;
};
step: number;
metrics: Array<{
color: string;
data_points: number[];
line_type: string;
title: string;
}>;
metrics: MetricResponse[];
};

type CommonRequest = {
Expand Down Expand Up @@ -155,7 +158,7 @@ export default class RestApiBackend implements Backend {
const promises = request.targets
.filter((target) => !target.hide)
.map((target) => {
return this.getSingleGraph(request.range, target);
return this.getSingleGraph(request.range, target, request.scopedVars);
});
return await Promise.all(promises).then((data) => ({ data }));
}
Expand Down Expand Up @@ -250,7 +253,7 @@ export default class RestApiBackend implements Backend {
return result;
}

async getSingleGraph(range: TimeRange, query: CmkQuery): Promise<DataQueryResponseData> {
async getSingleGraph(range: TimeRange, query: CmkQuery, scopedVars: ScopedVars = {}): Promise<DataQueryResponseData> {
// it's not about a single graph line, but a single chart. grafana supports
// to query multiple graphs in one request, but we have to unwind this, as
// our api only supports a single chart/query per api call.
Expand Down Expand Up @@ -322,6 +325,8 @@ export default class RestApiBackend implements Backend {

const { time_range, step, metrics } = response.data;

updateMetricTitles(metrics, query, scopedVars);

const timeValues = [];
let currentTime: DateTime = dateTime(time_range.start);
const endTime: DateTime = dateTime(time_range.end);
Expand Down
22 changes: 18 additions & 4 deletions src/backend/web.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { DataQueryRequest, DataQueryResponse, FieldType, MetricFindValue, MutableDataFrame } from '@grafana/data';
import {
DataQueryRequest,
DataQueryResponse,
FieldType,
MetricFindValue,
MutableDataFrame,
ScopedVars,
} from '@grafana/data';
import { BackendSrvRequest, FetchError, FetchResponse, getBackendSrv } from '@grafana/runtime';
import { MetricFindQuery } from 'RequestSpec';
import { defaults, get, isUndefined, zip } from 'lodash';

import { CmkQuery, defaultQuery } from '../types';
import { updateQuery } from '../utils';
import { updateMetricTitles, updateQuery } from '../utils';
import {
WebAPiGetGraphResult,
WebApiResponse,
Expand Down Expand Up @@ -89,7 +96,7 @@ export default class WebApiBackend implements Backend {
.map((target) => {
// TODO: check if the defaults call is still necessary.
const query = defaults(target, defaultQuery);
return this.getGraphQuery([from, to], query);
return this.getGraphQuery([from, to], query, options.scopedVars);
});
return Promise.all(promises).then((data) => ({ data }));
}
Expand Down Expand Up @@ -145,7 +152,11 @@ export default class WebApiBackend implements Backend {
}
}

async getGraphQuery(range: number[], query: CmkQuery): Promise<MutableDataFrame<unknown>> {
async getGraphQuery(
range: number[],
query: CmkQuery,
scopedVars: ScopedVars = {}
): Promise<MutableDataFrame<unknown>> {
updateQuery(query);
const graph = get(query, 'requestSpec.graph');
if (isUndefined(graph) || graph === '') {
Expand Down Expand Up @@ -173,8 +184,11 @@ export default class WebApiBackend implements Backend {
if (response.result_code !== 0) {
throw new Error(`${response.result}`);
}

const { start_time, step, curves } = response.result;

updateMetricTitles(curves, query, scopedVars);

const frame = new MutableDataFrame({
refId: query.refId,
fields: [
Expand Down
12 changes: 11 additions & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { DataQuery, DataSourceJsonData } from '@grafana/data';
import { DataSourceJsonData } from '@grafana/data';
import { DataQuery } from '@grafana/schema';

import { RequestSpec, defaultRequestSpec } from './RequestSpec';

Expand Down Expand Up @@ -78,3 +79,12 @@ export interface SecureJsonData {
export interface ResponseDataAutocomplete {
choices: Array<[string, string]>;
}

export enum LabelVariableNames {
ORIGINAL = '$label',
SITE = '$filter_site',
HOSTNAME = '$filter_host_name',
HOST_IN_GROUP = '$filter_host_in_group',
SERVICE = '$filter_service',
SERVICE_IN_GROUP = '$filter_service_in_group',
}
60 changes: 57 additions & 3 deletions src/ui/QueryEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { InlineFieldRow, VerticalGroup } from '@grafana/ui';
import { Button, Icon, InlineFieldRow, Toggletip, VerticalGroup } from '@grafana/ui';
import React from 'react';

import { DataSource } from '../DataSource';
import { Aggregation, GraphType, RequestSpec } from '../RequestSpec';
import { CmkQuery, DataSourceOptions } from '../types';
import { CmkQuery, DataSourceOptions, LabelVariableNames } from '../types';
import { aggregationToPresentation, updateQuery } from '../utils';
import { CheckMkSelect } from './components';
import { CheckMkSelect, GenericField } from './components';
import { Filters } from './filters';
import { labelForRequestSpecKey } from './utils';

Expand All @@ -19,6 +19,8 @@ export const QueryEditor = (props: Props): JSX.Element => {
const [qAggregation, setQAggregation] = React.useState(rs.aggregation || 'off');
const [qGraphType, setQGraphType] = React.useState(rs.graph_type || 'predefined_graph');
const [qGraph, setQGraph] = React.useState(rs.graph);
const [qLabel, setQLabel] = React.useState(rs.label);

const filters: Partial<RequestSpec> = {
// make sure to only include keys filters should change, otherwise they could
// overwrite other fields!
Expand All @@ -43,6 +45,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
graph_type: qGraphType,
graph: qGraph,
aggregation: qAggregation,
label: qLabel,
};

// TODO: not sure if this is a dirty hack or a great solution:
Expand Down Expand Up @@ -109,6 +112,55 @@ export const QueryEditor = (props: Props): JSX.Element => {
/>
);

const LabelsTooltip = (
<Toggletip
content={
<>
In addition to the variables provided by Grafana, the following variables hold the values provided in the
filters:
<ul style={{ paddingLeft: '20px' }}>
{[
[LabelVariableNames.SITE, 'Site name'],
[LabelVariableNames.HOSTNAME, 'Host name'],
[LabelVariableNames.HOST_IN_GROUP, 'Group containing the host'],
[LabelVariableNames.SERVICE, 'Service name'],
[LabelVariableNames.SERVICE_IN_GROUP, 'Group containing the service'],
].map((lbl) => (
<li key={lbl[0]}>
<strong>{lbl[0]}</strong>: {lbl[1]}
</li>
))}
</ul>
<br />
Also, the original label is available as
<ul style={{ paddingLeft: '20px' }}>
<li>
<strong>{LabelVariableNames.ORIGINAL}</strong>
</li>
</ul>
</>
}
closeButton={false}
placement="right-end"
>
<Button type="button" fill="text" size="sm" tooltip={'Variables support information'}>
<Icon name="question-circle" />
</Button>
</Toggletip>
);

const labelField = (
<GenericField
requestSpecKey="label"
label={labelForRequestSpecKey('label', requestSpec)}
value={qLabel}
onChange={setQLabel}
dataTestId="custom-label-field"
placeholder={LabelVariableNames.ORIGINAL}
suffix={LabelsTooltip}
></GenericField>
);

if (editionMode === 'RAW') {
return (
<VerticalGroup>
Expand All @@ -122,6 +174,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
/>
{graphTypeSelect}
{graphSelect}
{labelField}
</VerticalGroup>
);
} else {
Expand All @@ -139,6 +192,7 @@ export const QueryEditor = (props: Props): JSX.Element => {
/>
{graphTypeSelect}
{graphSelect}
{labelField}
</VerticalGroup>
);
}
Expand Down
61 changes: 61 additions & 0 deletions src/ui/components.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,6 +288,67 @@ export const Filter = <T extends RequestSpecNegatableOptionKeys>(props: FilterPr
);
};

interface GenericFieldProps<Key extends RequestSpecStringKeys> extends CommonProps<RequestSpec[Key]> {
requestSpecKey: Key;
children?: React.ReactNode;
width?: number;
tooltip?: string;
dataTestId?: string;
placeholder?: string;
prefix?: React.ReactNode;
suffix?: React.ReactNode;
}
export const GenericField = <T extends RequestSpecStringKeys>(props: GenericFieldProps<T>) => {
const {
label,
width = 32,
onChange,
value,
requestSpecKey,
children = null,
tooltip,
placeholder = 'none',
prefix,
suffix,
} = props;
const { dataTestId = `${requestSpecKey}-filter-input` } = props;

const [textValue, setTextValue] = React.useState(value !== undefined ? value : '');

const debouncedOnChange = React.useMemo(
lpetrora marked this conversation as resolved.
Show resolved Hide resolved
() =>
debounce((newValue) => {
onChange(newValue);
}, 1000),
[onChange]
);

const onValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setTextValue(event.target.value); // update text input
debouncedOnChange(event.target.value); // only the last debounce call comes through
};

return (
<HorizontalGroup>
<InlineField label={label} labelWidth={LABEL_WIDTH} tooltip={tooltip}>
<>
<Input
width={width}
type="text"
value={textValue}
onChange={onValueChange}
placeholder={placeholder}
data-test-id={dataTestId}
prefix={prefix}
suffix={suffix}
/>
{children}
</>
</InlineField>
</HorizontalGroup>
);
};

const SingleTag = (props: {
index: number;
onChange: (newValue: TagValue) => void;
Expand Down
1 change: 1 addition & 0 deletions src/ui/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export const labelForRequestSpecKey = (key: keyof RequestSpec, rq: Partial<Reque
aggregation: 'Aggregation',
graph_type: 'Graph type',
graph: rq.graph_type === 'predefined_graph' ? 'Predefined graph' : 'Single metric',
label: 'Custom label',
};
return table[key];
};
32 changes: 30 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ import { getTemplateSrv } from '@grafana/runtime';
import { isUndefined } from 'lodash';

import { Aggregation, FiltersRequestSpec, NegatableOption, RequestSpec, TagValue } from './RequestSpec';
import { CmkQuery } from './types';
import { MetricResponse } from './backend/rest';
import { CmkQuery, LabelVariableNames } from './types';
import { Presentation } from './ui/autocomplete';
import { requestSpecFromLegacy } from './webapi';
import { WebApiCurve, requestSpecFromLegacy } from './webapi';

export const titleCase = (str: string): string => str[0].toUpperCase() + str.slice(1).toLowerCase();

Expand Down Expand Up @@ -276,3 +277,30 @@ export function toLiveStatusQuery(filter: Partial<FiltersRequestSpec>, table: 'h
query: query,
};
}

type GrapResponse = WebApiCurve | MetricResponse;

export function updateMetricTitles(metrics: GrapResponse[], query: CmkQuery, scopedVars: ScopedVars = {}) {
const titleTemplate = query.requestSpec?.label || LabelVariableNames.ORIGINAL;
if (titleTemplate !== LabelVariableNames.ORIGINAL) {
scopedVars = {
...scopedVars,
[LabelVariableNames.SITE.substring(1)]: { value: query.requestSpec?.site },
[LabelVariableNames.HOSTNAME.substring(1)]: { value: query.requestSpec?.host_name },
[LabelVariableNames.HOST_IN_GROUP.substring(1)]: { value: query.requestSpec?.host_in_group?.value },
[LabelVariableNames.SERVICE.substring(1)]: { value: query.requestSpec?.service },
[LabelVariableNames.SERVICE_IN_GROUP.substring(1)]: { value: query.requestSpec?.service_in_group?.value },
};

Object.keys(scopedVars).forEach((key) => {
if (scopedVars[key] === undefined) {
delete scopedVars[key];
}
});

metrics.forEach((metric) => {
scopedVars[LabelVariableNames.ORIGINAL.substring(1)] = { value: metric.title };
metric.title = getTemplateSrv().replace(titleTemplate, scopedVars);
});
}
}
Loading
Loading