Skip to content

Commit

Permalink
Metric labels do not include hostname when two queries are used
Browse files Browse the repository at this point in the history
This change adds a custom label input box and allows the user to
customize the labels. It corresponding filter is created, these
variables are available:
* $filter_site: Site name
* $filter_host_name: Host name
* $filter_host_in_group: Group containing the host
* $filter_service: Service name
* $filter_service_in_group: Group containing the service

$label is always available and contains the original label sent by
Checmk.

CMK-15138
  • Loading branch information
lpetrora committed Nov 30, 2023
1 parent 076a845 commit d074ee9
Show file tree
Hide file tree
Showing 18 changed files with 1,860 additions and 1,452 deletions.
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(
() =>
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

0 comments on commit d074ee9

Please sign in to comment.