From 471bc77ccaa96197c5062d2d0e33c69c10cccfe2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cau=C3=AA=20Marcondes?= <55978943+cauemarcondes@users.noreply.github.com> Date: Tue, 14 May 2024 14:39:30 +0100 Subject: [PATCH] [APM] multi signal inventory summary metrics (#182960) closes https://github.com/elastic/kibana/issues/181719 Payload example: ``` { "services": { "services": [ { "asset": { "signalTypes": { "asset.traces": true, "asset.logs": true }, "identifyingMetadata": [ "service.name" ] }, "service": { "name": "synth-node-0", "environment": "Synthtrace: traces_logs_assets" }, "metrics": { "latency": 1000000, "throughput": 0.001388888904963992, "transactionErrorRate": 0.5, "logRatePerMinute": 0.002777777809927984, "logErrorRate": null } }, { "asset": { "signalTypes": { "asset.traces": true }, "identifyingMetadata": [ "service.name" ] }, "service": { "name": "synth-node-1", "environment": "Synthtrace: traces_logs_assets" }, "metrics": { "latency": 1000000, "throughput": 0.001388888904963992, "transactionErrorRate": 0.5 } }, { "asset": { "signalTypes": { "asset.traces": true }, "identifyingMetadata": [ "service.name" ] }, "service": { "name": "synth-node-2", "environment": "Synthtrace: traces_logs_assets" }, "metrics": { "latency": 1000000, "throughput": 0.001388888904963992, "transactionErrorRate": 0.5 } }, { "asset": { "signalTypes": { "asset.logs": true }, "identifyingMetadata": [ "service.name" ] }, "service": { "name": "synth-java" }, "metrics": { "logRatePerMinute": 0.002083333357445988, "logErrorRate": 0.3333333333333333 } } ] } } ``` --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com> Co-authored-by: Kate Patticha --- .../observability_solution/apm/kibana.jsonc | 3 +- .../assets/services/get_service_assets.ts | 59 ++++++++- .../get_services_transaction_stats.ts | 120 ++++++++++++++++++ .../server/routes/assets/services/routes.ts | 30 ++++- .../server/routes/assets/services/types.ts | 18 ++- .../get_service_names_per_signal_type.test.ts | 104 +++++++++++++++ .../get_service_names_per_signal_type.ts | 20 +++ .../apm/server/types.ts | 6 + .../observability_solution/apm/tsconfig.json | 1 + .../logs_data_access/server/index.ts | 5 + .../services/get_logs_rates_service/index.ts | 16 ++- .../asset_services/asset_services.spec.ts | 78 ++++++++++-- 12 files changed, 433 insertions(+), 27 deletions(-) create mode 100644 x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_services_transaction_stats.ts create mode 100644 x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.test.ts create mode 100644 x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.ts diff --git a/x-pack/plugins/observability_solution/apm/kibana.jsonc b/x-pack/plugins/observability_solution/apm/kibana.jsonc index e549a6b18883cd..aa87258afbe367 100644 --- a/x-pack/plugins/observability_solution/apm/kibana.jsonc +++ b/x-pack/plugins/observability_solution/apm/kibana.jsonc @@ -29,7 +29,8 @@ "dataViews", "lens", "maps", - "uiActions" + "uiActions", + "logsDataAccess" ], "optionalPlugins": [ "actions", diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_service_assets.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_service_assets.ts index a467ffd3f71d4a..524a20e07e00c9 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_service_assets.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_service_assets.ts @@ -4,13 +4,19 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ -import { Logger } from '@kbn/core/server'; import { errors } from '@elastic/elasticsearch'; +import { ElasticsearchClient, Logger } from '@kbn/core/server'; +import { LogsDataAccessPluginStart } from '@kbn/logs-data-access-plugin/server'; import { WrappedElasticsearchClientError } from '@kbn/observability-plugin/server'; +import { ApmServiceTransactionDocumentType } from '../../../../common/document_type'; +import { RollupInterval } from '../../../../common/rollup'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; import { AssetsESClient } from '../../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients'; import { withApmSpan } from '../../../utils/with_apm_span'; -import { ServiceAssetDocument } from './types'; import { getAssets } from '../get_assets'; +import { getServiceNamesPerSignalType } from '../utils/get_service_names_per_signal_type'; +import { getServicesTransactionStats } from './get_services_transaction_stats'; +import { ServiceAssetDocument } from './types'; export const MAX_NUMBER_OF_SERVICES = 1_000; @@ -20,12 +26,24 @@ export async function getServiceAssets({ end, kuery, logger, + apmEventClient, + logsDataAccessStart, + esClient, + documentType, + rollupInterval, + useDurationSummary, }: { assetsESClient: AssetsESClient; start: number; end: number; kuery: string; logger: Logger; + apmEventClient: APMEventClient; + logsDataAccessStart: LogsDataAccessPluginStart; + esClient: ElasticsearchClient; + documentType: ApmServiceTransactionDocumentType; + rollupInterval: RollupInterval; + useDurationSummary: boolean; }) { return withApmSpan('get_service_assets', async () => { try { @@ -38,7 +56,7 @@ export async function getServiceAssets({ assetType: 'service', }); - return response.hits.hits.map((hit) => { + const services = response.hits.hits.map((hit) => { const serviceAsset = hit._source as ServiceAssetDocument; return { @@ -52,6 +70,41 @@ export async function getServiceAssets({ }, }; }); + + const { logsServiceNames, tracesServiceNames } = getServiceNamesPerSignalType(services); + + const [traceMetrics = {}, logsMetrics = {}] = await Promise.all([ + tracesServiceNames.length + ? getServicesTransactionStats({ + apmEventClient, + start, + end, + kuery, + serviceNames: tracesServiceNames, + documentType, + rollupInterval, + useDurationSummary, + }) + : undefined, + logsServiceNames.length + ? logsDataAccessStart.services.getLogsRatesService({ + esClient, + identifyingMetadata: 'service.name', + timeFrom: start, + timeTo: end, + serviceNames: logsServiceNames, + }) + : undefined, + ]); + + return services.map((item) => { + const serviceTraceMetrics = traceMetrics[item.service.name]; + const serviceLogsMetrics = logsMetrics[item.service.name]; + return { + ...item, + metrics: { ...serviceTraceMetrics, ...serviceLogsMetrics }, + }; + }); } catch (error) { // If the index does not exist, handle it gracefully if ( diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_services_transaction_stats.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_services_transaction_stats.ts new file mode 100644 index 00000000000000..a4d8a45551de28 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/get_services_transaction_stats.ts @@ -0,0 +1,120 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server'; +import { ApmServiceTransactionDocumentType } from '../../../../common/document_type'; +import { SERVICE_NAME, TRANSACTION_TYPE } from '../../../../common/es_fields/apm'; +import { RollupInterval } from '../../../../common/rollup'; +import { isDefaultTransactionType } from '../../../../common/transaction_types'; +import { maybe } from '../../../../common/utils/maybe'; +import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput'; +import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client'; +import { getDurationFieldForTransactions } from '../../../lib/helpers/transactions'; +import { + calculateFailedTransactionRate, + getOutcomeAggregation, +} from '../../../lib/helpers/transaction_error_rate'; +import { MAX_NUMBER_OF_SERVICES } from './get_service_assets'; + +export interface TraceMetrics { + latency: number | null; + throughput: number | null; + transactionErrorRate: number | null; +} + +interface AssetServicesMetricsMap { + [serviceName: string]: TraceMetrics; +} + +export async function getServicesTransactionStats({ + apmEventClient, + start, + end, + kuery, + serviceNames, + documentType, + rollupInterval, + useDurationSummary, +}: { + apmEventClient: APMEventClient; + start: number; + end: number; + kuery: string; + serviceNames: string[]; + documentType: ApmServiceTransactionDocumentType; + rollupInterval: RollupInterval; + useDurationSummary: boolean; +}): Promise { + const response = await apmEventClient.search('get_services_transaction_stats', { + apm: { + sources: [{ documentType, rollupInterval }], + }, + body: { + track_total_hits: false, + size: 0, + query: { + bool: { + filter: [ + ...rangeQuery(start, end), + ...kqlQuery(kuery), + { terms: { [SERVICE_NAME]: serviceNames } }, + ], + }, + }, + aggs: { + services: { + terms: { + field: SERVICE_NAME, + size: MAX_NUMBER_OF_SERVICES, + }, + aggs: { + transactionType: { + terms: { + field: TRANSACTION_TYPE, + }, + aggs: { + avg_duration: { + avg: { + field: getDurationFieldForTransactions(documentType, useDurationSummary), + }, + }, + ...getOutcomeAggregation(documentType), + }, + }, + }, + }, + }, + }, + }); + + return ( + response.aggregations?.services.buckets.reduce((acc, bucket) => { + const serviceName = bucket.key as string; + const topTransactionTypeBucket = maybe( + bucket.transactionType.buckets.find(({ key }) => isDefaultTransactionType(key as string)) ?? + bucket.transactionType.buckets[0] + ); + + return { + ...acc, + [serviceName]: { + latency: topTransactionTypeBucket?.avg_duration.value || null, + throughput: topTransactionTypeBucket + ? calculateThroughputWithRange({ + start, + end, + value: topTransactionTypeBucket.doc_count, + }) + : null, + transactionErrorRate: topTransactionTypeBucket + ? calculateFailedTransactionRate(topTransactionTypeBucket) + : null, + }, + }; + }, {}) || {} + ); +} diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/routes.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/routes.ts index 876ff495eae17d..ff79cd80e1f47f 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/routes.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/routes.ts @@ -5,28 +5,39 @@ * 2.0. */ import * as t from 'io-ts'; -import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; +import { toBooleanRt } from '@kbn/io-ts-utils'; import { createAssetsESClient } from '../../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients'; +import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client'; +import { createApmServerRoute } from '../../apm_routes/create_apm_server_route'; +import { kueryRt, rangeRt, serviceTransactionDataSourceRt } from '../../default_api_types'; import { getServiceAssets } from './get_service_assets'; -import { kueryRt, rangeRt } from '../../default_api_types'; import { AssetServicesResponse } from './types'; const servicesAssetsRoute = createApmServerRoute({ endpoint: 'GET /internal/apm/assets/services', params: t.type({ - query: t.intersection([kueryRt, rangeRt]), + query: t.intersection([ + kueryRt, + rangeRt, + serviceTransactionDataSourceRt, + t.type({ useDurationSummary: toBooleanRt }), + ]), }), options: { tags: ['access:apm'] }, async handler(resources): Promise { - const { context, params, request } = resources; - const coreContext = await context.core; + const { context, params, request, plugins } = resources; + const [coreContext, apmEventClient, logsDataAccessStart] = await Promise.all([ + context.core, + getApmEventClient(resources), + plugins.logsDataAccess.start(), + ]); const assetsESClient = await createAssetsESClient({ request, esClient: coreContext.elasticsearch.client.asCurrentUser, }); - const { start, end, kuery } = params.query; + const { start, end, kuery, documentType, rollupInterval, useDurationSummary } = params.query; const services = await getServiceAssets({ assetsESClient, @@ -34,7 +45,14 @@ const servicesAssetsRoute = createApmServerRoute({ end, kuery, logger: resources.logger, + apmEventClient, + logsDataAccessStart, + esClient: coreContext.elasticsearch.client.asCurrentUser, + documentType, + rollupInterval, + useDurationSummary, }); + return { services }; }, }); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/types.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/types.ts index 14fb8a012239c1..e63c12f27a1726 100644 --- a/x-pack/plugins/observability_solution/apm/server/routes/assets/services/types.ts +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/services/types.ts @@ -5,9 +5,12 @@ * 2.0. */ -export interface SignalTypes { - 'asset.trace'?: boolean; - 'asset.logs'?: boolean; +import { LogsRatesMetrics } from '@kbn/logs-data-access-plugin/server'; +import { TraceMetrics } from './get_services_transaction_stats'; + +export enum SignalType { + ASSET_TRACES = 'asset.traces', + ASSET_LOGS = 'asset.logs', } interface ServiceItem { @@ -15,6 +18,8 @@ interface ServiceItem { name: string; } +type SignalTypes = Record; + interface AssetItem { signalTypes: SignalTypes; identifyingMetadata: string[]; @@ -28,6 +33,11 @@ export interface ServiceAssetDocument { service: ServiceItem; } +export interface AssetService { + asset: AssetItem; + service: ServiceItem; +} + export interface AssetServicesResponse { - services: Array<{ asset: AssetItem; service: ServiceItem }>; + services: Array; } diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.test.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.test.ts new file mode 100644 index 00000000000000..89c67769bf47a6 --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.test.ts @@ -0,0 +1,104 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AssetService } from '../services/types'; +import { getServiceNamesPerSignalType } from './get_service_names_per_signal_type'; + +describe('getServiceNamesPerSignalType', () => { + it('returns empty arrays when serviceAssets is empty', () => { + const serviceAssets: AssetService[] = []; + const result = getServiceNamesPerSignalType(serviceAssets); + expect(result).toEqual({ tracesServiceNames: [], logsServiceNames: [] }); + }); + + it('returns service names for assets with traces signal types', () => { + const serviceAssets: AssetService[] = [ + { + asset: { + signalTypes: { 'asset.traces': true, 'asset.logs': false }, + identifyingMetadata: [], + }, + service: { name: 'Service1' }, + }, + { + asset: { + signalTypes: { 'asset.traces': false, 'asset.logs': false }, + identifyingMetadata: [], + }, + service: { name: 'Service2' }, + }, + ]; + const result = getServiceNamesPerSignalType(serviceAssets); + expect(result).toEqual({ tracesServiceNames: ['Service1'], logsServiceNames: [] }); + }); + + it('returns service names for assets with logs signal types', () => { + const serviceAssets: AssetService[] = [ + { + asset: { + signalTypes: { 'asset.logs': true, 'asset.traces': false }, + identifyingMetadata: [], + }, + service: { name: 'Service1' }, + }, + { + asset: { + signalTypes: { 'asset.logs': false, 'asset.traces': false }, + identifyingMetadata: [], + }, + service: { name: 'Service2' }, + }, + ]; + const result = getServiceNamesPerSignalType(serviceAssets); + expect(result).toEqual({ tracesServiceNames: [], logsServiceNames: ['Service1'] }); + }); + + it('returns empty arrays when there are no assets with signal types', () => { + const serviceAssets: AssetService[] = [ + { + asset: { + signalTypes: { 'asset.logs': false, 'asset.traces': false }, + identifyingMetadata: [], + }, + service: { name: 'Service1' }, + }, + { + asset: { + signalTypes: { 'asset.logs': false, 'asset.traces': false }, + identifyingMetadata: [], + }, + service: { name: 'Service2' }, + }, + ]; + const result = getServiceNamesPerSignalType(serviceAssets); + expect(result).toEqual({ tracesServiceNames: [], logsServiceNames: [] }); + }); + + it('returns service names for assets with logs and traces signal types', () => { + const serviceAssets: AssetService[] = [ + { + asset: { + signalTypes: { 'asset.logs': true, 'asset.traces': true }, + identifyingMetadata: [], + }, + service: { name: 'Service1' }, + }, + { + asset: { + signalTypes: { 'asset.logs': true, 'asset.traces': true }, + identifyingMetadata: [], + }, + service: { name: 'Service2' }, + }, + ]; + const result = getServiceNamesPerSignalType(serviceAssets); + expect(result).toEqual({ + tracesServiceNames: ['Service1', 'Service2'], + logsServiceNames: ['Service1', 'Service2'], + }); + }); +}); diff --git a/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.ts b/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.ts new file mode 100644 index 00000000000000..94efeb96a3c1bb --- /dev/null +++ b/x-pack/plugins/observability_solution/apm/server/routes/assets/utils/get_service_names_per_signal_type.ts @@ -0,0 +1,20 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import { AssetService, SignalType } from '../services/types'; + +export function getServiceNamesPerSignalType(serviceAssets: AssetService[]) { + const tracesServiceNames = serviceAssets + .filter(({ asset }) => asset.signalTypes[SignalType.ASSET_TRACES]) + .map(({ service }) => service.name); + + const logsServiceNames = serviceAssets + .filter(({ asset }) => asset.signalTypes[SignalType.ASSET_LOGS]) + .map(({ service }) => service.name); + + return { tracesServiceNames, logsServiceNames }; +} diff --git a/x-pack/plugins/observability_solution/apm/server/types.ts b/x-pack/plugins/observability_solution/apm/server/types.ts index 7c91ecf2fd85e9..1aeb6defe9a57d 100644 --- a/x-pack/plugins/observability_solution/apm/server/types.ts +++ b/x-pack/plugins/observability_solution/apm/server/types.ts @@ -53,6 +53,10 @@ import { ProfilingDataAccessPluginSetup, ProfilingDataAccessPluginStart, } from '@kbn/profiling-data-access-plugin/server'; +import { + LogsDataAccessPluginSetup, + LogsDataAccessPluginStart, +} from '@kbn/logs-data-access-plugin/server'; import type { ObservabilityAIAssistantServerSetup, ObservabilityAIAssistantServerStart, @@ -88,6 +92,7 @@ export interface APMPluginSetupDependencies { usageCollection?: UsageCollectionSetup; customIntegrations?: CustomIntegrationsPluginSetup; profilingDataAccess?: ProfilingDataAccessPluginSetup; + logsDataAccess: LogsDataAccessPluginSetup; } export interface APMPluginStartDependencies { // required dependencies @@ -114,4 +119,5 @@ export interface APMPluginStartDependencies { usageCollection?: undefined; customIntegrations?: CustomIntegrationsPluginStart; profilingDataAccess?: ProfilingDataAccessPluginStart; + logsDataAccess: LogsDataAccessPluginStart; } diff --git a/x-pack/plugins/observability_solution/apm/tsconfig.json b/x-pack/plugins/observability_solution/apm/tsconfig.json index 4d6b30cbcb6c1b..4a43f0821c700c 100644 --- a/x-pack/plugins/observability_solution/apm/tsconfig.json +++ b/x-pack/plugins/observability_solution/apm/tsconfig.json @@ -116,6 +116,7 @@ "@kbn/react-kibana-context-theme", "@kbn/core-http-request-handler-context-server", "@kbn/search-types", + "@kbn/logs-data-access-plugin", "@kbn/ebt-tools", "@kbn/presentation-publishing" ], diff --git a/x-pack/plugins/observability_solution/logs_data_access/server/index.ts b/x-pack/plugins/observability_solution/logs_data_access/server/index.ts index ee394c191c276f..99ef11a0bd9531 100644 --- a/x-pack/plugins/observability_solution/logs_data_access/server/index.ts +++ b/x-pack/plugins/observability_solution/logs_data_access/server/index.ts @@ -9,6 +9,11 @@ import type { LogsDataAccessPluginSetup, LogsDataAccessPluginStart } from './plu export type { LogsDataAccessPluginSetup, LogsDataAccessPluginStart }; +export type { + LogsRatesMetrics, + LogsRatesServiceReturnType, +} from './services/get_logs_rates_service'; + export async function plugin(initializerContext: PluginInitializerContext) { const { LogsDataAccessPlugin } = await import('./plugin'); return new LogsDataAccessPlugin(initializerContext); diff --git a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rates_service/index.ts b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rates_service/index.ts index 1634270309689a..0ef186d16b1802 100644 --- a/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rates_service/index.ts +++ b/x-pack/plugins/observability_solution/logs_data_access/server/services/get_logs_rates_service/index.ts @@ -26,11 +26,13 @@ interface LogRateQueryAggregation { services: estypes.AggregationsTermsAggregateBase; } +export interface LogsRatesMetrics { + logRatePerMinute: number; + logErrorRate: null | number; +} + export interface LogsRatesServiceReturnType { - [serviceName: string]: { - logRatePerMinute: number; - logErrorRate: null | number; - }; + [serviceName: string]: LogsRatesMetrics; } export function createGetLogsRatesService(params: RegisterServicesParams) { @@ -47,6 +49,12 @@ export function createGetLogsRatesService(params: RegisterServicesParams) { query: { bool: { filter: [ + { + exists: { + // For now, we don't want to count APM server logs or any other logs that don't have the log.level field. + field: 'log.level', + }, + }, { terms: { [identifyingMetadata]: serviceNames, diff --git a/x-pack/test/apm_api_integration/tests/asset_services/asset_services.spec.ts b/x-pack/test/apm_api_integration/tests/asset_services/asset_services.spec.ts index 983c2c7ec7c2ff..64a202fe2ca8e0 100644 --- a/x-pack/test/apm_api_integration/tests/asset_services/asset_services.spec.ts +++ b/x-pack/test/apm_api_integration/tests/asset_services/asset_services.spec.ts @@ -21,15 +21,20 @@ import { APIReturnType, APIClientRequestParamsOf, } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api'; +import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type'; +import { RollupInterval } from '@kbn/apm-plugin/common/rollup'; import { FtrProviderContext } from '../../common/ftr_provider_context'; export default function ApiTest({ getService }: FtrProviderContext) { const registry = getService('registry'); const apmApiClient = getService('apmApiClient'); const assetsSynthtraceClient = getService('assetsSynthtraceEsClient'); + const apmSynthtraceClient = getService('apmSynthtraceEsClient'); + const logSynthtraceClient = getService('logSynthtraceEsClient'); - const start = new Date(moment().subtract(10, 'minutes').valueOf()).toISOString(); - const end = new Date(moment().valueOf()).toISOString(); + const now = new Date(); + const start = new Date(moment(now).subtract(10, 'minutes').valueOf()).toISOString(); + const end = new Date(moment(now).valueOf()).toISOString(); const range = timerange(start, end); async function getServiceAssets( @@ -44,6 +49,9 @@ export default function ApiTest({ getService }: FtrProviderContext) { start, end, kuery: '', + documentType: ApmDocumentType.TransactionEvent, + rollupInterval: RollupInterval.None, + useDurationSummary: false, ...overrides?.query, }, }, @@ -65,7 +73,7 @@ export default function ApiTest({ getService }: FtrProviderContext) { }); registry.when('Asset services when data is loaded', { config: 'basic', archives: [] }, () => { - before(async () => { + before(() => { const transactionName = '240rpm/75% 1000ms'; const successfulTimestamps = range.interval('1m').rate(1); @@ -167,16 +175,27 @@ export default function ApiTest({ getService }: FtrProviderContext) { function* createGeneratorFromArray(arr: Array>) { yield* arr; } + + const logsValuesArray = [...logEvents]; + const logsGen = createGeneratorFromArray(logsValuesArray); + const logsGenAssets = createGeneratorFromArray(logsValuesArray); + const traces = instances.flatMap((instance) => instanceSpans(instance)); + const tracesGen = createGeneratorFromArray(traces); const tracesGenAssets = createGeneratorFromArray(traces); - // - return await assetsSynthtraceClient.index( - Readable.from(Array.from(logEvents).concat(Array.from(tracesGenAssets))) - ); + return Promise.all([ + assetsSynthtraceClient.index( + Readable.from(Array.from(logsGenAssets).concat(Array.from(tracesGenAssets))) + ), + logSynthtraceClient.index(logsGen), + apmSynthtraceClient.index(tracesGen), + ]); }); after(async () => { + await logSynthtraceClient.clean(); + await apmSynthtraceClient.clean(); await assetsSynthtraceClient.clean(); }); @@ -215,6 +234,42 @@ export default function ApiTest({ getService }: FtrProviderContext) { expect(logsOnly?.asset.signalTypes).to.eql({ 'asset.logs': true }); expect(logsOnly?.service.environment).not.to.be('testing'); }); + + it('return traces and logs metrics for services when multi-signals', () => { + const multiSignalService = response.body.services.find( + (item) => item.service.name === 'multisignal-service' + ); + + expect(multiSignalService?.metrics.latency).to.be(1000 * 1000); // microseconds + expect(multiSignalService?.metrics.throughput).to.be(2); + expect(multiSignalService?.metrics.transactionErrorRate).to.be(0.5); + expect(multiSignalService?.metrics.logRatePerMinute).to.be(1); + expect(multiSignalService?.metrics.logErrorRate).to.be(1); + }); + + it('return traces only metrics', () => { + const apmService = response.body.services.find( + (item) => item.service.name === 'apm-only-service' + ); + + expect(apmService?.metrics.latency).to.be(1000 * 1000); // microseconds + expect(apmService?.metrics.throughput).to.be(2); + expect(apmService?.metrics.transactionErrorRate).to.be(0.5); + expect(apmService?.metrics.logRatePerMinute).to.be(undefined); + expect(apmService?.metrics.logErrorRate).to.be(undefined); + }); + + it('return logs only metrics', () => { + const logsService = response.body.services.find( + (item) => item.service.name === 'logs-only-service' + ); + + expect(logsService?.metrics.latency).to.be(undefined); + expect(logsService?.metrics.throughput).to.be(undefined); + expect(logsService?.metrics.transactionErrorRate).to.be(undefined); + expect(logsService?.metrics.logRatePerMinute).to.be(1); + expect(logsService?.metrics.logErrorRate).to.be(1); + }); }); describe('when additional filters are applied', () => { @@ -229,8 +284,8 @@ export default function ApiTest({ getService }: FtrProviderContext) { it('returns services when the time range is within the data range', async () => { response = await getServiceAssets({ query: { - start: new Date(moment().subtract(2, 'days').valueOf()).toISOString(), - end: new Date(moment().add(1, 'days').valueOf()).toISOString(), + start: new Date(moment(now).subtract(2, 'days').valueOf()).toISOString(), + end: new Date(moment(now).add(1, 'days').valueOf()).toISOString(), }, }); @@ -246,6 +301,11 @@ export default function ApiTest({ getService }: FtrProviderContext) { const service = response.body.services[0]; expect(service.service.name).to.be('logs-only-service'); expect(service.asset.signalTypes['asset.logs']).to.be(true); + expect(service.metrics.latency).to.be(undefined); + expect(service.metrics.throughput).to.be(undefined); + expect(service.metrics.transactionErrorRate).to.be(undefined); + expect(service.metrics.logRatePerMinute).to.be(1); + expect(service.metrics.logErrorRate).to.be(1); }); it('returns not services when filtering by a field that does not exist in assets', async () => {