mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Add waterfall to dependency operations (#143257)
Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
d583ecc1d1
commit
f648ef35da
23 changed files with 508 additions and 188 deletions
|
@ -23,6 +23,7 @@ import { ITableColumn, ManagedTable } from '../../../shared/managed_table';
|
|||
import { getComparisonEnabled } from '../../../shared/time_comparison/get_comparison_enabled';
|
||||
import { TruncateWithTooltip } from '../../../shared/truncate_with_tooltip';
|
||||
import { DependencyOperationDetailLink } from '../../dependency_operation_detail_view/dependency_operation_detail_link';
|
||||
import { TransactionTab } from '../../transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
|
||||
interface OperationStatisticsItem extends SpanMetricGroup {
|
||||
spanName: string;
|
||||
|
@ -35,7 +36,13 @@ function OperationLink({ spanName }: { spanName: string }) {
|
|||
<TruncateWithTooltip
|
||||
data-test-subj="apmOperationsListAppLink"
|
||||
text={spanName}
|
||||
content={<DependencyOperationDetailLink {...query} spanName={spanName} />}
|
||||
content={
|
||||
<DependencyOperationDetailLink
|
||||
{...query}
|
||||
spanName={spanName}
|
||||
detailTab={TransactionTab.timeline}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -9,22 +9,28 @@ import {
|
|||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLink,
|
||||
EuiRadio,
|
||||
EuiText,
|
||||
EuiTitle,
|
||||
RIGHT_ALIGNMENT,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ValuesType } from 'utility-types';
|
||||
import { EventOutcome } from '../../../../common/event_outcome';
|
||||
import { asMillisecondDuration } from '../../../../common/utils/formatters';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { FetcherResult, FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { useTheme } from '../../../hooks/use_theme';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { ITableColumn, ManagedTable } from '../../shared/managed_table';
|
||||
import { push } from '../../shared/links/url_helpers';
|
||||
import {
|
||||
ITableColumn,
|
||||
ManagedTable,
|
||||
SortFunction,
|
||||
} from '../../shared/managed_table';
|
||||
import { ServiceLink } from '../../shared/service_link';
|
||||
import { TimestampTooltip } from '../../shared/timestamp_tooltip';
|
||||
|
||||
|
@ -32,15 +38,23 @@ type DependencySpan = ValuesType<
|
|||
APIReturnType<'GET /internal/apm/dependencies/operations/spans'>['spans']
|
||||
>;
|
||||
|
||||
export function DependencyOperationDetailTraceList() {
|
||||
export function DependencyOperationDetailTraceList({
|
||||
spanFetch,
|
||||
sortFn,
|
||||
}: {
|
||||
spanFetch: FetcherResult<
|
||||
APIReturnType<'GET /internal/apm/dependencies/operations/spans'>
|
||||
>;
|
||||
sortFn: SortFunction<DependencySpan>;
|
||||
}) {
|
||||
const router = useApmRouter();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const theme = useTheme();
|
||||
|
||||
const {
|
||||
query: {
|
||||
dependencyName,
|
||||
spanName,
|
||||
comparisonEnabled,
|
||||
environment,
|
||||
offset,
|
||||
|
@ -49,8 +63,11 @@ export function DependencyOperationDetailTraceList() {
|
|||
refreshInterval,
|
||||
refreshPaused,
|
||||
kuery,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
sortField = '@timestamp',
|
||||
sortDirection = 'desc',
|
||||
pageSize = 10,
|
||||
page = 1,
|
||||
spanId,
|
||||
},
|
||||
} = useApmParams('/dependencies/operation');
|
||||
|
||||
|
@ -99,9 +116,24 @@ export function DependencyOperationDetailTraceList() {
|
|||
return href;
|
||||
}
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const columns: Array<ITableColumn<DependencySpan>> = [
|
||||
{
|
||||
name: '',
|
||||
field: 'spanId',
|
||||
render: (_, { spanId: itemSpanId }) => {
|
||||
return (
|
||||
<EuiRadio
|
||||
id={itemSpanId}
|
||||
onChange={(value) => {
|
||||
push(history, {
|
||||
query: { spanId: value ? itemSpanId : '' },
|
||||
});
|
||||
}}
|
||||
checked={itemSpanId === spanId}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.dependencyOperationDetailTraceListOutcomeColumn',
|
||||
|
@ -121,38 +153,6 @@ export function DependencyOperationDetailTraceList() {
|
|||
return <EuiBadge color={color}>{outcome}</EuiBadge>;
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.dependencyOperationDetailTraceListTraceIdColumn',
|
||||
{ defaultMessage: 'Trace' }
|
||||
),
|
||||
field: 'traceId',
|
||||
truncateText: true,
|
||||
render: (
|
||||
_,
|
||||
{
|
||||
serviceName,
|
||||
traceId,
|
||||
transactionId,
|
||||
transactionName,
|
||||
transactionType,
|
||||
}
|
||||
) => {
|
||||
const href = getTraceLink({
|
||||
serviceName,
|
||||
traceId,
|
||||
transactionId,
|
||||
transactionType,
|
||||
transactionName,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiLink href={href} style={{ whiteSpace: 'nowrap' }}>
|
||||
{traceId.substr(0, 6)}
|
||||
</EuiLink>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
name: i18n.translate(
|
||||
'xpack.apm.dependencyOperationDetailTraceListServiceNameColumn',
|
||||
|
@ -190,6 +190,7 @@ export function DependencyOperationDetailTraceList() {
|
|||
),
|
||||
field: 'transactionName',
|
||||
truncateText: true,
|
||||
width: '60%',
|
||||
render: (
|
||||
_,
|
||||
{
|
||||
|
@ -239,35 +240,6 @@ export function DependencyOperationDetailTraceList() {
|
|||
},
|
||||
];
|
||||
|
||||
const { data = { spans: [] }, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/dependencies/operations/spans', {
|
||||
params: {
|
||||
query: {
|
||||
dependencyName,
|
||||
spanName,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
dependencyName,
|
||||
spanName,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
]
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
|
@ -281,15 +253,18 @@ export function DependencyOperationDetailTraceList() {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<ManagedTable
|
||||
tableLayout="auto"
|
||||
columns={columns}
|
||||
items={data?.spans}
|
||||
initialSortField="@timestamp"
|
||||
initialSortDirection="desc"
|
||||
initialPageSize={10}
|
||||
items={spanFetch.data?.spans || []}
|
||||
initialSortField={sortField}
|
||||
initialSortDirection={sortDirection}
|
||||
initialPageSize={pageSize}
|
||||
initialPageIndex={page}
|
||||
isLoading={
|
||||
status === FETCH_STATUS.LOADING ||
|
||||
status === FETCH_STATUS.NOT_INITIATED
|
||||
spanFetch.status === FETCH_STATUS.LOADING ||
|
||||
spanFetch.status === FETCH_STATUS.NOT_INITIATED
|
||||
}
|
||||
sortFn={sortFn}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -6,25 +6,141 @@
|
|||
*/
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { omit, orderBy } from 'lodash';
|
||||
import React, { useEffect, useMemo, useRef } from 'react';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import type { DependencySpan } from '../../../../server/routes/dependencies/get_top_dependency_spans';
|
||||
import { ChartPointerEventContextProvider } from '../../../context/chart_pointer_event/chart_pointer_event_context';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { useDependencyDetailOperationsBreadcrumb } from '../../../hooks/use_dependency_detail_operations_breadcrumb';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { DependencyMetricCharts } from '../../shared/dependency_metric_charts';
|
||||
import { DetailViewHeader } from '../../shared/detail_view_header';
|
||||
import { DependencyOperationDistributionChart } from './dependendecy_operation_distribution_chart';
|
||||
import { ResettingHeightRetainer } from '../../shared/height_retainer/resetting_height_container';
|
||||
import { push, replace } from '../../shared/links/url_helpers';
|
||||
import { SortFunction } from '../../shared/managed_table';
|
||||
import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetcher';
|
||||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { DependencyOperationDetailTraceList } from './dependency_operation_detail_trace_list';
|
||||
import { DependencyOperationDistributionChart } from './dependency_operation_distribution_chart';
|
||||
import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample';
|
||||
|
||||
export function DependencyOperationDetailView() {
|
||||
const router = useApmRouter();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const {
|
||||
query: { spanName, ...query },
|
||||
query,
|
||||
query: {
|
||||
spanName,
|
||||
dependencyName,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
kuery,
|
||||
environment,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
spanId,
|
||||
waterfallItemId,
|
||||
detailTab,
|
||||
sortField = '@timestamp',
|
||||
sortDirection = 'desc',
|
||||
},
|
||||
} = useApmParams('/dependencies/operation');
|
||||
|
||||
useDependencyDetailOperationsBreadcrumb();
|
||||
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const queryWithoutSpanName = omit(query, 'spanName');
|
||||
|
||||
const spanFetch = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/dependencies/operations/spans', {
|
||||
params: {
|
||||
query: {
|
||||
dependencyName,
|
||||
spanName,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
},
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
dependencyName,
|
||||
spanName,
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
sampleRangeFrom,
|
||||
sampleRangeTo,
|
||||
]
|
||||
);
|
||||
|
||||
const getSortedSamples: SortFunction<DependencySpan> = (
|
||||
items,
|
||||
localSortField,
|
||||
localSortDirection
|
||||
) => {
|
||||
return orderBy(items, localSortField, localSortDirection);
|
||||
};
|
||||
|
||||
const samples = useMemo(() => {
|
||||
return (
|
||||
getSortedSamples(
|
||||
spanFetch.data?.spans ?? [],
|
||||
sortField,
|
||||
sortDirection
|
||||
).map((span) => ({
|
||||
spanId: span.spanId,
|
||||
traceId: span.traceId,
|
||||
transactionId: span.transactionId,
|
||||
})) || []
|
||||
);
|
||||
}, [spanFetch.data?.spans, sortField, sortDirection]);
|
||||
|
||||
const selectedSample = useMemo(() => {
|
||||
return samples.find((sample) => sample.spanId === spanId);
|
||||
}, [samples, spanId]);
|
||||
|
||||
const waterfallFetch = useWaterfallFetcher({
|
||||
traceId: selectedSample?.traceId,
|
||||
transactionId: selectedSample?.transactionId,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const queryRef = useRef(query);
|
||||
|
||||
queryRef.current = query;
|
||||
|
||||
useEffect(() => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
history,
|
||||
page: queryRef.current.page ?? 0,
|
||||
pageSize: queryRef.current.pageSize ?? 10,
|
||||
replace,
|
||||
samples,
|
||||
spanFetchStatus: spanFetch.status,
|
||||
spanId,
|
||||
});
|
||||
}, [samples, spanId, history, queryRef, router, spanFetch.status]);
|
||||
|
||||
const isWaterfallLoading =
|
||||
spanFetch.status === FETCH_STATUS.NOT_INITIATED ||
|
||||
(spanFetch.status === FETCH_STATUS.LOADING && samples.length === 0) ||
|
||||
waterfallFetch.status === FETCH_STATUS.LOADING ||
|
||||
!waterfallFetch.waterfall.entryWaterfallTransaction;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column">
|
||||
<EuiFlexItem>
|
||||
|
@ -33,7 +149,9 @@ export function DependencyOperationDetailView() {
|
|||
'xpack.apm.dependecyOperationDetailView.header.backLinkLabel',
|
||||
{ defaultMessage: 'All operations' }
|
||||
)}
|
||||
backHref={router.link('/dependencies/operations', { query })}
|
||||
backHref={router.link('/dependencies/operations', {
|
||||
query: queryWithoutSpanName,
|
||||
})}
|
||||
title={spanName}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
|
@ -50,7 +168,39 @@ export function DependencyOperationDetailView() {
|
|||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder>
|
||||
<DependencyOperationDetailTraceList />
|
||||
<DependencyOperationDetailTraceList
|
||||
spanFetch={spanFetch}
|
||||
sortFn={getSortedSamples}
|
||||
/>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiPanel hasBorder>
|
||||
<ResettingHeightRetainer reset={!isWaterfallLoading}>
|
||||
<WaterfallWithSummary
|
||||
environment={environment}
|
||||
waterfallFetchResult={waterfallFetch}
|
||||
traceSamples={samples}
|
||||
traceSamplesFetchStatus={spanFetch.status}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, { query: { spanId: sample.spanId } });
|
||||
}}
|
||||
onTabClick={(tab) => {
|
||||
push(history, {
|
||||
query: {
|
||||
detailTab: tab,
|
||||
},
|
||||
});
|
||||
}}
|
||||
serviceName={
|
||||
waterfallFetch.waterfall.entryWaterfallTransaction?.doc.service
|
||||
.name
|
||||
}
|
||||
waterfallItemId={waterfallItemId}
|
||||
detailTab={detailTab}
|
||||
selectedSample={selectedSample || null}
|
||||
/>
|
||||
</ResettingHeightRetainer>
|
||||
</EuiPanel>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
|
|
|
@ -0,0 +1,108 @@
|
|||
/*
|
||||
* 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 { range } from 'lodash';
|
||||
import { maybeRedirectToAvailableSpanSample } from './maybe_redirect_to_available_span_sample';
|
||||
import { replace as urlHelpersReplace } from '../../shared/links/url_helpers';
|
||||
import { History } from 'history';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
|
||||
describe('maybeRedirectToAvailableSpanSample', () => {
|
||||
const samples: Array<{
|
||||
spanId: string;
|
||||
traceId: string;
|
||||
transactionId: string;
|
||||
}> = range(11).map((_, index) => ({
|
||||
spanId: (index + 1).toString(),
|
||||
traceId: '',
|
||||
transactionId: '',
|
||||
}));
|
||||
|
||||
let defaultParams: Omit<
|
||||
Parameters<typeof maybeRedirectToAvailableSpanSample>[0],
|
||||
'replace'
|
||||
> & { replace: jest.MockedFunction<typeof urlHelpersReplace> };
|
||||
|
||||
beforeEach(() => {
|
||||
defaultParams = {
|
||||
samples,
|
||||
page: 0,
|
||||
pageSize: 10,
|
||||
history: {
|
||||
location: {
|
||||
search: '',
|
||||
},
|
||||
} as History,
|
||||
spanFetchStatus: FETCH_STATUS.SUCCESS,
|
||||
replace: jest.fn(),
|
||||
};
|
||||
});
|
||||
|
||||
it('does not redirect while loading', () => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
...defaultParams,
|
||||
spanId: undefined,
|
||||
spanFetchStatus: FETCH_STATUS.LOADING,
|
||||
});
|
||||
expect(defaultParams.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirects to the first available span if no span is selected', () => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
...defaultParams,
|
||||
spanId: undefined,
|
||||
page: 1,
|
||||
spanFetchStatus: FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
expect(defaultParams.replace).toHaveBeenCalled();
|
||||
|
||||
expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
|
||||
spanId: samples[0].spanId,
|
||||
page: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('redirects to the first available span if the currently selected sample is not found', () => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
...defaultParams,
|
||||
page: 1,
|
||||
spanId: '12',
|
||||
spanFetchStatus: FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
expect(defaultParams.replace).toHaveBeenCalled();
|
||||
|
||||
expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
|
||||
spanId: samples[0].spanId,
|
||||
page: '0',
|
||||
});
|
||||
});
|
||||
|
||||
it('does not redirect if the sample is found', () => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
...defaultParams,
|
||||
page: 0,
|
||||
spanId: '1',
|
||||
spanFetchStatus: FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
expect(defaultParams.replace).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('redirects to the page of the currently selected sample', () => {
|
||||
maybeRedirectToAvailableSpanSample({
|
||||
...defaultParams,
|
||||
page: 0,
|
||||
spanId: '11',
|
||||
spanFetchStatus: FETCH_STATUS.SUCCESS,
|
||||
});
|
||||
|
||||
expect(defaultParams.replace).toHaveBeenCalled();
|
||||
|
||||
expect(defaultParams.replace.mock.calls[0][1].query).toEqual({
|
||||
page: '1',
|
||||
spanId: '11',
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { History } from 'history';
|
||||
import { FETCH_STATUS } from '../../../hooks/use_fetcher';
|
||||
import { replace as urlHelpersReplace } from '../../shared/links/url_helpers';
|
||||
|
||||
export function maybeRedirectToAvailableSpanSample({
|
||||
spanFetchStatus,
|
||||
spanId,
|
||||
pageSize,
|
||||
page,
|
||||
replace,
|
||||
samples,
|
||||
history,
|
||||
}: {
|
||||
spanFetchStatus: FETCH_STATUS;
|
||||
spanId?: string;
|
||||
pageSize: number;
|
||||
page: number;
|
||||
replace: typeof urlHelpersReplace;
|
||||
history: History;
|
||||
samples: Array<{ spanId: string; traceId: string; transactionId: string }>;
|
||||
}) {
|
||||
if (spanFetchStatus !== FETCH_STATUS.SUCCESS) {
|
||||
// we're still loading, don't do anything
|
||||
return;
|
||||
}
|
||||
|
||||
const nextSpanId =
|
||||
samples.find((sample) => sample.spanId === spanId)?.spanId ||
|
||||
samples[0]?.spanId ||
|
||||
'';
|
||||
|
||||
const indexOfNextSample =
|
||||
samples.findIndex((sample) => sample.spanId === nextSpanId) ?? 0;
|
||||
|
||||
const nextPageIndex = Math.floor((indexOfNextSample + 1) / (pageSize ?? 10));
|
||||
|
||||
if (page !== nextPageIndex || (spanId ?? '') !== nextSpanId) {
|
||||
replace(history, {
|
||||
query: { spanId: nextSpanId, page: nextPageIndex.toString() },
|
||||
});
|
||||
}
|
||||
}
|
|
@ -20,10 +20,6 @@ import { useWaterfallFetcher } from '../transaction_details/use_waterfall_fetche
|
|||
import { WaterfallWithSummary } from '../transaction_details/waterfall_with_summary';
|
||||
import { TraceSearchBox } from './trace_search_box';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
traceSamples: [],
|
||||
};
|
||||
|
||||
export function TraceExplorer() {
|
||||
const [query, setQuery] = useState<TraceSearchQuery>({
|
||||
query: '',
|
||||
|
@ -58,11 +54,7 @@ export function TraceExplorer() {
|
|||
rangeTo,
|
||||
});
|
||||
|
||||
const {
|
||||
data = INITIAL_DATA,
|
||||
status,
|
||||
error,
|
||||
} = useFetcher(
|
||||
const { data, status, error } = useFetcher(
|
||||
(callApmApi) => {
|
||||
return callApmApi('GET /internal/apm/traces/find', {
|
||||
params: {
|
||||
|
@ -80,7 +72,7 @@ export function TraceExplorer() {
|
|||
);
|
||||
|
||||
useEffect(() => {
|
||||
const nextSample = data.traceSamples[0];
|
||||
const nextSample = data?.traceSamples[0];
|
||||
const nextWaterfallItemId = '';
|
||||
history.replace({
|
||||
...history.location,
|
||||
|
@ -141,7 +133,8 @@ export function TraceExplorer() {
|
|||
<EuiFlexItem>
|
||||
<WaterfallWithSummary
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
traceSamplesFetchResult={traceSamplesFetchResult}
|
||||
traceSamples={traceSamplesFetchResult.data?.traceSamples}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
environment={environment}
|
||||
onSampleClick={(sample) => {
|
||||
push(history, {
|
||||
|
|
|
@ -117,7 +117,8 @@ export function TransactionDistribution({
|
|||
waterfallItemId={waterfallItemId}
|
||||
detailTab={detailTab as TransactionTab | undefined}
|
||||
waterfallFetchResult={waterfallFetchResult}
|
||||
traceSamplesFetchResult={traceSamplesFetchResult}
|
||||
traceSamplesFetchStatus={traceSamplesFetchResult.status}
|
||||
traceSamples={traceSamplesFetchResult.data?.traceSamples}
|
||||
/>
|
||||
</div>
|
||||
</HeightRetainer>
|
||||
|
|
|
@ -22,55 +22,67 @@ import { MaybeViewTraceLink } from './maybe_view_trace_link';
|
|||
import { TransactionTab, TransactionTabs } from './transaction_tabs';
|
||||
import { Environment } from '../../../../../common/environment_rt';
|
||||
import { FETCH_STATUS } from '../../../../hooks/use_fetcher';
|
||||
import { TraceSamplesFetchResult } from '../../../../hooks/use_transaction_trace_samples_fetcher';
|
||||
import { WaterfallFetchResult } from '../use_waterfall_fetcher';
|
||||
|
||||
interface Props {
|
||||
interface Props<TSample extends {}> {
|
||||
waterfallFetchResult: WaterfallFetchResult;
|
||||
traceSamplesFetchResult: TraceSamplesFetchResult;
|
||||
traceSamples?: TSample[];
|
||||
traceSamplesFetchStatus: FETCH_STATUS;
|
||||
environment: Environment;
|
||||
onSampleClick: (sample: { transactionId: string; traceId: string }) => void;
|
||||
onTabClick: (tab: string) => void;
|
||||
onSampleClick: (sample: TSample) => void;
|
||||
onTabClick: (tab: TransactionTab) => void;
|
||||
serviceName?: string;
|
||||
waterfallItemId?: string;
|
||||
detailTab?: TransactionTab;
|
||||
selectedSample?: TSample | null;
|
||||
}
|
||||
|
||||
export function WaterfallWithSummary({
|
||||
export function WaterfallWithSummary<TSample extends {}>({
|
||||
waterfallFetchResult,
|
||||
traceSamplesFetchResult,
|
||||
traceSamples,
|
||||
traceSamplesFetchStatus,
|
||||
environment,
|
||||
onSampleClick,
|
||||
onTabClick,
|
||||
serviceName,
|
||||
waterfallItemId,
|
||||
detailTab,
|
||||
}: Props) {
|
||||
selectedSample,
|
||||
}: Props<TSample>) {
|
||||
const [sampleActivePage, setSampleActivePage] = useState(0);
|
||||
|
||||
const isControlled = selectedSample !== undefined;
|
||||
|
||||
const isLoading =
|
||||
waterfallFetchResult.status === FETCH_STATUS.LOADING ||
|
||||
traceSamplesFetchStatus === FETCH_STATUS.LOADING;
|
||||
const isSucceded =
|
||||
waterfallFetchResult.status === FETCH_STATUS.SUCCESS &&
|
||||
traceSamplesFetchStatus === FETCH_STATUS.SUCCESS;
|
||||
|
||||
useEffect(() => {
|
||||
setSampleActivePage(0);
|
||||
}, [traceSamplesFetchResult.data.traceSamples]);
|
||||
if (!isControlled) {
|
||||
setSampleActivePage(0);
|
||||
}
|
||||
}, [traceSamples, isControlled]);
|
||||
|
||||
const goToSample = (index: number) => {
|
||||
setSampleActivePage(index);
|
||||
const sample = traceSamplesFetchResult.data.traceSamples[index];
|
||||
const sample = traceSamples![index];
|
||||
if (!isControlled) {
|
||||
setSampleActivePage(index);
|
||||
}
|
||||
onSampleClick(sample);
|
||||
};
|
||||
|
||||
const { entryWaterfallTransaction } = waterfallFetchResult.waterfall;
|
||||
const isLoading =
|
||||
waterfallFetchResult.status === FETCH_STATUS.LOADING ||
|
||||
traceSamplesFetchResult.status === FETCH_STATUS.LOADING;
|
||||
const isSucceded =
|
||||
waterfallFetchResult.status === FETCH_STATUS.SUCCESS &&
|
||||
traceSamplesFetchResult.status === FETCH_STATUS.SUCCESS;
|
||||
const samplePageIndex = isControlled
|
||||
? selectedSample
|
||||
? traceSamples?.indexOf(selectedSample)
|
||||
: 0
|
||||
: sampleActivePage;
|
||||
|
||||
if (
|
||||
!entryWaterfallTransaction &&
|
||||
traceSamplesFetchResult.data.traceSamples.length === 0 &&
|
||||
isSucceded
|
||||
) {
|
||||
const { entryWaterfallTransaction } = waterfallFetchResult.waterfall;
|
||||
|
||||
if (!entryWaterfallTransaction && traceSamples?.length === 0 && isSucceded) {
|
||||
return (
|
||||
<EuiEmptyPrompt
|
||||
title={
|
||||
|
@ -100,10 +112,10 @@ export function WaterfallWithSummary({
|
|||
</EuiTitle>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{traceSamplesFetchResult.data.traceSamples.length > 0 && (
|
||||
{!!traceSamples?.length && (
|
||||
<EuiPagination
|
||||
pageCount={traceSamplesFetchResult.data.traceSamples.length}
|
||||
activePage={sampleActivePage}
|
||||
pageCount={traceSamples.length}
|
||||
activePage={samplePageIndex}
|
||||
onPageClick={goToSample}
|
||||
compressed
|
||||
/>
|
||||
|
|
|
@ -53,7 +53,8 @@ export function MaybeViewTraceLink({
|
|||
query: { comparisonEnabled, offset },
|
||||
} = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
'/traces/explorer',
|
||||
'/dependencies/operation'
|
||||
);
|
||||
|
||||
const latencyAggregationType =
|
||||
|
|
|
@ -33,7 +33,8 @@ interface Props {
|
|||
export function StickySpanProperties({ span, transaction }: Props) {
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
'/traces/explorer',
|
||||
'/dependencies/operation'
|
||||
);
|
||||
const { environment, comparisonEnabled, offset } = query;
|
||||
|
||||
|
|
|
@ -277,7 +277,8 @@ function RelatedErrors({
|
|||
const theme = useTheme();
|
||||
const { query } = useAnyOfApmParams(
|
||||
'/services/{serviceName}/transactions/view',
|
||||
'/traces/explorer'
|
||||
'/traces/explorer',
|
||||
'/dependencies/operation'
|
||||
);
|
||||
|
||||
let kuery = `${TRACE_ID} : "${item.doc.trace.id}"`;
|
||||
|
|
|
@ -20,6 +20,7 @@ import { DependencyDetailView } from '../../app/dependency_detail_view';
|
|||
import { DependenciesInventory } from '../../app/dependencies_inventory';
|
||||
import { DependencyOperationDetailView } from '../../app/dependency_operation_detail_view';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { TransactionTab } from '../../app/transaction_details/waterfall_with_summary/transaction_tabs';
|
||||
|
||||
export const DependenciesInventoryTitle = i18n.translate(
|
||||
'xpack.apm.views.dependenciesInventory.title',
|
||||
|
@ -73,13 +74,25 @@ export const dependencies = {
|
|||
query: t.intersection([
|
||||
t.type({
|
||||
spanName: t.string,
|
||||
detailTab: t.union([
|
||||
t.literal(TransactionTab.timeline),
|
||||
t.literal(TransactionTab.metadata),
|
||||
t.literal(TransactionTab.logs),
|
||||
]),
|
||||
}),
|
||||
t.partial({
|
||||
spanId: t.string,
|
||||
sampleRangeFrom: toNumberRt,
|
||||
sampleRangeTo: toNumberRt,
|
||||
waterfallItemId: t.string,
|
||||
}),
|
||||
]),
|
||||
}),
|
||||
defaults: {
|
||||
query: {
|
||||
detailTab: TransactionTab.timeline,
|
||||
},
|
||||
},
|
||||
element: <DependencyOperationDetailView />,
|
||||
},
|
||||
'/dependencies/overview': {
|
||||
|
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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, { useRef } from 'react';
|
||||
|
||||
export function ResettingHeightRetainer(
|
||||
props: React.DetailedHTMLProps<
|
||||
React.HTMLAttributes<HTMLDivElement>,
|
||||
HTMLDivElement
|
||||
> & { reset?: boolean }
|
||||
) {
|
||||
const { reset, ...containerProps } = props;
|
||||
const resetRef = useRef(reset);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
const minHeightRef = useRef(0);
|
||||
|
||||
if (resetRef.current !== reset) {
|
||||
minHeightRef.current = reset ? 0 : containerRef.current?.clientHeight ?? 0;
|
||||
|
||||
resetRef.current = reset;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
{...containerProps}
|
||||
ref={containerRef}
|
||||
style={{ minHeight: minHeightRef.current }}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -36,11 +36,7 @@ interface Props<T> {
|
|||
showPerPageOptions?: boolean;
|
||||
noItemsMessage?: React.ReactNode;
|
||||
sortItems?: boolean;
|
||||
sortFn?: (
|
||||
items: T[],
|
||||
sortField: string,
|
||||
sortDirection: 'asc' | 'desc'
|
||||
) => T[];
|
||||
sortFn?: SortFunction<T>;
|
||||
pagination?: boolean;
|
||||
isLoading?: boolean;
|
||||
error?: boolean;
|
||||
|
@ -57,6 +53,12 @@ function defaultSortFn<T extends any>(
|
|||
return orderBy(items, sortField, sortDirection);
|
||||
}
|
||||
|
||||
export type SortFunction<T> = (
|
||||
items: T[],
|
||||
sortField: string,
|
||||
sortDirection: 'asc' | 'desc'
|
||||
) => T[];
|
||||
|
||||
function UnoptimizedManagedTable<T>(props: Props<T>) {
|
||||
const history = useHistory();
|
||||
const {
|
||||
|
|
|
@ -31,6 +31,11 @@ describe('RedirectWithOffset', () => {
|
|||
.spyOn(useApmPluginContextExports, 'useApmPluginContext')
|
||||
.mockReturnValue({
|
||||
core: {
|
||||
http: {
|
||||
basePath: {
|
||||
prepend: () => {},
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
get: () => defaultSetting,
|
||||
},
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import { useRouter } from '@kbn/typed-react-router-config';
|
||||
import { useMemo } from 'react';
|
||||
import type { ApmRouter } from '../components/routing/apm_route_config';
|
||||
import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context';
|
||||
|
||||
|
@ -13,12 +14,13 @@ export function useApmRouter() {
|
|||
const router = useRouter();
|
||||
const { core } = useApmPluginContext();
|
||||
|
||||
const link = (...args: [any]) => {
|
||||
return core.http.basePath.prepend('/app/apm' + router.link(...args));
|
||||
};
|
||||
|
||||
return {
|
||||
...router,
|
||||
link,
|
||||
} as unknown as ApmRouter;
|
||||
return useMemo(
|
||||
() =>
|
||||
({
|
||||
...router,
|
||||
link: (...args: [any]) =>
|
||||
core.http.basePath.prepend('/app/apm' + router.link(...args)),
|
||||
} as unknown as ApmRouter),
|
||||
[core.http.basePath, router]
|
||||
);
|
||||
}
|
||||
|
|
|
@ -12,10 +12,6 @@ import { useApmServiceContext } from '../context/apm_service/use_apm_service_con
|
|||
import { useApmParams } from './use_apm_params';
|
||||
import { useTimeRange } from './use_time_range';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
traceSamples: [],
|
||||
};
|
||||
|
||||
export type TraceSamplesFetchResult = ReturnType<
|
||||
typeof useTransactionTraceSamplesFetcher
|
||||
>;
|
||||
|
@ -41,11 +37,7 @@ export function useTransactionTraceSamplesFetcher({
|
|||
urlParams: { transactionId, traceId, sampleRangeFrom, sampleRangeTo },
|
||||
} = useLegacyUrlParams();
|
||||
|
||||
const {
|
||||
data = INITIAL_DATA,
|
||||
status,
|
||||
error,
|
||||
} = useFetcher(
|
||||
const { data, status, error } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (serviceName && start && end && transactionType && transactionName) {
|
||||
return callApmApi(
|
||||
|
|
|
@ -5,14 +5,14 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import {
|
||||
kqlQuery,
|
||||
rangeQuery,
|
||||
termQuery,
|
||||
termsQuery,
|
||||
} from '@kbn/observability-plugin/server';
|
||||
import { compact, keyBy } from 'lodash';
|
||||
import { ProcessorEvent } from '@kbn/observability-plugin/common';
|
||||
import { keyBy } from 'lodash';
|
||||
import {
|
||||
AGENT_NAME,
|
||||
EVENT_OUTCOME,
|
||||
|
@ -20,6 +20,7 @@ import {
|
|||
SERVICE_NAME,
|
||||
SPAN_DESTINATION_SERVICE_RESOURCE,
|
||||
SPAN_DURATION,
|
||||
SPAN_ID,
|
||||
SPAN_NAME,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
|
@ -29,6 +30,7 @@ import {
|
|||
import { Environment } from '../../../common/environment_rt';
|
||||
import { EventOutcome } from '../../../common/event_outcome';
|
||||
import { environmentQuery } from '../../../common/utils/environment_query';
|
||||
import { maybe } from '../../../common/utils/maybe';
|
||||
import { AgentName } from '../../../typings/es_schemas/ui/fields/agent';
|
||||
import { APMEventClient } from '../../lib/helpers/create_es_client/create_apm_event_client';
|
||||
|
||||
|
@ -36,11 +38,12 @@ const MAX_NUM_SPANS = 1000;
|
|||
|
||||
export interface DependencySpan {
|
||||
'@timestamp': number;
|
||||
spanId: string;
|
||||
spanName: string;
|
||||
serviceName: string;
|
||||
agentName: AgentName;
|
||||
traceId: string;
|
||||
transactionId?: string;
|
||||
transactionId: string;
|
||||
transactionType?: string;
|
||||
transactionName?: string;
|
||||
duration: number;
|
||||
|
@ -84,6 +87,7 @@ export async function getTopDependencySpans({
|
|||
...kqlQuery(kuery),
|
||||
...termQuery(SPAN_DESTINATION_SERVICE_RESOURCE, dependencyName),
|
||||
...termQuery(SPAN_NAME, spanName),
|
||||
{ exists: { field: TRANSACTION_ID } },
|
||||
...((sampleRangeFrom ?? 0) >= 0 && (sampleRangeTo ?? 0) > 0
|
||||
? [
|
||||
{
|
||||
|
@ -100,6 +104,7 @@ export async function getTopDependencySpans({
|
|||
},
|
||||
},
|
||||
_source: [
|
||||
SPAN_ID,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
SPAN_NAME,
|
||||
|
@ -114,7 +119,7 @@ export async function getTopDependencySpans({
|
|||
})
|
||||
).hits.hits.map((hit) => hit._source);
|
||||
|
||||
const transactionIds = compact(spans.map((span) => span.transaction?.id));
|
||||
const transactionIds = spans.map((span) => span.transaction!.id);
|
||||
|
||||
const transactions = (
|
||||
await apmEventClient.search('get_transactions_for_dependency_spans', {
|
||||
|
@ -143,19 +148,18 @@ export async function getTopDependencySpans({
|
|||
);
|
||||
|
||||
return spans.map((span): DependencySpan => {
|
||||
const transaction = span.transaction
|
||||
? transactionsById[span.transaction.id]
|
||||
: undefined;
|
||||
const transaction = maybe(transactionsById[span.transaction!.id]);
|
||||
|
||||
return {
|
||||
'@timestamp': new Date(span['@timestamp']).getTime(),
|
||||
spanId: span.span.id,
|
||||
spanName: span.span.name,
|
||||
serviceName: span.service.name,
|
||||
agentName: span.agent.name,
|
||||
duration: span.span.duration.us,
|
||||
traceId: span.trace.id,
|
||||
outcome: (span.event?.outcome || EventOutcome.unknown) as EventOutcome,
|
||||
transactionId: transaction?.transaction.id,
|
||||
transactionId: span.transaction!.id,
|
||||
transactionType: transaction?.transaction.type,
|
||||
transactionName: transaction?.transaction.name,
|
||||
};
|
||||
|
|
|
@ -7164,7 +7164,6 @@
|
|||
"xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "Résultat",
|
||||
"xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "Service d'origine",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "Horodatage",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTraceIdColumn": "Trace",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "Nom de la transaction",
|
||||
"xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "Tous les intervalles",
|
||||
"xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "Intervalles ayant échoué",
|
||||
|
|
|
@ -7152,7 +7152,6 @@
|
|||
"xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "成果",
|
||||
"xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "発生元サービス",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "タイムスタンプ",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTraceIdColumn": "トレース",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "トランザクション名",
|
||||
"xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "すべてのスパン",
|
||||
"xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "失敗したスパン",
|
||||
|
|
|
@ -7168,7 +7168,6 @@
|
|||
"xpack.apm.dependencyOperationDetailTraceListOutcomeColumn": "结果",
|
||||
"xpack.apm.dependencyOperationDetailTraceListServiceNameColumn": "发起服务",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTimestampColumn": "时间戳",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTraceIdColumn": "跟踪",
|
||||
"xpack.apm.dependencyOperationDetailTraceListTransactionNameColumn": "事务名称",
|
||||
"xpack.apm.dependencyOperationDistributionChart.allSpansLegendLabel": "所有跨度",
|
||||
"xpack.apm.dependencyOperationDistributionChart.failedSpansLegendLabel": "失败的跨度",
|
||||
|
|
|
@ -155,7 +155,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
|
||||
expect(javaSpans.length + goSpans.length).to.eql(spans.length);
|
||||
|
||||
expect(omit(javaSpans[0], 'traceId', 'transactionId')).to.eql({
|
||||
expect(omit(javaSpans[0], 'spanId', 'traceId', 'transactionId')).to.eql({
|
||||
'@timestamp': 1609459200000,
|
||||
agentName: 'java',
|
||||
duration: 100000,
|
||||
|
@ -166,7 +166,7 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
outcome: 'success',
|
||||
});
|
||||
|
||||
expect(omit(goSpans[0], 'traceId', 'transactionId')).to.eql({
|
||||
expect(omit(goSpans[0], 'spanId', 'traceId', 'transactionId')).to.eql({
|
||||
'@timestamp': 1609459200000,
|
||||
agentName: 'go',
|
||||
duration: 50000,
|
||||
|
@ -223,34 +223,6 @@ export default function ApiTest({ getService }: FtrProviderContext) {
|
|||
});
|
||||
});
|
||||
|
||||
describe('when requesting spans without a transaction', () => {
|
||||
it('should return the spans without transaction metadata', async () => {
|
||||
const response = await callApi({
|
||||
dependencyName: 'elasticsearch',
|
||||
spanName: 'without transaction',
|
||||
});
|
||||
|
||||
const { spans } = response.body;
|
||||
|
||||
const spanNames = uniq(spans.map((span) => span.spanName));
|
||||
|
||||
expect(spanNames).to.eql(['without transaction']);
|
||||
|
||||
expect(omit(spans[0], 'traceId')).to.eql({
|
||||
'@timestamp': 1609459200000,
|
||||
agentName: 'java',
|
||||
duration: 200000,
|
||||
serviceName: 'java',
|
||||
spanName: 'without transaction',
|
||||
outcome: 'unknown',
|
||||
});
|
||||
|
||||
expect(spans[0].transactionType).not.to.be.ok();
|
||||
expect(spans[0].transactionId).not.to.be.ok();
|
||||
expect(spans[0].transactionName).not.to.be.ok();
|
||||
});
|
||||
});
|
||||
|
||||
describe('when requesting spans within a specific sample range', () => {
|
||||
it('returns only spans whose duration falls into the requested range', async () => {
|
||||
const response = await callApi({
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue