Skip to content

Commit ff10d70

Browse files
Fix problem with validation (#224961)
## Summary Recently, an alarm was fired in the Kibana Serverless Slack Channel due a problem with Data Usage. Issue explanation: > Kibana's data_usage plugin allows collecting misc stats about Kibana usage. > The browser side performs requests to /internal/api/data_usage/*, providing stats related to the user interaction with the UI. > Recently, in an internal customer project, on production, one of these requests (POST /internal/api/data_usage/metrics) contained a payload that was deemed invalid by the server-side validation logic. > > The handler on that endpoint logged an error message. > This error message was spotted by a Rule. > Consequently, the rule fired an alert in our Slack channel. > > We shouldn't have invalid payloads coming from browser side, so unless someone intentionally tampered with the HTTP request, this indicates a bug in our browser-side logic. > Customer Impact: This was an isolated error on an internal project, but other folks within Elastic have spotted the same error message in their projects. Due to that error, we might be missing a few data_usage metrics. The issue was caused because the plugin validation was waiting for an array for the data property, but the payload from the API was returning `null`. The validation was incorrect in the Kibana side since only [name](https://github.com/elastic/autoops-services/blob/master/monitoring/service/specs/serverless_project_metrics_api.yaml#L189) is mandatory. --------- Co-authored-by: kibanamachine <[email protected]>
1 parent e566fec commit ff10d70

File tree

3 files changed

+114
-5
lines changed

3 files changed

+114
-5
lines changed

x-pack/platform/plugins/private/data_usage/common/rest_types/usage_metrics.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ export const UsageMetricsAutoOpsResponseSchema = {
123123
schema.object({
124124
name: schema.string(),
125125
error: schema.nullable(schema.string()),
126-
data: schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 })),
126+
data: schema.nullable(
127+
schema.arrayOf(schema.arrayOf(schema.number(), { minSize: 2, maxSize: 2 }))
128+
),
127129
})
128130
)
129131
),

x-pack/platform/plugins/private/data_usage/server/routes/internal/usage_metrics.test.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,111 @@ describe('registerUsageMetricsRoute', () => {
190190
});
191191
});
192192

193+
describe('when metric type data is null', () => {
194+
beforeEach(() => {
195+
jest.spyOn(DataUsageService.prototype, 'getMetrics').mockResolvedValue({
196+
ingest_rate: [
197+
{
198+
name: '.ds-1',
199+
error: null,
200+
data: null,
201+
},
202+
{
203+
name: '.ds-2',
204+
error: null,
205+
data: [
206+
[1726858530000, 12894623],
207+
[1726862130000, 14436905],
208+
],
209+
},
210+
],
211+
storage_retained: [
212+
{
213+
name: '.ds-1',
214+
error: null,
215+
data: [
216+
[1726858530000, 12576413],
217+
[1726862130000, 13956423],
218+
],
219+
},
220+
{
221+
name: '.ds-2',
222+
error: null,
223+
data: null,
224+
},
225+
],
226+
search_vcu: [],
227+
ingest_vcu: [],
228+
ml_vcu: [],
229+
index_latency: [],
230+
index_rate: [],
231+
search_latency: [],
232+
search_rate: [],
233+
});
234+
});
235+
it('should correctly transform response when metric type data is null', async () => {
236+
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest
237+
.fn()
238+
.mockResolvedValue({
239+
data_streams: [{ name: '.ds-1' }, { name: '.ds-2' }],
240+
});
241+
242+
registerUsageMetricsRoute(router, mockedDataUsageContext);
243+
244+
const mockRequest = httpServerMock.createKibanaRequest({
245+
body: {
246+
from: utcTimeRange.start,
247+
to: utcTimeRange.end,
248+
metricTypes: ['ingest_rate', 'storage_retained'],
249+
dataStreams: ['.ds-1', '.ds-2'],
250+
},
251+
});
252+
const mockResponse = httpServerMock.createResponseFactory();
253+
const mockRouter = mockCore.http.createRouter.mock.results[0].value;
254+
const [[, handler]] = mockRouter.versioned.post.mock.results[0].value.addVersion.mock.calls;
255+
await handler(context, mockRequest, mockResponse);
256+
257+
expect(mockResponse.ok).toHaveBeenCalledTimes(1);
258+
expect(mockResponse.ok.mock.calls[0][0]).toEqual({
259+
body: {
260+
ingest_rate: [
261+
{
262+
name: '.ds-1',
263+
data: [],
264+
},
265+
{
266+
name: '.ds-2',
267+
data: [
268+
{ x: 1726858530000, y: 12894623 },
269+
{ x: 1726862130000, y: 14436905 },
270+
],
271+
},
272+
],
273+
storage_retained: [
274+
{
275+
name: '.ds-1',
276+
data: [
277+
{ x: 1726858530000, y: 12576413 },
278+
{ x: 1726862130000, y: 13956423 },
279+
],
280+
},
281+
{
282+
name: '.ds-2',
283+
data: [],
284+
},
285+
],
286+
search_vcu: [],
287+
ingest_vcu: [],
288+
ml_vcu: [],
289+
index_latency: [],
290+
index_rate: [],
291+
search_latency: [],
292+
search_rate: [],
293+
},
294+
});
295+
});
296+
});
297+
193298
// TODO: fix this test
194299
it.skip('should throw error if error on requesting auto ops service', async () => {
195300
(await context.core).elasticsearch.client.asCurrentUser.indices.getDataStream = jest

x-pack/platform/plugins/private/data_usage/server/routes/internal/usage_metrics_handler.ts

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,10 +112,12 @@ export function transformMetricsData(
112112
metricType,
113113
series.map((metricSeries) => ({
114114
name: metricSeries.name,
115-
data: (metricSeries.data as Array<[number, number]>).map(([timestamp, value]) => ({
116-
x: timestamp,
117-
y: value,
118-
})),
115+
data: Array.isArray(metricSeries.data)
116+
? (metricSeries.data as Array<[number, number]>).map(([timestamp, value]) => ({
117+
x: timestamp,
118+
y: value,
119+
}))
120+
: [],
119121
})),
120122
])
121123
) as UsageMetricsResponseSchemaBody;

0 commit comments

Comments
 (0)