mirror of
https://github.com/elastic/kibana.git
synced 2025-06-27 18:51:07 -04:00
[Discover][APM] Fix Duration widget & Trace summary for unprocessed OTEL spans (#224697)
## Summary This PR fixes some code paths/requests for calculating Duration histograms and Trace summaries to account for unprocessed OTEL data. It isn't the nicest way of doing things, because deep in APM, there is of course an assumption that data is being dealt with in ECS format. Probably longer term, this should be refactored, but this at least adds in support for unprocessed OTEL for the duration widget.  Related to #221521 ## How to test * Add the following to your `kibana.dev.yml` file: ```yaml discover.experimental.enabledProfiles: - observability-traces-data-source-profile - observability-traces-transaction-document-profile - observability-traces-span-document-profile ``` * Set up a source of unprocessed OTEL data to feed into ES (Open telemetry demo, etc) * Set your space to Observability mode, and go to Discover. * Query for `traces-*` and open any record for the document viewer * The Span Overview waterfall should show the duration histogram that match the document's formatted duration in the Table/JSON tabs, as well as the trace summary information (trace id & span name) --------- Co-authored-by: Milosz Marcinkowski <38698566+miloszmarcinkowski@users.noreply.github.com>
This commit is contained in:
parent
63134aa8eb
commit
1c2995447f
21 changed files with 379 additions and 231 deletions
|
@ -25,6 +25,7 @@ export function Duration({ duration, parent }: DurationProps) {
|
|||
if (!parent) {
|
||||
<EuiText size="xs">{asDuration(duration)}</EuiText>;
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiText size="xs">
|
||||
{asDuration(duration)}
|
||||
|
|
|
@ -8,7 +8,6 @@
|
|||
*/
|
||||
|
||||
import React, { useMemo } from 'react';
|
||||
import { flatten } from 'lodash';
|
||||
|
||||
import type {
|
||||
BrushEndListener,
|
||||
|
@ -69,6 +68,7 @@ interface DurationDistributionChartProps {
|
|||
dataTestSubPrefix?: string;
|
||||
showAxisTitle?: boolean;
|
||||
showLegend?: boolean;
|
||||
isOtelData?: boolean;
|
||||
}
|
||||
|
||||
const getAnnotationsStyle = (color = 'gray'): LineAnnotationStyle => ({
|
||||
|
@ -115,6 +115,7 @@ export function DurationDistributionChart({
|
|||
dataTestSubPrefix,
|
||||
showAxisTitle = true,
|
||||
showLegend = true,
|
||||
isOtelData = false,
|
||||
}: DurationDistributionChartProps) {
|
||||
const chartThemes = useChartThemes();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
@ -136,8 +137,8 @@ export function DurationDistributionChart({
|
|||
);
|
||||
|
||||
// This will create y axis ticks for 1, 10, 100, 1000 ...
|
||||
const { yTicks, yAxisMaxDomain, yAxisDomain } = useMemo(() => {
|
||||
const yMax = Math.max(...flatten(data.map((d) => d.histogram)).map((d) => d.doc_count)) ?? 0;
|
||||
const { yTicks, yAxisMaxDomain, yAxisDomain, formattedData } = useMemo(() => {
|
||||
const yMax = Math.max(...data.flatMap((d) => d.histogram).map((d) => d.doc_count)) ?? 0;
|
||||
const computedYTicks = Math.max(1, Math.ceil(Math.log10(yMax)));
|
||||
const computedMaxDomain = Math.pow(10, computedYTicks);
|
||||
|
||||
|
@ -148,8 +149,14 @@ export function DurationDistributionChart({
|
|||
min: Y_AXIS_MIN_DOMAIN,
|
||||
max: computedMaxDomain,
|
||||
},
|
||||
formattedData: isOtelData
|
||||
? data.map((d) => ({
|
||||
...d,
|
||||
histogram: d.histogram.map((hist) => ({ ...hist, key: hist.key * 0.001 })),
|
||||
}))
|
||||
: data,
|
||||
};
|
||||
}, [data]);
|
||||
}, [isOtelData, data]);
|
||||
|
||||
const selectionAnnotation = useMemo(() => {
|
||||
return selection !== undefined
|
||||
|
@ -169,11 +176,11 @@ export function DurationDistributionChart({
|
|||
|
||||
const chartData = useMemo(
|
||||
() =>
|
||||
data.map((d) => ({
|
||||
formattedData.map((d) => ({
|
||||
...d,
|
||||
histogram: replaceHistogramZerosWithMinimumDomainValue(d.histogram),
|
||||
})),
|
||||
[data]
|
||||
[formattedData]
|
||||
);
|
||||
|
||||
return (
|
||||
|
|
|
@ -33,6 +33,7 @@ export const SPAN_SUBTYPE_FIELD = 'span.subtype';
|
|||
export const SPAN_DESTINATION_SERVICE_RESOURCE_FIELD = 'span.destination.service.resource';
|
||||
export const PROCESSOR_EVENT_FIELD = 'processor.event';
|
||||
export const OTEL_SPAN_KIND = 'kind';
|
||||
export const OTEL_DURATION = 'duration';
|
||||
|
||||
export const LOG_FILE_PATH_FIELD = 'log.file.path';
|
||||
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
|
||||
|
|
|
@ -126,6 +126,8 @@ export interface SpanDocumentOverview
|
|||
UserAgentFields {
|
||||
'transaction.id'?: string;
|
||||
'transaction.name'?: string;
|
||||
duration?: number;
|
||||
kind?: string;
|
||||
}
|
||||
|
||||
export interface TraceFields {
|
||||
|
|
|
@ -34,6 +34,8 @@ const fields: Array<keyof SpanDocumentOverview> = [
|
|||
fieldConstants.USER_AGENT_NAME_FIELD,
|
||||
fieldConstants.USER_AGENT_VERSION_FIELD,
|
||||
fieldConstants.PROCESSOR_EVENT_FIELD,
|
||||
fieldConstants.OTEL_DURATION,
|
||||
fieldConstants.OTEL_SPAN_KIND,
|
||||
];
|
||||
|
||||
export function getSpanDocumentOverview(
|
||||
|
|
|
@ -90,7 +90,7 @@ describe('spanDocumentProfileProvider', () => {
|
|||
).toEqual(RESOLUTION_MISMATCH);
|
||||
});
|
||||
|
||||
it('matches records with the correct data stream type and any OTEL `kind` field', () => {
|
||||
it('matches records with the correct data stream type and any OTEL `kind` field (unprocessed spans)', () => {
|
||||
expect(
|
||||
spanDocumentProfileProvider.resolve({
|
||||
rootContext: getRootContext({ profileId }),
|
||||
|
@ -103,7 +103,7 @@ describe('spanDocumentProfileProvider', () => {
|
|||
).toEqual(RESOLUTION_MATCH);
|
||||
});
|
||||
|
||||
it('defaults to matching records with the correct data stream type but no processor event field', () => {
|
||||
it('defaults to matching records with the correct data stream type but no processor event field (unprocessed spans)', () => {
|
||||
expect(
|
||||
spanDocumentProfileProvider.resolve({
|
||||
rootContext: getRootContext({ profileId }),
|
||||
|
|
|
@ -8,12 +8,7 @@
|
|||
*/
|
||||
|
||||
import type { DataTableRecord } from '@kbn/discover-utils';
|
||||
import {
|
||||
DATASTREAM_TYPE_FIELD,
|
||||
getFieldValue,
|
||||
OTEL_SPAN_KIND,
|
||||
PROCESSOR_EVENT_FIELD,
|
||||
} from '@kbn/discover-utils';
|
||||
import { DATASTREAM_TYPE_FIELD, getFieldValue, PROCESSOR_EVENT_FIELD } from '@kbn/discover-utils';
|
||||
import { TRACES_PRODUCT_FEATURE_ID } from '../../../../../../common/constants';
|
||||
import type { DocumentProfileProvider } from '../../../../profiles';
|
||||
import { DocumentType, SolutionType } from '../../../../profiles';
|
||||
|
@ -70,10 +65,9 @@ const getIsSpanRecord = ({ record }: { record: DataTableRecord }) => {
|
|||
const isSpanDocument = (record: DataTableRecord) => {
|
||||
const dataStreamType = getFieldValue(record, DATASTREAM_TYPE_FIELD);
|
||||
const processorEvent = getFieldValue(record, PROCESSOR_EVENT_FIELD);
|
||||
const spanKind = getFieldValue(record, OTEL_SPAN_KIND);
|
||||
|
||||
const isApmSpan = processorEvent === 'span';
|
||||
const isOtelSpan = spanKind != null || processorEvent == null;
|
||||
const isOtelSpan = processorEvent == null;
|
||||
|
||||
return dataStreamType === 'traces' && (isApmSpan || isOtelSpan);
|
||||
};
|
||||
|
|
|
@ -0,0 +1,185 @@
|
|||
/*
|
||||
* 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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import createContainer from 'constate';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
OTEL_DURATION,
|
||||
PARENT_ID_FIELD,
|
||||
PROCESSOR_EVENT_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
SPAN_NAME_FIELD,
|
||||
TRACE_ID_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
TRANSACTION_ID_FIELD,
|
||||
TRANSACTION_NAME_FIELD,
|
||||
} from '@kbn/discover-utils';
|
||||
import { getUnifiedDocViewerServices } from '../../../../../../plugin';
|
||||
|
||||
interface UseTransactionPrams {
|
||||
traceId?: string;
|
||||
transactionId?: string;
|
||||
indexPattern: string;
|
||||
}
|
||||
|
||||
interface GetTransactionParams {
|
||||
traceId: string;
|
||||
transactionId?: string;
|
||||
indexPattern: string;
|
||||
data: DataPublicPluginStart;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
async function getRootSpanData({
|
||||
traceId,
|
||||
transactionId,
|
||||
indexPattern,
|
||||
data,
|
||||
signal,
|
||||
}: GetTransactionParams) {
|
||||
return lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: {
|
||||
index: indexPattern,
|
||||
size: 1,
|
||||
body: {
|
||||
timeout: '20s',
|
||||
fields: [
|
||||
TRANSACTION_NAME_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
SPAN_NAME_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
OTEL_DURATION,
|
||||
],
|
||||
query: {
|
||||
bool: transactionId
|
||||
? {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
[TRANSACTION_ID_FIELD]: transactionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
[PROCESSOR_EVENT_FIELD]: 'transaction',
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
[TRACE_ID_FIELD]: traceId,
|
||||
},
|
||||
},
|
||||
],
|
||||
must_not: [
|
||||
{ exists: { field: PARENT_ID_FIELD } },
|
||||
{ exists: { field: 'parent_span_id' } },
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ abortSignal: signal }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export interface Trace {
|
||||
name: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const useRootSpan = ({ traceId, transactionId, indexPattern }: UseTransactionPrams) => {
|
||||
const { data, core } = getUnifiedDocViewerServices();
|
||||
const [trace, setTrace] = useState<Trace | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!traceId) {
|
||||
setTrace(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await getRootSpanData({
|
||||
traceId,
|
||||
transactionId,
|
||||
indexPattern,
|
||||
data,
|
||||
signal,
|
||||
});
|
||||
|
||||
const fields = result.rawResponse.hits.hits[0]?.fields;
|
||||
const name = fields?.[TRANSACTION_NAME_FIELD] || fields?.[SPAN_NAME_FIELD];
|
||||
const duration = resolveDuration(fields);
|
||||
|
||||
if (name && duration) {
|
||||
setTrace({
|
||||
name,
|
||||
duration,
|
||||
});
|
||||
}
|
||||
} catch (err) {
|
||||
if (!signal.aborted) {
|
||||
const error = err as Error;
|
||||
core.notifications.toasts.addDanger({
|
||||
title: i18n.translate('unifiedDocViewer.docViewerSpanOverview.useTrace.error', {
|
||||
defaultMessage: 'An error occurred while fetching the trace',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
setTrace(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return function onUnmount() {
|
||||
controller.abort();
|
||||
};
|
||||
}, [core.notifications.toasts, data, indexPattern, traceId, transactionId]);
|
||||
|
||||
return { loading, trace };
|
||||
};
|
||||
|
||||
export const [RootSpanProvider, useRootSpanContext] = createContainer(useRootSpan);
|
||||
|
||||
function resolveDuration(fields?: Record<string, any>): number | null {
|
||||
const duration = fields?.[TRANSACTION_DURATION_FIELD];
|
||||
|
||||
if (duration) {
|
||||
return duration;
|
||||
}
|
||||
|
||||
const otelDuration = fields?.[OTEL_DURATION];
|
||||
|
||||
if (otelDuration) {
|
||||
return otelDuration * 0.001;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
|
@ -11,7 +11,7 @@ import React from 'react';
|
|||
import { renderHook, waitFor } from '@testing-library/react';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { getUnifiedDocViewerServices } from '../../../../../../plugin';
|
||||
import { TransactionProvider, useTransactionContext } from '.';
|
||||
import { RootSpanProvider, useRootSpanContext } from '.';
|
||||
import { TRANSACTION_DURATION_FIELD, TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
|
||||
|
||||
jest.mock('../../../../../../plugin', () => ({
|
||||
|
@ -50,24 +50,24 @@ beforeEach(() => {
|
|||
lastValueFromMock.mockReset();
|
||||
});
|
||||
|
||||
describe('useTransaction hook', () => {
|
||||
describe('useRootSpan hook', () => {
|
||||
const wrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<TransactionProvider transactionId="test-transaction" indexPattern="test-index">
|
||||
<RootSpanProvider traceId="test-trace" transactionId="transaction-id" indexPattern="test-index">
|
||||
{children}
|
||||
</TransactionProvider>
|
||||
</RootSpanProvider>
|
||||
);
|
||||
|
||||
it('should start with loading true and transaction as null', async () => {
|
||||
it('should start with loading true and trace as null', async () => {
|
||||
lastValueFromMock.mockResolvedValue({});
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
const { result } = renderHook(() => useRootSpanContext(), { wrapper });
|
||||
|
||||
expect(result.current.loading).toBe(true);
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should update transaction when data is fetched successfully', async () => {
|
||||
const transactionName = 'Test Transaction';
|
||||
it('should update trace when data is fetched successfully', async () => {
|
||||
const transactionName = 'Test Trace';
|
||||
const transactionDuration = 1;
|
||||
lastValueFromMock.mockResolvedValue({
|
||||
rawResponse: {
|
||||
|
@ -84,50 +84,84 @@ describe('useTransaction hook', () => {
|
|||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
const { result } = renderHook(() => useRootSpanContext(), { wrapper });
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction?.name).toBe(transactionName);
|
||||
expect(result.current.transaction?.duration).toBe(transactionDuration);
|
||||
expect(result.current.trace?.name).toBe(transactionName);
|
||||
expect(result.current.trace?.duration).toBe(transactionDuration);
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle errors and set transaction to null, and show a toast error', async () => {
|
||||
const errorMessage = 'Search error';
|
||||
lastValueFromMock.mockRejectedValue(new Error(errorMessage));
|
||||
it('should update trace when OTEL data is fetched successfully', async () => {
|
||||
const newWrapper = ({ children }: { children: React.ReactNode }) => (
|
||||
<RootSpanProvider traceId="test-trace" indexPattern="test-index">
|
||||
{children}
|
||||
</RootSpanProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), { wrapper });
|
||||
const transactionName = 'Test Trace';
|
||||
const transactionDuration = 1;
|
||||
lastValueFromMock.mockResolvedValue({
|
||||
rawResponse: {
|
||||
hits: {
|
||||
hits: [
|
||||
{
|
||||
fields: {
|
||||
[TRANSACTION_NAME_FIELD]: transactionName,
|
||||
[TRANSACTION_DURATION_FIELD]: transactionDuration,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { result } = renderHook(() => useRootSpanContext(), { wrapper: newWrapper });
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction).toBeNull();
|
||||
expect(result.current.trace?.name).toBe(transactionName);
|
||||
expect(result.current.trace?.duration).toBe(transactionDuration);
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should handle errors and set trace to null, and show a toast error', async () => {
|
||||
const errorMessage = 'Search error';
|
||||
lastValueFromMock.mockRejectedValue(new Error(errorMessage));
|
||||
|
||||
const { result } = renderHook(() => useRootSpanContext(), { wrapper });
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.trace).toBeNull();
|
||||
expect(lastValueFrom).toHaveBeenCalledTimes(1);
|
||||
expect(mockAddDanger).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
title: 'An error occurred while fetching the transaction',
|
||||
title: 'An error occurred while fetching the trace',
|
||||
text: errorMessage,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set transaction to null and stop loading when transactionId is not provided', async () => {
|
||||
const wrapperWithoutTransactionId = ({ children }: { children: React.ReactNode }) => (
|
||||
<TransactionProvider transactionId={undefined} indexPattern="test-index">
|
||||
it('should set trace to null and stop loading when traceId is not provided', async () => {
|
||||
const wrapperWithoutTraceId = ({ children }: { children: React.ReactNode }) => (
|
||||
<RootSpanProvider traceId={undefined} indexPattern="test-index">
|
||||
{children}
|
||||
</TransactionProvider>
|
||||
</RootSpanProvider>
|
||||
);
|
||||
|
||||
const { result } = renderHook(() => useTransactionContext(), {
|
||||
wrapper: wrapperWithoutTransactionId,
|
||||
const { result } = renderHook(() => useRootSpanContext(), {
|
||||
wrapper: wrapperWithoutTraceId,
|
||||
});
|
||||
|
||||
await waitFor(() => !result.current.loading);
|
||||
|
||||
expect(result.current.loading).toBe(false);
|
||||
expect(result.current.transaction).toBeNull();
|
||||
expect(result.current.trace).toBeNull();
|
||||
expect(lastValueFrom).not.toHaveBeenCalled();
|
||||
});
|
||||
});
|
|
@ -46,6 +46,7 @@ interface GetLatencyChartParams {
|
|||
signal: AbortSignal;
|
||||
spanName: string;
|
||||
serviceName: string;
|
||||
isOtelSpan: boolean;
|
||||
}
|
||||
|
||||
const getSpanLatencyChart = ({
|
||||
|
@ -53,6 +54,7 @@ const getSpanLatencyChart = ({
|
|||
signal,
|
||||
spanName,
|
||||
serviceName,
|
||||
isOtelSpan,
|
||||
}: GetLatencyChartParams): Promise<{
|
||||
overallHistogram?: HistogramItem[];
|
||||
percentileThresholdValue?: number;
|
||||
|
@ -65,6 +67,7 @@ const getSpanLatencyChart = ({
|
|||
spanName,
|
||||
serviceName,
|
||||
chartType: 'spanLatency',
|
||||
isOtel: isOtelSpan,
|
||||
end: timeFilter.to,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
|
@ -83,9 +86,14 @@ interface SpanLatencyChartData {
|
|||
interface UseSpanLatencyChartParams {
|
||||
spanName: string;
|
||||
serviceName: string;
|
||||
isOtelSpan?: boolean;
|
||||
}
|
||||
|
||||
export const useSpanLatencyChart = ({ spanName, serviceName }: UseSpanLatencyChartParams) => {
|
||||
export const useSpanLatencyChart = ({
|
||||
spanName,
|
||||
serviceName,
|
||||
isOtelSpan = false,
|
||||
}: UseSpanLatencyChartParams) => {
|
||||
const { core } = getUnifiedDocViewerServices();
|
||||
const { euiTheme } = useEuiTheme();
|
||||
|
||||
|
@ -100,6 +108,7 @@ export const useSpanLatencyChart = ({ spanName, serviceName }: UseSpanLatencyCha
|
|||
signal,
|
||||
spanName,
|
||||
serviceName,
|
||||
isOtelSpan,
|
||||
});
|
||||
|
||||
return {
|
||||
|
|
|
@ -1,133 +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", the "GNU Affero General Public License v3.0 only", and the "Server Side
|
||||
* Public License v 1"; you may not use this file except in compliance with, at
|
||||
* your election, the "Elastic License 2.0", the "GNU Affero General Public
|
||||
* License v3.0 only", or the "Server Side Public License, v 1".
|
||||
*/
|
||||
|
||||
import createContainer from 'constate';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { DataPublicPluginStart } from '@kbn/data-plugin/public';
|
||||
import { lastValueFrom } from 'rxjs';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import {
|
||||
PROCESSOR_EVENT_FIELD,
|
||||
TRANSACTION_DURATION_FIELD,
|
||||
TRANSACTION_ID_FIELD,
|
||||
TRANSACTION_NAME_FIELD,
|
||||
} from '@kbn/discover-utils';
|
||||
import { getUnifiedDocViewerServices } from '../../../../../../plugin';
|
||||
|
||||
interface UseTransactionPrams {
|
||||
transactionId?: string;
|
||||
indexPattern: string;
|
||||
}
|
||||
|
||||
interface GetTransactionParams {
|
||||
transactionId: string;
|
||||
indexPattern: string;
|
||||
data: DataPublicPluginStart;
|
||||
signal: AbortSignal;
|
||||
}
|
||||
|
||||
async function getTransactionData({
|
||||
transactionId,
|
||||
indexPattern,
|
||||
data,
|
||||
signal,
|
||||
}: GetTransactionParams) {
|
||||
return lastValueFrom(
|
||||
data.search.search(
|
||||
{
|
||||
params: {
|
||||
index: indexPattern,
|
||||
size: 1,
|
||||
body: {
|
||||
timeout: '20s',
|
||||
fields: [TRANSACTION_NAME_FIELD, TRANSACTION_DURATION_FIELD],
|
||||
query: {
|
||||
bool: {
|
||||
must: [
|
||||
{
|
||||
term: {
|
||||
[TRANSACTION_ID_FIELD]: transactionId,
|
||||
},
|
||||
},
|
||||
{
|
||||
term: {
|
||||
[PROCESSOR_EVENT_FIELD]: 'transaction',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{ abortSignal: signal }
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
export interface Transaction {
|
||||
name: string;
|
||||
duration: number;
|
||||
}
|
||||
|
||||
const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) => {
|
||||
const { data, core } = getUnifiedDocViewerServices();
|
||||
const [transaction, setTransaction] = useState<Transaction | null>(null);
|
||||
const [loading, setLoading] = useState<boolean>(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!transactionId) {
|
||||
setTransaction(null);
|
||||
setLoading(false);
|
||||
return;
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
const { signal } = controller;
|
||||
|
||||
const fetchData = async () => {
|
||||
try {
|
||||
setLoading(true);
|
||||
const result = await getTransactionData({ transactionId, indexPattern, data, signal });
|
||||
|
||||
const fields = result.rawResponse.hits.hits[0]?.fields;
|
||||
const transactionName = fields?.[TRANSACTION_NAME_FIELD];
|
||||
const transactionDuration = fields?.[TRANSACTION_DURATION_FIELD];
|
||||
|
||||
setTransaction({
|
||||
name: transactionName || null,
|
||||
duration: transactionDuration || null,
|
||||
});
|
||||
} catch (err) {
|
||||
if (!signal.aborted) {
|
||||
const error = err as Error;
|
||||
core.notifications.toasts.addDanger({
|
||||
title: i18n.translate('unifiedDocViewer.docViewerSpanOverview.useTransaction.error', {
|
||||
defaultMessage: 'An error occurred while fetching the transaction',
|
||||
}),
|
||||
text: error.message,
|
||||
});
|
||||
setTransaction(null);
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchData();
|
||||
|
||||
return function onUnmount() {
|
||||
controller.abort();
|
||||
};
|
||||
}, [core.notifications.toasts, data, indexPattern, transactionId]);
|
||||
|
||||
return { loading, transaction };
|
||||
};
|
||||
|
||||
export const [TransactionProvider, useTransactionContext] = createContainer(useTransaction);
|
|
@ -9,6 +9,7 @@
|
|||
|
||||
import { EuiFlexGroup, EuiFlexItem, EuiPanel, EuiSpacer } from '@elastic/eui';
|
||||
import {
|
||||
OTEL_DURATION,
|
||||
SERVICE_NAME_FIELD,
|
||||
SPAN_DURATION_FIELD,
|
||||
SPAN_ID_FIELD,
|
||||
|
@ -23,7 +24,7 @@ import React, { useMemo } from 'react';
|
|||
import { FieldActionsProvider } from '../../../../hooks/use_field_actions';
|
||||
import { getUnifiedDocViewerServices } from '../../../../plugin';
|
||||
import { Trace } from '../components/trace';
|
||||
import { TransactionProvider } from './hooks/use_transaction';
|
||||
import { RootSpanProvider } from './hooks/use_root_span';
|
||||
import { spanFields } from './resources/fields';
|
||||
import { getSpanFieldConfiguration } from './resources/get_span_field_configuration';
|
||||
import { SpanDurationSummary } from './sub_components/span_duration_summary';
|
||||
|
@ -68,16 +69,24 @@ export function SpanOverview({
|
|||
[formattedDoc, flattenedDoc]
|
||||
);
|
||||
|
||||
const spanDuration = flattenedDoc[SPAN_DURATION_FIELD];
|
||||
const isOtelSpan =
|
||||
flattenedDoc[SPAN_DURATION_FIELD] == null && flattenedDoc[OTEL_DURATION] != null;
|
||||
|
||||
const spanDuration = isOtelSpan
|
||||
? flattenedDoc[OTEL_DURATION]! * 0.001
|
||||
: flattenedDoc[SPAN_DURATION_FIELD];
|
||||
|
||||
const traceId = flattenedDoc[TRACE_ID_FIELD];
|
||||
const transactionId = flattenedDoc[TRANSACTION_ID_FIELD];
|
||||
|
||||
return (
|
||||
<DataSourcesProvider indexes={indexes}>
|
||||
<RootTransactionProvider
|
||||
traceId={flattenedDoc[TRACE_ID_FIELD]}
|
||||
indexPattern={indexes.apm.traces}
|
||||
>
|
||||
<TransactionProvider transactionId={transactionId} indexPattern={indexes.apm.traces}>
|
||||
<RootTransactionProvider traceId={traceId} indexPattern={indexes.apm.traces}>
|
||||
<RootSpanProvider
|
||||
traceId={traceId}
|
||||
transactionId={transactionId}
|
||||
indexPattern={indexes.apm.traces}
|
||||
>
|
||||
<FieldActionsProvider
|
||||
columns={columns}
|
||||
filter={filter}
|
||||
|
@ -114,6 +123,7 @@ export function SpanOverview({
|
|||
spanDuration={spanDuration}
|
||||
spanName={flattenedDoc[SPAN_NAME_FIELD]}
|
||||
serviceName={flattenedDoc[SERVICE_NAME_FIELD]}
|
||||
isOtelSpan={isOtelSpan}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
|
@ -132,7 +142,7 @@ export function SpanOverview({
|
|||
</EuiFlexGroup>
|
||||
</EuiPanel>
|
||||
</FieldActionsProvider>
|
||||
</TransactionProvider>
|
||||
</RootSpanProvider>
|
||||
</RootTransactionProvider>
|
||||
</DataSourcesProvider>
|
||||
);
|
||||
|
|
|
@ -12,7 +12,7 @@ import { EuiFlexGroup, EuiFlexItem, EuiHorizontalRule, EuiText, EuiTitle } from
|
|||
import { i18n } from '@kbn/i18n';
|
||||
import { Duration, DurationDistributionChart } from '@kbn/apm-ui-shared';
|
||||
import { ProcessorEvent } from '@kbn/apm-types-shared';
|
||||
import { useTransactionContext } from '../../hooks/use_transaction';
|
||||
import { useRootSpanContext } from '../../hooks/use_root_span';
|
||||
import { useSpanLatencyChart } from '../../hooks/use_span_latency_chart';
|
||||
import { FieldWithoutActions } from '../../../components/field_without_actions';
|
||||
import { Section } from '../../../components/section';
|
||||
|
@ -21,14 +21,16 @@ export interface SpanDurationSummaryProps {
|
|||
spanDuration: number;
|
||||
spanName: string;
|
||||
serviceName: string;
|
||||
isOtelSpan: boolean;
|
||||
}
|
||||
|
||||
export function SpanDurationSummary({
|
||||
spanDuration,
|
||||
spanName,
|
||||
serviceName,
|
||||
isOtelSpan,
|
||||
}: SpanDurationSummaryProps) {
|
||||
const { transaction, loading } = useTransactionContext();
|
||||
const { trace, loading } = useRootSpanContext();
|
||||
const {
|
||||
data: latencyChartData,
|
||||
loading: latencyChartLoading,
|
||||
|
@ -36,6 +38,7 @@ export function SpanDurationSummary({
|
|||
} = useSpanLatencyChart({
|
||||
spanName,
|
||||
serviceName,
|
||||
isOtelSpan,
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -65,7 +68,7 @@ export function SpanDurationSummary({
|
|||
<EuiText size="xs">
|
||||
<Duration
|
||||
duration={spanDuration}
|
||||
parent={{ loading, duration: transaction?.duration, type: 'transaction' }}
|
||||
parent={{ loading, duration: trace?.duration, type: 'transaction' }}
|
||||
/>
|
||||
</EuiText>
|
||||
</FieldWithoutActions>
|
||||
|
@ -95,6 +98,7 @@ export function SpanDurationSummary({
|
|||
eventType={ProcessorEvent.span}
|
||||
showAxisTitle={false}
|
||||
showLegend={false}
|
||||
isOtelData={isOtelSpan}
|
||||
dataTestSubPrefix="docViewerSpanOverview"
|
||||
/>
|
||||
)}
|
||||
|
|
|
@ -11,7 +11,7 @@ import { TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
|
|||
import { EuiHorizontalRule } from '@elastic/eui';
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { FieldWithActions } from '../../components/field_with_actions/field_with_actions';
|
||||
import { useTransactionContext } from '../hooks/use_transaction';
|
||||
import { useRootSpanContext } from '../hooks/use_root_span';
|
||||
import { FieldConfiguration } from '../../resources/get_field_configuration';
|
||||
export interface SpanSummaryFieldProps {
|
||||
fieldId: string;
|
||||
|
@ -24,16 +24,16 @@ export function SpanSummaryField({
|
|||
fieldId,
|
||||
showActions = true,
|
||||
}: SpanSummaryFieldProps) {
|
||||
const { transaction, loading } = useTransactionContext();
|
||||
const { trace, loading } = useRootSpanContext();
|
||||
const [fieldValue, setFieldValue] = useState(fieldConfiguration.value);
|
||||
const isTransactionNameField = fieldId === TRANSACTION_NAME_FIELD;
|
||||
const isTransactionNameFieldWithoutValue = isTransactionNameField && !fieldValue;
|
||||
|
||||
useEffect(() => {
|
||||
if (isTransactionNameField && !fieldValue && transaction?.name && !loading) {
|
||||
setFieldValue(transaction.name);
|
||||
if (isTransactionNameField && !fieldValue && trace?.name && !loading) {
|
||||
setFieldValue(trace.name);
|
||||
}
|
||||
}, [transaction?.name, loading, fieldValue, isTransactionNameField]);
|
||||
}, [trace?.name, loading, fieldValue, isTransactionNameField]);
|
||||
|
||||
if (
|
||||
(!isTransactionNameFieldWithoutValue && !fieldValue) ||
|
||||
|
|
|
@ -33,12 +33,14 @@ export const fetchDurationHistogramRangeSteps = async ({
|
|||
searchMetrics,
|
||||
durationMinOverride,
|
||||
durationMaxOverride,
|
||||
isOtel = false,
|
||||
}: CommonCorrelationsQueryParams & {
|
||||
chartType: LatencyDistributionChartType;
|
||||
apmEventClient: APMEventClient;
|
||||
searchMetrics: boolean;
|
||||
durationMinOverride?: number;
|
||||
durationMaxOverride?: number;
|
||||
isOtel?: boolean;
|
||||
}): Promise<{
|
||||
durationMin?: number;
|
||||
durationMax?: number;
|
||||
|
@ -58,7 +60,7 @@ export const fetchDurationHistogramRangeSteps = async ({
|
|||
};
|
||||
}
|
||||
|
||||
const durationField = getDurationField(chartType, searchMetrics);
|
||||
const durationField = getDurationField(chartType, searchMetrics, isOtel);
|
||||
|
||||
// when using metrics data, ensure we filter by docs with the appropriate duration field
|
||||
const filteredQuery = searchMetrics
|
||||
|
@ -69,24 +71,28 @@ export const fetchDurationHistogramRangeSteps = async ({
|
|||
}
|
||||
: query;
|
||||
|
||||
const resp = await apmEventClient.search('get_duration_histogram_range_steps', {
|
||||
apm: {
|
||||
events: [getEventType(chartType, searchMetrics)],
|
||||
const resp = await apmEventClient.search(
|
||||
'get_duration_histogram_range_steps',
|
||||
{
|
||||
apm: {
|
||||
events: [getEventType(chartType, searchMetrics)],
|
||||
},
|
||||
track_total_hits: 1,
|
||||
size: 0,
|
||||
query: getCommonCorrelationsQuery({
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
query: filteredQuery,
|
||||
}),
|
||||
aggs: {
|
||||
duration_min: { min: { field: durationField } },
|
||||
duration_max: { max: { field: durationField } },
|
||||
},
|
||||
},
|
||||
track_total_hits: 1,
|
||||
size: 0,
|
||||
query: getCommonCorrelationsQuery({
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
query: filteredQuery,
|
||||
}),
|
||||
aggs: {
|
||||
duration_min: { min: { field: durationField } },
|
||||
duration_max: { max: { field: durationField } },
|
||||
},
|
||||
});
|
||||
{ skipProcessorEventFilter: isOtel }
|
||||
);
|
||||
|
||||
if (resp.hits.total.value === 0) {
|
||||
return { rangeSteps: getHistogramRangeSteps(0, 1, 100) };
|
||||
|
|
|
@ -23,11 +23,13 @@ export const fetchDurationPercentiles = async ({
|
|||
query,
|
||||
percents,
|
||||
searchMetrics,
|
||||
isOtel = false,
|
||||
}: CommonCorrelationsQueryParams & {
|
||||
chartType: LatencyDistributionChartType;
|
||||
apmEventClient: APMEventClient;
|
||||
percents?: number[];
|
||||
searchMetrics: boolean;
|
||||
isOtel?: boolean;
|
||||
}): Promise<{
|
||||
totalDocs: number;
|
||||
percentiles: Record<string, number>;
|
||||
|
@ -58,13 +60,15 @@ export const fetchDurationPercentiles = async ({
|
|||
hdr: {
|
||||
number_of_significant_value_digits: SIGNIFICANT_VALUE_DIGITS,
|
||||
},
|
||||
field: getDurationField(chartType, searchMetrics),
|
||||
field: getDurationField(chartType, searchMetrics, isOtel),
|
||||
...(Array.isArray(percents) ? { percents } : {}),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const response = await apmEventClient.search('get_duration_percentiles', params);
|
||||
const response = await apmEventClient.search('get_duration_percentiles', params, {
|
||||
skipProcessorEventFilter: isOtel,
|
||||
});
|
||||
|
||||
// return early with no results if the search didn't return any documents
|
||||
if (!response.aggregations || response.hits.total.value === 0) {
|
||||
|
|
|
@ -24,6 +24,7 @@ export const fetchDurationRanges = async ({
|
|||
query,
|
||||
chartType,
|
||||
searchMetrics,
|
||||
isOtel = false,
|
||||
}: {
|
||||
rangeSteps: number[];
|
||||
apmEventClient: APMEventClient;
|
||||
|
@ -34,6 +35,7 @@ export const fetchDurationRanges = async ({
|
|||
query: estypes.QueryDslQueryContainer;
|
||||
chartType: LatencyDistributionChartType;
|
||||
searchMetrics: boolean;
|
||||
isOtel?: boolean;
|
||||
}): Promise<{
|
||||
totalDocCount: number;
|
||||
durationRanges: Array<{ key: number; doc_count: number }>;
|
||||
|
@ -59,28 +61,32 @@ export const fetchDurationRanges = async ({
|
|||
ranges.push({ from: ranges[ranges.length - 1].to });
|
||||
}
|
||||
|
||||
const resp = await apmEventClient.search('get_duration_ranges', {
|
||||
apm: {
|
||||
events: [getEventType(chartType, searchMetrics)],
|
||||
},
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: getCommonCorrelationsQuery({
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
query: filteredQuery,
|
||||
}),
|
||||
aggs: {
|
||||
logspace_ranges: {
|
||||
range: {
|
||||
field: getDurationField(chartType, searchMetrics),
|
||||
ranges,
|
||||
const resp = await apmEventClient.search(
|
||||
'get_duration_ranges',
|
||||
{
|
||||
apm: {
|
||||
events: [getEventType(chartType, searchMetrics)],
|
||||
},
|
||||
track_total_hits: false,
|
||||
size: 0,
|
||||
query: getCommonCorrelationsQuery({
|
||||
start,
|
||||
end,
|
||||
environment,
|
||||
kuery,
|
||||
query: filteredQuery,
|
||||
}),
|
||||
aggs: {
|
||||
logspace_ranges: {
|
||||
range: {
|
||||
field: getDurationField(chartType, searchMetrics, isOtel),
|
||||
ranges,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
{ skipProcessorEventFilter: isOtel }
|
||||
);
|
||||
|
||||
const durationRanges =
|
||||
resp.aggregations?.logspace_ranges.buckets
|
||||
|
|
|
@ -6,6 +6,7 @@
|
|||
*/
|
||||
|
||||
import {
|
||||
DURATION,
|
||||
SPAN_DURATION,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_DURATION_HISTOGRAM,
|
||||
|
@ -20,7 +21,11 @@ const {
|
|||
spanLatency,
|
||||
} = LatencyDistributionChartType;
|
||||
|
||||
export function getDurationField(chartType: LatencyDistributionChartType, searchMetrics: boolean) {
|
||||
export function getDurationField(
|
||||
chartType: LatencyDistributionChartType,
|
||||
searchMetrics: boolean,
|
||||
isOtel: boolean
|
||||
) {
|
||||
switch (chartType) {
|
||||
case transactionLatency:
|
||||
if (searchMetrics) {
|
||||
|
@ -33,7 +38,7 @@ export function getDurationField(chartType: LatencyDistributionChartType, search
|
|||
return TRANSACTION_DURATION;
|
||||
case dependencyLatency:
|
||||
case spanLatency:
|
||||
return SPAN_DURATION;
|
||||
return isOtel ? DURATION : SPAN_DURATION;
|
||||
default:
|
||||
return TRANSACTION_DURATION;
|
||||
}
|
||||
|
|
|
@ -27,6 +27,7 @@ export async function getOverallLatencyDistribution({
|
|||
durationMinOverride,
|
||||
durationMaxOverride,
|
||||
searchMetrics,
|
||||
isOtel = false,
|
||||
}: {
|
||||
chartType: LatencyDistributionChartType;
|
||||
apmEventClient: APMEventClient;
|
||||
|
@ -39,6 +40,7 @@ export async function getOverallLatencyDistribution({
|
|||
durationMinOverride?: number;
|
||||
durationMaxOverride?: number;
|
||||
searchMetrics: boolean;
|
||||
isOtel?: boolean;
|
||||
}) {
|
||||
return withApmSpan('get_overall_latency_distribution', async () => {
|
||||
const overallLatencyDistribution: OverallLatencyDistributionResponse = {};
|
||||
|
@ -54,6 +56,7 @@ export async function getOverallLatencyDistribution({
|
|||
query,
|
||||
percentileThreshold,
|
||||
searchMetrics,
|
||||
isOtel,
|
||||
});
|
||||
|
||||
// finish early if we weren't able to identify the percentileThresholdValue.
|
||||
|
@ -73,6 +76,7 @@ export async function getOverallLatencyDistribution({
|
|||
searchMetrics,
|
||||
durationMinOverride,
|
||||
durationMaxOverride,
|
||||
isOtel,
|
||||
});
|
||||
|
||||
if (!rangeSteps) {
|
||||
|
@ -90,6 +94,7 @@ export async function getOverallLatencyDistribution({
|
|||
query,
|
||||
rangeSteps,
|
||||
searchMetrics,
|
||||
isOtel,
|
||||
});
|
||||
|
||||
overallLatencyDistribution.durationMin = durationMin;
|
||||
|
|
|
@ -20,11 +20,13 @@ export async function getPercentileThresholdValue({
|
|||
query,
|
||||
percentileThreshold,
|
||||
searchMetrics,
|
||||
isOtel,
|
||||
}: CommonCorrelationsQueryParams & {
|
||||
apmEventClient: APMEventClient;
|
||||
chartType: LatencyDistributionChartType;
|
||||
percentileThreshold: number;
|
||||
searchMetrics: boolean;
|
||||
isOtel: boolean;
|
||||
}) {
|
||||
const durationPercentiles = await fetchDurationPercentiles({
|
||||
apmEventClient,
|
||||
|
@ -35,6 +37,7 @@ export async function getPercentileThresholdValue({
|
|||
kuery,
|
||||
query,
|
||||
searchMetrics,
|
||||
isOtel,
|
||||
});
|
||||
|
||||
return durationPercentiles.percentiles[`${percentileThreshold}.0`];
|
||||
|
|
|
@ -33,6 +33,7 @@ const latencyOverallSpanDistributionRoute = createApmServerRoute({
|
|||
),
|
||||
durationMin: toNumberRt,
|
||||
durationMax: toNumberRt,
|
||||
isOtel: t.boolean,
|
||||
}),
|
||||
environmentRt,
|
||||
kueryRt,
|
||||
|
@ -59,6 +60,7 @@ const latencyOverallSpanDistributionRoute = createApmServerRoute({
|
|||
durationMax,
|
||||
termFilters,
|
||||
chartType,
|
||||
isOtel = false,
|
||||
} = resources.params.body;
|
||||
|
||||
return getOverallLatencyDistribution({
|
||||
|
@ -83,6 +85,7 @@ const latencyOverallSpanDistributionRoute = createApmServerRoute({
|
|||
durationMinOverride: durationMin,
|
||||
durationMaxOverride: durationMax,
|
||||
searchMetrics: false,
|
||||
isOtel,
|
||||
});
|
||||
},
|
||||
});
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue