mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(slo): Handle instanceId for historical summary (#163114)
This commit is contained in:
parent
2e02a6cf00
commit
498d6fdccc
16 changed files with 4460 additions and 1811 deletions
|
@ -159,11 +159,15 @@ const findSLOResponseSchema = t.type({
|
|||
});
|
||||
|
||||
const fetchHistoricalSummaryParamsSchema = t.type({
|
||||
body: t.type({ sloIds: t.array(sloIdSchema) }),
|
||||
body: t.type({ list: t.array(t.type({ sloId: sloIdSchema, instanceId: allOrAnyString })) }),
|
||||
});
|
||||
const fetchHistoricalSummaryResponseSchema = t.record(
|
||||
sloIdSchema,
|
||||
t.array(historicalSummarySchema)
|
||||
|
||||
const fetchHistoricalSummaryResponseSchema = t.array(
|
||||
t.type({
|
||||
sloId: sloIdSchema,
|
||||
instanceId: allOrAnyString,
|
||||
data: t.array(historicalSummarySchema),
|
||||
})
|
||||
);
|
||||
|
||||
const getSLODiagnosisParamsSchema = t.type({
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -5,18 +5,24 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { HistoricalSummaryResponse } from '@kbn/slo-schema';
|
||||
import { FetchHistoricalSummaryResponse } from '@kbn/slo-schema';
|
||||
import {
|
||||
HEALTHY_ROLLING_SLO,
|
||||
historicalSummaryData,
|
||||
} from '../../../data/slo/historical_summary_data';
|
||||
import { UseFetchHistoricalSummaryResponse, Params } from '../use_fetch_historical_summary';
|
||||
import { Params, UseFetchHistoricalSummaryResponse } from '../use_fetch_historical_summary';
|
||||
|
||||
export const useFetchHistoricalSummary = ({
|
||||
sloIds = [],
|
||||
list = [],
|
||||
}: Params): UseFetchHistoricalSummaryResponse => {
|
||||
const data: Record<string, HistoricalSummaryResponse[]> = {};
|
||||
sloIds.forEach((sloId) => (data[sloId] = historicalSummaryData[HEALTHY_ROLLING_SLO]));
|
||||
const data: FetchHistoricalSummaryResponse = [];
|
||||
list.forEach(({ sloId, instanceId }) =>
|
||||
data.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!.data,
|
||||
})
|
||||
);
|
||||
|
||||
return {
|
||||
isLoading: false,
|
||||
|
|
|
@ -31,7 +31,8 @@ export const sloKeys = {
|
|||
activeAlerts: () => [...sloKeys.all, 'activeAlerts'] as const,
|
||||
activeAlert: (sloIds: string[]) => [...sloKeys.activeAlerts(), sloIds] as const,
|
||||
historicalSummaries: () => [...sloKeys.all, 'historicalSummary'] as const,
|
||||
historicalSummary: (sloIds: string[]) => [...sloKeys.historicalSummaries(), sloIds] as const,
|
||||
historicalSummary: (list: Array<{ sloId: string; instanceId: string }>) =>
|
||||
[...sloKeys.historicalSummaries(), list] as const,
|
||||
globalDiagnosis: () => [...sloKeys.all, 'globalDiagnosis'] as const,
|
||||
burnRates: (sloId: string) => [...sloKeys.all, 'burnRates', sloId] as const,
|
||||
preview: (indicator?: Indicator) => [...sloKeys.all, 'preview', indicator] as const,
|
||||
|
|
|
@ -21,25 +21,26 @@ export interface UseFetchHistoricalSummaryResponse {
|
|||
}
|
||||
|
||||
export interface Params {
|
||||
sloIds: string[];
|
||||
list: Array<{ sloId: string; instanceId: string }>;
|
||||
shouldRefetch?: boolean;
|
||||
}
|
||||
|
||||
const LONG_REFETCH_INTERVAL = 1000 * 60; // 1 minute
|
||||
|
||||
export function useFetchHistoricalSummary({
|
||||
sloIds = [],
|
||||
list = [],
|
||||
shouldRefetch,
|
||||
}: Params): UseFetchHistoricalSummaryResponse {
|
||||
const { http } = useKibana().services;
|
||||
|
||||
const { isInitialLoading, isLoading, isError, isSuccess, isRefetching, data } = useQuery({
|
||||
queryKey: sloKeys.historicalSummary(sloIds),
|
||||
queryKey: sloKeys.historicalSummary(list),
|
||||
queryFn: async ({ signal }) => {
|
||||
try {
|
||||
const response = await http.post<FetchHistoricalSummaryResponse>(
|
||||
'/internal/observability/slos/_historical_summary',
|
||||
{
|
||||
body: JSON.stringify({ sloIds }),
|
||||
body: JSON.stringify({ list }),
|
||||
signal,
|
||||
}
|
||||
);
|
||||
|
|
|
@ -13,7 +13,7 @@ import {
|
|||
EuiTabbedContent,
|
||||
EuiTabbedContentTab,
|
||||
} from '@elastic/eui';
|
||||
import { SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE, SLOWithSummaryResponse } from '@kbn/slo-schema';
|
||||
import React, { Fragment, useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
|
||||
|
@ -41,14 +41,23 @@ type TabId = typeof OVERVIEW_TAB_ID | typeof ALERTS_TAB_ID;
|
|||
export function SloDetails({ slo, isAutoRefreshing }: Props) {
|
||||
const { search } = useLocation();
|
||||
const { data: activeAlerts } = useFetchActiveAlerts({ sloIds: [slo.id] });
|
||||
const { isLoading: historicalSummaryLoading, data: historicalSummaryBySlo = {} } =
|
||||
useFetchHistoricalSummary({ sloIds: [slo.id], shouldRefetch: isAutoRefreshing });
|
||||
const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } =
|
||||
useFetchHistoricalSummary({
|
||||
list: [{ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE }],
|
||||
shouldRefetch: isAutoRefreshing,
|
||||
});
|
||||
|
||||
const sloHistoricalSummary = historicalSummaries.find(
|
||||
(historicalSummary) =>
|
||||
historicalSummary.sloId === slo.id &&
|
||||
historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE)
|
||||
);
|
||||
|
||||
const errorBudgetBurnDownData = formatHistoricalData(
|
||||
historicalSummaryBySlo[slo.id],
|
||||
sloHistoricalSummary?.data,
|
||||
'error_budget_remaining'
|
||||
);
|
||||
const historicalSliData = formatHistoricalData(historicalSummaryBySlo[slo.id], 'sli_value');
|
||||
const historicalSliData = formatHistoricalData(sloHistoricalSummary?.data, 'sli_value');
|
||||
|
||||
const tabs: EuiTabbedContentTab[] = [
|
||||
{
|
||||
|
|
|
@ -158,7 +158,7 @@ describe('SLO Details Page', () => {
|
|||
useLicenseMock.mockReturnValue({ hasAtLeast: () => true });
|
||||
useFetchHistoricalSummaryMock.mockReturnValue({
|
||||
isLoading: true,
|
||||
data: {},
|
||||
data: [],
|
||||
});
|
||||
|
||||
render(<SloDetailsPage />);
|
||||
|
|
|
@ -28,7 +28,8 @@ const Template: ComponentStory<typeof Component> = (props: SloListItemProps) =>
|
|||
|
||||
const defaultProps = {
|
||||
slo: buildSlo(),
|
||||
historicalSummary: historicalSummaryData[HEALTHY_ROLLING_SLO],
|
||||
historicalSummary: historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!
|
||||
.data,
|
||||
};
|
||||
|
||||
export const SloListItem = Template.bind({});
|
||||
|
|
|
@ -25,8 +25,10 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
|
||||
const { data: activeAlertsBySlo } = useFetchActiveAlerts({ sloIds });
|
||||
const { data: rulesBySlo } = useFetchRulesForSlo({ sloIds });
|
||||
const { isLoading: historicalSummaryLoading, data: historicalSummaryBySlo } =
|
||||
useFetchHistoricalSummary({ sloIds });
|
||||
const { isLoading: historicalSummaryLoading, data: historicalSummaries = [] } =
|
||||
useFetchHistoricalSummary({
|
||||
list: sloList.map((slo) => ({ sloId: slo.id, instanceId: slo.instanceId ?? ALL_VALUE })),
|
||||
});
|
||||
|
||||
if (!loading && !error && sloList.length === 0) {
|
||||
return <SloListEmpty />;
|
||||
|
@ -42,7 +44,13 @@ export function SloListItems({ sloList, loading, error }: Props) {
|
|||
<SloListItem
|
||||
activeAlerts={activeAlertsBySlo[slo.id]}
|
||||
rules={rulesBySlo?.[slo.id]}
|
||||
historicalSummary={historicalSummaryBySlo?.[slo.id]}
|
||||
historicalSummary={
|
||||
historicalSummaries.find(
|
||||
(historicalSummary) =>
|
||||
historicalSummary.sloId === slo.id &&
|
||||
historicalSummary.instanceId === (slo.instanceId ?? ALL_VALUE)
|
||||
)?.data
|
||||
}
|
||||
historicalSummaryLoading={historicalSummaryLoading}
|
||||
slo={slo}
|
||||
/>
|
||||
|
|
|
@ -5,20 +5,19 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { HistoricalSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
import React from 'react';
|
||||
import {
|
||||
HEALTHY_ROLLING_SLO,
|
||||
DEGRADING_FAST_ROLLING_SLO,
|
||||
HEALTHY_RANDOM_ROLLING_SLO,
|
||||
HEALTHY_ROLLING_SLO,
|
||||
HEALTHY_STEP_DOWN_ROLLING_SLO,
|
||||
historicalSummaryData,
|
||||
DEGRADING_FAST_ROLLING_SLO,
|
||||
NO_DATA_TO_HEALTHY_ROLLING_SLO,
|
||||
} from '../../../data/slo/historical_summary_data';
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { SloSparkline as Component, Props } from './slo_sparkline';
|
||||
import { Props, SloSparkline as Component } from './slo_sparkline';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
|
@ -33,7 +32,9 @@ AreaWithHealthyFlatData.args = {
|
|||
chart: 'area',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toBudgetBurnDown(historicalSummaryData[HEALTHY_ROLLING_SLO]),
|
||||
data: toBudgetBurnDown(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const AreaWithHealthyRandomData = Template.bind({});
|
||||
|
@ -41,7 +42,9 @@ AreaWithHealthyRandomData.args = {
|
|||
chart: 'area',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toBudgetBurnDown(historicalSummaryData[HEALTHY_RANDOM_ROLLING_SLO]),
|
||||
data: toBudgetBurnDown(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_RANDOM_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const AreaWithHealthyStepDownData = Template.bind({});
|
||||
|
@ -49,7 +52,9 @@ AreaWithHealthyStepDownData.args = {
|
|||
chart: 'area',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toBudgetBurnDown(historicalSummaryData[HEALTHY_STEP_DOWN_ROLLING_SLO]),
|
||||
data: toBudgetBurnDown(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_STEP_DOWN_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const AreaWithDegradingLinearData = Template.bind({});
|
||||
|
@ -57,7 +62,9 @@ AreaWithDegradingLinearData.args = {
|
|||
chart: 'area',
|
||||
state: 'error',
|
||||
id: 'history',
|
||||
data: toBudgetBurnDown(historicalSummaryData[DEGRADING_FAST_ROLLING_SLO]),
|
||||
data: toBudgetBurnDown(
|
||||
historicalSummaryData.find((datum) => datum.sloId === DEGRADING_FAST_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const AreaWithNoDataToDegradingLinearData = Template.bind({});
|
||||
|
@ -65,7 +72,9 @@ AreaWithNoDataToDegradingLinearData.args = {
|
|||
chart: 'area',
|
||||
state: 'error',
|
||||
id: 'history',
|
||||
data: toBudgetBurnDown(historicalSummaryData[NO_DATA_TO_HEALTHY_ROLLING_SLO]),
|
||||
data: toBudgetBurnDown(
|
||||
historicalSummaryData.find((datum) => datum.sloId === NO_DATA_TO_HEALTHY_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const LineWithHealthyFlatData = Template.bind({});
|
||||
|
@ -73,7 +82,9 @@ LineWithHealthyFlatData.args = {
|
|||
chart: 'line',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toSliHistory(historicalSummaryData[HEALTHY_ROLLING_SLO]),
|
||||
data: toSliHistory(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const LineWithHealthyRandomData = Template.bind({});
|
||||
|
@ -81,7 +92,9 @@ LineWithHealthyRandomData.args = {
|
|||
chart: 'line',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toSliHistory(historicalSummaryData[HEALTHY_RANDOM_ROLLING_SLO]),
|
||||
data: toSliHistory(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_RANDOM_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const LineWithHealthyStepDownData = Template.bind({});
|
||||
|
@ -89,7 +102,9 @@ LineWithHealthyStepDownData.args = {
|
|||
chart: 'line',
|
||||
state: 'success',
|
||||
id: 'history',
|
||||
data: toSliHistory(historicalSummaryData[HEALTHY_STEP_DOWN_ROLLING_SLO]),
|
||||
data: toSliHistory(
|
||||
historicalSummaryData.find((datum) => datum.sloId === HEALTHY_STEP_DOWN_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const LineWithDegradingLinearData = Template.bind({});
|
||||
|
@ -97,7 +112,9 @@ LineWithDegradingLinearData.args = {
|
|||
chart: 'line',
|
||||
state: 'error',
|
||||
id: 'history',
|
||||
data: toSliHistory(historicalSummaryData[DEGRADING_FAST_ROLLING_SLO]),
|
||||
data: toSliHistory(
|
||||
historicalSummaryData.find((datum) => datum.sloId === DEGRADING_FAST_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
export const LineWithNoDataToDegradingLinearData = Template.bind({});
|
||||
|
@ -105,7 +122,9 @@ LineWithNoDataToDegradingLinearData.args = {
|
|||
chart: 'line',
|
||||
state: 'error',
|
||||
id: 'history',
|
||||
data: toSliHistory(historicalSummaryData[NO_DATA_TO_HEALTHY_ROLLING_SLO]),
|
||||
data: toSliHistory(
|
||||
historicalSummaryData.find((datum) => datum.sloId === NO_DATA_TO_HEALTHY_ROLLING_SLO)!.data
|
||||
),
|
||||
};
|
||||
|
||||
function toBudgetBurnDown(data: HistoricalSummaryResponse[]) {
|
||||
|
|
|
@ -5,16 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { ComponentStory } from '@storybook/react';
|
||||
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import React from 'react';
|
||||
import {
|
||||
HEALTHY_ROLLING_SLO,
|
||||
historicalSummaryData,
|
||||
} from '../../../data/slo/historical_summary_data';
|
||||
import { buildSlo } from '../../../data/slo/slo';
|
||||
import { SloSummary as Component, Props } from './slo_summary';
|
||||
import { KibanaReactStorybookDecorator } from '../../../utils/kibana_react.storybook_decorator';
|
||||
import { Props, SloSummary as Component } from './slo_summary';
|
||||
|
||||
export default {
|
||||
component: Component,
|
||||
|
@ -26,7 +25,8 @@ const Template: ComponentStory<typeof Component> = (props: Props) => <Component
|
|||
|
||||
const defaultProps = {
|
||||
slo: buildSlo(),
|
||||
historicalSummary: historicalSummaryData[HEALTHY_ROLLING_SLO],
|
||||
historicalSummary: historicalSummaryData.find((datum) => datum.sloId === HEALTHY_ROLLING_SLO)!
|
||||
.data,
|
||||
historicalSummaryLoading: false,
|
||||
};
|
||||
|
||||
|
|
|
@ -5,17 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FetchHistoricalSummaryResponse } from '@kbn/slo-schema';
|
||||
|
||||
import { HistoricalSummaryResponse } from '@kbn/slo-schema';
|
||||
import { ChartData } from '../../typings/slo';
|
||||
|
||||
type DataType = 'error_budget_remaining' | 'error_budget_consumed' | 'sli_value';
|
||||
|
||||
export function formatHistoricalData(
|
||||
historicalSummary: FetchHistoricalSummaryResponse[string] | undefined,
|
||||
historicalSummary: HistoricalSummaryResponse[] | undefined = [],
|
||||
dataType: DataType
|
||||
): ChartData[] {
|
||||
function getDataValue(data: FetchHistoricalSummaryResponse[string][number]) {
|
||||
function getDataValue(data: HistoricalSummaryResponse) {
|
||||
switch (dataType) {
|
||||
case 'error_budget_consumed':
|
||||
return data.errorBudget.consumed;
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -10,7 +10,7 @@ import {
|
|||
FetchHistoricalSummaryResponse,
|
||||
fetchHistoricalSummaryResponseSchema,
|
||||
} from '@kbn/slo-schema';
|
||||
import { HistoricalSummaryClient } from './historical_summary_client';
|
||||
import { HistoricalSummaryClient, SLOWithInstanceId } from './historical_summary_client';
|
||||
import { SLORepository } from './slo_repository';
|
||||
|
||||
export class FetchHistoricalSummary {
|
||||
|
@ -19,11 +19,20 @@ export class FetchHistoricalSummary {
|
|||
private historicalSummaryClient: HistoricalSummaryClient
|
||||
) {}
|
||||
|
||||
public async execute({
|
||||
sloIds,
|
||||
}: FetchHistoricalSummaryParams): Promise<FetchHistoricalSummaryResponse> {
|
||||
public async execute(
|
||||
params: FetchHistoricalSummaryParams
|
||||
): Promise<FetchHistoricalSummaryResponse> {
|
||||
const sloIds = params.list.map((slo) => slo.sloId);
|
||||
const sloList = await this.repository.findAllByIds(sloIds);
|
||||
const historicalSummaryBySlo = await this.historicalSummaryClient.fetch(sloList);
|
||||
return fetchHistoricalSummaryResponseSchema.encode(historicalSummaryBySlo);
|
||||
|
||||
const list: SLOWithInstanceId[] = params.list.map(({ sloId, instanceId }) => ({
|
||||
sloId,
|
||||
instanceId,
|
||||
slo: sloList.find((slo) => slo.id === sloId)!,
|
||||
}));
|
||||
|
||||
const historicalSummary = await this.historicalSummaryClient.fetch(list);
|
||||
|
||||
return fetchHistoricalSummaryResponseSchema.encode(historicalSummary);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import moment from 'moment';
|
||||
import { oneMinute, oneMonth, thirtyDays } from './fixtures/duration';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
|
@ -140,16 +141,18 @@ describe('FetchHistoricalSummary', () => {
|
|||
const slo = createSLO({
|
||||
timeWindow: { type: 'rolling', duration: thirtyDays() },
|
||||
objective: { target: 0.95 },
|
||||
groupBy: ALL_VALUE,
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30));
|
||||
const client = new DefaultHistoricalSummaryClient(esClientMock);
|
||||
|
||||
const results = await client.fetch([slo]);
|
||||
results[slo.id].forEach((dailyResult) =>
|
||||
const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]);
|
||||
|
||||
results[0].data.forEach((dailyResult) =>
|
||||
expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) })
|
||||
);
|
||||
|
||||
expect(results[slo.id]).toHaveLength(180);
|
||||
expect(results[0].data).toHaveLength(180);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -159,16 +162,17 @@ describe('FetchHistoricalSummary', () => {
|
|||
timeWindow: { type: 'rolling', duration: thirtyDays() },
|
||||
budgetingMethod: 'timeslices',
|
||||
objective: { target: 0.95, timesliceTarget: 0.9, timesliceWindow: oneMinute() },
|
||||
groupBy: ALL_VALUE,
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30));
|
||||
const client = new DefaultHistoricalSummaryClient(esClientMock);
|
||||
|
||||
const results = await client.fetch([slo]);
|
||||
const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]);
|
||||
|
||||
results[slo.id].forEach((dailyResult) =>
|
||||
results[0].data.forEach((dailyResult) =>
|
||||
expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) })
|
||||
);
|
||||
expect(results[slo.id]).toHaveLength(180);
|
||||
expect(results[0].data).toHaveLength(180);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -185,13 +189,12 @@ describe('FetchHistoricalSummary', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO());
|
||||
const client = new DefaultHistoricalSummaryClient(esClientMock);
|
||||
|
||||
const results = await client.fetch([slo]);
|
||||
const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]);
|
||||
|
||||
results[slo.id].forEach((dailyResult) =>
|
||||
results[0].data.forEach((dailyResult) =>
|
||||
expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) })
|
||||
);
|
||||
|
||||
expect(results[slo.id]).toHaveLength(108);
|
||||
expect(results[0].data).toHaveLength(108);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -208,13 +211,35 @@ describe('FetchHistoricalSummary', () => {
|
|||
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForMonthlyCalendarAlignedSLO());
|
||||
const client = new DefaultHistoricalSummaryClient(esClientMock);
|
||||
|
||||
const results = await client.fetch([slo]);
|
||||
const results = await client.fetch([{ slo, sloId: slo.id, instanceId: ALL_VALUE }]);
|
||||
|
||||
results[slo.id].forEach((dailyResult) =>
|
||||
results[0].data.forEach((dailyResult) =>
|
||||
expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) })
|
||||
);
|
||||
|
||||
expect(results[slo.id]).toHaveLength(108);
|
||||
expect(results[0].data).toHaveLength(108);
|
||||
});
|
||||
});
|
||||
|
||||
it("filters with the 'instanceId' when provided", async () => {
|
||||
const slo = createSLO({
|
||||
timeWindow: { type: 'rolling', duration: thirtyDays() },
|
||||
objective: { target: 0.95 },
|
||||
groupBy: 'host',
|
||||
});
|
||||
esClientMock.msearch.mockResolvedValueOnce(generateEsResponseForRollingSLO(30));
|
||||
const client = new DefaultHistoricalSummaryClient(esClientMock);
|
||||
|
||||
const results = await client.fetch([{ slo, sloId: slo.id, instanceId: 'host-abc' }]);
|
||||
|
||||
expect(
|
||||
// @ts-ignore
|
||||
esClientMock.msearch.mock.calls[0][0].searches[1].query.bool.filter[3]
|
||||
).toEqual({ term: { 'slo.instanceId': 'host-abc' } });
|
||||
|
||||
results[0].data.forEach((dailyResult) =>
|
||||
expect(dailyResult).toMatchSnapshot({ date: expect.any(Date) })
|
||||
);
|
||||
expect(results[0].data).toHaveLength(180);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -8,14 +8,17 @@
|
|||
import { MsearchMultisearchBody } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import {
|
||||
ALL_VALUE,
|
||||
calendarAlignedTimeWindowSchema,
|
||||
Duration,
|
||||
fetchHistoricalSummaryResponseSchema,
|
||||
occurrencesBudgetingMethodSchema,
|
||||
rollingTimeWindowSchema,
|
||||
timeslicesBudgetingMethodSchema,
|
||||
toMomentUnitOfTime,
|
||||
} from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import * as t from 'io-ts';
|
||||
import moment from 'moment';
|
||||
import { SLO_DESTINATION_INDEX_PATTERN } from '../../assets/constants';
|
||||
import { DateRange, HistoricalSummary, SLO, SLOId } from '../../domain/models';
|
||||
|
@ -44,36 +47,44 @@ interface DailyAggBucket {
|
|||
};
|
||||
}
|
||||
|
||||
export interface SLOWithInstanceId {
|
||||
sloId: SLOId;
|
||||
instanceId: string;
|
||||
slo: SLO;
|
||||
}
|
||||
|
||||
export type HistoricalSummaryResponse = t.TypeOf<typeof fetchHistoricalSummaryResponseSchema>;
|
||||
|
||||
export interface HistoricalSummaryClient {
|
||||
fetch(sloList: SLO[]): Promise<Record<SLOId, HistoricalSummary[]>>;
|
||||
fetch(list: SLOWithInstanceId[]): Promise<HistoricalSummaryResponse>;
|
||||
}
|
||||
|
||||
export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient {
|
||||
constructor(private esClient: ElasticsearchClient) {}
|
||||
|
||||
async fetch(sloList: SLO[]): Promise<Record<SLOId, HistoricalSummary[]>> {
|
||||
const dateRangeBySlo = sloList.reduce<Record<SLOId, DateRange>>((acc, slo) => {
|
||||
acc[slo.id] = getDateRange(slo);
|
||||
async fetch(list: SLOWithInstanceId[]): Promise<HistoricalSummaryResponse> {
|
||||
const dateRangeBySlo = list.reduce<Record<SLOId, DateRange>>((acc, { sloId, slo }) => {
|
||||
acc[sloId] = getDateRange(slo);
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
const searches = sloList.flatMap((slo) => [
|
||||
const searches = list.flatMap(({ sloId, instanceId, slo }) => [
|
||||
{ index: SLO_DESTINATION_INDEX_PATTERN },
|
||||
generateSearchQuery(slo, dateRangeBySlo[slo.id]),
|
||||
generateSearchQuery(slo, instanceId, dateRangeBySlo[sloId]),
|
||||
]);
|
||||
|
||||
const historicalSummaryBySlo: Record<SLOId, HistoricalSummary[]> = {};
|
||||
const historicalSummary: HistoricalSummaryResponse = [];
|
||||
if (searches.length === 0) {
|
||||
return historicalSummaryBySlo;
|
||||
return historicalSummary;
|
||||
}
|
||||
|
||||
const result = await this.esClient.msearch({ searches });
|
||||
|
||||
for (let i = 0; i < result.responses.length; i++) {
|
||||
const slo = sloList[i];
|
||||
const { slo, sloId, instanceId } = list[i];
|
||||
if ('error' in result.responses[i]) {
|
||||
// handle errorneous responses with an empty historical summary
|
||||
historicalSummaryBySlo[slo.id] = [];
|
||||
// handle errorneous responses with an empty historical summary data
|
||||
historicalSummary.push({ sloId, instanceId, data: [] });
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -81,26 +92,32 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient {
|
|||
const buckets = (result.responses[i].aggregations?.daily?.buckets as DailyAggBucket[]) || [];
|
||||
|
||||
if (rollingTimeWindowSchema.is(slo.timeWindow)) {
|
||||
historicalSummaryBySlo[slo.id] = handleResultForRolling(slo, buckets);
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForRolling(slo, buckets),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
if (calendarAlignedTimeWindowSchema.is(slo.timeWindow)) {
|
||||
if (timeslicesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
const dateRange = dateRangeBySlo[slo.id];
|
||||
historicalSummaryBySlo[slo.id] = handleResultForCalendarAlignedAndTimeslices(
|
||||
slo,
|
||||
buckets,
|
||||
dateRange
|
||||
);
|
||||
const dateRange = dateRangeBySlo[sloId];
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForCalendarAlignedAndTimeslices(slo, buckets, dateRange),
|
||||
});
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
if (occurrencesBudgetingMethodSchema.is(slo.budgetingMethod)) {
|
||||
historicalSummaryBySlo[slo.id] = handleResultForCalendarAlignedAndOccurrences(
|
||||
slo,
|
||||
buckets
|
||||
);
|
||||
historicalSummary.push({
|
||||
sloId,
|
||||
instanceId,
|
||||
data: handleResultForCalendarAlignedAndOccurrences(slo, buckets),
|
||||
});
|
||||
continue;
|
||||
}
|
||||
|
||||
|
@ -110,7 +127,7 @@ export class DefaultHistoricalSummaryClient implements HistoricalSummaryClient {
|
|||
assertNever(slo.timeWindow);
|
||||
}
|
||||
|
||||
return historicalSummaryBySlo;
|
||||
return historicalSummary;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -186,13 +203,22 @@ function handleResultForRolling(slo: SLO, buckets: DailyAggBucket[]): Historical
|
|||
});
|
||||
}
|
||||
|
||||
function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearchBody {
|
||||
function generateSearchQuery(
|
||||
slo: SLO,
|
||||
instanceId: string,
|
||||
dateRange: DateRange
|
||||
): MsearchMultisearchBody {
|
||||
const unit = toMomentUnitOfTime(slo.timeWindow.duration.unit);
|
||||
const timeWindowDurationInDays = moment.duration(slo.timeWindow.duration.value, unit).asDays();
|
||||
|
||||
const { fixedInterval, bucketsPerDay } =
|
||||
getFixedIntervalAndBucketsPerDay(timeWindowDurationInDays);
|
||||
|
||||
const extraFilterByInstanceId =
|
||||
!!slo.groupBy && slo.groupBy !== ALL_VALUE && instanceId !== ALL_VALUE
|
||||
? [{ term: { 'slo.instanceId': instanceId } }]
|
||||
: [];
|
||||
|
||||
return {
|
||||
size: 0,
|
||||
query: {
|
||||
|
@ -208,6 +234,7 @@ function generateSearchQuery(slo: SLO, dateRange: DateRange): MsearchMultisearch
|
|||
},
|
||||
},
|
||||
},
|
||||
...extraFilterByInstanceId,
|
||||
],
|
||||
},
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue