Tempo: Support alerting and recorded rules in TraceQL metrics (#105094)

* Tempo: Support alerting and recorded rules

* Remove step and hide in options

* Update query type shown in alerting

* Remove labelKeys

* Update test
This commit is contained in:
Joey
2025-05-21 07:55:08 +01:00
committed by GitHub
parent 437f3ff936
commit fe704843a9
7 changed files with 203 additions and 210 deletions
+2 -13
View File
@@ -67,13 +67,6 @@ func TransformInstantMetricsResponse(query *dataquery.TempoQuery, resp tempopb.Q
for i, series := range resp.Series {
name, labels := transformLabelsAndGetName(series.Labels)
labelKeys := make([]string, 0, len(labels))
labelFields := make([]*data.Field, 0, len(labels))
for key := range labels {
labelKeys = append(labelKeys, key)
labelFields = append(labelFields, data.NewField(key, nil, []string{}))
}
timeField := data.NewField("time", nil, []time.Time{})
valueField := data.NewField("value", labels, []float64{})
valueField.Config = &data.FieldConfig{
@@ -83,17 +76,13 @@ func TransformInstantMetricsResponse(query *dataquery.TempoQuery, resp tempopb.Q
frame := &data.Frame{
RefID: name,
Name: name,
Fields: append([]*data.Field{timeField}, append(labelFields, valueField)...),
Fields: append([]*data.Field{timeField}, valueField),
Meta: &data.FrameMeta{
PreferredVisualization: data.VisTypeTable,
},
}
labelValues := make([]interface{}, len(labels))
for idx, key := range labelKeys {
labelValues[idx] = strings.Trim(labels[key], "\"")
}
row := append([]interface{}{time.Now()}, append(labelValues, series.GetValue())...)
row := append([]interface{}{time.Now()}, series.GetValue())
frame.AppendRow(row...)
frames[i] = frame
+2 -16
View File
@@ -117,12 +117,6 @@ func TestTransformInstantMetricsResponse(t *testing.T) {
resp := tempopb.QueryInstantResponse{
Series: []*tempopb.InstantSeries{
{
Labels: []v1.KeyValue{
{
Key: "label",
Value: &v1.AnyValue{Value: &v1.AnyValue_StringValue{StringValue: "value"}},
},
},
Value: 123.45,
PromLabels: "label=\"value\"",
},
@@ -134,22 +128,14 @@ func TestTransformInstantMetricsResponse(t *testing.T) {
assert.Len(t, frames, 1)
frame := frames[0]
assert.Equal(t, "value", frame.RefID)
assert.Equal(t, "value", frame.Name)
assert.Len(t, frame.Fields, 3)
assert.Len(t, frame.Fields, 2)
timeField := frame.Fields[0]
assert.Equal(t, "time", timeField.Name)
assert.Equal(t, 1, timeField.Len())
assert.IsType(t, time.Time{}, timeField.At(0))
labelField := frame.Fields[1]
assert.Equal(t, "label", labelField.Name)
assert.Equal(t, 1, labelField.Len())
assert.IsType(t, "", labelField.At(0))
assert.Equal(t, "value", labelField.At(0))
valueField := frame.Fields[2]
valueField := frame.Fields[1]
assert.Equal(t, "value", valueField.Name)
assert.Equal(t, 1, valueField.Len())
assert.IsType(t, 0.0, valueField.At(0))
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { PureComponent } from 'react';
import { QueryEditorProps, SelectableValue } from '@grafana/data';
import { CoreApp, QueryEditorProps, SelectableValue } from '@grafana/data';
import { config, reportInteraction } from '@grafana/runtime';
import {
Button,
@@ -68,6 +68,7 @@ class TempoQueryFieldComponent extends PureComponent<Props, State> {
render() {
const { query, onChange, datasource, app } = this.props;
const isAlerting = app === CoreApp.UnifiedAlerting;
const graphDatasourceUid = datasource.serviceMap?.datasourceUid;
@@ -114,41 +115,43 @@ class TempoQueryFieldComponent extends PureComponent<Props, State> {
/>
</div>
</Modal>
<InlineFieldRow>
<InlineField label="Query type" grow={true}>
<Stack gap={1} alignItems="center" justifyContent="space-between">
<RadioButtonGroup<TempoQueryType>
options={queryTypeOptions}
value={query.queryType}
onChange={(v) => {
reportInteraction('grafana_traces_query_type_changed', {
datasourceType: 'tempo',
app: app ?? '',
grafana_version: config.buildInfo.version,
newQueryType: v,
previousQueryType: query.queryType ?? '',
});
{!isAlerting && (
<InlineFieldRow>
<InlineField label="Query type" grow={true}>
<Stack gap={1} alignItems="center" justifyContent="space-between">
<RadioButtonGroup<TempoQueryType>
options={queryTypeOptions}
value={query.queryType}
onChange={(v) => {
reportInteraction('grafana_traces_query_type_changed', {
datasourceType: 'tempo',
app: app ?? '',
grafana_version: config.buildInfo.version,
newQueryType: v,
previousQueryType: query.queryType ?? '',
});
this.onClearResults();
onChange({
...query,
queryType: v,
});
}}
size="md"
/>
<Button
variant="secondary"
size="sm"
onClick={() => {
this.setState({ uploadModalOpen: true });
}}
>
Import trace
</Button>
</Stack>
</InlineField>
</InlineFieldRow>
this.onClearResults();
onChange({
...query,
queryType: v,
});
}}
size="md"
/>
<Button
variant="secondary"
size="sm"
onClick={() => {
this.setState({ uploadModalOpen: true });
}}
>
Import trace
</Button>
</Stack>
</InlineField>
</InlineFieldRow>
)}
{query.queryType === 'traceqlSearch' && (
<TraceQLSearch
datasource={this.props.datasource}
@@ -283,6 +283,7 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
query={query}
searchStreaming={datasource.isStreamingSearchEnabled() ?? false}
metricsStreaming={datasource.isStreamingMetricsEnabled() ?? false}
app={app}
/>
</div>
{error ? (
@@ -6,7 +6,7 @@
"executable": "gpx_tempo",
"metrics": true,
"alerting": false,
"alerting": true,
"annotations": false,
"logs": false,
"streaming": false,
@@ -77,6 +77,7 @@ export function QueryEditor(props: Props) {
onChange={props.onChange}
searchStreaming={props.datasource.isStreamingSearchEnabled() ?? false}
metricsStreaming={props.datasource.isStreamingMetricsEnabled() ?? false}
app={props.app}
/>
</div>
</>
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import * as React from 'react';
import { useToggle } from 'react-use';
import { GrafanaTheme2 } from '@grafana/data';
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { EditorField, EditorRow } from '@grafana/plugin-ui';
import { AutoSizeInput, RadioButtonGroup, useStyles2 } from '@grafana/ui';
@@ -16,6 +16,7 @@ interface Props {
query: Partial<TempoQuery> & TempoQuery;
searchStreaming: boolean;
metricsStreaming: boolean;
app?: CoreApp;
}
/**
@@ -31,162 +32,174 @@ const parseIntWithFallback = (val: string, fallback: number) => {
return isNaN(parsed) ? fallback : parsed;
};
export const TempoQueryBuilderOptions = React.memo<Props>(({ onChange, query, searchStreaming, metricsStreaming }) => {
const styles = useStyles2(getStyles);
const [isOpen, toggleOpen] = useToggle(false);
export const TempoQueryBuilderOptions = React.memo<Props>(
({ onChange, query, searchStreaming, metricsStreaming, app }) => {
const styles = useStyles2(getStyles);
const [isOpen, toggleOpen] = useToggle(false);
const isAlerting = app === CoreApp.UnifiedAlerting;
if (!query.hasOwnProperty('limit')) {
query.limit = DEFAULT_LIMIT;
}
if (!query.hasOwnProperty('limit')) {
query.limit = DEFAULT_LIMIT;
}
if (!query.hasOwnProperty('tableType')) {
query.tableType = SearchTableType.Traces;
}
if (!query.hasOwnProperty('tableType')) {
query.tableType = SearchTableType.Traces;
}
if (!query.hasOwnProperty('metricsQueryType')) {
query.metricsQueryType = MetricsQueryType.Range;
}
if (!query.hasOwnProperty('metricsQueryType')) {
query.metricsQueryType = MetricsQueryType.Range;
}
const onLimitChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, limit: parseIntWithFallback(e.currentTarget.value, DEFAULT_LIMIT) });
};
const onSpssChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, spss: parseIntWithFallback(e.currentTarget.value, DEFAULT_SPSS) });
};
const onTableTypeChange = (val: SearchTableType) => {
onChange({ ...query, tableType: val });
};
const onMetricsQueryTypeChange = (val: MetricsQueryType) => {
onChange({ ...query, metricsQueryType: val });
};
const onStepChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, step: e.currentTarget.value });
};
if (isAlerting && query.metricsQueryType === MetricsQueryType.Range) {
onChange({ ...query, metricsQueryType: MetricsQueryType.Instant });
}
// There's a bug in Tempo which causes the exemplars param to be ignored. It's commented out for now.
const onLimitChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, limit: parseIntWithFallback(e.currentTarget.value, DEFAULT_LIMIT) });
};
const onSpssChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, spss: parseIntWithFallback(e.currentTarget.value, DEFAULT_SPSS) });
};
const onTableTypeChange = (val: SearchTableType) => {
onChange({ ...query, tableType: val });
};
const onMetricsQueryTypeChange = (val: MetricsQueryType) => {
onChange({ ...query, metricsQueryType: val });
};
const onStepChange = (e: React.FormEvent<HTMLInputElement>) => {
onChange({ ...query, step: e.currentTarget.value });
};
// const onExemplarsChange = (e: React.FormEvent<HTMLInputElement>) => {
// const exemplars = parseInt(e.currentTarget.value, 10);
// if (!isNaN(exemplars) && exemplars >= 0) {
// onChange({ ...query, exemplars });
// } else {
// onChange({ ...query, exemplars: undefined });
// }
// };
// There's a bug in Tempo which causes the exemplars param to be ignored. It's commented out for now.
const collapsedSearchOptions = [
`Limit: ${query.limit || DEFAULT_LIMIT}`,
`Spans Limit: ${query.spss || DEFAULT_SPSS}`,
`Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`,
'|',
`Streaming: ${searchStreaming ? 'Enabled' : 'Disabled'}`,
];
// const onExemplarsChange = (e: React.FormEvent<HTMLInputElement>) => {
// const exemplars = parseInt(e.currentTarget.value, 10);
// if (!isNaN(exemplars) && exemplars >= 0) {
// onChange({ ...query, exemplars });
// } else {
// onChange({ ...query, exemplars: undefined });
// }
// };
const collapsedMetricsOptions = [
`Step: ${query.step || 'auto'}`,
`Type: ${query.metricsQueryType === MetricsQueryType.Range ? 'Range' : 'Instant'}`,
'|',
`Streaming: ${metricsStreaming ? 'Enabled' : 'Disabled'}`,
// `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`,
];
const collapsedSearchOptions = [
`Limit: ${query.limit || DEFAULT_LIMIT}`,
`Spans Limit: ${query.spss || DEFAULT_SPSS}`,
`Table Format: ${query.tableType === SearchTableType.Traces ? 'Traces' : 'Spans'}`,
'|',
`Streaming: ${searchStreaming ? 'Enabled' : 'Disabled'}`,
];
return (
<EditorRow>
<div className={styles.options}>
<QueryOptionGroup
title="Search Options"
collapsedInfo={collapsedSearchOptions}
isOpen={isOpen}
onToggle={toggleOpen}
>
<EditorField label="Limit" tooltip="Maximum number of traces to return.">
<AutoSizeInput
className="width-4"
placeholder="auto"
type="number"
min={1}
defaultValue={query.limit || DEFAULT_LIMIT}
onCommitChange={onLimitChange}
value={query.limit}
/>
</EditorField>
<EditorField label="Span Limit" tooltip="Maximum number of spans to return for each span set.">
<AutoSizeInput
className="width-4"
placeholder="auto"
type="number"
min={1}
defaultValue={query.spss || DEFAULT_SPSS}
onCommitChange={onSpssChange}
value={query.spss}
/>
</EditorField>
<EditorField label="Table Format" tooltip="How the query data should be displayed in the results table">
<RadioButtonGroup
options={[
{ label: 'Traces', value: SearchTableType.Traces },
{ label: 'Spans', value: SearchTableType.Spans },
]}
value={query.tableType}
onChange={onTableTypeChange}
/>
</EditorField>
<EditorField label="Streaming" tooltip={<StreamingTooltip />} tooltipInteractive>
<div>{searchStreaming ? 'Enabled' : 'Disabled'}</div>
</EditorField>
</QueryOptionGroup>
const collapsedMetricsOptions = [
`Step: ${query.step || 'auto'}`,
`Type: ${query.metricsQueryType === MetricsQueryType.Range ? 'Range' : 'Instant'}`,
'|',
`Streaming: ${metricsStreaming ? 'Enabled' : 'Disabled'}`,
// `Exemplars: ${query.exemplars !== undefined ? query.exemplars : 'auto'}`,
];
<QueryOptionGroup
title="Metrics Options"
collapsedInfo={collapsedMetricsOptions}
isOpen={isOpen}
onToggle={toggleOpen}
>
<EditorField
label="Step"
tooltip="Defines the step for metric queries. Use duration notation, for example 30s or 1m"
return (
<EditorRow>
<div className={styles.options}>
{!isAlerting && (
<QueryOptionGroup
title="Search Options"
collapsedInfo={collapsedSearchOptions}
isOpen={isOpen}
onToggle={toggleOpen}
>
<EditorField label="Limit" tooltip="Maximum number of traces to return.">
<AutoSizeInput
className="width-4"
placeholder="auto"
type="number"
min={1}
defaultValue={query.limit || DEFAULT_LIMIT}
onCommitChange={onLimitChange}
value={query.limit}
/>
</EditorField>
<EditorField label="Span Limit" tooltip="Maximum number of spans to return for each span set.">
<AutoSizeInput
className="width-4"
placeholder="auto"
type="number"
min={1}
defaultValue={query.spss || DEFAULT_SPSS}
onCommitChange={onSpssChange}
value={query.spss}
/>
</EditorField>
<EditorField label="Table Format" tooltip="How the query data should be displayed in the results table">
<RadioButtonGroup
options={[
{ label: 'Traces', value: SearchTableType.Traces },
{ label: 'Spans', value: SearchTableType.Spans },
]}
value={query.tableType}
onChange={onTableTypeChange}
/>
</EditorField>
<EditorField label="Streaming" tooltip={<StreamingTooltip />} tooltipInteractive>
<div>{searchStreaming ? 'Enabled' : 'Disabled'}</div>
</EditorField>
</QueryOptionGroup>
)}
<QueryOptionGroup
title="Metrics Options"
collapsedInfo={collapsedMetricsOptions}
isOpen={isOpen}
onToggle={toggleOpen}
>
<AutoSizeInput
className="width-4"
placeholder="auto"
type="string"
defaultValue={query.step}
onCommitChange={onStepChange}
value={query.step}
/>
</EditorField>
<EditorField label="Type" tooltip="Type of metrics query to run">
<RadioButtonGroup
options={[
{ label: 'Range', value: MetricsQueryType.Range },
{ label: 'Instant', value: MetricsQueryType.Instant },
]}
value={query.metricsQueryType}
onChange={onMetricsQueryTypeChange}
/>
</EditorField>
{!isAlerting && (
<EditorField
label="Step"
tooltip="Defines the step for metric queries. Use duration notation, for example 30s or 1m"
>
<AutoSizeInput
className="width-4"
placeholder="auto"
type="string"
defaultValue={query.step}
onCommitChange={onStepChange}
value={query.step}
/>
</EditorField>
)}
<EditorField label="Type" tooltip="Type of metrics query to run">
<RadioButtonGroup
options={[
{ label: 'Range', value: MetricsQueryType.Range },
{ label: 'Instant', value: MetricsQueryType.Instant },
]}
value={query.metricsQueryType}
onChange={onMetricsQueryTypeChange}
disabled={isAlerting}
/>
</EditorField>
<EditorField label="Streaming" tooltip={<StreamingTooltip />} tooltipInteractive>
<div>{metricsStreaming ? 'Enabled' : 'Disabled'}</div>
</EditorField>
{/*<EditorField*/}
{/* label="Exemplars"*/}
{/* tooltip="Defines the amount of exemplars to request for metric queries. A value of 0 means no exemplars."*/}
{/*>*/}
{/* <AutoSizeInput*/}
{/* className="width-4"*/}
{/* placeholder="auto"*/}
{/* type="string"*/}
{/* defaultValue={query.exemplars}*/}
{/* onCommitChange={onExemplarsChange}*/}
{/* value={query.exemplars}*/}
{/* />*/}
{/*</EditorField>*/}
</QueryOptionGroup>
</div>
</EditorRow>
);
});
<EditorField label="Streaming" tooltip={<StreamingTooltip />} tooltipInteractive>
<div>{metricsStreaming ? 'Enabled' : 'Disabled'}</div>
</EditorField>
{/*<EditorField*/}
{/* label="Exemplars"*/}
{/* tooltip="Defines the amount of exemplars to request for metric queries. A value of 0 means no exemplars."*/}
{/*>*/}
{/* <AutoSizeInput*/}
{/* className="width-4"*/}
{/* placeholder="auto"*/}
{/* type="string"*/}
{/* defaultValue={query.exemplars}*/}
{/* onCommitChange={onExemplarsChange}*/}
{/* value={query.exemplars}*/}
{/* />*/}
{/*</EditorField>*/}
</QueryOptionGroup>
</div>
</EditorRow>
);
}
);
const StreamingTooltip = () => {
return (