[APM] Remove Aggregated Critical Path flamegraph view feature (#213270)

closes [#212256](https://github.com/elastic/kibana/issues/212256)

## Summary


Remove the Aggregated Critical Path feature. The necessary data needed
for this feature comes from a query running a `scripted_metric` agg.

`scripted_metric` agg queries will be rewritten in other features.
However, for the Aggregated Critical Path case, running queries with
available aggregations won't achieve the same accuracy in a performant
way. Another solution would be needed for this.

Given that this feature has been in tech preview since 8.6, and the
adoption number is low, we decided to remove this feature


| Before | After |
|--------|------|
|<img width="800" alt="image"
src="https://github.com/user-attachments/assets/fdb83c13-818f-49c7-ab3c-046dff0a53d1"
/>|<img width="800" alt="image"
src="https://github.com/user-attachments/assets/4739cca2-ae19-4041-8b41-e87c8041e2c1"
/>|


with `observability:apmEnableCriticalPath` enabled

| Before | After |
|--------|------|
|<img width="800" alt="image"
src="https://github.com/user-attachments/assets/95d31db9-9e0d-4095-8300-2625f420da5c"
/>|<img width="800" alt="image"
src="https://github.com/user-attachments/assets/7f845d85-7a6e-4d45-910f-a5bcee159760"
/>|

---------

Co-authored-by: Elastic Machine <elasticmachine@users.noreply.github.com>
This commit is contained in:
Carlos Crespo 2025-03-10 12:01:38 +01:00 committed by GitHub
parent 6c281caceb
commit 3bd48eaefd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
18 changed files with 6 additions and 1625 deletions

View file

@ -10685,8 +10685,6 @@
"xpack.apm.crashTable.noCrashesLabel": "Pas de pannes trouvées",
"xpack.apm.crashTable.occurrencesColumnLabel": "Occurrences",
"xpack.apm.crashTable.typeColumnLabel": "Type",
"xpack.apm.criticalPathFlameGraph.selfTime": "Heure automatique : {value}",
"xpack.apm.criticalPathFlameGraph.totalTime": "Temps total : {value}",
"xpack.apm.customLink.buttom.create": "Créer un lien personnalisé",
"xpack.apm.customLink.buttom.create.title": "Créer",
"xpack.apm.customLink.buttom.manage": "Gérer des liens personnalisés",
@ -11853,8 +11851,6 @@
"xpack.apm.tooltip.link.apmServerDocs": "documents",
"xpack.apm.tooltip.maxGroup.message": "La cardinalité des données APM collectées est trop élevée. Veuillez consulter {apmServerDocs} pour atténuer les risques liés à la situation.",
"xpack.apm.traceExplorer.appName": "APM",
"xpack.apm.traceExplorer.criticalPathTab": "Chemin critique agrégé",
"xpack.apm.traceExplorer.waterfallTab": "Cascade",
"xpack.apm.traceLink.fetchingTraceLabel": "Récupération des traces...",
"xpack.apm.traceOverview.topTracesTab": "Premières traces",
"xpack.apm.traceOverview.traceExplorerTab": "Explorer",
@ -11928,7 +11924,6 @@
"xpack.apm.transactionDetails.statusCode": "Code du statut",
"xpack.apm.transactionDetails.syncBadgeAsync": "async",
"xpack.apm.transactionDetails.syncBadgeBlocking": "blocage",
"xpack.apm.transactionDetails.tabs.aggregatedCriticalPathLabel": "Chemin critique agrégé",
"xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "Corrélations des transactions ayant échoué",
"xpack.apm.transactionDetails.tabs.latencyLabel": "Corrélations de latence",
"xpack.apm.transactionDetails.tabs.ProfilingLabel": "Universal Profiling",
@ -46479,4 +46474,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "Ce champ est requis.",
"xpack.watcher.watcherDescription": "Détectez les modifications survenant dans vos données en créant, gérant et monitorant des alertes."
}
}
}

View file

@ -10555,8 +10555,6 @@
"xpack.apm.crashTable.noCrashesLabel": "クラッシュが見つかりません",
"xpack.apm.crashTable.occurrencesColumnLabel": "出現回数",
"xpack.apm.crashTable.typeColumnLabel": "型",
"xpack.apm.criticalPathFlameGraph.selfTime": "自己時間:{value}",
"xpack.apm.criticalPathFlameGraph.totalTime": "合計時間:{value}",
"xpack.apm.customLink.buttom.create": "カスタムリンクを作成",
"xpack.apm.customLink.buttom.create.title": "作成",
"xpack.apm.customLink.buttom.manage": "カスタムリンクを管理",
@ -11723,8 +11721,6 @@
"xpack.apm.tooltip.link.apmServerDocs": "ドキュメント",
"xpack.apm.tooltip.maxGroup.message": "収集されているAPMデータのカーディナリティが高すぎます。この状況を軽減するには、{apmServerDocs}を確認してください。",
"xpack.apm.traceExplorer.appName": "APM",
"xpack.apm.traceExplorer.criticalPathTab": "集約されたクリティカルパス",
"xpack.apm.traceExplorer.waterfallTab": "ウォーターフォール",
"xpack.apm.traceLink.fetchingTraceLabel": "トレースを取得中...",
"xpack.apm.traceOverview.topTracesTab": "上位のトレース",
"xpack.apm.traceOverview.traceExplorerTab": "エクスプローラー",
@ -11798,7 +11794,6 @@
"xpack.apm.transactionDetails.statusCode": "ステータスコード",
"xpack.apm.transactionDetails.syncBadgeAsync": "非同期",
"xpack.apm.transactionDetails.syncBadgeBlocking": "ブロック",
"xpack.apm.transactionDetails.tabs.aggregatedCriticalPathLabel": "集約されたクリティカルパス",
"xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "失敗したトランザクションの相関関係",
"xpack.apm.transactionDetails.tabs.latencyLabel": "遅延の相関関係",
"xpack.apm.transactionDetails.tabs.ProfilingLabel": "ユニバーサルプロファイリング",
@ -46330,4 +46325,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "フィールドを選択してください。",
"xpack.watcher.watcherDescription": "アラートの作成、管理、監視によりデータへの変更を検知します。"
}
}
}

View file

@ -10377,8 +10377,6 @@
"xpack.apm.crashTable.noCrashesLabel": "未找到故障",
"xpack.apm.crashTable.occurrencesColumnLabel": "发生次数",
"xpack.apm.crashTable.typeColumnLabel": "类型",
"xpack.apm.criticalPathFlameGraph.selfTime": "独自时间:{value}",
"xpack.apm.criticalPathFlameGraph.totalTime": "总时间:{value}",
"xpack.apm.customLink.buttom.create": "创建定制链接",
"xpack.apm.customLink.buttom.create.title": "创建",
"xpack.apm.customLink.buttom.manage": "管理定制链接",
@ -11527,8 +11525,6 @@
"xpack.apm.tooltip.link.apmServerDocs": "文档",
"xpack.apm.tooltip.maxGroup.message": "正收集的 APM 数据的基数过高。请复查 {apmServerDocs} 以缓解该情况。",
"xpack.apm.traceExplorer.appName": "APM",
"xpack.apm.traceExplorer.criticalPathTab": "已聚合关键路径",
"xpack.apm.traceExplorer.waterfallTab": "瀑布",
"xpack.apm.traceLink.fetchingTraceLabel": "正在提取追溯信息......",
"xpack.apm.traceOverview.topTracesTab": "排名靠前跟踪",
"xpack.apm.traceOverview.traceExplorerTab": "浏览器",
@ -11603,7 +11599,6 @@
"xpack.apm.transactionDetails.statusCode": "状态代码",
"xpack.apm.transactionDetails.syncBadgeAsync": "异步",
"xpack.apm.transactionDetails.syncBadgeBlocking": "正在阻止",
"xpack.apm.transactionDetails.tabs.aggregatedCriticalPathLabel": "已聚合关键路径",
"xpack.apm.transactionDetails.tabs.failedTransactionsCorrelationsLabel": "失败事务相关性",
"xpack.apm.transactionDetails.tabs.latencyLabel": "延迟相关性",
"xpack.apm.transactionDetails.tabs.ProfilingLabel": "Universal Profiling",
@ -45652,4 +45647,4 @@
"xpack.watcher.watchEdit.thresholdWatchExpression.aggType.fieldIsRequiredValidationMessage": "此字段必填。",
"xpack.watcher.watcherDescription": "通过创建、管理和监测警报来检测数据中的更改。"
}
}
}

View file

@ -1,87 +0,0 @@
/*
* 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 { sumBy } from 'lodash';
import type { CriticalPathResponse } from '../../server/routes/traces/get_aggregated_critical_path';
export interface CriticalPathTreeNode {
nodeId: string;
children: CriticalPathTreeNode[];
countInclusive: number;
countExclusive: number;
}
export function getAggregatedCriticalPathRootNodes(params: {
criticalPath: CriticalPathResponse;
}): {
rootNodes: CriticalPathTreeNode[];
maxDepth: number;
numNodes: number;
} {
let maxDepth = 20; // min max depth
const { criticalPath } = params;
let numNodes = 0;
function mergeNodesWithSameOperationId(nodes: CriticalPathTreeNode[]): CriticalPathTreeNode[] {
const nodesByOperationId: Record<string, CriticalPathTreeNode> = {};
const mergedNodes = nodes.reduce<CriticalPathTreeNode[]>((prev, node, index, array) => {
const nodeId = node.nodeId;
const operationId = criticalPath.operationIdByNodeId[nodeId];
if (nodesByOperationId[operationId]) {
const prevNode = nodesByOperationId[operationId];
prevNode.children.push(...node.children);
prevNode.countExclusive += node.countExclusive;
prevNode.countInclusive += node.countInclusive;
return prev;
}
nodesByOperationId[operationId] = node;
prev.push(node);
return prev;
}, []);
numNodes += mergedNodes.length;
mergedNodes.forEach((node) => {
node.children = mergeNodesWithSameOperationId(node.children);
});
return mergedNodes;
}
function getNode(nodeId: string, depth: number): CriticalPathTreeNode {
maxDepth = Math.max(maxDepth, depth);
const children = criticalPath.nodes[nodeId].map((childNodeId) =>
getNode(childNodeId, depth + 1)
);
const nodeCountExclusive = criticalPath.timeByNodeId[nodeId] || 0;
const nodeCountInclusive =
sumBy(children, (child) => child.countInclusive) + nodeCountExclusive;
return {
nodeId,
children,
countInclusive: nodeCountInclusive,
countExclusive: nodeCountExclusive,
};
}
const rootNodes = mergeNodesWithSameOperationId(
criticalPath.rootNodes.map((nodeId) => getNode(nodeId, 1))
);
return {
rootNodes,
maxDepth,
numNodes,
};
}

View file

@ -1,9 +0,0 @@
/*
* 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.
*/
// for API tests
export { getAggregatedCriticalPathRootNodes } from './critical_path/get_aggregated_critical_path_root_nodes';

View file

@ -4,22 +4,17 @@
* 2.0; you may not use this file except in compliance with the Elastic License
* 2.0.
*/
import { EuiFlexGroup, EuiFlexItem, EuiTab, EuiTabs } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import React, { useEffect, useMemo, useState } from 'react';
import { useHistory } from 'react-router-dom';
import type { TraceSearchQuery } from '../../../../common/trace_explorer';
import { TraceSearchType } from '../../../../common/trace_explorer';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useApmRouter } from '../../../hooks/use_apm_router';
import { useApmRoutePath } from '../../../hooks/use_apm_route_path';
import { useTimeRange } from '../../../hooks/use_time_range';
import { TraceExplorerSamplesFetcherContextProvider } from '../../../hooks/use_trace_explorer_samples';
import type { APIClientRequestParamsOf } from '../../../services/rest/create_call_apm_api';
import { ApmDatePicker } from '../../shared/date_picker/apm_date_picker';
import { push } from '../../shared/links/url_helpers';
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
import { TransactionTab } from '../transaction_details/waterfall_with_summary/transaction_tabs';
import { TraceSearchBox } from './trace_search_box';
export function TraceExplorer({ children }: { children: React.ReactElement }) {
@ -29,7 +24,6 @@ export function TraceExplorer({ children }: { children: React.ReactElement }) {
});
const {
query,
query: { rangeFrom, rangeTo, environment, query: queryFromUrlParams, type: typeFromUrlParams },
} = useApmParams('/traces/explorer');
@ -61,10 +55,6 @@ export function TraceExplorer({ children }: { children: React.ReactElement }) {
};
}, [start, end, environment, queryFromUrlParams, typeFromUrlParams]);
const router = useApmRouter();
const routePath = useApmRoutePath();
return (
<TraceExplorerSamplesFetcherContextProvider params={params}>
<EuiFlexGroup direction="column" gutterSize="s">
@ -93,47 +83,6 @@ export function TraceExplorer({ children }: { children: React.ReactElement }) {
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiTabs>
<EuiTab
href={router.link('/traces/explorer/waterfall', {
query: {
...query,
traceId: '',
transactionId: '',
waterfallItemId: '',
detailTab: TransactionTab.timeline,
},
})}
isSelected={routePath === '/traces/explorer/waterfall'}
>
{i18n.translate('xpack.apm.traceExplorer.waterfallTab', {
defaultMessage: 'Waterfall',
})}
</EuiTab>
<EuiTab
href={router.link('/traces/explorer/critical_path', {
query,
})}
isSelected={routePath === '/traces/explorer/critical_path'}
>
<EuiFlexGroup direction="row" gutterSize="s">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.apm.traceExplorer.criticalPathTab', {
defaultMessage: 'Aggregated critical path',
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
<TechnicalPreviewBadge
icon="beaker"
size="s"
style={{ verticalAlign: 'middle' }}
/>
</EuiFlexItem>
</EuiFlexGroup>
</EuiTab>
</EuiTabs>
</EuiFlexItem>
<EuiFlexItem>{children}</EuiFlexItem>
</EuiFlexGroup>
</TraceExplorerSamplesFetcherContextProvider>

View file

@ -1,51 +0,0 @@
/*
* 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 React, { useMemo, useCallback } from 'react';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { useApmParams } from '../../../hooks/use_apm_params';
import { useTimeRange } from '../../../hooks/use_time_range';
import { useTraceExplorerSamples } from '../../../hooks/use_trace_explorer_samples';
import { CriticalPathFlamegraph } from '../../shared/critical_path_flamegraph';
export function TraceExplorerAggregatedCriticalPath() {
const {
query: { rangeFrom, rangeTo },
} = useApmParams('/traces/explorer/critical_path');
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const {
data: { traceSamples },
status: samplesFetchStatus,
} = useTraceExplorerSamples();
const { onPageReady } = usePerformanceContext();
const traceIds = useMemo(() => {
return traceSamples.map((sample) => sample.traceId);
}, [traceSamples]);
const handleOnLoadTable = useCallback(() => {
onPageReady({
meta: {
rangeFrom: start,
rangeTo: end,
},
customMetrics: {
key1: 'traceIds',
value1: traceIds.length,
},
});
}, [start, end, traceIds, onPageReady]);
return (
<CriticalPathFlamegraph
start={start}
end={end}
traceIds={traceIds}
traceIdsFetchStatus={samplesFetchStatus}
onLoadTable={handleOnLoadTable}
/>
);
}

View file

@ -1,77 +0,0 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import React, { useMemo, useCallback } from 'react';
import { usePerformanceContext } from '@kbn/ebt-tools';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
import { useTimeRange } from '../../../hooks/use_time_range';
import { CriticalPathFlamegraph } from '../../shared/critical_path_flamegraph';
import { TechnicalPreviewBadge } from '../../shared/technical_preview_badge';
import type { TabContentProps } from './transaction_details_tabs';
function TransactionDetailAggregatedCriticalPath({ traceSamplesFetchResult }: TabContentProps) {
const {
path: { serviceName },
query: { rangeFrom, rangeTo, transactionName },
} = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
'/mobile-services/{serviceName}/transactions/view'
);
const { onPageReady } = usePerformanceContext();
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
const traceIds = useMemo(() => {
return traceSamplesFetchResult.data?.traceSamples.map((sample) => sample.traceId) ?? [];
}, [traceSamplesFetchResult.data]);
const handleOnLoadTable = useCallback(() => {
onPageReady({
meta: {
rangeFrom: start,
rangeTo: end,
},
customMetrics: {
key1: 'traceIds',
value1: traceIds.length,
},
});
}, [start, end, traceIds, onPageReady]);
return (
<CriticalPathFlamegraph
start={start}
end={end}
traceIdsFetchStatus={traceSamplesFetchResult.status}
traceIds={traceIds}
serviceName={serviceName}
transactionName={transactionName}
onLoadTable={handleOnLoadTable}
/>
);
}
export const aggregatedCriticalPathTab = {
dataTestSubj: 'apmAggregatedCriticalPathTabButton',
key: 'aggregatedCriticalPath',
label: (
<EuiFlexGroup gutterSize="s" direction="row">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.apm.transactionDetails.tabs.aggregatedCriticalPathLabel', {
defaultMessage: 'Aggregated critical path',
})}
</EuiFlexItem>
<EuiFlexItem>
<TechnicalPreviewBadge icon="beaker" size="s" style={{ verticalAlign: 'middle' }} />
</EuiFlexItem>
</EuiFlexGroup>
),
component: TransactionDetailAggregatedCriticalPath,
};

View file

@ -15,13 +15,11 @@ import { usePerformanceContext } from '@kbn/ebt-tools';
import { maybe } from '../../../../common/utils/maybe';
import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_params';
import { useAnyOfApmParams } from '../../../hooks/use_apm_params';
import { useCriticalPathFeatureEnabledSetting } from '../../../hooks/use_critical_path_feature_enabled_setting';
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { useSampleChartSelection } from '../../../hooks/use_sample_chart_selection';
import type { TraceSamplesFetchResult } from '../../../hooks/use_transaction_trace_samples_fetcher';
import { useTransactionTraceSamplesFetcher } from '../../../hooks/use_transaction_trace_samples_fetcher';
import { fromQuery, toQuery } from '../../shared/links/url_helpers';
import { aggregatedCriticalPathTab } from './aggregated_critical_path_tab';
import { failedTransactionsCorrelationsTab } from './failed_transactions_correlations_tab';
import { latencyCorrelationsTab } from './latency_correlations_tab';
import { profilingTab } from './profiling_tab';
@ -46,22 +44,18 @@ export function TransactionDetailsTabs() {
);
const { agentName } = useApmServiceContext();
const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting();
const isTransactionProfilingEnabled = useTransactionProfilingSetting();
const { onPageReady } = usePerformanceContext();
const availableTabs = useMemo(() => {
const tabs = [traceSamplesTab, latencyCorrelationsTab, failedTransactionsCorrelationsTab];
if (isCriticalPathFeatureEnabled) {
tabs.push(aggregatedCriticalPathTab);
}
if (isTransactionProfilingEnabled && isJavaAgentName(agentName)) {
tabs.push(profilingTab);
}
return tabs;
}, [agentName, isCriticalPathFeatureEnabled, isTransactionProfilingEnabled]);
}, [agentName, isTransactionProfilingEnabled]);
const { urlParams } = useLegacyUrlParams();
const history = useHistory();

View file

@ -39,11 +39,6 @@ const TopTracesOverview = dynamic(() =>
const TraceExplorer = dynamic(() =>
import('../../app/trace_explorer').then((mod) => ({ default: mod.TraceExplorer }))
);
const TraceExplorerAggregatedCriticalPath = dynamic(() =>
import('../../app/trace_explorer/trace_explorer_aggregated_critical_path').then((mod) => ({
default: mod.TraceExplorerAggregatedCriticalPath,
}))
);
const TraceExplorerWaterfall = dynamic(() =>
import('../../app/trace_explorer/trace_explorer_waterfall').then((mod) => ({
default: mod.TraceExplorerWaterfall,
@ -199,9 +194,6 @@ export const homeRoute = {
},
},
},
'/traces/explorer/critical_path': {
element: <TraceExplorerAggregatedCriticalPath />,
},
'/traces/explorer': {
element: <RedirectTo pathname="/traces/explorer/waterfall" />,
},

View file

@ -1,113 +0,0 @@
/*
* 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiPanel } from '@elastic/eui';
import React from 'react';
import { ProcessorEvent } from '@kbn/observability-plugin/common';
import { i18n } from '@kbn/i18n';
import { AgentIcon } from '@kbn/custom-icons';
import type { CriticalPathResponse } from '../../../../server/routes/traces/get_aggregated_critical_path';
import {
AGENT_NAME,
SERVICE_NAME,
SPAN_NAME,
SPAN_SUBTYPE,
SPAN_TYPE,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../../common/es_fields/apm';
import { SpanIcon } from '../span_icon';
import { asPercent } from '../../../../common/utils/formatters';
export function CriticalPathFlamegraphTooltip({
metadata,
countInclusive,
countExclusive,
totalCount,
}: {
metadata?: CriticalPathResponse['metadata'][string];
countInclusive: number;
countExclusive: number;
totalCount: number;
}) {
if (!metadata) {
return <></>;
}
return (
<EuiPanel>
<EuiFlexGroup direction="column" gutterSize="s">
{metadata['processor.event'] === ProcessorEvent.transaction ? (
<EuiFlexItem grow={false}>
<EuiFlexGroup
direction="row"
gutterSize="s"
style={{ overflowWrap: 'anywhere' }}
alignItems="center"
>
<EuiFlexItem grow={false}>{metadata[TRANSACTION_NAME]}</EuiFlexItem>
<EuiFlexItem grow={false}>
<EuiBadge>{metadata[TRANSACTION_TYPE]}</EuiBadge>
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
) : (
<EuiFlexItem>
<EuiFlexGroup
direction="row"
gutterSize="s"
style={{ overflowWrap: 'anywhere' }}
alignItems="center"
>
<EuiFlexItem grow={false}>
<SpanIcon
type={metadata[SPAN_TYPE]}
subtype={metadata[SPAN_SUBTYPE]}
role="presentation"
/>
</EuiFlexItem>
<EuiFlexItem grow={false}>{metadata[SPAN_NAME]}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiHorizontalRule margin="none" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="row" gutterSize="xs" alignItems="center">
<EuiFlexItem grow={false}>
<AgentIcon agentName={metadata[AGENT_NAME]} size="l" role="presentation" />
</EuiFlexItem>
<EuiFlexItem grow={false}>{metadata[SERVICE_NAME]}</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem>
<EuiHorizontalRule margin="none" />
</EuiFlexItem>
<EuiFlexItem>
<EuiFlexGroup direction="column" gutterSize="s">
<EuiFlexItem grow={false}>
{i18n.translate('xpack.apm.criticalPathFlameGraph.selfTime', {
defaultMessage: 'Self time: {value}',
values: {
value: asPercent(countExclusive / totalCount, 1),
},
})}
</EuiFlexItem>
<EuiFlexItem grow={false}>
{i18n.translate('xpack.apm.criticalPathFlameGraph.totalTime', {
defaultMessage: 'Total time: {value}',
values: {
value: asPercent(countInclusive / totalCount, 1),
},
})}
</EuiFlexItem>
</EuiFlexGroup>
</EuiFlexItem>
</EuiFlexGroup>
</EuiPanel>
);
}

View file

@ -1,132 +0,0 @@
/*
* 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 { ColumnarViewModel } from '@elastic/charts';
import { memoize, sumBy } from 'lodash';
import { lighten, parseToRgb } from 'polished';
import seedrandom from 'seedrandom';
import type { CriticalPathResponse } from '../../../../server/routes/traces/get_aggregated_critical_path';
import type { CriticalPathTreeNode } from '../../../../common/critical_path/get_aggregated_critical_path_root_nodes';
import { getAggregatedCriticalPathRootNodes } from '../../../../common/critical_path/get_aggregated_critical_path_root_nodes';
const lightenColor = lighten(0.2);
export function criticalPathToFlamegraph(
params: {
criticalPath: CriticalPathResponse;
colors: string[];
} & ({ serviceName: string; transactionName: string } | {})
): {
viewModel: ColumnarViewModel;
operationId: string[];
countExclusive: Float64Array;
sum: number;
} {
let sum = 0;
const { criticalPath, colors } = params;
const { rootNodes, maxDepth, numNodes } = getAggregatedCriticalPathRootNodes(params);
// include the root node
const totalSize = numNodes + 1;
const operationId = new Array<string>(totalSize);
const countInclusive = new Float64Array(totalSize);
const countExclusive = new Float64Array(totalSize);
const label = new Array<string>(totalSize);
const position = new Float32Array(totalSize * 2);
const size = new Float32Array(totalSize);
const color = new Float32Array(totalSize * 4);
// eslint-disable-next-line guard-for-in
for (const nodeId in criticalPath.timeByNodeId) {
const count = criticalPath.timeByNodeId[nodeId];
sum += count;
}
let maxValue = 0;
let index = 0;
const availableColors: Array<[number, number, number, number]> = colors.map((vizColor) => {
const rgb = parseToRgb(lightenColor(vizColor));
return [rgb.red / 255, rgb.green / 255, rgb.blue / 255, 1];
});
const pickColor = memoize((identifier: string) => {
const idx = Math.abs(seedrandom(identifier).int32()) % availableColors.length;
return availableColors[idx];
});
function addNodeToFlamegraph(node: CriticalPathTreeNode, x: number, y: number) {
let nodeOperationId: string;
let nodeLabel: string;
let operationMetadata: CriticalPathResponse['metadata'][string] | undefined;
if (node.nodeId === 'root') {
nodeOperationId = '';
nodeLabel = 'root';
} else {
nodeOperationId = criticalPath.operationIdByNodeId[node.nodeId];
operationMetadata = criticalPath.metadata[nodeOperationId];
nodeLabel =
operationMetadata['processor.event'] === 'transaction'
? operationMetadata['transaction.name']
: operationMetadata['span.name'];
}
operationId[index] = nodeOperationId;
countInclusive[index] = node.countInclusive;
countExclusive[index] = node.countExclusive;
label[index] = nodeLabel;
position[index * 2] = x / maxValue;
position[index * 2 + 1] = 1 - (y + 1) / (maxDepth + 1);
size[index] = node.countInclusive / maxValue;
const identifier =
operationMetadata?.['processor.event'] === 'transaction'
? operationMetadata['transaction.type']
: operationMetadata?.['span.subtype'] || operationMetadata?.['span.type'] || '';
color.set(pickColor(identifier), index * 4);
index++;
let childX = x;
node.children.forEach((child) => {
addNodeToFlamegraph(child, childX, y + 1);
childX += child.countInclusive;
});
}
const root: CriticalPathTreeNode = {
children: rootNodes,
nodeId: 'root',
countExclusive: 0,
countInclusive: sumBy(rootNodes, 'countInclusive'),
};
maxValue = root.countInclusive;
addNodeToFlamegraph(root, 0, 0);
return {
viewModel: {
value: countInclusive,
label,
color,
position0: position,
position1: position,
size0: size,
size1: size,
},
operationId,
countExclusive,
sum,
};
}

View file

@ -1,162 +0,0 @@
/*
* 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 { Datum } from '@elastic/charts';
import { Chart, Flame, Settings, Tooltip } from '@elastic/charts';
import { EuiFlexGroup, EuiFlexItem, EuiLoadingSpinner, euiPaletteColorBlind } from '@elastic/eui';
import { css } from '@emotion/css';
import { useChartThemes } from '@kbn/observability-shared-plugin/public';
import { uniqueId } from 'lodash';
import React, { useEffect, useMemo, useRef } from 'react';
import { i18n } from '@kbn/i18n';
import type { FETCH_STATUS } from '../../../hooks/use_fetcher';
import { isSuccess } from '../../../hooks/use_fetcher';
import { useFetcher, isPending } from '../../../hooks/use_fetcher';
import { CriticalPathFlamegraphTooltip } from './critical_path_flamegraph_tooltip';
import { criticalPathToFlamegraph } from './critical_path_to_flamegraph';
const chartClassName = css`
flex-grow: 1;
`;
export function CriticalPathFlamegraph(
props: {
start: string;
end: string;
traceIds: string[];
traceIdsFetchStatus: FETCH_STATUS;
onLoadTable?: () => void;
} & ({ serviceName: string; transactionName: string } | {})
) {
const { start, end, traceIds, traceIdsFetchStatus, onLoadTable } = props;
const serviceName = 'serviceName' in props ? props.serviceName : null;
const transactionName = 'transactionName' in props ? props.transactionName : null;
// Use a reference to time range, to not invalidate the API fetch
// we only care for traceIds, start/end are there to limit the search
// request to a certain time range. It shouldn't affect the actual results
// of the search.
const timerange = useRef({ start, end });
timerange.current = { start, end };
const { data: { criticalPath } = { criticalPath: null }, status: criticalPathFetchStatus } =
useFetcher(
(callApmApi) => {
if (!traceIds.length) {
return Promise.resolve({ criticalPath: null });
}
return callApmApi('POST /internal/apm/traces/aggregated_critical_path', {
params: {
body: {
start: timerange.current.start,
end: timerange.current.end,
traceIds,
serviceName,
transactionName,
},
},
});
},
[timerange, traceIds, serviceName, transactionName]
);
useEffect(() => {
if (isSuccess(criticalPathFetchStatus) && isSuccess(traceIdsFetchStatus)) {
onLoadTable?.();
}
}, [start, end, criticalPathFetchStatus, traceIdsFetchStatus, traceIds, onLoadTable]);
const chartThemes = useChartThemes();
const isLoading = isPending(traceIdsFetchStatus) || isPending(criticalPathFetchStatus);
const flameGraph = useMemo(() => {
if (!criticalPath) {
return undefined;
}
const colors = euiPaletteColorBlind({});
const flamegraph = criticalPathToFlamegraph({
criticalPath,
colors,
});
return {
...flamegraph,
// make sure Flame re-renders when data changes, workaround for https://github.com/elastic/elastic-charts/issues/1766
key: uniqueId(),
};
}, [criticalPath]);
const themeOverrides = {
chartMargins: { top: 0, left: 0, bottom: 0, right: 0 },
chartPaddings: { left: 0, right: 0, top: 0, bottom: 0 },
};
return (
<EuiFlexGroup
direction="column"
gutterSize="l"
alignItems="stretch"
justifyContent="center"
style={{ minHeight: 400 }}
>
{isLoading ? (
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
<EuiLoadingSpinner size="l" />
</EuiFlexItem>
) : (
flameGraph && (
<EuiFlexItem grow>
<Chart key={flameGraph.key} className={chartClassName}>
<Tooltip
customTooltip={(tooltipProps) => {
const valueIndex = tooltipProps.values[0].valueAccessor as number;
const operationId = flameGraph.operationId[valueIndex];
const operationMetadata = criticalPath?.metadata[operationId];
const countInclusive = flameGraph.viewModel.value[valueIndex];
const countExclusive = flameGraph.countExclusive[valueIndex];
return (
<CriticalPathFlamegraphTooltip
metadata={operationMetadata}
countInclusive={countInclusive}
countExclusive={countExclusive}
totalCount={flameGraph.viewModel.value[0]}
/>
);
}}
/>
<Settings
theme={[
{
chartMargins: themeOverrides.chartMargins,
chartPaddings: themeOverrides.chartPaddings,
},
...chartThemes.theme,
]}
baseTheme={chartThemes.baseTheme}
onElementClick={(elements) => {}}
locale={i18n.getLocale()}
/>
<Flame
id="aggregated_critical_path"
columnarData={flameGraph.viewModel}
valueAccessor={(d: Datum) => d.value as number}
valueFormatter={(value) => `${value}`}
animation={{ duration: 100 }}
controlProviderCallback={{}}
/>
</Chart>
</EuiFlexItem>
)
)}
</EuiFlexGroup>
);
}

View file

@ -1,422 +0,0 @@
/*
* 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 { ProcessorEvent } from '@kbn/observability-plugin/common';
import { rangeQuery, termsQuery } from '@kbn/observability-plugin/server';
import type { Logger } from '@kbn/logging';
import type {
AGENT_NAME,
PROCESSOR_EVENT,
SERVICE_NAME,
SPAN_NAME,
SPAN_SUBTYPE,
SPAN_TYPE,
TRANSACTION_NAME,
TRANSACTION_TYPE,
} from '../../../common/es_fields/apm';
import { TRACE_ID } from '../../../common/es_fields/apm';
import type { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
import type { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
type OperationMetadata = {
[SERVICE_NAME]: string;
[AGENT_NAME]: AgentName;
} & (
| {
[PROCESSOR_EVENT]: ProcessorEvent.transaction;
[TRANSACTION_TYPE]: string;
[TRANSACTION_NAME]: string;
}
| {
[PROCESSOR_EVENT]: ProcessorEvent.span;
[SPAN_NAME]: string;
[SPAN_TYPE]: string;
[SPAN_SUBTYPE]: string;
}
);
type OperationId = string;
type NodeId = string;
export interface CriticalPathResponse {
metadata: Record<OperationId, OperationMetadata>;
timeByNodeId: Record<NodeId, number>;
nodes: Record<NodeId, NodeId[]>;
rootNodes: NodeId[];
operationIdByNodeId: Record<NodeId, OperationId>;
}
const TWO_DAYS_MS = 2 * 24 * 60 * 60 * 1000;
export async function getAggregatedCriticalPath({
traceIds,
start,
end,
apmEventClient,
serviceName,
transactionName,
logger,
}: {
traceIds: string[];
start: number;
end: number;
apmEventClient: APMEventClient;
serviceName: string | null;
transactionName: string | null;
logger: Logger;
}): Promise<{ criticalPath: CriticalPathResponse | null }> {
const now = Date.now();
const response = await apmEventClient.search('get_aggregated_critical_path', {
apm: {
events: [ProcessorEvent.span, ProcessorEvent.transaction],
},
size: 0,
track_total_hits: false,
query: {
bool: {
filter: [
...termsQuery(TRACE_ID, ...traceIds),
// we need a range query to allow ES to skip shards based on the time range,
// but we need enough padding to make sure we get the full trace
...rangeQuery(start - TWO_DAYS_MS, end + TWO_DAYS_MS),
],
},
},
aggs: {
critical_path: {
scripted_metric: {
params: {
// can't send null parameters to ES. undefined will be removed during JSON serialisation
serviceName: serviceName || undefined,
transactionName: transactionName || undefined,
},
init_script: {
source: `
state.eventsById = [:];
state.metadataByOperationId = [:];
`,
},
map_script: {
source: `
String toHash (def item) {
long FNV_32_INIT = 0x811c9dc5L;
long FNV_32_PRIME = 0x01000193L;
char[] chars = item.toString().toCharArray();
long rv = FNV_32_INIT;
int len = chars.length;
for(int i = 0; i < len; i++) {
byte bt = (byte) chars[i];
rv ^= bt;
rv *= FNV_32_PRIME;
}
return rv.toString();
}
def id;
double duration;
def operationMetadata = [
"service.name": $('service.name', ''),
"processor.event": $('processor.event', ''),
"agent.name": $('agent.name', '')
];
def spanName = $('span.name', null);
id = $('span.id', null);
if (id != null && spanName != null) {
operationMetadata.put('span.name', spanName);
def spanType = $('span.type', '');
if (spanType != '') {
operationMetadata.put('span.type', spanType);
}
def spanSubtype = $('span.subtype', '');
if (spanSubtype != '') {
operationMetadata.put('span.subtype', spanSubtype);
}
duration = $('span.duration.us', 0);
} else {
id = $('transaction.id', '');
operationMetadata.put('transaction.name', $('transaction.name', ''));
operationMetadata.put('transaction.type', $('transaction.type', ''));
duration = $('transaction.duration.us', 0);
}
String operationId = toHash(operationMetadata);
def map = [
"traceId": $('trace.id', ''),
"id": id,
"parentId": $('parent.id', null),
"operationId": operationId,
"timestamp": $('timestamp.us', 0),
"duration": duration
];
if (state.metadataByOperationId[operationId] == null) {
state.metadataByOperationId.put(operationId, operationMetadata);
}
state.eventsById.put(id, map);
`,
},
combine_script: {
source: 'return state;',
},
reduce_script: {
source: `
String toHash (def item) {
long FNV_32_INIT = 0x811c9dc5L;
long FNV_32_PRIME = 0x01000193L;
char[] chars = item.toString().toCharArray();
long rv = FNV_32_INIT;
int len = chars.length;
for(int i = 0; i < len; i++) {
byte bt = (byte) chars[i];
rv ^= bt;
rv *= FNV_32_PRIME;
}
return rv.toString();
}
def processEvent (def context, def event) {
if (context.processedEvents[event.id] != null) {
return context.processedEvents[event.id];
}
def processedEvent = [
"children": []
];
if(event.parentId != null) {
def parent = context.events[event.parentId];
if (parent == null) {
return null;
}
def processedParent = processEvent(context, parent);
if (processedParent == null) {
return null;
}
processedParent.children.add(processedEvent);
}
context.processedEvents.put(event.id, processedEvent);
processedEvent.putAll(event);
if (context.params.serviceName != null && context.params.transactionName != null) {
def metadata = context.metadata[event.operationId];
if (metadata != null
&& context.params.serviceName == metadata['service.name']
&& metadata['transaction.name'] != null
&& context.params.transactionName == metadata['transaction.name']
) {
context.entryTransactions.add(processedEvent);
}
} else if (event.parentId == null) {
context.entryTransactions.add(processedEvent);
}
return processedEvent;
}
double getClockSkew (def context, def item, def parent ) {
if (parent == null) {
return 0;
}
def processorEvent = context.metadata[item.operationId]['processor.event'];
def isTransaction = processorEvent == 'transaction';
if (!isTransaction) {
return parent.skew;
}
double parentStart = parent.timestamp + parent.skew;
double offsetStart = parentStart - item.timestamp;
if (offsetStart > 0) {
double latency = Math.round(Math.max(parent.duration - item.duration, 0) / 2);
return offsetStart + latency;
}
return 0;
}
void setOffsetAndSkew ( def context, def event, def parent, def startOfTrace ) {
event.skew = getClockSkew(context, event, parent);
event.offset = event.timestamp - startOfTrace;
for(child in event.children) {
setOffsetAndSkew(context, child, event, startOfTrace);
}
event.end = event.offset + event.skew + event.duration;
}
void count ( def context, def nodeId, def duration ) {
context.timeByNodeId[nodeId] = (context.timeByNodeId[nodeId] ?: 0) + duration;
}
void scan ( def context, def item, def start, def end, def path ) {
def nodeId = toHash(path);
def childNodes = context.nodes[nodeId] != null ? context.nodes[nodeId] : [];
context.nodes[nodeId] = childNodes;
context.operationIdByNodeId[nodeId] = item.operationId;
if (item.children.size() == 0) {
count(context, nodeId, end - start);
return;
}
item.children.sort((a, b) -> {
if (b.end === a.end) {
return 0;
}
if (b.end > a.end) {
return 1;
}
return -1;
});
def scanTime = end;
for(child in item.children) {
double normalizedChildStart = Math.max(child.offset + child.skew, start);
double childEnd = child.offset + child.skew + child.duration;
double normalizedChildEnd = Math.min(childEnd, scanTime);
def isOnCriticalPath = !(
normalizedChildStart >= scanTime ||
normalizedChildEnd < start ||
childEnd > scanTime
);
if (!isOnCriticalPath) {
continue;
}
def childPath = path.clone();
childPath.add(child.operationId);
def childId = toHash(childPath);
if(!childNodes.contains(childId)) {
childNodes.add(childId);
}
if (normalizedChildEnd < (scanTime - 1000)) {
count(context, nodeId, scanTime - normalizedChildEnd);
}
scan(context, child, normalizedChildStart, childEnd, childPath);
scanTime = normalizedChildStart;
}
if (scanTime > start) {
count(context, nodeId, scanTime - start);
}
}
def events = [:];
def metadata = [:];
def processedEvents = [:];
def entryTransactions = [];
def timeByNodeId = [:];
def nodes = [:];
def rootNodes = [];
def operationIdByNodeId = [:];
def context = [
"events": events,
"metadata": metadata,
"processedEvents": processedEvents,
"entryTransactions": entryTransactions,
"timeByNodeId": timeByNodeId,
"nodes": nodes,
"operationIdByNodeId": operationIdByNodeId,
"params": params
];
for(state in states) {
if (state.eventsById != null) {
events.putAll(state.eventsById);
}
if (state.metadataByOperationId != null) {
metadata.putAll(state.metadataByOperationId);
}
}
for(def event: events.values()) {
processEvent(context, event);
}
for(transaction in context.entryTransactions) {
transaction.skew = 0;
transaction.offset = 0;
setOffsetAndSkew(context, transaction, null, transaction.timestamp);
def path = [];
def parent = transaction;
while (parent != null) {
path.add(parent.operationId);
if (parent.parentId == null) {
break;
}
parent = context.processedEvents[parent.parentId];
}
Collections.reverse(path);
def nodeId = toHash(path);
scan(context, transaction, 0, transaction.duration, path);
if (!rootNodes.contains(nodeId)) {
rootNodes.add(nodeId);
}
}
return [
"timeByNodeId": timeByNodeId,
"metadata": metadata,
"nodes": nodes,
"rootNodes": rootNodes,
"operationIdByNodeId": operationIdByNodeId
];`,
},
},
},
},
});
logger.debug(`Retrieved critical path in ${Date.now() - now}ms, took: ${response.took}ms`);
if (!response.aggregations) {
return {
criticalPath: null,
};
}
const criticalPath = response.aggregations?.critical_path.value as CriticalPathResponse;
return {
criticalPath,
};
}

View file

@ -6,7 +6,7 @@
*/
import * as t from 'io-ts';
import { nonEmptyStringRt, toNumberRt } from '@kbn/io-ts-utils';
import { toNumberRt } from '@kbn/io-ts-utils';
import { TraceSearchType } from '../../../common/trace_explorer';
import { getSearchTransactionsEvents } from '../../lib/helpers/transactions';
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
@ -24,8 +24,6 @@ import type { TraceSamplesResponse } from './get_trace_samples_by_query';
import { getTraceSamplesByQuery } from './get_trace_samples_by_query';
import { getRandomSampler } from '../../lib/helpers/get_random_sampler';
import { getApmEventClient } from '../../lib/helpers/get_apm_event_client';
import type { CriticalPathResponse } from './get_aggregated_critical_path';
import { getAggregatedCriticalPath } from './get_aggregated_critical_path';
import { getSpan } from '../transactions/get_span';
import type { Transaction } from '../../../typings/es_schemas/ui/transaction';
import type { Span } from '../../../typings/es_schemas/ui/span';
@ -247,40 +245,6 @@ const findTracesRoute = createApmServerRoute({
},
});
const aggregatedCriticalPathRoute = createApmServerRoute({
endpoint: 'POST /internal/apm/traces/aggregated_critical_path',
params: t.type({
body: t.intersection([
t.type({
traceIds: t.array(t.string),
serviceName: t.union([nonEmptyStringRt, t.null]),
transactionName: t.union([nonEmptyStringRt, t.null]),
}),
rangeRt,
]),
}),
security: { authz: { requiredPrivileges: ['apm'] } },
handler: async (resources): Promise<{ criticalPath: CriticalPathResponse | null }> => {
const {
params: {
body: { traceIds, start, end, serviceName, transactionName },
},
} = resources;
const apmEventClient = await getApmEventClient(resources);
return getAggregatedCriticalPath({
traceIds,
start,
end,
apmEventClient,
serviceName,
transactionName,
logger: resources.logger,
});
},
});
const transactionFromTraceByIdRoute = createApmServerRoute({
endpoint: 'GET /internal/apm/traces/{traceId}/transactions/{transactionId}',
params: t.type({
@ -352,7 +316,6 @@ export const traceRouteRepository = {
...rootTransactionByTraceIdRoute,
...transactionByIdRoute,
...findTracesRoute,
...aggregatedCriticalPathRoute,
...transactionFromTraceByIdRoute,
...spanFromTraceByIdRoute,
...transactionByNameRoute,

View file

@ -1,438 +0,0 @@
/*
* 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 { getAggregatedCriticalPathRootNodes } from '@kbn/apm-plugin/common';
import { apm, ApmFields, SynthtraceGenerator, timerange } from '@kbn/apm-synthtrace-client';
import expect from '@kbn/expect';
import { Assign } from '@kbn/utility-types';
import { compact, invert, sortBy, uniq } from 'lodash';
import { Readable } from 'stream';
import type { ApmSynthtraceEsClient } from '@kbn/apm-synthtrace';
import type { SupertestReturnType } from '../../../../services/apm_api';
import type { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_context';
export default function ApiTest({ getService }: DeploymentAgnosticFtrProviderContext) {
const apmApiClient = getService('apmApi');
const synthtrace = getService('synthtrace');
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
type Node = ReturnType<typeof getAggregatedCriticalPathRootNodes>['rootNodes'][0];
type Metadata = NonNullable<
SupertestReturnType<'POST /internal/apm/traces/aggregated_critical_path'>['body']['criticalPath']
>['metadata'][string];
type HydratedNode = Assign<Node, { metadata?: Metadata; children: HydratedNode[] }>;
interface FormattedNode {
name: string;
value: number;
children: FormattedNode[];
}
describe('Aggregated critical path', () => {
let apmSynthtraceEsClient: ApmSynthtraceEsClient;
// format tree in somewhat concise format for easier testing
function formatTree(nodes: HydratedNode[]): FormattedNode[] {
return sortBy(
nodes.map((node) => {
const name =
node.metadata?.['processor.event'] === 'transaction'
? node.metadata['transaction.name']
: node.metadata?.['span.name'] || 'root';
return { name, value: node.countExclusive, children: formatTree(node.children) };
}),
(node) => node.name
);
}
async function fetchAndBuildCriticalPathTree(
options: { fn: () => SynthtraceGenerator<ApmFields> } & (
| { serviceName: string; transactionName: string }
| {}
)
) {
const { fn } = options;
const generator = fn();
const unserialized = Array.from(generator);
const serialized = unserialized.flatMap((event) => event.serialize());
const traceIds = compact(uniq(serialized.map((event) => event['trace.id'])));
await apmSynthtraceEsClient.index(Readable.from(unserialized));
return apmApiClient
.readUser({
endpoint: 'POST /internal/apm/traces/aggregated_critical_path',
params: {
body: {
start: new Date(start).toISOString(),
end: new Date(end).toISOString(),
traceIds,
serviceName: 'serviceName' in options ? options.serviceName : null,
transactionName: 'transactionName' in options ? options.transactionName : null,
},
},
})
.then((response) => {
const criticalPath = response.body.criticalPath!;
const nodeIdByOperationId = invert(criticalPath.operationIdByNodeId);
const { rootNodes, maxDepth } = getAggregatedCriticalPathRootNodes({
criticalPath,
});
function hydrateNode(node: Node): HydratedNode {
return {
...node,
metadata: criticalPath.metadata[criticalPath.operationIdByNodeId[node.nodeId]],
children: node.children.map(hydrateNode),
};
}
return {
rootNodes: rootNodes.map(hydrateNode),
maxDepth,
criticalPath,
nodeIdByOperationId,
};
});
}
before(async () => {
apmSynthtraceEsClient = await synthtrace.createApmSynthtraceEsClient();
});
after(() => apmSynthtraceEsClient.clean());
it('builds up the correct tree for a single transaction', async () => {
const java = apm
.service({ name: 'java', environment: 'production', agentName: 'java' })
.instance('java');
const duration = 1000;
const rate = 10;
const { rootNodes } = await fetchAndBuildCriticalPathTree({
fn: () =>
timerange(start, end)
.interval('15m')
.rate(rate)
.generator((timestamp) => {
return java.transaction('GET /api').timestamp(timestamp).duration(duration);
}),
});
expect(rootNodes.length).to.be(1);
expect(rootNodes[0].countInclusive).to.eql(duration * rate * 1000);
expect(rootNodes[0].countExclusive).to.eql(duration * rate * 1000);
expect(rootNodes[0].metadata).to.eql({
'processor.event': 'transaction',
'transaction.type': 'request',
'service.name': 'java',
'agent.name': 'java',
'transaction.name': 'GET /api',
});
});
it('builds up the correct tree for a complicated trace', async () => {
const java = apm
.service({ name: 'java', environment: 'production', agentName: 'java' })
.instance('java');
const rate = 10;
const { rootNodes } = await fetchAndBuildCriticalPathTree({
fn: () =>
timerange(start, end)
.interval('15m')
.rate(rate)
.generator((timestamp) => {
return java
.transaction('GET /api')
.timestamp(timestamp)
.duration(1000)
.children(
java
.span('GET /_search', 'db', 'elasticsearch')
.timestamp(timestamp)
.duration(400),
java
.span('get index stats', 'custom')
.timestamp(timestamp)
.duration(500)
.children(
java
.span('GET /*/_stats', 'db', 'elasticsearch')
.timestamp(timestamp + 50)
.duration(450)
)
);
}),
});
expect(rootNodes.length).to.be(1);
expect(rootNodes[0].countInclusive).to.eql(1000 * rate * 1000);
expect(rootNodes[0].children.length).to.eql(1);
expect(formatTree(rootNodes)).to.eql([
{
name: 'GET /api',
value: 500 * 1000 * rate,
children: [
{
name: 'get index stats',
value: 50 * 1000 * rate,
children: [{ name: 'GET /*/_stats', value: 450 * 1000 * rate, children: [] }],
},
],
},
]);
});
it('slices traces and merges root nodes if service name and transaction name are set', async () => {
// this test also fails when hashCode() is used in the scripted metric aggregation,
// due to collisions.
const upstreamA = apm
.service({ name: 'upstreamA', environment: 'production', agentName: 'java' })
.instance('upstreamA');
const upstreamB = apm
.service({ name: 'upstreamB', environment: 'production', agentName: 'java' })
.instance('upstreamB');
const downstream = apm
.service({ name: 'downstream', environment: 'production', agentName: 'java' })
.instance('downstream');
const rate = 10;
function generateTrace() {
return timerange(start, end)
.interval('15m')
.rate(rate)
.generator((timestamp) => {
return [
upstreamA
.transaction('GET /upstreamA')
.timestamp(timestamp)
.duration(500)
.children(
upstreamA
.span('GET /downstream', 'external', 'http')
.timestamp(timestamp)
.duration(500)
.children(
downstream
.transaction('downstream')
.timestamp(timestamp + 50)
.duration(400)
.children(
downstream
.span('from upstreamA', 'custom')
.timestamp(timestamp + 100)
.duration(300)
)
)
),
upstreamB
.transaction('GET /upstreamB')
.timestamp(timestamp)
.duration(500)
.children(
upstreamB
.span('GET /downstream', 'external', 'http')
.timestamp(timestamp)
.duration(500)
.children(
downstream
.transaction('downstream')
.timestamp(timestamp + 50)
.duration(400)
.children(
downstream
.span('from upstreamB', 'custom')
.timestamp(timestamp + 100)
.duration(300)
)
)
),
];
});
}
const { rootNodes: unfilteredRootNodes } = await fetchAndBuildCriticalPathTree({
fn: () => generateTrace(),
});
await apmSynthtraceEsClient.clean();
const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({
fn: () => generateTrace(),
serviceName: 'downstream',
transactionName: 'downstream',
});
expect(formatTree(unfilteredRootNodes)).eql([
{
name: 'GET /upstreamA',
value: 0,
children: [
{
name: 'GET /downstream',
value: 100 * 1000 * rate,
children: [
{
name: 'downstream',
value: 100 * 1000 * rate,
children: [
{
name: 'from upstreamA',
value: 300 * 1000 * rate,
children: [],
},
],
},
],
},
],
},
{
name: 'GET /upstreamB',
value: 0,
children: [
{
name: 'GET /downstream',
value: 100 * 1000 * rate,
children: [
{
name: 'downstream',
value: 100 * 1000 * rate,
children: [
{
name: 'from upstreamB',
value: 300 * 1000 * rate,
children: [],
},
],
},
],
},
],
},
]);
expect(formatTree(filteredRootNodes)).eql([
{
name: 'downstream',
value: 2 * 100 * 1000 * rate,
children: [
{
name: 'from upstreamA',
value: 300 * 1000 * rate,
children: [],
},
{
name: 'from upstreamB',
value: 300 * 1000 * rate,
children: [],
},
],
},
]);
});
it('calculates the critical path for a specific transaction if its not part of the critical path of the entire trace', async () => {
const upstream = apm
.service({ name: 'upstream', environment: 'production', agentName: 'java' })
.instance('upstream');
const downstreamA = apm
.service({ name: 'downstreamA', environment: 'production', agentName: 'java' })
.instance('downstreamB');
const downstreamB = apm
.service({ name: 'downstreamB', environment: 'production', agentName: 'java' })
.instance('downstreamB');
const rate = 10;
function generateTrace() {
return timerange(start, end)
.interval('15m')
.rate(rate)
.generator((timestamp) => {
return [
upstream
.transaction('GET /upstream')
.timestamp(timestamp)
.duration(500)
.children(
upstream
.span('GET /downstreamA', 'external', 'http')
.timestamp(timestamp)
.duration(500)
.children(
downstreamA
.transaction('downstreamA')
.timestamp(timestamp + 50)
.duration(400)
),
upstream
.span('GET /downstreamB', 'external', 'http')
.timestamp(timestamp)
.duration(400)
.children(
downstreamB
.transaction('downstreamB')
.timestamp(timestamp + 50)
.duration(400)
)
),
];
});
}
const { rootNodes: unfilteredRootNodes } = await fetchAndBuildCriticalPathTree({
fn: () => generateTrace(),
});
expect(formatTree(unfilteredRootNodes)[0].children[0].children).to.eql([
{
name: 'downstreamA',
value: 400 * rate * 1000,
children: [],
},
]);
await apmSynthtraceEsClient.clean();
const { rootNodes: filteredRootNodes } = await fetchAndBuildCriticalPathTree({
fn: () => generateTrace(),
serviceName: 'downstreamB',
transactionName: 'downstreamB',
});
expect(formatTree(filteredRootNodes)).to.eql([
{
name: 'downstreamB',
value: 400 * rate * 1000,
children: [],
},
]);
});
});
}

View file

@ -10,7 +10,6 @@ import { DeploymentAgnosticFtrProviderContext } from '../../../../ftr_provider_c
export default function ({ loadTestFile }: DeploymentAgnosticFtrProviderContext) {
describe('Traces', () => {
loadTestFile(require.resolve('./large_trace/large_trace.spec.ts'));
loadTestFile(require.resolve('./critical_path.spec.ts'));
loadTestFile(require.resolve('./find_traces.spec.ts'));
loadTestFile(require.resolve('./span_details.spec.ts'));
loadTestFile(require.resolve('./top_traces.spec.ts'));

View file

@ -159,16 +159,6 @@ describe.skip('Serverless', () => {
'have.class',
'euiSideNavItemButton-isSelected'
);
cy.visitKibana('/app/apm/traces/explorer/critical_path');
cy.getByTestSubj('nav-item-id-observability_project_nav.apm').should(
'have.class',
'euiSideNavItemButton-isOpen'
);
cy.getByTestSubj('nav-item-id-observability_project_nav.apm.apm:traces').should(
'have.class',
'euiSideNavItemButton-isSelected'
);
});
it('sets AIOps nav item as active', () => {