[APM] Add sparklines to the multi-signal view table (#187782)

Closes #187567

## Summary

This PR adds sparklines to the multi-signal view table


![image](d29aa76f-1ec1-4720-bf85-84e818971bc0)

## 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:
jennypavlova 2024-07-11 12:00:26 +02:00 committed by GitHub
parent 57be31c8cb
commit 35b5fcc4c4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 421 additions and 27 deletions

View file

@ -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>
</>
);
};
}

View file

@ -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}
/>
);
},

View file

@ -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}>

View file

@ -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,
};

View file

@ -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 }>;

View file

@ -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(),
},
},
},

View file

@ -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);
});
});
}
);
}