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:
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
Reference in New Issue
Block a user