[APM] Service metrics/continuous rollups follow-up work (#150266)

Co-authored-by: Søren Louv-Jansen <sorenlouv@gmail.com>
Closes https://github.com/elastic/kibana/issues/150265
This commit is contained in:
Dario Gieselaar 2023-02-07 15:29:47 +01:00 committed by GitHub
parent 9119a00a60
commit b96d46c06b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1476 additions and 368 deletions

View file

@ -6,6 +6,7 @@
* Side Public License, v 1.
*/
import type { Moment } from 'moment';
import { Interval } from './interval';
export class Timerange {
@ -20,9 +21,14 @@ export class Timerange {
}
}
export function timerange(from: Date | number, to: Date | number) {
return new Timerange(
from instanceof Date ? from : new Date(from),
to instanceof Date ? to : new Date(to)
);
type DateLike = Date | number | Moment | string;
function getDateFrom(date: DateLike): Date {
if (date instanceof Date) return date;
if (typeof date === 'number' || typeof date === 'string') return new Date(date);
return date.toDate();
}
export function timerange(from: Date | number | Moment, to: Date | number | Moment) {
return new Timerange(getDateFrom(from), getDateFrom(to));
}

View file

@ -0,0 +1,43 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { ApmFields, hashKeysOf } from '@kbn/apm-synthtrace-client';
import { identity, noop, pick } from 'lodash';
import { createApmMetricAggregator } from './create_apm_metric_aggregator';
const KEY_FIELDS: Array<keyof ApmFields> = [
'agent.name',
'service.environment',
'service.name',
'service.language.name',
];
export function createServiceSummaryMetricsAggregator(flushInterval: string) {
return createApmMetricAggregator(
{
filter: () => true,
getAggregateKey: (event) => {
// see https://github.com/elastic/apm-server/blob/main/x-pack/apm-server/aggregation/txmetrics/aggregator.go
return hashKeysOf(event, KEY_FIELDS);
},
flushInterval,
init: (event) => {
const set = pick(event, KEY_FIELDS);
return {
...set,
'metricset.name': 'service_summary',
'metricset.interval': flushInterval,
'processor.event': 'metric',
'processor.name': 'metric',
};
},
},
noop,
identity
);
}

View file

@ -34,7 +34,8 @@ export function getRoutingTransform() {
} else if (
metricsetName === 'transaction' ||
metricsetName === 'service_transaction' ||
metricsetName === 'service_destination'
metricsetName === 'service_destination' ||
metricsetName === 'service_summary'
) {
index = `metrics-apm.${metricsetName}.${document['metricset.interval']!}-default`;
} else {

View file

@ -21,6 +21,7 @@ import { Logger } from '../../../utils/create_logger';
import { fork, sequential } from '../../../utils/stream_utils';
import { createBreakdownMetricsAggregator } from '../../aggregators/create_breakdown_metrics_aggregator';
import { createServiceMetricsAggregator } from '../../aggregators/create_service_metrics_aggregator';
import { createServiceSummaryMetricsAggregator } from '../../aggregators/create_service_summary_metrics_aggregator';
import { createSpanMetricsAggregator } from '../../aggregators/create_span_metrics_aggregator';
import { createTransactionMetricsAggregator } from '../../aggregators/create_transaction_metrics_aggregator';
import { getApmServerMetadataTransform } from './get_apm_server_metadata_transform';
@ -111,6 +112,7 @@ export class ApmSynthtraceEsClient {
index: dataStreams,
allow_no_indices: true,
ignore_unavailable: true,
expand_wildcards: ['open', 'hidden'],
});
}
@ -123,6 +125,9 @@ export class ApmSynthtraceEsClient {
createServiceMetricsAggregator('1m'),
createServiceMetricsAggregator('10m'),
createServiceMetricsAggregator('60m'),
createServiceSummaryMetricsAggregator('1m'),
createServiceSummaryMetricsAggregator('10m'),
createServiceSummaryMetricsAggregator('60m'),
createSpanMetricsAggregator('1m'),
createSpanMetricsAggregator('10m'),
createSpanMetricsAggregator('60m'),

View file

@ -0,0 +1,49 @@
/*
* 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 and the Server Side Public License, v 1; you may not use this file except
* in compliance with, at your election, the Elastic License 2.0 or the Server
* Side Public License, v 1.
*/
import { apm, ApmFields } from '@kbn/apm-synthtrace-client';
import { Scenario } from '../cli/scenario';
const scenario: Scenario<ApmFields> = async ({ logger, scenarioOpts }) => {
return {
generate: ({ range }) => {
const withTx = apm
.service('service-with-transactions', 'production', 'java')
.instance('instance');
const withErrorsOnly = apm
.service('service-with-errors-only', 'production', 'java')
.instance('instance');
const withAppMetricsOnly = apm
.service('service-with-app-metrics-only', 'production', 'java')
.instance('instance');
return range
.interval('1m')
.rate(1)
.generator((timestamp) => {
return [
withTx.transaction('GET /api').duration(100).timestamp(timestamp),
withErrorsOnly
.error({
message: 'An unknown error occurred',
})
.timestamp(timestamp),
withAppMetricsOnly
.appMetrics({
'system.memory.actual.free': 1,
'system.memory.total': 2,
})
.timestamp(timestamp),
];
});
},
};
};
export default scenario;

View file

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Synthtrace ES Client indexer indexes documents 1`] = `
exports[`Synthtrace ES Client indexer indexes documents 2`] = `
Array [
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
@ -68,6 +68,33 @@ Array [
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
"metricset": Object {
@ -119,6 +146,24 @@ Array [
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:30:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:30:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:30:00.000Z",
"metricset": Object {
@ -200,6 +245,42 @@ Array [
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T01:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T01:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T00:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T01:00:00.000Z",
"metricset": Object {
"name": "service_summary",
},
"processor": Object {
"event": "metric",
},
},
Object {
"@timestamp": "2022-01-01T01:00:00.000Z",
"metricset": Object {

View file

@ -72,7 +72,7 @@ describe('Synthtrace ES Client indexer', () => {
const events = await toArray(datasource);
expect(events.length).toBe(24);
expect(events.length).toMatchInlineSnapshot(`33`);
const mapped = events.map((event) =>
pick(event, '@timestamp', 'processor.event', 'metricset.name')

View file

@ -434,6 +434,10 @@ export const stackManagementSchema: MakeSchemaFrom<UsageStats> = {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmEnableContinuousRollups': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },
},
'observability:apmAgentExplorerView': {
type: 'boolean',
_meta: { description: 'Non-default value of setting.' },

View file

@ -41,6 +41,7 @@ export interface UsageStats {
'observability:enableComparisonByDefault': boolean;
'observability:enableServiceGroups': boolean;
'observability:apmEnableServiceMetrics': boolean;
'observability:apmEnableContinuousRollups': boolean;
'observability:apmAWSLambdaPriceFactor': string;
'observability:apmAWSLambdaRequestCostPerMillion': number;
'observability:enableInfrastructureHostsView': boolean;

View file

@ -8910,6 +8910,12 @@
"description": "Non-default value of setting."
}
},
"observability:apmEnableContinuousRollups": {
"type": "boolean",
"_meta": {
"description": "Non-default value of setting."
}
},
"observability:apmAgentExplorerView": {
"type": "boolean",
"_meta": {

View file

@ -12,7 +12,8 @@ type AnyApmDocumentType =
| ApmDocumentType.ServiceTransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionEvent
| ApmDocumentType.ServiceDestinationMetric;
| ApmDocumentType.ServiceDestinationMetric
| ApmDocumentType.ServiceSummaryMetric;
export interface ApmDataSource<
TDocumentType extends AnyApmDocumentType = AnyApmDocumentType

View file

@ -10,4 +10,10 @@ export enum ApmDocumentType {
ServiceTransactionMetric = 'serviceTransactionMetric',
TransactionEvent = 'transactionEvent',
ServiceDestinationMetric = 'serviceDestinationMetric',
ServiceSummaryMetric = 'serviceSummaryMetric',
}
export type ApmServiceTransactionDocumentType =
| ApmDocumentType.ServiceTransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionEvent;

View file

@ -224,38 +224,48 @@ GET apm-*-metric-*,metrics-apm*/_search?terminate_after=1000
# Transactions in service inventory page
Service metrics is an aggregated metric document that holds latency and throughput metrics pivoted by `service.name + service.environment + transaction.type`
Service transaction metrics are aggregated metric documents that hold latency and throughput metrics pivoted by `service.name`, `service.environment` and `transaction.type`. Additionally, `agent.name` and `service.language.name` are included as metadata.
The decision to use service metrics aggregation or not is determined in [getServiceInventorySearchSource](https://github.com/elastic/kibana/blob/5d585ea375be551a169a0bea49b011819b9ac669/x-pack/plugins/apm/server/lib/helpers/get_service_inventory_search_source.ts#L12) and [getSearchServiceMetrics](https://github.com/elastic/kibana/blob/5d585ea375be551a169a0bea49b011819b9ac669/x-pack/plugins/apm/server/lib/helpers/service_metrics/index.ts#L38)
We use the response from the `GET /internal/apm/time_range_metadata` endpoint to determine what data source is available. A data source is considered available if there is either data before the current time range, or, if there is no data at all before the current time range, if there is data within the current time range. This means that existing deployments will use transaction metrics right after upgrading (instead of using service transaction metrics and seeing a mostly blank screen), but also that new deployments immediately get the benefits of service transaction metrics, instead of falling all the way back to transaction events.
A pre-aggregated document where `_doc_count` is the number of transaction events
```
{
"_doc_count": 627,
"_doc_count": 4,
"@timestamp": "2021-09-01T10:00:00.000Z",
"processor.event": "metric",
"metricset.name": "service",
"metricset.name": "service_transaction",
"metricset.interval": "1m",
"service": {
"environment": "production",
"name": "web-go"
},
"transaction": {
"duration.summary": {
"sum": 376492831,
"value_count": 627
"sum": 1000,
"value_count": 4
},
"duration.histogram": {
"counts": [ 4 ],
"values": [ 250 ]
},
"success_count": 476,
"failure_count": 151,
"type": "request"
},
"event": {
"success_count": {
"sum": 1,
"value_count": 2
}
}
}
```
- `_doc_count` is the number of bucket counts
- `transaction.duration.summary` is an [aggregate_metric_double](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/aggregate-metric-double.html) field and holds an aggregated transaction duration summary, for service metrics
- `failure_count` holds an aggregated count of transactions with the outcome "failure"
- `success_count` holds an aggregated count of transactions with the outcome "success"
- `transaction.duration.summary` is an [aggregate_metric_double](https://www.elastic.co/guide/en/elasticsearch/reference/7.17/aggregate-metric-double.html) field and holds an aggregated transaction duration summary, for service transaction metrics
- `event.success_count` holds an aggregate metric double that describes the _success rate_. E.g., in this example, the success rate is 50% (1/2).
In addition to `service_transaction`, `service_summary` metrics are also generated. Every service outputs these, even when it does not record any transaction (that also means there is no transaction data on this metric). This means that we can use `service_summary` to display services without transactions, i.e. services that only have app/system metrics or errors.
### Latency

View file

@ -17,6 +17,8 @@ import {
enableInspectEsQueries,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
apmEnableServiceMetrics,
apmEnableContinuousRollups,
} from '@kbn/observability-plugin/common';
import { isEmpty } from 'lodash';
import React from 'react';
@ -32,6 +34,8 @@ const apmSettingsKeys = [
apmLabsButton,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
apmEnableServiceMetrics,
apmEnableContinuousRollups,
];
export function GeneralSettings() {

View file

@ -5,11 +5,16 @@
* 2.0.
*/
import React, { createContext } from 'react';
import {
apmEnableServiceMetrics,
apmEnableContinuousRollups,
} from '@kbn/observability-plugin/common';
import { TimeRangeMetadata } from '../../../common/time_range_metadata';
import { useApmParams } from '../../hooks/use_apm_params';
import { useApmRoutePath } from '../../hooks/use_apm_route_path';
import { FetcherResult, useFetcher } from '../../hooks/use_fetcher';
import { useTimeRange } from '../../hooks/use_time_range';
import { useApmPluginContext } from '../apm_plugin/use_apm_plugin_context';
export const TimeRangeMetadataContext = createContext<
FetcherResult<TimeRangeMetadata> | undefined
@ -20,6 +25,10 @@ export function TimeRangeMetadataContextProvider({
}: {
children: React.ReactElement;
}) {
const {
core: { uiSettings },
} = useApmPluginContext();
const { query } = useApmParams('/*');
const kuery = 'kuery' in query ? query.kuery : '';
@ -37,6 +46,16 @@ export function TimeRangeMetadataContextProvider({
const routePath = useApmRoutePath();
const enableServiceTransactionMetrics = uiSettings.get<boolean>(
apmEnableServiceMetrics,
true
);
const enableContinuousRollups = uiSettings.get<boolean>(
apmEnableContinuousRollups,
true
);
const isOperationView =
routePath.startsWith('/dependencies/operation') ||
routePath.startsWith('/dependencies/operations');
@ -50,11 +69,20 @@ export function TimeRangeMetadataContextProvider({
end,
kuery,
useSpanName: isOperationView,
enableServiceTransactionMetrics,
enableContinuousRollups,
},
},
});
},
[start, end, kuery, isOperationView]
[
start,
end,
kuery,
isOperationView,
enableServiceTransactionMetrics,
enableContinuousRollups,
]
);
return (

View file

@ -28,7 +28,11 @@ export function processorEventsToIndex(
events: ProcessorEvent[],
indices: ApmIndicesConfig
) {
return uniq(events.map((event) => indices[processorEventIndexMap[event]]));
return uniq(
events.flatMap((event) =>
indices[processorEventIndexMap[event]].split(',').map((str) => str.trim())
)
);
}
export function getRequestBase(options: {

View file

@ -9,15 +9,17 @@ import type {
EqlSearchRequest,
FieldCapsRequest,
FieldCapsResponse,
MsearchMultisearchBody,
MsearchMultisearchHeader,
TermsEnumRequest,
TermsEnumResponse,
} from '@elastic/elasticsearch/lib/api/types';
import { ValuesType } from 'utility-types';
import { ElasticsearchClient, KibanaRequest } from '@kbn/core/server';
import type { ESSearchRequest, InferSearchResponseOf } from '@kbn/es-types';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { unwrapEsResponse } from '@kbn/observability-plugin/server';
import { compact, omit } from 'lodash';
import { ValuesType } from 'utility-types';
import { ApmDataSource } from '../../../../../common/data_source';
import { APMError } from '../../../../../typings/es_schemas/ui/apm_error';
import { Metric } from '../../../../../typings/es_schemas/ui/metric';
@ -31,8 +33,8 @@ import {
getDebugTitle,
} from '../call_async_with_debug';
import { cancelEsRequestOnAbort } from '../cancel_es_request_on_abort';
import { processorEventsToIndex, getRequestBase } from './get_request_base';
import { ProcessorEventOfDocumentType } from '../document_type';
import { getRequestBase, processorEventsToIndex } from './get_request_base';
export type APMEventESSearchRequest = Omit<ESSearchRequest, 'index'> & {
apm: {
@ -81,6 +83,10 @@ type TypedSearchResponse<TParams extends APMEventESSearchRequest> =
TParams
>;
interface TypedMSearchResponse<TParams extends APMEventESSearchRequest> {
responses: Array<TypedSearchResponse<TParams>>;
}
export interface APMEventClientConfig {
esClient: ElasticsearchClient;
debug: boolean;
@ -163,7 +169,7 @@ export class APMEventClient {
this.forceSyntheticSource && events.includes(ProcessorEvent.metric);
const searchParams = {
...omit(params, 'apm'),
...omit(params, 'apm', 'body'),
index,
body: {
...params.body,
@ -193,12 +199,63 @@ export class APMEventClient {
});
}
async msearch<TParams extends APMEventESSearchRequest>(
operationName: string,
...allParams: TParams[]
): Promise<TypedMSearchResponse<TParams>> {
const searches = allParams
.map((params) => {
const { index, filters } = getRequestBase({
apm: params.apm,
indices: this.indices,
});
const searchParams: [MsearchMultisearchHeader, MsearchMultisearchBody] =
[
{
index,
preference: 'any',
...(this.includeFrozen ? { ignore_throttled: false } : {}),
ignore_unavailable: true,
expand_wildcards: ['open' as const, 'hidden' as const],
},
{
...omit(params, 'apm', 'body'),
...params.body,
query: {
bool: {
filter: compact([params.body.query, ...filters]),
},
},
},
];
return searchParams;
})
.flat();
return this.callAsyncWithDebug({
cb: (opts) =>
this.esClient.msearch(
{
searches,
},
opts
) as unknown as Promise<{
body: TypedMSearchResponse<TParams>;
}>,
operationName,
params: searches,
requestType: 'msearch',
});
}
async eqlSearch(operationName: string, params: APMEventEqlSearchRequest) {
const index = processorEventsToIndex(params.apm.events, this.indices);
const requestParams = {
index,
...omit(params, 'apm'),
index,
};
return this.callAsyncWithDebug({
@ -216,8 +273,8 @@ export class APMEventClient {
const index = processorEventsToIndex(params.apm.events, this.indices);
const requestParams = {
index,
...omit(params, 'apm'),
index,
};
return this.callAsyncWithDebug({
@ -235,8 +292,8 @@ export class APMEventClient {
const index = processorEventsToIndex(params.apm.events, this.indices);
const requestParams = {
index: Array.isArray(index) ? index.join(',') : index,
...omit(params, 'apm'),
index: index.join(','),
};
return this.callAsyncWithDebug({

View file

@ -51,6 +51,15 @@ const documentTypeConfigMap: Record<
}),
rollupIntervals: defaultRollupIntervals,
},
[ApmDocumentType.ServiceSummaryMetric]: {
processorEvent: ProcessorEvent.metric,
getQuery: (rollupInterval) => ({
bool: {
filter: getDefaultFilter('service_summary', rollupInterval),
},
}),
rollupIntervals: defaultRollupIntervals,
},
[ApmDocumentType.TransactionMetric]: {
processorEvent: ProcessorEvent.metric,
getQuery: (rollupInterval) => ({

View file

@ -5,7 +5,6 @@
* 2.0.
*/
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { flatten } from 'lodash';
import { ApmDataSource } from '../../../common/data_source';
import { ApmDocumentType } from '../../../common/document_type';
import { RollupInterval } from '../../../common/rollup';
@ -17,63 +16,131 @@ export async function getDocumentSources({
start,
end,
kuery,
enableServiceTransactionMetrics,
enableContinuousRollups,
}: {
apmEventClient: APMEventClient;
start: number;
end: number;
kuery: string;
enableServiceTransactionMetrics: boolean;
enableContinuousRollups: boolean;
}) {
const sources: Array<ApmDataSource & { hasDocs: boolean }> = flatten(
await Promise.all(
[
ApmDocumentType.ServiceTransactionMetric as const,
ApmDocumentType.TransactionMetric as const,
].map(async (documentType) => {
const docTypeConfig = getConfigForDocumentType(documentType);
const allHasDocs = await Promise.all(
docTypeConfig.rollupIntervals.map(async (rollupInterval) => {
const response = await apmEventClient.search(
'check_document_type_availability',
{
apm: {
sources: [
{
documentType,
rollupInterval,
},
],
},
body: {
track_total_hits: 1,
size: 0,
terminate_after: 1,
query: {
bool: {
filter: [...kqlQuery(kuery), ...rangeQuery(start, end)],
},
},
},
}
);
const currentRange = rangeQuery(start, end);
const diff = end - start;
const kql = kqlQuery(kuery);
const beforeRange = rangeQuery(start - diff, end - diff);
return {
const sourcesToCheck = [
...(enableServiceTransactionMetrics
? [ApmDocumentType.ServiceTransactionMetric as const]
: []),
ApmDocumentType.TransactionMetric as const,
].flatMap((documentType) => {
const docTypeConfig = getConfigForDocumentType(documentType);
return (
enableContinuousRollups
? docTypeConfig.rollupIntervals
: [RollupInterval.OneMinute]
).flatMap((rollupInterval) => {
const searchParams = {
apm: {
sources: [
{
documentType,
rollupInterval,
hasDocs: response.hits.total.value > 0,
};
})
);
},
],
},
body: {
track_total_hits: 1,
size: 0,
terminate_after: 1,
},
};
return allHasDocs;
})
)
return {
documentType,
rollupInterval,
before: {
...searchParams,
body: {
...searchParams.body,
query: {
bool: {
filter: [...kql, ...beforeRange],
},
},
},
},
current: {
...searchParams,
body: {
...searchParams.body,
query: {
bool: {
filter: [...kql, ...currentRange],
},
},
},
},
};
});
});
const allSearches = sourcesToCheck.flatMap(({ before, current }) => [
before,
current,
]);
const allResponses = (
await apmEventClient.msearch('get_document_availability', ...allSearches)
).responses;
const checkedSources = sourcesToCheck.map((source, index) => {
const responseBefore = allResponses[index * 2];
const responseAfter = allResponses[index * 2 + 1];
const { documentType, rollupInterval } = source;
const hasDataBefore = responseBefore.hits.total.value > 0;
const hasDataAfter = responseAfter.hits.total.value > 0;
return {
documentType,
rollupInterval,
hasDataBefore,
hasDataAfter,
};
});
const hasAnyDataBefore = checkedSources.some(
(source) => source.hasDataBefore
);
sources.push({
const sources: Array<ApmDataSource & { hasDocs: boolean }> =
checkedSources.map((source) => {
const { documentType, hasDataAfter, hasDataBefore, rollupInterval } =
source;
const hasData = hasDataBefore || hasDataAfter;
return {
documentType,
rollupInterval,
// If there is any data before, we require that data is available before
// this time range to mark this source as available. If we don't do that,
// users that upgrade to a version that starts generating service tx metrics
// will see a mostly empty screen for a while after upgrading.
// If we only check before, users with a new deployment will use raw transaction
// events.
hasDocs: hasAnyDataBefore ? hasDataBefore : hasData,
};
});
return sources.concat({
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
});
return sources;
}

View file

@ -91,7 +91,11 @@ export async function getSearchTransactionsEvents({
}
export function getDurationFieldForTransactions(
typeOrSearchAgggregatedTransactions: ApmDocumentType | boolean
typeOrSearchAgggregatedTransactions:
| ApmDocumentType.ServiceTransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionEvent
| boolean
) {
let type: ApmDocumentType;
if (typeOrSearchAgggregatedTransactions === true) {

View file

@ -22,7 +22,7 @@ export const probabilityRt = t.type({
});
export const kueryRt = t.type({ kuery: t.string });
export const dataSourceRt = t.type({
export const serviceTransactionDataSourceRt = t.type({
documentType: t.union([
t.literal(ApmDocumentType.ServiceTransactionMetric),
t.literal(ApmDocumentType.TransactionMetric),

View file

@ -41,7 +41,10 @@ interface AggregationParams {
end: number;
serviceGroup: ServiceGroup | null;
randomSampler: RandomSampler;
documentType: ApmDocumentType;
documentType:
| ApmDocumentType.ServiceTransactionMetric
| ApmDocumentType.TransactionMetric
| ApmDocumentType.TransactionEvent;
rollupInterval: RollupInterval;
}

View file

@ -6,7 +6,7 @@
*/
import { Logger } from '@kbn/logging';
import { ApmDocumentType } from '../../../../common/document_type';
import { ApmServiceTransactionDocumentType } from '../../../../common/document_type';
import { RollupInterval } from '../../../../common/rollup';
import { ServiceGroup } from '../../../../common/service_groups';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
@ -15,7 +15,7 @@ import { MlClient } from '../../../lib/helpers/get_ml_client';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
import { withApmSpan } from '../../../utils/with_apm_span';
import { getHealthStatuses } from './get_health_statuses';
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
import { getServicesWithoutTransactions } from './get_services_without_transactions';
import { getServicesAlerts } from './get_service_alerts';
import { getServiceTransactionStats } from './get_service_transaction_stats';
import { mergeServiceStats } from './merge_service_stats';
@ -46,7 +46,7 @@ export async function getServicesItems({
end: number;
serviceGroup: ServiceGroup | null;
randomSampler: RandomSampler;
documentType: ApmDocumentType;
documentType: ApmServiceTransactionDocumentType;
rollupInterval: RollupInterval;
}) {
return withApmSpan('get_services_items', async () => {
@ -64,7 +64,7 @@ export async function getServicesItems({
const [
{ serviceStats, serviceOverflowCount },
{ services, maxServiceCountExceeded },
{ services: servicesWithoutTransactions, maxServiceCountExceeded },
healthStatuses,
alertCounts,
] = await Promise.all([
@ -72,7 +72,7 @@ export async function getServicesItems({
...commonParams,
apmEventClient,
}),
getServicesFromErrorAndMetricDocuments({
getServicesWithoutTransactions({
...commonParams,
apmEventClient,
}),
@ -90,7 +90,7 @@ export async function getServicesItems({
items:
mergeServiceStats({
serviceStats,
servicesFromErrorAndMetricDocuments: services,
servicesWithoutTransactions,
healthStatuses,
alertCounts,
}) ?? [],

View file

@ -18,8 +18,10 @@ import { serviceGroupQuery } from '../../../lib/service_group_query';
import { ServiceGroup } from '../../../../common/service_groups';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { ApmDocumentType } from '../../../../common/document_type';
import { RollupInterval } from '../../../../common/rollup';
export async function getServicesFromErrorAndMetricDocuments({
export async function getServicesWithoutTransactions({
environment,
apmEventClient,
maxNumServices,
@ -28,6 +30,8 @@ export async function getServicesFromErrorAndMetricDocuments({
end,
serviceGroup,
randomSampler,
documentType,
rollupInterval,
}: {
apmEventClient: APMEventClient;
environment: string;
@ -37,13 +41,29 @@ export async function getServicesFromErrorAndMetricDocuments({
end: number;
serviceGroup: ServiceGroup | null;
randomSampler: RandomSampler;
documentType: ApmDocumentType;
rollupInterval: RollupInterval;
}) {
const isServiceTransactionMetric =
documentType === ApmDocumentType.ServiceTransactionMetric;
const response = await apmEventClient.search(
'get_services_from_error_and_metric_documents',
isServiceTransactionMetric
? 'get_services_from_service_summary'
: 'get_services_from_error_and_metric_documents',
{
apm: {
events: [ProcessorEvent.metric, ProcessorEvent.error],
},
apm: isServiceTransactionMetric
? {
sources: [
{
documentType: ApmDocumentType.ServiceSummaryMetric,
rollupInterval,
},
],
}
: {
events: [ProcessorEvent.metric, ProcessorEvent.error],
},
body: {
track_total_hits: false,
size: 0,

View file

@ -40,7 +40,7 @@ describe('mergeServiceStats', () => {
throughput: 4,
}),
],
servicesFromErrorAndMetricDocuments: [
servicesWithoutTransactions: [
{
environments: ['production'],
serviceName: 'opbeans-java',
@ -93,7 +93,7 @@ describe('mergeServiceStats', () => {
environments: ['staging'],
}),
],
servicesFromErrorAndMetricDocuments: [
servicesWithoutTransactions: [
{
environments: ['production'],
serviceName: 'opbeans-java',
@ -142,7 +142,7 @@ describe('mergeServiceStats', () => {
environments: ['staging'],
}),
],
servicesFromErrorAndMetricDocuments: [],
servicesWithoutTransactions: [],
healthStatuses: [
{
healthStatus: ServiceHealthStatus.healthy,
@ -179,7 +179,7 @@ describe('mergeServiceStats', () => {
environments: ['staging'],
}),
],
servicesFromErrorAndMetricDocuments: [
servicesWithoutTransactions: [
{
environments: ['production'],
serviceName: 'opbeans-java',

View file

@ -9,30 +9,29 @@ import { asMutableArray } from '../../../../common/utils/as_mutable_array';
import { joinByKey } from '../../../../common/utils/join_by_key';
import { getServicesAlerts } from './get_service_alerts';
import { getHealthStatuses } from './get_health_statuses';
import { getServicesFromErrorAndMetricDocuments } from './get_services_from_error_and_metric_documents';
import { getServicesWithoutTransactions } from './get_services_without_transactions';
import { getServiceTransactionStats } from './get_service_transaction_stats';
export function mergeServiceStats({
serviceStats,
servicesFromErrorAndMetricDocuments,
servicesWithoutTransactions,
healthStatuses,
alertCounts,
}: {
serviceStats: Awaited<
ReturnType<typeof getServiceTransactionStats>
>['serviceStats'];
servicesFromErrorAndMetricDocuments: Awaited<
ReturnType<typeof getServicesFromErrorAndMetricDocuments>
servicesWithoutTransactions: Awaited<
ReturnType<typeof getServicesWithoutTransactions>
>['services'];
healthStatuses: Awaited<ReturnType<typeof getHealthStatuses>>;
alertCounts: Awaited<ReturnType<typeof getServicesAlerts>>;
}) {
const foundServiceNames = serviceStats.map(({ serviceName }) => serviceName);
const servicesWithOnlyMetricDocuments =
servicesFromErrorAndMetricDocuments.filter(
({ serviceName }) => !foundServiceNames.includes(serviceName)
);
const servicesWithOnlyMetricDocuments = servicesWithoutTransactions.filter(
({ serviceName }) => !foundServiceNames.includes(serviceName)
);
const allServiceNames = foundServiceNames.concat(
servicesWithOnlyMetricDocuments.map(({ serviceName }) => serviceName)
@ -47,7 +46,7 @@ export function mergeServiceStats({
return joinByKey(
asMutableArray([
...serviceStats,
...servicesFromErrorAndMetricDocuments,
...servicesWithoutTransactions,
...matchedHealthStatuses,
...alertCounts,
] as const),

View file

@ -7,7 +7,7 @@
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
import { keyBy } from 'lodash';
import { ApmDocumentType } from '../../../../common/document_type';
import { ApmServiceTransactionDocumentType } from '../../../../common/document_type';
import {
SERVICE_NAME,
TRANSACTION_TYPE,
@ -19,7 +19,7 @@ import {
} from '../../../../common/transaction_types';
import { environmentQuery } from '../../../../common/utils/environment_query';
import { getOffsetInMs } from '../../../../common/utils/get_offset_in_ms';
import { calculateThroughputWithRange } from '../../../lib/helpers/calculate_throughput';
import { calculateThroughputWithInterval } from '../../../lib/helpers/calculate_throughput';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
import { getDurationFieldForTransactions } from '../../../lib/helpers/transactions';
@ -46,7 +46,7 @@ export async function getServiceTransactionDetailedStats({
environment: string;
kuery: string;
apmEventClient: APMEventClient;
documentType: ApmDocumentType;
documentType: ApmServiceTransactionDocumentType;
rollupInterval: RollupInterval;
bucketSizeInSeconds: number;
offset?: string;
@ -159,9 +159,8 @@ export async function getServiceTransactionDetailedStats({
throughput: topTransactionTypeBucket.timeseries.buckets.map(
(dateBucket) => ({
x: dateBucket.key + offsetInMs,
y: calculateThroughputWithRange({
start,
end,
y: calculateThroughputWithInterval({
bucketSize: bucketSizeInSeconds,
value: dateBucket.doc_count,
}),
})
@ -189,7 +188,7 @@ export async function getServiceDetailedStatsPeriods({
environment: string;
kuery: string;
apmEventClient: APMEventClient;
documentType: ApmDocumentType;
documentType: ApmServiceTransactionDocumentType;
rollupInterval: RollupInterval;
bucketSizeInSeconds: number;
offset?: string;

View file

@ -5,7 +5,7 @@
* 2.0.
*/
import { ApmDocumentType } from '../../../../common/document_type';
import { ApmServiceTransactionDocumentType } from '../../../../common/document_type';
import { RollupInterval } from '../../../../common/rollup';
import { APMEventClient } from '../../../lib/helpers/create_es_client/create_apm_event_client';
import { RandomSampler } from '../../../lib/helpers/get_random_sampler';
@ -28,7 +28,7 @@ export async function getServicesDetailedStatistics({
environment: string;
kuery: string;
apmEventClient: APMEventClient;
documentType: ApmDocumentType;
documentType: ApmServiceTransactionDocumentType;
rollupInterval: RollupInterval;
bucketSizeInSeconds: number;
offset?: string;

View file

@ -32,7 +32,7 @@ import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
import { withApmSpan } from '../../utils/with_apm_span';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
import {
dataSourceRt,
serviceTransactionDataSourceRt,
environmentRt,
kueryRt,
probabilityRt,
@ -64,7 +64,7 @@ const servicesRoute = createApmServerRoute({
t.partial({ serviceGroup: t.string }),
t.intersection([
probabilityRt,
dataSourceRt,
serviceTransactionDataSourceRt,
environmentRt,
kueryRt,
rangeRt,
@ -179,7 +179,7 @@ const servicesDetailedStatisticsRoute = createApmServerRoute({
environmentRt,
kueryRt,
rangeRt,
t.intersection([offsetRt, probabilityRt, dataSourceRt]),
t.intersection([offsetRt, probabilityRt, serviceTransactionDataSourceRt]),
t.type({
bucketSizeInSeconds: toNumberRt,
}),

View file

@ -17,7 +17,11 @@ export const timeRangeMetadataRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/time_range_metadata',
params: t.type({
query: t.intersection([
t.type({ useSpanName: toBooleanRt }),
t.type({
useSpanName: toBooleanRt,
enableServiceTransactionMetrics: toBooleanRt,
enableContinuousRollups: toBooleanRt,
}),
kueryRt,
rangeRt,
]),
@ -29,7 +33,14 @@ export const timeRangeMetadataRoute = createApmServerRoute({
const apmEventClient = await getApmEventClient(resources);
const {
query: { useSpanName, start, end, kuery },
query: {
useSpanName,
start,
end,
kuery,
enableServiceTransactionMetrics,
enableContinuousRollups,
},
} = resources.params;
const [isUsingServiceDestinationMetrics, sources] = await Promise.all([
@ -45,6 +56,8 @@ export const timeRangeMetadataRoute = createApmServerRoute({
start,
end,
kuery,
enableServiceTransactionMetrics,
enableContinuousRollups,
}),
]);

View file

@ -27,6 +27,8 @@ export {
enableAgentExplorerView,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
apmEnableServiceMetrics,
apmEnableContinuousRollups,
enableCriticalPath,
profilingElasticsearchPlugin,
} from './ui_settings_keys';

View file

@ -22,4 +22,6 @@ export const enableAgentExplorerView = 'observability:apmAgentExplorerView';
export const apmAWSLambdaPriceFactor = 'observability:apmAWSLambdaPriceFactor';
export const apmAWSLambdaRequestCostPerMillion = 'observability:apmAWSLambdaRequestCostPerMillion';
export const enableCriticalPath = 'observability:apmEnableCriticalPath';
export const apmEnableServiceMetrics = 'observability:apmEnableServiceMetrics';
export const apmEnableContinuousRollups = 'observability:apmEnableContinuousRollups';
export const profilingElasticsearchPlugin = 'observability:profilingElasticsearchPlugin';

View file

@ -150,7 +150,7 @@ export function getInspectResponse({
return {
id,
json: esRequestParams.body,
json: esRequestParams.body ?? esRequestParams,
name: id,
response: {
json: esError ? esError.originalError : esResponse,

View file

@ -23,11 +23,17 @@ import {
enableAwsLambdaMetrics,
apmAWSLambdaPriceFactor,
apmAWSLambdaRequestCostPerMillion,
apmEnableServiceMetrics,
apmEnableContinuousRollups,
enableCriticalPath,
enableInfrastructureHostsView,
profilingElasticsearchPlugin,
} from '../common/ui_settings_keys';
const betaLabel = i18n.translate('xpack.observability.uiSettings.betaLabel', {
defaultMessage: 'beta',
});
const technicalPreviewLabel = i18n.translate(
'xpack.observability.uiSettings.technicalPreviewLabel',
{ defaultMessage: 'technical preview' }
@ -287,6 +293,34 @@ export const uiSettings: Record<string, UiSettings> = {
value: 0.2,
schema: schema.number({ min: 0 }),
},
[apmEnableServiceMetrics]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.apmEnableServiceMetrics', {
defaultMessage: 'Service transaction metrics',
}),
value: true,
description: i18n.translate('xpack.observability.apmEnableServiceMetricsDescription', {
defaultMessage:
'{betaLabel} Enables the usage of service transaction metrics, which are low cardinality metrics that can be used by certain views like the service inventory for faster loading times.',
values: { betaLabel: `<em>[${betaLabel}]</em>` },
}),
schema: schema.boolean(),
requiresPageReload: true,
},
[apmEnableContinuousRollups]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.apmEnableContinuousRollups', {
defaultMessage: 'Continuous rollups',
}),
value: true,
description: i18n.translate('xpack.observability.apmEnableContinuousRollupsDescription', {
defaultMessage:
'{betaLabel} When continuous rollups is enabled, the UI will select metrics with the appropriate resolution. On larger time ranges, lower resolution metrics will be used, which will improve loading times.',
values: { betaLabel: `<em>[${betaLabel}]</em>` },
}),
schema: schema.boolean(),
requiresPageReload: true,
},
[enableCriticalPath]: {
category: [observabilityFeatureId],
name: i18n.translate('xpack.observability.enableCriticalPath', {

View file

@ -0,0 +1,298 @@
/*
* 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 moment from 'moment';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
import { ApmDocumentType } from '@kbn/apm-plugin/common/document_type';
import { RollupInterval } from '@kbn/apm-plugin/common/rollup';
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ApmApiError } from '../../common/apm_api_supertest';
type ServicesDetailedStatisticsReturn =
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
export default function ApiTest({ getService }: FtrProviderContext) {
const registry = getService('registry');
const apmApiClient = getService('apmApiClient');
const archiveName = 'apm_8.0.0';
const metadata = archives_metadata[archiveName];
const { start, end } = metadata;
const serviceNames = ['opbeans-java', 'opbeans-go'];
registry.when(
'Services detailed statistics when data is not loaded',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: '',
offset: '1d',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(response.body.currentPeriod).to.be.empty();
expect(response.body.previousPeriod).to.be.empty();
});
}
);
registry.when(
'Services detailed statistics when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
_inspect: true,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
servicesDetailedStatistics = response.body;
});
it('returns current period data', async () => {
expect(servicesDetailedStatistics.currentPeriod).not.to.be.empty();
});
it("doesn't returns previous period data", async () => {
expect(servicesDetailedStatistics.previousPeriod).to.be.empty();
});
it('returns current data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
});
});
it('returns correct statistics', () => {
const statistics = servicesDetailedStatistics.currentPeriod[serviceNames[0]];
expect(statistics.latency.length).to.be.greaterThan(0);
expect(statistics.throughput.length).to.be.greaterThan(0);
expect(statistics.transactionErrorRate.length).to.be.greaterThan(0);
// latency
const nonNullLantencyDataPoints = statistics.latency.filter(({ y }) => isFiniteNumber(y));
expect(nonNullLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullThroughputDataPoints = statistics.throughput.filter(({ y }) =>
isFiniteNumber(y)
);
expect(nonNullThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullTransactionErrorRateDataPoints = statistics.transactionErrorRate.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
});
it('returns empty when empty service names is passed', async () => {
try {
await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify([]),
},
},
});
expect().fail('Expected API call to throw an error');
} catch (error: unknown) {
const apiError = error as ApmApiError;
expect(apiError.res.status).eql(400);
expect(apiError.res.body.message).eql('serviceNames cannot be empty');
}
});
it('filters by environment', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'production',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(Object.keys(response.body.currentPeriod).length).to.be(1);
expect(response.body.currentPeriod['opbeans-java']).not.to.be.empty();
});
it('filters by kuery', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: 'transaction.type : "invalid_transaction_type"',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(Object.keys(response.body.currentPeriod)).to.be.empty();
});
}
);
registry.when(
'Services detailed statistics with time comparison',
{ config: 'basic', archives: [archiveName] },
() => {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
offset: '15m',
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
servicesDetailedStatistics = response.body;
});
it('returns current period data', async () => {
expect(servicesDetailedStatistics.currentPeriod).not.to.be.empty();
});
it('returns previous period data', async () => {
expect(servicesDetailedStatistics.previousPeriod).not.to.be.empty();
});
it('returns current data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
});
});
it('returns previous data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
});
});
it('returns correct statistics', () => {
const currentPeriodStatistics = servicesDetailedStatistics.currentPeriod[serviceNames[0]];
const previousPeriodStatistics = servicesDetailedStatistics.previousPeriod[serviceNames[0]];
expect(currentPeriodStatistics.latency.length).to.be.greaterThan(0);
expect(currentPeriodStatistics.throughput.length).to.be.greaterThan(0);
expect(currentPeriodStatistics.transactionErrorRate.length).to.be.greaterThan(0);
// latency
const nonNullCurrentPeriodLantencyDataPoints = currentPeriodStatistics.latency.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullCurrentPeriodLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullCurrentPeriodThroughputDataPoints = currentPeriodStatistics.throughput.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullCurrentPeriodThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullCurrentPeriodTransactionErrorRateDataPoints =
currentPeriodStatistics.transactionErrorRate.filter(({ y }) => isFiniteNumber(y));
expect(nonNullCurrentPeriodTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.latency.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.throughput.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.transactionErrorRate.length).to.be.greaterThan(0);
// latency
const nonNullPreviousPeriodLantencyDataPoints = previousPeriodStatistics.latency.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullPreviousPeriodLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullPreviousPeriodThroughputDataPoints =
previousPeriodStatistics.throughput.filter(({ y }) => isFiniteNumber(y));
expect(nonNullPreviousPeriodThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullPreviousPeriodTransactionErrorRateDataPoints =
previousPeriodStatistics.transactionErrorRate.filter(({ y }) => isFiniteNumber(y));
expect(nonNullPreviousPeriodTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
});
}
);
}

View file

@ -5,14 +5,15 @@
* 2.0.
*/
import expect from '@kbn/expect';
import moment from 'moment';
import { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { isFiniteNumber } from '@kbn/apm-plugin/common/utils/is_finite_number';
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 archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
import { apm, timerange } from '@kbn/apm-synthtrace-client';
import { uniq, map } from 'lodash';
import { FtrProviderContext } from '../../common/ftr_provider_context';
import { ApmApiError } from '../../common/apm_api_supertest';
type ServicesDetailedStatisticsReturn =
APIReturnType<'POST /internal/apm/services/detailed_statistics'>;
@ -22,49 +23,18 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const apmApiClient = getService('apmApiClient');
const archiveName = 'apm_8.0.0';
const metadata = archives_metadata[archiveName];
const { start, end } = metadata;
const serviceNames = ['opbeans-java', 'opbeans-go'];
const synthtrace = getService('synthtraceEsClient');
const start = '2021-01-01T00:00:00.000Z';
const end = '2021-01-01T00:59:59.999Z';
const serviceNames = ['my-service'];
registry.when(
'Services detailed statistics when data is not loaded',
'Services detailed statistics when data is generated',
{ config: 'basic', archives: [] },
() => {
it('handles the empty state', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: '',
offset: '1d',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(response.body.currentPeriod).to.be.empty();
expect(response.body.previousPeriod).to.be.empty();
});
}
);
registry.when(
'Services detailed statistics when data is loaded',
{ config: 'basic', archives: [archiveName] },
() => {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
@ -86,212 +56,132 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
expect(response.status).to.be(200);
servicesDetailedStatistics = response.body;
});
it('returns current period data', async () => {
expect(servicesDetailedStatistics.currentPeriod).not.to.be.empty();
});
it("doesn't returns previous period data", async () => {
expect(servicesDetailedStatistics.previousPeriod).to.be.empty();
});
it('returns current data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
});
});
it('returns correct statistics', () => {
const statistics = servicesDetailedStatistics.currentPeriod[serviceNames[0]];
expect(statistics.latency.length).to.be.greaterThan(0);
expect(statistics.throughput.length).to.be.greaterThan(0);
expect(statistics.transactionErrorRate.length).to.be.greaterThan(0);
// latency
const nonNullLantencyDataPoints = statistics.latency.filter(({ y }) => isFiniteNumber(y));
expect(nonNullLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullThroughputDataPoints = statistics.throughput.filter(({ y }) =>
isFiniteNumber(y)
);
expect(nonNullThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullTransactionErrorRateDataPoints = statistics.transactionErrorRate.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
});
it('returns empty when empty service names is passed', async () => {
try {
await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify([]),
},
},
});
expect().fail('Expected API call to throw an error');
} catch (error: unknown) {
const apiError = error as ApmApiError;
expect(apiError.res.status).eql(400);
expect(apiError.res.body.message).eql('serviceNames cannot be empty');
}
});
it('filters by environment', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'production',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(Object.keys(response.body.currentPeriod).length).to.be(1);
expect(response.body.currentPeriod['opbeans-java']).not.to.be.empty();
});
it('filters by kuery', async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start,
end,
environment: 'ENVIRONMENT_ALL',
kuery: 'transaction.type : "invalid_transaction_type"',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
});
expect(response.status).to.be(200);
expect(Object.keys(response.body.currentPeriod)).to.be.empty();
expect(response.body.currentPeriod).to.be.empty();
expect(response.body.previousPeriod).to.be.empty();
});
}
);
async function getStats(
overrides?: Partial<
APIClientRequestParamsOf<'POST /internal/apm/services/detailed_statistics'>['params']['query']
>
) {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/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 detailed statistics with time comparison',
{ config: 'basic', archives: [archiveName] },
'Services detailed statistics when data is generated',
{ config: 'basic', archives: [] },
() => {
let servicesDetailedStatistics: ServicesDetailedStatisticsReturn;
const instance = apm.service('my-service', 'production', 'java').instance('instance');
const EXPECTED_TPM = 5;
const EXPECTED_LATENCY = 1000;
const EXPECTED_FAILURE_RATE = 0.25;
before(async () => {
const response = await apmApiClient.readUser({
endpoint: `POST /internal/apm/services/detailed_statistics`,
params: {
query: {
start: moment(end).subtract(15, 'minutes').toISOString(),
end,
offset: '15m',
environment: 'ENVIRONMENT_ALL',
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
bucketSizeInSeconds: 60,
},
body: {
serviceNames: JSON.stringify(serviceNames),
},
},
const interval = timerange(new Date(start).getTime(), new Date(end).getTime() - 1).interval(
'1m'
);
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());
function checkStats() {
const stats = servicesDetailedStatistics.currentPeriod['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'
);
}
describe('and transaction metrics are used', () => {
before(async () => {
servicesDetailedStatistics = await getStats();
});
expect(response.status).to.be(200);
servicesDetailedStatistics = response.body;
});
it('returns current period data', async () => {
expect(servicesDetailedStatistics.currentPeriod).not.to.be.empty();
});
it('returns previous period data', async () => {
expect(servicesDetailedStatistics.previousPeriod).not.to.be.empty();
});
it('returns current data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
it('returns the expected statistics', () => {
checkStats();
});
});
it('returns previous data for requested service names', () => {
serviceNames.forEach((serviceName) => {
expect(servicesDetailedStatistics.currentPeriod[serviceName]).not.to.be.empty();
describe('and service transaction metrics are used', () => {
before(async () => {
servicesDetailedStatistics = await getStats({
documentType: ApmDocumentType.ServiceTransactionMetric,
});
});
it('returns the expected statistics', () => {
checkStats();
});
});
it('returns correct statistics', () => {
const currentPeriodStatistics = servicesDetailedStatistics.currentPeriod[serviceNames[0]];
const previousPeriodStatistics = servicesDetailedStatistics.previousPeriod[serviceNames[0]];
expect(currentPeriodStatistics.latency.length).to.be.greaterThan(0);
expect(currentPeriodStatistics.throughput.length).to.be.greaterThan(0);
expect(currentPeriodStatistics.transactionErrorRate.length).to.be.greaterThan(0);
describe('and rolled up data is used', () => {
before(async () => {
servicesDetailedStatistics = await getStats({
rollupInterval: RollupInterval.TenMinutes,
bucketSizeInSeconds: 600,
});
});
// latency
const nonNullCurrentPeriodLantencyDataPoints = currentPeriodStatistics.latency.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullCurrentPeriodLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullCurrentPeriodThroughputDataPoints = currentPeriodStatistics.throughput.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullCurrentPeriodThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullCurrentPeriodTransactionErrorRateDataPoints =
currentPeriodStatistics.transactionErrorRate.filter(({ y }) => isFiniteNumber(y));
expect(nonNullCurrentPeriodTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.latency.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.throughput.length).to.be.greaterThan(0);
expect(previousPeriodStatistics.transactionErrorRate.length).to.be.greaterThan(0);
// latency
const nonNullPreviousPeriodLantencyDataPoints = previousPeriodStatistics.latency.filter(
({ y }) => isFiniteNumber(y)
);
expect(nonNullPreviousPeriodLantencyDataPoints.length).to.be.greaterThan(0);
// throughput
const nonNullPreviousPeriodThroughputDataPoints =
previousPeriodStatistics.throughput.filter(({ y }) => isFiniteNumber(y));
expect(nonNullPreviousPeriodThroughputDataPoints.length).to.be.greaterThan(0);
// transaction erro rate
const nonNullPreviousPeriodTransactionErrorRateDataPoints =
previousPeriodStatistics.transactionErrorRate.filter(({ y }) => isFiniteNumber(y));
expect(nonNullPreviousPeriodTransactionErrorRateDataPoints.length).to.be.greaterThan(0);
it('returns the expected statistics', () => {
checkStats();
});
});
}
);

View file

@ -31,7 +31,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
const archiveEnd = archiveRange.end;
const start = '2021-10-01T00:00:00.000Z';
const end = '2021-10-01T00:05:00.000Z';
const end = '2021-10-01T01:00:00.000Z';
registry.when(
'APM Services Overview with a basic license when data is not generated',
@ -105,6 +105,29 @@ export default function ApiTest({ getService }: FtrProviderContext) {
},
};
function checkStats() {
const multipleEnvService = response.body.items.find(
(item) => item.serviceName === 'multiple-env-service'
);
const totalRps = config.multiple.prod.rps + config.multiple.dev.rps;
expect(multipleEnvService).to.eql({
serviceName: 'multiple-env-service',
transactionType: 'request',
environments: ['production', 'development'],
agentName: 'go',
latency:
1000 *
((config.multiple.prod.duration * config.multiple.prod.rps +
config.multiple.dev.duration * config.multiple.dev.rps) /
totalRps),
throughput: totalRps * 60,
transactionErrorRate:
config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps),
});
}
before(async () => {
return synthtrace.index([
transactionInterval
@ -179,26 +202,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
});
it('returns the correct statistics', () => {
const multipleEnvService = response.body.items.find(
(item) => item.serviceName === 'multiple-env-service'
);
const totalRps = config.multiple.prod.rps + config.multiple.dev.rps;
expect(multipleEnvService).to.eql({
serviceName: 'multiple-env-service',
transactionType: 'request',
environments: ['production', 'development'],
agentName: 'go',
latency:
1000 *
((config.multiple.prod.duration * config.multiple.prod.rps +
config.multiple.dev.duration * config.multiple.dev.rps) /
totalRps),
throughput: totalRps * 60,
transactionErrorRate:
config.multiple.dev.rps / (config.multiple.prod.rps + config.multiple.dev.rps),
});
checkStats();
});
it('returns services without transaction data', () => {
@ -310,6 +314,60 @@ export default function ApiTest({ getService }: FtrProviderContext) {
expect(multipleEnvService?.transactionType).to.eql('rpc');
});
});
describe('when using service transaction metrics', () => {
before(async () => {
response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services',
params: {
query: {
start,
end,
environment: ENVIRONMENT_ALL.value,
kuery: '',
probability: 1,
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.OneMinute,
},
},
});
});
it('returns services without transaction data', () => {
const serviceNames = response.body.items.map((item) => item.serviceName);
expect(serviceNames).to.contain('metric-only-service');
expect(serviceNames).to.contain('error-only-service');
});
it('returns the correct statistics', () => {
checkStats();
});
});
describe('when using rolled up data', () => {
before(async () => {
response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/services',
params: {
query: {
start,
end,
environment: ENVIRONMENT_ALL.value,
kuery: '',
probability: 1,
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
},
},
});
});
it('returns the correct statistics', () => {
checkStats();
});
});
}
);

View file

@ -0,0 +1,404 @@
/*
* 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 { apm, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { APIClientRequestParamsOf } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
import { omit, sortBy } from 'lodash';
import moment, { Moment } from 'moment';
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 synthtraceEsClient = getService('synthtraceEsClient');
const esClient = getService('es');
const start = moment('2022-01-01T00:00:00.000Z');
const end = moment('2022-01-02T00:00:00.000Z').subtract(1, 'millisecond');
async function getTimeRangeMedata(
overrides: Partial<
Omit<
APIClientRequestParamsOf<'GET /internal/apm/time_range_metadata'>['params']['query'],
'start' | 'end'
>
> & { start: Moment; end: Moment }
) {
const response = await apmApiClient.readUser({
endpoint: 'GET /internal/apm/time_range_metadata',
params: {
query: {
start: overrides.start.toISOString(),
end: overrides.end.toISOString(),
enableContinuousRollups: true,
enableServiceTransactionMetrics: true,
useSpanName: false,
kuery: '',
...omit(overrides, 'start', 'end'),
},
},
});
return {
...response.body,
sources: sortBy(response.body.sources, ['documentType', 'rollupInterval']),
};
}
registry.when('Time range metadata without data', { config: 'basic', archives: [] }, () => {
it('handles empty state', async () => {
const response = await getTimeRangeMedata({
start,
end,
});
expect(response.isUsingServiceDestinationMetrics).to.eql(false);
expect(response.sources.filter((source) => source.hasDocs)).to.eql([
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
]);
});
});
registry.when(
'Time range metadata when generating data',
{ config: 'basic', archives: [] },
() => {
before(() => {
const instance = apm.service('my-service', 'production', 'java').instance('instance');
return synthtraceEsClient.index(
timerange(moment(start).subtract(1, 'day'), end)
.interval('1m')
.rate(1)
.generator((timestamp) => {
return instance.transaction('GET /api').duration(100).timestamp(timestamp);
})
);
});
after(() => {
return synthtraceEsClient.clean();
});
describe('with default settings', () => {
it('returns all available document sources', async () => {
const response = await getTimeRangeMedata({
start,
end,
});
expect(response.sources).to.eql([
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
]);
});
});
describe('with continuous rollups disabled', () => {
it('returns only 1m intervals', async () => {
const response = await getTimeRangeMedata({
start,
end,
enableContinuousRollups: false,
});
expect(response.sources).to.eql([
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
]);
});
});
describe('with service metrics disabled', () => {
it('only returns tx metrics and events as available sources', async () => {
const response = await getTimeRangeMedata({
start,
end,
enableServiceTransactionMetrics: false,
});
expect(response.sources).to.eql([
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
]);
});
});
describe('when data is available before the time range', () => {
it('marks all those sources as available', async () => {
const response = await getTimeRangeMedata({
start: moment(start).add(12, 'hours'),
end: moment(end).add(12, 'hours'),
});
expect(response.sources).to.eql([
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
]);
});
});
describe('when data is not available before the time range, but is within the time range', () => {
it('marks those sources as available', async () => {
const response = await getTimeRangeMedata({
start: moment(start).add(6, 'hours'),
end: moment(end).add(6, 'hours'),
});
expect(response.sources).to.eql([
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.ServiceTransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionEvent,
rollupInterval: RollupInterval.None,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
]);
});
});
describe('when service metrics are only available in the current time range', () => {
before(async () => {
await esClient.deleteByQuery({
index: 'metrics-apm*',
query: {
bool: {
filter: [
{
terms: {
'metricset.name': ['service_transaction', 'service_summary'],
},
},
{
range: {
'@timestamp': {
lte: start.toISOString(),
},
},
},
],
},
},
refresh: true,
expand_wildcards: ['open', 'hidden'],
});
});
it('marks service transaction metrics as unavailable', async () => {
const response = await getTimeRangeMedata({
start,
end,
});
expect(
response.sources.filter(
(source) =>
source.documentType === ApmDocumentType.ServiceTransactionMetric &&
source.hasDocs === false
).length
).to.eql(3);
expect(
response.sources.filter(
(source) =>
source.documentType === ApmDocumentType.TransactionMetric && source.hasDocs === true
).length
).to.eql(3);
});
});
describe('after deleting a specific data set', () => {
before(async () => {
await esClient.deleteByQuery({
index: 'metrics-apm*',
query: {
bool: {
filter: [
{
terms: {
'metricset.name': ['transaction'],
},
},
{
term: {
'metricset.interval': '1m',
},
},
],
},
},
refresh: true,
expand_wildcards: ['open', 'hidden'],
});
});
it('marks that data source as unavailable', async () => {
const response = await getTimeRangeMedata({
start,
end,
});
expect(
response.sources.filter(
(source) => source.documentType === ApmDocumentType.TransactionMetric
)
).to.eql([
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.TenMinutes,
hasDocs: true,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.OneMinute,
hasDocs: false,
},
{
documentType: ApmDocumentType.TransactionMetric,
rollupInterval: RollupInterval.SixtyMinutes,
hasDocs: true,
},
]);
});
});
after(() => synthtraceEsClient.clean());
}
);
}