mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 09:19:04 -04:00
[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:
parent
9119a00a60
commit
b96d46c06b
38 changed files with 1476 additions and 368 deletions
|
@ -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));
|
||||
}
|
||||
|
|
|
@ -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
|
||||
);
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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;
|
|
@ -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 {
|
||||
|
|
|
@ -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')
|
||||
|
|
|
@ -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.' },
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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": {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -10,4 +10,10 @@ export enum ApmDocumentType {
|
|||
ServiceTransactionMetric = 'serviceTransactionMetric',
|
||||
TransactionEvent = 'transactionEvent',
|
||||
ServiceDestinationMetric = 'serviceDestinationMetric',
|
||||
ServiceSummaryMetric = 'serviceSummaryMetric',
|
||||
}
|
||||
|
||||
export type ApmServiceTransactionDocumentType =
|
||||
| ApmDocumentType.ServiceTransactionMetric
|
||||
| ApmDocumentType.TransactionMetric
|
||||
| ApmDocumentType.TransactionEvent;
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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 (
|
||||
|
|
|
@ -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: {
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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) => ({
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -41,7 +41,10 @@ interface AggregationParams {
|
|||
end: number;
|
||||
serviceGroup: ServiceGroup | null;
|
||||
randomSampler: RandomSampler;
|
||||
documentType: ApmDocumentType;
|
||||
documentType:
|
||||
| ApmDocumentType.ServiceTransactionMetric
|
||||
| ApmDocumentType.TransactionMetric
|
||||
| ApmDocumentType.TransactionEvent;
|
||||
rollupInterval: RollupInterval;
|
||||
}
|
||||
|
||||
|
|
|
@ -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,
|
||||
}) ?? [],
|
||||
|
|
|
@ -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,
|
|
@ -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',
|
||||
|
|
|
@ -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),
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
]);
|
||||
|
||||
|
|
|
@ -27,6 +27,8 @@ export {
|
|||
enableAgentExplorerView,
|
||||
apmAWSLambdaPriceFactor,
|
||||
apmAWSLambdaRequestCostPerMillion,
|
||||
apmEnableServiceMetrics,
|
||||
apmEnableContinuousRollups,
|
||||
enableCriticalPath,
|
||||
profilingElasticsearchPlugin,
|
||||
} from './ui_settings_keys';
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -150,7 +150,7 @@ export function getInspectResponse({
|
|||
|
||||
return {
|
||||
id,
|
||||
json: esRequestParams.body,
|
||||
json: esRequestParams.body ?? esRequestParams,
|
||||
name: id,
|
||||
response: {
|
||||
json: esError ? esError.originalError : esResponse,
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -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();
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
|
|
|
@ -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());
|
||||
}
|
||||
);
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue