Skip to content

Commit

Permalink
[APM] multi signal inventory summary metrics (elastic#182960)
Browse files Browse the repository at this point in the history
closes elastic#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 <aikaterini.patticha@elastic.co>
  • Loading branch information
3 people authored May 14, 2024
1 parent 1a61313 commit 471bc77
Show file tree
Hide file tree
Showing 12 changed files with 433 additions and 27 deletions.
3 changes: 2 additions & 1 deletion x-pack/plugins/observability_solution/apm/kibana.jsonc
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@
"dataViews",
"lens",
"maps",
"uiActions"
"uiActions",
"logsDataAccess"
],
"optionalPlugins": [
"actions",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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 {
Expand All @@ -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 {
Expand All @@ -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 (
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AssetServicesMetricsMap> {
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<AssetServicesMetricsMap>((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,
},
};
}, {}) || {}
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,36 +5,54 @@
* 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<AssetServicesResponse> {
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,
start,
end,
kuery,
logger: resources.logger,
apmEventClient,
logsDataAccessStart,
esClient: coreContext.elasticsearch.client.asCurrentUser,
documentType,
rollupInterval,
useDurationSummary,
});

return { services };
},
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,21 @@
* 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 {
environment?: string;
name: string;
}

type SignalTypes = Record<SignalType, boolean | undefined>;

interface AssetItem {
signalTypes: SignalTypes;
identifyingMetadata: string[];
Expand All @@ -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<AssetService & { metrics: TraceMetrics & LogsRatesMetrics }>;
}
Loading

0 comments on commit 471bc77

Please sign in to comment.