mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
fix(slo): introduce cursor pagination in Find SLO API (#203712)
This commit is contained in:
parent
a8e1bf46a3
commit
464d361cc7
14 changed files with 323 additions and 84 deletions
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { merge } from 'lodash';
|
||||
import { findSLOParamsSchema } from './find';
|
||||
|
||||
const BASE_REQUEST = {
|
||||
query: {
|
||||
filters: 'irrelevant',
|
||||
kqlQuery: 'irrelevant',
|
||||
page: '1',
|
||||
perPage: '25',
|
||||
sortBy: 'error_budget_consumed',
|
||||
sortDirection: 'asc',
|
||||
hideStale: true,
|
||||
},
|
||||
};
|
||||
|
||||
describe('FindSLO schema validation', () => {
|
||||
it.each(['not_an_array', 42, [], [42, 'ok']])(
|
||||
'returns an error when searchAfter is not a valid JSON array (%s)',
|
||||
(searchAfter) => {
|
||||
const request = merge(BASE_REQUEST, {
|
||||
query: {
|
||||
searchAfter,
|
||||
},
|
||||
});
|
||||
const result = findSLOParamsSchema.decode(request);
|
||||
expect(result._tag === 'Left').toBe(true);
|
||||
}
|
||||
);
|
||||
|
||||
it('parses searchAfter correctly', () => {
|
||||
const request = merge(BASE_REQUEST, {
|
||||
query: {
|
||||
searchAfter: JSON.stringify([1, 'ok']),
|
||||
},
|
||||
});
|
||||
const result = findSLOParamsSchema.decode(request);
|
||||
expect(result._tag === 'Right').toBe(true);
|
||||
});
|
||||
});
|
|
@ -4,8 +4,9 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import * as t from 'io-ts';
|
||||
import { toBooleanRt } from '@kbn/io-ts-utils';
|
||||
import { either, isRight } from 'fp-ts/lib/Either';
|
||||
import * as t from 'io-ts';
|
||||
import { sloWithDataResponseSchema } from '../slo';
|
||||
|
||||
const sortDirectionSchema = t.union([t.literal('asc'), t.literal('desc')]);
|
||||
|
@ -19,24 +20,64 @@ const sortBySchema = t.union([
|
|||
t.literal('burn_rate_1d'),
|
||||
]);
|
||||
|
||||
const searchAfterArraySchema = t.array(t.union([t.string, t.number]));
|
||||
type SearchAfterArray = t.TypeOf<typeof searchAfterArraySchema>;
|
||||
|
||||
const searchAfterSchema = new t.Type<SearchAfterArray, string, unknown>(
|
||||
'SearchAfter',
|
||||
(input: unknown): input is SearchAfterArray =>
|
||||
Array.isArray(input) &&
|
||||
input.length > 0 &&
|
||||
input.every((item) => typeof item === 'string' || typeof item === 'number'),
|
||||
(input: unknown, context: t.Context) =>
|
||||
either.chain(t.string.validate(input, context), (value: string) => {
|
||||
try {
|
||||
const parsedValue = JSON.parse(value);
|
||||
const decoded = searchAfterArraySchema.decode(parsedValue);
|
||||
if (isRight(decoded)) {
|
||||
return t.success(decoded.right);
|
||||
}
|
||||
return t.failure(
|
||||
input,
|
||||
context,
|
||||
'Invalid searchAfter value, must be a JSON array of strings or numbers'
|
||||
);
|
||||
} catch (err) {
|
||||
return t.failure(
|
||||
input,
|
||||
context,
|
||||
'Invalid searchAfter value, must be a JSON array of strings or numbers'
|
||||
);
|
||||
}
|
||||
}),
|
||||
(input: SearchAfterArray): string => JSON.stringify(input)
|
||||
);
|
||||
|
||||
const findSLOParamsSchema = t.partial({
|
||||
query: t.partial({
|
||||
filters: t.string,
|
||||
kqlQuery: t.string,
|
||||
// Used for page pagination
|
||||
page: t.string,
|
||||
perPage: t.string,
|
||||
sortBy: sortBySchema,
|
||||
sortDirection: sortDirectionSchema,
|
||||
hideStale: toBooleanRt,
|
||||
// Used for cursor pagination, searchAfter is a JSON array
|
||||
searchAfter: searchAfterSchema,
|
||||
size: t.string,
|
||||
}),
|
||||
});
|
||||
|
||||
const findSLOResponseSchema = t.type({
|
||||
page: t.number,
|
||||
perPage: t.number,
|
||||
total: t.number,
|
||||
results: t.array(sloWithDataResponseSchema),
|
||||
});
|
||||
const findSLOResponseSchema = t.intersection([
|
||||
t.type({
|
||||
page: t.number,
|
||||
perPage: t.number,
|
||||
total: t.number,
|
||||
results: t.array(sloWithDataResponseSchema),
|
||||
}),
|
||||
t.partial({ searchAfter: searchAfterArraySchema, size: t.number }),
|
||||
]);
|
||||
|
||||
type FindSLOParams = t.TypeOf<typeof findSLOParamsSchema.props.query>;
|
||||
type FindSLOResponse = t.OutputOf<typeof findSLOResponseSchema>;
|
||||
|
|
|
@ -57,7 +57,7 @@ import { ManageSLO } from '../../services/manage_slo';
|
|||
import { ResetSLO } from '../../services/reset_slo';
|
||||
import { SloDefinitionClient } from '../../services/slo_definition_client';
|
||||
import { getSloSettings, storeSloSettings } from '../../services/slo_settings';
|
||||
import { DefaultSummarySearchClient } from '../../services/summary_search_client';
|
||||
import { DefaultSummarySearchClient } from '../../services/summary_search_client/summary_search_client';
|
||||
import { DefaultSummaryTransformGenerator } from '../../services/summary_transform_generator/summary_transform_generator';
|
||||
import { createTransformGenerators } from '../../services/transform_generators';
|
||||
import { createSloServerRoute } from '../create_slo_server_route';
|
||||
|
|
|
@ -12,7 +12,7 @@ import { FindSLO } from './find_slo';
|
|||
import { createSLO } from './fixtures/slo';
|
||||
import { createSLORepositoryMock, createSummarySearchClientMock } from './mocks';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { SummaryResult, SummarySearchClient } from './summary_search_client';
|
||||
import type { SummaryResult, SummarySearchClient } from './summary_search_client/types';
|
||||
|
||||
describe('FindSLO', () => {
|
||||
let mockRepository: jest.Mocked<SLORepository>;
|
||||
|
@ -151,16 +151,27 @@ describe('FindSLO', () => {
|
|||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it("throws an error when 'perPage > 5000'", async () => {
|
||||
beforeEach(() => {
|
||||
const slo = createSLO();
|
||||
mockSummarySearchClient.search.mockResolvedValueOnce(summarySearchResult(slo));
|
||||
mockRepository.findAllByIds.mockResolvedValueOnce([slo]);
|
||||
});
|
||||
|
||||
it("throws an error when 'perPage' > 5000", async () => {
|
||||
await expect(findSLO.execute({ perPage: '5000' })).resolves.not.toThrow();
|
||||
await expect(findSLO.execute({ perPage: '5001' })).rejects.toThrowError(
|
||||
'perPage limit set to 5000'
|
||||
);
|
||||
});
|
||||
|
||||
describe('Cursor Pagination', () => {
|
||||
it("throws an error when 'size' > 5000", async () => {
|
||||
await expect(findSLO.execute({ size: '5000' })).resolves.not.toThrow();
|
||||
await expect(findSLO.execute({ size: '5001' })).rejects.toThrowError(
|
||||
'size limit set to 5000'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -5,16 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { FindSLOParams, FindSLOResponse, findSLOResponseSchema, Pagination } from '@kbn/slo-schema';
|
||||
import { FindSLOParams, FindSLOResponse, findSLOResponseSchema } from '@kbn/slo-schema';
|
||||
import { keyBy } from 'lodash';
|
||||
import { SLODefinition } from '../domain/models';
|
||||
import { IllegalArgumentError } from '../errors';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { Sort, SummaryResult, SummarySearchClient } from './summary_search_client';
|
||||
import type {
|
||||
Pagination,
|
||||
Sort,
|
||||
SummaryResult,
|
||||
SummarySearchClient,
|
||||
} from './summary_search_client/types';
|
||||
|
||||
const DEFAULT_PAGE = 1;
|
||||
const DEFAULT_PER_PAGE = 25;
|
||||
const MAX_PER_PAGE = 5000;
|
||||
const DEFAULT_SIZE = 100;
|
||||
const MAX_PER_PAGE_OR_SIZE = 5000;
|
||||
|
||||
export class FindSLO {
|
||||
constructor(
|
||||
|
@ -38,8 +44,10 @@ export class FindSLO {
|
|||
);
|
||||
|
||||
return findSLOResponseSchema.encode({
|
||||
page: summaryResults.page,
|
||||
perPage: summaryResults.perPage,
|
||||
page: 'page' in summaryResults ? summaryResults.page : DEFAULT_PAGE,
|
||||
perPage: 'perPage' in summaryResults ? summaryResults.perPage : DEFAULT_PER_PAGE,
|
||||
size: 'size' in summaryResults ? summaryResults.size : undefined,
|
||||
searchAfter: 'searchAfter' in summaryResults ? summaryResults.searchAfter : undefined,
|
||||
total: summaryResults.total,
|
||||
results: mergeSloWithSummary(localSloDefinitions, summaryResults.results),
|
||||
});
|
||||
|
@ -78,16 +86,29 @@ function mergeSloWithSummary(
|
|||
}
|
||||
|
||||
function toPagination(params: FindSLOParams): Pagination {
|
||||
const isCursorBased = !!params.searchAfter || !!params.size;
|
||||
|
||||
if (isCursorBased) {
|
||||
const size = Number(params.size);
|
||||
if (!isNaN(size) && size > MAX_PER_PAGE_OR_SIZE) {
|
||||
throw new IllegalArgumentError('size limit set to 5000');
|
||||
}
|
||||
|
||||
return {
|
||||
searchAfter: params.searchAfter,
|
||||
size: !isNaN(size) && size > 0 ? size : DEFAULT_SIZE,
|
||||
};
|
||||
}
|
||||
|
||||
const page = Number(params.page);
|
||||
const perPage = Number(params.perPage);
|
||||
|
||||
if (!isNaN(perPage) && perPage > MAX_PER_PAGE) {
|
||||
throw new IllegalArgumentError(`perPage limit set to ${MAX_PER_PAGE}`);
|
||||
if (!isNaN(perPage) && perPage > MAX_PER_PAGE_OR_SIZE) {
|
||||
throw new IllegalArgumentError('perPage limit set to 5000');
|
||||
}
|
||||
|
||||
return {
|
||||
page: !isNaN(page) && page >= 1 ? page : DEFAULT_PAGE,
|
||||
perPage: !isNaN(perPage) && perPage >= 0 ? perPage : DEFAULT_PER_PAGE,
|
||||
perPage: !isNaN(perPage) && perPage > 0 ? perPage : DEFAULT_PER_PAGE,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -25,7 +25,7 @@ export const aSummaryDocument = (
|
|||
|
||||
export const aHitFromSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2',
|
||||
_index: '.slo-observability.summary-v3.3',
|
||||
_id: uuidv4(),
|
||||
_score: 1,
|
||||
_source,
|
||||
|
@ -34,7 +34,7 @@ export const aHitFromSummaryIndex = (_source: any) => {
|
|||
|
||||
export const aHitFromTempSummaryIndex = (_source: any) => {
|
||||
return {
|
||||
_index: '.slo-observability.summary-v2.temp',
|
||||
_index: '.slo-observability.summary-v3.3.temp',
|
||||
_id: uuidv4(),
|
||||
_score: 1,
|
||||
_source,
|
||||
|
|
|
@ -22,3 +22,4 @@ export * from './summary_client';
|
|||
export * from './get_slo_groupings';
|
||||
export * from './find_slo_groups';
|
||||
export * from './get_slo_health';
|
||||
export * from './summary_search_client/summary_search_client';
|
||||
|
|
|
@ -9,7 +9,7 @@ import { ResourceInstaller } from '../resource_installer';
|
|||
import { BurnRatesClient } from '../burn_rates_client';
|
||||
import { SLORepository } from '../slo_repository';
|
||||
import { SummaryClient } from '../summary_client';
|
||||
import { SummarySearchClient } from '../summary_search_client';
|
||||
import { SummarySearchClient } from '../summary_search_client/types';
|
||||
import { TransformManager } from '../transform_manager';
|
||||
|
||||
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
||||
|
|
|
@ -8,13 +8,14 @@
|
|||
import { ElasticsearchClientMock, elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { loggerMock } from '@kbn/logging-mocks';
|
||||
import { Pagination } from '@kbn/slo-schema/src/models/pagination';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { createSLO } from '../fixtures/slo';
|
||||
import {
|
||||
aHitFromSummaryIndex,
|
||||
aHitFromTempSummaryIndex,
|
||||
aSummaryDocument,
|
||||
} from './fixtures/summary_search_document';
|
||||
import { DefaultSummarySearchClient, Sort, SummarySearchClient } from './summary_search_client';
|
||||
} from '../fixtures/summary_search_document';
|
||||
import { DefaultSummarySearchClient } from './summary_search_client';
|
||||
import type { Sort, SummarySearchClient } from './types';
|
||||
|
||||
const defaultSort: Sort = {
|
||||
field: 'sli_value',
|
||||
|
@ -169,6 +170,12 @@ describe('Summary Search Client', () => {
|
|||
sliValue: {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.id': {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
track_total_hits: true,
|
||||
},
|
||||
|
@ -202,6 +209,12 @@ describe('Summary Search Client', () => {
|
|||
sliValue: {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.id': {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
track_total_hits: true,
|
||||
},
|
||||
|
@ -229,7 +242,16 @@ describe('Summary Search Client', () => {
|
|||
},
|
||||
},
|
||||
size: 40,
|
||||
sort: { isTempDoc: { order: 'asc' }, sliValue: { order: 'asc' } },
|
||||
sort: {
|
||||
isTempDoc: { order: 'asc' },
|
||||
sliValue: { order: 'asc' },
|
||||
'slo.id': {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
track_total_hits: true,
|
||||
},
|
||||
]);
|
|
@ -5,58 +5,30 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { SearchTotalHits } from '@elastic/elasticsearch/lib/api/types';
|
||||
import type * as estypes from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { ElasticsearchClient, Logger, SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { isCCSRemoteIndexName } from '@kbn/es-query';
|
||||
import { ALL_VALUE, Paginated, Pagination } from '@kbn/slo-schema';
|
||||
import { ALL_VALUE } from '@kbn/slo-schema';
|
||||
import { assertNever } from '@kbn/std';
|
||||
import { partition } from 'lodash';
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../common/constants';
|
||||
import { Groupings, SLODefinition, SLOId, StoredSLOSettings, Summary } from '../domain/models';
|
||||
import { toHighPrecision } from '../utils/number';
|
||||
import { createEsParams, typedSearch } from '../utils/queries';
|
||||
import { getListOfSummaryIndices, getSloSettings } from './slo_settings';
|
||||
import { EsSummaryDocument } from './summary_transform_generator/helpers/create_temp_summary';
|
||||
import { getElasticsearchQueryOrThrow, parseStringFilters } from './transform_generators';
|
||||
import { fromRemoteSummaryDocumentToSloDefinition } from './unsafe_federated/remote_summary_doc_to_slo';
|
||||
import { getFlattenedGroupings } from './utils';
|
||||
|
||||
export interface SummaryResult {
|
||||
sloId: SLOId;
|
||||
instanceId: string;
|
||||
summary: Summary;
|
||||
groupings: Groupings;
|
||||
remote?: {
|
||||
kibanaUrl: string;
|
||||
remoteName: string;
|
||||
slo: SLODefinition;
|
||||
};
|
||||
}
|
||||
|
||||
type SortField =
|
||||
| 'error_budget_consumed'
|
||||
| 'error_budget_remaining'
|
||||
| 'sli_value'
|
||||
| 'status'
|
||||
| 'burn_rate_5m'
|
||||
| 'burn_rate_1h'
|
||||
| 'burn_rate_1d';
|
||||
|
||||
export interface Sort {
|
||||
field: SortField;
|
||||
direction: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export interface SummarySearchClient {
|
||||
search(
|
||||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination,
|
||||
hideStale?: boolean
|
||||
): Promise<Paginated<SummaryResult>>;
|
||||
}
|
||||
import { SLO_SUMMARY_DESTINATION_INDEX_PATTERN } from '../../../common/constants';
|
||||
import { StoredSLOSettings } from '../../domain/models';
|
||||
import { toHighPrecision } from '../../utils/number';
|
||||
import { createEsParams, typedSearch } from '../../utils/queries';
|
||||
import { getListOfSummaryIndices, getSloSettings } from '../slo_settings';
|
||||
import { EsSummaryDocument } from '../summary_transform_generator/helpers/create_temp_summary';
|
||||
import { getElasticsearchQueryOrThrow, parseStringFilters } from '../transform_generators';
|
||||
import { fromRemoteSummaryDocumentToSloDefinition } from '../unsafe_federated/remote_summary_doc_to_slo';
|
||||
import { getFlattenedGroupings } from '../utils';
|
||||
import type {
|
||||
Paginated,
|
||||
Pagination,
|
||||
Sort,
|
||||
SortField,
|
||||
SummaryResult,
|
||||
SummarySearchClient,
|
||||
} from './types';
|
||||
import { isCursorPagination } from './types';
|
||||
|
||||
export class DefaultSummarySearchClient implements SummarySearchClient {
|
||||
constructor(
|
||||
|
@ -76,6 +48,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
const parsedFilters = parseStringFilters(filters, this.logger);
|
||||
const settings = await getSloSettings(this.soClient);
|
||||
const { indices } = await getListOfSummaryIndices(this.esClient, settings);
|
||||
|
||||
const esParams = createEsParams({
|
||||
index: indices,
|
||||
track_total_hits: true,
|
||||
|
@ -98,9 +71,14 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
[toDocumentSortField(sort.field)]: {
|
||||
order: sort.direction,
|
||||
},
|
||||
'slo.id': {
|
||||
order: 'asc',
|
||||
},
|
||||
'slo.instanceId': {
|
||||
order: 'asc',
|
||||
},
|
||||
},
|
||||
from: (pagination.page - 1) * pagination.perPage,
|
||||
size: pagination.perPage * 2, // twice as much as we return, in case they are all duplicate temp/non-temp summary
|
||||
...toPaginationQuery(pagination),
|
||||
});
|
||||
|
||||
try {
|
||||
|
@ -109,9 +87,9 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
esParams
|
||||
);
|
||||
|
||||
const total = (summarySearch.hits.total as SearchTotalHits).value ?? 0;
|
||||
const total = summarySearch.hits.total.value ?? 0;
|
||||
if (total === 0) {
|
||||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
return { total: 0, ...pagination, results: [] };
|
||||
}
|
||||
|
||||
const [tempSummaryDocuments, summaryDocuments] = partition(
|
||||
|
@ -129,12 +107,16 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
|
||||
const finalResults = summaryDocuments
|
||||
.concat(tempSummaryDocumentsDeduped)
|
||||
.slice(0, pagination.perPage);
|
||||
.slice(0, isCursorPagination(pagination) ? pagination.size : pagination.perPage);
|
||||
|
||||
const finalTotal = total - (tempSummaryDocuments.length - tempSummaryDocumentsDeduped.length);
|
||||
|
||||
const paginationResults = isCursorPagination(pagination)
|
||||
? { searchAfter: finalResults[finalResults.length - 1].sort, size: pagination.size }
|
||||
: pagination;
|
||||
|
||||
return {
|
||||
...pagination,
|
||||
...paginationResults,
|
||||
total: finalTotal,
|
||||
results: finalResults.map((doc) => {
|
||||
const summaryDoc = doc._source;
|
||||
|
@ -179,7 +161,7 @@ export class DefaultSummarySearchClient implements SummarySearchClient {
|
|||
};
|
||||
} catch (err) {
|
||||
this.logger.error(`Error while searching SLO summary documents. ${err}`);
|
||||
return { total: 0, perPage: pagination.perPage, page: pagination.page, results: [] };
|
||||
return { total: 0, ...pagination, results: [] };
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -251,3 +233,19 @@ function toDocumentSortField(field: SortField) {
|
|||
assertNever(field);
|
||||
}
|
||||
}
|
||||
|
||||
function toPaginationQuery(
|
||||
pagination: Pagination
|
||||
): { size: number; search_after?: Array<string | number> } | { size: number; from: number } {
|
||||
if (isCursorPagination(pagination)) {
|
||||
return {
|
||||
size: pagination.size * 2, // Potential duplicates between temp and non-temp summaries
|
||||
search_after: pagination.searchAfter,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
size: pagination.perPage * 2, // Potential duplicates between temp and non-temp summaries
|
||||
from: (pagination.page - 1) * pagination.perPage,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { Groupings, SLODefinition, SLOId, Summary } from '../../domain/models';
|
||||
|
||||
interface SummaryResult {
|
||||
sloId: SLOId;
|
||||
instanceId: string;
|
||||
summary: Summary;
|
||||
groupings: Groupings;
|
||||
remote?: {
|
||||
kibanaUrl: string;
|
||||
remoteName: string;
|
||||
slo: SLODefinition;
|
||||
};
|
||||
}
|
||||
|
||||
type SortField =
|
||||
| 'error_budget_consumed'
|
||||
| 'error_budget_remaining'
|
||||
| 'sli_value'
|
||||
| 'status'
|
||||
| 'burn_rate_5m'
|
||||
| 'burn_rate_1h'
|
||||
| 'burn_rate_1d';
|
||||
|
||||
interface Sort {
|
||||
field: SortField;
|
||||
direction: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
type Pagination = CursorPagination | OffsetPagination;
|
||||
|
||||
interface CursorPagination {
|
||||
searchAfter?: Array<string | number>;
|
||||
size: number;
|
||||
}
|
||||
|
||||
function isCursorPagination(pagination: Pagination): pagination is CursorPagination {
|
||||
return (pagination as CursorPagination).size !== undefined;
|
||||
}
|
||||
|
||||
interface OffsetPagination {
|
||||
page: number;
|
||||
perPage: number;
|
||||
}
|
||||
|
||||
type Paginated<T> = CursorPaginated<T> | OffsetPaginated<T>;
|
||||
|
||||
interface CursorPaginated<T> {
|
||||
total: number;
|
||||
searchAfter?: Array<string | number>;
|
||||
size: number;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
interface OffsetPaginated<T> {
|
||||
total: number;
|
||||
page: number;
|
||||
perPage: number;
|
||||
results: T[];
|
||||
}
|
||||
|
||||
interface SummarySearchClient {
|
||||
search(
|
||||
kqlQuery: string,
|
||||
filters: string,
|
||||
sort: Sort,
|
||||
pagination: Pagination,
|
||||
hideStale?: boolean
|
||||
): Promise<Paginated<SummaryResult>>;
|
||||
}
|
||||
|
||||
export type {
|
||||
SummaryResult,
|
||||
SortField,
|
||||
Sort,
|
||||
Pagination,
|
||||
CursorPagination,
|
||||
OffsetPagination as PagePagination,
|
||||
Paginated,
|
||||
CursorPaginated,
|
||||
OffsetPaginated as PagePaginated,
|
||||
SummarySearchClient,
|
||||
};
|
||||
export { isCursorPagination };
|
|
@ -40,9 +40,8 @@ export function parseStringFilters(filters: string, logger: Logger) {
|
|||
return JSON.parse(filters);
|
||||
} catch (e) {
|
||||
logger.info(`Failed to parse filters: ${e}`);
|
||||
return {};
|
||||
}
|
||||
|
||||
return {};
|
||||
}
|
||||
|
||||
export function parseIndex(index: string): string | string[] {
|
||||
|
|
|
@ -65,24 +65,34 @@ export default function ({ getService }: DeploymentAgnosticFtrProviderContext) {
|
|||
await retry.tryForTime(180 * 1000, async () => {
|
||||
let response = await supertestWithoutAuth
|
||||
.get(`/api/observability/slos`)
|
||||
.query({ page: 1, perPage: 333 })
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders)
|
||||
.send();
|
||||
|
||||
expect(response.body.results).length(2);
|
||||
expect(response.body.page).eql(1);
|
||||
expect(response.body.perPage).eql(333);
|
||||
expect(response.body.total).eql(2);
|
||||
|
||||
response = await supertestWithoutAuth
|
||||
.get(`/api/observability/slos?kqlQuery=slo.name%3Airrelevant`)
|
||||
.get(`/api/observability/slos`)
|
||||
.query({ size: 222, kqlQuery: 'slo.name:irrelevant' })
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders)
|
||||
.send()
|
||||
.expect(200);
|
||||
|
||||
expect(response.body.page).eql(1); // always return page with default value
|
||||
expect(response.body.perPage).eql(25); // always return perPage with default value
|
||||
expect(response.body.size).eql(222);
|
||||
expect(response.body.searchAfter).ok();
|
||||
expect(response.body.results).length(1);
|
||||
expect(response.body.results[0].id).eql(sloId2);
|
||||
|
||||
response = await supertestWithoutAuth
|
||||
.get(`/api/observability/slos?kqlQuery=slo.name%3Aintegration`)
|
||||
.get(`/api/observability/slos`)
|
||||
.query({ kqlQuery: 'slo.name:integration' })
|
||||
.set(adminRoleAuthc.apiKeyHeader)
|
||||
.set(internalHeaders)
|
||||
.send()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue