Skip to content

Commit

Permalink
feat(slo): Introduce createSLO application service (#140523)
Browse files Browse the repository at this point in the history
  • Loading branch information
kdelemme authored Sep 14, 2022
1 parent 0f3a66f commit 91c2457
Show file tree
Hide file tree
Showing 14 changed files with 227 additions and 44 deletions.
41 changes: 15 additions & 26 deletions x-pack/plugins/observability/server/routes/slo/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,20 +5,26 @@
* 2.0.
*/

import uuid from 'uuid';
import {
CreateSLO,
DefaultResourceInstaller,
DefaultTransformInstaller,
KibanaSavedObjectsSLORepository,
ResourceInstaller,
TransformInstaller,
} from '../../services/slo';
import {
ApmTransactionDurationTransformGenerator,
ApmTransactionErrorRateTransformGenerator,
TransformGenerator,
} from '../../services/slo/transform_generators';
import { SLO } from '../../types/models';
import { SLITypes } from '../../types/models';
import { createSLOParamsSchema } from '../../types/schema';
import { createObservabilityServerRoute } from '../create_observability_server_route';

const transformGenerators: Record<SLITypes, TransformGenerator> = {
'slo.apm.transaction_duration': new ApmTransactionDurationTransformGenerator(),
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
};

const createSLORoute = createObservabilityServerRoute({
endpoint: 'POST /api/observability/slos',
options: {
Expand All @@ -30,31 +36,14 @@ const createSLORoute = createObservabilityServerRoute({
const soClient = (await context.core).savedObjects.client;
const spaceId = spacesService.getSpaceId(request);

const resourceInstaller = new ResourceInstaller(esClient, logger);
const resourceInstaller = new DefaultResourceInstaller(esClient, logger);
const repository = new KibanaSavedObjectsSLORepository(soClient);
const transformInstaller = new TransformInstaller(
{
'slo.apm.transaction_duration': new ApmTransactionDurationTransformGenerator(),
'slo.apm.transaction_error_rate': new ApmTransactionErrorRateTransformGenerator(),
},
esClient,
logger
);

await resourceInstaller.ensureCommonResourcesInstalled(spaceId);

const slo: SLO = {
...params.body,
id: uuid.v1(),
settings: {
destination_index: params.body.settings?.destination_index,
},
};
const transformInstaller = new DefaultTransformInstaller(transformGenerators, esClient, logger);
const createSLO = new CreateSLO(resourceInstaller, repository, transformInstaller, spaceId);

await repository.save(slo);
await transformInstaller.installAndStartTransform(slo, spaceId);
const response = await createSLO.execute(params.body);

return slo;
return response;
},
});

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
/*
* 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 { CreateSLO } from './create_slo';
import { createAPMTransactionErrorRateIndicator, createSLOParams } from './fixtures/slo';
import {
createResourceInstallerMock,
createSLORepositoryMock,
createTransformInstallerMock,
} from './mocks';
import { ResourceInstaller } from './resource_installer';
import { SLORepository } from './slo_repository';
import { TransformInstaller } from './transform_installer';

const SPACE_ID = 'some-space-id';

describe('createSLO', () => {
let mockResourceInstaller: jest.Mocked<ResourceInstaller>;
let mockRepository: jest.Mocked<SLORepository>;
let mockTransformInstaller: jest.Mocked<TransformInstaller>;
let createSLO: CreateSLO;

beforeEach(() => {
mockResourceInstaller = createResourceInstallerMock();
mockRepository = createSLORepositoryMock();
mockTransformInstaller = createTransformInstallerMock();
createSLO = new CreateSLO(
mockResourceInstaller,
mockRepository,
mockTransformInstaller,
SPACE_ID
);
});

describe('happy path', () => {
it('calls the expected services', async () => {
const sloParams = createSLOParams(createAPMTransactionErrorRateIndicator());
const response = await createSLO.execute(sloParams);

expect(mockResourceInstaller.ensureCommonResourcesInstalled).toHaveBeenCalledWith(SPACE_ID);
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(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')
);
const sloParams = createSLOParams(createAPMTransactionErrorRateIndicator());

await expect(createSLO.execute(sloParams)).rejects.toThrowError('Transform Error');
expect(mockRepository.deleteById).toBeCalled();
});
});
});
56 changes: 56 additions & 0 deletions x-pack/plugins/observability/server/services/slo/create_slo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
/*
* 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 uuid from 'uuid';

import { SLO } from '../../types/models';
import { ResourceInstaller } from './resource_installer';
import { SLORepository } from './slo_repository';
import { TransformInstaller } from './transform_installer';

import { CreateSLOParams, CreateSLOResponse } from '../../types/schema';

export class CreateSLO {
constructor(
private resourceInstaller: ResourceInstaller,
private repository: SLORepository,
private transformInstaller: TransformInstaller,
private spaceId: string
) {}

public async execute(sloParams: CreateSLOParams): Promise<CreateSLOResponse> {
const slo = this.toSLO(sloParams);

await this.resourceInstaller.ensureCommonResourcesInstalled(this.spaceId);
await this.repository.save(slo);

try {
await this.transformInstaller.installAndStartTransform(slo, this.spaceId);
} catch (err) {
await this.repository.deleteById(slo.id);
throw err;
}

return this.toResponse(slo);
}

private toSLO(sloParams: CreateSLOParams): SLO {
return {
...sloParams,
id: uuid.v1(),
settings: {
destination_index: sloParams.settings?.destination_index,
},
};
}

private toResponse(slo: SLO): CreateSLOResponse {
return {
id: slo.id,
};
}
}
16 changes: 13 additions & 3 deletions x-pack/plugins/observability/server/services/slo/fixtures/slo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,12 +7,11 @@

import uuid from 'uuid';
import { SLI, SLO } from '../../../types/models';
import { CreateSLOParams } from '../../../types/schema';

export const createSLO = (indicator: SLI): SLO => ({
id: uuid.v1(),
const commonSLO: Omit<CreateSLOParams, 'indicator'> = {
name: 'irrelevant',
description: 'irrelevant',
indicator,
time_window: {
duration: '7d',
is_rolling: true,
Expand All @@ -21,9 +20,20 @@ export const createSLO = (indicator: SLI): SLO => ({
objective: {
target: 0.999,
},
};

export const createSLOParams = (indicator: SLI): CreateSLOParams => ({
...commonSLO,
indicator,
});

export const createSLO = (indicator: SLI): SLO => ({
...commonSLO,
id: uuid.v1(),
settings: {
destination_index: 'some-index',
},
indicator,
});

export const createAPMTransactionErrorRateIndicator = (params = {}): SLI => ({
Expand Down
1 change: 1 addition & 0 deletions x-pack/plugins/observability/server/services/slo/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@
export * from './resource_installer';
export * from './slo_repository';
export * from './transform_installer';
export * from './create_slo';
32 changes: 32 additions & 0 deletions x-pack/plugins/observability/server/services/slo/mocks/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/*
* 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 { ResourceInstaller } from '../resource_installer';
import { SLORepository } from '../slo_repository';
import { TransformInstaller } from '../transform_installer';

const createResourceInstallerMock = (): jest.Mocked<ResourceInstaller> => {
return {
ensureCommonResourcesInstalled: jest.fn(),
};
};

const createTransformInstallerMock = (): jest.Mocked<TransformInstaller> => {
return {
installAndStartTransform: jest.fn(),
};
};

const createSLORepositoryMock = (): jest.Mocked<SLORepository> => {
return {
save: jest.fn(),
findById: jest.fn(),
deleteById: jest.fn(),
};
};

export { createResourceInstallerMock, createTransformInstallerMock, createSLORepositoryMock };
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,14 @@ import {
SLO_INGEST_PIPELINE_NAME,
SLO_RESOURCES_VERSION,
} from '../../assets/constants';
import { ResourceInstaller } from './resource_installer';
import { DefaultResourceInstaller } from './resource_installer';

describe('resourceInstaller', () => {
describe("when the common resources don't exist", () => {
it('installs the common resources', async () => {
const mockClusterClient = elasticsearchServiceMock.createElasticsearchClient();
mockClusterClient.indices.existsIndexTemplate.mockResponseOnce(false);
const installer = new ResourceInstaller(mockClusterClient, loggerMock.create());
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());

await installer.ensureCommonResourcesInstalled();

Expand Down Expand Up @@ -51,7 +51,7 @@ describe('resourceInstaller', () => {
mockClusterClient.ingest.getPipeline.mockResponseOnce({
[SLO_INGEST_PIPELINE_NAME]: { _meta: { version: SLO_RESOURCES_VERSION } },
} as IngestGetPipelineResponse);
const installer = new ResourceInstaller(mockClusterClient, loggerMock.create());
const installer = new DefaultResourceInstaller(mockClusterClient, loggerMock.create());

await installer.ensureCommonResourcesInstalled();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,11 @@ import { getSLOSettingsTemplate } from '../../assets/component_templates/slo_set
import { getSLOIndexTemplate } from '../../assets/index_templates/slo_index_templates';
import { getSLOPipelineTemplate } from '../../assets/ingest_templates/slo_pipeline_template';

export class ResourceInstaller {
export interface ResourceInstaller {
ensureCommonResourcesInstalled(spaceId: string): Promise<void>;
}

export class DefaultResourceInstaller implements ResourceInstaller {
constructor(private esClient: ElasticsearchClient, private logger: Logger) {}

public async ensureCommonResourcesInstalled(spaceId: string = 'default'): Promise<void> {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function aStoredSLO(slo: SLO): SavedObject<StoredSLO> {
};
}

describe('sloRepository', () => {
describe('KibanaSavedObjectsSLORepository', () => {
let soClientMock: jest.Mocked<SavedObjectsClientContract>;

beforeEach(() => {
Expand Down Expand Up @@ -71,4 +71,12 @@ describe('sloRepository', () => {
expect(foundSLO).toEqual(anSLO);
expect(soClientMock.get).toHaveBeenCalledWith(SO_SLO_TYPE, anSLO.id);
});

it('removes an SLO', async () => {
const repository = new KibanaSavedObjectsSLORepository(soClientMock);

await repository.deleteById(anSLO.id);

expect(soClientMock.delete).toHaveBeenCalledWith(SO_SLO_TYPE, anSLO.id);
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import { SO_SLO_TYPE } from '../../saved_objects';
export interface SLORepository {
save(slo: SLO): Promise<SLO>;
findById(id: string): Promise<SLO>;
deleteById(id: string): Promise<void>;
}

export class KibanaSavedObjectsSLORepository implements SLORepository {
Expand All @@ -33,6 +34,10 @@ export class KibanaSavedObjectsSLORepository implements SLORepository {
const slo = await this.soClient.get<StoredSLO>(SO_SLO_TYPE, id);
return toSLOModel(slo.attributes);
}

async deleteById(id: string): Promise<void> {
await this.soClient.delete(SO_SLO_TYPE, id);
}
}

function toSLOModel(slo: StoredSLO): SLO {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ import {
MappingRuntimeFieldType,
TransformPutTransformRequest,
} from '@elastic/elasticsearch/lib/api/types';
import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants';
import { getSLOTransformTemplate } from '../../../assets/transform_templates/slo_transform_template';
import { TransformGenerator } from '.';
import { getSLODestinationIndexName, SLO_INGEST_PIPELINE_NAME } from '../../../assets/constants';
import {
apmTransactionErrorRateSLOSchema,
APMTransactionErrorRateSLO,
SLO,
} from '../../../types/models';
import { ALL_VALUE } from '../../../types/schema';
import { TransformGenerator } from '.';

const APM_SOURCE_INDEX = 'metrics-apm*';
const ALLOWED_STATUS_CODES = ['2xx', '3xx', '4xx', '5xx'];
Expand Down
Loading

0 comments on commit 91c2457

Please sign in to comment.