forked from elastic/kibana
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
add licensed feature usage API (elastic#63549)
* add licensed feature usage API * add FTR test and plugin * jsdoc * fix FTR test suite name * remove clear API * accept Date for notifyUsage
- Loading branch information
1 parent
2d275db
commit ae0907f
Showing
18 changed files
with
502 additions
and
13 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,30 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
import { IRouter, StartServicesAccessor } from 'src/core/server'; | ||
import { LicensingPluginStart } from '../types'; | ||
|
||
export function registerFeatureUsageRoute( | ||
router: IRouter, | ||
getStartServices: StartServicesAccessor<{}, LicensingPluginStart> | ||
) { | ||
router.get( | ||
{ path: '/api/licensing/feature_usage', validate: false }, | ||
async (context, request, response) => { | ||
const [, , { featureUsage }] = await getStartServices(); | ||
return response.ok({ | ||
body: [...featureUsage.getLastUsages().entries()].reduce( | ||
(res, [featureName, lastUsage]) => { | ||
return { | ||
...res, | ||
[featureName]: new Date(lastUsage).toISOString(), | ||
}; | ||
}, | ||
{} | ||
), | ||
}); | ||
} | ||
); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
43 changes: 43 additions & 0 deletions
43
x-pack/plugins/licensing/server/services/feature_usage_service.mock.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { | ||
FeatureUsageService, | ||
FeatureUsageServiceSetup, | ||
FeatureUsageServiceStart, | ||
} from './feature_usage_service'; | ||
|
||
const createSetupMock = (): jest.Mocked<FeatureUsageServiceSetup> => { | ||
const mock = { | ||
register: jest.fn(), | ||
}; | ||
|
||
return mock; | ||
}; | ||
|
||
const createStartMock = (): jest.Mocked<FeatureUsageServiceStart> => { | ||
const mock = { | ||
notifyUsage: jest.fn(), | ||
getLastUsages: jest.fn(), | ||
}; | ||
|
||
return mock; | ||
}; | ||
|
||
const createServiceMock = (): jest.Mocked<PublicMethodsOf<FeatureUsageService>> => { | ||
const mock = { | ||
setup: jest.fn(() => createSetupMock()), | ||
start: jest.fn(() => createStartMock()), | ||
}; | ||
|
||
return mock; | ||
}; | ||
|
||
export const featureUsageMock = { | ||
create: createServiceMock, | ||
createSetup: createSetupMock, | ||
createStart: createStartMock, | ||
}; |
116 changes: 116 additions & 0 deletions
116
x-pack/plugins/licensing/server/services/feature_usage_service.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,116 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { FeatureUsageService } from './feature_usage_service'; | ||
|
||
describe('FeatureUsageService', () => { | ||
let service: FeatureUsageService; | ||
|
||
beforeEach(() => { | ||
service = new FeatureUsageService(); | ||
}); | ||
|
||
afterEach(() => { | ||
jest.restoreAllMocks(); | ||
}); | ||
|
||
const toObj = (map: ReadonlyMap<string, any>): Record<string, any> => | ||
Object.fromEntries(map.entries()); | ||
|
||
describe('#setup', () => { | ||
describe('#register', () => { | ||
it('throws when registering the same feature twice', () => { | ||
const setup = service.setup(); | ||
setup.register('foo'); | ||
expect(() => { | ||
setup.register('foo'); | ||
}).toThrowErrorMatchingInlineSnapshot(`"Feature 'foo' has already been registered."`); | ||
}); | ||
}); | ||
}); | ||
|
||
describe('#start', () => { | ||
describe('#notifyUsage', () => { | ||
it('allows to notify a feature usage', () => { | ||
const setup = service.setup(); | ||
setup.register('feature'); | ||
const start = service.start(); | ||
start.notifyUsage('feature', 127001); | ||
|
||
expect(start.getLastUsages().get('feature')).toBe(127001); | ||
}); | ||
|
||
it('can receive a Date object', () => { | ||
const setup = service.setup(); | ||
setup.register('feature'); | ||
const start = service.start(); | ||
|
||
const usageTime = new Date(2015, 9, 21, 17, 54, 12); | ||
start.notifyUsage('feature', usageTime); | ||
expect(start.getLastUsages().get('feature')).toBe(usageTime.getTime()); | ||
}); | ||
|
||
it('uses the current time when `usedAt` is unspecified', () => { | ||
jest.spyOn(Date, 'now').mockReturnValue(42); | ||
|
||
const setup = service.setup(); | ||
setup.register('feature'); | ||
const start = service.start(); | ||
start.notifyUsage('feature'); | ||
|
||
expect(start.getLastUsages().get('feature')).toBe(42); | ||
}); | ||
|
||
it('throws when notifying for an unregistered feature', () => { | ||
service.setup(); | ||
const start = service.start(); | ||
expect(() => { | ||
start.notifyUsage('unregistered'); | ||
}).toThrowErrorMatchingInlineSnapshot(`"Feature 'unregistered' is not registered."`); | ||
}); | ||
}); | ||
|
||
describe('#getLastUsages', () => { | ||
it('returns the last usage for all used features', () => { | ||
const setup = service.setup(); | ||
setup.register('featureA'); | ||
setup.register('featureB'); | ||
const start = service.start(); | ||
start.notifyUsage('featureA', 127001); | ||
start.notifyUsage('featureB', 6666); | ||
|
||
expect(toObj(start.getLastUsages())).toEqual({ | ||
featureA: 127001, | ||
featureB: 6666, | ||
}); | ||
}); | ||
|
||
it('returns the last usage even after notifying for an older usage', () => { | ||
const setup = service.setup(); | ||
setup.register('featureA'); | ||
const start = service.start(); | ||
start.notifyUsage('featureA', 1000); | ||
start.notifyUsage('featureA', 500); | ||
|
||
expect(toObj(start.getLastUsages())).toEqual({ | ||
featureA: 1000, | ||
}); | ||
}); | ||
|
||
it('does not return entries for unused registered features', () => { | ||
const setup = service.setup(); | ||
setup.register('featureA'); | ||
setup.register('featureB'); | ||
const start = service.start(); | ||
start.notifyUsage('featureA', 127001); | ||
|
||
expect(toObj(start.getLastUsages())).toEqual({ | ||
featureA: 127001, | ||
}); | ||
}); | ||
}); | ||
}); | ||
}); |
63 changes: 63 additions & 0 deletions
63
x-pack/plugins/licensing/server/services/feature_usage_service.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
/* | ||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one | ||
* or more contributor license agreements. Licensed under the Elastic License; | ||
* you may not use this file except in compliance with the Elastic License. | ||
*/ | ||
|
||
import { isDate } from 'lodash'; | ||
|
||
/** @public */ | ||
export interface FeatureUsageServiceSetup { | ||
/** | ||
* Register a feature to be able to notify of it's usages using the {@link FeatureUsageServiceStart | service start contract}. | ||
*/ | ||
register(featureName: string): void; | ||
} | ||
|
||
/** @public */ | ||
export interface FeatureUsageServiceStart { | ||
/** | ||
* Notify of a registered feature usage at given time. | ||
* | ||
* @param featureName - the name of the feature to notify usage of | ||
* @param usedAt - Either a `Date` or an unix timestamp with ms. If not specified, it will be set to the current time. | ||
*/ | ||
notifyUsage(featureName: string, usedAt?: Date | number): void; | ||
/** | ||
* Return a map containing last usage timestamp for all features. | ||
* Features that were not used yet do not appear in the map. | ||
*/ | ||
getLastUsages(): ReadonlyMap<string, number>; | ||
} | ||
|
||
export class FeatureUsageService { | ||
private readonly features: string[] = []; | ||
private readonly lastUsages = new Map<string, number>(); | ||
|
||
public setup(): FeatureUsageServiceSetup { | ||
return { | ||
register: featureName => { | ||
if (this.features.includes(featureName)) { | ||
throw new Error(`Feature '${featureName}' has already been registered.`); | ||
} | ||
this.features.push(featureName); | ||
}, | ||
}; | ||
} | ||
|
||
public start(): FeatureUsageServiceStart { | ||
return { | ||
notifyUsage: (featureName, usedAt = Date.now()) => { | ||
if (!this.features.includes(featureName)) { | ||
throw new Error(`Feature '${featureName}' is not registered.`); | ||
} | ||
if (isDate(usedAt)) { | ||
usedAt = usedAt.getTime(); | ||
} | ||
const currentValue = this.lastUsages.get(featureName) ?? 0; | ||
this.lastUsages.set(featureName, Math.max(usedAt, currentValue)); | ||
}, | ||
getLastUsages: () => new Map(this.lastUsages.entries()), | ||
}; | ||
} | ||
} |
Oops, something went wrong.