mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
feat(slo): Delete SLO (#140760)
This commit is contained in:
parent
0f7cfd16f7
commit
d29521e897
23 changed files with 605 additions and 244 deletions
|
@ -13,3 +13,5 @@ export const SLO_RESOURCES_VERSION = 1;
|
|||
|
||||
export const getSLODestinationIndexName = (spaceId: string) =>
|
||||
`${SLO_INDEX_TEMPLATE_NAME}-v${SLO_RESOURCES_VERSION}-${spaceId}`;
|
||||
|
||||
export const getSLOTransformId = (sloId: string) => `slo-${sloId}`;
|
||||
|
|
17
x-pack/plugins/observability/server/errors/errors.ts
Normal file
17
x-pack/plugins/observability/server/errors/errors.ts
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
export class ObservabilityError extends Error {
|
||||
constructor(message?: string) {
|
||||
super(message);
|
||||
this.name = this.constructor.name;
|
||||
}
|
||||
}
|
||||
|
||||
export class SLONotFound extends ObservabilityError {}
|
16
x-pack/plugins/observability/server/errors/handler.ts
Normal file
16
x-pack/plugins/observability/server/errors/handler.ts
Normal file
|
@ -0,0 +1,16 @@
|
|||
/*
|
||||
* 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 { ObservabilityError, SLONotFound } from './errors';
|
||||
|
||||
export function getHTTPResponseCode(error: ObservabilityError): number {
|
||||
if (error instanceof SLONotFound) {
|
||||
return 404;
|
||||
}
|
||||
|
||||
return 400;
|
||||
}
|
9
x-pack/plugins/observability/server/errors/index.ts
Normal file
9
x-pack/plugins/observability/server/errors/index.ts
Normal file
|
@ -0,0 +1,9 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export * from './errors';
|
||||
export * from './handler';
|
|
@ -17,6 +17,7 @@ import { RuleDataPluginService } from '@kbn/rule-registry-plugin/server';
|
|||
import { SpacesServiceStart } from '@kbn/spaces-plugin/server';
|
||||
import { ObservabilityRequestHandlerContext } from '../types';
|
||||
import { AbstractObservabilityServerRouteRepository } from './types';
|
||||
import { getHTTPResponseCode, ObservabilityError } from '../errors';
|
||||
|
||||
export function registerRoutes({
|
||||
repository,
|
||||
|
@ -71,6 +72,24 @@ export function registerRoutes({
|
|||
|
||||
return response.ok({ body: data });
|
||||
} catch (error) {
|
||||
if (error instanceof ObservabilityError) {
|
||||
logger.error(error.message);
|
||||
return response.customError({
|
||||
statusCode: getHTTPResponseCode(error),
|
||||
body: {
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
logger.error(error.output.payload.message);
|
||||
return response.customError({
|
||||
statusCode: error.output.statusCode,
|
||||
body: { message: error.output.payload.message },
|
||||
});
|
||||
}
|
||||
|
||||
logger.error(error);
|
||||
const opts = {
|
||||
statusCode: 500,
|
||||
|
@ -79,16 +98,12 @@ export function registerRoutes({
|
|||
},
|
||||
};
|
||||
|
||||
if (Boom.isBoom(error)) {
|
||||
opts.statusCode = error.output.statusCode;
|
||||
}
|
||||
|
||||
if (error instanceof errors.RequestAbortedError) {
|
||||
opts.statusCode = 499;
|
||||
opts.body.message = 'Client closed request';
|
||||
}
|
||||
|
||||
return response.custom(opts);
|
||||
return response.customError(opts);
|
||||
}
|
||||
}
|
||||
);
|
||||
|
|
|
@ -7,8 +7,9 @@
|
|||
|
||||
import {
|
||||
CreateSLO,
|
||||
DeleteSLO,
|
||||
DefaultResourceInstaller,
|
||||
DefaultTransformInstaller,
|
||||
DefaultTransformManager,
|
||||
KibanaSavedObjectsSLORepository,
|
||||
} from '../../services/slo';
|
||||
import {
|
||||
|
@ -17,7 +18,7 @@ import {
|
|||
TransformGenerator,
|
||||
} from '../../services/slo/transform_generators';
|
||||
import { SLITypes } from '../../types/models';
|
||||
import { createSLOParamsSchema } from '../../types/schema';
|
||||
import { createSLOParamsSchema, deleteSLOParamsSchema } from '../../types/schema';
|
||||
import { createObservabilityServerRoute } from '../create_observability_server_route';
|
||||
|
||||
const transformGenerators: Record<SLITypes, TransformGenerator> = {
|
||||
|
@ -36,10 +37,15 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
const soClient = (await context.core).savedObjects.client;
|
||||
const spaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const resourceInstaller = new DefaultResourceInstaller(esClient, logger);
|
||||
const resourceInstaller = new DefaultResourceInstaller(esClient, logger, spaceId);
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
const transformInstaller = new DefaultTransformInstaller(transformGenerators, esClient, logger);
|
||||
const createSLO = new CreateSLO(resourceInstaller, repository, transformInstaller, spaceId);
|
||||
const transformManager = new DefaultTransformManager(
|
||||
transformGenerators,
|
||||
esClient,
|
||||
logger,
|
||||
spaceId
|
||||
);
|
||||
const createSLO = new CreateSLO(resourceInstaller, repository, transformManager);
|
||||
|
||||
const response = await createSLO.execute(params.body);
|
||||
|
||||
|
@ -47,4 +53,29 @@ const createSLORoute = createObservabilityServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
export const slosRouteRepository = createSLORoute;
|
||||
const deleteSLORoute = createObservabilityServerRoute({
|
||||
endpoint: 'DELETE /api/observability/slos/{id}',
|
||||
options: {
|
||||
tags: [],
|
||||
},
|
||||
params: deleteSLOParamsSchema,
|
||||
handler: async ({ context, request, params, logger, spacesService }) => {
|
||||
const esClient = (await context.core).elasticsearch.client.asCurrentUser;
|
||||
const soClient = (await context.core).savedObjects.client;
|
||||
const spaceId = spacesService.getSpaceId(request);
|
||||
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClient);
|
||||
const transformManager = new DefaultTransformManager(
|
||||
transformGenerators,
|
||||
esClient,
|
||||
logger,
|
||||
spaceId
|
||||
);
|
||||
|
||||
const deleteSLO = new DeleteSLO(repository, transformManager, esClient);
|
||||
|
||||
await deleteSLO.execute(params.path.id);
|
||||
},
|
||||
});
|
||||
|
||||
export const slosRouteRepository = { ...createSLORoute, ...deleteSLORoute };
|
||||
|
|
|
@ -10,57 +10,60 @@ import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtu
|
|||
import {
|
||||
createResourceInstallerMock,
|
||||
createSLORepositoryMock,
|
||||
createTransformInstallerMock,
|
||||
createTransformManagerMock,
|
||||
} from './mocks';
|
||||
import { ResourceInstaller } from './resource_installer';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformInstaller } from './transform_installer';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
const SPACE_ID = 'some-space-id';
|
||||
|
||||
describe('createSLO', () => {
|
||||
describe('CreateSLO', () => {
|
||||
let mockResourceInstaller: jest.Mocked<ResourceInstaller>;
|
||||
let mockRepository: jest.Mocked<SLORepository>;
|
||||
let mockTransformInstaller: jest.Mocked<TransformInstaller>;
|
||||
let mockTransformManager: jest.Mocked<TransformManager>;
|
||||
let createSLO: CreateSLO;
|
||||
|
||||
beforeEach(() => {
|
||||
mockResourceInstaller = createResourceInstallerMock();
|
||||
mockRepository = createSLORepositoryMock();
|
||||
mockTransformInstaller = createTransformInstallerMock();
|
||||
createSLO = new CreateSLO(
|
||||
mockResourceInstaller,
|
||||
mockRepository,
|
||||
mockTransformInstaller,
|
||||
SPACE_ID
|
||||
);
|
||||
mockTransformManager = createTransformManagerMock();
|
||||
createSLO = new CreateSLO(mockResourceInstaller, mockRepository, mockTransformManager);
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('calls the expected services', async () => {
|
||||
const sloParams = createSLOParams(createAPMTransactionErrorRateIndicator());
|
||||
mockTransformManager.install.mockResolvedValue('slo-transform-id');
|
||||
|
||||
const response = await createSLO.execute(sloParams);
|
||||
|
||||
expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledWith(SPACE_ID);
|
||||
expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalled();
|
||||
expect(mockRepository.save).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...sloParams, id: expect.any(String) })
|
||||
);
|
||||
expect(mockTransformInstaller.installAndStartTransform).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...sloParams, id: expect.any(String) }),
|
||||
SPACE_ID
|
||||
expect(mockTransformManager.install).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ ...sloParams, id: expect.any(String) })
|
||||
);
|
||||
expect(mockTransformManager.start).toHaveBeenCalledWith('slo-transform-id');
|
||||
expect(response).toEqual(expect.objectContaining({ id: expect.any(String) }));
|
||||
});
|
||||
});
|
||||
|
||||
describe('unhappy path', () => {
|
||||
it('deletes the SLO saved objects when transform installation fails', async () => {
|
||||
mockTransformInstaller.installAndStartTransform.mockRejectedValue(
|
||||
new Error('Transform Error')
|
||||
);
|
||||
it('deletes the SLO when transform installation fails', async () => {
|
||||
mockTransformManager.install.mockRejectedValue(new Error('Transform install error'));
|
||||
const sloParams = createSLOParams(createAPMTransactionErrorRateIndicator());
|
||||
|
||||
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform Error');
|
||||
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform install error');
|
||||
expect(mockRepository.deleteById).toBeCalled();
|
||||
});
|
||||
|
||||
it('removes the transform and deletes the SLO when transform start fails', async () => {
|
||||
mockTransformManager.install.mockResolvedValue('slo-transform-id');
|
||||
mockTransformManager.start.mockRejectedValue(new Error('Transform start error'));
|
||||
const sloParams = createSLOParams(createAPMTransactionErrorRateIndicator());
|
||||
|
||||
await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform start error');
|
||||
expect(mockTransformManager.uninstall).toBeCalledWith('slo-transform-id');
|
||||
expect(mockRepository.deleteById).toBeCalled();
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,31 +10,41 @@ import uuid from 'uuid';
|
|||
import { SLO } from '../../types/models';
|
||||
import { ResourceInstaller } from './resource_installer';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformInstaller } from './transform_installer';
|
||||
|
||||
import { TransformManager } from './transform_manager';
|
||||
import { CreateSLOParams, CreateSLOResponse } from '../../types/schema';
|
||||
|
||||
export class CreateSLO {
|
||||
constructor(
|
||||
private resourceInstaller: ResourceInstaller,
|
||||
private repository: SLORepository,
|
||||
private transformInstaller: TransformInstaller,
|
||||
private spaceId: string
|
||||
private transformManager: TransformManager
|
||||
) {}
|
||||
|
||||
public async execute(sloParams: CreateSLOParams): Promise<CreateSLOResponse> {
|
||||
const slo = this.toSLO(sloParams);
|
||||
|
||||
await this.resourceInstaller.ensureCommonResourcesInstalled(this.spaceId);
|
||||
await this.resourceInstaller.ensureCommonResourcesInstalled();
|
||||
await this.repository.save(slo);
|
||||
|
||||
let sloTransformId;
|
||||
try {
|
||||
await this.transformInstaller.installAndStartTransform(slo, this.spaceId);
|
||||
sloTransformId = await this.transformManager.install(slo);
|
||||
} catch (err) {
|
||||
await this.repository.deleteById(slo.id);
|
||||
throw err;
|
||||
}
|
||||
|
||||
try {
|
||||
await this.transformManager.start(sloTransformId);
|
||||
} catch (err) {
|
||||
await Promise.all([
|
||||
this.transformManager.uninstall(sloTransformId),
|
||||
this.repository.deleteById(slo.id),
|
||||
]);
|
||||
|
||||
throw err;
|
||||
}
|
||||
|
||||
return this.toResponse(slo);
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,51 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { elasticsearchServiceMock } from '@kbn/core/server/mocks';
|
||||
import { getSLOTransformId } from '../../assets/constants';
|
||||
import { DeleteSLO } from './delete_slo';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
|
||||
import { createSLORepositoryMock, createTransformManagerMock } from './mocks';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
describe('DeleteSLO', () => {
|
||||
let mockRepository: jest.Mocked<SLORepository>;
|
||||
let mockTransformManager: jest.Mocked<TransformManager>;
|
||||
let mockEsClient: jest.Mocked<ElasticsearchClient>;
|
||||
let deleteSLO: DeleteSLO;
|
||||
|
||||
beforeEach(() => {
|
||||
mockRepository = createSLORepositoryMock();
|
||||
mockTransformManager = createTransformManagerMock();
|
||||
mockEsClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
deleteSLO = new DeleteSLO(mockRepository, mockTransformManager, mockEsClient);
|
||||
});
|
||||
|
||||
describe('happy path', () => {
|
||||
it('removes the transform, the roll up data and the SLO from the repository', async () => {
|
||||
const slo = createSLO(createAPMTransactionErrorRateIndicator());
|
||||
mockRepository.findById.mockResolvedValueOnce(slo);
|
||||
|
||||
await deleteSLO.execute(slo.id);
|
||||
|
||||
expect(mockTransformManager.stop).toHaveBeenCalledWith(getSLOTransformId(slo.id));
|
||||
expect(mockTransformManager.uninstall).toHaveBeenCalledWith(getSLOTransformId(slo.id));
|
||||
expect(mockEsClient.deleteByQuery).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
query: {
|
||||
match: {
|
||||
'slo.id': slo.id,
|
||||
},
|
||||
},
|
||||
})
|
||||
);
|
||||
expect(mockRepository.deleteById).toHaveBeenCalledWith(slo.id);
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { getSLOTransformId, SLO_INDEX_TEMPLATE_NAME } from '../../assets/constants';
|
||||
import { SLO } from '../../types/models';
|
||||
import { SLORepository } from './slo_repository';
|
||||
import { TransformManager } from './transform_manager';
|
||||
|
||||
export class DeleteSLO {
|
||||
constructor(
|
||||
private repository: SLORepository,
|
||||
private transformManager: TransformManager,
|
||||
private esClient: ElasticsearchClient
|
||||
) {}
|
||||
|
||||
public async execute(sloId: string): Promise<void> {
|
||||
const slo = await this.repository.findById(sloId);
|
||||
|
||||
const sloTransformId = getSLOTransformId(sloId);
|
||||
await this.transformManager.stop(sloTransformId);
|
||||
await this.transformManager.uninstall(sloTransformId);
|
||||
|
||||
await this.deleteRollupData(slo);
|
||||
await this.repository.deleteById(sloId);
|
||||
}
|
||||
|
||||
private async deleteRollupData(slo: SLO): Promise<void> {
|
||||
await this.esClient.deleteByQuery({
|
||||
index: slo.settings.destination_index ?? `${SLO_INDEX_TEMPLATE_NAME}*`,
|
||||
wait_for_completion: false,
|
||||
query: {
|
||||
match: {
|
||||
'slo.id': slo.id,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
|
@ -7,5 +7,6 @@
|
|||
|
||||
export * from './resource_installer';
|
||||
export * from './slo_repository';
|
||||
export * from './transform_installer';
|
||||
export * from './transform_manager';
|
||||
export * from './create_slo';
|
||||
export * from './delete_slo';
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { ResourceInstaller } from '../resource_installer';
|
||||
import { SLORepository } from '../slo_repository';
|
||||
import { TransformInstaller } from '../transform_installer';
|
||||
import { TransformManager } from '../transform_manager';
|
||||
|
||||
const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
||||
return {
|
||||
|
@ -15,9 +15,12 @@ const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
|
|||
};
|
||||
};
|
||||
|
||||
const createTransformInstallerMock = (): jest.Mocked<TransformInstaller> => {
|
||||
const createTransformManagerMock = (): jest.Mocked<TransformManager> => {
|
||||
return {
|
||||
installAndStartTransform: jest.fn(),
|
||||
install: jest.fn(),
|
||||
uninstall: jest.fn(),
|
||||
start: jest.fn(),
|
||||
stop: jest.fn(),
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -29,4 +32,4 @@ const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
|
|||
};
|
||||
};
|
||||
|
||||
export { createResourceInstallerMock, createTransformInstallerMock, createSLORepositoryMock };
|
||||
export { createResourceInstallerMock, createTransformManagerMock, createSLORepositoryMock };
|
||||
|
|
|
@ -22,7 +22,11 @@ describe('resourceInstaller', () => {
|
|||
it('installs the common resources', async () => {
|
||||
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
|
||||
mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false);
|
||||
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());
|
||||
const installer = new DefaultResourceInstaller(
|
||||
mockClusterClient,
|
||||
loggerMock.create(),
|
||||
'space-id'
|
||||
);
|
||||
|
||||
await installer.ensureCommonResourcesInstalled();
|
||||
|
||||
|
@ -51,7 +55,11 @@ describe('resourceInstaller', () => {
|
|||
mockClusterClient.ingest.getPipeline.mockResponseOnce({
|
||||
[SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } },
|
||||
} as IngestGetPipelineResponse);
|
||||
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());
|
||||
const installer = new DefaultResourceInstaller(
|
||||
mockClusterClient,
|
||||
loggerMock.create(),
|
||||
'space-id'
|
||||
);
|
||||
|
||||
await installer.ensureCommonResourcesInstalled();
|
||||
|
||||
|
|
|
@ -25,13 +25,17 @@ import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_temp
|
|||
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';
|
||||
|
||||
export interface ResourceInstaller {
|
||||
ensureCommonResourcesInstalled(spaceId: string): Promise<void>;
|
||||
ensureCommonResourcesInstalled(): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultResourceInstaller implements ResourceInstaller {
|
||||
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}
|
||||
constructor(
|
||||
private esClient: ElasticsearchClient,
|
||||
private logger: Logger,
|
||||
private spaceId: string
|
||||
) {}
|
||||
|
||||
public async ensureCommonResourcesInstalled(spaceId: string = 'default'): Promise<void> {
|
||||
public async ensureCommonResourcesInstalled(): Promise<void> {
|
||||
const alreadyInstalled = await this.areResourcesAlreadyInstalled();
|
||||
|
||||
if (alreadyInstalled) {
|
||||
|
@ -61,7 +65,7 @@ export class DefaultResourceInstaller implements ResourceInstaller {
|
|||
await this.createOrUpdateIngestPipelineTemplate(
|
||||
getSLOPipelineTemplate(
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
this.getPipelinePrefix(SLO_RESOURCES_VERSION, spaceId)
|
||||
this.getPipelinePrefix(SLO_RESOURCES_VERSION, this.spaceId)
|
||||
)
|
||||
);
|
||||
} catch (err) {
|
||||
|
|
|
@ -6,24 +6,16 @@
|
|||
*/
|
||||
|
||||
import { SavedObject } from '@kbn/core-saved-objects-common';
|
||||
import { SavedObjectsClientContract } from '@kbn/core/server';
|
||||
import { SavedObjectsClientContract, SavedObjectsErrorHelpers } from '@kbn/core/server';
|
||||
import { savedObjectsClientMock } from '@kbn/core/server/mocks';
|
||||
|
||||
import { SLO, StoredSLO } from '../../types/models';
|
||||
import { SO_SLO_TYPE } from '../../saved_objects';
|
||||
import { KibanaSavedObjectsSLORepository } from './slo_repository';
|
||||
import { createSLO } from './fixtures/slo';
|
||||
import { createAPMTransactionDurationIndicator, createSLO } from './fixtures/slo';
|
||||
import { SLONotFound } from '../../errors';
|
||||
|
||||
const anSLO = createSLO({
|
||||
type: 'slo.apm.transaction_duration',
|
||||
params: {
|
||||
environment: 'irrelevant',
|
||||
service: 'irrelevant',
|
||||
transaction_type: 'irrelevant',
|
||||
transaction_name: 'irrelevant',
|
||||
'threshold.us': 200000,
|
||||
},
|
||||
});
|
||||
const SOME_SLO = createSLO(createAPMTransactionDurationIndicator());
|
||||
|
||||
function aStoredSLO(slo: SLO): SavedObject<StoredSLO> {
|
||||
return {
|
||||
|
@ -45,38 +37,61 @@ describe('KibanaSavedObjectsSLORepository', () => {
|
|||
soClientMock = savedObjectsClientMock.create();
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
it('findById throws when an SLO is not found', async () => {
|
||||
soClientMock.get.mockRejectedValueOnce(SavedObjectsErrorHelpers.createGenericNotFoundError());
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
|
||||
|
||||
await expect(repository.findById('inexistant-slo-id')).rejects.toThrowError(
|
||||
new SLONotFound('SLO [inexistant-slo-id] not found')
|
||||
);
|
||||
});
|
||||
|
||||
it('deleteById throws when an SLO is not found', async () => {
|
||||
soClientMock.delete.mockRejectedValueOnce(
|
||||
SavedObjectsErrorHelpers.createGenericNotFoundError()
|
||||
);
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
|
||||
|
||||
await expect(repository.deleteById('inexistant-slo-id')).rejects.toThrowError(
|
||||
new SLONotFound('SLO [inexistant-slo-id] not found')
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('saves the SLO', async () => {
|
||||
soClientMock.create.mockResolvedValueOnce(aStoredSLO(anSLO));
|
||||
soClientMock.create.mockResolvedValueOnce(aStoredSLO(SOME_SLO));
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
|
||||
|
||||
const savedSLO = await repository.save(anSLO);
|
||||
const savedSLO = await repository.save(SOME_SLO);
|
||||
|
||||
expect(savedSLO).toEqual(anSLO);
|
||||
expect(savedSLO).toEqual(SOME_SLO);
|
||||
expect(soClientMock.create).toHaveBeenCalledWith(
|
||||
SO_SLO_TYPE,
|
||||
expect.objectContaining({
|
||||
...anSLO,
|
||||
...SOME_SLO,
|
||||
updated_at: expect.anything(),
|
||||
created_at: expect.anything(),
|
||||
})
|
||||
}),
|
||||
{ id: SOME_SLO.id }
|
||||
);
|
||||
});
|
||||
|
||||
it('finds an existing SLO', async () => {
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
|
||||
soClientMock.get.mockResolvedValueOnce(aStoredSLO(anSLO));
|
||||
soClientMock.get.mockResolvedValueOnce(aStoredSLO(SOME_SLO));
|
||||
|
||||
const foundSLO = await repository.findById(anSLO.id);
|
||||
const foundSLO = await repository.findById(SOME_SLO.id);
|
||||
|
||||
expect(foundSLO).toEqual(anSLO);
|
||||
expect(soClientMock.get).toHaveBeenCalledWith(SO_SLO_TYPE, anSLO.id);
|
||||
expect(foundSLO).toEqual(SOME_SLO);
|
||||
expect(soClientMock.get).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id);
|
||||
});
|
||||
|
||||
it('removes an SLO', async () => {
|
||||
it('deletes an SLO', async () => {
|
||||
const repository = new KibanaSavedObjectsSLORepository(soClientMock);
|
||||
|
||||
await repository.deleteById(anSLO.id);
|
||||
await repository.deleteById(SOME_SLO.id);
|
||||
|
||||
expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, anSLO.id);
|
||||
expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, SOME_SLO.id);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -6,9 +6,11 @@
|
|||
*/
|
||||
|
||||
import { SavedObjectsClientContract } from '@kbn/core-saved-objects-api-server';
|
||||
import { SavedObjectsErrorHelpers } from '@kbn/core-saved-objects-utils-server';
|
||||
|
||||
import { StoredSLO, SLO } from '../../types/models';
|
||||
import { SO_SLO_TYPE } from '../../saved_objects';
|
||||
import { SLONotFound } from '../../errors';
|
||||
|
||||
export interface SLORepository {
|
||||
save(slo: SLO): Promise<SLO>;
|
||||
|
@ -21,22 +23,40 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
|
|||
|
||||
async save(slo: SLO): Promise<SLO> {
|
||||
const now = new Date().toISOString();
|
||||
const savedSLO = await this.soClient.create<StoredSLO>(SO_SLO_TYPE, {
|
||||
...slo,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
});
|
||||
const savedSLO = await this.soClient.create<StoredSLO>(
|
||||
SO_SLO_TYPE,
|
||||
{
|
||||
...slo,
|
||||
created_at: now,
|
||||
updated_at: now,
|
||||
},
|
||||
{ id: slo.id }
|
||||
);
|
||||
|
||||
return toSLOModel(savedSLO.attributes);
|
||||
}
|
||||
|
||||
async findById(id: string): Promise<SLO> {
|
||||
const slo = await this.soClient.get<StoredSLO>(SO_SLO_TYPE, id);
|
||||
return toSLOModel(slo.attributes);
|
||||
try {
|
||||
const slo = await this.soClient.get<StoredSLO>(SO_SLO_TYPE, id);
|
||||
return toSLOModel(slo.attributes);
|
||||
} catch (err) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
|
||||
throw new SLONotFound(`SLO [${id}] not found`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async deleteById(id: string): Promise<void> {
|
||||
await this.soClient.delete(SO_SLO_TYPE, id);
|
||||
try {
|
||||
await this.soClient.delete(SO_SLO_TYPE, id);
|
||||
} catch (err) {
|
||||
if (SavedObjectsErrorHelpers.isNotFoundError(err)) {
|
||||
throw new SLONotFound(`SLO [${id}] not found`);
|
||||
}
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -10,7 +10,11 @@ import {
|
|||
MappingRuntimeFieldType,
|
||||
TransformPutTransformRequest,
|
||||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants';
|
||||
import {
|
||||
getSLODestinationIndexName,
|
||||
getSLOTransformId,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
} from '../../../assets/constants';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import {
|
||||
SLO,
|
||||
|
@ -38,7 +42,7 @@ export class ApmTransactionDurationTransformGenerator implements TransformGenera
|
|||
}
|
||||
|
||||
private buildTransformId(slo: APMTransactionDurationSLO): string {
|
||||
return `slo-${slo.id}`;
|
||||
return getSLOTransformId(slo.id);
|
||||
}
|
||||
|
||||
private buildSource(slo: APMTransactionDurationSLO) {
|
||||
|
|
|
@ -12,7 +12,11 @@ import {
|
|||
} from '@elastic/elasticsearch/lib/api/types';
|
||||
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
|
||||
import { TransformGenerator } from '.';
|
||||
import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants';
|
||||
import {
|
||||
getSLODestinationIndexName,
|
||||
getSLOTransformId,
|
||||
SLO_INGEST_PIPELINE_NAME,
|
||||
} from '../../../assets/constants';
|
||||
import {
|
||||
apmTransactionErrorRateSLOSchema,
|
||||
APMTransactionErrorRateSLO,
|
||||
|
@ -40,7 +44,7 @@ export class ApmTransactionErrorRateTransformGenerator implements TransformGener
|
|||
}
|
||||
|
||||
private buildTransformId(slo: APMTransactionErrorRateSLO): string {
|
||||
return `slo-${slo.id}`;
|
||||
return getSLOTransformId(slo.id);
|
||||
}
|
||||
|
||||
private buildSource(slo: APMTransactionErrorRateSLO) {
|
||||
|
|
|
@ -1,102 +0,0 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { DefaultTransformInstaller } from './transform_installer';
|
||||
import {
|
||||
ApmTransactionErrorRateTransformGenerator,
|
||||
TransformGenerator,
|
||||
} from './transform_generators';
|
||||
import { SLO, SLITypes } from '../../types/models';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
|
||||
|
||||
describe('TransformerGenerator', () => {
|
||||
let esClientMock: jest.Mocked<ElasticsearchClient>;
|
||||
let loggerMock: jest.Mocked<MockedLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
});
|
||||
|
||||
describe('Unhappy path', () => {
|
||||
it('throws when no generator exists for the slo indicator type', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_duration': new DummyTransformGenerator(),
|
||||
};
|
||||
const service = new DefaultTransformInstaller(generators, esClientMock, loggerMock);
|
||||
|
||||
await expect(
|
||||
service.installAndStartTransform(
|
||||
createSLO({
|
||||
type: 'slo.apm.transaction_error_rate',
|
||||
params: {
|
||||
environment: 'irrelevant',
|
||||
service: 'irrelevant',
|
||||
transaction_name: 'irrelevant',
|
||||
transaction_type: 'irrelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
).rejects.toThrowError('Unsupported SLO type: slo.apm.transaction_error_rate');
|
||||
});
|
||||
|
||||
it('throws when transform generator fails', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_duration': new FailTransformGenerator(),
|
||||
};
|
||||
const service = new DefaultTransformInstaller(generators, esClientMock, loggerMock);
|
||||
|
||||
await expect(
|
||||
service.installAndStartTransform(
|
||||
createSLO({
|
||||
type: 'slo.apm.transaction_duration',
|
||||
params: {
|
||||
environment: 'irrelevant',
|
||||
service: 'irrelevant',
|
||||
transaction_name: 'irrelevant',
|
||||
transaction_type: 'irrelevant',
|
||||
'threshold.us': 250000,
|
||||
},
|
||||
})
|
||||
)
|
||||
).rejects.toThrowError('Some error');
|
||||
});
|
||||
});
|
||||
|
||||
it('installs and starts the transform', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
};
|
||||
const service = new DefaultTransformInstaller(generators, esClientMock, loggerMock);
|
||||
|
||||
await service.installAndStartTransform(createSLO(createAPMTransactionErrorRateIndicator()));
|
||||
|
||||
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(1);
|
||||
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
class DummyTransformGenerator implements TransformGenerator {
|
||||
getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
return {} as TransformPutTransformRequest;
|
||||
}
|
||||
}
|
||||
|
||||
class FailTransformGenerator implements TransformGenerator {
|
||||
getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
throw new Error('Some error');
|
||||
}
|
||||
}
|
|
@ -1,56 +0,0 @@
|
|||
/*
|
||||
* 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 { errors } from '@elastic/elasticsearch';
|
||||
import { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
|
||||
import { SLO, SLITypes } from '../../types/models';
|
||||
import { TransformGenerator } from './transform_generators';
|
||||
|
||||
export interface TransformInstaller {
|
||||
installAndStartTransform(slo: SLO, spaceId: string): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultTransformInstaller implements TransformInstaller {
|
||||
constructor(
|
||||
private generators: Record<SLITypes, TransformGenerator>,
|
||||
private esClient: ElasticsearchClient,
|
||||
private logger: Logger
|
||||
) {}
|
||||
|
||||
async installAndStartTransform(slo: SLO, spaceId: string = 'default'): Promise<void> {
|
||||
const generator = this.generators[slo.indicator.type];
|
||||
if (!generator) {
|
||||
this.logger.error(`No transform generator found for ${slo.indicator.type} SLO type`);
|
||||
throw new Error(`Unsupported SLO type: ${slo.indicator.type}`);
|
||||
}
|
||||
|
||||
const transformParams = generator.getTransformParams(slo, spaceId);
|
||||
try {
|
||||
await this.esClient.transform.putTransform(transformParams);
|
||||
} catch (err) {
|
||||
// swallow the error if the transform already exists.
|
||||
const isAlreadyExistError =
|
||||
err instanceof errors.ResponseError &&
|
||||
err?.body?.error?.type === 'resource_already_exists_exception';
|
||||
if (!isAlreadyExistError) {
|
||||
this.logger.error(`Cannot create transform for ${slo.indicator.type} SLO type: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await this.esClient.transform.startTransform(
|
||||
{ transform_id: transformParams.transform_id },
|
||||
{ ignore: [409] }
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(`Cannot start transform id ${transformParams.transform_id}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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.
|
||||
*/
|
||||
/* eslint-disable max-classes-per-file */
|
||||
|
||||
import { elasticsearchServiceMock, loggingSystemMock } from '@kbn/core/server/mocks';
|
||||
import { ElasticsearchClient } from '@kbn/core/server';
|
||||
import { MockedLogger } from '@kbn/logging-mocks';
|
||||
import { TransformPutTransformRequest } from '@elastic/elasticsearch/lib/api/types';
|
||||
|
||||
import { DefaultTransformManager } from './transform_manager';
|
||||
import {
|
||||
ApmTransactionErrorRateTransformGenerator,
|
||||
TransformGenerator,
|
||||
} from './transform_generators';
|
||||
import { SLO, SLITypes } from '../../types/models';
|
||||
import { createAPMTransactionErrorRateIndicator, createSLO } from './fixtures/slo';
|
||||
|
||||
const SPACE_ID = 'space-id';
|
||||
|
||||
describe('TransformManager', () => {
|
||||
let esClientMock: jest.Mocked<ElasticsearchClient>;
|
||||
let loggerMock: jest.Mocked<MockedLogger>;
|
||||
|
||||
beforeEach(() => {
|
||||
esClientMock = elasticsearchServiceMock.createElasticsearchClient();
|
||||
loggerMock = loggingSystemMock.createLogger();
|
||||
});
|
||||
|
||||
describe('Install', () => {
|
||||
describe('Unhappy path', () => {
|
||||
it('throws when no generator exists for the slo indicator type', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_duration': new DummyTransformGenerator(),
|
||||
};
|
||||
const service = new DefaultTransformManager(generators, esClientMock, loggerMock, SPACE_ID);
|
||||
|
||||
await expect(
|
||||
service.install(
|
||||
createSLO({
|
||||
type: 'slo.apm.transaction_error_rate',
|
||||
params: {
|
||||
environment: 'irrelevant',
|
||||
service: 'irrelevant',
|
||||
transaction_name: 'irrelevant',
|
||||
transaction_type: 'irrelevant',
|
||||
},
|
||||
})
|
||||
)
|
||||
).rejects.toThrowError('Unsupported SLO type: slo.apm.transaction_error_rate');
|
||||
});
|
||||
|
||||
it('throws when transform generator fails', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_duration': new FailTransformGenerator(),
|
||||
};
|
||||
const transformManager = new DefaultTransformManager(
|
||||
generators,
|
||||
esClientMock,
|
||||
loggerMock,
|
||||
SPACE_ID
|
||||
);
|
||||
|
||||
await expect(
|
||||
transformManager.install(
|
||||
createSLO({
|
||||
type: 'slo.apm.transaction_duration',
|
||||
params: {
|
||||
environment: 'irrelevant',
|
||||
service: 'irrelevant',
|
||||
transaction_name: 'irrelevant',
|
||||
transaction_type: 'irrelevant',
|
||||
'threshold.us': 250000,
|
||||
},
|
||||
})
|
||||
)
|
||||
).rejects.toThrowError('Some error');
|
||||
});
|
||||
});
|
||||
|
||||
it('installs the transform', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
};
|
||||
const transformManager = new DefaultTransformManager(
|
||||
generators,
|
||||
esClientMock,
|
||||
loggerMock,
|
||||
SPACE_ID
|
||||
);
|
||||
const slo = createSLO(createAPMTransactionErrorRateIndicator());
|
||||
|
||||
const transformId = await transformManager.install(slo);
|
||||
|
||||
expect(esClientMock.transform.putTransform).toHaveBeenCalledTimes(1);
|
||||
expect(transformId).toBe(`slo-${slo.id}`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Start', () => {
|
||||
it('starts the transform', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
};
|
||||
const transformManager = new DefaultTransformManager(
|
||||
generators,
|
||||
esClientMock,
|
||||
loggerMock,
|
||||
SPACE_ID
|
||||
);
|
||||
|
||||
await transformManager.start('slo-transform-id');
|
||||
|
||||
expect(esClientMock.transform.startTransform).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Stop', () => {
|
||||
it('stops the transform', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
};
|
||||
const transformManager = new DefaultTransformManager(
|
||||
generators,
|
||||
esClientMock,
|
||||
loggerMock,
|
||||
SPACE_ID
|
||||
);
|
||||
|
||||
await transformManager.stop('slo-transform-id');
|
||||
|
||||
expect(esClientMock.transform.stopTransform).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Uninstall', () => {
|
||||
it('uninstalls the transform', async () => {
|
||||
// @ts-ignore defining only a subset of the possible SLI
|
||||
const generators: Record<SLITypes, TransformGenerator> = {
|
||||
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
|
||||
};
|
||||
const transformManager = new DefaultTransformManager(
|
||||
generators,
|
||||
esClientMock,
|
||||
loggerMock,
|
||||
SPACE_ID
|
||||
);
|
||||
|
||||
await transformManager.uninstall('slo-transform-id');
|
||||
|
||||
expect(esClientMock.transform.deleteTransform).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
class DummyTransformGenerator implements TransformGenerator {
|
||||
getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
return {} as TransformPutTransformRequest;
|
||||
}
|
||||
}
|
||||
|
||||
class FailTransformGenerator implements TransformGenerator {
|
||||
getTransformParams(slo: SLO): TransformPutTransformRequest {
|
||||
throw new Error('Some error');
|
||||
}
|
||||
}
|
|
@ -0,0 +1,83 @@
|
|||
/*
|
||||
* 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 { ElasticsearchClient, Logger } from '@kbn/core/server';
|
||||
|
||||
import { SLO, SLITypes } from '../../types/models';
|
||||
import { TransformGenerator } from './transform_generators';
|
||||
|
||||
type TransformId = string;
|
||||
|
||||
export interface TransformManager {
|
||||
install(slo: SLO): Promise<TransformId>;
|
||||
start(transformId: TransformId): Promise<void>;
|
||||
stop(transformId: TransformId): Promise<void>;
|
||||
uninstall(transformId: TransformId): Promise<void>;
|
||||
}
|
||||
|
||||
export class DefaultTransformManager implements TransformManager {
|
||||
constructor(
|
||||
private generators: Record<SLITypes, TransformGenerator>,
|
||||
private esClient: ElasticsearchClient,
|
||||
private logger: Logger,
|
||||
private spaceId: string
|
||||
) {}
|
||||
|
||||
async install(slo: SLO): Promise<TransformId> {
|
||||
const generator = this.generators[slo.indicator.type];
|
||||
if (!generator) {
|
||||
this.logger.error(`No transform generator found for ${slo.indicator.type} SLO type`);
|
||||
throw new Error(`Unsupported SLO type: ${slo.indicator.type}`);
|
||||
}
|
||||
|
||||
const transformParams = generator.getTransformParams(slo, this.spaceId);
|
||||
try {
|
||||
await this.esClient.transform.putTransform(transformParams);
|
||||
} catch (err) {
|
||||
this.logger.error(`Cannot create transform for ${slo.indicator.type} SLO type: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
|
||||
return transformParams.transform_id;
|
||||
}
|
||||
|
||||
async start(transformId: TransformId): Promise<void> {
|
||||
try {
|
||||
await this.esClient.transform.startTransform(
|
||||
{ transform_id: transformId },
|
||||
{ ignore: [409] }
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(`Cannot start transform id ${transformId}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async stop(transformId: TransformId): Promise<void> {
|
||||
try {
|
||||
await this.esClient.transform.stopTransform(
|
||||
{ transform_id: transformId, wait_for_completion: true },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(`Cannot stop transform id ${transformId}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
|
||||
async uninstall(transformId: TransformId): Promise<void> {
|
||||
try {
|
||||
await this.esClient.transform.deleteTransform(
|
||||
{ transform_id: transformId, force: true },
|
||||
{ ignore: [404] }
|
||||
);
|
||||
} catch (err) {
|
||||
this.logger.error(`Cannot delete transform id ${transformId}: ${err}`);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -85,3 +85,9 @@ export type CreateSLOResponse = t.TypeOf<typeof createSLOResponseSchema>;
|
|||
export const createSLOParamsSchema = t.type({
|
||||
body: createSLOBodySchema,
|
||||
});
|
||||
|
||||
export const deleteSLOParamsSchema = t.type({
|
||||
path: t.type({
|
||||
id: t.string,
|
||||
}),
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue