{showHeaderForFirstTimeUsers &&
}
@@ -240,7 +610,12 @@ export function getTopSceneFor(metric?: string) {
}
}
-function getVariableSet(initialDS?: string, metric?: string, initialFilters?: AdHocVariableFilter[]) {
+function getVariableSet(
+ initialDS?: string,
+ metric?: string,
+ initialFilters?: AdHocVariableFilter[],
+ otelJoinQuery?: string
+) {
return new SceneVariableSet({
variables: [
new DataSourceVariable({
@@ -250,6 +625,24 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad
value: initialDS,
pluginId: 'prometheus',
}),
+ new CustomVariable({
+ name: VAR_OTEL_DEPLOYMENT_ENV,
+ label: 'Deployment environment',
+ hide: VariableHide.hideVariable,
+ value: undefined,
+ placeholder: 'Select',
+ isMulti: true,
+ }),
+ new AdHocFiltersVariable({
+ name: VAR_OTEL_RESOURCES,
+ label: 'Select resource attributes',
+ addFilterButtonText: 'Select resource attributes',
+ datasource: trailDS,
+ hide: VariableHide.hideVariable,
+ layout: 'vertical',
+ defaultKeys: [],
+ applyMode: 'manual',
+ }),
new AdHocFiltersVariable({
name: VAR_FILTERS,
addFilterButtonText: 'Add label',
@@ -258,9 +651,11 @@ function getVariableSet(initialDS?: string, metric?: string, initialFilters?: Ad
layout: config.featureToggles.newFiltersUI ? 'combobox' : 'vertical',
filters: initialFilters ?? [],
baseFilters: getBaseFiltersForMetric(metric),
+ applyMode: 'manual',
// since we only support prometheus datasources, this is always true
supportsMultiValueOperators: true,
}),
+ ...getVariablesWithOtelJoinQueryConstant(otelJoinQuery ?? ''),
],
});
}
diff --git a/public/app/features/trails/DataTrailSettings.tsx b/public/app/features/trails/DataTrailSettings.tsx
index 4b9a4e7ee0a..2584b7bb930 100644
--- a/public/app/features/trails/DataTrailSettings.tsx
+++ b/public/app/features/trails/DataTrailSettings.tsx
@@ -3,8 +3,12 @@ import { css } from '@emotion/css';
import { GrafanaTheme2 } from '@grafana/data';
import { SceneComponentProps, SceneObjectBase, SceneObjectState } from '@grafana/scenes';
import { Dropdown, Switch, ToolbarButton, useStyles2 } from '@grafana/ui';
+import { Trans } from '@grafana/ui/src/utils/i18n';
+import { MetricScene } from './MetricScene';
+import { MetricSelectScene } from './MetricSelect/MetricSelectScene';
import { reportExploreMetrics } from './interactions';
+import { getTrailFor } from './utils';
export interface DataTrailSettingsState extends SceneObjectState {
stickyMainGraph?: boolean;
@@ -29,19 +33,42 @@ export class DataTrailSettings extends SceneObjectBase
{
this.setState({ isOpen });
};
+ public onTogglePreviews = () => {
+ const trail = getTrailFor(this);
+ trail.setState({ showPreviews: !trail.state.showPreviews });
+ };
+
static Component = ({ model }: SceneComponentProps) => {
const { stickyMainGraph, isOpen } = model.useState();
const styles = useStyles2(getStyles);
+ const trail = getTrailFor(model);
+
+ const { showPreviews, topScene } = trail.useState();
+
const renderPopover = () => {
return (
/* eslint-disable-next-line jsx-a11y/no-static-element-interactions, jsx-a11y/click-events-have-key-events */
evt.stopPropagation()}>
Settings
-
-
Always keep selected metric graph in-view
-
-
+ {topScene instanceof MetricScene && (
+
+
+
+ Always keep selected metric graph in-view
+
+
+
+
+ )}
+ {topScene instanceof MetricSelectScene && (
+
+
+ Show previews of metric graphs
+
+
+
+ )}
);
};
diff --git a/public/app/features/trails/DataTrailsHistory.tsx b/public/app/features/trails/DataTrailsHistory.tsx
index 322cbca0b55..26b3d6b4e05 100644
--- a/public/app/features/trails/DataTrailsHistory.tsx
+++ b/public/app/features/trails/DataTrailsHistory.tsx
@@ -19,13 +19,15 @@ import { Stack, Tooltip, useStyles2 } from '@grafana/ui';
import { DataTrail, DataTrailState, getTopSceneFor } from './DataTrail';
import { SerializedTrailHistory } from './TrailStore/TrailStore';
import { reportExploreMetrics } from './interactions';
-import { VAR_FILTERS } from './shared';
+import { VAR_FILTERS, VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_RESOURCES } from './shared';
import { getTrailFor, isSceneTimeRangeState } from './utils';
export interface DataTrailsHistoryState extends SceneObjectState {
currentStep: number;
steps: DataTrailHistoryStep[];
filtersApplied: string[];
+ otelResources: string[];
+ otelDepEnvs: string[];
}
export function isDataTrailsHistoryState(state: SceneObjectState): state is DataTrailsHistoryState {
@@ -46,7 +48,7 @@ export interface DataTrailHistoryStep {
parentIndex: number;
}
-export type TrailStepType = 'filters' | 'time' | 'metric' | 'start' | 'metric_page';
+export type TrailStepType = 'filters' | 'time' | 'metric' | 'start' | 'metric_page' | 'dep_env' | 'resource';
const filterSubst = ` $2 `;
const filterPipeRegex = /(\|)(=|=~|!=|>|<|!~)(\|)/g;
@@ -56,11 +58,19 @@ const stepDescriptionMap: Record = {
metric_page: 'Metric select page',
filters: 'Filter applied:',
time: 'Time range changed:',
+ dep_env: 'Deployment environment selected:',
+ resource: 'Resource attribute selected:',
};
export class DataTrailHistory extends SceneObjectBase {
public constructor(state: Partial) {
- super({ steps: state.steps ?? [], currentStep: state.currentStep ?? 0, filtersApplied: [] });
+ super({
+ steps: state.steps ?? [],
+ currentStep: state.currentStep ?? 0,
+ filtersApplied: [],
+ otelResources: [],
+ otelDepEnvs: [],
+ });
this.addActivationHandler(this._onActivate.bind(this));
}
@@ -113,6 +123,20 @@ export class DataTrailHistory extends SceneObjectBase {
this.addTrailStep(trail, 'filters', parseFilterTooltip(urlState, filtersApplied));
this.setState({ filtersApplied });
}
+
+ if (evt.payload.state.name === VAR_OTEL_DEPLOYMENT_ENV) {
+ const otelDepEnvs = this.state.otelDepEnvs;
+ const urlState = sceneUtils.getUrlState(trail);
+ this.addTrailStep(trail, 'dep_env', parseDepEnvTooltip(urlState, otelDepEnvs));
+ this.setState({ otelDepEnvs });
+ }
+
+ if (evt.payload.state.name === VAR_OTEL_RESOURCES) {
+ const otelResources = this.state.otelResources;
+ const urlState = sceneUtils.getUrlState(trail);
+ this.addTrailStep(trail, 'resource', parseOtelResourcesTooltip(urlState, otelResources));
+ this.setState({ otelResources });
+ }
});
trail.subscribeToEvent(SceneObjectStateChangedEvent, (evt) => {
@@ -172,6 +196,8 @@ export class DataTrailHistory extends SceneObjectBase {
const stepIndex = this.state.steps.length;
const parentIndex = type === 'start' ? -1 : this.state.currentStep;
const filtersApplied = this.state.filtersApplied;
+ const otelResources = this.state.otelResources;
+ const otelDepEnvs = this.state.otelDepEnvs;
let detail = '';
switch (step.type) {
@@ -184,10 +210,16 @@ export class DataTrailHistory extends SceneObjectBase {
case 'time':
detail = parseTimeTooltip(step.urlValues);
break;
+ case 'dep_env':
+ detail = parseDepEnvTooltip(step.urlValues, otelDepEnvs);
+ case 'resource':
+ detail = parseOtelResourcesTooltip(step.urlValues, otelResources);
}
this.setState({
filtersApplied,
+ otelDepEnvs,
+ otelResources,
currentStep: stepIndex,
steps: [
...this.state.steps,
@@ -336,6 +368,46 @@ export function parseFilterTooltip(urlValues: SceneObjectUrlValues, filtersAppli
return detail.replace(filterPipeRegex, filterSubst);
}
+export function parseOtelResourcesTooltip(urlValues: SceneObjectUrlValues, otelResources: string[]): string {
+ let detail = '';
+ const varOtelResources = urlValues['var-otel_resources'];
+ if (isDataTrailHistoryFilter(varOtelResources)) {
+ detail =
+ varOtelResources.filter((f) => {
+ if (f !== '' && !otelResources.includes(f)) {
+ otelResources.push(f);
+ return true;
+ }
+ return false;
+ })[0] ?? '';
+ }
+ // filters saved as key|operator|value
+ // we need to remove pipes (|)
+ return detail.replace(filterPipeRegex, filterSubst);
+}
+
+export function parseDepEnvTooltip(urlValues: SceneObjectUrlValues, otelDepEnvs: string[]): string {
+ let detail = '';
+ const varDepEnv = urlValues['var-deployment_environment'];
+
+ if (typeof varDepEnv === 'string') {
+ return varDepEnv;
+ }
+
+ if (isDataTrailHistoryFilter(varDepEnv)) {
+ detail =
+ varDepEnv?.filter((f) => {
+ if (f !== '' && !otelDepEnvs.includes(f)) {
+ otelDepEnvs.push(f);
+ return true;
+ }
+ return false;
+ })[0] ?? '';
+ }
+
+ return detail;
+}
+
function getStyles(theme: GrafanaTheme2) {
const visTheme = theme.visualization;
@@ -408,6 +480,8 @@ function getStyles(theme: GrafanaTheme2) {
metric: generateStepTypeStyle(visTheme.getColorByName('orange')),
metric_page: generateStepTypeStyle(visTheme.getColorByName('orange')),
time: generateStepTypeStyle(theme.colors.primary.main),
+ resource: generateStepTypeStyle(visTheme.getColorByName('purple')),
+ dep_env: generateStepTypeStyle(visTheme.getColorByName('purple')),
},
};
}
diff --git a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
index b629f1f9b7e..f6a5001689e 100644
--- a/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
+++ b/public/app/features/trails/MetricSelect/MetricSelectScene.tsx
@@ -34,6 +34,7 @@ import { StatusWrapper } from '../StatusWrapper';
import { Node, Parser } from '../groop/parser';
import { getMetricDescription } from '../helpers/MetricDatasourceHelper';
import { reportExploreMetrics } from '../interactions';
+import { limitOtelMatchTerms } from '../otel/util';
import {
getVariablesWithMetricConstant,
MetricSelectedEvent,
@@ -62,7 +63,6 @@ export interface MetricSelectSceneState extends SceneObjectState {
body: SceneFlexLayout | SceneCSSGridLayout;
rootGroup?: Node;
metricPrefix?: string;
- showPreviews?: boolean;
metricNames?: string[];
metricNamesLoading?: boolean;
metricNamesError?: string;
@@ -85,7 +85,6 @@ export class MetricSelectScene extends SceneObjectBase i
constructor(state: Partial) {
super({
- showPreviews: true,
$variables: state.$variables,
metricPrefix: state.metricPrefix ?? METRIC_PREFIX_ALL,
body:
@@ -182,6 +181,34 @@ export class MetricSelectScene extends SceneObjectBase i
}
});
+ this._subs.add(
+ trail.subscribeToState(({ otelTargets }, oldState) => {
+ // if the otel targets have changed, get the new list of metrics
+ if (
+ otelTargets?.instances !== oldState.otelTargets?.instances &&
+ otelTargets?.jobs !== oldState.otelTargets?.jobs
+ ) {
+ this._debounceRefreshMetricNames();
+ }
+ })
+ );
+
+ this._subs.add(
+ trail.subscribeToState(({ useOtelExperience }, oldState) => {
+ // users will most likely not switch this off but for now,
+ // update metric names when changing useOtelExperience
+ this._debounceRefreshMetricNames();
+ })
+ );
+
+ this._subs.add(
+ trail.subscribeToState(({ showPreviews }, oldState) => {
+ // move showPreviews into the settings
+ // build layout when toggled
+ this.buildLayout();
+ })
+ );
+
this._debounceRefreshMetricNames();
}
@@ -193,7 +220,7 @@ export class MetricSelectScene extends SceneObjectBase i
return;
}
- const matchTerms = [];
+ const matchTerms: string[] = [];
const filtersVar = sceneGraph.lookupVariable(VAR_FILTERS, this);
const hasFilters = filtersVar instanceof AdHocFiltersVariable && filtersVar.getValue()?.valueOf();
@@ -206,6 +233,26 @@ export class MetricSelectScene extends SceneObjectBase i
matchTerms.push(`__name__=~"${metricSearchRegex}"`);
}
+ let noOtelMetrics = false;
+ let missingOtelTargets = false;
+
+ if (trail.state.useOtelExperience) {
+ const jobsList = trail.state.otelTargets?.jobs;
+ const instancesList = trail.state.otelTargets?.instances;
+ // no targets have this combination of filters so there are no metrics that can be joined
+ // show no metrics
+ if (jobsList && jobsList.length > 0 && instancesList && instancesList.length > 0) {
+ const otelMatches = limitOtelMatchTerms(matchTerms, jobsList, instancesList, missingOtelTargets);
+
+ missingOtelTargets = otelMatches.missingOtelTargets;
+
+ matchTerms.push(otelMatches.jobsRegex);
+ matchTerms.push(otelMatches.instancesRegex);
+ } else {
+ noOtelMetrics = true;
+ }
+ }
+
const match = `{${matchTerms.join(',')}}`;
const datasourceUid = sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
this.setState({ metricNamesLoading: true, metricNamesError: undefined, metricNamesWarning: undefined });
@@ -227,12 +274,23 @@ export class MetricSelectScene extends SceneObjectBase i
metricNames = metricNames.filter((metric) => !prefixRegex || prefixRegex.test(metric));
}
- const metricNamesWarning = response.limitReached
+ let metricNamesWarning = response.limitReached
? `This feature will only return up to ${MAX_METRIC_NAMES} metric names for performance reasons. ` +
`This limit is being exceeded for the current data source. ` +
`Add search terms or label filters to narrow down the number of metric names returned.`
: undefined;
+ // if there are no otel targets for otel resources, there will be no labels
+ if (noOtelMetrics) {
+ metricNames = [];
+ metricNamesWarning = undefined;
+ }
+
+ if (missingOtelTargets) {
+ metricNamesWarning +=
+ 'The list of metrics is not complete. Select more OTel resource attributes to see a full list of metrics.';
+ }
+
let bodyLayout = this.state.body;
let rootGroupNode = this.state.rootGroup;
@@ -340,6 +398,8 @@ export class MetricSelectScene extends SceneObjectBase i
}
private async buildLayout() {
+ const trail = getTrailFor(this);
+ const showPreviews = trail.state.showPreviews;
// Temp hack when going back to select metric scene and variable updates
if (this.ignoreNextUpdate) {
this.ignoreNextUpdate = false;
@@ -348,8 +408,6 @@ export class MetricSelectScene extends SceneObjectBase i
const children: SceneFlexItem[] = [];
- const trail = getTrailFor(this);
-
const metricsList = this.sortedPreviewMetrics();
// Get the current filters to determine the count of them
@@ -362,7 +420,7 @@ export class MetricSelectScene extends SceneObjectBase i
const metadata = await trail.getMetricMetadata(metric.name);
const description = getMetricDescription(metadata);
- if (this.state.showPreviews) {
+ if (showPreviews) {
if (metric.itemRef && metric.isPanel) {
children.push(metric.itemRef.resolve());
continue;
@@ -385,7 +443,7 @@ export class MetricSelectScene extends SceneObjectBase i
}
}
- const rowTemplate = this.state.showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT;
+ const rowTemplate = showPreviews ? ROW_PREVIEW_HEIGHT : ROW_CARD_HEIGHT;
this.state.body.setState({ children, autoRows: rowTemplate });
}
@@ -426,29 +484,23 @@ export class MetricSelectScene extends SceneObjectBase i
});
};
- public onTogglePreviews = () => {
- this.setState({ showPreviews: !this.state.showPreviews });
- this.buildLayout();
+ public onToggleOtelExperience = () => {
+ const trail = getTrailFor(this);
+ const useOtelExperience = trail.state.useOtelExperience;
+
+ trail.setState({ useOtelExperience: !useOtelExperience });
};
public static Component = ({ model }: SceneComponentProps) => {
- const {
- showPreviews,
- body,
- metricNames,
- metricNamesError,
- metricNamesLoading,
- metricNamesWarning,
- rootGroup,
- metricPrefix,
- } = model.useState();
+ const { body, metricNames, metricNamesError, metricNamesLoading, metricNamesWarning, rootGroup, metricPrefix } =
+ model.useState();
const { children } = body.useState();
const trail = getTrailFor(model);
const styles = useStyles2(getStyles);
const [warningDismissed, dismissWarning] = useReducer(() => true, false);
- const { metricSearch } = trail.useState();
+ const { metricSearch, useOtelExperience, hasOtelResources, isStandardOtel } = trail.useState();
const tooStrict = children.length === 0 && metricSearch;
const noMetrics = !metricNamesLoading && metricNames && metricNames.length === 0;
@@ -509,7 +561,40 @@ export class MetricSelectScene extends SceneObjectBase i
]}
/>
-
+ {hasOtelResources && (
+
+ Filter by
+
+ This switch enables filtering by OTel resources for OTel native data sources.
+
+ }
+ />
+
+ }
+ className={styles.displayOption}
+ >
+