mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
Explore Metrics: Get OTel resources and filter metrics and labels (#91221)
* add OTel filter in metric select scene * add resource query to get matching OTEL job&instance * filter metrics by OTEL resources * only add otel select if DS has OTEL matching job and instance * add folder for otel resources * upate metric select for new otel folder * move otel api call * get otel resources for labels for single series job/instance target_info * add otel resources to adhoc variable dropdown * update otel api to check for standardization and return labels * label types for api * check standardization, show otel variable, select depenv, update other variables * remove otel target list from metric select scene * load resources if dep_env label has already been selected * exclude previously used filters * do not check standardization if there are already otel filters * drop filters when switching data sources * add experience var for switching to otel experience * remove otel from variables and place near settings * add error for non-standard prom with otel resources * fix typescript errors, remove ts-ignores * add custom variable for deployment environment like app-olly * fix name of otel variable * add function for getting otel resources from variables * add otel join query const * update standard check to be simpler * allow for unstandard otel data sources but give warning * add otelJoinQuery to the base query and clean up variables when state changes * refactor otel functions to return filters for targets, use targets to filter metrics * update metric names on otel target filter change * when no otel targets for otel resource filter, show no metrics * move switch to settings, default to use experience, refactor otel checks * clean code * fix refactor to add hasOtelResources for showing the switch in settings * sort otel resources by blessed list * reset otel when data source is changed * move otel experience toggle back outside settings * move showPreviews into settings * do not re-add otel resources from blessed list to filters when already selected * add otel join query variable to histogram base query * only show settings for appropriate scenes * show info tooltip the same but show error on hover for disabling otel exp for unstandard DS * refactor tagKeys and tagValues for otel resources variable, fix promoted list ordering, fix dep env state bug * default dep env value * apply var filters only where they are using VAR_FILTER_EXPR in queryies * change copy for labels to attributes * do not group_left job label when already joining by job * update copy for label variable when using otel * remove isStandard check for now because of data staleness in Prometheus * default to showing heatmap for histograms * add trail history for selecting dep env and otel resources * add otel resource attributes tests for DataTrail * move otel functions to utils * write tests for otel api calls * write tests for otel utils functions * fix history * standard otel has target_info metric and deployment_environment resource attributes * fix tests * refactor otel functions for updating state and variables * clean code * fix tests * fix tests * mock checkDataSourceForOtelResources * fix tests * update query tests with otelJoinQuery and default to heatmap for _bucket metrics * fix tests for otel api * fix trail history test * fix trail store tests for missing otel variables * make i18n-extract * handle target_info with inconsistent job and instance labels * fix otel copy and <Trans> component * fix custom variable deployment environment bug when switchiing data sources from non otel to otel * fix linting error for trans component * format i18nKey correctly * clean up old comments * add frontend hardening for OTel job and instance metric list filtering * fix test for deployment environment custom variable to use changeValueTo * fix i18n * remove comments for fixed bug * edit skipped tests
This commit is contained in:
parent
542105b680
commit
4d1adf9db4
@ -5279,8 +5279,7 @@ exports[`better eslint`] = {
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
],
|
||||
"public/app/features/trails/DataTrailSettings.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"],
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "1"]
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
],
|
||||
"public/app/features/trails/DataTrailsHistory.tsx:5381": [
|
||||
[0, 0, 0, "No untranslated strings. Wrap text with <Trans />", "0"]
|
||||
|
@ -211,13 +211,15 @@ export class BreakdownScene extends SceneObjectBase<BreakdownSceneState> {
|
||||
const { labels, body, loading, value, blockingMessage } = model.useState();
|
||||
const styles = useStyles2(getStyles);
|
||||
|
||||
const { useOtelExperience } = getTrailFor(model).useState();
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
<StatusWrapper {...{ isLoading: loading, blockingMessage }}>
|
||||
<div className={styles.controls}>
|
||||
{!loading && labels.length && (
|
||||
<div className={styles.controlsLeft}>
|
||||
<Field label="By label">
|
||||
<Field label={useOtelExperience ? 'By metric attribute' : 'By label'}>
|
||||
<BreakdownLabelSelector options={labels} value={value} onChange={model.onChange} />
|
||||
</Field>
|
||||
</div>
|
||||
|
@ -68,6 +68,8 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
|
||||
const variable = model.getVariable();
|
||||
const { loading: labelsLoading, options: labelOptions } = variable.useState();
|
||||
|
||||
const { useOtelExperience } = getTrailFor(model).useState();
|
||||
|
||||
// Get unit name from the metric name
|
||||
const metricScene = getMetricSceneFor(model);
|
||||
const metric = metricScene.state.metric;
|
||||
@ -110,7 +112,11 @@ export class MetricOverviewScene extends SceneObjectBase<MetricOverviewSceneStat
|
||||
</Stack>
|
||||
<Stack direction="column" gap={0.5}>
|
||||
<Text weight={'medium'}>
|
||||
<Trans i18nKey="trails.metric-overview.labels-label">Labels</Trans>
|
||||
{useOtelExperience ? (
|
||||
<Trans i18nKey="trails.metric-overview.metric-attributes">Metric attributes</Trans>
|
||||
) : (
|
||||
<Trans i18nKey="trails.metric-overview.labels">Labels</Trans>
|
||||
)}
|
||||
</Text>
|
||||
{labelOptions.length === 0 && 'Unable to fetch labels.'}
|
||||
{labelOptions.map((l) => (
|
||||
|
@ -4,6 +4,8 @@ function expandExpr(shortenedExpr: string) {
|
||||
return shortenedExpr.replace('...', '${metric}{${filters}}');
|
||||
}
|
||||
|
||||
const otelJoinQuery = '${otel_join_query}';
|
||||
|
||||
describe('getAutoQueriesForMetric', () => {
|
||||
describe('for the summary/histogram types', () => {
|
||||
const etc = '{${filters}}[$__rate_interval]';
|
||||
@ -14,19 +16,20 @@ describe('getAutoQueriesForMetric', () => {
|
||||
|
||||
test('main query is the mean', () => {
|
||||
const [{ expr }] = result.main.queries;
|
||||
const mean = `sum(rate(SUM_OR_HIST_sum${etc}))/sum(rate(SUM_OR_HIST_count${etc}))`;
|
||||
const mean = `sum(rate(SUM_OR_HIST_sum${etc}) ${otelJoinQuery})/sum(rate(SUM_OR_HIST_count${etc}) ${otelJoinQuery})`;
|
||||
|
||||
expect(expr).toBe(mean);
|
||||
});
|
||||
|
||||
test('preview query is the mean', () => {
|
||||
const [{ expr }] = result.preview.queries;
|
||||
const mean = `sum(rate(SUM_OR_HIST_sum${etc}))/sum(rate(SUM_OR_HIST_count${etc}))`;
|
||||
const mean = `sum(rate(SUM_OR_HIST_sum${etc}) ${otelJoinQuery})/sum(rate(SUM_OR_HIST_count${etc}) ${otelJoinQuery})`;
|
||||
expect(expr).toBe(mean);
|
||||
});
|
||||
|
||||
test('breakdown query is the mean by group', () => {
|
||||
const [{ expr }] = result.breakdown.queries;
|
||||
const meanBreakdown = `sum(rate(SUM_OR_HIST_sum${etc}))${byGroup}/sum(rate(SUM_OR_HIST_count${etc}))${byGroup}`;
|
||||
const meanBreakdown = `sum(rate(SUM_OR_HIST_sum${etc}) ${otelJoinQuery})${byGroup}/sum(rate(SUM_OR_HIST_count${etc}) ${otelJoinQuery})${byGroup}`;
|
||||
expect(expr).toBe(meanBreakdown);
|
||||
});
|
||||
|
||||
@ -40,19 +43,19 @@ describe('getAutoQueriesForMetric', () => {
|
||||
|
||||
test('main query is an overall rate', () => {
|
||||
const [{ expr }] = result.main.queries;
|
||||
const overallRate = `sum(rate(\${metric}${etc}))`;
|
||||
const overallRate = `sum(rate(\${metric}${etc}) ${otelJoinQuery})`;
|
||||
expect(expr).toBe(overallRate);
|
||||
});
|
||||
|
||||
test('preview query is an overall rate', () => {
|
||||
const [{ expr }] = result.preview.queries;
|
||||
const overallRate = `sum(rate(\${metric}${etc}))`;
|
||||
const overallRate = `sum(rate(\${metric}${etc}) ${otelJoinQuery})`;
|
||||
expect(expr).toBe(overallRate);
|
||||
});
|
||||
|
||||
test('breakdown query is an overall rate by group', () => {
|
||||
const [{ expr }] = result.breakdown.queries;
|
||||
const overallRateBreakdown = `sum(rate(\${metric}${etc}))${byGroup}`;
|
||||
const overallRateBreakdown = `sum(rate(\${metric}${etc}) ${otelJoinQuery})${byGroup}`;
|
||||
expect(expr).toBe(overallRateBreakdown);
|
||||
});
|
||||
|
||||
@ -61,6 +64,7 @@ describe('getAutoQueriesForMetric', () => {
|
||||
});
|
||||
});
|
||||
|
||||
// ***WE DEFAULT TO HEATMAP HERE
|
||||
describe('metrics with _bucket suffix', () => {
|
||||
const result = getAutoQueriesForMetric('HIST_bucket');
|
||||
|
||||
@ -99,9 +103,10 @@ describe('getAutoQueriesForMetric', () => {
|
||||
});
|
||||
});
|
||||
|
||||
test('preview panel has 50th percentile query', () => {
|
||||
test('preview panel has heatmap query', () => {
|
||||
const [{ expr }] = result.preview.queries;
|
||||
expect(expr).toBe(percentileQueries.get(50));
|
||||
const expected = 'sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})';
|
||||
expect(expr).toBe(expected);
|
||||
});
|
||||
|
||||
const percentileGroupedQueries = new Map<number, string>();
|
||||
@ -130,34 +135,35 @@ describe('getAutoQueriesForMetric', () => {
|
||||
describe('Consider result.main query (only first)', () => {
|
||||
it.each([
|
||||
// no rate
|
||||
['PREFIX_general', 'avg(...)', 'short', 1],
|
||||
['PREFIX_bytes', 'avg(...)', 'bytes', 1],
|
||||
['PREFIX_seconds', 'avg(...)', 's', 1],
|
||||
['PREFIX_general', 'avg(... ${otel_join_query})', 'short', 1],
|
||||
['PREFIX_bytes', 'avg(... ${otel_join_query})', 'bytes', 1],
|
||||
['PREFIX_seconds', 'avg(... ${otel_join_query})', 's', 1],
|
||||
// rate with counts per second
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]))', 'cps', 1],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps', 1],
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps', 1], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps', 1],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps', 1],
|
||||
// rate with seconds per second
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short', 1], // s/s
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'short', 1], // s/s
|
||||
// rate with bytes per second
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps', 1], // bytes/s
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'Bps', 1], // bytes/s
|
||||
// mean with non-rated units
|
||||
[
|
||||
'PREFIX_seconds_sum',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})',
|
||||
's',
|
||||
1,
|
||||
],
|
||||
[
|
||||
'PREFIX_bytes_sum',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]) ${otel_join_query})',
|
||||
'bytes',
|
||||
1,
|
||||
],
|
||||
// ***WE DEFAULT TO HEATMAP HERE
|
||||
// Bucket
|
||||
['PREFIX_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'short', 3],
|
||||
['PREFIX_seconds_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 's', 3],
|
||||
['PREFIX_bytes_bucket', 'histogram_quantile(0.99, sum by(le) (rate(...[$__rate_interval])))', 'bytes', 3],
|
||||
['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short', 1],
|
||||
['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's', 1],
|
||||
['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes', 1],
|
||||
])('Given metric %p expect %p with unit %p', (metric, expr, unit, queryCount) => {
|
||||
const result = getAutoQueriesForMetric(metric);
|
||||
|
||||
@ -173,32 +179,32 @@ describe('getAutoQueriesForMetric', () => {
|
||||
describe('Consider result.preview query (only first)', () => {
|
||||
it.each([
|
||||
// no rate
|
||||
['PREFIX_general', 'avg(...)', 'short'],
|
||||
['PREFIX_bytes', 'avg(...)', 'bytes'],
|
||||
['PREFIX_seconds', 'avg(...)', 's'],
|
||||
['PREFIX_general', 'avg(... ${otel_join_query})', 'short'],
|
||||
['PREFIX_bytes', 'avg(... ${otel_join_query})', 'bytes'],
|
||||
['PREFIX_seconds', 'avg(... ${otel_join_query})', 's'],
|
||||
// rate with counts per second
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]))', 'cps'], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]))', 'cps'],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))', 'cps'],
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps'], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps'],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'cps'],
|
||||
// rate with seconds per second
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))', 'short'], // s/s
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'short'], // s/s
|
||||
// rate with bytes per second
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))', 'Bps'], // bytes/s
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})', 'Bps'], // bytes/s
|
||||
// mean with non-rated units
|
||||
[
|
||||
'PREFIX_seconds_sum',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})',
|
||||
's',
|
||||
],
|
||||
[
|
||||
'PREFIX_bytes_sum',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]) ${otel_join_query})/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]) ${otel_join_query})',
|
||||
'bytes',
|
||||
],
|
||||
// Bucket
|
||||
['PREFIX_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'short'],
|
||||
['PREFIX_seconds_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 's'],
|
||||
['PREFIX_bytes_bucket', 'histogram_quantile(0.5, sum by(le) (rate(...[$__rate_interval])))', 'bytes'],
|
||||
['PREFIX_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'short'],
|
||||
['PREFIX_seconds_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 's'],
|
||||
['PREFIX_bytes_bucket', 'sum by(le) (rate(...[$__rate_interval])${otel_join_query})', 'bytes'],
|
||||
])('Given metric %p expect %p with unit %p', (metric, expr, unit) => {
|
||||
const result = getAutoQueriesForMetric(metric);
|
||||
|
||||
@ -216,32 +222,44 @@ describe('getAutoQueriesForMetric', () => {
|
||||
describe('Consider result.breakdown query (only first)', () => {
|
||||
it.each([
|
||||
// no rate
|
||||
['PREFIX_general', 'avg(...)by(${groupby})', 'short'],
|
||||
['PREFIX_bytes', 'avg(...)by(${groupby})', 'bytes'],
|
||||
['PREFIX_seconds', 'avg(...)by(${groupby})', 's'],
|
||||
['PREFIX_general', 'avg(... ${otel_join_query})by(${groupby})', 'short'],
|
||||
['PREFIX_bytes', 'avg(... ${otel_join_query})by(${groupby})', 'bytes'],
|
||||
['PREFIX_seconds', 'avg(... ${otel_join_query})by(${groupby})', 's'],
|
||||
// rate with counts per second
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'cps'],
|
||||
['PREFIX_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})by(${groupby})', 'cps'], // cps = counts per second
|
||||
['PREFIX_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})by(${groupby})', 'cps'],
|
||||
['PREFIX_seconds_count', 'sum(rate(...[$__rate_interval]) ${otel_join_query})by(${groupby})', 'cps'],
|
||||
// rate with seconds per second
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'short'], // s/s
|
||||
['PREFIX_seconds_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})by(${groupby})', 'short'], // s/s
|
||||
// rate with bytes per second
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]))by(${groupby})', 'Bps'], // bytes/s
|
||||
['PREFIX_bytes_total', 'sum(rate(...[$__rate_interval]) ${otel_join_query})by(${groupby})', 'Bps'], // bytes/s
|
||||
// mean with non-rated units
|
||||
[
|
||||
'PREFIX_seconds_sum',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]))by(${groupby})/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]))by(${groupby})',
|
||||
'sum(rate(PREFIX_seconds_sum{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate(PREFIX_seconds_count{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})',
|
||||
's',
|
||||
],
|
||||
[
|
||||
'PREFIX_bytes_sum',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]))by(${groupby})/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]))by(${groupby})',
|
||||
'sum(rate(PREFIX_bytes_sum{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})/sum(rate(PREFIX_bytes_count{${filters}}[$__rate_interval]) ${otel_join_query})by(${groupby})',
|
||||
'bytes',
|
||||
],
|
||||
// Bucket
|
||||
['PREFIX_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'short'],
|
||||
['PREFIX_seconds_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 's'],
|
||||
['PREFIX_bytes_bucket', 'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])))', 'bytes'],
|
||||
[
|
||||
'PREFIX_bucket',
|
||||
'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))',
|
||||
'short',
|
||||
],
|
||||
[
|
||||
'PREFIX_seconds_bucket',
|
||||
'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))',
|
||||
's',
|
||||
],
|
||||
[
|
||||
'PREFIX_bytes_bucket',
|
||||
'histogram_quantile(0.5, sum by(le, ${groupby}) (rate(...[$__rate_interval])${otel_join_query}))',
|
||||
'bytes',
|
||||
],
|
||||
])('Given metric %p expect %p with unit %p', (metric, expr, unit) => {
|
||||
const result = getAutoQueriesForMetric(metric);
|
||||
|
||||
@ -274,15 +292,15 @@ describe('getAutoQueriesForMetric', () => {
|
||||
variant: 'percentiles',
|
||||
unit: 'short',
|
||||
exprs: [
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'heatmap',
|
||||
unit: 'short',
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]))'],
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'],
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -293,15 +311,15 @@ describe('getAutoQueriesForMetric', () => {
|
||||
variant: 'percentiles',
|
||||
unit: 's',
|
||||
exprs: [
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'heatmap',
|
||||
unit: 's',
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]))'],
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'],
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -312,15 +330,15 @@ describe('getAutoQueriesForMetric', () => {
|
||||
variant: 'percentiles',
|
||||
unit: 'bytes',
|
||||
exprs: [
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])))',
|
||||
'histogram_quantile(0.99, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.9, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
'histogram_quantile(0.5, sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query}))',
|
||||
],
|
||||
},
|
||||
{
|
||||
variant: 'heatmap',
|
||||
unit: 'bytes',
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval]))'],
|
||||
exprs: ['sum by(le) (rate(${metric}{${filters}}[$__rate_interval])${otel_join_query})'],
|
||||
},
|
||||
],
|
||||
],
|
||||
@ -338,7 +356,7 @@ describe('getAutoQueriesForMetric', () => {
|
||||
});
|
||||
|
||||
describe('Able to handle unconventional metric names', () => {
|
||||
it.each([['PRODUCT_High_Priority_items_', 'avg(...)', 'short', 1]])(
|
||||
it.each([['PRODUCT_High_Priority_items_', 'avg(... ${otel_join_query})', 'short', 1]])(
|
||||
'Given metric %p expect %p with unit %p',
|
||||
(metric, expr, unit, queryCount) => {
|
||||
const result = getAutoQueriesForMetric(metric);
|
||||
|
@ -1,8 +1,10 @@
|
||||
import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR } from 'app/features/trails/shared';
|
||||
import { VAR_METRIC_EXPR, VAR_FILTERS_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from 'app/features/trails/shared';
|
||||
|
||||
const GENERAL_BASE_QUERY = `${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}`;
|
||||
const GENERAL_RATE_BASE_QUERY = `rate(${GENERAL_BASE_QUERY}[$__rate_interval])`;
|
||||
|
||||
export function getGeneralBaseQuery(rate: boolean) {
|
||||
return rate ? GENERAL_RATE_BASE_QUERY : GENERAL_BASE_QUERY;
|
||||
return rate
|
||||
? `${GENERAL_RATE_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}`
|
||||
: `${GENERAL_BASE_QUERY} ${VAR_OTEL_JOIN_QUERY_EXPR}`;
|
||||
}
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { PromQuery } from '@grafana/prometheus';
|
||||
|
||||
import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR } from '../../shared';
|
||||
import { VAR_FILTERS_EXPR, VAR_GROUP_BY_EXP, VAR_METRIC_EXPR, VAR_OTEL_JOIN_QUERY_EXPR } from '../../shared';
|
||||
import { heatmapGraphBuilder } from '../graph-builders/heatmap';
|
||||
import { percentilesGraphBuilder } from '../graph-builders/percentiles';
|
||||
import { simpleGraphBuilder } from '../graph-builders/simple';
|
||||
@ -47,10 +47,10 @@ export function createHistogramMetricQueryDefs(metricParts: string[]) {
|
||||
vizBuilder: () => heatmapGraphBuilder(heatmap),
|
||||
};
|
||||
|
||||
return { preview: p50, main: percentiles, variants: [percentiles, heatmap], breakdown: breakdown };
|
||||
return { preview: heatmap, main: heatmap, variants: [percentiles, heatmap], breakdown: breakdown };
|
||||
}
|
||||
|
||||
const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])`;
|
||||
const BASE_QUERY = `rate(${VAR_METRIC_EXPR}${VAR_FILTERS_EXPR}[$__rate_interval])${VAR_OTEL_JOIN_QUERY_EXPR}`;
|
||||
|
||||
function baseQuery(groupings: string[] = []) {
|
||||
const sumByList = ['le', ...groupings];
|
||||
|
@ -1,5 +1,6 @@
|
||||
import { VariableHide } from '@grafana/data';
|
||||
import { locationService, setDataSourceSrv } from '@grafana/runtime';
|
||||
import { AdHocFiltersVariable, sceneGraph } from '@grafana/scenes';
|
||||
import { AdHocFiltersVariable, ConstantVariable, CustomVariable, sceneGraph } from '@grafana/scenes';
|
||||
import { DataSourceType } from 'app/features/alerting/unified/utils/datasource';
|
||||
|
||||
import { MockDataSourceSrv, mockDataSource } from '../alerting/unified/mocks';
|
||||
@ -8,10 +9,23 @@ import { activateFullSceneTree } from '../dashboard-scene/utils/test-utils';
|
||||
import { DataTrail } from './DataTrail';
|
||||
import { MetricScene } from './MetricScene';
|
||||
import { MetricSelectScene } from './MetricSelect/MetricSelectScene';
|
||||
import { MetricSelectedEvent, VAR_FILTERS } from './shared';
|
||||
import {
|
||||
MetricSelectedEvent,
|
||||
VAR_FILTERS,
|
||||
VAR_OTEL_DEPLOYMENT_ENV,
|
||||
VAR_OTEL_JOIN_QUERY,
|
||||
VAR_OTEL_RESOURCES,
|
||||
} from './shared';
|
||||
|
||||
jest.mock('./otel/api', () => ({
|
||||
totalOtelResources: jest.fn(() => ({ job: 'oteldemo', instance: 'instance' })),
|
||||
getDeploymentEnvironments: jest.fn(() => ['production', 'staging']),
|
||||
isOtelStandardization: jest.fn(() => true),
|
||||
}));
|
||||
|
||||
describe('DataTrail', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(DataTrail.prototype, 'checkDataSourceForOTelResources').mockImplementation(() => Promise.resolve());
|
||||
setDataSourceSrv(
|
||||
new MockDataSourceSrv({
|
||||
prom: mockDataSource({
|
||||
@ -22,6 +36,10 @@ describe('DataTrail', () => {
|
||||
);
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
jest.restoreAllMocks();
|
||||
});
|
||||
|
||||
describe('Given starting non-embedded trail with url sync and no url state', () => {
|
||||
let trail: DataTrail;
|
||||
const preTrailUrl = '/';
|
||||
@ -459,4 +477,74 @@ describe('DataTrail', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('OTel resources attributes', () => {
|
||||
let trail: DataTrail;
|
||||
const preTrailUrl =
|
||||
'/trail?from=now-1h&to=now&var-ds=edwxqcebl0cg0c&var-deployment_environment=oteldemo01&var-otel_resources=k8s_cluster_name%7C%3D%7Cappo11ydev01&var-filters=&refresh=&metricPrefix=all&metricSearch=http&actionView=breakdown&var-groupby=$__all&metric=http_client_duration_milliseconds_bucket';
|
||||
|
||||
function getOtelDepEnvVar(trail: DataTrail) {
|
||||
const variable = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, trail);
|
||||
if (variable instanceof CustomVariable) {
|
||||
return variable;
|
||||
}
|
||||
throw new Error('getDepEnvVar failed');
|
||||
}
|
||||
|
||||
function getOtelJoinQueryVar(trail: DataTrail) {
|
||||
const variable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, trail);
|
||||
if (variable instanceof ConstantVariable) {
|
||||
return variable;
|
||||
}
|
||||
throw new Error('getDepEnvVar failed');
|
||||
}
|
||||
|
||||
function getOtelResourcesVar(trail: DataTrail) {
|
||||
const variable = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, trail);
|
||||
if (variable instanceof AdHocFiltersVariable) {
|
||||
return variable;
|
||||
}
|
||||
throw new Error('getOtelResourcesVar failed');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
trail = new DataTrail({});
|
||||
locationService.push(preTrailUrl);
|
||||
activateFullSceneTree(trail);
|
||||
getOtelResourcesVar(trail).setState({ filters: [{ key: 'service_name', operator: '=', value: 'adservice' }] });
|
||||
getOtelDepEnvVar(trail).changeValueTo('production');
|
||||
});
|
||||
|
||||
it('should start with hidden dep env variable', () => {
|
||||
const depEnvVarHide = getOtelDepEnvVar(trail).state.hide;
|
||||
expect(depEnvVarHide).toBe(VariableHide.hideVariable);
|
||||
});
|
||||
|
||||
it('should start with hidden otel resources variable', () => {
|
||||
const resourcesVarHide = getOtelResourcesVar(trail).state.hide;
|
||||
expect(resourcesVarHide).toBe(VariableHide.hideVariable);
|
||||
});
|
||||
|
||||
it('should start with hidden otel join query variable', () => {
|
||||
const joinQueryVarHide = getOtelJoinQueryVar(trail).state.hide;
|
||||
expect(joinQueryVarHide).toBe(VariableHide.hideVariable);
|
||||
});
|
||||
|
||||
it('should add history step for when updating the otel resource variable', () => {
|
||||
expect(trail.state.history.state.steps[2].type).toBe('resource');
|
||||
});
|
||||
|
||||
it('Should have otel resource attribute selected as "service_name=adservice"', () => {
|
||||
expect(getOtelResourcesVar(trail).state.filters[0].key).toBe('service_name');
|
||||
expect(getOtelResourcesVar(trail).state.filters[0].value).toBe('adservice');
|
||||
});
|
||||
|
||||
it('Should have deployment environment selected as "production"', () => {
|
||||
expect(getOtelDepEnvVar(trail).getValue()).toBe('production');
|
||||
});
|
||||
|
||||
it('should add history step for when updating the dep env variable', () => {
|
||||
expect(trail.state.history.state.steps[3].type).toBe('dep_env');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -1,9 +1,20 @@
|
||||
import { css } from '@emotion/css';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { AdHocVariableFilter, GrafanaTheme2, urlUtil, VariableHide } from '@grafana/data';
|
||||
import {
|
||||
AdHocVariableFilter,
|
||||
GetTagResponse,
|
||||
GrafanaTheme2,
|
||||
MetricFindValue,
|
||||
RawTimeRange,
|
||||
VariableHide,
|
||||
urlUtil,
|
||||
} from '@grafana/data';
|
||||
import { config, locationService, useChromeHeaderHeight } from '@grafana/runtime';
|
||||
import {
|
||||
AdHocFiltersVariable,
|
||||
ConstantVariable,
|
||||
CustomVariable,
|
||||
DataSourceVariable,
|
||||
SceneComponentProps,
|
||||
SceneControlsSpacer,
|
||||
@ -33,7 +44,21 @@ import { MetricsHeader } from './MetricsHeader';
|
||||
import { getTrailStore } from './TrailStore/TrailStore';
|
||||
import { MetricDatasourceHelper } from './helpers/MetricDatasourceHelper';
|
||||
import { reportChangeInLabelFilters } from './interactions';
|
||||
import { MetricSelectedEvent, trailDS, VAR_DATASOURCE, VAR_FILTERS } from './shared';
|
||||
import { getDeploymentEnvironments, TARGET_INFO_FILTER, totalOtelResources } from './otel/api';
|
||||
import { OtelResourcesObject, OtelTargetType } from './otel/types';
|
||||
import { sortResources, getOtelJoinQuery, getOtelResourcesObject } from './otel/util';
|
||||
import {
|
||||
getVariablesWithOtelJoinQueryConstant,
|
||||
MetricSelectedEvent,
|
||||
trailDS,
|
||||
VAR_DATASOURCE,
|
||||
VAR_DATASOURCE_EXPR,
|
||||
VAR_FILTERS,
|
||||
VAR_OTEL_DEPLOYMENT_ENV,
|
||||
VAR_OTEL_JOIN_QUERY,
|
||||
VAR_OTEL_RESOURCES,
|
||||
} from './shared';
|
||||
import { getTrailFor } from './utils';
|
||||
|
||||
export interface DataTrailState extends SceneObjectState {
|
||||
topScene?: SceneObject;
|
||||
@ -47,6 +72,16 @@ export interface DataTrailState extends SceneObjectState {
|
||||
initialDS?: string;
|
||||
initialFilters?: AdHocVariableFilter[];
|
||||
|
||||
// this is for otel, if the data source has it, it will be updated here
|
||||
hasOtelResources?: boolean;
|
||||
useOtelExperience?: boolean;
|
||||
otelTargets?: OtelTargetType; // all the targets with job and instance regex, job=~"<job-v>|<job-v>"", instance=~"<instance-v>|<instance-v>"
|
||||
otelJoinQuery?: string;
|
||||
isStandardOtel?: boolean;
|
||||
|
||||
// moved into settings
|
||||
showPreviews?: boolean;
|
||||
|
||||
// Synced with url
|
||||
metric?: string;
|
||||
metricSearch?: string;
|
||||
@ -58,7 +93,10 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
public constructor(state: Partial<DataTrailState>) {
|
||||
super({
|
||||
$timeRange: state.$timeRange ?? new SceneTimeRange({}),
|
||||
$variables: state.$variables ?? getVariableSet(state.initialDS, state.metric, state.initialFilters),
|
||||
// the initial variables should include a metric for metric scene and the otelJoinQuery.
|
||||
// NOTE: The other OTEL filters should be included too before this work is merged
|
||||
$variables:
|
||||
state.$variables ?? getVariableSet(state.initialDS, state.metric, state.initialFilters, state.otelJoinQuery),
|
||||
controls: state.controls ?? [
|
||||
new VariableValueSelectors({ layout: 'vertical' }),
|
||||
new SceneControlsSpacer(),
|
||||
@ -68,6 +106,12 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
history: state.history ?? new DataTrailHistory({}),
|
||||
settings: state.settings ?? new DataTrailSettings({}),
|
||||
createdAt: state.createdAt ?? new Date().getTime(),
|
||||
// default to false but update this to true on checkOtelSandardization()
|
||||
// or true if the user either turned on the experience
|
||||
useOtelExperience: state.useOtelExperience ?? false,
|
||||
// preserve the otel join query
|
||||
otelJoinQuery: state.otelJoinQuery ?? '',
|
||||
showPreviews: true,
|
||||
...state,
|
||||
});
|
||||
|
||||
@ -106,11 +150,45 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
}
|
||||
|
||||
protected _variableDependency = new VariableDependencyConfig(this, {
|
||||
variableNames: [VAR_DATASOURCE],
|
||||
variableNames: [VAR_DATASOURCE, VAR_OTEL_RESOURCES, VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_JOIN_QUERY],
|
||||
onReferencedVariableValueChanged: async (variable: SceneVariable) => {
|
||||
const { name } = variable.state;
|
||||
|
||||
if (name === VAR_DATASOURCE) {
|
||||
this.datasourceHelper.reset();
|
||||
|
||||
// fresh check for otel experience
|
||||
this.checkDataSourceForOTelResources();
|
||||
// clear filters on resetting the data source
|
||||
const adhocVariable = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||
if (adhocVariable instanceof AdHocFiltersVariable) {
|
||||
adhocVariable.setState({ filters: [] });
|
||||
}
|
||||
}
|
||||
|
||||
// update otel variables when changed
|
||||
if (this.state.useOtelExperience && (name === VAR_OTEL_DEPLOYMENT_ENV || name === VAR_OTEL_RESOURCES)) {
|
||||
// for state and variables
|
||||
const timeRange: RawTimeRange | undefined = this.state.$timeRange?.state;
|
||||
const datasourceUid = sceneGraph.interpolate(this, VAR_DATASOURCE_EXPR);
|
||||
const otelDepEnvVariable = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, this);
|
||||
const otelResourcesVariable = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, this);
|
||||
const otelJoinQueryVariable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, this);
|
||||
|
||||
if (
|
||||
timeRange &&
|
||||
otelResourcesVariable instanceof AdHocFiltersVariable &&
|
||||
otelJoinQueryVariable instanceof ConstantVariable &&
|
||||
otelDepEnvVariable instanceof CustomVariable
|
||||
) {
|
||||
this.updateOtelData(
|
||||
datasourceUid,
|
||||
timeRange,
|
||||
otelDepEnvVariable,
|
||||
otelResourcesVariable,
|
||||
otelJoinQueryVariable
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
});
|
||||
@ -208,12 +286,304 @@ export class DataTrail extends SceneObjectBase<DataTrailState> {
|
||||
this.setState(stateUpdate);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check that the data source has otel resources
|
||||
* Check that the data source is standard for OTEL
|
||||
* Show a warning if not
|
||||
* Update the following variables:
|
||||
* deployment_environment (first filter), otelResources (filters), otelJoinQuery (used in the query)
|
||||
* Enable the otel experience
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
public async checkDataSourceForOTelResources() {
|
||||
// call up in to the parent trail
|
||||
const trail = getTrailFor(this);
|
||||
|
||||
// get the time range
|
||||
const timeRange: RawTimeRange | undefined = trail.state.$timeRange?.state;
|
||||
|
||||
if (timeRange) {
|
||||
const otelResourcesVariable = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, this);
|
||||
const otelDepEnvVariable = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, this);
|
||||
const otelJoinQueryVariable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, this);
|
||||
const filtersVariable = sceneGraph.lookupVariable(VAR_FILTERS, this);
|
||||
|
||||
const datasourceUid = sceneGraph.interpolate(trail, VAR_DATASOURCE_EXPR);
|
||||
|
||||
const otelTargets = await totalOtelResources(datasourceUid, timeRange);
|
||||
const deploymentEnvironments = await getDeploymentEnvironments(datasourceUid, timeRange);
|
||||
const hasOtelResources = otelTargets.jobs.length > 0 && otelTargets.instances.length > 0;
|
||||
if (
|
||||
otelResourcesVariable instanceof AdHocFiltersVariable &&
|
||||
otelDepEnvVariable instanceof CustomVariable &&
|
||||
otelJoinQueryVariable instanceof ConstantVariable &&
|
||||
filtersVariable instanceof AdHocFiltersVariable
|
||||
) {
|
||||
// HERE WE START THE OTEL EXPERIENCE ENGINE
|
||||
// 1. Set deployment variable values
|
||||
// 2. update all other variables and state
|
||||
if (hasOtelResources && deploymentEnvironments.length > 0) {
|
||||
// apply VAR FILTERS manually
|
||||
// otherwise they will appear anywhere the query contains {} characters
|
||||
filtersVariable.setState({
|
||||
addFilterButtonText: 'Select metric attributes',
|
||||
label: 'Select metric attribute',
|
||||
});
|
||||
|
||||
// 1. set deployment variable values
|
||||
let varQuery = '';
|
||||
const options = deploymentEnvironments.map((env) => {
|
||||
varQuery += env + ',';
|
||||
return { value: env, label: env };
|
||||
});
|
||||
// We have to have a default value because custom variable requires it
|
||||
// we choose one default value to help filter metrics
|
||||
// The work flow for OTel begins with users selecting a deployment environment
|
||||
const defaultDepEnv = options[0].value; // usually production
|
||||
// On starting the explore metrics workflow, the custom variable has no value
|
||||
// Even if there is state, the value is always ''
|
||||
// The only reference to state values are in the text
|
||||
const otelDepEnvValue = otelDepEnvVariable.state.text;
|
||||
|
||||
// TypeScript issue: VariableValue is either a string or array but does not have any string or array methods on it to check that it is empty
|
||||
const notInitialvalue = otelDepEnvValue !== '' && otelDepEnvValue.toLocaleString() !== '';
|
||||
|
||||
const depEnvInitialValue = notInitialvalue ? otelDepEnvValue : defaultDepEnv;
|
||||
|
||||
otelDepEnvVariable?.setState({
|
||||
value: depEnvInitialValue,
|
||||
options: options,
|
||||
hide: VariableHide.dontHide,
|
||||
});
|
||||
|
||||
this.updateOtelData(
|
||||
datasourceUid,
|
||||
timeRange,
|
||||
otelDepEnvVariable,
|
||||
otelResourcesVariable,
|
||||
otelJoinQueryVariable,
|
||||
deploymentEnvironments,
|
||||
hasOtelResources
|
||||
);
|
||||
} else {
|
||||
// reset filters to apply auto, anywhere there are {} characters
|
||||
this.resetOtelExperience(
|
||||
otelResourcesVariable,
|
||||
otelDepEnvVariable,
|
||||
otelJoinQueryVariable,
|
||||
filtersVariable,
|
||||
hasOtelResources,
|
||||
deploymentEnvironments
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This function is used to update state and otel variables
|
||||
*
|
||||
* 1. Set the otelResources adhoc tagKey and tagValues filter functions
|
||||
2. Get the otel join query for state and variable
|
||||
3. Update state with the following
|
||||
- otel join query
|
||||
- otelTargets used to filter metrics
|
||||
For initialization we also update the following
|
||||
- has otel resources flag
|
||||
- isStandardOtel flag (for enabliing the otel experience toggle)
|
||||
- and useOtelExperience
|
||||
* @param datasourceUid
|
||||
* @param timeRange
|
||||
* @param otelDepEnvVariable
|
||||
* @param otelResourcesVariable
|
||||
* @param otelJoinQueryVariable
|
||||
* @param deploymentEnvironments
|
||||
* @param hasOtelResources
|
||||
*/
|
||||
async updateOtelData(
|
||||
datasourceUid: string,
|
||||
timeRange: RawTimeRange,
|
||||
otelDepEnvVariable: CustomVariable,
|
||||
otelResourcesVariable: AdHocFiltersVariable,
|
||||
otelJoinQueryVariable: ConstantVariable,
|
||||
deploymentEnvironments?: string[],
|
||||
hasOtelResources?: boolean
|
||||
) {
|
||||
// 1. Set the otelResources adhoc tagKey and tagValues filter functions
|
||||
// get the labels for otel resources
|
||||
// collection of filters for the otel resource variable
|
||||
// filter label names and label values
|
||||
// the first filter is {__name__="target_info"}
|
||||
let filters: AdHocVariableFilter[] = [TARGET_INFO_FILTER];
|
||||
|
||||
// always start with the deployment environment
|
||||
const depEnvValue = '' + otelDepEnvVariable?.getValue();
|
||||
|
||||
if (depEnvValue) {
|
||||
// update the operator if more than one
|
||||
const op = depEnvValue.includes(',') ? '=~' : '=';
|
||||
// the second filter is deployment_environment
|
||||
const filter = {
|
||||
key: 'deployment_environment',
|
||||
value: depEnvValue.split(',').join('|'),
|
||||
operator: op,
|
||||
};
|
||||
|
||||
filters.push(filter);
|
||||
}
|
||||
// next we check the otel resources adhoc variable for filters
|
||||
const values = otelResourcesVariable.getValue();
|
||||
|
||||
if (values && otelResourcesVariable.state.filters.length > 0) {
|
||||
filters = filters.concat(otelResourcesVariable.state.filters);
|
||||
}
|
||||
// the datasourceHelper will give us access to the
|
||||
// Prometheus functions getTagKeys and getTagValues
|
||||
// because we can access the ds
|
||||
const datasourceHelper = this.datasourceHelper;
|
||||
// now we reset the override tagKeys and tagValues functions of the adhoc variable
|
||||
otelResourcesVariable.setState({
|
||||
getTagKeysProvider: async (
|
||||
variable: AdHocFiltersVariable,
|
||||
currentKey: string | null
|
||||
): Promise<{
|
||||
replace?: boolean;
|
||||
values: GetTagResponse | MetricFindValue[];
|
||||
}> => {
|
||||
// apply filters here
|
||||
let values = await datasourceHelper.getTagKeys({ filters });
|
||||
values = sortResources(values, filters.map((f) => f.key).concat(currentKey ?? ''));
|
||||
return { replace: true, values };
|
||||
},
|
||||
getTagValuesProvider: async (
|
||||
variable: AdHocFiltersVariable,
|
||||
filter: AdHocVariableFilter
|
||||
): Promise<{
|
||||
replace?: boolean;
|
||||
values: GetTagResponse | MetricFindValue[];
|
||||
}> => {
|
||||
// apply filters here
|
||||
// remove current selected filter if refiltering
|
||||
filters = filters.filter((f) => f.key !== filter.key);
|
||||
const values = await datasourceHelper.getTagValues({ key: filter.key, filters });
|
||||
return { replace: true, values };
|
||||
},
|
||||
hide: VariableHide.hideLabel,
|
||||
});
|
||||
|
||||
// 2. Get the otel join query for state and variable
|
||||
// Because we need to define the deployment environment variable
|
||||
// we also need to update the otel join query state and variable
|
||||
const resourcesObject: OtelResourcesObject = getOtelResourcesObject(this);
|
||||
const otelJoinQuery = getOtelJoinQuery(resourcesObject);
|
||||
|
||||
// update the otel join query variable too
|
||||
otelJoinQueryVariable.setState({ value: otelJoinQuery });
|
||||
|
||||
// 3. Update state with the following
|
||||
// - otel join query
|
||||
// - otelTargets used to filter metrics
|
||||
// now we can filter target_info targets by deployment_environment="somevalue"
|
||||
// and use these new targets to reduce the metrics
|
||||
// for initialization we also update the following
|
||||
// - has otel resources flag
|
||||
// - and default to useOtelExperience
|
||||
const otelTargets = await totalOtelResources(datasourceUid, timeRange, resourcesObject.filters);
|
||||
|
||||
// we pass in deploymentEnvironments and hasOtelResources on start
|
||||
if (hasOtelResources && deploymentEnvironments) {
|
||||
this.setState({
|
||||
otelTargets,
|
||||
otelJoinQuery,
|
||||
hasOtelResources,
|
||||
isStandardOtel: deploymentEnvironments.length > 0,
|
||||
useOtelExperience: true,
|
||||
});
|
||||
} else {
|
||||
// we are updating on variable changes
|
||||
this.setState({
|
||||
otelTargets,
|
||||
otelJoinQuery,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
resetOtelExperience(
|
||||
otelResourcesVariable: AdHocFiltersVariable,
|
||||
otelDepEnvVariable: CustomVariable,
|
||||
otelJoinQueryVariable: ConstantVariable,
|
||||
filtersVariable: AdHocFiltersVariable,
|
||||
hasOtelResources?: boolean,
|
||||
deploymentEnvironments?: string[]
|
||||
) {
|
||||
// reset filters to apply auto, anywhere there are {} characters
|
||||
filtersVariable.setState({
|
||||
addFilterButtonText: 'Add label',
|
||||
label: 'Select label',
|
||||
});
|
||||
|
||||
// if there are no resources reset the otel variables and otel state
|
||||
// or if not standard
|
||||
otelResourcesVariable.setState({
|
||||
defaultKeys: [],
|
||||
hide: VariableHide.hideVariable,
|
||||
});
|
||||
|
||||
otelDepEnvVariable.setState({
|
||||
value: '',
|
||||
hide: VariableHide.hideVariable,
|
||||
});
|
||||
|
||||
otelJoinQueryVariable.setState({ value: '' });
|
||||
|
||||
// full reset when a data source fails the check
|
||||
if (hasOtelResources && deploymentEnvironments) {
|
||||
this.setState({
|
||||
hasOtelResources,
|
||||
isStandardOtel: deploymentEnvironments.length > 0,
|
||||
useOtelExperience: false,
|
||||
otelTargets: { jobs: [], instances: [] },
|
||||
otelJoinQuery: '',
|
||||
});
|
||||
} else {
|
||||
// partial reset when a user turns off the otel experience
|
||||
this.setState({
|
||||
otelTargets: { jobs: [], instances: [] },
|
||||
otelJoinQuery: '',
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrail>) => {
|
||||
const { controls, topScene, history, settings } = model.useState();
|
||||
const { controls, topScene, history, settings, useOtelExperience, hasOtelResources } = model.useState();
|
||||
|
||||
const chromeHeaderHeight = useChromeHeaderHeight();
|
||||
const styles = useStyles2(getStyles, chromeHeaderHeight ?? 0);
|
||||
const showHeaderForFirstTimeUsers = getTrailStore().recent.length < 2;
|
||||
|
||||
useEffect(() => {
|
||||
// check if the otel experience has been enabled
|
||||
if (!useOtelExperience) {
|
||||
// if the experience has been turned off, reset the otel variables
|
||||
const otelResourcesVariable = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, model);
|
||||
const otelDepEnvVariable = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, model);
|
||||
const otelJoinQueryVariable = sceneGraph.lookupVariable(VAR_OTEL_JOIN_QUERY, model);
|
||||
const filtersvariable = sceneGraph.lookupVariable(VAR_FILTERS, model);
|
||||
|
||||
if (
|
||||
otelResourcesVariable instanceof AdHocFiltersVariable &&
|
||||
otelDepEnvVariable instanceof CustomVariable &&
|
||||
otelJoinQueryVariable instanceof ConstantVariable &&
|
||||
filtersvariable instanceof AdHocFiltersVariable
|
||||
) {
|
||||
model.resetOtelExperience(otelResourcesVariable, otelDepEnvVariable, otelJoinQueryVariable, filtersvariable);
|
||||
}
|
||||
} else {
|
||||
// if experience is enabled, check standardization and update the otel variables
|
||||
model.checkDataSourceForOTelResources();
|
||||
}
|
||||
}, [model, hasOtelResources, useOtelExperience]);
|
||||
|
||||
return (
|
||||
<div className={styles.container}>
|
||||
{showHeaderForFirstTimeUsers && <MetricsHeader />}
|
||||
@ -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 ?? ''),
|
||||
],
|
||||
});
|
||||
}
|
||||
|
@ -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<DataTrailSettingsState> {
|
||||
this.setState({ isOpen });
|
||||
};
|
||||
|
||||
public onTogglePreviews = () => {
|
||||
const trail = getTrailFor(this);
|
||||
trail.setState({ showPreviews: !trail.state.showPreviews });
|
||||
};
|
||||
|
||||
static Component = ({ model }: SceneComponentProps<DataTrailSettings>) => {
|
||||
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 */
|
||||
<div className={styles.popover} onClick={(evt) => evt.stopPropagation()}>
|
||||
<div className={styles.heading}>Settings</div>
|
||||
<div className={styles.options}>
|
||||
<div>Always keep selected metric graph in-view</div>
|
||||
<Switch value={stickyMainGraph} onChange={model.onToggleStickyMainGraph} />
|
||||
</div>
|
||||
{topScene instanceof MetricScene && (
|
||||
<div className={styles.options}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.settings.always-keep-selected-metric-graph-in-view">
|
||||
Always keep selected metric graph in-view
|
||||
</Trans>
|
||||
</div>
|
||||
<Switch value={stickyMainGraph} onChange={model.onToggleStickyMainGraph} />
|
||||
</div>
|
||||
)}
|
||||
{topScene instanceof MetricSelectScene && (
|
||||
<div className={styles.options}>
|
||||
<div>
|
||||
<Trans i18nKey="trails.settings.show-previews-of-metric-graphs">Show previews of metric graphs</Trans>
|
||||
</div>
|
||||
<Switch value={showPreviews} onChange={model.onTogglePreviews} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
@ -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<TrailStepType, string> = {
|
||||
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<DataTrailsHistoryState> {
|
||||
public constructor(state: Partial<DataTrailsHistoryState>) {
|
||||
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<DataTrailsHistoryState> {
|
||||
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<DataTrailsHistoryState> {
|
||||
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<DataTrailsHistoryState> {
|
||||
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')),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
@ -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<MetricSelectSceneState> i
|
||||
|
||||
constructor(state: Partial<MetricSelectSceneState>) {
|
||||
super({
|
||||
showPreviews: true,
|
||||
$variables: state.$variables,
|
||||
metricPrefix: state.metricPrefix ?? METRIC_PREFIX_ALL,
|
||||
body:
|
||||
@ -182,6 +181,34 @@ export class MetricSelectScene extends SceneObjectBase<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectSceneState> 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<MetricSelectScene>) => {
|
||||
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<MetricSelectSceneState> i
|
||||
]}
|
||||
/>
|
||||
</Field>
|
||||
<InlineSwitch showLabel={true} label="Show previews" value={showPreviews} onChange={model.onTogglePreviews} />
|
||||
{hasOtelResources && (
|
||||
<Field
|
||||
label={
|
||||
<div className={styles.displayOptionTooltip}>
|
||||
<Trans i18nKey="trails.metric-select.filter-by">Filter by</Trans>
|
||||
<IconButton
|
||||
name={'info-circle'}
|
||||
size="sm"
|
||||
variant={'secondary'}
|
||||
tooltip={
|
||||
<Trans i18nKey="trails.metric-select.otel-switch">
|
||||
This switch enables filtering by OTel resources for OTel native data sources.
|
||||
</Trans>
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
className={styles.displayOption}
|
||||
>
|
||||
<div
|
||||
title={
|
||||
!isStandardOtel ? 'This setting is disabled because this is not an OTel native data source.' : ''
|
||||
}
|
||||
>
|
||||
<InlineSwitch
|
||||
disabled={!isStandardOtel}
|
||||
showLabel={true}
|
||||
label="Otel experience"
|
||||
value={useOtelExperience}
|
||||
onChange={model.onToggleOtelExperience}
|
||||
/>
|
||||
</div>
|
||||
</Field>
|
||||
)}
|
||||
</div>
|
||||
{metricNamesError && (
|
||||
<Alert title="Unable to retrieve metric names" severity="error">
|
||||
|
@ -14,13 +14,13 @@ describe('getPreviewPanelFor', () => {
|
||||
}
|
||||
|
||||
test('When there are no filters, replace the ${filters} variable', () => {
|
||||
const expected = 'avg(${metric}{__ignore_usage__=""})';
|
||||
const expected = 'avg(${metric}{__ignore_usage__=""} ${otel_join_query})';
|
||||
const expr = callAndGetExpr(0);
|
||||
expect(expr).toStrictEqual(expected);
|
||||
});
|
||||
|
||||
test('When there are 1 or more filters, append to the ${filters} variable', () => {
|
||||
const expected = 'avg(${metric}{${filters},__ignore_usage__=""})';
|
||||
const expected = 'avg(${metric}{${filters},__ignore_usage__=""} ${otel_join_query})';
|
||||
|
||||
for (let i = 1; i < 10; ++i) {
|
||||
const expr = callAndGetExpr(1);
|
||||
|
@ -17,6 +17,8 @@ jest.mock('@grafana/runtime', () => ({
|
||||
|
||||
describe('TrailStore', () => {
|
||||
beforeAll(() => {
|
||||
jest.spyOn(DataTrail.prototype, 'checkDataSourceForOTelResources').mockImplementation(() => Promise.resolve());
|
||||
|
||||
let localStore: Record<string, string> = {};
|
||||
|
||||
const localStorageMock = {
|
||||
@ -494,6 +496,8 @@ describe('TrailStore', () => {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
'var-ds': 'prom-mock',
|
||||
'var-deployment_environment': ['undefined'],
|
||||
'var-otel_resources': [''],
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
},
|
||||
@ -691,6 +695,8 @@ describe('TrailStore', () => {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
'var-ds': 'prom-mock',
|
||||
'var-deployment_environment': ['undefined'],
|
||||
'var-otel_resources': [''],
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
},
|
||||
@ -702,6 +708,8 @@ describe('TrailStore', () => {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
'var-ds': 'prom-mock',
|
||||
'var-deployment_environment': ['undefined'],
|
||||
'var-otel_resources': [''],
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
},
|
||||
@ -713,6 +721,8 @@ describe('TrailStore', () => {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
'var-ds': 'prom-mock',
|
||||
'var-deployment_environment': ['undefined'],
|
||||
'var-otel_resources': [''],
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
},
|
||||
@ -732,6 +742,8 @@ describe('TrailStore', () => {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
'var-ds': 'prom-mock',
|
||||
'var-deployment_environment': ['undefined'],
|
||||
'var-otel_resources': [''],
|
||||
'var-filters': [],
|
||||
refresh: '',
|
||||
},
|
||||
|
@ -1,5 +1,10 @@
|
||||
import { DataSourceApi } from '@grafana/data';
|
||||
import { PromMetricsMetadata, PromMetricsMetadataItem } from '@grafana/prometheus';
|
||||
import {
|
||||
DataSourceApi,
|
||||
DataSourceGetTagKeysOptions,
|
||||
DataSourceGetTagValuesOptions,
|
||||
MetricFindValue,
|
||||
} from '@grafana/data';
|
||||
import { PrometheusDatasource, PromMetricsMetadata, PromMetricsMetadataItem, PromQuery } from '@grafana/prometheus';
|
||||
import PromQlLanguageProvider from '@grafana/prometheus/src/language_provider';
|
||||
import { getDataSourceSrv } from '@grafana/runtime';
|
||||
|
||||
@ -55,6 +60,44 @@ export class MetricDatasourceHelper {
|
||||
const metadata = await this._metricsMetadata;
|
||||
return metadata?.[metric];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for filtering label names for OTel resources to add custom match filters
|
||||
* - target_info metric
|
||||
* - deployment_environment label
|
||||
* - all other OTel filters
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
public async getTagKeys(options: DataSourceGetTagKeysOptions<PromQuery>): Promise<MetricFindValue[]> {
|
||||
const ds = await this.getDatasource();
|
||||
|
||||
if (ds instanceof PrometheusDatasource) {
|
||||
const keys = await ds.getTagKeys(options);
|
||||
return keys;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Used for filtering label values for OTel resources to add custom match filters
|
||||
* - target_info metric
|
||||
* - deployment_environment label
|
||||
* - all other OTel filters
|
||||
* @param options
|
||||
* @returns
|
||||
*/
|
||||
public async getTagValues(options: DataSourceGetTagValuesOptions<PromQuery>) {
|
||||
const ds = await this.getDatasource();
|
||||
|
||||
if (ds instanceof PrometheusDatasource) {
|
||||
const keys = await ds.getTagValues(options);
|
||||
return keys;
|
||||
}
|
||||
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function getMetricDescription(metadata?: PromMetricsMetadataItem) {
|
||||
|
90
public/app/features/trails/otel/api.test.ts
Normal file
90
public/app/features/trails/otel/api.test.ts
Normal file
@ -0,0 +1,90 @@
|
||||
import { RawTimeRange } from '@grafana/data';
|
||||
import { BackendSrvRequest } from '@grafana/runtime';
|
||||
|
||||
import { getOtelResources, totalOtelResources, isOtelStandardization, getDeploymentEnvironments } from './api';
|
||||
|
||||
jest.mock('@grafana/runtime', () => ({
|
||||
getBackendSrv: () => {
|
||||
return {
|
||||
get: (
|
||||
url: string,
|
||||
params?: Record<string, string | number>,
|
||||
requestId?: string,
|
||||
options?: Partial<BackendSrvRequest>
|
||||
) => {
|
||||
if (requestId === 'explore-metrics-otel-resources') {
|
||||
return Promise.resolve({ data: ['job', 'instance', 'deployment_environment'] });
|
||||
} else if (requestId === 'explore-metrics-otel-check-total') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
result: [
|
||||
{ metric: { job: 'job1', instance: 'instance1' } },
|
||||
{ metric: { job: 'job2', instance: 'instance2' } },
|
||||
],
|
||||
},
|
||||
});
|
||||
} else if (requestId === 'explore-metrics-otel-check-standard') {
|
||||
return Promise.resolve({
|
||||
data: {
|
||||
result: [{ metric: { job: 'job1', instance: 'instance1' } }],
|
||||
},
|
||||
});
|
||||
} else if (requestId === 'explore-metrics-otel-resources-deployment-env') {
|
||||
return Promise.resolve({ data: ['env1', 'env2'] });
|
||||
}
|
||||
return [];
|
||||
},
|
||||
};
|
||||
},
|
||||
}));
|
||||
|
||||
describe('OTEL API', () => {
|
||||
const dataSourceUid = 'test-uid';
|
||||
const timeRange: RawTimeRange = {
|
||||
from: 'now-1h',
|
||||
to: 'now',
|
||||
};
|
||||
|
||||
afterAll(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
|
||||
describe('getOtelResources', () => {
|
||||
it('should fetch and filter OTEL resources', async () => {
|
||||
const resources = await getOtelResources(dataSourceUid, timeRange);
|
||||
|
||||
expect(resources).toEqual(['job', 'instance']);
|
||||
});
|
||||
});
|
||||
|
||||
describe('totalOtelResources', () => {
|
||||
it('should fetch total OTEL resources', async () => {
|
||||
const result = await totalOtelResources(dataSourceUid, timeRange);
|
||||
|
||||
expect(result).toEqual({
|
||||
jobs: ['job1', 'job2'],
|
||||
instances: ['instance1', 'instance2'],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('isOtelStandardization', () => {
|
||||
// keeping for reference because standardization for OTel by series on target_info for job&instance is not consistent
|
||||
// There is a bug currently where there is stale data in Prometheus resulting in duplicate series for job&instance at random times
|
||||
// When this is resolved, we can check for standardization again
|
||||
xit('should check if OTEL standardization is met when there are no duplicate series on target_info for job&instance', async () => {
|
||||
// will return duplicates, see mock above
|
||||
const isStandard = await isOtelStandardization(dataSourceUid, timeRange);
|
||||
|
||||
expect(isStandard).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDeploymentEnvironments', () => {
|
||||
it('should fetch deployment environments', async () => {
|
||||
const environments = await getDeploymentEnvironments(dataSourceUid, timeRange);
|
||||
|
||||
expect(environments).toEqual(['env1', 'env2']);
|
||||
});
|
||||
});
|
||||
});
|
166
public/app/features/trails/otel/api.ts
Normal file
166
public/app/features/trails/otel/api.ts
Normal file
@ -0,0 +1,166 @@
|
||||
import { RawTimeRange } from '@grafana/data';
|
||||
import { getPrometheusTime } from '@grafana/prometheus/src/language_utils';
|
||||
import { getBackendSrv } from '@grafana/runtime';
|
||||
|
||||
import { OtelResponse, LabelResponse, OtelTargetType } from './types';
|
||||
|
||||
const OTEL_RESOURCE_EXCLUDED_FILTERS = ['__name__', 'deployment_environment']; // name is handled by metric search metrics bar
|
||||
/**
|
||||
* Function used to test for OTEL
|
||||
* When filters are added, we can also get a list of otel targets used to reduce the metric list
|
||||
* */
|
||||
const otelTargetInfoQuery = (filters?: string) => `count(target_info{${filters ?? ''}}) by (job, instance)`;
|
||||
|
||||
export const TARGET_INFO_FILTER = { key: '__name__', value: 'target_info', operator: '=' };
|
||||
|
||||
/**
|
||||
* Query the DS for target_info matching job and instance.
|
||||
* Parse the results to get label filters.
|
||||
* @param dataSourceUid
|
||||
* @param timeRange
|
||||
* @returns OtelResourcesType[], labels for the query result requesting matching job and instance on target_info metric
|
||||
*/
|
||||
export async function getOtelResources(
|
||||
dataSourceUid: string,
|
||||
timeRange: RawTimeRange,
|
||||
excludedFilters?: string[],
|
||||
matchFilters?: string
|
||||
): Promise<string[]> {
|
||||
const allExcludedFilters = (excludedFilters ?? []).concat(OTEL_RESOURCE_EXCLUDED_FILTERS);
|
||||
|
||||
const start = getPrometheusTime(timeRange.from, false);
|
||||
const end = getPrometheusTime(timeRange.to, true);
|
||||
|
||||
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/labels`;
|
||||
const params: Record<string, string | number> = {
|
||||
start,
|
||||
end,
|
||||
'match[]': `{__name__="target_info"${matchFilters ? `,${matchFilters}` : ''}}`,
|
||||
};
|
||||
|
||||
const response = await getBackendSrv().get<LabelResponse>(url, params, 'explore-metrics-otel-resources');
|
||||
|
||||
// exclude __name__ or deployment_environment or previously chosen filters
|
||||
const resources = response.data?.filter((resource) => !allExcludedFilters.includes(resource)).map((el: string) => el);
|
||||
|
||||
return resources;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total amount of job/instance pairs on target info metric
|
||||
*
|
||||
* @param dataSourceUid
|
||||
* @param timeRange
|
||||
* @param expr
|
||||
* @returns
|
||||
*/
|
||||
export async function totalOtelResources(
|
||||
dataSourceUid: string,
|
||||
timeRange: RawTimeRange,
|
||||
filters?: string
|
||||
): Promise<OtelTargetType> {
|
||||
const start = getPrometheusTime(timeRange.from, false);
|
||||
const end = getPrometheusTime(timeRange.to, true);
|
||||
|
||||
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/query`;
|
||||
const paramsTotalTargets: Record<string, string | number> = {
|
||||
start,
|
||||
end,
|
||||
query: otelTargetInfoQuery(filters),
|
||||
};
|
||||
|
||||
const responseTotal = await getBackendSrv().get<OtelResponse>(
|
||||
url,
|
||||
paramsTotalTargets,
|
||||
'explore-metrics-otel-check-total'
|
||||
);
|
||||
|
||||
let jobs: string[] = [];
|
||||
let instances: string[] = [];
|
||||
|
||||
responseTotal.data.result.forEach((result) => {
|
||||
// NOTE: sometimes there are target_info series with
|
||||
// - both job and instance labels
|
||||
// - only job label
|
||||
// - only instance label
|
||||
// Here we make sure both of them are present
|
||||
// because we use this collection to filter metric names
|
||||
if (result.metric.job && result.metric.instance) {
|
||||
jobs.push(result.metric.job);
|
||||
instances.push(result.metric.instance);
|
||||
}
|
||||
});
|
||||
|
||||
const otelTargets: OtelTargetType = {
|
||||
jobs,
|
||||
instances,
|
||||
};
|
||||
|
||||
return otelTargets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Look for duplicated series in target_info metric by job and instance labels
|
||||
* If each job&instance combo is unique, the data source is otel standardized.
|
||||
* If there is a count by job&instance on target_info greater than one,
|
||||
* the data source is not standardized
|
||||
*
|
||||
* @param dataSourceUid
|
||||
* @param timeRange
|
||||
* @param expr
|
||||
* @returns
|
||||
*/
|
||||
export async function isOtelStandardization(
|
||||
dataSourceUid: string,
|
||||
timeRange: RawTimeRange,
|
||||
expr?: string
|
||||
): Promise<boolean> {
|
||||
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/query`;
|
||||
|
||||
const start = getPrometheusTime(timeRange.from, false);
|
||||
const end = getPrometheusTime(timeRange.to, true);
|
||||
|
||||
const paramsTargets: Record<string, string | number> = {
|
||||
start,
|
||||
end,
|
||||
// any data source with duplicated series will have a count > 1
|
||||
query: `${otelTargetInfoQuery()} > 1`,
|
||||
};
|
||||
|
||||
const response = await getBackendSrv().get<OtelResponse>(url, paramsTargets, 'explore-metrics-otel-check-standard');
|
||||
|
||||
// the response should be not greater than zero if it is standard
|
||||
const checkStandard = !(response.data.result.length > 0);
|
||||
|
||||
return checkStandard;
|
||||
}
|
||||
|
||||
/**
|
||||
* Query the DS for deployment environment label values.
|
||||
*
|
||||
* @param dataSourceUid
|
||||
* @param timeRange
|
||||
* @returns string[], values for the deployment_environment label
|
||||
*/
|
||||
export async function getDeploymentEnvironments(dataSourceUid: string, timeRange: RawTimeRange): Promise<string[]> {
|
||||
const start = getPrometheusTime(timeRange.from, false);
|
||||
const end = getPrometheusTime(timeRange.to, true);
|
||||
|
||||
const url = `/api/datasources/uid/${dataSourceUid}/resources/api/v1/label/deployment_environment/values`;
|
||||
const params: Record<string, string | number> = {
|
||||
start,
|
||||
end,
|
||||
'match[]': '{__name__="target_info"}',
|
||||
};
|
||||
|
||||
const response = await getBackendSrv().get<LabelResponse>(
|
||||
url,
|
||||
params,
|
||||
'explore-metrics-otel-resources-deployment-env'
|
||||
);
|
||||
|
||||
// exclude __name__ or deployment_environment or previously chosen filters
|
||||
const resources = response.data;
|
||||
|
||||
return resources;
|
||||
}
|
32
public/app/features/trails/otel/types.ts
Normal file
32
public/app/features/trails/otel/types.ts
Normal file
@ -0,0 +1,32 @@
|
||||
export type OtelResponse = {
|
||||
data: {
|
||||
result: [
|
||||
{
|
||||
metric: {
|
||||
job: string;
|
||||
instance: string;
|
||||
};
|
||||
},
|
||||
];
|
||||
};
|
||||
status: 'success' | 'error';
|
||||
error?: 'string';
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
export type LabelResponse = {
|
||||
data: string[];
|
||||
status: 'success' | 'error';
|
||||
error?: 'string';
|
||||
warnings?: string[];
|
||||
};
|
||||
|
||||
export type OtelTargetType = {
|
||||
jobs: string[];
|
||||
instances: string[];
|
||||
};
|
||||
|
||||
export type OtelResourcesObject = {
|
||||
filters: string;
|
||||
labels: string;
|
||||
};
|
195
public/app/features/trails/otel/util.ts
Normal file
195
public/app/features/trails/otel/util.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
import { AdHocFiltersVariable, CustomVariable, sceneGraph, SceneObject } from '@grafana/scenes';
|
||||
|
||||
import { VAR_OTEL_DEPLOYMENT_ENV, VAR_OTEL_RESOURCES } from '../shared';
|
||||
|
||||
import { OtelResourcesObject } from './types';
|
||||
|
||||
export const blessedList = (): Record<string, number> => {
|
||||
return {
|
||||
cloud_availability_zone: 0,
|
||||
cloud_region: 0,
|
||||
container_name: 0,
|
||||
k8s_cluster_name: 0,
|
||||
k8s_container_name: 0,
|
||||
k8s_cronjob_name: 0,
|
||||
k8s_daemonset_name: 0,
|
||||
k8s_deployment_name: 0,
|
||||
k8s_job_name: 0,
|
||||
k8s_namespace_name: 0,
|
||||
k8s_pod_name: 0,
|
||||
k8s_replicaset_name: 0,
|
||||
k8s_statefulset_name: 0,
|
||||
service_instance_id: 0,
|
||||
service_name: 0,
|
||||
service_namespace: 0,
|
||||
};
|
||||
};
|
||||
|
||||
export function sortResources(resources: MetricFindValue[], excluded: string[]) {
|
||||
// these may be filtered
|
||||
const promotedList = blessedList();
|
||||
|
||||
const blessed = Object.keys(promotedList);
|
||||
|
||||
resources = resources.filter((resource) => {
|
||||
// if not in the list keep it
|
||||
const val = (resource.value ?? '').toString();
|
||||
|
||||
if (!blessed.includes(val)) {
|
||||
return true;
|
||||
}
|
||||
// remove blessed filters
|
||||
// but indicate which are available
|
||||
promotedList[val] = 1;
|
||||
return false;
|
||||
});
|
||||
|
||||
const promotedResources = Object.keys(promotedList)
|
||||
.filter((resource) => promotedList[resource] && !excluded.includes(resource))
|
||||
.map((v) => ({ text: v }));
|
||||
|
||||
// put the filters first
|
||||
return promotedResources.concat(resources);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a collection of labels and labels filters.
|
||||
* This data is used to build the join query to filter with otel resources
|
||||
*
|
||||
* @param otelResourcesObject
|
||||
* @returns a string that is used to add a join query to filter otel resources
|
||||
*/
|
||||
export function getOtelJoinQuery(otelResourcesObject: OtelResourcesObject): string {
|
||||
let otelResourcesJoinQuery = '';
|
||||
if (otelResourcesObject.filters && otelResourcesObject.labels) {
|
||||
// add support for otel data sources that are not standardized, i.e., have non unique target_info series by job, instance
|
||||
otelResourcesJoinQuery = `* on (job, instance) group_left(${otelResourcesObject.labels}) topk by (job, instance) (1, target_info{${otelResourcesObject.filters}})`;
|
||||
}
|
||||
|
||||
return otelResourcesJoinQuery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an object containing all the filters for otel resources as well as a list of labels
|
||||
*
|
||||
* @param scene
|
||||
* @param firstQueryVal
|
||||
* @returns
|
||||
*/
|
||||
export function getOtelResourcesObject(scene: SceneObject, firstQueryVal?: string): OtelResourcesObject {
|
||||
const otelResources = sceneGraph.lookupVariable(VAR_OTEL_RESOURCES, scene);
|
||||
// add deployment env to otel resource filters
|
||||
const otelDepEnv = sceneGraph.lookupVariable(VAR_OTEL_DEPLOYMENT_ENV, scene);
|
||||
|
||||
let otelResourcesObject = { labels: '', filters: '' };
|
||||
|
||||
if (otelResources instanceof AdHocFiltersVariable && otelDepEnv instanceof CustomVariable) {
|
||||
// get the collection of adhoc filters
|
||||
const otelFilters = otelResources.state.filters;
|
||||
|
||||
// get the value for deployment_environment variable
|
||||
let otelDepEnvValue = String(otelDepEnv.getValue());
|
||||
// check if there are multiple environments
|
||||
const isMulti = otelDepEnvValue.includes(',');
|
||||
// start with the default label filters for deployment_environment
|
||||
let op = '=';
|
||||
let val = firstQueryVal ? firstQueryVal : otelDepEnvValue;
|
||||
// update the filters if multiple deployment environments selected
|
||||
if (isMulti) {
|
||||
op = '=~';
|
||||
val = val.split(',').join('|');
|
||||
}
|
||||
|
||||
// start with the deployment environment
|
||||
let allFilters = `deployment_environment${op}"${val}"`;
|
||||
let allLabels = 'deployment_environment';
|
||||
|
||||
// add the other OTEL resource filters
|
||||
for (let i = 0; i < otelFilters?.length; i++) {
|
||||
const labelName = otelFilters[i].key;
|
||||
const op = otelFilters[i].operator;
|
||||
const labelValue = otelFilters[i].value;
|
||||
|
||||
allFilters += `,${labelName}${op}"${labelValue}"`;
|
||||
|
||||
const addLabelToGroupLeft = labelName !== 'job' && labelName !== 'instance';
|
||||
|
||||
if (addLabelToGroupLeft) {
|
||||
allLabels += `,${labelName}`;
|
||||
}
|
||||
}
|
||||
|
||||
otelResourcesObject.labels = allLabels;
|
||||
otelResourcesObject.filters = allFilters;
|
||||
|
||||
return otelResourcesObject;
|
||||
}
|
||||
return otelResourcesObject;
|
||||
}
|
||||
|
||||
/**
|
||||
* This function checks that when adding OTel job and instance filters
|
||||
* to the label values request for a list of metrics,
|
||||
* the total character count of the request does not exceed 2000 characters
|
||||
*
|
||||
* @param matchTerms __name__ and other Prom filters
|
||||
* @param jobsList list of jobs in target_info
|
||||
* @param instancesList list of instances in target_info
|
||||
* @param missingOtelTargets flag to indicate truncated job and instance filters
|
||||
* @returns
|
||||
*/
|
||||
export function limitOtelMatchTerms(
|
||||
matchTerms: string[],
|
||||
jobsList: string[],
|
||||
instancesList: string[],
|
||||
missingOtelTargets: boolean
|
||||
): { missingOtelTargets: boolean; jobsRegex: string; instancesRegex: string } {
|
||||
const charLimit = 2000;
|
||||
|
||||
let initialCharAmount = matchTerms.join(',').length;
|
||||
|
||||
// start to add values to the regex and start quote
|
||||
let jobsRegex = 'job=~"';
|
||||
let instancesRegex = 'instance=~"';
|
||||
|
||||
// iterate through the jobs and instances,
|
||||
// count the chars as they are added,
|
||||
// stop before the total count reaches 2000
|
||||
// show a warning that there are missing OTel targets and
|
||||
// the user must select more OTel resource attributes
|
||||
for (let i = 0; i < jobsList.length; i++) {
|
||||
// use or character for the count
|
||||
const orChars = i === 0 ? 0 : 2;
|
||||
// count all the characters that will go into the match terms
|
||||
const checkCharAmount =
|
||||
initialCharAmount +
|
||||
jobsRegex.length +
|
||||
jobsList[i].length +
|
||||
instancesRegex.length +
|
||||
instancesList[i].length +
|
||||
orChars;
|
||||
|
||||
if (checkCharAmount <= charLimit) {
|
||||
if (i === 0) {
|
||||
jobsRegex += `${jobsList[i]}`;
|
||||
instancesRegex += `${instancesList[i]}`;
|
||||
} else {
|
||||
jobsRegex += `|${jobsList[i]}`;
|
||||
instancesRegex += `|${instancesList[i]}`;
|
||||
}
|
||||
} else {
|
||||
missingOtelTargets = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
// complete the quote after values have been added
|
||||
jobsRegex += '"';
|
||||
instancesRegex += '"';
|
||||
|
||||
return {
|
||||
missingOtelTargets,
|
||||
jobsRegex,
|
||||
instancesRegex,
|
||||
};
|
||||
}
|
191
public/app/features/trails/otel/utils.test.ts
Normal file
191
public/app/features/trails/otel/utils.test.ts
Normal file
@ -0,0 +1,191 @@
|
||||
import { MetricFindValue } from '@grafana/data';
|
||||
|
||||
import { sortResources, getOtelJoinQuery, blessedList, limitOtelMatchTerms } from './util';
|
||||
|
||||
describe('sortResources', () => {
|
||||
it('should sort and filter resources correctly', () => {
|
||||
const resources: MetricFindValue[] = [
|
||||
{ text: 'cloud_region', value: 'cloud_region' },
|
||||
{ text: 'custom_resource', value: 'custom_resource' },
|
||||
];
|
||||
const excluded: string[] = ['cloud_region'];
|
||||
|
||||
const result = sortResources(resources, excluded);
|
||||
|
||||
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOtelJoinQuery', () => {
|
||||
it('should return the correct join query', () => {
|
||||
const otelResourcesObject = {
|
||||
filters: 'job="test-job",instance="test-instance"',
|
||||
labels: 'deployment_environment,custom_label',
|
||||
};
|
||||
|
||||
const result = getOtelJoinQuery(otelResourcesObject);
|
||||
|
||||
expect(result).toBe(
|
||||
'* on (job, instance) group_left(deployment_environment,custom_label) topk by (job, instance) (1, target_info{job="test-job",instance="test-instance"})'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an empty string if filters or labels are missing', () => {
|
||||
const otelResourcesObject = {
|
||||
filters: '',
|
||||
labels: '',
|
||||
};
|
||||
|
||||
const result = getOtelJoinQuery(otelResourcesObject);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('blessedList', () => {
|
||||
it('should return the correct blessed list', () => {
|
||||
const result = blessedList();
|
||||
expect(result).toEqual({
|
||||
cloud_availability_zone: 0,
|
||||
cloud_region: 0,
|
||||
container_name: 0,
|
||||
k8s_cluster_name: 0,
|
||||
k8s_container_name: 0,
|
||||
k8s_cronjob_name: 0,
|
||||
k8s_daemonset_name: 0,
|
||||
k8s_deployment_name: 0,
|
||||
k8s_job_name: 0,
|
||||
k8s_namespace_name: 0,
|
||||
k8s_pod_name: 0,
|
||||
k8s_replicaset_name: 0,
|
||||
k8s_statefulset_name: 0,
|
||||
service_instance_id: 0,
|
||||
service_name: 0,
|
||||
service_namespace: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('sortResources', () => {
|
||||
it('should sort and filter resources correctly', () => {
|
||||
const resources: MetricFindValue[] = [
|
||||
{ text: 'cloud_region', value: 'cloud_region' },
|
||||
{ text: 'custom_resource', value: 'custom_resource' },
|
||||
];
|
||||
const excluded: string[] = ['cloud_region'];
|
||||
|
||||
const result = sortResources(resources, excluded);
|
||||
|
||||
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]);
|
||||
});
|
||||
|
||||
it('should promote blessed resources and exclude specified ones', () => {
|
||||
const resources: MetricFindValue[] = [
|
||||
{ text: 'custom_resource', value: 'custom_resource' },
|
||||
{ text: 'k8s_cluster_name', value: 'k8s_cluster_name' },
|
||||
];
|
||||
const excluded: string[] = ['k8s_cluster_name'];
|
||||
|
||||
const result = sortResources(resources, excluded);
|
||||
|
||||
expect(result).toEqual([{ text: 'custom_resource', value: 'custom_resource' }]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getOtelJoinQuery', () => {
|
||||
it('should return the correct join query', () => {
|
||||
const otelResourcesObject = {
|
||||
filters: 'job="test-job",instance="test-instance"',
|
||||
labels: 'deployment_environment,custom_label',
|
||||
};
|
||||
|
||||
const result = getOtelJoinQuery(otelResourcesObject);
|
||||
|
||||
expect(result).toBe(
|
||||
'* on (job, instance) group_left(deployment_environment,custom_label) topk by (job, instance) (1, target_info{job="test-job",instance="test-instance"})'
|
||||
);
|
||||
});
|
||||
|
||||
it('should return an empty string if filters or labels are missing', () => {
|
||||
const otelResourcesObject = {
|
||||
filters: '',
|
||||
labels: '',
|
||||
};
|
||||
|
||||
const result = getOtelJoinQuery(otelResourcesObject);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('limitOtelMatchTerms', () => {
|
||||
it('should limit the OTel match terms if the total match term character count exceeds 2000', () => {
|
||||
// the initial match is 1980 characters
|
||||
const promMatchTerms: string[] = [
|
||||
`${[...Array(1979).keys()]
|
||||
.map((el) => {
|
||||
return '0';
|
||||
})
|
||||
.join('')}"`,
|
||||
];
|
||||
// job=~"" is 7 chars
|
||||
// instance=~"" is 12 characters
|
||||
|
||||
// 7 + 12 + 1979 = 1998
|
||||
// so we have room to add 2 more characters
|
||||
// attribute values that are b will be left out
|
||||
const jobs = ['a', 'b', 'c'];
|
||||
const instances = ['d', 'e', 'f'];
|
||||
|
||||
const missingOtelTargets = false;
|
||||
|
||||
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets);
|
||||
|
||||
expect(result.missingOtelTargets).toEqual(true);
|
||||
expect(result.jobsRegex).toEqual('job=~"a"');
|
||||
expect(result.instancesRegex).toEqual('instance=~"d"');
|
||||
});
|
||||
|
||||
it('should include | char in the count', () => {
|
||||
// the initial match is 1980 characters
|
||||
const promMatchTerms: string[] = [
|
||||
`${[...Array(1975).keys()]
|
||||
.map((el) => {
|
||||
return '0';
|
||||
})
|
||||
.join('')}"`,
|
||||
];
|
||||
// job=~"" is 7 chars
|
||||
// instance=~"" is 12 characters
|
||||
|
||||
// 7 + 12 + 1975 = 1994
|
||||
// so we have room to add 6 more characters
|
||||
// the extra 6 characters will be 'a|b' and 'd|e'
|
||||
const jobs = ['a', 'b', 'c'];
|
||||
const instances = ['d', 'e', 'f'];
|
||||
|
||||
const missingOtelTargets = false;
|
||||
|
||||
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets);
|
||||
|
||||
expect(result.missingOtelTargets).toEqual(true);
|
||||
expect(result.jobsRegex).toEqual('job=~"a|b"');
|
||||
expect(result.instancesRegex).toEqual('instance=~"d|e"');
|
||||
});
|
||||
|
||||
it('should add all OTel job and instance matches if the character count is less that 2000', () => {
|
||||
const promMatchTerms: string[] = [];
|
||||
|
||||
const jobs = ['job1', 'job2', 'job3', 'job4', 'job5'];
|
||||
|
||||
const instances = ['instance1', 'instance2', 'instance3', 'instance4', 'instance5'];
|
||||
|
||||
const missingOtelTargets = false;
|
||||
|
||||
const result = limitOtelMatchTerms(promMatchTerms, jobs, instances, missingOtelTargets);
|
||||
|
||||
expect(result.missingOtelTargets).toEqual(false);
|
||||
expect(result.jobsRegex).toEqual('job=~"job1|job2|job3|job4|job5"');
|
||||
expect(result.instancesRegex).toEqual('instance=~"instance1|instance2|instance3|instance4|instance5"');
|
||||
});
|
||||
});
|
@ -23,6 +23,12 @@ export const VAR_DATASOURCE = 'ds';
|
||||
export const VAR_DATASOURCE_EXPR = '${ds}';
|
||||
export const VAR_LOGS_DATASOURCE = 'logsDs';
|
||||
export const VAR_LOGS_DATASOURCE_EXPR = '${logsDs}';
|
||||
export const VAR_OTEL_RESOURCES = 'otel_resources';
|
||||
export const VAR_OTEL_RESOURCES_EXPR = '${otel_resources}';
|
||||
export const VAR_OTEL_DEPLOYMENT_ENV = 'deployment_environment';
|
||||
export const VAR_OTEL_DEPLOYMENT_ENV_EXPR = '${deployment_environment}';
|
||||
export const VAR_OTEL_JOIN_QUERY = 'otel_join_query';
|
||||
export const VAR_OTEL_JOIN_QUERY_EXPR = '${otel_join_query}';
|
||||
|
||||
export const LOGS_METRIC = '$__logs__';
|
||||
export const KEY_SQR_METRIC_VIZ_QUERY = 'sqr-metric-viz-query';
|
||||
@ -48,6 +54,16 @@ export function getVariablesWithMetricConstant(metric: string) {
|
||||
];
|
||||
}
|
||||
|
||||
export function getVariablesWithOtelJoinQueryConstant(otelJoinQuery: string) {
|
||||
return [
|
||||
new ConstantVariable({
|
||||
name: VAR_OTEL_JOIN_QUERY,
|
||||
value: otelJoinQuery,
|
||||
hide: VariableHide.hideVariable,
|
||||
}),
|
||||
];
|
||||
}
|
||||
|
||||
export class MetricSelectedEvent extends BusEventWithPayload<string | undefined> {
|
||||
public static type = 'metric-selected-event';
|
||||
}
|
||||
|
@ -2593,11 +2593,20 @@
|
||||
"trails": {
|
||||
"metric-overview": {
|
||||
"description-label": "Description",
|
||||
"labels-label": "Labels",
|
||||
"labels": "Labels",
|
||||
"metric-attributes": "Metric attributes",
|
||||
"no-description": "No description available",
|
||||
"type-label": "Type",
|
||||
"unit-label": "Unit",
|
||||
"unknown-type": "Unknown"
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Filter by",
|
||||
"otel-switch": "This switch enables filtering by OTel resources for OTel native data sources."
|
||||
},
|
||||
"settings": {
|
||||
"always-keep-selected-metric-graph-in-view": "Always keep selected metric graph in-view",
|
||||
"show-previews-of-metric-graphs": "Show previews of metric graphs"
|
||||
}
|
||||
},
|
||||
"transformations": {
|
||||
|
@ -2593,11 +2593,20 @@
|
||||
"trails": {
|
||||
"metric-overview": {
|
||||
"description-label": "Đęşčřįpŧįőʼn",
|
||||
"labels-label": "Ŀäþęľş",
|
||||
"labels": "Ŀäþęľş",
|
||||
"metric-attributes": "Męŧřįč äŧŧřįþūŧęş",
|
||||
"no-description": "Ńő đęşčřįpŧįőʼn äväįľäþľę",
|
||||
"type-label": "Ŧypę",
|
||||
"unit-label": "Ůʼnįŧ",
|
||||
"unknown-type": "Ůʼnĸʼnőŵʼn"
|
||||
},
|
||||
"metric-select": {
|
||||
"filter-by": "Fįľŧęř þy",
|
||||
"otel-switch": "Ŧĥįş şŵįŧčĥ ęʼnäþľęş ƒįľŧęřįʼnģ þy ØŦęľ řęşőūřčęş ƒőř ØŦęľ ʼnäŧįvę đäŧä şőūřčęş."
|
||||
},
|
||||
"settings": {
|
||||
"always-keep-selected-metric-graph-in-view": "Åľŵäyş ĸęęp şęľęčŧęđ męŧřįč ģřäpĥ įʼn-vįęŵ",
|
||||
"show-previews-of-metric-graphs": "Ŝĥőŵ přęvįęŵş őƒ męŧřįč ģřäpĥş"
|
||||
}
|
||||
},
|
||||
"transformations": {
|
||||
|
Loading…
Reference in New Issue
Block a user