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