Skip to content
Merged
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions x-pack/plugins/apm/server/feature.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,9 @@ export const APM_FEATURE = {

export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps';
export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum';
Copy link
Member

@sorenlouv sorenlouv Sep 17, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Since a feature name and a feature license belongs together I think we should group them like:

Suggested change
export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps';
export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum';
export const APM_SERVICE_MAPS_FEATURE = {
name: 'APM service maps',
license: 'platinum'
}


export const APM_ML_FEATURE_NAME = 'APM machine learning';
export const APM_ML_LICENSE_TYPE = 'platinum';

export const APM_CUSTOM_LINKS_FEATURE_NAME = 'APM custom links';
export const APM_CUSTOM_LINKS_LICENSE_TYPE = 'gold';
12 changes: 12 additions & 0 deletions x-pack/plugins/apm/server/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ import { ObservabilityPluginSetup } from '../../observability/server';
import { SecurityPluginSetup } from '../../security/server';
import { TaskManagerSetupContract } from '../../task_manager/server';
import {
APM_CUSTOM_LINKS_FEATURE_NAME,
APM_CUSTOM_LINKS_LICENSE_TYPE,
APM_FEATURE,
APM_ML_FEATURE_NAME,
APM_ML_LICENSE_TYPE,
APM_SERVICE_MAPS_FEATURE_NAME,
APM_SERVICE_MAPS_LICENSE_TYPE,
} from './feature';
Expand Down Expand Up @@ -132,6 +136,14 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
APM_SERVICE_MAPS_FEATURE_NAME,
APM_SERVICE_MAPS_LICENSE_TYPE
);
plugins.licensing.featureUsage.register(
APM_ML_FEATURE_NAME,
APM_ML_LICENSE_TYPE
);
plugins.licensing.featureUsage.register(
APM_CUSTOM_LINKS_FEATURE_NAME,
APM_CUSTOM_LINKS_LICENSE_TYPE
);

createApmApi().init(core, {
config$: mergedConfig$,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { setupRequest } from '../../lib/helpers/setup_request';
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
import { APM_ML_FEATURE_NAME } from '../../feature';

// get ML anomaly detection jobs for each environment
export const anomalyDetectionJobsRoute = createRoute(() => ({
Expand Down Expand Up @@ -62,6 +63,7 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({
}

await createAnomalyDetectionJobs(setup, environments, context.logger);
context.licensing.featureUsage.notifyUsage(APM_ML_FEATURE_NAME);
},
}));

Expand Down
31 changes: 31 additions & 0 deletions x-pack/plugins/apm/server/routes/settings/custom_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/

import { i18n } from '@kbn/i18n';
import Boom from 'boom';
import * as t from 'io-ts';
import { pick } from 'lodash';
import { ILicense } from '../../../../licensing/common/types';
import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options';
import { APM_CUSTOM_LINKS_FEATURE_NAME } from '../../feature';
import { setupRequest } from '../../lib/helpers/setup_request';
import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link';
import {
Expand All @@ -17,6 +22,18 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
import { createRoute } from '../create_route';

function isActiveGoldLicense(license: ILicense) {
return license.isActive && license.hasAtLeast('gold');
}

const INVALID_LICENSE = i18n.translate(
'xpack.apm.settings.customizeUI.customLink.forbidden',
{
defaultMessage:
"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services.",
}
);

export const customLinkTransactionRoute = createRoute(() => ({
path: '/api/apm/settings/custom_links/transaction',
params: {
Expand All @@ -37,6 +54,9 @@ export const listCustomLinksRoute = createRoute(() => ({
query: filterOptionsRt,
},
handler: async ({ context, request }) => {
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
const setup = await setupRequest(context, request);
const { query } = context.params;
// picks only the items listed in FILTER_OPTIONS
Expand All @@ -55,9 +75,14 @@ export const createCustomLinkRoute = createRoute(() => ({
tags: ['access:apm', 'access:apm_write'],
},
handler: async ({ context, request }) => {
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
const setup = await setupRequest(context, request);
const customLink = context.params.body;
const res = await createOrUpdateCustomLink({ customLink, setup });

context.licensing.featureUsage.notifyUsage(APM_CUSTOM_LINKS_FEATURE_NAME);
return res;
},
}));
Expand All @@ -75,6 +100,9 @@ export const updateCustomLinkRoute = createRoute(() => ({
tags: ['access:apm', 'access:apm_write'],
},
handler: async ({ context, request }) => {
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
const setup = await setupRequest(context, request);
const { id } = context.params.path;
const customLink = context.params.body;
Expand All @@ -99,6 +127,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({
tags: ['access:apm', 'access:apm_write'],
},
handler: async ({ context, request }) => {
if (!isActiveGoldLicense(context.licensing.license)) {
throw Boom.forbidden(INVALID_LICENSE);
}
const setup = await setupRequest(context, request);
const { id } = context.params.path;
const res = await deleteCustomLink({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -149,13 +149,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
log.error(JSON.stringify(res, null, 2));
},
},
{
req: {
url: `/api/apm/settings/custom_links`,
},
expectForbidden: expect404,
expectResponse: expect200,
},
{
req: {
url: `/api/apm/settings/custom_links/transaction`,
Expand Down
143 changes: 10 additions & 133 deletions x-pack/test/apm_api_integration/basic/tests/settings/custom_link.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,75 +3,16 @@
* or more contributor license agreements. Licensed under the Elastic License;
* you may not use this file except in compliance with the Elastic License.
*/
import URL from 'url';
import expect from '@kbn/expect';
import { expectSnapshot } from '../../../common/match_snapshot';
import { CustomLink } from '../../../../../plugins/apm/common/custom_link/custom_link_types';
import { FtrProviderContext } from '../../../common/ftr_provider_context';

export default function customLinksTests({ getService }: FtrProviderContext) {
const supertestRead = getService('supertestAsApmReadUser');
const supertestWrite = getService('supertestAsApmWriteUser');
const log = getService('log');
const esArchiver = getService('esArchiver');

const archiveName = 'apm_8.0.0';

function searchCustomLinks(filters?: any) {
const path = URL.format({
pathname: `/api/apm/settings/custom_links`,
query: filters,
});
return supertestRead.get(path).set('kbn-xsrf', 'foo');
}

async function createCustomLink(customLink: CustomLink) {
log.debug('creating configuration', customLink);
const res = await supertestWrite
.post(`/api/apm/settings/custom_links`)
.send(customLink)
.set('kbn-xsrf', 'foo');

throwOnError(res);

return res;
}

async function updateCustomLink(id: string, customLink: CustomLink) {
log.debug('updating configuration', id, customLink);
const res = await supertestWrite
.put(`/api/apm/settings/custom_links/${id}`)
.send(customLink)
.set('kbn-xsrf', 'foo');

throwOnError(res);

return res;
}

async function deleteCustomLink(id: string) {
log.debug('deleting configuration', id);
const res = await supertestWrite
.delete(`/api/apm/settings/custom_links/${id}`)
.set('kbn-xsrf', 'foo');

throwOnError(res);

return res;
}

function throwOnError(res: any) {
const { statusCode, req, body } = res;
if (statusCode !== 200) {
throw new Error(`
Endpoint: ${req.method} ${req.path}
Service: ${JSON.stringify(res.request._data.service)}
Status code: ${statusCode}
Response: ${body.message}`);
}
}

describe('custom links', () => {
before(async () => {
it('is only be available to users with Gold license (or higher)', async () => {
const customLink = {
url: 'https://elastic.co',
label: 'with filters',
Expand All @@ -80,80 +21,16 @@ export default function customLinksTests({ getService }: FtrProviderContext) {
{ key: 'transaction.type', value: 'qux' },
],
} as CustomLink;
await createCustomLink(customLink);
});
it('fetches a custom link', async () => {
const { status, body } = await searchCustomLinks({
'service.name': 'baz',
'transaction.type': 'qux',
});
const { label, url, filters } = body[0];

expect(status).to.equal(200);
expect({ label, url, filters }).to.eql({
label: 'with filters',
url: 'https://elastic.co',
filters: [
{ key: 'service.name', value: 'baz' },
{ key: 'transaction.type', value: 'qux' },
],
});
});
it('updates a custom link', async () => {
let { status, body } = await searchCustomLinks({
'service.name': 'baz',
'transaction.type': 'qux',
});
expect(status).to.equal(200);
await updateCustomLink(body[0].id, {
label: 'foo',
url: 'https://elastic.co?service.name={{service.name}}',
filters: [
{ key: 'service.name', value: 'quz' },
{ key: 'transaction.name', value: 'bar' },
],
});
({ status, body } = await searchCustomLinks({
'service.name': 'quz',
'transaction.name': 'bar',
}));
const { label, url, filters } = body[0];
expect(status).to.equal(200);
expect({ label, url, filters }).to.eql({
label: 'foo',
url: 'https://elastic.co?service.name={{service.name}}',
filters: [
{ key: 'service.name', value: 'quz' },
{ key: 'transaction.name', value: 'bar' },
],
});
});
it('deletes a custom link', async () => {
let { status, body } = await searchCustomLinks({
'service.name': 'quz',
'transaction.name': 'bar',
});
expect(status).to.equal(200);
await deleteCustomLink(body[0].id);
({ status, body } = await searchCustomLinks({
'service.name': 'quz',
'transaction.name': 'bar',
}));
expect(status).to.equal(200);
expect(body).to.eql([]);
});
const response = await supertestWrite
.post(`/api/apm/settings/custom_links`)
.send(customLink)
.set('kbn-xsrf', 'foo');

describe('transaction', () => {
before(() => esArchiver.load(archiveName));
after(() => esArchiver.unload(archiveName));
expect(response.status).to.be(403);

it('fetches a transaction sample', async () => {
const response = await supertestRead.get(
'/api/apm/settings/custom_links/transaction?service.name=opbeans-java'
);
expect(response.status).to.be(200);
expect(response.body.service.name).to.eql('opbeans-java');
});
expectSnapshot(response.body.message).toMatchInline(
`"To create custom links, you must be subscribed to an Elastic Gold license or above. With it, you'll have the ability to create custom links to improve your workflow when analyzing your services."`
);
});
});
}
1 change: 1 addition & 0 deletions x-pack/test/apm_api_integration/trial/tests/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ export default function observabilityApiIntegrationTests({ loadTestFile }: FtrPr
});

describe('Settings', function () {
loadTestFile(require.resolve('./settings/custom_link.ts'));
describe('Anomaly detection', function () {
loadTestFile(require.resolve('./settings/anomaly_detection/no_access_user'));
loadTestFile(require.resolve('./settings/anomaly_detection/read_user'));
Expand Down
Loading