mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
# Backport This will backport the following commits from `main` to `8.18`: - [[APM] Breakdown Top dependencies API (#211441)](https://github.com/elastic/kibana/pull/211441) <!--- Backport version: 9.6.6 --> ### Questions ? Please refer to the [Backport tool documentation](https://github.com/sorenlouv/backport) <!--BACKPORT [{"author":{"name":"Cauê Marcondes","email":"55978943+cauemarcondes@users.noreply.github.com"},"sourceCommit":{"committedDate":"2025-03-10T16:24:37Z","message":"[APM] Breakdown Top dependencies API (#211441)\n\ncloses https://github.com/elastic/kibana/issues/210552\n\nBefore:\n- Top dependencies API returned baseline and comparison timeseries data.\n\n<img width=\"1208\" alt=\"Screenshot 2025-02-14 at 14 27 28\"\nsrc=\"https://github.com/user-attachments/assets/f7770395-0575-4950-9acd-8808de5794b7\"\n/>\n\n\nAfter:\n- Removing timeseries and comparison data.\n- The API is ~2s faster than before.\n- Response size is also smaller after removing the timeseries data.\n\n<img width=\"1203\" alt=\"Screenshot 2025-02-14 at 14 26 34\"\nsrc=\"https://github.com/user-attachments/assets/5bd2ed09-1d2e-4ef1-8e55-6c3e9fba6348\"\n/>\n\nCreated a new API: `POST\n/internal/apm/dependencies/top_dependencies/statistics` to fetch the\nstatistics for the visible dependencies.\n\n---------\n\nCo-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>\nCo-authored-by: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com>","sha":"a6fd5b7e101b7e0d13b15220a247d4a29e5c0405","branchLabelMapping":{"^v9.1.0$":"main","^v8.19.0$":"8.x","^v(\\d+).(\\d+).\\d+$":"$1.$2"}},"sourcePullRequest":{"labels":["release_note:enhancement","backport:prev-minor","backport:prev-major","Team:obs-ux-infra_services","v9.1.0"],"title":"[APM] Breakdown Top dependencies API","number":211441,"url":"https://github.com/elastic/kibana/pull/211441","mergeCommit":{"message":"[APM] Breakdown Top dependencies API (#211441)\n\ncloses https://github.com/elastic/kibana/issues/210552\n\nBefore:\n- Top dependencies API returned baseline and comparison timeseries data.\n\n<img width=\"1208\" alt=\"Screenshot 2025-02-14 at 14 27 28\"\nsrc=\"https://github.com/user-attachments/assets/f7770395-0575-4950-9acd-8808de5794b7\"\n/>\n\n\nAfter:\n- Removing timeseries and comparison data.\n- The API is ~2s faster than before.\n- Response size is also smaller after removing the timeseries data.\n\n<img width=\"1203\" alt=\"Screenshot 2025-02-14 at 14 26 34\"\nsrc=\"https://github.com/user-attachments/assets/5bd2ed09-1d2e-4ef1-8e55-6c3e9fba6348\"\n/>\n\nCreated a new API: `POST\n/internal/apm/dependencies/top_dependencies/statistics` to fetch the\nstatistics for the visible dependencies.\n\n---------\n\nCo-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>\nCo-authored-by: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com>","sha":"a6fd5b7e101b7e0d13b15220a247d4a29e5c0405"}},"sourceBranch":"main","suggestedTargetBranches":[],"targetPullRequestStates":[{"branch":"main","label":"v9.1.0","branchLabelMappingKey":"^v9.1.0$","isSourceBranch":true,"state":"MERGED","url":"https://github.com/elastic/kibana/pull/211441","number":211441,"mergeCommit":{"message":"[APM] Breakdown Top dependencies API (#211441)\n\ncloses https://github.com/elastic/kibana/issues/210552\n\nBefore:\n- Top dependencies API returned baseline and comparison timeseries data.\n\n<img width=\"1208\" alt=\"Screenshot 2025-02-14 at 14 27 28\"\nsrc=\"https://github.com/user-attachments/assets/f7770395-0575-4950-9acd-8808de5794b7\"\n/>\n\n\nAfter:\n- Removing timeseries and comparison data.\n- The API is ~2s faster than before.\n- Response size is also smaller after removing the timeseries data.\n\n<img width=\"1203\" alt=\"Screenshot 2025-02-14 at 14 26 34\"\nsrc=\"https://github.com/user-attachments/assets/5bd2ed09-1d2e-4ef1-8e55-6c3e9fba6348\"\n/>\n\nCreated a new API: `POST\n/internal/apm/dependencies/top_dependencies/statistics` to fetch the\nstatistics for the visible dependencies.\n\n---------\n\nCo-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>\nCo-authored-by: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com>","sha":"a6fd5b7e101b7e0d13b15220a247d4a29e5c0405"}}]}] BACKPORT--> --------- Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
85089b2030
commit
74e4ea6c2e
17 changed files with 975 additions and 513 deletions
|
@ -36,19 +36,19 @@ export type Node = ServiceNode | DependencyNode;
|
|||
export interface ConnectionStats {
|
||||
latency: {
|
||||
value: number | null;
|
||||
timeseries: Coordinate[];
|
||||
timeseries?: Coordinate[];
|
||||
};
|
||||
throughput: {
|
||||
value: number | null;
|
||||
timeseries: Coordinate[];
|
||||
timeseries?: Coordinate[];
|
||||
};
|
||||
errorRate: {
|
||||
value: number | null;
|
||||
timeseries: Coordinate[];
|
||||
timeseries?: Coordinate[];
|
||||
};
|
||||
totalTime: {
|
||||
value: number | null;
|
||||
timeseries: Coordinate[];
|
||||
timeseries?: Coordinate[];
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -4,25 +4,44 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useEuiTheme } from '@elastic/eui';
|
||||
import { css } from '@emotion/react';
|
||||
import { METRIC_TYPE } from '@kbn/analytics';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useUiTracker } from '@kbn/observability-shared-plugin/public';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { getNodeName, NodeType } from '../../../../../common/connections';
|
||||
import { orderBy } from 'lodash';
|
||||
import React, { useMemo } from 'react';
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
import { NodeType, getNodeName } from '../../../../../common/connections';
|
||||
import { useApmParams } from '../../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { FETCH_STATUS, isPending, useFetcher } from '../../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../../hooks/use_time_range';
|
||||
import type { DependenciesItem } from '../../../shared/dependencies_table';
|
||||
import {
|
||||
DependenciesTable,
|
||||
INITIAL_SORTING_FIELD,
|
||||
INITIAL_SORTING_DIRECTION,
|
||||
} from '../../../shared/dependencies_table';
|
||||
import { DependencyLink } from '../../../shared/links/dependency_link';
|
||||
import { DependenciesTable } from '../../../shared/dependencies_table';
|
||||
import { isTimeComparison } from '../../../shared/time_comparison/get_comparison_options';
|
||||
import { RandomSamplerBadge } from '../random_sampler_badge';
|
||||
|
||||
const INITIAL_PAGE_SIZE = 25;
|
||||
|
||||
export function DependenciesInventoryTable() {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo, environment, kuery, comparisonEnabled, offset },
|
||||
query: {
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
environment,
|
||||
kuery,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
page = 0,
|
||||
pageSize = INITIAL_PAGE_SIZE,
|
||||
sortDirection = INITIAL_SORTING_DIRECTION,
|
||||
sortField = INITIAL_SORTING_FIELD,
|
||||
},
|
||||
} = useApmParams('/dependencies/inventory');
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
@ -37,61 +56,150 @@ export function DependenciesInventoryTable() {
|
|||
}
|
||||
|
||||
return callApmApi('GET /internal/apm/dependencies/top_dependencies', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
numBuckets: 8,
|
||||
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
|
||||
kuery,
|
||||
},
|
||||
},
|
||||
params: { query: { start, end, environment, numBuckets: 8, kuery } },
|
||||
}).then((response) => {
|
||||
return {
|
||||
...response,
|
||||
requestId: uuidv4(),
|
||||
};
|
||||
});
|
||||
},
|
||||
[start, end, environment, offset, kuery, comparisonEnabled]
|
||||
[start, end, environment, kuery]
|
||||
);
|
||||
|
||||
const dependencies =
|
||||
data?.dependencies.map((dependency) => {
|
||||
const { location } = dependency;
|
||||
const name = getNodeName(location);
|
||||
const visibleDependenciesNames = useMemo(
|
||||
() =>
|
||||
data?.dependencies
|
||||
? orderBy(
|
||||
data.dependencies.map((item) => ({
|
||||
name: getNodeName(item.location),
|
||||
impact: item.currentStats.impact,
|
||||
latency: item.currentStats.latency.value,
|
||||
throughput: item.currentStats.throughput.value,
|
||||
failureRate: item.currentStats.errorRate.value,
|
||||
})),
|
||||
sortField,
|
||||
sortDirection
|
||||
)
|
||||
.slice(page * pageSize, (page + 1) * pageSize)
|
||||
.map(({ name }) => name)
|
||||
.sort()
|
||||
: undefined,
|
||||
[data?.dependencies, page, pageSize, sortDirection, sortField]
|
||||
);
|
||||
|
||||
if (location.type !== NodeType.dependency) {
|
||||
throw new Error('Expected a dependency node');
|
||||
const { data: timeseriesData, status: timeseriesStatus } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (data?.requestId && visibleDependenciesNames?.length) {
|
||||
return callApmApi('POST /internal/apm/dependencies/top_dependencies/statistics', {
|
||||
params: {
|
||||
query: {
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
numBuckets: 8,
|
||||
offset: comparisonEnabled && isTimeComparison(offset) ? offset : undefined,
|
||||
kuery,
|
||||
},
|
||||
body: {
|
||||
dependencyNames: JSON.stringify(visibleDependenciesNames),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
const link = (
|
||||
<DependencyLink
|
||||
type={location.spanType}
|
||||
subtype={location.spanSubtype}
|
||||
query={{
|
||||
dependencyName: location.dependencyName,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
environment,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
}}
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
app: 'apm',
|
||||
metricType: METRIC_TYPE.CLICK,
|
||||
metric: 'dependencies_inventory_to_dependency_detail',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
// Disables exhaustive deps because the statistics api must only be called when the rendered items changed or when comparison is toggled or changed.
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[data?.requestId, visibleDependenciesNames, comparisonEnabled, offset],
|
||||
// Do not invalidate this API call when the refresh button is clicked
|
||||
{ skipTimeRangeRefreshUpdate: true }
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
currentStats: dependency.currentStats,
|
||||
previousStats: dependency.previousStats,
|
||||
link,
|
||||
};
|
||||
}) ?? [];
|
||||
const dependencies: DependenciesItem[] = useMemo(
|
||||
() =>
|
||||
data?.dependencies.map((dependency) => {
|
||||
const { location } = dependency;
|
||||
const name = getNodeName(location);
|
||||
|
||||
if (location.type !== NodeType.dependency) {
|
||||
throw new Error('Expected a dependency node');
|
||||
}
|
||||
const link = (
|
||||
<DependencyLink
|
||||
type={location.spanType}
|
||||
subtype={location.spanSubtype}
|
||||
query={{
|
||||
dependencyName: location.dependencyName,
|
||||
comparisonEnabled,
|
||||
offset,
|
||||
environment,
|
||||
kuery,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
}}
|
||||
onClick={() => {
|
||||
trackEvent({
|
||||
app: 'apm',
|
||||
metricType: METRIC_TYPE.CLICK,
|
||||
metric: 'dependencies_inventory_to_dependency_detail',
|
||||
});
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
return {
|
||||
name,
|
||||
currentStats: {
|
||||
impact: dependency.currentStats.impact,
|
||||
totalTime: { value: dependency.currentStats.totalTime.value },
|
||||
latency: {
|
||||
value: dependency.currentStats.latency.value,
|
||||
timeseries: timeseriesData?.currentTimeseries[name]?.latency,
|
||||
},
|
||||
throughput: {
|
||||
value: dependency.currentStats.throughput.value,
|
||||
timeseries: timeseriesData?.currentTimeseries[name]?.throughput,
|
||||
},
|
||||
errorRate: {
|
||||
value: dependency.currentStats.errorRate.value,
|
||||
timeseries: timeseriesData?.currentTimeseries[name]?.errorRate,
|
||||
},
|
||||
},
|
||||
previousStats: {
|
||||
impact: dependency.previousStats?.impact ?? 0,
|
||||
totalTime: { value: dependency.previousStats?.totalTime.value ?? null },
|
||||
latency: {
|
||||
value: dependency.previousStats?.latency.value ?? null,
|
||||
timeseries: timeseriesData?.comparisonTimeseries?.[name]?.latency,
|
||||
},
|
||||
throughput: {
|
||||
value: dependency.previousStats?.throughput.value ?? null,
|
||||
timeseries: timeseriesData?.comparisonTimeseries?.[name]?.throughput,
|
||||
},
|
||||
errorRate: {
|
||||
value: dependency.previousStats?.errorRate.value ?? null,
|
||||
timeseries: timeseriesData?.comparisonTimeseries?.[name]?.errorRate,
|
||||
},
|
||||
},
|
||||
link,
|
||||
};
|
||||
}) ?? [],
|
||||
[
|
||||
comparisonEnabled,
|
||||
data?.dependencies,
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
timeseriesData?.comparisonTimeseries,
|
||||
timeseriesData?.currentTimeseries,
|
||||
trackEvent,
|
||||
]
|
||||
);
|
||||
const showRandomSamplerBadge = data?.sampled && status === FETCH_STATUS.SUCCESS;
|
||||
const fetchingStatus =
|
||||
isPending(status) || isPending(timeseriesStatus) ? FETCH_STATUS.LOADING : FETCH_STATUS.SUCCESS;
|
||||
|
||||
return (
|
||||
<>
|
||||
|
@ -109,9 +217,9 @@ export function DependenciesInventoryTable() {
|
|||
nameColumnTitle={i18n.translate('xpack.apm.dependenciesInventory.dependencyTableColumn', {
|
||||
defaultMessage: 'Dependency',
|
||||
})}
|
||||
status={status}
|
||||
status={fetchingStatus}
|
||||
compact={false}
|
||||
initialPageSize={25}
|
||||
initialPageSize={INITIAL_PAGE_SIZE}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -28,17 +28,17 @@ export interface SpanMetricGroup {
|
|||
impact: number | null;
|
||||
currentStats:
|
||||
| {
|
||||
latency: Coordinate[];
|
||||
throughput: Coordinate[];
|
||||
failureRate: Coordinate[];
|
||||
latency?: Coordinate[];
|
||||
throughput?: Coordinate[];
|
||||
failureRate?: Coordinate[];
|
||||
}
|
||||
| undefined;
|
||||
previousStats:
|
||||
| {
|
||||
latency: Coordinate[];
|
||||
throughput: Coordinate[];
|
||||
failureRate: Coordinate[];
|
||||
impact: number;
|
||||
latency?: Coordinate[];
|
||||
throughput?: Coordinate[];
|
||||
failureRate?: Coordinate[];
|
||||
impact?: number;
|
||||
}
|
||||
| undefined;
|
||||
}
|
||||
|
|
|
@ -7,7 +7,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiTitle } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import React, { useMemo } from 'react';
|
||||
import type { ConnectionStatsItemWithComparisonData } from '../../../../common/connections';
|
||||
import { useBreakpoints } from '../../../hooks/use_breakpoints';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
|
@ -24,6 +24,8 @@ export type DependenciesItem = Omit<ConnectionStatsItemWithComparisonData, 'loca
|
|||
link: React.ReactElement;
|
||||
};
|
||||
|
||||
export const INITIAL_SORTING_FIELD = 'impact';
|
||||
export const INITIAL_SORTING_DIRECTION = 'desc';
|
||||
interface Props {
|
||||
dependencies: DependenciesItem[];
|
||||
initialPageSize: number;
|
||||
|
@ -35,51 +37,55 @@ interface Props {
|
|||
compact?: boolean;
|
||||
showPerPageOptions?: boolean;
|
||||
showSparkPlots?: boolean;
|
||||
onChangeRenderedItems?: (items: FormattedSpanMetricGroup[]) => void;
|
||||
}
|
||||
|
||||
type FormattedSpanMetricGroup = SpanMetricGroup & {
|
||||
export type FormattedSpanMetricGroup = SpanMetricGroup & {
|
||||
name: string;
|
||||
link: React.ReactElement;
|
||||
};
|
||||
|
||||
export function DependenciesTable(props: Props) {
|
||||
const {
|
||||
dependencies,
|
||||
fixedHeight,
|
||||
link,
|
||||
title,
|
||||
nameColumnTitle,
|
||||
status,
|
||||
compact = true,
|
||||
showPerPageOptions = true,
|
||||
initialPageSize,
|
||||
showSparkPlots,
|
||||
} = props;
|
||||
|
||||
export function DependenciesTable({
|
||||
dependencies,
|
||||
fixedHeight,
|
||||
link,
|
||||
title,
|
||||
nameColumnTitle,
|
||||
status,
|
||||
compact = true,
|
||||
showPerPageOptions = true,
|
||||
initialPageSize,
|
||||
showSparkPlots,
|
||||
onChangeRenderedItems,
|
||||
}: Props) {
|
||||
const { isLarge } = useBreakpoints();
|
||||
const shouldShowSparkPlots = showSparkPlots ?? !isLarge;
|
||||
|
||||
const items: FormattedSpanMetricGroup[] = dependencies.map((dependency) => ({
|
||||
name: dependency.name,
|
||||
link: dependency.link,
|
||||
latency: dependency.currentStats.latency.value,
|
||||
throughput: dependency.currentStats.throughput.value,
|
||||
failureRate: dependency.currentStats.errorRate.value,
|
||||
impact: dependency.currentStats.impact,
|
||||
currentStats: {
|
||||
latency: dependency.currentStats.latency.timeseries,
|
||||
throughput: dependency.currentStats.throughput.timeseries,
|
||||
failureRate: dependency.currentStats.errorRate.timeseries,
|
||||
},
|
||||
previousStats: dependency.previousStats
|
||||
? {
|
||||
latency: dependency.previousStats.latency.timeseries,
|
||||
throughput: dependency.previousStats.throughput.timeseries,
|
||||
failureRate: dependency.previousStats.errorRate.timeseries,
|
||||
impact: dependency.previousStats.impact,
|
||||
}
|
||||
: undefined,
|
||||
}));
|
||||
const items: FormattedSpanMetricGroup[] = useMemo(
|
||||
() =>
|
||||
dependencies.map((dependency) => ({
|
||||
name: dependency.name,
|
||||
link: dependency.link,
|
||||
latency: dependency.currentStats.latency.value,
|
||||
throughput: dependency.currentStats.throughput.value,
|
||||
failureRate: dependency.currentStats.errorRate.value,
|
||||
impact: dependency.currentStats.impact,
|
||||
currentStats: {
|
||||
latency: dependency.currentStats.latency.timeseries,
|
||||
throughput: dependency.currentStats.throughput.timeseries,
|
||||
failureRate: dependency.currentStats.errorRate.timeseries,
|
||||
},
|
||||
previousStats: dependency.previousStats
|
||||
? {
|
||||
latency: dependency.previousStats.latency.timeseries,
|
||||
throughput: dependency.previousStats.throughput.timeseries,
|
||||
failureRate: dependency.previousStats.errorRate.timeseries,
|
||||
impact: dependency.previousStats.impact,
|
||||
}
|
||||
: undefined,
|
||||
})),
|
||||
[dependencies]
|
||||
);
|
||||
|
||||
const columns: Array<ITableColumn<FormattedSpanMetricGroup>> = [
|
||||
{
|
||||
|
@ -133,11 +139,12 @@ export function DependenciesTable(props: Props) {
|
|||
columns={columns}
|
||||
items={items}
|
||||
noItemsMessage={noItemsMessage}
|
||||
initialSortField="impact"
|
||||
initialSortDirection="desc"
|
||||
initialSortField={INITIAL_SORTING_FIELD}
|
||||
initialSortDirection={INITIAL_SORTING_DIRECTION}
|
||||
pagination={true}
|
||||
showPerPageOptions={showPerPageOptions}
|
||||
initialPageSize={initialPageSize}
|
||||
onChangeRenderedItems={onChangeRenderedItems}
|
||||
/>
|
||||
</OverviewTableContainer>
|
||||
</EuiFlexItem>
|
||||
|
|
|
@ -87,6 +87,7 @@ export function useFetcher<TReturn>(
|
|||
options: {
|
||||
preservePreviousData?: boolean;
|
||||
showToastOnError?: boolean;
|
||||
skipTimeRangeRefreshUpdate?: boolean;
|
||||
} = {}
|
||||
): FetcherResult<InferResponseType<TReturn>> & { refetch: () => void } {
|
||||
const { notifications } = useKibana();
|
||||
|
@ -99,6 +100,21 @@ export function useFetcher<TReturn>(
|
|||
const { timeRangeId } = useTimeRangeId();
|
||||
const { addInspectorRequest } = useInspectorContext();
|
||||
|
||||
const deps = useMemo(() => {
|
||||
const _deps = [counter, preservePreviousData, showToastOnError, ...fnDeps];
|
||||
if (options.skipTimeRangeRefreshUpdate !== true) {
|
||||
_deps.push(timeRangeId);
|
||||
}
|
||||
return _deps;
|
||||
}, [
|
||||
counter,
|
||||
fnDeps,
|
||||
options.skipTimeRangeRefreshUpdate,
|
||||
preservePreviousData,
|
||||
showToastOnError,
|
||||
timeRangeId,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
let controller: AbortController = new AbortController();
|
||||
|
||||
|
@ -176,14 +192,7 @@ export function useFetcher<TReturn>(
|
|||
controller.abort();
|
||||
};
|
||||
/* eslint-disable react-hooks/exhaustive-deps */
|
||||
}, [
|
||||
counter,
|
||||
preservePreviousData,
|
||||
timeRangeId,
|
||||
showToastOnError,
|
||||
...fnDeps,
|
||||
/* eslint-enable react-hooks/exhaustive-deps */
|
||||
]);
|
||||
}, deps);
|
||||
|
||||
return useMemo(() => {
|
||||
return {
|
||||
|
|
|
@ -5,7 +5,6 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { sum } from 'lodash';
|
||||
import objectHash from 'object-hash';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
|
@ -41,6 +40,7 @@ export const getStats = async ({
|
|||
filter,
|
||||
numBuckets,
|
||||
offset,
|
||||
withTimeseries,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
|
@ -48,6 +48,7 @@ export const getStats = async ({
|
|||
filter: QueryDslQueryContainer[];
|
||||
numBuckets: number;
|
||||
offset?: string;
|
||||
withTimeseries: boolean;
|
||||
}) => {
|
||||
const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({
|
||||
start,
|
||||
|
@ -61,6 +62,7 @@ export const getStats = async ({
|
|||
endWithOffset,
|
||||
filter,
|
||||
numBuckets,
|
||||
withTimeseries,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -85,27 +87,15 @@ export const getStats = async ({
|
|||
type: NodeType.dependency as const,
|
||||
},
|
||||
value: {
|
||||
count: sum(bucket.timeseries.buckets.map((dateBucket) => dateBucket.count.value ?? 0)),
|
||||
latency_sum: sum(
|
||||
bucket.timeseries.buckets.map((dateBucket) => dateBucket.latency_sum.value ?? 0)
|
||||
),
|
||||
error_count: sum(
|
||||
bucket.timeseries.buckets.flatMap(
|
||||
(dateBucket) =>
|
||||
dateBucket[EVENT_OUTCOME].buckets.find(
|
||||
(outcomeBucket) => outcomeBucket.key === EventOutcome.failure
|
||||
)?.count.value ?? 0
|
||||
)
|
||||
),
|
||||
count: bucket.doc_count ?? 0,
|
||||
latency_sum: bucket.total_latency_sum.value ?? 0,
|
||||
error_count: bucket.error_count.doc_count ?? 0,
|
||||
},
|
||||
timeseries: bucket.timeseries.buckets.map((dateBucket) => ({
|
||||
timeseries: bucket.timeseries?.buckets.map((dateBucket) => ({
|
||||
x: dateBucket.key + offsetInMs,
|
||||
count: dateBucket.count.value ?? 0,
|
||||
latency_sum: dateBucket.latency_sum.value ?? 0,
|
||||
error_count:
|
||||
dateBucket[EVENT_OUTCOME].buckets.find(
|
||||
(outcomeBucket) => outcomeBucket.key === EventOutcome.failure
|
||||
)?.count.value ?? 0,
|
||||
count: dateBucket.doc_count ?? 0,
|
||||
latency_sum: dateBucket.total_latency_sum.value ?? 0,
|
||||
error_count: dateBucket.error_count.doc_count ?? 0,
|
||||
})),
|
||||
};
|
||||
}) ?? []
|
||||
|
@ -118,6 +108,7 @@ async function getConnectionStats({
|
|||
endWithOffset,
|
||||
filter,
|
||||
numBuckets,
|
||||
withTimeseries,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
startWithOffset: number;
|
||||
|
@ -125,7 +116,27 @@ async function getConnectionStats({
|
|||
filter: QueryDslQueryContainer[];
|
||||
numBuckets: number;
|
||||
after?: { serviceName: string | number; dependencyName: string | number };
|
||||
withTimeseries: boolean;
|
||||
dependencyNames?: string[];
|
||||
}) {
|
||||
const statsAggs = {
|
||||
total_latency_sum: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
|
||||
},
|
||||
},
|
||||
total_latency_count: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
|
||||
},
|
||||
},
|
||||
error_count: {
|
||||
filter: {
|
||||
bool: { filter: [{ terms: { [EVENT_OUTCOME]: [EventOutcome.failure] } }] },
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
return apmEventClient.search('get_connection_stats', {
|
||||
apm: {
|
||||
sources: [
|
||||
|
@ -192,55 +203,27 @@ async function getConnectionStats({
|
|||
},
|
||||
},
|
||||
},
|
||||
total_latency_sum: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
|
||||
},
|
||||
},
|
||||
total_latency_count: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
|
||||
},
|
||||
},
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
numBuckets,
|
||||
minBucketSize: 60,
|
||||
}).intervalString,
|
||||
extended_bounds: {
|
||||
min: startWithOffset,
|
||||
max: endWithOffset,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
latency_sum: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM,
|
||||
},
|
||||
},
|
||||
count: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
|
||||
},
|
||||
},
|
||||
[EVENT_OUTCOME]: {
|
||||
terms: {
|
||||
field: EVENT_OUTCOME,
|
||||
},
|
||||
aggs: {
|
||||
count: {
|
||||
sum: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT,
|
||||
...statsAggs,
|
||||
...(withTimeseries
|
||||
? {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSize({
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
numBuckets,
|
||||
minBucketSize: 60,
|
||||
}).intervalString,
|
||||
extended_bounds: {
|
||||
min: startWithOffset,
|
||||
max: endWithOffset,
|
||||
},
|
||||
},
|
||||
aggs: statsAggs,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
: undefined),
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
@ -5,16 +5,16 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import type { ValuesType } from 'utility-types';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/types';
|
||||
import { merge } from 'lodash';
|
||||
import type { QueryDslQueryContainer } from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import type { ValuesType } from 'utility-types';
|
||||
import { joinByKey } from '../../../../common/utils/join_by_key';
|
||||
import { getStats } from './get_stats';
|
||||
import { getDestinationMap } from './get_destination_map';
|
||||
import { calculateThroughputWithRange } from '../../helpers/calculate_throughput';
|
||||
import { withApmSpan } from '../../../utils/with_apm_span';
|
||||
import { calculateThroughputWithRange } from '../../helpers/calculate_throughput';
|
||||
import type { APMEventClient } from '../../helpers/create_es_client/create_apm_event_client';
|
||||
import type { RandomSampler } from '../../helpers/get_random_sampler';
|
||||
import { getDestinationMap } from './get_destination_map';
|
||||
import { getStats } from './get_stats';
|
||||
|
||||
export function getConnectionStats({
|
||||
apmEventClient,
|
||||
|
@ -25,6 +25,7 @@ export function getConnectionStats({
|
|||
collapseBy,
|
||||
offset,
|
||||
randomSampler,
|
||||
withTimeseries = true,
|
||||
}: {
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
|
@ -34,6 +35,7 @@ export function getConnectionStats({
|
|||
collapseBy: 'upstream' | 'downstream';
|
||||
offset?: string;
|
||||
randomSampler: RandomSampler;
|
||||
withTimeseries?: boolean;
|
||||
}) {
|
||||
return withApmSpan('get_connection_stats_and_map', async () => {
|
||||
const [allMetrics, { nodesBydependencyName: destinationMap, sampled }] = await Promise.all([
|
||||
|
@ -44,6 +46,7 @@ export function getConnectionStats({
|
|||
filter,
|
||||
numBuckets,
|
||||
offset,
|
||||
withTimeseries,
|
||||
}),
|
||||
getDestinationMap({
|
||||
apmEventClient,
|
||||
|
@ -84,12 +87,15 @@ export function getConnectionStats({
|
|||
latency_sum: prev.value.latency_sum + current.value.latency_sum,
|
||||
error_count: prev.value.error_count + current.value.error_count,
|
||||
},
|
||||
timeseries: joinByKey([...prev.timeseries, ...current.timeseries], 'x', (a, b) => ({
|
||||
x: a.x,
|
||||
count: a.count + b.count,
|
||||
latency_sum: a.latency_sum + b.latency_sum,
|
||||
error_count: a.error_count + b.error_count,
|
||||
})),
|
||||
timeseries:
|
||||
prev.timeseries && current.timeseries
|
||||
? joinByKey([...prev.timeseries, ...current.timeseries], 'x', (a, b) => ({
|
||||
x: a.x,
|
||||
count: a.count + b.count,
|
||||
latency_sum: a.latency_sum + b.latency_sum,
|
||||
error_count: a.error_count + b.error_count,
|
||||
}))
|
||||
: undefined,
|
||||
};
|
||||
},
|
||||
{
|
||||
|
@ -108,14 +114,14 @@ export function getConnectionStats({
|
|||
mergedStats.value.count > 0
|
||||
? mergedStats.value.latency_sum / mergedStats.value.count
|
||||
: null,
|
||||
timeseries: mergedStats.timeseries.map((point) => ({
|
||||
timeseries: mergedStats.timeseries?.map((point) => ({
|
||||
x: point.x,
|
||||
y: point.count > 0 ? point.latency_sum / point.count : null,
|
||||
})),
|
||||
},
|
||||
totalTime: {
|
||||
value: mergedStats.value.latency_sum,
|
||||
timeseries: mergedStats.timeseries.map((point) => ({
|
||||
timeseries: mergedStats.timeseries?.map((point) => ({
|
||||
x: point.x,
|
||||
y: point.latency_sum,
|
||||
})),
|
||||
|
@ -129,7 +135,7 @@ export function getConnectionStats({
|
|||
value: mergedStats.value.count,
|
||||
})
|
||||
: null,
|
||||
timeseries: mergedStats.timeseries.map((point) => ({
|
||||
timeseries: mergedStats.timeseries?.map((point) => ({
|
||||
x: point.x,
|
||||
y:
|
||||
point.count > 0
|
||||
|
@ -146,7 +152,7 @@ export function getConnectionStats({
|
|||
mergedStats.value.count > 0
|
||||
? (mergedStats.value.error_count ?? 0) / mergedStats.value.count
|
||||
: null,
|
||||
timeseries: mergedStats.timeseries.map((point) => ({
|
||||
timeseries: mergedStats.timeseries?.map((point) => ({
|
||||
x: point.x,
|
||||
y: point.count > 0 ? (point.error_count ?? 0) / point.count : null,
|
||||
})),
|
||||
|
|
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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 type { DependenciesTimeseriesBuckes } from './get_dependencies_timeseries_statistics';
|
||||
import { parseDependenciesStats } from './get_dependencies_timeseries_statistics';
|
||||
|
||||
describe('parseDependenciesStats', () => {
|
||||
const offsetInMs = 1000;
|
||||
|
||||
test('should parse dependency stats correctly with all values', () => {
|
||||
const dependencies = [
|
||||
{
|
||||
key: 'service-A',
|
||||
timeseries: {
|
||||
buckets: [
|
||||
{
|
||||
key: 1700000000000,
|
||||
doc_count: 10,
|
||||
total_count: { value: 10 },
|
||||
failures: { total_count: { value: 2 }, doc_count: 2 },
|
||||
latency_sum: { value: 5000 },
|
||||
latency_count: { value: 10 },
|
||||
throughput: { value: 50 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
] as DependenciesTimeseriesBuckes;
|
||||
|
||||
const result = parseDependenciesStats({ dependencies, offsetInMs });
|
||||
|
||||
expect(result).toEqual({
|
||||
'service-A': {
|
||||
latency: [{ x: 1700000001000, y: 500 }],
|
||||
errorRate: [{ x: 1700000001000, y: 0.2 }],
|
||||
throughput: [{ x: 1700000001000, y: 50 }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing optional values correctly', () => {
|
||||
const dependencies = [
|
||||
{
|
||||
key: 'service-B',
|
||||
timeseries: {
|
||||
buckets: [
|
||||
{
|
||||
key: 1700000000000,
|
||||
doc_count: 5,
|
||||
failures: { doc_count: 1 },
|
||||
latency_sum: { value: 2000 },
|
||||
latency_count: { value: 5 },
|
||||
throughput: {},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
] as DependenciesTimeseriesBuckes;
|
||||
|
||||
const result = parseDependenciesStats({ dependencies, offsetInMs });
|
||||
|
||||
expect(result).toEqual({
|
||||
'service-B': {
|
||||
latency: [{ x: 1700000001000, y: 400 }],
|
||||
errorRate: [{ x: 1700000001000, y: 0.2 }],
|
||||
throughput: [{ x: 1700000001000, y: undefined }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should handle missing failures field', () => {
|
||||
const dependencies = [
|
||||
{
|
||||
key: 'service-C',
|
||||
timeseries: {
|
||||
buckets: [
|
||||
{
|
||||
key: 1700000000000,
|
||||
doc_count: 8,
|
||||
failures: { doc_count: 0 },
|
||||
total_count: { value: 8 },
|
||||
latency_sum: { value: 4000 },
|
||||
latency_count: { value: 8 },
|
||||
throughput: { value: 30 },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
] as DependenciesTimeseriesBuckes;
|
||||
|
||||
const result = parseDependenciesStats({ dependencies, offsetInMs });
|
||||
|
||||
expect(result).toEqual({
|
||||
'service-C': {
|
||||
latency: [{ x: 1700000001000, y: 500 }],
|
||||
errorRate: [{ x: 1700000001000, y: 0 }],
|
||||
throughput: [{ x: 1700000001000, y: 30 }],
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
test('should return an empty object when dependencies are empty', () => {
|
||||
const result = parseDependenciesStats({ dependencies: [], offsetInMs });
|
||||
expect(result).toEqual({});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,260 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { EVENT_OUTCOME, SPAN_DESTINATION_SERVICE_RESOURCE } from '../../../common/es_fields/apm';
|
||||
import { EventOutcome } from '../../../common/event_outcome';
|
||||
import { getBucketSize } from '../../../common/utils/get_bucket_size';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { getOffsetInMs } from '../../../common/utils/get_offset_in_ms';
|
||||
import type { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
import {
|
||||
getDocCountFieldForServiceDestinationStatistics,
|
||||
getDocumentTypeFilterForServiceDestinationStatistics,
|
||||
getLatencyFieldForServiceDestinationStatistics,
|
||||
getProcessorEventForServiceDestinationStatistics,
|
||||
} from '../../lib/helpers/spans/get_is_using_service_destination_metrics';
|
||||
|
||||
interface Options {
|
||||
dependencyNames: string[];
|
||||
searchServiceDestinationMetrics: boolean;
|
||||
apmEventClient: APMEventClient;
|
||||
start: number;
|
||||
end: number;
|
||||
environment: string;
|
||||
kuery: string;
|
||||
offset?: string;
|
||||
numBuckets: number;
|
||||
}
|
||||
|
||||
interface Statistics {
|
||||
latency: Array<{ x: number; y: number }>;
|
||||
errorRate: Array<{ x: number; y: number }>;
|
||||
throughput: Array<{ x: number; y: number | null }>;
|
||||
}
|
||||
|
||||
async function fetchDependenciesTimeseriesStatistics({
|
||||
dependencyNames,
|
||||
searchServiceDestinationMetrics,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
numBuckets,
|
||||
}: Options) {
|
||||
const response = await apmEventClient.search('get_latency_for_dependency', {
|
||||
apm: {
|
||||
events: [getProcessorEventForServiceDestinationStatistics(searchServiceDestinationMetrics)],
|
||||
},
|
||||
body: {
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...environmentQuery(environment),
|
||||
...kqlQuery(kuery),
|
||||
...rangeQuery(start, end),
|
||||
...getDocumentTypeFilterForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
{ terms: { [SPAN_DESTINATION_SERVICE_RESOURCE]: dependencyNames } },
|
||||
],
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
dependencies: {
|
||||
terms: {
|
||||
field: SPAN_DESTINATION_SERVICE_RESOURCE,
|
||||
},
|
||||
aggs: {
|
||||
timeseries: {
|
||||
date_histogram: {
|
||||
field: '@timestamp',
|
||||
fixed_interval: getBucketSize({
|
||||
start,
|
||||
end,
|
||||
numBuckets,
|
||||
minBucketSize: 60,
|
||||
}).intervalString,
|
||||
extended_bounds: {
|
||||
min: start,
|
||||
max: end,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
// latency
|
||||
latency_sum: {
|
||||
sum: {
|
||||
field: getLatencyFieldForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
},
|
||||
},
|
||||
...(searchServiceDestinationMetrics
|
||||
? {
|
||||
latency_count: {
|
||||
sum: {
|
||||
field: getDocCountFieldForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
// error
|
||||
...(searchServiceDestinationMetrics
|
||||
? {
|
||||
total_count: {
|
||||
sum: {
|
||||
field: getDocCountFieldForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
failures: {
|
||||
filter: {
|
||||
term: {
|
||||
[EVENT_OUTCOME]: EventOutcome.failure,
|
||||
},
|
||||
},
|
||||
aggs: {
|
||||
...(searchServiceDestinationMetrics
|
||||
? {
|
||||
total_count: {
|
||||
sum: {
|
||||
field: getDocCountFieldForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
},
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
},
|
||||
},
|
||||
// throughput
|
||||
throughput: {
|
||||
rate: {
|
||||
...(searchServiceDestinationMetrics
|
||||
? {
|
||||
field: getDocCountFieldForServiceDestinationStatistics(
|
||||
searchServiceDestinationMetrics
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
unit: 'minute',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return response.aggregations?.dependencies.buckets || [];
|
||||
}
|
||||
|
||||
export type DependenciesTimeseriesBuckes = Awaited<
|
||||
ReturnType<typeof fetchDependenciesTimeseriesStatistics>
|
||||
>;
|
||||
|
||||
export function parseDependenciesStats({
|
||||
dependencies,
|
||||
offsetInMs,
|
||||
}: {
|
||||
dependencies: DependenciesTimeseriesBuckes;
|
||||
offsetInMs: number;
|
||||
}) {
|
||||
return (
|
||||
dependencies.reduce<Record<string, Statistics>>((acc, bucket) => {
|
||||
const stats: Statistics = {
|
||||
latency: [],
|
||||
errorRate: [],
|
||||
throughput: [],
|
||||
};
|
||||
|
||||
for (const statsBucket of bucket.timeseries.buckets) {
|
||||
const totalCount = statsBucket.total_count?.value ?? statsBucket.doc_count;
|
||||
const failureCount =
|
||||
statsBucket.failures.total_count?.value ?? statsBucket.failures.doc_count;
|
||||
const x = statsBucket.key + offsetInMs;
|
||||
|
||||
stats.latency.push({
|
||||
x,
|
||||
y:
|
||||
(statsBucket.latency_sum.value ?? 0) /
|
||||
(statsBucket.latency_count?.value ?? statsBucket.doc_count),
|
||||
});
|
||||
stats.errorRate.push({ x, y: failureCount / totalCount });
|
||||
stats.throughput.push({ x, y: statsBucket.throughput.value });
|
||||
}
|
||||
|
||||
acc[bucket.key] = stats;
|
||||
return acc;
|
||||
}, {}) ?? {}
|
||||
);
|
||||
}
|
||||
|
||||
export interface DependenciesTimeseriesStatisticsResponse {
|
||||
currentTimeseries: Record<string, Statistics>;
|
||||
comparisonTimeseries: Record<string, Statistics> | null;
|
||||
}
|
||||
|
||||
export async function getDependenciesTimeseriesStatistics({
|
||||
apmEventClient,
|
||||
dependencyNames,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
searchServiceDestinationMetrics,
|
||||
offset,
|
||||
numBuckets,
|
||||
}: Options): Promise<DependenciesTimeseriesStatisticsResponse> {
|
||||
const { offsetInMs, startWithOffset, endWithOffset } = getOffsetInMs({
|
||||
start,
|
||||
end,
|
||||
offset,
|
||||
});
|
||||
|
||||
const [currentTimeseries, comparisonTimeseries] = await Promise.all([
|
||||
fetchDependenciesTimeseriesStatistics({
|
||||
dependencyNames,
|
||||
searchServiceDestinationMetrics,
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
kuery,
|
||||
environment,
|
||||
numBuckets,
|
||||
}),
|
||||
offset
|
||||
? fetchDependenciesTimeseriesStatistics({
|
||||
dependencyNames,
|
||||
searchServiceDestinationMetrics,
|
||||
apmEventClient,
|
||||
start: startWithOffset,
|
||||
end: endWithOffset,
|
||||
kuery,
|
||||
environment,
|
||||
numBuckets,
|
||||
})
|
||||
: null,
|
||||
]);
|
||||
|
||||
return {
|
||||
currentTimeseries: parseDependenciesStats({ dependencies: currentTimeseries, offsetInMs: 0 }),
|
||||
comparisonTimeseries: comparisonTimeseries?.length
|
||||
? parseDependenciesStats({ dependencies: comparisonTimeseries, offsetInMs })
|
||||
: null,
|
||||
};
|
||||
}
|
|
@ -27,6 +27,7 @@ interface Options {
|
|||
offset?: string;
|
||||
kuery: string;
|
||||
randomSampler: RandomSampler;
|
||||
withTimeseries: boolean;
|
||||
}
|
||||
|
||||
interface TopDependenciesForTimeRange {
|
||||
|
@ -44,6 +45,7 @@ async function getTopDependenciesForTimeRange({
|
|||
offset,
|
||||
kuery,
|
||||
randomSampler,
|
||||
withTimeseries,
|
||||
}: Options): Promise<TopDependenciesForTimeRange> {
|
||||
const { statsItems, sampled } = await getConnectionStats({
|
||||
apmEventClient,
|
||||
|
@ -54,6 +56,7 @@ async function getTopDependenciesForTimeRange({
|
|||
offset,
|
||||
collapseBy: 'downstream',
|
||||
randomSampler,
|
||||
withTimeseries,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -5,13 +5,15 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { toBooleanRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import { jsonRt, toBooleanRt, toNumberRt } from '@kbn/io-ts-utils';
|
||||
import * as t from 'io-ts';
|
||||
import { offsetRt } from '../../../common/comparison_rt';
|
||||
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
|
||||
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { environmentRt, kueryRt, rangeRt } from '../default_api_types';
|
||||
import type { DependenciesTimeseriesStatisticsResponse } from './get_dependencies_timeseries_statistics';
|
||||
import { getDependenciesTimeseriesStatistics } from './get_dependencies_timeseries_statistics';
|
||||
import type { DependencyLatencyDistributionResponse } from './get_dependency_latency_distribution';
|
||||
import { getDependencyLatencyDistribution } from './get_dependency_latency_distribution';
|
||||
import { getErrorRateChartsForDependency } from './get_error_rate_charts_for_dependency';
|
||||
|
@ -32,14 +34,9 @@ import { getUpstreamServicesForDependency } from './get_upstream_services_for_de
|
|||
|
||||
const topDependenciesRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/dependencies/top_dependencies',
|
||||
params: t.intersection([
|
||||
t.type({
|
||||
query: t.intersection([rangeRt, environmentRt, kueryRt, t.type({ numBuckets: toNumberRt })]),
|
||||
}),
|
||||
t.partial({
|
||||
query: offsetRt,
|
||||
}),
|
||||
]),
|
||||
params: t.type({
|
||||
query: t.intersection([rangeRt, environmentRt, kueryRt, t.type({ numBuckets: toNumberRt })]),
|
||||
}),
|
||||
security: { authz: { requiredPrivileges: ['apm'] } },
|
||||
handler: async (resources): Promise<TopDependenciesResponse> => {
|
||||
const { request, core } = resources;
|
||||
|
@ -49,7 +46,7 @@ const topDependenciesRoute = createApmServerRoute({
|
|||
getApmEventClient(resources),
|
||||
getRandomSampler({ coreStart, request, probability: 1 }),
|
||||
]);
|
||||
const { environment, offset, numBuckets, kuery, start, end } = resources.params.query;
|
||||
const { environment, numBuckets, kuery, start, end } = resources.params.query;
|
||||
|
||||
return getTopDependencies({
|
||||
apmEventClient,
|
||||
|
@ -58,8 +55,37 @@ const topDependenciesRoute = createApmServerRoute({
|
|||
numBuckets,
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
randomSampler,
|
||||
withTimeseries: false,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
const topDependenciesStatisticsRoute = createApmServerRoute({
|
||||
endpoint: 'POST /internal/apm/dependencies/top_dependencies/statistics',
|
||||
params: t.type({
|
||||
query: t.intersection([
|
||||
t.intersection([environmentRt, kueryRt, rangeRt, offsetRt]),
|
||||
t.type({ numBuckets: toNumberRt }),
|
||||
]),
|
||||
body: t.type({ dependencyNames: jsonRt.pipe(t.array(t.string)) }),
|
||||
}),
|
||||
security: { authz: { requiredPrivileges: ['apm'] } },
|
||||
handler: async (resources): Promise<DependenciesTimeseriesStatisticsResponse> => {
|
||||
const apmEventClient = await getApmEventClient(resources);
|
||||
const { environment, offset, numBuckets, kuery, start, end } = resources.params.query;
|
||||
const { dependencyNames } = resources.params.body;
|
||||
|
||||
return getDependenciesTimeseriesStatistics({
|
||||
apmEventClient,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
offset,
|
||||
dependencyNames,
|
||||
searchServiceDestinationMetrics: true,
|
||||
numBuckets,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
@ -403,4 +429,5 @@ export const dependencisRouteRepository = {
|
|||
...dependencyOperationsRoute,
|
||||
...dependencyLatencyDistributionChartsRoute,
|
||||
...topDependencySpansRoute,
|
||||
...topDependenciesStatisticsRoute,
|
||||
};
|
||||
|
|
|
@ -54,7 +54,7 @@ export async function getServiceDependenciesBreakdown({
|
|||
|
||||
return {
|
||||
title: getNodeName(location),
|
||||
data: stats.totalTime.timeseries,
|
||||
data: stats.totalTime.timeseries || [],
|
||||
};
|
||||
});
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
|||
|
||||
export const dataConfig = {
|
||||
rate: 20,
|
||||
errorRate: 5,
|
||||
transaction: {
|
||||
name: 'GET /api/product/list',
|
||||
duration: 1000,
|
||||
|
@ -33,26 +34,45 @@ export async function generateData({
|
|||
const instance = apm
|
||||
.service({ name: 'synth-go', environment: 'production', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
const { rate, transaction, span } = dataConfig;
|
||||
const { rate, transaction, span, errorRate } = dataConfig;
|
||||
|
||||
await apmSynthtraceEsClient.index(
|
||||
timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.children(
|
||||
instance
|
||||
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.destination(span.destination)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
)
|
||||
);
|
||||
const successfulEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.children(
|
||||
instance
|
||||
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.destination(span.destination)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
);
|
||||
|
||||
const failureEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(errorRate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.failure()
|
||||
.children(
|
||||
instance
|
||||
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
|
||||
.duration(transaction.duration)
|
||||
.failure()
|
||||
.destination(span.destination)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
);
|
||||
|
||||
await apmSynthtraceEsClient.index([successfulEvents, failureEvents]);
|
||||
}
|
||||
|
|
|
@ -13,6 +13,8 @@ import { dataConfig, generateData } from './generate_data';
|
|||
import { roundNumber } from '../utils/common';
|
||||
|
||||
type TopDependencies = APIReturnType<'GET /internal/apm/dependencies/top_dependencies'>;
|
||||
type TopDependenciesStatistics =
|
||||
APIReturnType<'POST /internal/apm/dependencies/top_dependencies/statistics'>;
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
|
@ -20,6 +22,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
const bucketSize = Math.round((end - start) / (60 * 1000));
|
||||
|
||||
async function callApi() {
|
||||
return await apmApiClient.readUser({
|
||||
|
@ -31,7 +34,24 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
numBuckets: 20,
|
||||
offset: '',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function callStatisticsApi() {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: 'POST /internal/apm/dependencies/top_dependencies/statistics',
|
||||
params: {
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
numBuckets: 20,
|
||||
},
|
||||
body: {
|
||||
dependencyNames: JSON.stringify([dataConfig.span.destination]),
|
||||
},
|
||||
},
|
||||
});
|
||||
|
@ -48,13 +68,15 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
describe('when data is generated', () => {
|
||||
let topDependencies: TopDependencies;
|
||||
let topDependenciesStats: TopDependenciesStatistics;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
const [response, statisticsResponse] = await Promise.all([callApi(), callStatisticsApi()]);
|
||||
topDependencies = response.body;
|
||||
topDependenciesStats = statisticsResponse.body;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
@ -90,6 +112,13 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
expect(dependencies.currentStats).to.have.property('impact');
|
||||
});
|
||||
|
||||
it("doesn't have timeseries stats", () => {
|
||||
expect(dependencies.currentStats.latency).to.not.have.property('timeseries');
|
||||
expect(dependencies.currentStats.totalTime).to.not.have.property('timeseries');
|
||||
expect(dependencies.currentStats.throughput).to.not.have.property('timeseries');
|
||||
expect(dependencies.currentStats.errorRate).to.not.have.property('timeseries');
|
||||
});
|
||||
|
||||
it('returns the correct latency', () => {
|
||||
const {
|
||||
currentStats: { latency },
|
||||
|
@ -97,38 +126,52 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
|
||||
const { transaction } = dataConfig;
|
||||
|
||||
expect(latency.value).to.be(transaction.duration * 1000);
|
||||
expect(latency.timeseries.every(({ y }) => y === transaction.duration * 1000)).to.be(
|
||||
true
|
||||
);
|
||||
const expectedValue = transaction.duration * 1000;
|
||||
expect(latency.value).to.be(expectedValue);
|
||||
expect(
|
||||
topDependenciesStats.currentTimeseries[dataConfig.span.destination].latency.every(
|
||||
({ y }) => y === expectedValue
|
||||
)
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the correct throughput', () => {
|
||||
const {
|
||||
currentStats: { throughput },
|
||||
} = dependencies;
|
||||
const { rate } = dataConfig;
|
||||
const { rate, errorRate } = dataConfig;
|
||||
|
||||
expect(roundNumber(throughput.value)).to.be(roundNumber(rate));
|
||||
const totalRate = rate + errorRate;
|
||||
expect(roundNumber(throughput.value)).to.be(roundNumber(totalRate));
|
||||
expect(
|
||||
topDependenciesStats.currentTimeseries[dataConfig.span.destination].throughput.every(
|
||||
({ y }) => roundNumber(y) === roundNumber(totalRate)
|
||||
)
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the correct total time', () => {
|
||||
const {
|
||||
currentStats: { totalTime },
|
||||
} = dependencies;
|
||||
const { rate, transaction } = dataConfig;
|
||||
const { rate, transaction, errorRate } = dataConfig;
|
||||
|
||||
expect(
|
||||
totalTime.timeseries.every(({ y }) => y === rate * transaction.duration * 1000)
|
||||
).to.be(true);
|
||||
const expectedValuePerBucket = (rate + errorRate) * transaction.duration * 1000;
|
||||
expect(totalTime.value).to.be(expectedValuePerBucket * bucketSize);
|
||||
});
|
||||
|
||||
it('returns the correct error rate', () => {
|
||||
const {
|
||||
currentStats: { errorRate },
|
||||
} = dependencies;
|
||||
expect(errorRate.value).to.be(0);
|
||||
expect(errorRate.timeseries.every(({ y }) => y === 0)).to.be(true);
|
||||
const { rate, errorRate: dataConfigErroRate } = dataConfig;
|
||||
const expectedValue = dataConfigErroRate / (rate + dataConfigErroRate);
|
||||
expect(errorRate.value).to.be(expectedValue);
|
||||
expect(
|
||||
topDependenciesStats.currentTimeseries[dataConfig.span.destination].errorRate.every(
|
||||
({ y }) => y === expectedValue
|
||||
)
|
||||
).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -63,7 +63,7 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
]);
|
||||
|
||||
const currentStatsLatencyValues = body.services[0].currentStats.latency.timeseries;
|
||||
expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true);
|
||||
expect(currentStatsLatencyValues?.every(({ y }) => y === 1000000)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* 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 type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
|
||||
export const dataConfig = {
|
||||
rate: 20,
|
||||
errorRate: 5,
|
||||
transaction: {
|
||||
name: 'GET /api/product/list',
|
||||
duration: 1000,
|
||||
},
|
||||
span: {
|
||||
name: 'GET apm-*/_search',
|
||||
type: 'db',
|
||||
subType: 'elasticsearch',
|
||||
destination: 'elasticsearch',
|
||||
},
|
||||
};
|
||||
|
||||
export async function generateData({
|
||||
apmSynthtraceEsClient,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const instance = apm
|
||||
.service({ name: 'synth-go', environment: 'production', agentName: 'go' })
|
||||
.instance('instance-a');
|
||||
const { rate, transaction, span, errorRate } = dataConfig;
|
||||
|
||||
const successfulEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(rate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.children(
|
||||
instance
|
||||
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
|
||||
.duration(transaction.duration)
|
||||
.success()
|
||||
.destination(span.destination)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
);
|
||||
|
||||
const failureEvents = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(errorRate)
|
||||
.generator((timestamp) =>
|
||||
instance
|
||||
.transaction({ transactionName: transaction.name })
|
||||
.timestamp(timestamp)
|
||||
.duration(transaction.duration)
|
||||
.failure()
|
||||
.children(
|
||||
instance
|
||||
.span({ spanName: span.name, spanType: span.type, spanSubtype: span.subType })
|
||||
.duration(transaction.duration)
|
||||
.failure()
|
||||
.destination(span.destination)
|
||||
.timestamp(timestamp)
|
||||
)
|
||||
);
|
||||
|
||||
await apmSynthtraceEsClient.index([successfulEvents, failureEvents]);
|
||||
}
|
|
@ -5,33 +5,22 @@
|
|||
* 2.0.
|
||||
*/
|
||||
import expect from '@kbn/expect';
|
||||
import { last, pick } from 'lodash';
|
||||
import { DependencyNode } from '@kbn/apm-plugin/common/connections';
|
||||
import type { ValuesType } from 'utility-types';
|
||||
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
|
||||
import type { APIReturnType } from '@kbn/apm-plugin/public/services/rest/create_call_apm_api';
|
||||
import { type Node, NodeType } from '@kbn/apm-plugin/common/connections';
|
||||
import {
|
||||
ENVIRONMENT_ALL,
|
||||
ENVIRONMENT_NOT_DEFINED,
|
||||
} from '@kbn/apm-plugin/common/environment_filter_values';
|
||||
import { NodeType } from '@kbn/apm-plugin/common/connections';
|
||||
import type { DeploymentAgnosticFtrProviderContext } from '../../../../../ftr_provider_context';
|
||||
import { roundNumber } from '../../utils/common';
|
||||
import { generateDependencyData } from '../generate_data';
|
||||
import { apmDependenciesMapping, createServiceDependencyDocs } from './es_utils';
|
||||
import { generateData, dataConfig } from './generate_data';
|
||||
|
||||
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
|
||||
const apmApiClient = getService('apmApi');
|
||||
const synthtrace = getService('synthtrace');
|
||||
const es = getService('es');
|
||||
const start = new Date('2021-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2021-01-01T00:15:00.000Z').getTime() - 1;
|
||||
const dependencyName = 'elasticsearch';
|
||||
const serviceName = 'synth-go';
|
||||
const bucketSize = Math.round((end - start) / (60 * 1000));
|
||||
|
||||
function getName(node: Node) {
|
||||
return node.type === NodeType.service ? node.serviceName : node.dependencyName;
|
||||
}
|
||||
const serviceName = 'synth-go';
|
||||
|
||||
async function callApi() {
|
||||
return await apmApiClient.readUser({
|
||||
|
@ -60,271 +49,91 @@ export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderCon
|
|||
});
|
||||
|
||||
describe('when specific data is loaded', () => {
|
||||
let response: {
|
||||
status: number;
|
||||
body: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>;
|
||||
};
|
||||
|
||||
const indices = {
|
||||
metric: 'apm-dependencies-metric',
|
||||
transaction: 'apm-dependencies-transaction',
|
||||
span: 'apm-dependencies-span',
|
||||
};
|
||||
|
||||
const startTime = new Date(start).getTime();
|
||||
const endTime = new Date(end).getTime();
|
||||
|
||||
after(async () => {
|
||||
const allIndices = Object.values(indices).join(',');
|
||||
const indexExists = await es.indices.exists({ index: allIndices });
|
||||
if (indexExists) {
|
||||
await es.indices.delete({
|
||||
index: allIndices,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
before(async () => {
|
||||
await es.indices.create({
|
||||
index: indices.metric,
|
||||
body: {
|
||||
mappings: apmDependenciesMapping,
|
||||
},
|
||||
});
|
||||
|
||||
await es.indices.create({
|
||||
index: indices.transaction,
|
||||
body: {
|
||||
mappings: apmDependenciesMapping,
|
||||
},
|
||||
});
|
||||
|
||||
await es.indices.create({
|
||||
index: indices.span,
|
||||
body: {
|
||||
mappings: apmDependenciesMapping,
|
||||
},
|
||||
});
|
||||
|
||||
const docs = [
|
||||
...createServiceDependencyDocs({
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
},
|
||||
agentName: 'java',
|
||||
span: {
|
||||
type: 'external',
|
||||
subtype: 'http',
|
||||
},
|
||||
resource: 'opbeans-node:3000',
|
||||
outcome: 'success',
|
||||
responseTime: {
|
||||
count: 2,
|
||||
sum: 10,
|
||||
},
|
||||
time: startTime,
|
||||
to: {
|
||||
service: {
|
||||
name: 'opbeans-node',
|
||||
},
|
||||
agentName: 'nodejs',
|
||||
},
|
||||
}),
|
||||
...createServiceDependencyDocs({
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
},
|
||||
agentName: 'java',
|
||||
span: {
|
||||
type: 'external',
|
||||
subtype: 'http',
|
||||
},
|
||||
resource: 'opbeans-node:3000',
|
||||
outcome: 'failure',
|
||||
responseTime: {
|
||||
count: 1,
|
||||
sum: 10,
|
||||
},
|
||||
time: startTime,
|
||||
}),
|
||||
...createServiceDependencyDocs({
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
},
|
||||
agentName: 'java',
|
||||
span: {
|
||||
type: 'external',
|
||||
subtype: 'http',
|
||||
},
|
||||
resource: 'postgres',
|
||||
outcome: 'success',
|
||||
responseTime: {
|
||||
count: 1,
|
||||
sum: 3,
|
||||
},
|
||||
time: startTime,
|
||||
}),
|
||||
...createServiceDependencyDocs({
|
||||
service: {
|
||||
name: 'opbeans-java',
|
||||
environment: 'production',
|
||||
},
|
||||
agentName: 'java',
|
||||
span: {
|
||||
type: 'external',
|
||||
subtype: 'http',
|
||||
},
|
||||
resource: 'opbeans-node-via-proxy',
|
||||
outcome: 'success',
|
||||
responseTime: {
|
||||
count: 1,
|
||||
sum: 1,
|
||||
},
|
||||
time: endTime - 1,
|
||||
to: {
|
||||
service: {
|
||||
name: 'opbeans-node',
|
||||
},
|
||||
agentName: 'nodejs',
|
||||
},
|
||||
}),
|
||||
];
|
||||
|
||||
const bulkActions = docs.reduce(
|
||||
(prev, doc) => {
|
||||
return [...prev, { index: { _index: indices[doc.processor.event] } }, doc];
|
||||
},
|
||||
[] as Array<
|
||||
| {
|
||||
index: {
|
||||
_index: string;
|
||||
};
|
||||
}
|
||||
| ValuesType<typeof docs>
|
||||
>
|
||||
);
|
||||
|
||||
await es.bulk({
|
||||
body: bulkActions,
|
||||
refresh: 'wait_for',
|
||||
});
|
||||
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/services/{serviceName}/dependencies`,
|
||||
params: {
|
||||
path: { serviceName: 'opbeans-java' },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
numBuckets: 20,
|
||||
environment: ENVIRONMENT_ALL.value,
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('returns a 200', () => {
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns two dependencies', () => {
|
||||
expect(response.body.serviceDependencies.length).to.be(2);
|
||||
});
|
||||
|
||||
it('returns opbeans-node as a dependency', () => {
|
||||
const opbeansNode = response.body.serviceDependencies.find(
|
||||
(item) => getName(item.location) === 'opbeans-node'
|
||||
);
|
||||
|
||||
expect(opbeansNode !== undefined).to.be(true);
|
||||
|
||||
const values = {
|
||||
latency: roundNumber(opbeansNode?.currentStats.latency.value),
|
||||
throughput: roundNumber(opbeansNode?.currentStats.throughput.value),
|
||||
errorRate: roundNumber(opbeansNode?.currentStats.errorRate.value),
|
||||
impact: opbeansNode?.currentStats.impact,
|
||||
...pick(opbeansNode?.location, 'serviceName', 'type', 'agentName', 'environment'),
|
||||
};
|
||||
|
||||
const count = 4;
|
||||
const sum = 21;
|
||||
const errors = 1;
|
||||
|
||||
expect(values).to.eql({
|
||||
agentName: 'nodejs',
|
||||
environment: ENVIRONMENT_NOT_DEFINED.value,
|
||||
serviceName: 'opbeans-node',
|
||||
type: 'service',
|
||||
errorRate: roundNumber(errors / count),
|
||||
latency: roundNumber(sum / count),
|
||||
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
|
||||
impact: 100,
|
||||
});
|
||||
|
||||
const firstValue = roundNumber(opbeansNode?.currentStats.latency.timeseries[0].y);
|
||||
const lastValue = roundNumber(last(opbeansNode?.currentStats.latency.timeseries)?.y);
|
||||
|
||||
expect(firstValue).to.be(roundNumber(20 / 3));
|
||||
expect(lastValue).to.be(1);
|
||||
});
|
||||
|
||||
it('returns postgres as an external dependency', () => {
|
||||
const postgres = response.body.serviceDependencies.find(
|
||||
(item) => getName(item.location) === 'postgres'
|
||||
);
|
||||
|
||||
expect(postgres !== undefined).to.be(true);
|
||||
|
||||
const values = {
|
||||
latency: roundNumber(postgres?.currentStats.latency.value),
|
||||
throughput: roundNumber(postgres?.currentStats.throughput.value),
|
||||
errorRate: roundNumber(postgres?.currentStats.errorRate.value),
|
||||
impact: postgres?.currentStats.impact,
|
||||
...pick(postgres?.location, 'spanType', 'spanSubtype', 'dependencyName', 'type'),
|
||||
};
|
||||
|
||||
const count = 1;
|
||||
const sum = 3;
|
||||
const errors = 0;
|
||||
|
||||
expect(values).to.eql({
|
||||
spanType: 'external',
|
||||
spanSubtype: 'http',
|
||||
dependencyName: 'postgres',
|
||||
type: 'dependency',
|
||||
errorRate: roundNumber(errors / count),
|
||||
latency: roundNumber(sum / count),
|
||||
throughput: roundNumber(count / ((endTime - startTime) / 1000 / 60)),
|
||||
impact: 0,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let dependencies: APIReturnType<'GET /internal/apm/services/{serviceName}/dependencies'>;
|
||||
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
|
||||
|
||||
before(async () => {
|
||||
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
|
||||
await generateDependencyData({ apmSynthtraceEsClient, start, end });
|
||||
await generateData({ apmSynthtraceEsClient, start, end });
|
||||
const response = await callApi();
|
||||
dependencies = response.body;
|
||||
});
|
||||
|
||||
after(() => apmSynthtraceEsClient.clean());
|
||||
|
||||
it('returns a list of dependencies for a service', async () => {
|
||||
const { status, body } = await callApi();
|
||||
it('returns one dependency', () => {
|
||||
expect(dependencies.serviceDependencies.length).to.be(1);
|
||||
});
|
||||
|
||||
expect(status).to.be(200);
|
||||
it('returns correct dependency information', () => {
|
||||
const location = dependencies.serviceDependencies[0].location as DependencyNode;
|
||||
const { span } = dataConfig;
|
||||
|
||||
expect(location.type).to.be(NodeType.dependency);
|
||||
expect(location.dependencyName).to.be(span.destination);
|
||||
expect(location.spanType).to.be(span.type);
|
||||
expect(location.spanSubtype).to.be(span.subType);
|
||||
expect(location).to.have.property('id');
|
||||
});
|
||||
|
||||
it("doesn't have previous stats", () => {
|
||||
expect(dependencies.serviceDependencies[0].previousStats).to.be(null);
|
||||
});
|
||||
|
||||
it('has an "impact" property', () => {
|
||||
expect(dependencies.serviceDependencies[0].currentStats).to.have.property('impact');
|
||||
});
|
||||
|
||||
it('returns the correct latency', () => {
|
||||
const {
|
||||
currentStats: { latency },
|
||||
} = dependencies.serviceDependencies[0];
|
||||
|
||||
const { transaction } = dataConfig;
|
||||
|
||||
const expectedValue = transaction.duration * 1000;
|
||||
expect(latency.value).to.be(expectedValue);
|
||||
expect(latency.timeseries?.every(({ y }) => y === expectedValue)).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the correct throughput', () => {
|
||||
const {
|
||||
currentStats: { throughput },
|
||||
} = dependencies.serviceDependencies[0];
|
||||
const { rate, errorRate } = dataConfig;
|
||||
|
||||
const expectedThroughput = rate + errorRate;
|
||||
expect(roundNumber(throughput.value)).to.be(roundNumber(expectedThroughput));
|
||||
expect(
|
||||
body.serviceDependencies.map(
|
||||
({ location }) => (location as DependencyNode).dependencyName
|
||||
throughput.timeseries?.every(
|
||||
({ y }) => roundNumber(y) === roundNumber(expectedThroughput / bucketSize)
|
||||
)
|
||||
).to.eql([dependencyName]);
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
const currentStatsLatencyValues =
|
||||
body.serviceDependencies[0].currentStats.latency.timeseries;
|
||||
expect(currentStatsLatencyValues.every(({ y }) => y === 1000000)).to.be(true);
|
||||
it('returns the correct total time', () => {
|
||||
const {
|
||||
currentStats: { totalTime },
|
||||
} = dependencies.serviceDependencies[0];
|
||||
const { rate, transaction, errorRate } = dataConfig;
|
||||
|
||||
const expectedValuePerBucket = (rate + errorRate) * transaction.duration * 1000;
|
||||
expect(totalTime.value).to.be(expectedValuePerBucket * bucketSize);
|
||||
expect(
|
||||
totalTime.timeseries?.every(
|
||||
({ y }) => roundNumber(y) === roundNumber(expectedValuePerBucket)
|
||||
)
|
||||
).to.be(true);
|
||||
});
|
||||
|
||||
it('returns the correct error rate', () => {
|
||||
const {
|
||||
currentStats: { errorRate },
|
||||
} = dependencies.serviceDependencies[0];
|
||||
const { rate, errorRate: dataConfigErroRate } = dataConfig;
|
||||
const expectedValue = dataConfigErroRate / (rate + dataConfigErroRate);
|
||||
expect(errorRate.value).to.be(expectedValue);
|
||||
expect(errorRate.timeseries?.every(({ y }) => y === expectedValue)).to.be(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue