[APM] Add waterfall to dependency operations (#143257)

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
Dario Gieselaar 2022-10-27 19:09:54 +02:00 committed by GitHub
parent d583ecc1d1
commit f648ef35da
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 508 additions and 188 deletions

View file

@ -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}
/>
}
/>
);
}

View file

@ -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>

View file

@ -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>

View file

@ -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',
});
});
});

View file

@ -0,0 +1,49 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the Elastic License
* 2.0; 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() },
});
}
}

View file

@ -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, {

View file

@ -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>

View file

@ -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
/>

View file

@ -53,7 +53,8 @@ export function MaybeViewTraceLink({
query: { comparisonEnabled, offset },
} = useAnyOfApmParams(
'/services/{serviceName}/transactions/view',
'/traces/explorer'
'/traces/explorer',
'/dependencies/operation'
);
const latencyAggregationType =

View file

@ -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;

View file

@ -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}"`;

View file

@ -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': {

View file

@ -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 }}
/>
);
}

View file

@ -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 {

View file

@ -31,6 +31,11 @@ describe('RedirectWithOffset', () => {
.spyOn(useApmPluginContextExports, 'useApmPluginContext')
.mockReturnValue({
core: {
http: {
basePath: {
prepend: () => {},
},
},
uiSettings: {
get: () => defaultSetting,
},

View file

@ -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]
);
}

View file

@ -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(

View file

@ -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,
};

View file

@ -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é",

View file

@ -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": "失敗したスパン",

View file

@ -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": "失败的跨度",

View file

@ -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({