Skip to content

Commit 3a65925

Browse files
[APM] Track usage of Gold+ features (#77630) (#77854)
* adding license check * fixing api test * refactoring
1 parent 97e7137 commit 3a65925

File tree

11 files changed

+271
-163
lines changed

11 files changed

+271
-163
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
/*
2+
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
3+
* or more contributor license agreements. Licensed under the Elastic License;
4+
* you may not use this file except in compliance with the Elastic License.
5+
*/
6+
import { i18n } from '@kbn/i18n';
7+
8+
export const INVALID_LICENSE = i18n.translate(
9+
'xpack.apm.settings.customizeUI.customLink.license.text',
10+
{
11+
defaultMessage:
12+
"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.",
13+
}
14+
);

x-pack/plugins/apm/public/components/app/Settings/CustomizeUI/CustomLink/index.tsx

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import { EuiPanel, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
88
import { isEmpty } from 'lodash';
99
import React, { useEffect, useState } from 'react';
10-
import { i18n } from '@kbn/i18n';
10+
import { INVALID_LICENSE } from '../../../../../../common/custom_link';
1111
import { CustomLink } from '../../../../../../common/custom_link/custom_link_types';
1212
import { useLicense } from '../../../../../hooks/useLicense';
1313
import { useFetcher, FETCH_STATUS } from '../../../../../hooks/useFetcher';
@@ -94,15 +94,7 @@ export function CustomLinkOverview() {
9494
/>
9595
)
9696
) : (
97-
<LicensePrompt
98-
text={i18n.translate(
99-
'xpack.apm.settings.customizeUI.customLink.license.text',
100-
{
101-
defaultMessage:
102-
"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.",
103-
}
104-
)}
105-
/>
97+
<LicensePrompt text={INVALID_LICENSE} />
10698
)}
10799
</EuiPanel>
108100
</>

x-pack/plugins/apm/server/feature.ts

Lines changed: 45 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,12 @@
55
*/
66

77
import { i18n } from '@kbn/i18n';
8+
import { LicenseType } from '../../licensing/common/types';
89
import { AlertType } from '../common/alert_types';
10+
import {
11+
LicensingPluginSetup,
12+
LicensingRequestHandlerContext,
13+
} from '../../licensing/server';
914

1015
export const APM_FEATURE = {
1116
id: 'apm',
@@ -58,5 +63,43 @@ export const APM_FEATURE = {
5863
},
5964
};
6065

61-
export const APM_SERVICE_MAPS_FEATURE_NAME = 'APM service maps';
62-
export const APM_SERVICE_MAPS_LICENSE_TYPE = 'platinum';
66+
interface Feature {
67+
name: string;
68+
license: LicenseType;
69+
}
70+
type FeatureName = 'serviceMaps' | 'ml' | 'customLinks';
71+
export const features: Record<FeatureName, Feature> = {
72+
serviceMaps: {
73+
name: 'APM service maps',
74+
license: 'platinum',
75+
},
76+
ml: {
77+
name: 'APM machine learning',
78+
license: 'platinum',
79+
},
80+
customLinks: {
81+
name: 'APM custom links',
82+
license: 'gold',
83+
},
84+
};
85+
86+
export function registerFeaturesUsage({
87+
licensingPlugin,
88+
}: {
89+
licensingPlugin: LicensingPluginSetup;
90+
}) {
91+
Object.values(features).forEach(({ name, license }) => {
92+
licensingPlugin.featureUsage.register(name, license);
93+
});
94+
}
95+
96+
export function notifyFeatureUsage({
97+
licensingPlugin,
98+
featureName,
99+
}: {
100+
licensingPlugin: LicensingRequestHandlerContext;
101+
featureName: FeatureName;
102+
}) {
103+
const feature = features[featureName];
104+
licensingPlugin.featureUsage.notifyUsage(feature.name);
105+
}

x-pack/plugins/apm/server/plugin.ts

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,11 +26,7 @@ import { MlPluginSetup } from '../../ml/server';
2626
import { ObservabilityPluginSetup } from '../../observability/server';
2727
import { SecurityPluginSetup } from '../../security/server';
2828
import { TaskManagerSetupContract } from '../../task_manager/server';
29-
import {
30-
APM_FEATURE,
31-
APM_SERVICE_MAPS_FEATURE_NAME,
32-
APM_SERVICE_MAPS_LICENSE_TYPE,
33-
} from './feature';
29+
import { APM_FEATURE, registerFeaturesUsage } from './feature';
3430
import { registerApmAlerts } from './lib/alerts/register_apm_alerts';
3531
import { createApmTelemetry } from './lib/apm_telemetry';
3632
import { getInternalSavedObjectsClient } from './lib/helpers/get_internal_saved_objects_client';
@@ -128,10 +124,8 @@ export class APMPlugin implements Plugin<APMPluginSetup> {
128124
});
129125

130126
plugins.features.registerKibanaFeature(APM_FEATURE);
131-
plugins.licensing.featureUsage.register(
132-
APM_SERVICE_MAPS_FEATURE_NAME,
133-
APM_SERVICE_MAPS_LICENSE_TYPE
134-
);
127+
128+
registerFeaturesUsage({ licensingPlugin: plugins.licensing });
135129

136130
createApmApi().init(core, {
137131
config$: mergedConfig$,

x-pack/plugins/apm/server/routes/service_map.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ import { getServiceMap } from '../lib/service_map/get_service_map';
1515
import { getServiceMapServiceNodeInfo } from '../lib/service_map/get_service_map_service_node_info';
1616
import { createRoute } from './create_route';
1717
import { rangeRt, uiFiltersRt } from './default_api_types';
18-
import { APM_SERVICE_MAPS_FEATURE_NAME } from '../feature';
18+
import { notifyFeatureUsage } from '../feature';
1919
import { getSearchAggregatedTransactions } from '../lib/helpers/aggregated_transactions';
2020
import { getParsedUiFilters } from '../lib/helpers/convert_ui_filters/get_parsed_ui_filters';
2121

@@ -37,7 +37,11 @@ export const serviceMapRoute = createRoute(() => ({
3737
if (!isActivePlatinumLicense(context.licensing.license)) {
3838
throw Boom.forbidden(invalidLicenseMessage);
3939
}
40-
context.licensing.featureUsage.notifyUsage(APM_SERVICE_MAPS_FEATURE_NAME);
40+
41+
notifyFeatureUsage({
42+
licensingPlugin: context.licensing,
43+
featureName: 'serviceMaps',
44+
});
4145

4246
const logger = context.logger;
4347
const setup = await setupRequest(context, request);

x-pack/plugins/apm/server/routes/settings/anomaly_detection.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import { setupRequest } from '../../lib/helpers/setup_request';
1515
import { getAllEnvironments } from '../../lib/environments/get_all_environments';
1616
import { hasLegacyJobs } from '../../lib/anomaly_detection/has_legacy_jobs';
1717
import { getSearchAggregatedTransactions } from '../../lib/helpers/aggregated_transactions';
18+
import { notifyFeatureUsage } from '../../feature';
1819

1920
// get ML anomaly detection jobs for each environment
2021
export const anomalyDetectionJobsRoute = createRoute(() => ({
@@ -62,6 +63,10 @@ export const createAnomalyDetectionJobsRoute = createRoute(() => ({
6263
}
6364

6465
await createAnomalyDetectionJobs(setup, environments, context.logger);
66+
notifyFeatureUsage({
67+
licensingPlugin: context.licensing,
68+
featureName: 'ml',
69+
});
6570
},
6671
}));
6772

x-pack/plugins/apm/server/routes/settings/custom_link.ts

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,14 @@
33
* or more contributor license agreements. Licensed under the Elastic License;
44
* you may not use this file except in compliance with the Elastic License.
55
*/
6+
7+
import Boom from 'boom';
68
import * as t from 'io-ts';
79
import { pick } from 'lodash';
10+
import { INVALID_LICENSE } from '../../../common/custom_link';
11+
import { ILicense } from '../../../../licensing/common/types';
812
import { FILTER_OPTIONS } from '../../../common/custom_link/custom_link_filter_options';
13+
import { notifyFeatureUsage } from '../../feature';
914
import { setupRequest } from '../../lib/helpers/setup_request';
1015
import { createOrUpdateCustomLink } from '../../lib/settings/custom_link/create_or_update_custom_link';
1116
import {
@@ -17,6 +22,10 @@ import { getTransaction } from '../../lib/settings/custom_link/get_transaction';
1722
import { listCustomLinks } from '../../lib/settings/custom_link/list_custom_links';
1823
import { createRoute } from '../create_route';
1924

25+
function isActiveGoldLicense(license: ILicense) {
26+
return license.isActive && license.hasAtLeast('gold');
27+
}
28+
2029
export const customLinkTransactionRoute = createRoute(() => ({
2130
path: '/api/apm/settings/custom_links/transaction',
2231
params: {
@@ -37,6 +46,9 @@ export const listCustomLinksRoute = createRoute(() => ({
3746
query: filterOptionsRt,
3847
},
3948
handler: async ({ context, request }) => {
49+
if (!isActiveGoldLicense(context.licensing.license)) {
50+
throw Boom.forbidden(INVALID_LICENSE);
51+
}
4052
const setup = await setupRequest(context, request);
4153
const { query } = context.params;
4254
// picks only the items listed in FILTER_OPTIONS
@@ -55,9 +67,17 @@ export const createCustomLinkRoute = createRoute(() => ({
5567
tags: ['access:apm', 'access:apm_write'],
5668
},
5769
handler: async ({ context, request }) => {
70+
if (!isActiveGoldLicense(context.licensing.license)) {
71+
throw Boom.forbidden(INVALID_LICENSE);
72+
}
5873
const setup = await setupRequest(context, request);
5974
const customLink = context.params.body;
6075
const res = await createOrUpdateCustomLink({ customLink, setup });
76+
77+
notifyFeatureUsage({
78+
licensingPlugin: context.licensing,
79+
featureName: 'customLinks',
80+
});
6181
return res;
6282
},
6383
}));
@@ -75,6 +95,9 @@ export const updateCustomLinkRoute = createRoute(() => ({
7595
tags: ['access:apm', 'access:apm_write'],
7696
},
7797
handler: async ({ context, request }) => {
98+
if (!isActiveGoldLicense(context.licensing.license)) {
99+
throw Boom.forbidden(INVALID_LICENSE);
100+
}
78101
const setup = await setupRequest(context, request);
79102
const { id } = context.params.path;
80103
const customLink = context.params.body;
@@ -99,6 +122,9 @@ export const deleteCustomLinkRoute = createRoute(() => ({
99122
tags: ['access:apm', 'access:apm_write'],
100123
},
101124
handler: async ({ context, request }) => {
125+
if (!isActiveGoldLicense(context.licensing.license)) {
126+
throw Boom.forbidden(INVALID_LICENSE);
127+
}
102128
const setup = await setupRequest(context, request);
103129
const { id } = context.params.path;
104130
const res = await deleteCustomLink({

x-pack/test/apm_api_integration/basic/tests/feature_controls.ts

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -149,13 +149,6 @@ export default function featureControlsTests({ getService }: FtrProviderContext)
149149
log.error(JSON.stringify(res, null, 2));
150150
},
151151
},
152-
{
153-
req: {
154-
url: `/api/apm/settings/custom_links`,
155-
},
156-
expectForbidden: expect404,
157-
expectResponse: expect200,
158-
},
159152
{
160153
req: {
161154
url: `/api/apm/settings/custom_links/transaction`,

0 commit comments

Comments
 (0)