Skip to content

Commit

Permalink
[Alerting] Adding telemetry usage counter for create rule API when pr…
Browse files Browse the repository at this point in the history
…edefined ID is specified (#108572)

* Adding telemetry usage counter for create rule API when predefined ID is specified

* Checking explicitly for default space id

* Handling undefined space id when spaces is disabled

* Consolidating logic in single function

* Tracking total usage and custom usage
  • Loading branch information
ymao1 authored Aug 17, 2021
1 parent f4022d1 commit 3ab111e
Show file tree
Hide file tree
Showing 13 changed files with 580 additions and 44 deletions.
33 changes: 33 additions & 0 deletions x-pack/plugins/alerting/server/plugin.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
*/

import { AlertingPlugin, AlertingPluginsSetup, PluginSetupContract } from './plugin';
import { createUsageCollectionSetupMock } from 'src/plugins/usage_collection/server/mocks';
import { coreMock, statusServiceMock } from '../../../../src/core/server/mocks';
import { licensingMock } from '../../licensing/server/mocks';
import { encryptedSavedObjectsMock } from '../../encrypted_saved_objects/server/mocks';
Expand Down Expand Up @@ -60,6 +61,38 @@ describe('Alerting Plugin', () => {
);
});

it('should create usage counter if usageCollection plugin is defined', async () => {
const context = coreMock.createPluginInitializerContext<AlertsConfig>({
healthCheck: {
interval: '5m',
},
invalidateApiKeysTask: {
interval: '5m',
removalDelay: '1h',
},
maxEphemeralActionsPerAlert: 10,
});
plugin = new AlertingPlugin(context);

const encryptedSavedObjectsSetup = encryptedSavedObjectsMock.createSetup();
const usageCollectionSetup = createUsageCollectionSetupMock();

const setupMocks = coreMock.createSetup();
// need await to test number of calls of setupMocks.status.set, because it is under async function which awaiting core.getStartServices()
await plugin.setup(setupMocks, {
licensing: licensingMock.createSetup(),
encryptedSavedObjects: encryptedSavedObjectsSetup,
taskManager: taskManagerMock.createSetup(),
eventLog: eventLogServiceMock.create(),
actions: actionsMock.createSetup(),
statusService: statusServiceMock.createSetupContract(),
usageCollection: usageCollectionSetup,
});

expect(usageCollectionSetup.createUsageCounter).toHaveBeenCalled();
expect(usageCollectionSetup.registerCollector).toHaveBeenCalled();
});

describe('registerType()', () => {
let setup: PluginSetupContract;
const sampleAlertType: AlertType<never, never, never, never, never, 'default'> = {
Expand Down
12 changes: 10 additions & 2 deletions x-pack/plugins/alerting/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ import {
SavedObjectsBulkGetObject,
ServiceStatusLevels,
} from '../../../../src/core/server';
import type { AlertingRequestHandlerContext } from './types';
import { AlertingRequestHandlerContext, ALERTS_FEATURE_ID } from './types';
import { defineRoutes } from './routes';
import { LICENSE_TYPE, LicensingPluginSetup, LicensingPluginStart } from '../../licensing/server';
import {
Expand Down Expand Up @@ -221,6 +221,9 @@ export class AlertingPlugin {
});
}

// Usage counter for telemetry
const usageCounter = plugins.usageCollection?.createUsageCounter(ALERTS_FEATURE_ID);

setupSavedObjects(
core.savedObjects,
plugins.encryptedSavedObjects,
Expand Down Expand Up @@ -274,7 +277,12 @@ export class AlertingPlugin {
// Routes
const router = core.http.createRouter<AlertingRequestHandlerContext>();
// Register routes
defineRoutes(router, this.licenseState, plugins.encryptedSavedObjects);
defineRoutes({
router,
licenseState: this.licenseState,
usageCounter,
encryptedSavedObjects: plugins.encryptedSavedObjects,
});

return {
registerType<
Expand Down
201 changes: 194 additions & 7 deletions x-pack/plugins/alerting/server/routes/create_rule.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import { rulesClientMock } from '../rules_client.mock';
import { AlertTypeDisabledError } from '../lib';
import { AsApiContract } from './lib';
import { SanitizedAlert } from '../types';
import { encryptedSavedObjectsMock } from '../../../encrypted_saved_objects/server/mocks';
import { usageCountersServiceMock } from 'src/plugins/usage_collection/server/usage_counters/usage_counters_service.mock';

const rulesClient = rulesClientMock.create();

Expand Down Expand Up @@ -105,8 +107,16 @@ describe('createRuleRoute', () => {
it('creates a rule with proper parameters', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();

createRuleRoute(router, licenseState);
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');

createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
usageCounter: mockUsageCounter,
});

const [config, handler] = router.post.mock.calls[0];

Expand All @@ -124,6 +134,7 @@ describe('createRuleRoute', () => {

expect(await handler(context, req, res)).toEqual({ body: createResult });

expect(mockUsageCounter.incrementCounter).not.toHaveBeenCalled();
expect(rulesClient.create).toHaveBeenCalledTimes(1);
expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -166,15 +177,186 @@ describe('createRuleRoute', () => {
});
});

it('allows providing a custom id', async () => {
it('allows providing a custom id when space is undefined', async () => {
const expectedResult = {
...createResult,
id: 'custom-id',
};
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');

createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
usageCounter: mockUsageCounter,
});

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id?}"`);

rulesClient.create.mockResolvedValueOnce({
...mockedAlert,
id: 'custom-id',
});

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: { id: 'custom-id' },
body: ruleToCreate,
},
['ok']
);

expect(await handler(context, req, res)).toEqual({ body: expectedResult });

expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {
"actions": Array [
Object {
"group": "default",
"id": "2",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "1",
"consumer": "bar",
"enabled": true,
"name": "abc",
"notifyWhen": "onActionGroupChange",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"tags": Array [
"foo",
],
"throttle": "30s",
},
"options": Object {
"id": "custom-id",
},
},
]
`);

expect(res.ok).toHaveBeenCalledWith({
body: expectedResult,
});
});

it('allows providing a custom id in default space', async () => {
const expectedResult = {
...createResult,
id: 'custom-id',
};
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');

createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
usageCounter: mockUsageCounter,
});

const [config, handler] = router.post.mock.calls[0];

expect(config.path).toMatchInlineSnapshot(`"/api/alerting/rule/{id?}"`);

rulesClient.create.mockResolvedValueOnce({
...mockedAlert,
id: 'custom-id',
});
rulesClient.getSpaceId.mockReturnValueOnce('default');

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
{
params: { id: 'custom-id' },
body: ruleToCreate,
},
['ok']
);

createRuleRoute(router, licenseState);
expect(await handler(context, req, res)).toEqual({ body: expectedResult });

expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(1);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Object {
"data": Object {
"actions": Array [
Object {
"group": "default",
"id": "2",
"params": Object {
"foo": true,
},
},
],
"alertTypeId": "1",
"consumer": "bar",
"enabled": true,
"name": "abc",
"notifyWhen": "onActionGroupChange",
"params": Object {
"bar": true,
},
"schedule": Object {
"interval": "10s",
},
"tags": Array [
"foo",
],
"throttle": "30s",
},
"options": Object {
"id": "custom-id",
},
},
]
`);

expect(res.ok).toHaveBeenCalledWith({
body: expectedResult,
});
});

it('allows providing a custom id in non-default space', async () => {
const expectedResult = {
...createResult,
id: 'custom-id',
};
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });
const mockUsageCountersSetup = usageCountersServiceMock.createSetupContract();
const mockUsageCounter = mockUsageCountersSetup.createUsageCounter('test');

createRuleRoute({
router,
licenseState,
encryptedSavedObjects,
usageCounter: mockUsageCounter,
});

const [config, handler] = router.post.mock.calls[0];

Expand All @@ -184,6 +366,7 @@ describe('createRuleRoute', () => {
...mockedAlert,
id: 'custom-id',
});
rulesClient.getSpaceId.mockReturnValueOnce('another-space');

const [context, req, res] = mockHandlerArguments(
{ rulesClient },
Expand All @@ -196,6 +379,7 @@ describe('createRuleRoute', () => {

expect(await handler(context, req, res)).toEqual({ body: expectedResult });

expect(mockUsageCounter.incrementCounter).toHaveBeenCalledTimes(2);
expect(rulesClient.create).toHaveBeenCalledTimes(1);
expect(rulesClient.create.mock.calls[0]).toMatchInlineSnapshot(`
Array [
Expand Down Expand Up @@ -241,8 +425,9 @@ describe('createRuleRoute', () => {
it('ensures the license allows creating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });

createRuleRoute(router, licenseState);
createRuleRoute({ router, licenseState, encryptedSavedObjects });

const [, handler] = router.post.mock.calls[0];

Expand All @@ -258,12 +443,13 @@ describe('createRuleRoute', () => {
it('ensures the license check prevents creating rules', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });

(verifyApiAccess as jest.Mock).mockImplementation(() => {
throw new Error('OMG');
});

createRuleRoute(router, licenseState);
createRuleRoute({ router, licenseState, encryptedSavedObjects });

const [, handler] = router.post.mock.calls[0];

Expand All @@ -279,8 +465,9 @@ describe('createRuleRoute', () => {
it('ensures the rule type gets validated for the license', async () => {
const licenseState = licenseStateMock.create();
const router = httpServiceMock.createRouter();
const encryptedSavedObjects = encryptedSavedObjectsMock.createSetup({ canEncrypt: true });

createRuleRoute(router, licenseState);
createRuleRoute({ router, licenseState, encryptedSavedObjects });

const [, handler] = router.post.mock.calls[0];

Expand Down
Loading

0 comments on commit 3ab111e

Please sign in to comment.