diff --git a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts index b47450eca235c..307ba5714e0f4 100644 --- a/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts +++ b/x-pack/plugins/security_solution_serverless/server/endpoint/services/metering_service.ts @@ -10,7 +10,7 @@ import type { ElasticsearchClient } from '@kbn/core/server'; import { ENDPOINT_HEARTBEAT_INDEX } from '@kbn/security-solution-plugin/common/endpoint/constants'; import type { EndpointHeartbeat } from '@kbn/security-solution-plugin/common/endpoint/types'; -import { ProductLine, type ProductTier } from '../../../common/product'; +import { ProductLine, ProductTier } from '../../../common/product'; import type { UsageRecord, MeteringCallbackInput } from '../../types'; import type { ServerlessSecurityConfig } from '../../config'; @@ -20,6 +20,7 @@ const SAMPLE_PERIOD_SECONDS = 3600; const THRESHOLD_MINUTES = 30; export class EndpointMeteringService { + private type: ProductLine.endpoint | `${ProductLine.cloud}_${ProductLine.endpoint}` | undefined; private tier: ProductTier | undefined; public getUsageRecords = async ({ @@ -30,6 +31,11 @@ export class EndpointMeteringService { lastSuccessfulReport, config, }: MeteringCallbackInput): Promise => { + this.setType(config); + if (!this.type) { + return []; + } + this.setTier(config); const heartbeatsResponse = await this.getHeartbeatsSince( @@ -42,14 +48,6 @@ export class EndpointMeteringService { return []; } - if (!this.tier) { - throw new Error( - `no product tier information found for heartbeats: ${JSON.stringify( - heartbeatsResponse.hits.hits - )}` - ); - } - return heartbeatsResponse.hits.hits.reduce((acc, { _source }) => { if (!_source) { return acc; @@ -108,11 +106,14 @@ export class EndpointMeteringService { timestamp.setMilliseconds(0); return { + // keep endpoint instead of this.type as id prefix so + // we don't double count in the event of add-on changes id: `endpoint-${agentId}-${timestamp}`, usage_timestamp: timestampStr, creation_timestamp: timestampStr, usage: { - type: 'security_solution_endpoint', + // type postfix is used to determine the PLI to bill + type: `security_solution_${this.type}`, period_seconds: SAMPLE_PERIOD_SECONDS, quantity: 1, }, @@ -126,15 +127,44 @@ export class EndpointMeteringService { }; } + private setType(config: ServerlessSecurityConfig) { + if (this.type) { + return; + } + + let hasCloudAddOn = false; + let hasEndpointAddOn = false; + config.productTypes.forEach((productType) => { + if (productType.product_line === ProductLine.cloud) { + hasCloudAddOn = true; + } + if (productType.product_line === ProductLine.endpoint) { + hasEndpointAddOn = true; + } + }); + + if (hasEndpointAddOn) { + this.type = ProductLine.endpoint; + return; + } + if (hasCloudAddOn) { + this.type = `${ProductLine.cloud}_${ProductLine.endpoint}`; + } + } + private setTier(config: ServerlessSecurityConfig) { if (this.tier) { return; } - const endpoint = config.productTypes.find( - (productType) => productType.product_line === ProductLine.endpoint + const product = config.productTypes.find( + (productType) => + // tiers are always matching so either is fine + productType.product_line === ProductLine.endpoint || + productType.product_line === ProductLine.cloud ); - this.tier = endpoint?.product_tier; + // default essentials is safe since we only reach tier if add-on exists + this.tier = product?.product_tier || ProductTier.essentials; } }