Skip to content

Commit 08460f6

Browse files
MiriamApariciokibanamachinecrespocarlos
authored
[Metrics][Discover] Fix fields dropdown by casting dimension fields to string (#242432)
Closes #239952 ### Summary This PR resolves the data inconsistency between metric charts and the "Filter by value" dropdown, where values visible in charts were missing from the filter list. #### Problem The root cause was that the same dimension field (e.g., `attributes.cpu`) could have different data types across different indices: - `long` in one index - `keyword` in another index Elasticsearch aggregations and ES|QL queries fail when trying to process fields with inconsistent types, resulting in incomplete dimension values in the dropdown. #### Solution **1. Updated `get_dimentions.ts`:** - Migrated from Elasticsearch `_search` API with `terms` aggregations to ES|QL - Added explicit type casting to string using `::STRING` syntax - Increased dimension value limit from 20 to 1000 - Simplified response mapping logic **2. Updated `create_esql_query.ts`:** - Added type casting logic for dimensions in `WHERE` clauses - Applied the same `needsStringCasting()` check used in `CONCAT` operations - Ensures consistent string type handling for null checks BEFORE <img width="2304" height="560" alt="image" src="https://github.com/user-attachments/assets/e74c73af-838e-4282-b6f7-9778f03e562d" /> AFTER <img width="1043" height="292" alt="Screenshot 2025-11-10 at 14 37 35" src="https://github.com/user-attachments/assets/ade48ce5-1290-4fc7-afb4-57adcf3ebaf3" /> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Carlos Crespo <carloshenrique.leonelcrespo@elastic.co>
1 parent 4235b49 commit 08460f6

File tree

6 files changed

+60
-59
lines changed

6 files changed

+60
-59
lines changed

src/platform/packages/shared/kbn-traced-es-client/src/create_traced_es_client.ts

Lines changed: 6 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -176,30 +176,25 @@ export function createTracedEsClient({
176176
.query(
177177
{ ...parameters },
178178
{
179-
querystring: {
180-
drop_null_columns: true,
181-
},
179+
querystring: { drop_null_columns: true },
182180
...requestOpts,
183181
}
184182
)
185183
.then((response) => {
186-
const esqlResponse = response as unknown as UnparsedEsqlResponseOf<EsqlOutput>;
187-
188184
const transform = options?.transform ?? 'none';
189185

190186
if (transform === 'none') {
191-
return esqlResponse;
187+
return response;
192188
}
193189

194-
const parsedResponse = { hits: esqlResultToPlainObjects(esqlResponse) };
190+
const rawEsqlResponse = response.body as unknown as UnparsedEsqlResponseOf<EsqlOutput>;
191+
const hits = esqlResultToPlainObjects(rawEsqlResponse);
195192

196193
if (transform === 'plain') {
197-
return parsedResponse;
194+
return { ...response, body: { hits } };
198195
}
199196

200-
return {
201-
hits: parsedResponse.hits.map((hit) => unflattenObject(hit)),
202-
};
197+
return { ...response, body: { hits: hits.map((hit) => unflattenObject(hit)) } };
203198
}) as Promise<{ body: InferEsqlResponseOf<EsqlOutput, EsqlOptions> }>;
204199
});
205200
},

src/platform/packages/shared/kbn-unified-metrics-grid/src/common/utils/esql/create_esql_query.test.ts

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ TS metrics-*
9191
expect(query).toBe(
9292
`
9393
TS metrics-*
94-
| WHERE \`host.ip\` IS NOT NULL AND \`host.name\` IS NOT NULL
94+
| WHERE \`host.ip\`::STRING IS NOT NULL AND \`host.name\` IS NOT NULL
9595
| STATS AVG(cpu.usage) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend), \`host.ip\`, \`host.name\`
9696
| EVAL ${DIMENSIONS_COLUMN} = CONCAT(\`host.ip\`::STRING, " › ", \`host.name\`)
9797
| DROP \`host.ip\`, \`host.name\`
@@ -107,7 +107,7 @@ TS metrics-*
107107
expect(query).toBe(
108108
`
109109
TS metrics-*
110-
| WHERE \`cpu.cores\` IS NOT NULL AND \`host.name\` IS NOT NULL
110+
| WHERE \`cpu.cores\`::STRING IS NOT NULL AND \`host.name\` IS NOT NULL
111111
| STATS AVG(cpu.usage) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend), \`cpu.cores\`, \`host.name\`
112112
| EVAL ${DIMENSIONS_COLUMN} = CONCAT(\`cpu.cores\`::STRING, " › ", \`host.name\`)
113113
| DROP \`cpu.cores\`, \`host.name\`
@@ -123,7 +123,7 @@ TS metrics-*
123123
expect(query).toBe(
124124
`
125125
TS metrics-*
126-
| WHERE \`host.ip\` IS NOT NULL AND \`host.name\` IS NOT NULL AND \`cpu.cores\` IS NOT NULL
126+
| WHERE \`host.ip\`::STRING IS NOT NULL AND \`host.name\` IS NOT NULL AND \`cpu.cores\`::STRING IS NOT NULL
127127
| STATS AVG(cpu.usage) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend), \`host.ip\`, \`host.name\`, \`cpu.cores\`
128128
| EVAL ${DIMENSIONS_COLUMN} = CONCAT(\`host.ip\`::STRING, " › ", \`host.name\`, " › ", \`cpu.cores\`::STRING)
129129
| DROP \`host.ip\`, \`host.name\`, \`cpu.cores\`
@@ -271,7 +271,7 @@ TS metrics-*
271271
expect(query).toBe(
272272
`
273273
TS metrics-*
274-
| WHERE \`host-ip\` IS NOT NULL AND \`service-name\` IS NOT NULL
274+
| WHERE \`host-ip\`::STRING IS NOT NULL AND \`service-name\` IS NOT NULL
275275
| STATS AVG(cpu.usage) BY BUCKET(@timestamp, 100, ?_tstart, ?_tend), \`host-ip\`, \`service-name\`
276276
| EVAL ${DIMENSIONS_COLUMN} = CONCAT(\`host-ip\`::STRING, " › ", \`service-name\`)
277277
| DROP \`host-ip\`, \`service-name\`

src/platform/packages/shared/kbn-unified-metrics-grid/src/common/utils/esql/create_esql_query.ts

Lines changed: 17 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ export function createESQLQuery({ metric, dimensions = [], filters }: CreateESQL
5353

5454
const whereConditions: QueryOperator[] = [];
5555
const valuesByField = new Map<string, Set<string>>();
56+
const dimensionTypeMap = new Map(metricDimensions?.map((dim) => [dim.name, dim.type]));
57+
5658
if (filters && filters.length) {
5759
for (const filter of filters) {
5860
const currentValues = valuesByField.get(filter.field);
@@ -64,23 +66,31 @@ export function createESQLQuery({ metric, dimensions = [], filters }: CreateESQL
6466
}
6567

6668
valuesByField.forEach((value, key) => {
69+
const dimType = dimensionTypeMap.get(key);
70+
const escapedKey = sanitazeESQLInput(key);
71+
const castedKey =
72+
dimType && needsStringCasting(dimType) ? `${escapedKey}::STRING` : escapedKey;
73+
6774
whereConditions.push(
68-
where(
69-
`${sanitazeESQLInput(key)} IN (${new Array(value.size).fill('?').join(', ')})`,
70-
Array.from(value)
71-
)
75+
where(`${castedKey} IN (${new Array(value.size).fill('?').join(', ')})`, Array.from(value))
7276
);
7377
});
7478
}
7579

76-
const dimensionTypeMap = new Map(metricDimensions?.map((dim) => [dim.name, dim.type]));
77-
7880
const unfilteredDimensions = (dimensions ?? []).filter((dim) => !valuesByField.has(dim));
7981
const queryPipeline = source.pipe(
8082
...whereConditions,
8183
unfilteredDimensions.length > 0
8284
? where(
83-
unfilteredDimensions.map((dim) => `${sanitazeESQLInput(dim)} IS NOT NULL`).join(' AND ')
85+
unfilteredDimensions
86+
.map((dim) => {
87+
const dimType = dimensionTypeMap.get(dim);
88+
const escapedDim = sanitazeESQLInput(dim);
89+
const castedDim =
90+
dimType && needsStringCasting(dimType) ? `${escapedDim}::STRING` : escapedDim;
91+
return `${castedDim} IS NOT NULL`;
92+
})
93+
.join(' AND ')
8494
)
8595
: (query) => query,
8696
stats(

src/platform/plugins/shared/metrics_experience/server/routes/dimensions/get_dimentions.ts renamed to src/platform/plugins/shared/metrics_experience/server/routes/dimensions/get_dimensions.ts

Lines changed: 30 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
import type { Logger } from '@kbn/core/server';
1111
import { dateRangeQuery } from '@kbn/es-query';
1212
import type { TracedElasticsearchClient } from '@kbn/traced-es-client';
13-
import type { estypes } from '@elastic/elasticsearch';
13+
import { from as fromCommand, evaluate, where, stats, sort, limit } from '@kbn/esql-composer';
1414

1515
interface CreateDimensionsParams {
1616
esClient: TracedElasticsearchClient;
@@ -33,43 +33,38 @@ export const getDimensions = async ({
3333
return [];
3434
}
3535

36+
const source = fromCommand(indices);
37+
const query = source
38+
.pipe(
39+
evaluate('??dim = ??dim::string', { dim: dimensions[0] }),
40+
where('??dim IS NOT NULL', { dim: dimensions[0] }),
41+
stats('BY ??dim', {
42+
dim: dimensions[0],
43+
}),
44+
sort('??dim', { dim: dimensions[0] }),
45+
limit(20)
46+
)
47+
.toString();
48+
3649
try {
37-
const response = await esClient.search('get_dimensions', {
38-
index: indices.join(','),
39-
track_total_hits: false,
40-
size: 0,
41-
query: {
42-
bool: {
43-
filter: [...dateRangeQuery(from, to)],
50+
const response = await esClient.esql(
51+
'get_dimensions',
52+
{
53+
query,
54+
filter: {
55+
bool: {
56+
filter: [...dateRangeQuery(from, to)],
57+
},
4458
},
4559
},
46-
// Create aggregations for each dimension
47-
aggs: dimensions.reduce((acc, currDimension) => {
48-
acc[currDimension] = {
49-
terms: {
50-
field: currDimension,
51-
size: 20,
52-
order: { _key: 'asc' },
53-
},
54-
};
55-
56-
return acc;
57-
}, {} as Record<string, Pick<estypes.AggregationsAggregationContainer, 'terms'>>),
58-
});
59-
60-
const aggregations = response.aggregations;
61-
62-
const values = dimensions.flatMap((dimension) => {
63-
const agg = aggregations?.[dimension];
64-
return (
65-
agg?.buckets?.map((bucket) => ({
66-
value: String(bucket.key ?? ''),
67-
field: dimension,
68-
})) ?? []
69-
);
70-
});
71-
72-
return values;
60+
{
61+
transform: 'plain',
62+
}
63+
);
64+
return response.hits.map((hit) => ({
65+
value: String(hit[dimensions[0]]),
66+
field: dimensions[0],
67+
}));
7368
} catch (error) {
7469
logger.error('Error fetching dimension values:', error);
7570
return [];

src/platform/plugins/shared/metrics_experience/server/routes/dimensions/route.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ import { isoToEpoch } from '@kbn/zod-helpers';
1313
import { parse as dateMathParse } from '@kbn/datemath';
1414
import { getRequestAbortedSignal } from '@kbn/data-plugin/server';
1515
import { createRoute } from '../create_route';
16-
import { getDimensions } from './get_dimentions';
16+
import { getDimensions } from './get_dimensions';
1717
import { throwNotFoundIfMetricsExperienceDisabled } from '../../lib/utils';
1818

1919
export const getDimensionsRoute = createRoute({

src/platform/plugins/shared/metrics_experience/tsconfig.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@
2020
"@kbn/otel-semantic-conventions",
2121
"@kbn/i18n",
2222
"@kbn/core-chrome-browser",
23-
"@kbn/data-plugin"
23+
"@kbn/data-plugin",
24+
"@kbn/esql-composer"
2425
]
2526
}

0 commit comments

Comments
 (0)