mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 17:59:23 -04:00
[APM] Add sparklines to the multi-signal view table (#187782)
Closes #187567 ## Summary This PR adds sparklines to the multi-signal view table  ## Testing 1. Enable `observability:apmEnableMultiSignal` in advanced settings <details> <summary>2. Run the entities definition in the dev tools</summary> ``` POST kbn:/internal/api/entities/definition { "id": "apm-services-with-metadata", "name": "Services from logs and metrics", "displayNameTemplate": "test", "history": { "timestampField": "@timestamp", "interval": "5m" }, "type": "service", "indexPatterns": [ "logs-*", "metrics-*" ], "timestampField": "@timestamp", "lookback": "5m", "identityFields": [ { "field": "service.name", "optional": false }, { "field": "service.environment", "optional": true } ], "identityTemplate": "{{service.name}}:{{service.environment}}", "metadata": [ "tags", "host.name", "data_stream.type", "service.name", "service.instance.id", "service.namespace", "service.environment", "service.version", "service.runtime.name", "service.runtime.version", "service.node.name", "service.language.name", "agent.name", "cloud.provider", "cloud.instance.id", "cloud.availability_zone", "cloud.instance.name", "cloud.machine.type", "container.id" ], "metrics": [ { "name": "latency", "equation": "A", "metrics": [ { "name": "A", "aggregation": "avg", "field": "transaction.duration.histogram" } ] }, { "name": "throughput", "equation": "A / 5", "metrics": [ { "name": "A", "aggregation": "doc_count", "filter": "transaction.duration.histogram:*" } ] }, { "name": "failedTransactionRate", "equation": "A / B", "metrics": [ { "name": "A", "aggregation": "doc_count", "filter": "event.outcome: \"failure\"" }, { "name": "B", "aggregation": "doc_count", "filter": "event.outcome: *" } ] }, { "name": "logErrorRate", "equation": "A / B", "metrics": [ { "name": "A", "aggregation": "doc_count", "filter": "log.level: \"error\"" }, { "name": "B", "aggregation": "doc_count", "filter": "log.level: *" } ] }, { "name": "logRatePerMinute", "equation": "A / 5", "metrics": [ { "name": "A", "aggregation": "doc_count", "filter": "log.level: \"error\"" } ] } ] } ``` </details> 3. Generate data with synthrace 1. logs only: `node scripts/synthtrace simple_logs.ts` 2. APM only: `node scripts/synthtrace simple_trace.ts` 4. Open services inventory - the sparklines should be visible next to the values in the table (big screen only like in the services table) <img width="1920" alt="image" src="698de74c
-0d54-4f70-9802-5b80bfc74511"> - on small screens, the sparklines should not be visible <img width="989" alt="image" src="4bef372d
-7b1c-4e50-a3e2-d11ec0df5bc1">
This commit is contained in:
parent
57be31c8cb
commit
35b5fcc4c4
7 changed files with 421 additions and 27 deletions
|
@ -8,9 +8,10 @@ import { EuiFlexItem, EuiFlexGroup } from '@elastic/eui';
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { ApmDocumentType } from '../../../../../common/document_type';
|
||||
import { APIReturnType } from '../../../../services/rest/create_call_apm_api';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import { EmptyMessage } from '../../../shared/empty_message';
|
||||
import { SearchBar } from '../../../shared/search_bar/search_bar';
|
||||
|
@ -22,6 +23,9 @@ import {
|
|||
MultiSignalServicesTable,
|
||||
ServiceInventoryFieldName,
|
||||
} from './table/multi_signal_services_table';
|
||||
import { ServiceListItem } from '../../../../../common/service_inventory';
|
||||
import { usePreferredDataSourceAndBucketSize } from '../../../../hooks/use_preferred_data_source_and_bucket_size';
|
||||
import { useProgressiveFetcher } from '../../../../hooks/use_progressive_fetcher';
|
||||
|
||||
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;
|
||||
|
||||
|
@ -74,9 +78,72 @@ function useServicesEntitiesMainStatisticsFetcher() {
|
|||
return { mainStatisticsData: data, mainStatisticsStatus: status };
|
||||
}
|
||||
|
||||
export const MultiSignalInventory = () => {
|
||||
function useServicesEntitiesDetailedStatisticsFetcher({
|
||||
mainStatisticsFetch,
|
||||
services,
|
||||
}: {
|
||||
mainStatisticsFetch: ReturnType<typeof useServicesEntitiesMainStatisticsFetcher>;
|
||||
services: ServiceListItem[];
|
||||
}) {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo, environment, kuery },
|
||||
} = useApmParams('/services');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const dataSourceOptions = usePreferredDataSourceAndBucketSize({
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
type: ApmDocumentType.ServiceTransactionMetric,
|
||||
numBuckets: 20,
|
||||
});
|
||||
|
||||
const { mainStatisticsData, mainStatisticsStatus } = mainStatisticsFetch;
|
||||
|
||||
const timeseriesDataFetch = useProgressiveFetcher(
|
||||
(callApmApi) => {
|
||||
const serviceNames = services.map(({ serviceName }) => serviceName);
|
||||
|
||||
if (
|
||||
start &&
|
||||
end &&
|
||||
serviceNames.length > 0 &&
|
||||
mainStatisticsStatus === FETCH_STATUS.SUCCESS &&
|
||||
dataSourceOptions
|
||||
) {
|
||||
return callApmApi('POST /internal/apm/entities/services/detailed_statistics', {
|
||||
params: {
|
||||
query: {
|
||||
environment,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
documentType: dataSourceOptions.source.documentType,
|
||||
rollupInterval: dataSourceOptions.source.rollupInterval,
|
||||
bucketSizeInSeconds: dataSourceOptions.bucketSizeInSeconds,
|
||||
},
|
||||
body: {
|
||||
// Service name is sorted to guarantee the same order every time this API is called so the result can be cached.
|
||||
serviceNames: JSON.stringify(serviceNames.sort()),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
// only fetches detailed statistics when requestId is invalidated by main statistics api call or offset is changed
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[mainStatisticsData.requestId, services],
|
||||
{ preservePreviousData: false }
|
||||
);
|
||||
|
||||
return { timeseriesDataFetch };
|
||||
}
|
||||
|
||||
export function MultiSignalInventory() {
|
||||
const [searchQuery, setSearchQuery] = React.useState('');
|
||||
const { mainStatisticsData, mainStatisticsStatus } = useServicesEntitiesMainStatisticsFetcher();
|
||||
const mainStatisticsFetch = useServicesEntitiesMainStatisticsFetcher();
|
||||
|
||||
const initialSortField = ServiceInventoryFieldName.Throughput;
|
||||
|
||||
|
@ -86,6 +153,11 @@ export const MultiSignalInventory = () => {
|
|||
fieldsToSearch: [ServiceInventoryFieldName.ServiceName],
|
||||
});
|
||||
|
||||
const { timeseriesDataFetch } = useServicesEntitiesDetailedStatisticsFetcher({
|
||||
mainStatisticsFetch,
|
||||
services: mainStatisticsData.services,
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiFlexGroup gutterSize="m">
|
||||
|
@ -110,6 +182,8 @@ export const MultiSignalInventory = () => {
|
|||
initialSortField={initialSortField}
|
||||
initialPageSize={INITIAL_PAGE_SIZE}
|
||||
initialSortDirection={INITIAL_SORT_DIRECTION}
|
||||
timeseriesData={timeseriesDataFetch?.data}
|
||||
timeseriesDataLoading={timeseriesDataFetch.status === FETCH_STATUS.LOADING}
|
||||
noItemsMessage={
|
||||
<EmptyMessage
|
||||
heading={i18n.translate('xpack.apm.servicesTable.notFoundLabel', {
|
||||
|
@ -122,4 +196,4 @@ export const MultiSignalInventory = () => {
|
|||
</EuiFlexGroup>
|
||||
</>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
|
|
@ -31,15 +31,26 @@ import { TruncateWithTooltip } from '../../../../shared/truncate_with_tooltip';
|
|||
import { ServiceInventoryFieldName } from './multi_signal_services_table';
|
||||
import { EntityServiceListItem, SignalTypes } from '../../../../../../common/entities/types';
|
||||
import { isApmSignal } from '../../../../../utils/get_signal_type';
|
||||
import { APIReturnType } from '../../../../../services/rest/create_call_apm_api';
|
||||
|
||||
type ServicesDetailedStatisticsAPIResponse =
|
||||
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;
|
||||
|
||||
export function getServiceColumns({
|
||||
query,
|
||||
breakpoints,
|
||||
link,
|
||||
timeseriesDataLoading,
|
||||
timeseriesData,
|
||||
}: {
|
||||
query: TypeOf<ApmRoutes, '/services'>['query'];
|
||||
breakpoints: Breakpoints;
|
||||
link: any;
|
||||
timeseriesDataLoading: boolean;
|
||||
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
|
||||
}): Array<ITableColumn<EntityServiceListItem>> {
|
||||
const { isSmall, isLarge } = breakpoints;
|
||||
const showWhenSmallOrGreaterThanLarge = isSmall || !isLarge;
|
||||
return [
|
||||
{
|
||||
field: ServiceInventoryFieldName.ServiceName,
|
||||
|
@ -90,17 +101,18 @@ export function getServiceColumns({
|
|||
sortable: true,
|
||||
dataType: 'number',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { metrics, signalTypes }) => {
|
||||
render: (_, { metrics, serviceName, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LATENCY_AVG);
|
||||
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
isLoading={false}
|
||||
isLoading={timeseriesDataLoading}
|
||||
series={timeseriesData?.currentPeriod?.apm[serviceName]?.latency}
|
||||
color={currentPeriodColor}
|
||||
hideSeries
|
||||
valueLabel={asMillisecondDuration(metrics.latency)}
|
||||
hideSeries={!showWhenSmallOrGreaterThanLarge}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -113,17 +125,18 @@ export function getServiceColumns({
|
|||
sortable: true,
|
||||
dataType: 'number',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { metrics, signalTypes }) => {
|
||||
render: (_, { metrics, serviceName, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.THROUGHPUT);
|
||||
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
isLoading={false}
|
||||
color={currentPeriodColor}
|
||||
hideSeries
|
||||
valueLabel={asTransactionRate(metrics.throughput)}
|
||||
isLoading={timeseriesDataLoading}
|
||||
series={timeseriesData?.currentPeriod?.apm[serviceName]?.throughput}
|
||||
hideSeries={!showWhenSmallOrGreaterThanLarge}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -136,17 +149,18 @@ export function getServiceColumns({
|
|||
sortable: true,
|
||||
dataType: 'number',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { metrics, signalTypes }) => {
|
||||
render: (_, { metrics, serviceName, signalTypes }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.FAILED_TRANSACTION_RATE);
|
||||
|
||||
return !isApmSignal(signalTypes) ? (
|
||||
<NotAvailableApmMetrics />
|
||||
) : (
|
||||
<ListMetric
|
||||
isLoading={false}
|
||||
color={currentPeriodColor}
|
||||
hideSeries
|
||||
valueLabel={asPercent(metrics.failedTransactionRate, 1)}
|
||||
isLoading={timeseriesDataLoading}
|
||||
series={timeseriesData?.currentPeriod?.apm[serviceName]?.transactionErrorRate}
|
||||
hideSeries={!showWhenSmallOrGreaterThanLarge}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -159,15 +173,16 @@ export function getServiceColumns({
|
|||
sortable: true,
|
||||
dataType: 'number',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { metrics }) => {
|
||||
render: (_, { metrics, serviceName }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_RATE);
|
||||
|
||||
return (
|
||||
<ListMetric
|
||||
isLoading={false}
|
||||
color={currentPeriodColor}
|
||||
hideSeries
|
||||
series={timeseriesData?.currentPeriod?.logRate[serviceName] ?? []}
|
||||
valueLabel={asDecimalOrInteger(metrics.logRatePerMinute)}
|
||||
hideSeries={!showWhenSmallOrGreaterThanLarge}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
@ -180,15 +195,16 @@ export function getServiceColumns({
|
|||
sortable: true,
|
||||
dataType: 'number',
|
||||
align: RIGHT_ALIGNMENT,
|
||||
render: (_, { metrics }) => {
|
||||
render: (_, { metrics, serviceName }) => {
|
||||
const { currentPeriodColor } = getTimeSeriesColor(ChartType.LOG_ERROR_RATE);
|
||||
|
||||
return (
|
||||
<ListMetric
|
||||
isLoading={false}
|
||||
color={currentPeriodColor}
|
||||
hideSeries
|
||||
series={timeseriesData?.currentPeriod?.logErrorRate[serviceName] ?? []}
|
||||
valueLabel={asPercent(metrics.logErrorRate, 1)}
|
||||
hideSeries={!showWhenSmallOrGreaterThanLarge}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
|
|
@ -17,6 +17,8 @@ import { ManagedTable } from '../../../../shared/managed_table';
|
|||
import { getServiceColumns } from './get_service_columns';
|
||||
|
||||
type MainStatisticsApiResponse = APIReturnType<'GET /internal/apm/entities/services'>;
|
||||
type ServicesDetailedStatisticsAPIResponse =
|
||||
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;
|
||||
|
||||
export enum ServiceInventoryFieldName {
|
||||
ServiceName = 'serviceName',
|
||||
|
@ -35,6 +37,8 @@ interface Props {
|
|||
initialSortDirection: 'asc' | 'desc';
|
||||
noItemsMessage: React.ReactNode;
|
||||
data: MainStatisticsApiResponse['services'];
|
||||
timeseriesDataLoading: boolean;
|
||||
timeseriesData?: ServicesDetailedStatisticsAPIResponse;
|
||||
}
|
||||
|
||||
export function MultiSignalServicesTable({
|
||||
|
@ -44,6 +48,8 @@ export function MultiSignalServicesTable({
|
|||
initialPageSize,
|
||||
initialSortDirection,
|
||||
noItemsMessage,
|
||||
timeseriesDataLoading,
|
||||
timeseriesData,
|
||||
}: Props) {
|
||||
const breakpoints = useBreakpoints();
|
||||
const { query } = useApmParams('/services');
|
||||
|
@ -55,8 +61,10 @@ export function MultiSignalServicesTable({
|
|||
query: omit(query, 'page', 'pageSize', 'sortDirection', 'sortField'),
|
||||
breakpoints,
|
||||
link,
|
||||
timeseriesDataLoading,
|
||||
timeseriesData,
|
||||
});
|
||||
}, [query, link, breakpoints]);
|
||||
}, [query, breakpoints, link, timeseriesDataLoading, timeseriesData]);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup gutterSize="xs" direction="column" responsive={false}>
|
||||
|
|
|
@ -4,12 +4,24 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import Boom from '@hapi/boom';
|
||||
import { toNumberRt, jsonRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { offsetRt } from '../../../../common/comparison_rt';
|
||||
import { getApmEventClient } from '../../../lib/helpers/get_apm_event_client';
|
||||
import { getRandomSampler } from '../../../lib/helpers/get_random_sampler';
|
||||
import { EntityServiceListItem } from '../../../../common/entities/types';
|
||||
import { environmentQuery } from '../../../../common/utils/environment_query';
|
||||
import { createEntitiesESClient } from '../../../lib/helpers/create_es_client/create_assets_es_client/create_assets_es_clients';
|
||||
import { createApmServerRoute } from '../../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from '../../default_api_types';
|
||||
import {
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
probabilityRt,
|
||||
rangeRt,
|
||||
serviceTransactionDataSourceRt,
|
||||
} from '../../default_api_types';
|
||||
import { getServiceTransactionDetailedStatsPeriods } from '../../services/get_services_detailed_statistics/get_service_transaction_detailed_statistics';
|
||||
import { getServiceEntities } from './get_service_entities';
|
||||
|
||||
export interface EntityServicesResponse {
|
||||
|
@ -46,6 +58,99 @@ const servicesEntitiesRoute = createApmServerRoute({
|
|||
},
|
||||
});
|
||||
|
||||
const servicesEntitiesDetailedStatisticsRoute = createApmServerRoute({
|
||||
endpoint: 'POST /internal/apm/entities/services/detailed_statistics',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
t.intersection([offsetRt, probabilityRt, serviceTransactionDataSourceRt]),
|
||||
t.type({
|
||||
bucketSizeInSeconds: toNumberRt,
|
||||
}),
|
||||
]),
|
||||
body: t.type({ serviceNames: jsonRt.pipe(t.array(t.string)) }),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (resources) => {
|
||||
const {
|
||||
context,
|
||||
params,
|
||||
request,
|
||||
plugins: { security, logsDataAccess },
|
||||
} = resources;
|
||||
|
||||
const [coreContext, logsDataAccessStart] = await Promise.all([
|
||||
context.core,
|
||||
logsDataAccess.start(),
|
||||
]);
|
||||
|
||||
const {
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
start,
|
||||
end,
|
||||
probability,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
bucketSizeInSeconds,
|
||||
} = params.query;
|
||||
|
||||
const { serviceNames } = params.body;
|
||||
|
||||
if (!serviceNames.length) {
|
||||
throw Boom.badRequest(`serviceNames cannot be empty`);
|
||||
}
|
||||
|
||||
const [apmEventClient, randomSampler] = await Promise.all([
|
||||
getApmEventClient(resources),
|
||||
getRandomSampler({ security, request, probability }),
|
||||
]);
|
||||
|
||||
const logsParams = {
|
||||
esClient: coreContext.elasticsearch.client.asCurrentUser,
|
||||
identifyingMetadata: 'service.name',
|
||||
timeFrom: start,
|
||||
timeTo: end,
|
||||
kuery,
|
||||
serviceEnvironmentQuery: environmentQuery(environment),
|
||||
serviceNames,
|
||||
};
|
||||
|
||||
const [
|
||||
currentPeriodLogsRateTimeseries,
|
||||
currentPeriodLogsErrorRateTimeseries,
|
||||
apmServiceTransactionDetailedStatsPeriods,
|
||||
] = await Promise.all([
|
||||
logsDataAccessStart.services.getLogsRateTimeseries(logsParams),
|
||||
logsDataAccessStart.services.getLogsErrorRateTimeseries(logsParams),
|
||||
getServiceTransactionDetailedStatsPeriods({
|
||||
environment,
|
||||
kuery,
|
||||
apmEventClient,
|
||||
documentType,
|
||||
rollupInterval,
|
||||
bucketSizeInSeconds,
|
||||
offset,
|
||||
serviceNames,
|
||||
start,
|
||||
end,
|
||||
randomSampler,
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
currentPeriod: {
|
||||
apm: { ...apmServiceTransactionDetailedStatsPeriods.currentPeriod },
|
||||
logErrorRate: { ...currentPeriodLogsErrorRateTimeseries },
|
||||
logRate: { ...currentPeriodLogsRateTimeseries },
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const serviceLogRateTimeseriesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/entities/services/{serviceName}/logs_rate_timeseries',
|
||||
params: t.type({
|
||||
|
@ -65,8 +170,8 @@ const serviceLogRateTimeseriesRoute = createApmServerRoute({
|
|||
const { serviceName } = params.path;
|
||||
const { start, end, kuery, environment } = params.query;
|
||||
|
||||
const curentPeriodlogsRateTimeseries = await logsDataAccessStart.services.getLogsRateTimeseries(
|
||||
{
|
||||
const currentPeriodLogsRateTimeseries =
|
||||
await logsDataAccessStart.services.getLogsRateTimeseries({
|
||||
esClient: coreContext.elasticsearch.client.asCurrentUser,
|
||||
identifyingMetadata: 'service.name',
|
||||
timeFrom: start,
|
||||
|
@ -74,10 +179,9 @@ const serviceLogRateTimeseriesRoute = createApmServerRoute({
|
|||
kuery,
|
||||
serviceEnvironmentQuery: environmentQuery(environment),
|
||||
serviceNames: [serviceName],
|
||||
}
|
||||
);
|
||||
});
|
||||
|
||||
return { currentPeriod: curentPeriodlogsRateTimeseries };
|
||||
return { currentPeriod: currentPeriodLogsRateTimeseries };
|
||||
},
|
||||
});
|
||||
|
||||
|
@ -118,4 +222,5 @@ export const servicesEntitiesRoutesRepository = {
|
|||
...servicesEntitiesRoute,
|
||||
...serviceLogRateTimeseriesRoute,
|
||||
...serviceLogErrorRateTimeseriesRoute,
|
||||
...servicesEntitiesDetailedStatisticsRoute,
|
||||
};
|
||||
|
|
|
@ -24,7 +24,7 @@ import {
|
|||
import { withApmSpan } from '../../../utils/with_apm_span';
|
||||
import { maybe } from '../../../../common/utils/maybe';
|
||||
|
||||
interface ServiceTransactionDetailedStat {
|
||||
export interface ServiceTransactionDetailedStat {
|
||||
serviceName: string;
|
||||
latency: Array<{ x: number; y: number | null }>;
|
||||
transactionErrorRate?: Array<{ x: number; y: number }>;
|
||||
|
|
|
@ -22,14 +22,14 @@ export interface LogsErrorRateTimeseries {
|
|||
kuery?: string;
|
||||
}
|
||||
|
||||
export const getLogErrorsAggegation = () => ({
|
||||
const getLogErrorsAggregation = () => ({
|
||||
terms: {
|
||||
field: LOG_LEVEL,
|
||||
include: ['error', 'ERROR'],
|
||||
},
|
||||
});
|
||||
|
||||
type LogErrorsAggregation = ReturnType<typeof getLogErrorsAggegation>;
|
||||
type LogErrorsAggregation = ReturnType<typeof getLogErrorsAggregation>;
|
||||
interface LogsErrorRateTimeseriesHistogram {
|
||||
timeseries: AggregationResultOf<
|
||||
{
|
||||
|
@ -103,7 +103,7 @@ export function createGetLogErrorRateTimeseries() {
|
|||
},
|
||||
},
|
||||
aggs: {
|
||||
logErrors: getLogErrorsAggegation(),
|
||||
logErrors: getLogErrorsAggregation(),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* 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 expect from '@kbn/expect';
|
||||
import {
|
||||
APIClientRequestParamsOf,
|
||||
APIReturnType,
|
||||
} 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 { apm, log, timerange } from '@kbn/apm-synthtrace-client';
|
||||
import { uniq, map } from 'lodash';
|
||||
import { FtrProviderContext } from '../../../common/ftr_provider_context';
|
||||
|
||||
type ServicesEntitiesDetailedStatisticsReturn =
|
||||
APIReturnType<'POST /internal/apm/entities/services/detailed_statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtrace = getService('apmSynthtraceEsClient');
|
||||
const logSynthtrace = getService('logSynthtraceEsClient');
|
||||
|
||||
const start = '2024-01-01T00:00:00.000Z';
|
||||
const end = '2024-01-01T00:59:59.999Z';
|
||||
|
||||
const serviceNames = ['my-service', 'synth-go'];
|
||||
const hostName = 'synth-host';
|
||||
const serviceName = 'synth-go';
|
||||
|
||||
const EXPECTED_TPM = 5;
|
||||
const EXPECTED_LATENCY = 1000;
|
||||
const EXPECTED_FAILURE_RATE = 0.25;
|
||||
const EXPECTED_LOG_RATE = 0.016666666666666666;
|
||||
const EXPECTED_LOG_ERROR_RATE = 1;
|
||||
|
||||
async function getStats(
|
||||
overrides?: Partial<
|
||||
APIClientRequestParamsOf<'POST /internal/apm/entities/services/detailed_statistics'>['params']['query']
|
||||
>
|
||||
) {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `POST /internal/apm/entities/services/detailed_statistics`,
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
probability: 1,
|
||||
documentType: ApmDocumentType.TransactionMetric,
|
||||
rollupInterval: RollupInterval.OneMinute,
|
||||
bucketSizeInSeconds: 60,
|
||||
...overrides,
|
||||
},
|
||||
body: {
|
||||
serviceNames: JSON.stringify(serviceNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.body;
|
||||
}
|
||||
|
||||
registry.when(
|
||||
'Services entities detailed statistics when data is generated',
|
||||
{ config: 'basic', archives: [] },
|
||||
() => {
|
||||
let servicesEntitiesDetailedStatistics: ServicesEntitiesDetailedStatisticsReturn;
|
||||
|
||||
const instance = apm.service('my-service', 'production', 'java').instance('instance');
|
||||
|
||||
before(async () => {
|
||||
const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval(
|
||||
'1m'
|
||||
);
|
||||
|
||||
await logSynthtrace.index([
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is a log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': serviceName,
|
||||
'host.name': hostName,
|
||||
'service.environment': 'test',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('2m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an error log message')
|
||||
.logLevel('error')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
timerange(start, end)
|
||||
.interval('5m')
|
||||
.rate(1)
|
||||
.generator((timestamp) =>
|
||||
log
|
||||
.create()
|
||||
.message('This is an info message')
|
||||
.logLevel('info')
|
||||
.timestamp(timestamp)
|
||||
.defaults({
|
||||
'log.file.path': '/my-service.log',
|
||||
'service.name': 'my-service',
|
||||
'host.name': hostName,
|
||||
'service.environment': 'production',
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
await synthtrace.index([
|
||||
interval.rate(3).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction('GET /api')
|
||||
.duration(EXPECTED_LATENCY)
|
||||
.outcome('success')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
interval.rate(1).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction('GET /api')
|
||||
.duration(EXPECTED_LATENCY)
|
||||
.outcome('failure')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
interval.rate(1).generator((timestamp) => {
|
||||
return instance
|
||||
.transaction('GET /api')
|
||||
.duration(EXPECTED_LATENCY)
|
||||
.outcome('unknown')
|
||||
.timestamp(timestamp);
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
after(() => synthtrace.clean());
|
||||
|
||||
describe('and transaction metrics are used', () => {
|
||||
before(async () => {
|
||||
servicesEntitiesDetailedStatistics = await getStats();
|
||||
});
|
||||
|
||||
it('returns the expected statistics for apm', () => {
|
||||
const stats = servicesEntitiesDetailedStatistics.currentPeriod.apm['my-service'];
|
||||
|
||||
expect(stats).not.empty();
|
||||
expect(uniq(map(stats.throughput, 'y'))).eql([EXPECTED_TPM], 'tpm');
|
||||
|
||||
expect(uniq(map(stats.latency, 'y'))).eql([EXPECTED_LATENCY * 1000], 'latency');
|
||||
|
||||
expect(uniq(map(stats.transactionErrorRate, 'y'))).eql(
|
||||
[EXPECTED_FAILURE_RATE],
|
||||
'errorRate'
|
||||
);
|
||||
});
|
||||
|
||||
it('returns the expected statistics for logs', () => {
|
||||
const statsLogErrorRate =
|
||||
servicesEntitiesDetailedStatistics.currentPeriod.logErrorRate[serviceName];
|
||||
const statsLogRate =
|
||||
servicesEntitiesDetailedStatistics.currentPeriod.logRate[serviceName];
|
||||
|
||||
expect(statsLogErrorRate.every(({ y }) => y === EXPECTED_LOG_ERROR_RATE)).to.be(true);
|
||||
expect(statsLogRate.every(({ y }) => y === EXPECTED_LOG_RATE)).to.be(true);
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue