Skip to content

Commit f6a2688

Browse files
[9.4] [APM] Omit HDR percentiles when duration histogram is exponential_histogram (#290874) (#291039)
# Backport This will backport the following commits from `main` to `9.4`: - [[APM] Omit HDR percentiles when duration histogram is exponential_histogram (#290874)](#290874) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Miriam","email":"31922082+MiriamAparicio@users.noreply.github.com"},"sourceCommit":{"committedDate":"2026-09-15T08:15:29Z","message":"[APM] Omit HDR percentiles when duration histogram is exponential_histogram (#290874)\n\n## Summary\n\n- Fixes Latency Distribution going blank or 500ing on OTel/mOTLP\nmetrics, where `transaction.duration.histogram` is mapped as\n`exponential_histogram` and Elasticsearch rejects HDR (`Percentiles\nalgorithm hdr must not be used with exponential_histogram field`).\n- Before aggregating, field caps checks whether any metric index maps\nthat field as `exponential_histogram` (including empty mappings: HDR\nfails on mapping even with 0 hits). If so, HDR is omitted; classic\n`histogram`-only clusters still use HDR.\n- Closes #289525\n\n## Test plan\n\n### Unit\n\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.test.ts`\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/has_exponential_histogram_mapping.test.ts`\n\n### Manual: synthtrace + exponential_histogram stub\n\n`otel_simple_trace` writes traces (and some OTel metrics) but **not**\nECS `transaction.duration.histogram` as `exponential_histogram`. The\nextra Dev Tools steps below are required to hit the bug path.\n\n1. Start the stack (`yarn es snapshot` / `yarn start`, or your usual\nlocal ES+Kibana).\n2. Ingest OTel traces (gives a service/transaction to open in APM; other\nlatency charts should already have data):\n\n ```bash\nnode scripts/synthtrace otel_simple_trace --local --from=now-1h --to=now\n ```\n\n3. In Console, create a high-priority template so `metrics-repro.otel-*`\nis not swallowed by `metrics-otel@template`, then recreate the data\nstream:\n\n ```json\n PUT /_index_template/repro-exp-histo\n {\n \"index_patterns\": [\"metrics-repro.otel-*\"],\n \"priority\": 1000,\n \"data_stream\": {},\n \"template\": {\n \"mappings\": {\n \"properties\": {\n \"@timestamp\": { \"type\": \"date\" },\n \"processor.event\": { \"type\": \"keyword\" },\n \"service.name\": { \"type\": \"keyword\" },\n \"service.environment\": { \"type\": \"keyword\" },\n \"transaction.name\": { \"type\": \"keyword\" },\n \"transaction.type\": { \"type\": \"keyword\" },\n\"transaction.duration.histogram\": { \"type\": \"exponential_histogram\" }\n }\n }\n }\n }\n\n DELETE /_data_stream/metrics-repro.otel-default\n PUT /_data_stream/metrics-repro.otel-default\n ```\n\n4. Index a metric doc **inside the APM time picker** (Last 15 minutes /\nLast 1 hour). Use a UTC timestamp a few minutes ago, not a future time.\n`service.environment` must be keyword and match the synthtrace\nenvironment:\n\n ```json\n POST /metrics-repro.otel-default/_doc?refresh=true\n {\n \"@timestamp\": \"<UTC now minus a few minutes>\",\n \"processor.event\": \"metric\",\n \"service.name\": \"sendotlp-otel-native-synth\",\n \"service.environment\": \"Synthtrace: otel_simple_trace\",\n \"transaction.name\": \"oteldemo.AdServiceSynth/GetAds\",\n \"transaction.type\": \"request\",\n \"transaction.duration.histogram\": {\n \"scale\": 0,\n \"sum\": 1000,\n \"min\": 100,\n \"max\": 2000,\n \"zero\": { \"count\": 0 },\n \"positive\": { \"indices\": [10], \"counts\": [5] }\n }\n }\n ```\n\n5. In APM: **Traces** → service `sendotlp-otel-native-synth` →\ntransaction `oteldemo.AdServiceSynth/GetAds` → **Latency Distribution**\n(same time range as the stub). Environment: `Synthtrace:\notel_simple_trace` or All.\n\n6. **Before this PR:** chart is empty or toasts `An error occurred\nfetching the overall latency distribution.` Inspect\n`get_duration_percentiles` has `hdr` and may show shard failures.\n\n7. **After this PR:**\n- Chart shows distribution bars (the failed-transactions overlay can\nstill be empty; the stub has no `event.outcome: failure`).\n- Inspect `get_duration_field_caps` returns `exponential_histogram` for\n`transaction.duration.histogram`.\n- Inspect `get_duration_percentiles` has `\"percentiles\": { \"field\":\n\"transaction.duration.histogram\" }` **without** `hdr`, `hits.total >=\n1`, numeric `95.0`.\n- Follow-up requests `get_duration_histogram_range_steps` and\n`get_duration_ranges` run.\n\n8. Optional classic check: without the stub (or on an Elastic-agent\nsynthtrace scenario that only has `histogram` mappings), Inspect should\nstill send `hdr` on `transaction.duration.histogram`.","sha":"6a558186d8d1f94af0dec60adf987a9d88f0c340","branchLabelMapping":{"^v9.6.0$":"main","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:skip","backport:version","v9.4.0","v9.5.0","v9.6.0","Team:obs-signals-traces"],"title":"[APM] Omit HDR percentiles when duration histogram is exponential_histogram","number":290874,"url":"https://github.com/elastic/kibana/pull/290874","mergeCommit":{"message":"[APM] Omit HDR percentiles when duration histogram is exponential_histogram (#290874)\n\n## Summary\n\n- Fixes Latency Distribution going blank or 500ing on OTel/mOTLP\nmetrics, where `transaction.duration.histogram` is mapped as\n`exponential_histogram` and Elasticsearch rejects HDR (`Percentiles\nalgorithm hdr must not be used with exponential_histogram field`).\n- Before aggregating, field caps checks whether any metric index maps\nthat field as `exponential_histogram` (including empty mappings: HDR\nfails on mapping even with 0 hits). If so, HDR is omitted; classic\n`histogram`-only clusters still use HDR.\n- Closes #289525\n\n## Test plan\n\n### Unit\n\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.test.ts`\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/has_exponential_histogram_mapping.test.ts`\n\n### Manual: synthtrace + exponential_histogram stub\n\n`otel_simple_trace` writes traces (and some OTel metrics) but **not**\nECS `transaction.duration.histogram` as `exponential_histogram`. The\nextra Dev Tools steps below are required to hit the bug path.\n\n1. Start the stack (`yarn es snapshot` / `yarn start`, or your usual\nlocal ES+Kibana).\n2. Ingest OTel traces (gives a service/transaction to open in APM; other\nlatency charts should already have data):\n\n ```bash\nnode scripts/synthtrace otel_simple_trace --local --from=now-1h --to=now\n ```\n\n3. In Console, create a high-priority template so `metrics-repro.otel-*`\nis not swallowed by `metrics-otel@template`, then recreate the data\nstream:\n\n ```json\n PUT /_index_template/repro-exp-histo\n {\n \"index_patterns\": [\"metrics-repro.otel-*\"],\n \"priority\": 1000,\n \"data_stream\": {},\n \"template\": {\n \"mappings\": {\n \"properties\": {\n \"@timestamp\": { \"type\": \"date\" },\n \"processor.event\": { \"type\": \"keyword\" },\n \"service.name\": { \"type\": \"keyword\" },\n \"service.environment\": { \"type\": \"keyword\" },\n \"transaction.name\": { \"type\": \"keyword\" },\n \"transaction.type\": { \"type\": \"keyword\" },\n\"transaction.duration.histogram\": { \"type\": \"exponential_histogram\" }\n }\n }\n }\n }\n\n DELETE /_data_stream/metrics-repro.otel-default\n PUT /_data_stream/metrics-repro.otel-default\n ```\n\n4. Index a metric doc **inside the APM time picker** (Last 15 minutes /\nLast 1 hour). Use a UTC timestamp a few minutes ago, not a future time.\n`service.environment` must be keyword and match the synthtrace\nenvironment:\n\n ```json\n POST /metrics-repro.otel-default/_doc?refresh=true\n {\n \"@timestamp\": \"<UTC now minus a few minutes>\",\n \"processor.event\": \"metric\",\n \"service.name\": \"sendotlp-otel-native-synth\",\n \"service.environment\": \"Synthtrace: otel_simple_trace\",\n \"transaction.name\": \"oteldemo.AdServiceSynth/GetAds\",\n \"transaction.type\": \"request\",\n \"transaction.duration.histogram\": {\n \"scale\": 0,\n \"sum\": 1000,\n \"min\": 100,\n \"max\": 2000,\n \"zero\": { \"count\": 0 },\n \"positive\": { \"indices\": [10], \"counts\": [5] }\n }\n }\n ```\n\n5. In APM: **Traces** → service `sendotlp-otel-native-synth` →\ntransaction `oteldemo.AdServiceSynth/GetAds` → **Latency Distribution**\n(same time range as the stub). Environment: `Synthtrace:\notel_simple_trace` or All.\n\n6. **Before this PR:** chart is empty or toasts `An error occurred\nfetching the overall latency distribution.` Inspect\n`get_duration_percentiles` has `hdr` and may show shard failures.\n\n7. **After this PR:**\n- Chart shows distribution bars (the failed-transactions overlay can\nstill be empty; the stub has no `event.outcome: failure`).\n- Inspect `get_duration_field_caps` returns `exponential_histogram` for\n`transaction.duration.histogram`.\n- Inspect `get_duration_percentiles` has `\"percentiles\": { \"field\":\n\"transaction.duration.histogram\" }` **without** `hdr`, `hits.total >=\n1`, numeric `95.0`.\n- Follow-up requests `get_duration_histogram_range_steps` and\n`get_duration_ranges` run.\n\n8. Optional classic check: without the stub (or on an Elastic-agent\nsynthtrace scenario that only has `histogram` mappings), Inspect should\nstill send `hdr` on `transaction.duration.histogram`.","sha":"6a558186d8d1f94af0dec60adf987a9d88f0c340"}},"sourceBranch":"main","suggestedTargetBranches":["9.4","9.5"],"targetPullRequestStates":[{"branch":"9.4","label":"v9.4.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"9.5","label":"v9.5.0","branchLabelMappingKey":"^v(\\d+).(\\d+).\\d+$","isSourceBranch":false,"state":"NOT_CREATED"},{"branch":"main","label":"v9.6.0","branchLabelMappingKey":"^v9.6.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/290874","number":290874,"mergeCommit":{"message":"[APM] Omit HDR percentiles when duration histogram is exponential_histogram (#290874)\n\n## Summary\n\n- Fixes Latency Distribution going blank or 500ing on OTel/mOTLP\nmetrics, where `transaction.duration.histogram` is mapped as\n`exponential_histogram` and Elasticsearch rejects HDR (`Percentiles\nalgorithm hdr must not be used with exponential_histogram field`).\n- Before aggregating, field caps checks whether any metric index maps\nthat field as `exponential_histogram` (including empty mappings: HDR\nfails on mapping even with 0 hits). If so, HDR is omitted; classic\n`histogram`-only clusters still use HDR.\n- Closes #289525\n\n## Test plan\n\n### Unit\n\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.test.ts`\n- [ ] `node scripts/jest\nx-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/has_exponential_histogram_mapping.test.ts`\n\n### Manual: synthtrace + exponential_histogram stub\n\n`otel_simple_trace` writes traces (and some OTel metrics) but **not**\nECS `transaction.duration.histogram` as `exponential_histogram`. The\nextra Dev Tools steps below are required to hit the bug path.\n\n1. Start the stack (`yarn es snapshot` / `yarn start`, or your usual\nlocal ES+Kibana).\n2. Ingest OTel traces (gives a service/transaction to open in APM; other\nlatency charts should already have data):\n\n ```bash\nnode scripts/synthtrace otel_simple_trace --local --from=now-1h --to=now\n ```\n\n3. In Console, create a high-priority template so `metrics-repro.otel-*`\nis not swallowed by `metrics-otel@template`, then recreate the data\nstream:\n\n ```json\n PUT /_index_template/repro-exp-histo\n {\n \"index_patterns\": [\"metrics-repro.otel-*\"],\n \"priority\": 1000,\n \"data_stream\": {},\n \"template\": {\n \"mappings\": {\n \"properties\": {\n \"@timestamp\": { \"type\": \"date\" },\n \"processor.event\": { \"type\": \"keyword\" },\n \"service.name\": { \"type\": \"keyword\" },\n \"service.environment\": { \"type\": \"keyword\" },\n \"transaction.name\": { \"type\": \"keyword\" },\n \"transaction.type\": { \"type\": \"keyword\" },\n\"transaction.duration.histogram\": { \"type\": \"exponential_histogram\" }\n }\n }\n }\n }\n\n DELETE /_data_stream/metrics-repro.otel-default\n PUT /_data_stream/metrics-repro.otel-default\n ```\n\n4. Index a metric doc **inside the APM time picker** (Last 15 minutes /\nLast 1 hour). Use a UTC timestamp a few minutes ago, not a future time.\n`service.environment` must be keyword and match the synthtrace\nenvironment:\n\n ```json\n POST /metrics-repro.otel-default/_doc?refresh=true\n {\n \"@timestamp\": \"<UTC now minus a few minutes>\",\n \"processor.event\": \"metric\",\n \"service.name\": \"sendotlp-otel-native-synth\",\n \"service.environment\": \"Synthtrace: otel_simple_trace\",\n \"transaction.name\": \"oteldemo.AdServiceSynth/GetAds\",\n \"transaction.type\": \"request\",\n \"transaction.duration.histogram\": {\n \"scale\": 0,\n \"sum\": 1000,\n \"min\": 100,\n \"max\": 2000,\n \"zero\": { \"count\": 0 },\n \"positive\": { \"indices\": [10], \"counts\": [5] }\n }\n }\n ```\n\n5. In APM: **Traces** → service `sendotlp-otel-native-synth` →\ntransaction `oteldemo.AdServiceSynth/GetAds` → **Latency Distribution**\n(same time range as the stub). Environment: `Synthtrace:\notel_simple_trace` or All.\n\n6. **Before this PR:** chart is empty or toasts `An error occurred\nfetching the overall latency distribution.` Inspect\n`get_duration_percentiles` has `hdr` and may show shard failures.\n\n7. **After this PR:**\n- Chart shows distribution bars (the failed-transactions overlay can\nstill be empty; the stub has no `event.outcome: failure`).\n- Inspect `get_duration_field_caps` returns `exponential_histogram` for\n`transaction.duration.histogram`.\n- Inspect `get_duration_percentiles` has `\"percentiles\": { \"field\":\n\"transaction.duration.histogram\" }` **without** `hdr`, `hits.total >=\n1`, numeric `95.0`.\n- Follow-up requests `get_duration_histogram_range_steps` and\n`get_duration_ranges` run.\n\n8. Optional classic check: without the stub (or on an Elastic-agent\nsynthtrace scenario that only has `histogram` mappings), Inspect should\nstill send `hdr` on `transaction.duration.histogram`.","sha":"6a558186d8d1f94af0dec60adf987a9d88f0c340"}}]}] BACKPORT--> Co-authored-by: Miriam <31922082+MiriamAparicio@users.noreply.github.com>
1 parent a1eed4e commit f6a2688

4 files changed

Lines changed: 274 additions & 3 deletions

File tree

Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { LatencyDistributionChartType } from '../../../../common/latency_distribution_chart_types';
9+
import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
10+
import { fetchDurationPercentiles } from './fetch_duration_percentiles';
11+
12+
const percentileValues = {
13+
'1.0': 10,
14+
'5.0': 20,
15+
'25.0': 50,
16+
'50.0': 100,
17+
'75.0': 200,
18+
'95.0': 400,
19+
'99.0': 800,
20+
};
21+
22+
function createApmEventClient({
23+
fieldTypes = {},
24+
}: {
25+
fieldTypes?: Record<string, { type: string }>;
26+
} = {}) {
27+
return {
28+
fieldCaps: jest.fn().mockResolvedValue({
29+
fields: {
30+
'transaction.duration.histogram': fieldTypes,
31+
},
32+
}),
33+
search: jest.fn().mockResolvedValue({
34+
hits: { total: { value: 1, relation: 'eq' } },
35+
aggregations: {
36+
duration_percentiles: { values: percentileValues },
37+
},
38+
}),
39+
} as unknown as APMEventClient & {
40+
fieldCaps: jest.Mock;
41+
search: jest.Mock;
42+
};
43+
}
44+
45+
const baseParams = {
46+
start: 0,
47+
end: 1,
48+
environment: 'ENVIRONMENT_ALL' as const,
49+
kuery: '',
50+
query: { match_all: {} },
51+
chartType: LatencyDistributionChartType.transactionLatency,
52+
};
53+
54+
describe('fetchDurationPercentiles', () => {
55+
it('keeps HDR percentiles for raw transaction events', async () => {
56+
const apmEventClient = createApmEventClient();
57+
58+
await fetchDurationPercentiles({
59+
...baseParams,
60+
apmEventClient,
61+
searchMetrics: false,
62+
});
63+
64+
expect(apmEventClient.fieldCaps).not.toHaveBeenCalled();
65+
expect(apmEventClient.search).toHaveBeenCalledWith(
66+
'get_duration_percentiles',
67+
expect.objectContaining({
68+
aggs: {
69+
duration_percentiles: {
70+
percentiles: expect.objectContaining({
71+
field: 'transaction.duration.us',
72+
hdr: { number_of_significant_value_digits: 3 },
73+
}),
74+
},
75+
},
76+
}),
77+
expect.anything()
78+
);
79+
});
80+
81+
it('keeps HDR percentiles for classic histogram mappings', async () => {
82+
const apmEventClient = createApmEventClient({
83+
fieldTypes: {
84+
histogram: { type: 'histogram' },
85+
},
86+
});
87+
88+
await fetchDurationPercentiles({
89+
...baseParams,
90+
apmEventClient,
91+
searchMetrics: true,
92+
});
93+
94+
expect(apmEventClient.fieldCaps).toHaveBeenCalledWith(
95+
'get_duration_field_caps',
96+
expect.objectContaining({
97+
fields: ['transaction.duration.histogram'],
98+
include_empty_fields: true,
99+
})
100+
);
101+
expect(apmEventClient.fieldCaps.mock.calls[0][1].index_filter).toBeUndefined();
102+
expect(apmEventClient.search.mock.calls[0][1].aggs.duration_percentiles.percentiles).toEqual(
103+
expect.objectContaining({
104+
field: 'transaction.duration.histogram',
105+
hdr: { number_of_significant_value_digits: 3 },
106+
})
107+
);
108+
});
109+
110+
it('omits HDR percentiles when the duration field is exponential_histogram', async () => {
111+
const apmEventClient = createApmEventClient({
112+
fieldTypes: {
113+
exponential_histogram: { type: 'exponential_histogram' },
114+
},
115+
});
116+
117+
await fetchDurationPercentiles({
118+
...baseParams,
119+
apmEventClient,
120+
searchMetrics: true,
121+
});
122+
123+
const percentilesAgg =
124+
apmEventClient.search.mock.calls[0][1].aggs.duration_percentiles.percentiles;
125+
expect(percentilesAgg.field).toBe('transaction.duration.histogram');
126+
expect(percentilesAgg.hdr).toBeUndefined();
127+
});
128+
129+
it('omits HDR percentiles when histogram and exponential_histogram mappings are mixed', async () => {
130+
const apmEventClient = createApmEventClient({
131+
fieldTypes: {
132+
histogram: { type: 'histogram' },
133+
exponential_histogram: { type: 'exponential_histogram' },
134+
},
135+
});
136+
137+
await fetchDurationPercentiles({
138+
...baseParams,
139+
apmEventClient,
140+
searchMetrics: true,
141+
});
142+
143+
expect(
144+
apmEventClient.search.mock.calls[0][1].aggs.duration_percentiles.percentiles.hdr
145+
).toBeUndefined();
146+
});
147+
});

x-pack/solutions/observability/plugins/apm/server/routes/correlations/queries/fetch_duration_percentiles.ts

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
*/
77

88
import { SIGNIFICANT_VALUE_DIGITS } from '../../../../common/correlations/constants';
9+
import { TRANSACTION_DURATION_HISTOGRAM } from '../../../../common/es_fields/apm';
910
import type { LatencyDistributionChartType } from '../../../../common/latency_distribution_chart_types';
1011
import { getCommonCorrelationsQuery } from './get_common_correlations_query';
1112
import type {
@@ -20,6 +21,7 @@ import {
2021
} from '../utils';
2122
import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
2223
import { getBackwardCompatibleDocumentTypeFilter } from '../../../lib/helpers/transactions';
24+
import { hasExponentialHistogramMapping } from './has_exponential_histogram_mapping';
2325

2426
export const fetchDurationPercentiles = async ({
2527
chartType,
@@ -62,6 +64,14 @@ export const fetchDurationPercentiles = async ({
6264
? getDurationFieldFromEntityType(entityType, isOtel)
6365
: getDurationField(chartType!, searchMetrics, isOtel);
6466

67+
const omitHdr =
68+
durationField === TRANSACTION_DURATION_HISTOGRAM &&
69+
(await hasExponentialHistogramMapping({
70+
apmEventClient,
71+
eventType,
72+
durationField,
73+
}));
74+
6575
const params = {
6676
apm: { events: [eventType] },
6777
track_total_hits: true,
@@ -76,10 +86,14 @@ export const fetchDurationPercentiles = async ({
7686
aggs: {
7787
duration_percentiles: {
7888
percentiles: {
79-
hdr: {
80-
number_of_significant_value_digits: SIGNIFICANT_VALUE_DIGITS,
81-
},
8289
field: durationField,
90+
...(omitHdr
91+
? {}
92+
: {
93+
hdr: {
94+
number_of_significant_value_digits: SIGNIFICANT_VALUE_DIGITS,
95+
},
96+
}),
8397
...(Array.isArray(percents) ? { percents } : {}),
8498
},
8599
},
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { ProcessorEvent } from '@kbn/observability-plugin/common';
9+
import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
10+
import { hasExponentialHistogramMapping } from './has_exponential_histogram_mapping';
11+
12+
describe('hasExponentialHistogramMapping', () => {
13+
const baseParams = {
14+
eventType: ProcessorEvent.metric,
15+
durationField: 'transaction.duration.histogram',
16+
};
17+
18+
it('returns false when the field is not mapped', async () => {
19+
const apmEventClient = {
20+
fieldCaps: jest.fn().mockResolvedValue({ fields: {} }),
21+
} as unknown as APMEventClient;
22+
23+
await expect(hasExponentialHistogramMapping({ ...baseParams, apmEventClient })).resolves.toBe(
24+
false
25+
);
26+
});
27+
28+
it('returns false for classic histogram mappings', async () => {
29+
const apmEventClient = {
30+
fieldCaps: jest.fn().mockResolvedValue({
31+
fields: {
32+
'transaction.duration.histogram': {
33+
histogram: { type: 'histogram' },
34+
},
35+
},
36+
}),
37+
} as unknown as APMEventClient;
38+
39+
await expect(hasExponentialHistogramMapping({ ...baseParams, apmEventClient })).resolves.toBe(
40+
false
41+
);
42+
});
43+
44+
it('returns true when any mapping is exponential_histogram', async () => {
45+
const apmEventClient = {
46+
fieldCaps: jest.fn().mockResolvedValue({
47+
fields: {
48+
'transaction.duration.histogram': {
49+
histogram: { type: 'histogram' },
50+
exponential_histogram: { type: 'exponential_histogram' },
51+
},
52+
},
53+
}),
54+
} as unknown as APMEventClient;
55+
56+
await expect(hasExponentialHistogramMapping({ ...baseParams, apmEventClient })).resolves.toBe(
57+
true
58+
);
59+
});
60+
61+
it('requests empty-field mappings without a time filter', async () => {
62+
const apmEventClient = {
63+
fieldCaps: jest.fn().mockResolvedValue({ fields: {} }),
64+
} as unknown as APMEventClient;
65+
66+
await hasExponentialHistogramMapping({ ...baseParams, apmEventClient });
67+
68+
expect(apmEventClient.fieldCaps).toHaveBeenCalledWith('get_duration_field_caps', {
69+
apm: { events: [ProcessorEvent.metric] },
70+
fields: ['transaction.duration.histogram'],
71+
include_empty_fields: true,
72+
});
73+
});
74+
});
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License
4+
* 2.0; you may not use this file except in compliance with the Elastic License
5+
* 2.0.
6+
*/
7+
8+
import { ES_FIELD_TYPES } from '@kbn/field-types';
9+
import type { ProcessorEvent } from '@kbn/observability-plugin/common';
10+
import type { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
11+
12+
/** True when any index maps the duration field as exponential_histogram (HDR percentiles are invalid). */
13+
export async function hasExponentialHistogramMapping({
14+
apmEventClient,
15+
eventType,
16+
durationField,
17+
}: {
18+
apmEventClient: APMEventClient;
19+
eventType: ProcessorEvent;
20+
durationField: string;
21+
}): Promise<boolean> {
22+
// Do not time-filter or skip empty fields: HDR fails on mapping even when
23+
// matching shards have 0 hits in the selected range.
24+
const fieldCaps = await apmEventClient.fieldCaps('get_duration_field_caps', {
25+
apm: { events: [eventType] },
26+
fields: [durationField],
27+
include_empty_fields: true,
28+
});
29+
30+
const fieldTypes = fieldCaps.fields[durationField];
31+
if (!fieldTypes) {
32+
return false;
33+
}
34+
35+
return Object.keys(fieldTypes).includes(ES_FIELD_TYPES.EXPONENTIAL_HISTOGRAM);
36+
}

0 commit comments

Comments
 (0)