Tempo: Add time range to tags and tag value requests (#107872)

* Add time range to tags and tag value requests

* Prettier

* Change timeRangeForTags to number and dropdown with options

* Listen to changes and update docs

* Update docs

* Prettier

* Text updates

* Extract getOptionsV2 params to type

* Use this.tagsForTimeRange

* Docs and move section up

* Prettier

* Update docs
This commit is contained in:
Joey
2025-07-21 10:14:44 +01:00
committed by GitHub
parent f7e55f2c5d
commit 96429d7dd8
15 changed files with 298 additions and 54 deletions
@@ -321,6 +321,22 @@ You can configure this setting as follows:
| **Time shift start** | Time shift for start of search. Default: `30m`. |
| **Time shift end** | Time shift for end of search. Default: `30m`. |
### Tags time range
The **Tags time range** feature controls how tag and tag-value queries are executed by specifying the time window applied to these requests. You can select one of the following options to constrain your queries:
| Name | Description |
| ------------------- | ---------------------------------- |
| **Last 30 minutes** | Last 30 minutes of selected range. |
| **Last 3 hours** | Last 3 hours of selected range. |
| **Last 24 hours** | Last 24 hours of selected range. |
| **Last 3 days** | Last 3 days of selected range. |
| **Last 7 days** | Last 7 days of selected range. |
### Tag limit
The **Tag limit** setting modifies the max number of tags and tag values to retrieve from Tempo. Default: 5000
### Span bar
The **Span bar** setting helps you display additional information in the span bar row.
@@ -333,10 +349,6 @@ You can choose one of three options:
| **Duration** | _(Default)_ Displays the span duration on the span bar row. |
| **Tag** | Displays the span tag on the span bar row. You must also specify which tag key to use to get the tag value, such as `component`. |
### Tag limit
The **Tag limit** setting modifies the max number of tags and tag values to retrieve from Tempo. Default: 5000
### Private data source connect
[//]: # 'Shared content for authentication section procedure in data sources'
@@ -161,6 +161,7 @@ class TempoQueryFieldComponent extends PureComponent<Props, State> {
app={app}
onClearResults={this.onClearResults}
addVariablesToOptions={this.props.addVariablesToOptions}
range={this.props.range}
/>
)}
{query.queryType === 'serviceMap' && (
@@ -174,6 +175,7 @@ class TempoQueryFieldComponent extends PureComponent<Props, State> {
onChange={onChange}
app={app}
onClearResults={this.onClearResults}
range={this.props.range}
/>
)}
</>
@@ -1,9 +1,9 @@
import { css } from '@emotion/css';
import { uniq } from 'lodash';
import { useState, useMemo } from 'react';
import { useMemo, useState } from 'react';
import useAsync from 'react-use/lib/useAsync';
import { SelectableValue } from '@grafana/data';
import { SelectableValue, TimeRange } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import { FetchError, getTemplateSrv, isFetchError } from '@grafana/runtime';
import { Select, Stack, useStyles2, InputActionMeta } from '@grafana/ui';
@@ -30,6 +30,8 @@ interface Props {
isMulti?: boolean;
allowCustomValue?: boolean;
addVariablesToOptions?: boolean;
range?: TimeRange;
timeRangeForTags?: number;
}
const SearchField = ({
filter,
@@ -45,6 +47,8 @@ const SearchField = ({
addVariablesToOptions,
isMulti = true,
allowCustomValue = true,
range,
timeRangeForTags,
}: Props) => {
const styles = useStyles2(getStyles);
const [alertText, setAlertText] = useState<string>();
@@ -57,7 +61,14 @@ const SearchField = ({
const updateOptions = async () => {
try {
const result = filter.tag ? await datasource.languageProvider.getOptionsV2(scopedTag, query) : [];
const result = filter.tag
? await datasource.languageProvider.getOptionsV2({
tag: scopedTag,
query,
timeRangeForTags,
range,
})
: [];
setAlertText(undefined);
setError(null);
return result;
@@ -77,6 +88,8 @@ const SearchField = ({
datasource.languageProvider,
setError,
query,
range,
timeRangeForTags,
]);
// Add selected option if it doesn't exist in the current list of options
@@ -2,7 +2,7 @@ import { css } from '@emotion/css';
import { useCallback, useEffect } from 'react';
import { v4 as uuidv4 } from 'uuid';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { AccessoryButton } from '@grafana/plugin-ui';
import { FetchError } from '@grafana/runtime';
import { useStyles2 } from '@grafana/ui';
@@ -41,6 +41,8 @@ interface Props {
hideValues?: boolean;
requireTagAndValue?: boolean;
addVariablesToOptions?: boolean;
range?: TimeRange;
timeRangeForTags?: number;
}
const TagsInput = ({
updateFilter,
@@ -54,6 +56,8 @@ const TagsInput = ({
requireTagAndValue,
generateQueryWithoutFilter,
addVariablesToOptions,
range,
timeRangeForTags,
}: Props) => {
const styles = useStyles2(getStyles);
const handleOnAdd = useCallback(
@@ -91,6 +95,8 @@ const TagsInput = ({
hideValue={hideValues}
query={generateQueryWithoutFilter(f)}
addVariablesToOptions={addVariablesToOptions}
range={range}
timeRangeForTags={timeRangeForTags}
/>
{(validInput(f) || filters.length > 1) && (
<AccessoryButton
@@ -1,7 +1,7 @@
import { css } from '@emotion/css';
import { useCallback, useEffect, useState } from 'react';
import { CoreApp, GrafanaTheme2 } from '@grafana/data';
import { CoreApp, GrafanaTheme2, TimeRange } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import { config, FetchError, getTemplateSrv, reportInteraction } from '@grafana/runtime';
import { Alert, Button, Stack, Select, useStyles2 } from '@grafana/ui';
@@ -28,11 +28,20 @@ interface Props {
onClearResults: () => void;
app?: CoreApp;
addVariablesToOptions?: boolean;
range?: TimeRange;
}
const hardCodedFilterIds = ['min-duration', 'max-duration', 'status'];
const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVariablesToOptions = true }: Props) => {
const TraceQLSearch = ({
datasource,
query,
onChange,
onClearResults,
app,
addVariablesToOptions = true,
range,
}: Props) => {
const styles = useStyles2(getStyles);
const [alertText, setAlertText] = useState<string>();
const [error, setError] = useState<Error | FetchError | null>(null);
@@ -74,7 +83,7 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
useEffect(() => {
const fetchTags = async () => {
try {
await datasource.languageProvider.start();
await datasource.languageProvider.start(range, datasource.timeRangeForTags);
setIsTagsLoading(false);
setAlertText(undefined);
} catch (error) {
@@ -84,7 +93,7 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
}
};
fetchTags();
}, [datasource, setAlertText]);
}, [datasource, setAlertText, range, datasource.timeRangeForTags]);
useEffect(() => {
// Initialize state with configured static filters that already have a value from the config
@@ -155,6 +164,8 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
hideTag={true}
query={generateQueryWithoutFilter(findFilter(f.id))}
addVariablesToOptions={addVariablesToOptions}
range={range}
timeRangeForTags={datasource.timeRangeForTags}
/>
</InlineSearchField>
)
@@ -179,6 +190,8 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
isMulti={false}
allowCustomValue={false}
addVariablesToOptions={addVariablesToOptions}
range={range}
timeRangeForTags={datasource.timeRangeForTags}
/>
</InlineSearchField>
<InlineSearchField
@@ -240,6 +253,8 @@ const TraceQLSearch = ({ datasource, query, onChange, onClearResults, app, addVa
generateQueryWithoutFilter={generateQueryWithoutFilter}
requireTagAndValue={true}
addVariablesToOptions={addVariablesToOptions}
range={range}
timeRangeForTags={datasource.timeRangeForTags}
/>
</InlineSearchField>
<AggregateByAlert
@@ -1,6 +1,6 @@
import { useEffect, useMemo, useState } from 'react';
import { DataQuery, SelectableValue } from '@grafana/data';
import { DataQuery, SelectableValue, TimeRange } from '@grafana/data';
import { InlineField, InlineFieldRow, InputActionMeta, Select } from '@grafana/ui';
import { TempoDatasource } from './datasource';
@@ -28,21 +28,30 @@ export type TempoVariableQueryEditorProps = {
onChange: (value: TempoVariableQuery) => void;
query: TempoVariableQuery;
datasource: TempoDatasource;
range?: TimeRange;
};
export const TempoVariableQueryEditor = ({ onChange, query, datasource }: TempoVariableQueryEditorProps) => {
export const TempoVariableQueryEditor = ({ onChange, query, datasource, range }: TempoVariableQueryEditorProps) => {
const [label, setLabel] = useState(query.label || '');
const [type, setType] = useState<number | undefined>(query.type);
const [labelOptions, setLabelOptions] = useState<Array<SelectableValue<string>>>([]);
const [labelQuery, setLabelQuery] = useState<string>('');
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (type === TempoVariableQueryType.LabelValues) {
datasource.labelNamesQuery().then((labelNames: Array<{ text: string }>) => {
setLabelOptions(labelNames.map(({ text }) => ({ label: text, value: text })));
});
setIsLoading(true);
datasource
.labelNamesQuery(range)
.then((labelNames: Array<{ text: string }>) => {
setLabelOptions(labelNames.map(({ text }) => ({ label: text, value: text })));
setIsLoading(false);
})
.catch(() => {
setIsLoading(false);
});
}
}, [datasource, query, type]);
}, [datasource, query, type, range]);
const options = useMemo(() => {
if (labelQuery.length === 0) {
@@ -122,6 +131,7 @@ export const TempoVariableQueryEditor = ({ onChange, query, datasource }: TempoV
width={32}
allowCustomValue
virtualized
isLoading={isLoading}
/>
</InlineField>
</InlineFieldRow>
@@ -25,6 +25,7 @@ import { QuerySettings } from './QuerySettings';
import { ServiceGraphSettings } from './ServiceGraphSettings';
import { StreamingSection } from './StreamingSection';
import { TagLimitSection } from './TagLimitSettings';
import { TagsTimeRangeSettings } from './TagsTimeRangeSettings';
import { TraceQLSearchSettings } from './TraceQLSearchSettings';
export type ConfigEditorProps = DataSourcePluginOptionsEditorProps;
@@ -118,9 +119,21 @@ const ConfigEditor = ({ options, onOptionsChange }: ConfigEditorProps) => {
<QuerySettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSubSection>
<SpanBarSection options={options} onOptionsChange={onOptionsChange} />
<ConfigSubSection
title="Tags time range"
description={
<ConfigDescriptionLink
description="Modify how tags and tag values queries are run."
suffix="tempo/configure-tempo-data-source/#tags-time-range"
feature="the tags time range"
/>
}
>
<TagsTimeRangeSettings options={options} onOptionsChange={onOptionsChange} />
</ConfigSubSection>
<TagLimitSection options={options} onOptionsChange={onOptionsChange} />
<SpanBarSection options={options} onOptionsChange={onOptionsChange} />
</Stack>
</ConfigSection>
</div>
@@ -0,0 +1,45 @@
import { DataSourcePluginOptionsEditorProps, updateDatasourcePluginJsonDataOption } from '@grafana/data';
import { Combobox, InlineField, InlineFieldRow, useStyles2 } from '@grafana/ui';
import { TempoJsonData } from '../types';
import { getStyles } from './QuerySettings';
interface Props extends DataSourcePluginOptionsEditorProps<TempoJsonData> {}
export const DEFAULT_TIME_RANGE_FOR_TAGS = 1800; // 60 * 30
export function TagsTimeRangeSettings({ options, onOptionsChange }: Props) {
const styles = useStyles2(getStyles);
const selectOptions = [
{ label: 'Last 30 minutes of selected range', value: DEFAULT_TIME_RANGE_FOR_TAGS },
{ label: 'Last 3 hours of selected range', value: 10800 }, // 60 * 60 * 3
{ label: 'Last 24 hours of selected range', value: 86400 }, // 60 * 60 * 24
{ label: 'Last 3 days of selected range', value: 259200 }, // 60 * 60 * 24 * 3
{ label: 'Last 7 days of selected range', value: 604800 }, // 60 * 60 * 24 * 7
];
return (
<div className={styles.container}>
<InlineFieldRow className={styles.row}>
<InlineField tooltip="Time range in tags and tag value queries" label="Time range in query" labelWidth={26}>
<Combobox
id="time-range-for-tags-select"
options={selectOptions}
value={options.jsonData?.timeRangeForTags || DEFAULT_TIME_RANGE_FOR_TAGS}
onChange={(v) => {
updateDatasourcePluginJsonDataOption(
{ onOptionsChange, options },
'timeRangeForTags',
v?.value ?? DEFAULT_TIME_RANGE_FOR_TAGS
);
}}
placeholder="Time range for tags"
width={40}
/>
</InlineField>
</InlineFieldRow>
</div>
);
}
@@ -21,6 +21,7 @@ import {
ScopedVars,
SelectableValue,
TestDataSourceResponse,
TimeRange,
urlUtil,
} from '@grafana/data';
import { NodeGraphOptions, SpanBarOptions, TraceToLogsOptions } from '@grafana/o11y-ds-frontend';
@@ -134,6 +135,8 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
metrics?: boolean;
};
timeRangeForTags?: number;
// The version of Tempo running on the backend. `null` if we cannot retrieve it for whatever reason
tempoVersion?: string | null;
@@ -149,7 +152,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
this.nodeGraph = instanceSettings.jsonData.nodeGraph;
this.traceQuery = instanceSettings.jsonData.traceQuery;
this.streamingEnabled = instanceSettings.jsonData.streamingEnabled;
this.timeRangeForTags = instanceSettings.jsonData.timeRangeForTags;
this.languageProvider = new TempoLanguageProvider(this);
if (!this.search?.filters) {
@@ -170,7 +173,7 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
this.variables = new TempoVariableSupport(this);
}
async executeVariableQuery(query: TempoVariableQuery) {
async executeVariableQuery(query: TempoVariableQuery, range?: TimeRange) {
// Avoid failing if the user did not select the query type (label names, label values, etc.)
if (query.type === undefined) {
return new Promise<Array<{ text: string }>>(() => []);
@@ -178,10 +181,10 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
switch (query.type) {
case TempoVariableQueryType.LabelNames: {
return await this.labelNamesQuery();
return await this.labelNamesQuery(range);
}
case TempoVariableQueryType.LabelValues: {
return this.labelValuesQuery(query.label);
return this.labelValuesQuery(query.label, range);
}
default: {
throw Error('Invalid query type: ' + query.type);
@@ -189,17 +192,19 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
}
}
async labelNamesQuery(): Promise<Array<{ text: string }>> {
await this.languageProvider.fetchTags();
async labelNamesQuery(range?: TimeRange): Promise<Array<{ text: string }>> {
await this.languageProvider.start(range, this.timeRangeForTags);
const tags = this.languageProvider.getAutocompleteTags();
return tags.filter((tag) => tag !== undefined).map((tag) => ({ text: tag }));
}
async labelValuesQuery(labelName?: string): Promise<Array<{ text: string }>> {
async labelValuesQuery(labelName?: string, range?: TimeRange): Promise<Array<{ text: string }>> {
if (!labelName) {
return [];
}
await this.languageProvider.start(range, this.timeRangeForTags);
let options;
try {
// Retrieve the scope of the tag
@@ -217,7 +222,11 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
// unless the tag has intrinsic scope
const scopeAndTag = scope === 'intrinsic' ? labelName : `${scope}.${labelName}`;
options = await this.languageProvider.getOptionsV2(scopeAndTag);
options = await this.languageProvider.getOptionsV2({
tag: scopeAndTag,
timeRangeForTags: this.timeRangeForTags,
range,
});
} catch {
// For V1, the tag name (e.g. `http.status_code`) is enough
options = await this.languageProvider.getOptionsV1(labelName);
@@ -243,15 +252,20 @@ export class TempoDatasource extends DataSourceWithBackend<TempoQuery, TempoJson
// Allows to retrieve the list of tag values for ad-hoc filters
getTagValues(options: DataSourceGetTagValuesOptions<TempoQuery>): Promise<Array<{ text: string }>> {
const query = this.languageProvider.generateQueryFromFilters({ adhocFilters: options.filters });
return this.tagValuesQuery(options.key, query);
return this.tagValuesQuery(options.key, query, options.timeRange);
}
async tagValuesQuery(tag: string, query: string): Promise<Array<{ text: string }>> {
async tagValuesQuery(tag: string, query: string, range?: TimeRange): Promise<Array<{ text: string }>> {
let options;
try {
// For V2, we need to send scope and tag name, e.g. `span.http.status_code`,
// unless the tag has intrinsic scope
options = await this.languageProvider.getOptionsV2(tag, query);
options = await this.languageProvider.getOptionsV2({
tag,
query,
timeRangeForTags: this.timeRangeForTags,
range,
});
} catch {
// For V1, the tag name (e.g. `http.status_code`) is enough
options = await this.languageProvider.getOptionsV1(getTagWithoutScope(tag));
@@ -1,4 +1,4 @@
import { AdHocVariableFilter, LanguageProvider, SelectableValue } from '@grafana/data';
import { AdHocVariableFilter, LanguageProvider, SelectableValue, TimeRange } from '@grafana/data';
import { getTemplateSrv } from '@grafana/runtime';
import { VariableFormatID } from '@grafana/schema';
@@ -9,6 +9,7 @@ import {
getTagsByScope,
getUnscopedTags,
} from './SearchTraceQLEditor/utils';
import { DEFAULT_TIME_RANGE_FOR_TAGS } from './configuration/TagsTimeRangeSettings';
import { TraceqlFilter, TraceqlSearchScope } from './dataquery.gen';
import { TempoDatasource } from './datasource';
import { enumIntrinsics, intrinsicsV1 } from './traceql/traceql';
@@ -20,10 +21,19 @@ export const TAGS_LIMIT = 5000;
// Limit maximum options in select dropdowns
export const OPTIONS_LIMIT = 1000;
interface GetOptionsV2 {
tag: string;
query?: string;
timeRangeForTags?: number;
range?: TimeRange;
}
export default class TempoLanguageProvider extends LanguageProvider {
datasource: TempoDatasource;
tagsV1?: string[];
tagsV2?: Scope[];
private previousRange?: TimeRange;
constructor(datasource: TempoDatasource, initialValues?: any) {
super();
@@ -36,9 +46,15 @@ export default class TempoLanguageProvider extends LanguageProvider {
return res?.data;
};
start = async () => {
if (!this.startTask) {
this.startTask = this.fetchTags().then(() => {
start = async (range?: TimeRange, timeRangeForTags?: number) => {
// Check if we need to refetch tags due to range changes (minute-level granularity)
const shouldRefetch = this.shouldRefreshLabels(range, this.previousRange);
if (!this.startTask || shouldRefetch) {
// Store the current range for future comparison
this.previousRange = range;
this.startTask = this.fetchTags(timeRangeForTags, range).then(() => {
return [];
});
}
@@ -46,14 +62,42 @@ export default class TempoLanguageProvider extends LanguageProvider {
return this.startTask;
};
roundMsToMin = (milliseconds: number) => {
return this.roundSecToMin(milliseconds / 1000);
};
roundSecToMin = (seconds: number) => {
return Math.floor(seconds / 60);
};
shouldRefreshLabels = (range?: TimeRange, prevRange?: TimeRange): boolean => {
if (range && prevRange) {
const sameMinuteFrom = this.roundMsToMin(range.from.valueOf()) === this.roundMsToMin(prevRange.from.valueOf());
const sameMinuteTo = this.roundMsToMin(range.to.valueOf()) === this.roundMsToMin(prevRange.to.valueOf());
// If both are same, don't need to refresh
return !(sameMinuteFrom && sameMinuteTo);
}
// If one is defined and the other is not, we should refresh
return prevRange !== range;
};
getTagsLimit = () => {
return this.datasource.instanceSettings.jsonData?.tagLimit || TAGS_LIMIT;
};
async fetchTags() {
async fetchTags(timeRangeForTags?: number, range?: TimeRange) {
let v1Resp, v2Resp;
try {
v2Resp = await this.request('/api/v2/search/tags', { limit: this.getTagsLimit() });
const params: { limit: number; start?: number; end?: number } = {
limit: this.getTagsLimit(),
};
if (timeRangeForTags && range && timeRangeForTags !== DEFAULT_TIME_RANGE_FOR_TAGS) {
const { start, end } = this.getTimeRangeForTags(timeRangeForTags, range);
params.start = start;
params.end = end;
}
v2Resp = await this.request(`/api/v2/search/tags`, params);
} catch (error) {
v1Resp = await this.request('/api/search/tags', []);
}
@@ -144,14 +188,23 @@ export default class TempoLanguageProvider extends LanguageProvider {
return options;
}
async getOptionsV2(tag: string, query?: string): Promise<Array<SelectableValue<string>>> {
async getOptionsV2({ tag, query, timeRangeForTags, range }: GetOptionsV2): Promise<Array<SelectableValue<string>>> {
const encodedTag = this.encodeTag(tag);
const response = await this.request(
`/api/v2/search/tag/${encodedTag}/values`,
query
? { q: getTemplateSrv().replace(query, {}, VariableFormatID.Pipe), limit: this.getTagsLimit() }
: { limit: this.getTagsLimit() }
);
const params: { q?: string; limit: number; start?: number; end?: number } = {
limit: this.getTagsLimit(),
};
if (query) {
params.q = getTemplateSrv().replace(query, {}, VariableFormatID.Pipe);
}
if (timeRangeForTags && range && timeRangeForTags !== DEFAULT_TIME_RANGE_FOR_TAGS) {
const { start, end } = this.getTimeRangeForTags(timeRangeForTags, range);
params.start = start;
params.end = end;
}
const response = await this.request(`/api/v2/search/tag/${encodedTag}/values`, params);
let options: Array<SelectableValue<string>> = [];
if (response && response.tagValues) {
response.tagValues.forEach((v: { type: string; value?: string }) => {
@@ -167,6 +220,15 @@ export default class TempoLanguageProvider extends LanguageProvider {
return options;
}
getTimeRangeForTags = (timeRangeForTags: number, range: TimeRange) => {
// Get tags from the last timeRangeForTags seconds, but don't go before the start of the range
// If timeRangeForTags is 1 hour and your query range is 24 hours, it will fetch tags from the last 1 hour of that 24-hour period
// If timeRangeForTags is larger than the total range duration, it will use the entire available range
const start = Math.max(range.from.unix(), range.to.unix() - timeRangeForTags);
const end = range.to.unix();
return { start, end };
};
/**
* Encode (serialize) a given tag for use in a URL.
*
@@ -88,6 +88,7 @@ export function QueryEditor(props: Props) {
onChange={props.onChange}
datasource={props.datasource}
onRunQuery={props.onRunQuery}
range={props.range}
/>
<div className={styles.optionsContainer}>
<TempoQueryBuilderOptions
@@ -1,11 +1,12 @@
import { css } from '@emotion/css';
import { useEffect, useRef, useState } from 'react';
import { GrafanaTheme2 } from '@grafana/data';
import { GrafanaTheme2, TimeRange } from '@grafana/data';
import { TemporaryAlert } from '@grafana/o11y-ds-frontend';
import { reportInteraction } from '@grafana/runtime';
import { CodeEditor, Monaco, monacoTypes, useTheme2 } from '@grafana/ui';
import { DEFAULT_TIME_RANGE_FOR_TAGS } from '../configuration/TagsTimeRangeSettings';
import { TempoDatasource } from '../datasource';
import { TempoQuery } from '../types';
@@ -20,13 +21,19 @@ interface Props {
onRunQuery: () => void;
datasource: TempoDatasource;
readOnly?: boolean;
range?: TimeRange;
}
export function TraceQLEditor(props: Props) {
const [alertText, setAlertText] = useState<string>();
const { query, onChange, onRunQuery, placeholder } = props;
const setupAutocompleteFn = useAutocomplete(props.datasource, setAlertText);
const setupAutocompleteFn = useAutocomplete(
props.datasource,
setAlertText,
props.datasource.timeRangeForTags ?? DEFAULT_TIME_RANGE_FOR_TAGS,
props.range
);
const theme = useTheme2();
const styles = getStyles(theme, placeholder);
@@ -192,20 +199,34 @@ function setupAutoSize(editor: monacoTypes.editor.IStandaloneCodeEditor) {
* Hook that returns function that will set up monaco autocomplete for the label selector
* @param datasource the Tempo datasource instance
* @param setAlertText setter for alert's text
* @param timeRangeForTags time range for tags and tag values queries
* @param range time range
*/
function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: string) => void) {
function useAutocomplete(
datasource: TempoDatasource,
setAlertText: (text?: string) => void,
timeRangeForTags: number,
range?: TimeRange
) {
// We need the provider ref so we can pass it the label/values data later. This is because we run the call for the
// values here but there is additional setup needed for the provider later on. We could run the getSeries() in the
// returned function but that is run after the monaco is mounted so would delay the request a bit when it does not
// need to.
const providerRef = useRef<CompletionProvider>(
new CompletionProvider({ languageProvider: datasource.languageProvider, setAlertText })
new CompletionProvider({
languageProvider: datasource.languageProvider,
setAlertText,
timeRangeForTags,
range,
})
);
const previousRangeRef = useRef<TimeRange | undefined>(range);
useEffect(() => {
const fetchTags = async () => {
try {
await datasource.languageProvider.start();
await datasource.languageProvider.start(range, timeRangeForTags);
setAlertText(undefined);
} catch (error) {
if (error instanceof Error) {
@@ -214,7 +235,20 @@ function useAutocomplete(datasource: TempoDatasource, setAlertText: (text?: stri
}
};
fetchTags();
}, [datasource, setAlertText]);
}, [datasource, setAlertText, range, timeRangeForTags]);
useEffect(() => {
const rangeChanged = datasource.languageProvider.shouldRefreshLabels(range, previousRangeRef.current);
if (rangeChanged) {
providerRef.current.range = range;
previousRangeRef.current = range;
}
}, [range, datasource.languageProvider]);
useEffect(() => {
providerRef.current.timeRangeForTags = timeRangeForTags;
}, [timeRangeForTags]);
const autocompleteDisposeFun = useRef<(() => void) | null>(null);
useEffect(() => {
@@ -1,6 +1,6 @@
import { IMarkdownString, languages } from 'monaco-editor';
import { SelectableValue } from '@grafana/data';
import { SelectableValue, TimeRange } from '@grafana/data';
import { isFetchError } from '@grafana/runtime';
import type { Monaco, monacoTypes } from '@grafana/ui';
@@ -25,6 +25,8 @@ type CompletionItem = MinimalCompletionItem & {
interface Props {
languageProvider: TempoLanguageProvider;
setAlertText: (text?: string) => void;
timeRangeForTags?: number;
range?: TimeRange;
}
/**
@@ -38,11 +40,15 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
languageProvider: TempoLanguageProvider;
registerInteractionCommandId: string | null;
setAlertText: (text?: string) => void;
timeRangeForTags?: number;
range?: TimeRange;
constructor(props: Props) {
this.languageProvider = props.languageProvider;
this.setAlertText = props.setAlertText;
this.registerInteractionCommandId = null;
this.timeRangeForTags = props.timeRangeForTags;
this.range = props.range;
}
triggerCharacters = ['{', '.', '[', '(', '=', '~', ' ', '"'];
@@ -391,14 +397,24 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
this.registerInteractionCommandId = id;
}
private async getTagValues(tagName: string, query: string): Promise<Array<SelectableValue<string>>> {
private async getTagValues(
tagName: string,
query: string,
timeRangeForTags?: number,
range?: TimeRange
): Promise<Array<SelectableValue<string>>> {
let tagValues: Array<SelectableValue<string>>;
const cacheKey = `${tagName}:${query}`;
if (this.cachedValues.hasOwnProperty(cacheKey)) {
tagValues = this.cachedValues[cacheKey];
} else {
tagValues = await this.languageProvider.getOptionsV2(tagName, query);
tagValues = await this.languageProvider.getOptionsV2({
tag: tagName,
query,
timeRangeForTags,
range,
});
this.cachedValues[cacheKey] = tagValues;
}
return tagValues;
@@ -461,7 +477,7 @@ export class CompletionProvider implements monacoTypes.languages.CompletionItemP
case 'SPANSET_IN_VALUE':
let tagValues;
try {
tagValues = await this.getTagValues(situation.tagName, situation.query);
tagValues = await this.getTagValues(situation.tagName, situation.query, this.timeRangeForTags, this.range);
setAlertText(undefined);
} catch (error) {
if (isFetchError(error)) {
@@ -26,6 +26,7 @@ export interface TempoJsonData extends DataSourceJsonData {
streamingEnabled?: {
search?: boolean;
};
timeRangeForTags?: number;
}
export interface TempoQuery extends TempoBase {
@@ -18,7 +18,7 @@ export class TempoVariableSupport extends CustomVariableSupport<TempoDatasource,
throw new Error('Datasource not initialized');
}
const result = this.datasource.executeVariableQuery(request.targets[0]);
const result = this.datasource.executeVariableQuery(request.targets[0], request.range);
return from(result).pipe(map((data) => ({ data })));
}
}