[Discover][APM] Custom overview tab for data_stream.type: "traces" in the new trace data source profile detail flyout (#210612)

## Summary

Closes https://github.com/elastic/kibana/issues/208693 and
https://github.com/elastic/kibana/issues/211785

This PR introduces a new Overview tab that will appear when the new
traces data source profile is enabled, and a `data_stream.type:
"traces"` document is being checked.

![Screenshot 2025-02-11 at 16 34
35](https://github.com/user-attachments/assets/0ab4e8bc-cb08-4582-8dc8-8a1065eb673a)


### Fields highlighted for clarity
|Transaction|Span|
|-|-|
|![Screenshot 2025-02-11 at 16 25
14](https://github.com/user-attachments/assets/c67b01d1-e494-4101-9834-5736b7f21835)|![Screenshot
2025-02-11 at 16 24
34](https://github.com/user-attachments/assets/c6e6e9d5-bc24-4993-a09c-c14ab55411dc)|

### Actions available for each field
![Screen Recording 2025-02-11 at 15 27
08](https://github.com/user-attachments/assets/e02b0850-d05b-434c-8f0f-9d5bda7c7beb)


### Detailed flat name for the dependency field
Since the source of the Dependency field cannot be easily assumed, we've
decided to add some metadata for it.

![Screenshot 2025-02-11 at 15 26
54](https://github.com/user-attachments/assets/6ed076bc-784a-4edb-8361-e1c2c3375e83)

### Filtering by transaction name on a span
It's important to note that `transaction.name` may not be present in a
span document. As a result, filtering by this field would likely return
only transaction documents, as they have `transaction.name` defined. cc:
@patpscal

![Screen Recording 2025-02-11 at 16 29
16](https://github.com/user-attachments/assets/526f7295-d88e-4688-aa9d-be1af542278b)

## How to test
* Add the following to your `kibana.dev.yml`:
```yaml
discover.experimental.enabledProfiles:
  - traces-data-source-profile
```
* Run synthtrace with `traces_logs_entitities.ts`
```
node scripts/synthtrace traces_logs_entities.ts  --from=now-24h --to=now
```
* Go to Discover and make sure the APM data view is selected
* Open the flyout for any document with `data_stream.type: "traces"`

---------

Co-authored-by: kibanamachine <42973632+kibanamachine@users.noreply.github.com>
Co-authored-by: Cauê Marcondes <55978943+cauemarcondes@users.noreply.github.com>
Co-authored-by: Carlos Crespo <crespocarlos@users.noreply.github.com>
This commit is contained in:
Irene Blanco 2025-02-25 09:08:19 +01:00 committed by GitHub
parent 4e4819ca20
commit af3409518f
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 2046 additions and 18 deletions

View file

@ -16,3 +16,21 @@ export interface TransactionDetailsByTraceIdLocatorParams extends SerializableRe
rangeTo?: string;
traceId: string;
}
export const DEPENDENCY_OVERVIEW_LOCATOR_ID = 'dependencyOverviewLocator';
export interface DependencyOverviewParams extends SerializableRecord {
dependencyName: string;
environment?: string;
rangeFrom?: string;
rangeTo?: string;
}
export const TRANSACTION_DETAILS_BY_NAME_LOCATOR = 'TransactionDetailsByNameLocator';
export interface TransactionDetailsByNameParams extends SerializableRecord {
serviceName: string;
transactionName: string;
rangeFrom?: string;
rangeTo?: string;
}

View file

@ -0,0 +1,19 @@
/*
* 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 type { SerializableRecord } from '@kbn/utility-types';
export interface DependencyOverviewParams extends SerializableRecord {
dependencyName: string;
environment?: string;
rangeFrom?: string;
rangeTo?: string;
}
export const DEPENDENCY_OVERVIEW_LOCATOR_ID = 'dependencyOverviewLocator';

View file

@ -37,6 +37,7 @@ export {
formatHit,
getDocId,
getLogDocumentOverview,
getTraceDocumentOverview,
getIgnoredReason,
getMessageFieldWithFallbacks,
getShouldShowFieldHandler,

View file

@ -15,13 +15,21 @@ export const MESSAGE_FIELD = 'message';
export const ERROR_MESSAGE_FIELD = 'error.message';
export const EVENT_ORIGINAL_FIELD = 'event.original';
export const EVENT_OUTCOME_FIELD = 'event.outcome';
export const INDEX_FIELD = '_index';
// Trace fields
export const TRACE_ID_FIELD = 'trace.id';
export const PARENT_ID_FIELD = 'parent.id';
export const TRANSACTION_ID_FIELD = 'transaction.id';
export const TRANSACTION_NAME_FIELD = 'transaction.name';
export const TRANSACTION_DURATION_FIELD = 'transaction.duration.us';
export const SPAN_NAME_FIELD = 'span.name';
export const SPAN_ACTION_FIELD = 'span.action';
export const SPAN_DURATION_FIELD = 'span.duration.us';
export const SPAN_TYPE_FIELD = 'span.type';
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 LOG_FILE_PATH_FIELD = 'log.file.path';
export const DATASTREAM_NAMESPACE_FIELD = 'data_stream.namespace';
@ -36,12 +44,16 @@ export const CLOUD_AVAILABILITY_ZONE_FIELD = 'cloud.availability_zone';
export const CLOUD_PROJECT_ID_FIELD = 'cloud.project.id';
export const CLOUD_INSTANCE_ID_FIELD = 'cloud.instance.id';
export const SERVICE_NAME_FIELD = 'service.name';
export const SERVICE_ENVIRONMENT_FIELD = 'service.environment';
export const ORCHESTRATOR_CLUSTER_NAME_FIELD = 'orchestrator.cluster.name';
export const ORCHESTRATOR_CLUSTER_ID_FIELD = 'orchestrator.cluster.id';
export const ORCHESTRATOR_RESOURCE_ID_FIELD = 'orchestrator.resource.id';
export const ORCHESTRATOR_NAMESPACE_FIELD = 'orchestrator.namespace';
export const CONTAINER_NAME_FIELD = 'container.name';
export const CONTAINER_ID_FIELD = 'container.id';
export const USER_AGENT_NAME_FIELD = 'user_agent.name';
export const USER_AGENT_VERSION_FIELD = 'user_agent.version';
export const HTTP_RESPONSE_STATUS_CODE_FIELD = 'http.response.status_code';
// Degraded Docs
export const IGNORED_FIELD = '_ignored';
@ -52,5 +64,3 @@ export const DEGRADED_DOCS_FIELDS = [IGNORED_FIELD, IGNORED_FIELD_VALUES_FIELD]
export const ERROR_STACK_TRACE = 'error.stack_trace';
export const ERROR_EXCEPTION_STACKTRACE_ABS_PATH = 'error.exception.stacktrace.abs_path';
export const ERROR_LOG_STACKTRACE_ABS_PATH = 'error.log.stacktrace.abs_path';
export const INDEX_FIELD = '_index';

View file

@ -112,3 +112,41 @@ export interface LogCloudFields {
'cloud.project.id'?: string;
'cloud.instance.id'?: string;
}
export interface TraceDocumentOverview
extends ServiceFields,
TransactionTraceFields,
SpanTraceFields,
UserAgentTraceFields {
'@timestamp': number;
'trace.id': string;
'parent.id'?: string;
'http.response.status_code'?: number;
'processor.event'?: 'span' | 'transaction';
}
export interface ServiceFields {
'service.name': string;
'service.environment': string;
'agent.name': string;
}
export interface TransactionTraceFields {
'transaction.id'?: string;
'transaction.name'?: string;
'transaction.duration.us'?: number;
}
export interface SpanTraceFields {
'span.name'?: string;
'span.action'?: string;
'span.duration.us'?: number;
'span.type'?: string;
'span.subtype'?: string;
'span.destination.service.resource'?: string;
}
export interface UserAgentTraceFields {
'user_agent.name'?: string;
'user_agent.version'?: string;
}

View file

@ -0,0 +1,43 @@
/*
* 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 { castArray } from 'lodash';
import { DataTableRecord, TraceDocumentOverview, fieldConstants } from '../..';
export function getTraceDocumentOverview(doc: DataTableRecord): TraceDocumentOverview {
const formatField = <T extends keyof TraceDocumentOverview>(field: T) =>
castArray(doc.flattened[field])[0] as TraceDocumentOverview[T];
const fields: Array<keyof TraceDocumentOverview> = [
fieldConstants.TIMESTAMP_FIELD,
fieldConstants.PARENT_ID_FIELD,
fieldConstants.HTTP_RESPONSE_STATUS_CODE_FIELD,
fieldConstants.TRACE_ID_FIELD,
fieldConstants.SERVICE_NAME_FIELD,
fieldConstants.SERVICE_ENVIRONMENT_FIELD,
fieldConstants.AGENT_NAME_FIELD,
fieldConstants.TRANSACTION_ID_FIELD,
fieldConstants.TRANSACTION_NAME_FIELD,
fieldConstants.TRANSACTION_DURATION_FIELD,
fieldConstants.SPAN_NAME_FIELD,
fieldConstants.SPAN_ACTION_FIELD,
fieldConstants.SPAN_DURATION_FIELD,
fieldConstants.SPAN_TYPE_FIELD,
fieldConstants.SPAN_SUBTYPE_FIELD,
fieldConstants.SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
fieldConstants.USER_AGENT_NAME_FIELD,
fieldConstants.USER_AGENT_VERSION_FIELD,
fieldConstants.PROCESSOR_EVENT_FIELD,
];
return fields.reduce((acc, field) => {
acc[field] = formatField(field);
return acc;
}, {} as { [key in keyof TraceDocumentOverview]?: string | number }) as TraceDocumentOverview;
}

View file

@ -14,6 +14,7 @@ export * from './format_value';
export * from './get_doc_id';
export * from './get_ignored_reason';
export * from './get_log_document_overview';
export * from './get_trace_document_overview';
export * from './get_message_field_with_fallbacks';
export * from './get_should_show_field_handler';
export * from './get_stack_trace_fields';

View file

@ -0,0 +1,55 @@
/*
* 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 { i18n } from '@kbn/i18n';
import React from 'react';
import { UnifiedDocViewerTracesOverview } from '@kbn/unified-doc-viewer-plugin/public';
import { DocViewsRegistry } from '@kbn/unified-doc-viewer';
import { DATASTREAM_TYPE_FIELD, PROCESSOR_EVENT_FIELD, getFieldValue } from '@kbn/discover-utils';
import { DocViewerExtensionParams, DocViewerExtension } from '../../../../types';
export const getDocViewer =
(prev: (params: DocViewerExtensionParams) => DocViewerExtension) =>
(params: DocViewerExtensionParams) => {
const prevValue = prev(params);
const isTrace = getFieldValue(params.record, DATASTREAM_TYPE_FIELD) === 'traces';
if (!isTrace) {
return prevValue;
}
const processorEvent = getFieldValue(params.record, PROCESSOR_EVENT_FIELD);
const documentType =
processorEvent === 'span'
? i18n.translate('discover.docViews.tracesOverview.spanTitle', {
defaultMessage: 'Span',
})
: i18n.translate('discover.docViews.tracesOverview.transactionTitle', {
defaultMessage: 'Transaction',
});
return {
...prevValue,
docViewsRegistry: (registry: DocViewsRegistry) => {
registry.add({
id: 'doc_view_traces_overview',
title: i18n.translate('discover.docViews.tracesOverview.title', {
defaultMessage: '{documentType} overview',
values: { documentType },
}),
order: 0,
component: (props) => {
return <UnifiedDocViewerTracesOverview {...props} />;
},
});
return prevValue.docViewsRegistry(registry);
},
};
};

View file

@ -9,6 +9,7 @@
import { DataSourceType, isDataSourceType } from '../../../../../common/data_sources';
import { DataSourceCategory, DataSourceProfileProvider } from '../../../profiles';
import { getDocViewer } from './accessors/get_doc_viewer';
import { getCellRenderers } from './accessors';
export const createTracesDataSourceProfileProvider = (): DataSourceProfileProvider => ({
@ -27,6 +28,7 @@ export const createTracesDataSourceProfileProvider = (): DataSourceProfileProvid
],
rowHeight: 5,
}),
getDocViewer,
getCellRenderers,
},
resolve: ({ dataSource }) => {

View file

@ -0,0 +1,121 @@
/*
* 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 React from 'react';
import { renderHook, waitFor } from '@testing-library/react';
import { lastValueFrom } from 'rxjs';
import { getUnifiedDocViewerServices } from '../../../plugin';
import { TransactionProvider, useTransactionContext } from './use_transaction';
jest.mock('../../../plugin', () => ({
getUnifiedDocViewerServices: jest.fn(),
}));
jest.mock('rxjs', () => {
const originalModule = jest.requireActual('rxjs');
return {
...originalModule,
lastValueFrom: jest.fn(),
};
});
const mockSearch = jest.fn();
const mockAddDanger = jest.fn();
(getUnifiedDocViewerServices as jest.Mock).mockReturnValue({
data: {
search: {
search: mockSearch,
},
},
core: {
notifications: {
toasts: {
addDanger: mockAddDanger,
},
},
},
});
beforeEach(() => {
jest.clearAllMocks();
(lastValueFrom as jest.Mock).mockReset();
});
describe('useTransaction hook', () => {
const wrapper = ({ children }: { children: React.ReactNode }) => (
<TransactionProvider transactionId="test-transaction" indexPattern="test-index">
{children}
</TransactionProvider>
);
it('should start with loading true and transaction as null', async () => {
(lastValueFrom as jest.Mock).mockResolvedValue({});
const { result } = renderHook(() => useTransactionContext(), { 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';
(lastValueFrom as jest.Mock).mockResolvedValue({
rawResponse: {
hits: {
hits: [{ _source: { transaction: { name: transactionName } } }],
},
},
});
const { result } = renderHook(() => useTransactionContext(), { wrapper });
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction?.name).toBe(transactionName);
expect(lastValueFrom).toHaveBeenCalledTimes(1);
});
it('should handle errors and set transaction.name as empty string, and show a toast error', async () => {
const errorMessage = 'Search error';
(lastValueFrom as jest.Mock).mockRejectedValue(new Error(errorMessage));
const { result } = renderHook(() => useTransactionContext(), { wrapper });
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction).toEqual({ name: '' });
expect(lastValueFrom).toHaveBeenCalledTimes(1);
expect(mockAddDanger).toHaveBeenCalledWith(
expect.objectContaining({
title: 'An error occurred while fetching the transaction',
text: errorMessage,
})
);
});
it('should set transaction.name as empty string and stop loading when transactionId is not provided', async () => {
const wrapperWithoutTransactionId = ({ children }: { children: React.ReactNode }) => (
<TransactionProvider transactionId={undefined} indexPattern="test-index">
{children}
</TransactionProvider>
);
const { result } = renderHook(() => useTransactionContext(), {
wrapper: wrapperWithoutTransactionId,
});
await waitFor(() => !result.current.loading);
expect(result.current.loading).toBe(false);
expect(result.current.transaction).toEqual({ name: '' });
expect(lastValueFrom).not.toHaveBeenCalled();
});
});

View file

@ -0,0 +1,97 @@
/*
* 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 { getUnifiedDocViewerServices } from '../../../plugin';
interface UseTransactionPrams {
transactionId?: string;
indexPattern: string;
}
interface GetTransactionParams {
transactionId: string;
indexPattern: string;
data: DataPublicPluginStart;
}
async function getTransactionData({ transactionId, indexPattern, data }: GetTransactionParams) {
return lastValueFrom(
data.search.search({
params: {
index: indexPattern,
size: 1,
body: {
timeout: '20s',
query: {
bool: {
must: [
{
term: {
'transaction.id': transactionId,
},
},
{
term: {
'processor.event': 'transaction',
},
},
],
},
},
},
},
})
);
}
const useTransaction = ({ transactionId, indexPattern }: UseTransactionPrams) => {
const { data, core } = getUnifiedDocViewerServices();
const [transaction, setTransaction] = useState<{ [key: string]: string } | null>(null);
const [loading, setLoading] = useState<boolean>(true);
useEffect(() => {
const fetchData = async () => {
if (transactionId) {
try {
setLoading(true);
const result = await getTransactionData({ transactionId, indexPattern, data });
const transactionName = result.rawResponse.hits.hits[0]?._source.transaction?.name;
setTransaction(transactionName ? { name: transactionName } : null);
} catch (err) {
const error = err as Error;
core.notifications.toasts.addDanger({
title: i18n.translate('unifiedDocViewer.docViewerTracesOverview.useTransaction.error', {
defaultMessage: 'An error occurred while fetching the transaction',
}),
text: error.message,
});
setTransaction({ name: '' });
} finally {
setLoading(false);
}
} else {
setTransaction({ name: '' });
setLoading(false);
}
};
fetchData();
}, [core.notifications.toasts, data, indexPattern, transactionId]);
return { loading, transaction };
};
export const [TransactionProvider, useTransactionContext] = createContainer(useTransaction);

View file

@ -0,0 +1,14 @@
/*
* 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 { TracesOverview } from './traces_overview';
// Required for usage in React.lazy
// eslint-disable-next-line import/no-default-export
export default TracesOverview;

View file

@ -0,0 +1,47 @@
/*
* 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 {
HTTP_RESPONSE_STATUS_CODE_FIELD,
SERVICE_NAME_FIELD,
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
SPAN_DURATION_FIELD,
SPAN_NAME_FIELD,
SPAN_SUBTYPE_FIELD,
SPAN_TYPE_FIELD,
TIMESTAMP_FIELD,
TRACE_ID_FIELD,
TRANSACTION_DURATION_FIELD,
TRANSACTION_NAME_FIELD,
USER_AGENT_NAME_FIELD,
USER_AGENT_VERSION_FIELD,
} from '@kbn/discover-utils';
export const spanFields = [
SPAN_NAME_FIELD,
TRANSACTION_NAME_FIELD,
SERVICE_NAME_FIELD,
TRACE_ID_FIELD,
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
TIMESTAMP_FIELD,
SPAN_DURATION_FIELD,
HTTP_RESPONSE_STATUS_CODE_FIELD,
SPAN_TYPE_FIELD,
SPAN_SUBTYPE_FIELD,
];
export const transactionFields = [
TRANSACTION_NAME_FIELD,
SERVICE_NAME_FIELD,
TRACE_ID_FIELD,
TIMESTAMP_FIELD,
TRANSACTION_DURATION_FIELD,
HTTP_RESPONSE_STATUS_CODE_FIELD,
USER_AGENT_NAME_FIELD,
USER_AGENT_VERSION_FIELD,
];

View file

@ -0,0 +1,166 @@
/*
* 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 {
SPAN_NAME_FIELD,
TRANSACTION_NAME_FIELD,
SERVICE_NAME_FIELD,
TRACE_ID_FIELD,
SPAN_DESTINATION_SERVICE_RESOURCE_FIELD,
TIMESTAMP_FIELD,
SPAN_DURATION_FIELD,
TRANSACTION_DURATION_FIELD,
HTTP_RESPONSE_STATUS_CODE_FIELD,
SPAN_TYPE_FIELD,
USER_AGENT_NAME_FIELD,
TraceDocumentOverview,
SPAN_SUBTYPE_FIELD,
USER_AGENT_VERSION_FIELD,
AGENT_NAME_FIELD,
SERVICE_ENVIRONMENT_FIELD,
} from '@kbn/discover-utils';
import { i18n } from '@kbn/i18n';
import React from 'react';
import { EuiBadge, EuiText } from '@elastic/eui';
import { PartialFieldMetadataPlain } from '@kbn/fields-metadata-plugin/common';
import { ServiceNameLink } from '../sub_components/service_name_link';
import { TraceIdLink } from '../sub_components/trace_id_link';
import { TransactionNameLink } from '../sub_components/transaction_name_link';
import { Timestamp } from '../sub_components/timestamp';
import { DependencyNameLink } from '../sub_components/dependency_name_link';
import { HttpStatusCode } from '../sub_components/http_status_code';
import { asDuration } from '../utils';
type FieldConfigValue = string | number | undefined;
export interface FieldConfiguration {
title: string;
content: (value: FieldConfigValue) => React.ReactNode;
value: FieldConfigValue;
fieldMetadata?: PartialFieldMetadataPlain;
}
export const getFieldConfiguration = (
attributes: TraceDocumentOverview
): Record<string, FieldConfiguration> => {
return {
[SPAN_NAME_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanName.title', {
defaultMessage: 'Span name',
}),
content: (value) => <EuiText size="xs">{value}</EuiText>,
value: attributes[SPAN_NAME_FIELD],
},
[TRANSACTION_NAME_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.transactionName.title', {
defaultMessage: 'Transaction name',
}),
content: (value) => (
<TransactionNameLink
serviceName={attributes[SERVICE_NAME_FIELD]}
transactionName={value as string}
/>
),
value: attributes[TRANSACTION_NAME_FIELD],
},
[SERVICE_NAME_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.service.title', {
defaultMessage: 'Service',
}),
content: (value) => (
<ServiceNameLink serviceName={value as string} agentName={attributes[AGENT_NAME_FIELD]} />
),
value: attributes[SERVICE_NAME_FIELD],
},
[TRACE_ID_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.traceId.title', {
defaultMessage: 'Trace ID',
}),
content: (value) => <TraceIdLink traceId={value as string} />,
value: attributes[TRACE_ID_FIELD],
},
[SPAN_DESTINATION_SERVICE_RESOURCE_FIELD]: {
title: i18n.translate(
'unifiedDocViewer.tracesOverview.details.spanDestinationServiceResource.title',
{
defaultMessage: 'Dependency',
}
),
content: (value) => (
<DependencyNameLink
dependencyName={value as string}
environment={attributes[SERVICE_ENVIRONMENT_FIELD]}
/>
),
value: attributes[SPAN_DESTINATION_SERVICE_RESOURCE_FIELD],
fieldMetadata: {
flat_name: 'span.destination.service.resource',
},
},
[TIMESTAMP_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.timestamp.title', {
defaultMessage: 'Start time',
}),
content: (value) => <Timestamp timestamp={value as number} />,
value: attributes[TIMESTAMP_FIELD],
},
[SPAN_DURATION_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanDuration.title', {
defaultMessage: 'Duration',
}),
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
value: attributes[SPAN_DURATION_FIELD] ?? 0,
},
[TRANSACTION_DURATION_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.transactionDuration.title', {
defaultMessage: 'Duration',
}),
content: (value) => <EuiText size="xs">{asDuration(value as number)}</EuiText>,
value: attributes[TRANSACTION_DURATION_FIELD] ?? 0,
},
[SPAN_TYPE_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanType.title', {
defaultMessage: 'Type',
}),
content: (value) => <EuiBadge color="hollow">{value}</EuiBadge>,
value: attributes[SPAN_TYPE_FIELD],
},
[SPAN_SUBTYPE_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.spanSubtype.title', {
defaultMessage: 'Subtype',
}),
content: (value) => <EuiBadge color="hollow">{value}</EuiBadge>,
value: attributes[SPAN_SUBTYPE_FIELD],
},
[HTTP_RESPONSE_STATUS_CODE_FIELD]: {
title: i18n.translate(
'unifiedDocViewer.tracesOverview.details.httpResponseStatusCode.title',
{
defaultMessage: 'Status code',
}
),
content: (value) => <HttpStatusCode code={value as number} />,
value: attributes[HTTP_RESPONSE_STATUS_CODE_FIELD],
},
[USER_AGENT_NAME_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.userAgent.title', {
defaultMessage: 'User agent',
}),
content: (value) => <EuiText size="xs">{value}</EuiText>,
value: attributes[USER_AGENT_NAME_FIELD],
},
[USER_AGENT_VERSION_FIELD]: {
title: i18n.translate('unifiedDocViewer.tracesOverview.details.userAgentVersion.title', {
defaultMessage: 'User agent version',
}),
content: (value) => <EuiText size="xs">{value}</EuiText>,
value: attributes[USER_AGENT_VERSION_FIELD],
},
};
};

View file

@ -0,0 +1,76 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
import { getRouterLinkProps } from '@kbn/router-utils';
import { DEPENDENCY_OVERVIEW_LOCATOR_ID } from '@kbn/deeplinks-observability';
import { getUnifiedDocViewerServices } from '../../../plugin';
interface DependencyNameLinkProps {
dependencyName: string;
environment: string;
}
export function DependencyNameLink({ dependencyName, environment }: DependencyNameLinkProps) {
const {
share: { url: urlService },
core,
data: dataService,
} = getUnifiedDocViewerServices();
const canViewApm = core.application.capabilities.apm?.show || false;
const { from: timeRangeFrom, to: timeRangeTo } =
dataService.query.timefilter.timefilter.getTime();
const apmLinkToDependencyOverviewLocator = urlService.locators.get<{
dependencyName: string;
environment: string;
rangeFrom: string;
rangeTo: string;
}>(DEPENDENCY_OVERVIEW_LOCATOR_ID);
const href = apmLinkToDependencyOverviewLocator?.getRedirectUrl({
dependencyName,
environment,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
const routeLinkProps = href
? getRouterLinkProps({
href,
onClick: () => {
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
apmLinkToDependencyOverviewLocator?.navigate({
dependencyName,
environment,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
},
})
: undefined;
const content = (
<EuiFlexGroup gutterSize="xs" alignItems="center">
<EuiFlexItem>
<EuiText size="xs">{dependencyName}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
return canViewApm && routeLinkProps ? (
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewDependencyNameLink">
{content}
</EuiLink>
) : (
content
);
}

View file

@ -0,0 +1,104 @@
/*
* 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 React, { useEffect, useRef, useState } from 'react';
import {
EuiFlexGroup,
EuiPopover,
EuiButtonIcon,
EuiPopoverTitle,
EuiToolTip,
PopoverAnchorPosition,
type EuiPopoverProps,
} from '@elastic/eui';
import { useUIFieldActions } from '../../../../hooks/use_field_actions';
interface HoverPopoverActionProps {
children: React.ReactChild;
field: string;
value: unknown;
formattedValue?: string;
title: string;
anchorPosition?: PopoverAnchorPosition;
display?: EuiPopoverProps['display'];
}
export const FieldHoverActionPopover = ({
children,
title,
field,
value,
formattedValue,
anchorPosition = 'upCenter',
display = 'inline-block',
}: HoverPopoverActionProps) => {
const [isPopoverOpen, setIsPopoverOpen] = useState(false);
const leaveTimer = useRef<NodeJS.Timeout | null>(null);
const uiFieldActions = useUIFieldActions({ field, value, formattedValue });
const clearTimeoutIfExists = () => {
if (leaveTimer.current) {
clearTimeout(leaveTimer.current);
}
};
// The timeout hack is required because we are using a Popover which ideally should be used with a mouseclick,
// but we are using it as a Tooltip. Which means we now need to manually handle the open and close
// state using the mouse hover events. This cause the popover to close even before the user could
// navigate actions inside it. Hence, to prevent this, we need this hack
const onMouseEnter = () => {
clearTimeoutIfExists();
setIsPopoverOpen(true);
};
const onMouseLeave = () => {
leaveTimer.current = setTimeout(() => {
return setIsPopoverOpen(false);
}, 100);
};
useEffect(function onUnmount() {
return () => {
clearTimeoutIfExists();
};
}, []);
return (
<span onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
<EuiPopover
button={children}
isOpen={isPopoverOpen}
anchorPosition={anchorPosition}
closePopover={closePopoverPlaceholder}
panelPaddingSize="s"
panelStyle={{ minWidth: '24px' }}
display={display}
>
<EuiPopoverTitle className="eui-textBreakWord" css={{ maxWidth: '200px' }}>
{title}
</EuiPopoverTitle>
<EuiFlexGroup wrap gutterSize="none" alignItems="center" justifyContent="spaceBetween">
{uiFieldActions.map((action) => (
<EuiToolTip content={action.label} key={action.id}>
<EuiButtonIcon
data-test-subj="unifiedDocViewTracesOverviewFieldHoverActionPopoverButton"
size="xs"
iconType={action.iconType}
aria-label={action.label}
onClick={action.onClick}
/>
</EuiToolTip>
))}
</EuiFlexGroup>
</EuiPopover>
</span>
);
};
const closePopoverPlaceholder = () => {};

View file

@ -0,0 +1,70 @@
/*
* 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 { EuiFlexGroup, EuiFlexItem, EuiIconTip, EuiLoadingSpinner, EuiTitle } from '@elastic/eui';
import React from 'react';
import { PartialFieldMetadataPlain } from '@kbn/fields-metadata-plugin/common';
import { FieldHoverActionPopover } from './field_hover_popover_action';
export interface FieldWithActionsProps {
field: string;
fieldMetadata?: PartialFieldMetadataPlain;
formattedValue: string;
label: string;
value: string;
children: React.ReactNode;
loading?: boolean;
}
export function FieldWithActions({
field,
fieldMetadata,
formattedValue,
label,
value,
loading,
children,
...props
}: FieldWithActionsProps) {
const hasFieldDescription = !!fieldMetadata?.flat_name;
if (!label) {
return null;
}
return (
<div {...props}>
<EuiFlexGroup>
<EuiFlexItem grow={1}>
<EuiFlexGroup alignItems="center" gutterSize="xs">
<EuiFlexItem grow={false}>
<EuiTitle size="xxxs">
<h3>{label}</h3>
</EuiTitle>
</EuiFlexItem>
{hasFieldDescription && (
<EuiFlexItem grow={false}>
<EuiIconTip content={fieldMetadata.flat_name} color="subdued" />
</EuiFlexItem>
)}
</EuiFlexGroup>
</EuiFlexItem>
<EuiFlexItem grow={2}>
<FieldHoverActionPopover title={value} value={value} field={field}>
<div className="eui-textBreakWord">
{loading && <EuiLoadingSpinner size="m" />}
{children}
</div>
</FieldHoverActionPopover>
</EuiFlexItem>
</EuiFlexGroup>
</div>
);
}

View file

@ -0,0 +1,120 @@
/*
* 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 React from 'react';
import { render, screen } from '@testing-library/react';
import { useEuiTheme } from '@elastic/eui';
import { HttpStatusCode } from '.';
import { httpStatusCodes } from './http_status_codes';
jest.mock('@elastic/eui', () => ({
...jest.requireActual('@elastic/eui'),
useEuiTheme: jest.fn(),
}));
const euiColorVisGrey0 = '111';
const euiColorVisSuccess0 = '222';
const euiColorVisWarning0 = '333';
const euiColorVisDanger0 = '444';
const expectTextInBadge = (text: string) =>
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeText')?.innerHTML).toEqual(
text
);
describe('HttpStatusCode', () => {
beforeEach(() => {
(useEuiTheme as jest.Mock).mockReturnValue({
euiTheme: {
colors: {
vis: {
euiColorVisGrey0,
euiColorVisSuccess0,
euiColorVisWarning0,
euiColorVisDanger0,
},
},
},
});
});
afterEach(() => {
jest.clearAllMocks();
});
it('should render the status code and its description correctly', () => {
const code = 200;
render(<HttpStatusCode code={code} />);
expectTextInBadge(`${code} ${httpStatusCodes[code.toString()]}`);
});
it('should return correct color for status codes starting with 1', () => {
const code = 100;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisGrey0}`
);
});
it('should return correct color for status codes starting with 2', () => {
const code = 200;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisSuccess0}`
);
});
it('should return correct color for status codes starting with 3', () => {
const code = 300;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisGrey0}`
);
});
it('should return correct color for status codes starting with 4', () => {
const code = 400;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisWarning0}`
);
});
it('should return correct color for status codes starting with 5', () => {
const code = 500;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisDanger0}`
);
});
it('should return correct color for status codes starting with 7', () => {
const code = 700;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
`background-color: ${euiColorVisDanger0}`
);
});
it('should return default color for unknown status codes', () => {
const code = 999;
render(<HttpStatusCode code={code} />);
expect(screen.queryByTestId('docViewerTracesOverviewHttpStatusCodeBadge')).toHaveStyle(
'background-color: default'
);
});
});

View file

@ -0,0 +1,74 @@
/*
* 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".
*/
// From https://github.com/for-GET/know-your-http-well/blob/master/json/status-codes.json
export const httpStatusCodes: { [key: string]: string } = {
'100': 'Continue',
'101': 'Switching Protocols',
'102': 'Processing',
'200': 'OK',
'201': 'Created',
'202': 'Accepted',
'203': 'Non-Authoritative Information',
'204': 'No Content',
'205': 'Reset Content',
'206': 'Partial Content',
'207': 'Multi-Status',
'226': 'IM Used',
'300': 'Multiple Choices',
'301': 'Moved Permanently',
'302': 'Found',
'303': 'See Other',
'304': 'Not Modified',
'305': 'Use Proxy',
'307': 'Temporary Redirect',
'308': 'Permanent Redirect',
'400': 'Bad Request',
'401': 'Unauthorized',
'402': 'Payment Required',
'403': 'Forbidden',
'404': 'Not Found',
'405': 'Method Not Allowed',
'406': 'Not Acceptable',
'407': 'Proxy Authentication Required',
'408': 'Request Timeout',
'409': 'Conflict',
'410': 'Gone',
'411': 'Length Required',
'412': 'Precondition Failed',
'413': 'Payload Too Large',
'414': 'URI Too Long',
'415': 'Unsupported Media Type',
'416': 'Range Not Satisfiable',
'417': 'Expectation Failed',
'418': "I'm a teapot",
'422': 'Unprocessable Entity',
'423': 'Locked',
'424': 'Failed Dependency',
'426': 'Upgrade Required',
'428': 'Precondition Required',
'429': 'Too Many Requests',
'431': 'Request Header Fields Too Large',
'451': 'Unavailable For Legal Reasons',
'500': 'Internal Server Error',
'501': 'Not Implemented',
'502': 'Bad Gateway',
'503': 'Service Unavailable',
'504': 'Gateway Time-out',
'505': 'HTTP Version Not Supported',
'506': 'Variant Also Negotiates',
'507': 'Insufficient Storage',
'511': 'Network Authentication Required',
'1xx': '**Informational**',
'2xx': '**Successful**',
'3xx': '**Redirection**',
'4xx': '**Client Error**',
'5xx': '**Server Error**',
'7xx': '**Developer Error**',
};

View file

@ -0,0 +1,31 @@
/*
* 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 React from 'react';
import { EuiBadge, EuiText } from '@elastic/eui';
import { httpStatusCodes } from './http_status_codes';
import { useGetHttpStatusColor } from './use_get_http_status_color';
interface HttpStatusCodeProps {
code: number;
}
export function HttpStatusCode({ code }: HttpStatusCodeProps) {
return (
<EuiBadge
color={useGetHttpStatusColor(code)}
data-test-subj="docViewerTracesOverviewHttpStatusCodeBadge"
>
<EuiText size="xs" data-test-subj="docViewerTracesOverviewHttpStatusCodeText">
{code} {httpStatusCodes[code.toString()]}
</EuiText>
</EuiBadge>
);
}

View file

@ -0,0 +1,25 @@
/*
* 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 { useEuiTheme } from '@elastic/eui';
export const useGetHttpStatusColor = (status: string | number) => {
const { euiTheme } = useEuiTheme();
const httpStatusCodeColors: Record<string, string> = {
1: euiTheme.colors.vis.euiColorVisGrey0,
2: euiTheme.colors.vis.euiColorVisSuccess0,
3: euiTheme.colors.vis.euiColorVisGrey0,
4: euiTheme.colors.vis.euiColorVisWarning0,
5: euiTheme.colors.vis.euiColorVisDanger0,
7: euiTheme.colors.vis.euiColorVisGrey0,
};
const firstStatusDigit = String(status).charAt(0);
return httpStatusCodeColors[firstStatusDigit] || 'default';
};

View file

@ -0,0 +1,85 @@
/*
* 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 React from 'react';
import { EuiFlexGroup, EuiFlexItem, EuiLink, EuiText } from '@elastic/eui';
import { getRouterLinkProps } from '@kbn/router-utils';
import { AgentIcon } from '@kbn/custom-icons';
import { AgentName } from '@kbn/elastic-agent-utils';
import { getUnifiedDocViewerServices } from '../../../plugin';
const SERVICE_OVERVIEW_LOCATOR_ID = 'serviceOverviewLocator';
interface ServiceNameLinkProps {
serviceName: string;
agentName: string;
}
export function ServiceNameLink({ serviceName, agentName }: ServiceNameLinkProps) {
const {
share: { url: urlService },
core,
data: dataService,
} = getUnifiedDocViewerServices();
const canViewApm = core.application.capabilities.apm?.show || false;
const { from: timeRangeFrom, to: timeRangeTo } =
dataService.query.timefilter.timefilter.getTime();
const apmLinkToServiceEntityLocator = urlService.locators.get<{
serviceName: string;
rangeFrom: string;
rangeTo: string;
}>(SERVICE_OVERVIEW_LOCATOR_ID);
const href = apmLinkToServiceEntityLocator?.getRedirectUrl({
serviceName,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
const routeLinkProps = href
? getRouterLinkProps({
href,
onClick: () => {
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
apmLinkToServiceEntityLocator?.navigate({
serviceName,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
},
})
: undefined;
const content = (
<EuiFlexGroup gutterSize="xs" alignItems="center">
{agentName && (
<EuiFlexItem grow={false}>
<AgentIcon agentName={agentName as AgentName} size="m" />
</EuiFlexItem>
)}
<EuiFlexItem>
<EuiText size="xs">{serviceName}</EuiText>
</EuiFlexItem>
</EuiFlexGroup>
);
return (
<>
{canViewApm && routeLinkProps ? (
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewServiceNameLink">
{content}
</EuiLink>
) : (
content
)}
</>
);
}

View file

@ -0,0 +1,38 @@
/*
* 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 React from 'react';
import { render, screen } from '@testing-library/react';
import moment from 'moment';
import { Timestamp } from './timestamp';
const timestamp = 1617549061000;
describe('Timestamp', () => {
it('should render both absolute and relative time correctly', () => {
render(<Timestamp timestamp={timestamp} />);
const absoluteTime = moment(timestamp).format('MMM D, YYYY @ HH:mm:ss');
const relativeTime = moment(timestamp).fromNow();
expect(screen.queryByTestId('docViewerTracesOverviewTimestamp')?.innerHTML).toEqual(
`${absoluteTime} (${relativeTime})`
);
});
it('should display the relative time correctly for a recent timestamp', () => {
const recentTimestamp = moment().subtract(10, 'minutes').valueOf();
render(<Timestamp timestamp={recentTimestamp} />);
const absoluteTime = moment(recentTimestamp).format('MMM D, YYYY @ HH:mm:ss');
const relativeTime = moment(recentTimestamp).fromNow();
expect(screen.queryByTestId('docViewerTracesOverviewTimestamp')?.innerHTML).toEqual(
`${absoluteTime} (${relativeTime})`
);
});
});

View file

@ -0,0 +1,28 @@
/*
* 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 React from 'react';
import { EuiText } from '@elastic/eui';
import moment from 'moment';
interface TimestampProps {
timestamp: number;
}
export function Timestamp({ timestamp }: TimestampProps) {
const momentTime = moment(timestamp);
const relativeTime = momentTime.fromNow();
const absoluteTime = momentTime.format('MMM D, YYYY @ HH:mm:ss');
return (
<EuiText size="xs" data-test-subj="docViewerTracesOverviewTimestamp">
{absoluteTime} ({relativeTime})
</EuiText>
);
}

View file

@ -0,0 +1,69 @@
/*
* 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 React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import { getRouterLinkProps } from '@kbn/router-utils';
import { TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR } from '@kbn/deeplinks-observability';
import { getUnifiedDocViewerServices } from '../../../plugin';
interface TraceIdLinkProps {
traceId: string;
}
export function TraceIdLink({ traceId }: TraceIdLinkProps) {
const {
share: { url: urlService },
core,
data: dataService,
} = getUnifiedDocViewerServices();
const canViewApm = core.application.capabilities.apm?.show || false;
const { from: timeRangeFrom, to: timeRangeTo } =
dataService.query.timefilter.timefilter.getTime();
const apmLinkToTransactionByTraceIdLocator = urlService.locators.get<{
traceId: string;
rangeFrom: string;
rangeTo: string;
}>(TRANSACTION_DETAILS_BY_TRACE_ID_LOCATOR);
const href = apmLinkToTransactionByTraceIdLocator?.getRedirectUrl({
traceId,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
const routeLinkProps = href
? getRouterLinkProps({
href,
onClick: () => {
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
apmLinkToTransactionByTraceIdLocator?.navigate({
traceId,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
},
})
: undefined;
const content = <EuiText size="xs">{traceId}</EuiText>;
return (
<>
{canViewApm && routeLinkProps ? (
<EuiLink {...routeLinkProps} data-test-subj="unifiedDocViewTracesOverviewTraceIdLink">
{content}
</EuiLink>
) : (
content
)}
</>
);
}

View file

@ -0,0 +1,57 @@
/*
* 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 { TRANSACTION_NAME_FIELD } from '@kbn/discover-utils';
import { EuiHorizontalRule } from '@elastic/eui';
import React, { useState, useEffect } from 'react';
import { useTransactionContext } from '../hooks/use_transaction';
import { FieldWithActions } from './field_with_actions/field_with_actions';
import { FieldConfiguration } from '../resources/get_field_configuration';
export interface TraceSummaryProps {
fieldId: string;
fieldConfiguration: FieldConfiguration;
}
export function TraceSummary({ fieldConfiguration, fieldId }: TraceSummaryProps) {
const { transaction, loading } = useTransactionContext();
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);
}
}, [transaction?.name, loading, fieldValue, isTransactionNameField]);
if (
(!isTransactionNameFieldWithoutValue && !fieldValue) ||
(isTransactionNameFieldWithoutValue && !loading)
) {
return null;
}
return (
<>
<FieldWithActions
data-test-subj={`unifiedDocViewTracesOverviewAttribute-${fieldId}`}
label={fieldConfiguration.title}
field={fieldId}
value={fieldValue as string}
formattedValue={fieldValue as string}
fieldMetadata={fieldConfiguration.fieldMetadata}
loading={isTransactionNameFieldWithoutValue && loading}
>
<div>{fieldConfiguration.content(fieldValue)}</div>
</FieldWithActions>
<EuiHorizontalRule margin="xs" />
</>
);
}

View file

@ -0,0 +1,76 @@
/*
* 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 React from 'react';
import { EuiLink, EuiText } from '@elastic/eui';
import { getRouterLinkProps } from '@kbn/router-utils';
import { TRANSACTION_DETAILS_BY_NAME_LOCATOR } from '@kbn/deeplinks-observability';
import { getUnifiedDocViewerServices } from '../../../plugin';
interface TransactionNameLinkProps {
serviceName: string;
transactionName: string;
}
export function TransactionNameLink({ transactionName, serviceName }: TransactionNameLinkProps) {
const {
share: { url: urlService },
core,
data: dataService,
} = getUnifiedDocViewerServices();
const canViewApm = core.application.capabilities.apm?.show || false;
const { from: timeRangeFrom, to: timeRangeTo } =
dataService.query.timefilter.timefilter.getTime();
const apmLinkToTransactionByNameLocator = urlService.locators.get<{
serviceName: string;
transactionName: string;
rangeFrom: string;
rangeTo: string;
}>(TRANSACTION_DETAILS_BY_NAME_LOCATOR);
const href = apmLinkToTransactionByNameLocator?.getRedirectUrl({
serviceName,
transactionName,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
const routeLinkProps = href
? getRouterLinkProps({
href,
onClick: () => {
// TODO add telemetry (https://github.com/elastic/kibana/issues/208919)
apmLinkToTransactionByNameLocator?.navigate({
serviceName,
transactionName,
rangeFrom: timeRangeFrom,
rangeTo: timeRangeTo,
});
},
})
: undefined;
const content = <EuiText size="xs">{transactionName}</EuiText>;
return (
<>
{canViewApm && routeLinkProps ? (
<EuiLink
{...routeLinkProps}
data-test-subj="unifiedDocViewTracesOverviewTransactionNameLink"
>
{content}
</EuiLink>
) : (
content
)}
</>
);
}

View file

@ -0,0 +1,78 @@
/*
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
* or more contributor license agreements. Licensed under the "Elastic License
* 2.0", 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 React from 'react';
import { DocViewRenderProps } from '@kbn/unified-doc-viewer/types';
import { EuiPanel, EuiSpacer, EuiTitle } from '@elastic/eui';
import { i18n } from '@kbn/i18n';
import { PROCESSOR_EVENT_FIELD, getTraceDocumentOverview } from '@kbn/discover-utils';
import { spanFields, transactionFields } from './resources/fields';
import { getFieldConfiguration } from './resources/get_field_configuration';
import { FieldActionsProvider } from '../../hooks/use_field_actions';
import { TransactionProvider } from './hooks/use_transaction';
import { TraceSummary } from './sub_components/trace_summary';
export type TracesOverviewProps = DocViewRenderProps;
export function TracesOverview({
columns,
dataView,
hit,
filter,
onAddColumn,
onRemoveColumn,
}: TracesOverviewProps) {
const parsedDoc = getTraceDocumentOverview(hit);
const isTransaction = parsedDoc[PROCESSOR_EVENT_FIELD] === 'transaction';
const detailType = isTransaction
? i18n.translate('unifiedDocViewer.docViewerTracesOverview.transactionTitle', {
defaultMessage: 'Transaction',
})
: i18n.translate('unifiedDocViewer.docViewerTracesOverview.spanTitle', {
defaultMessage: 'Span',
});
const detailTitle = i18n.translate('unifiedDocViewer.docViewerTracesOverview.title', {
defaultMessage: '{detailType} detail',
values: { detailType },
});
return (
<TransactionProvider
transactionId={parsedDoc['transaction.id']}
indexPattern={dataView.getIndexPattern()}
>
<FieldActionsProvider
columns={columns}
filter={filter}
onAddColumn={onAddColumn}
onRemoveColumn={onRemoveColumn}
>
<EuiPanel color="transparent" hasShadow={false} paddingSize="none">
<EuiSpacer size="m" />
<EuiTitle size="s">
<h2>{detailTitle}</h2>
</EuiTitle>
<EuiSpacer size="m" />
{(isTransaction ? transactionFields : spanFields).map((fieldId) => {
const fieldConfiguration = getFieldConfiguration(parsedDoc)[fieldId];
return (
<TraceSummary
key={fieldId}
fieldId={fieldId}
fieldConfiguration={fieldConfiguration}
/>
);
})}
</EuiPanel>
</FieldActionsProvider>
</TransactionProvider>
);
}

View file

@ -0,0 +1,10 @@
/*
* 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".
*/
export type Maybe<T> = T | null | undefined;

View file

@ -0,0 +1,10 @@
/*
* 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".
*/
export * from './common';

View file

@ -0,0 +1,45 @@
/*
* 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 { asDuration, toMicroseconds } from './duration';
describe('duration formatters', () => {
describe('asDuration', () => {
it('formats correctly with defaults', () => {
expect(asDuration(null)).toEqual('N/A');
expect(asDuration(undefined)).toEqual('N/A');
expect(asDuration(0)).toEqual('0 μs');
expect(asDuration(1)).toEqual('1 μs');
expect(asDuration(1)).toEqual('1 μs');
expect(asDuration(toMicroseconds(1, 'milliseconds'))).toEqual('1,000 μs');
expect(asDuration(toMicroseconds(1000, 'milliseconds'))).toEqual('1,000 ms');
expect(asDuration(toMicroseconds(10000, 'milliseconds'))).toEqual('10,000 ms');
expect(asDuration(toMicroseconds(20, 'seconds'))).toEqual('20 s');
expect(asDuration(toMicroseconds(10, 'minutes'))).toEqual('600 s');
expect(asDuration(toMicroseconds(11, 'minutes'))).toEqual('11 min');
expect(asDuration(toMicroseconds(1, 'hours'))).toEqual('60 min');
expect(asDuration(toMicroseconds(1.5, 'hours'))).toEqual('90 min');
expect(asDuration(toMicroseconds(10, 'hours'))).toEqual('600 min');
expect(asDuration(toMicroseconds(11, 'hours'))).toEqual('11 h');
});
it('falls back to default value', () => {
expect(asDuration(undefined, { defaultValue: 'nope' })).toEqual('nope');
});
});
describe('toMicroseconds', () => {
it('transformes to microseconds', () => {
expect(toMicroseconds(1, 'hours')).toEqual(3600000000);
expect(toMicroseconds(10, 'minutes')).toEqual(600000000);
expect(toMicroseconds(10, 'seconds')).toEqual(10000000);
expect(toMicroseconds(10, 'milliseconds')).toEqual(10000);
});
});
});

View file

@ -0,0 +1,162 @@
/*
* 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 { i18n } from '@kbn/i18n';
import moment from 'moment';
import { memoize, isFinite } from 'lodash';
import { asDecimalOrInteger, asInteger, NOT_AVAILABLE_LABEL } from './numeric';
import { Maybe } from '../../typings';
type TimeUnit = 'hours' | 'minutes' | 'seconds' | 'milliseconds';
type DurationTimeUnit = TimeUnit | 'microseconds';
interface FormatterOptions {
defaultValue?: string;
}
interface ConvertedDuration {
value: string;
unit?: string;
formatted: string;
}
export type TimeFormatter = (value: number, options?: FormatterOptions) => ConvertedDuration;
type TimeFormatterBuilder = (
max: number,
threshold?: number,
scalingFactor?: number
) => TimeFormatter;
export const toMicroseconds = (value: number, timeUnit: TimeUnit) =>
moment.duration(value, timeUnit).asMilliseconds() * 1000;
function getUnitLabelAndConvertedValue(
unitKey: DurationTimeUnit,
value: number,
threshold: number = 10
) {
const ms = value / 1000;
switch (unitKey) {
case 'hours': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.hoursTimeUnitLabel', {
defaultMessage: 'h',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asHours(), threshold),
};
}
case 'minutes': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.minutesTimeUnitLabel', {
defaultMessage: 'min',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asMinutes(), threshold),
};
}
case 'seconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.secondsTimeUnitLabel', {
defaultMessage: 's',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asSeconds(), threshold),
};
}
case 'milliseconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.millisTimeUnitLabel', {
defaultMessage: 'ms',
}),
convertedValue: asDecimalOrInteger(moment.duration(ms).asMilliseconds(), threshold),
};
}
case 'microseconds': {
return {
unitLabel: i18n.translate('unifiedDocViewer.formatters.duration.microsTimeUnitLabel', {
defaultMessage: 'μs',
}),
convertedValue: asInteger(value),
};
}
}
}
function convertTo({
unit,
microseconds,
defaultValue = NOT_AVAILABLE_LABEL,
threshold = 10,
}: {
unit: DurationTimeUnit;
microseconds: number;
defaultValue?: string;
threshold?: number;
}): ConvertedDuration {
if (!isFinite(microseconds)) {
return { value: defaultValue, formatted: defaultValue };
}
const { convertedValue, unitLabel } = getUnitLabelAndConvertedValue(
unit,
microseconds,
threshold
);
return {
value: convertedValue,
unit: unitLabel,
formatted: `${convertedValue} ${unitLabel}`,
};
}
function getDurationUnitKey(max: number, threshold = 10): DurationTimeUnit {
if (max > toMicroseconds(threshold, 'hours')) {
return 'hours';
}
if (max > toMicroseconds(threshold, 'minutes')) {
return 'minutes';
}
if (max > toMicroseconds(threshold, 'seconds')) {
return 'seconds';
}
if (max > toMicroseconds(1, 'milliseconds')) {
return 'milliseconds';
}
return 'microseconds';
}
// memoizer with a custom resolver to consider both arguments max/threshold.
// by default lodash's memoize only considers the first argument.
const getDurationFormatter: TimeFormatterBuilder = memoize(
(max: number, threshold: number = 10, scalingFactor: number = 1) => {
const unit = getDurationUnitKey(max, threshold);
return (value: number, { defaultValue }: FormatterOptions = {}) => {
return convertTo({
unit,
microseconds: isFinite(value) ? value * scalingFactor : value,
defaultValue,
threshold,
});
};
},
(max, threshold) => `${max}_${threshold}`
);
export function asDuration(
value: Maybe<number>,
{ defaultValue = NOT_AVAILABLE_LABEL }: FormatterOptions = {}
) {
if (value === null || value === undefined || !isFinite(value)) {
return defaultValue;
}
const formatter = getDurationFormatter(value);
return formatter(value, { defaultValue }).formatted;
}

View file

@ -0,0 +1,11 @@
/*
* 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".
*/
export * from './duration';
export * from './numeric';

View file

@ -0,0 +1,110 @@
/*
* 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 { asDecimal, asInteger, asDecimalOrInteger } from './numeric';
describe('formatters', () => {
describe('asDecimal', () => {
it.each([
[Infinity, 'N/A'],
[-Infinity, 'N/A'],
[null, 'N/A'],
[undefined, 'N/A'],
[NaN, 'N/A'],
])(
'displays the not available label when the number is not finite',
(value, formattedValue) => {
expect(asDecimal(value)).toBe(formattedValue);
}
);
it.each([
[0, '0.0'],
[0.005, '0.0'],
[1.23, '1.2'],
[12.34, '12.3'],
[123.45, '123.5'],
[1234.56, '1,234.6'],
[1234567.89, '1,234,567.9'],
])('displays the correct label when the number is finite', (value, formattedValue) => {
expect(asDecimal(value)).toBe(formattedValue);
});
});
describe('asInteger', () => {
it.each([
[Infinity, 'N/A'],
[-Infinity, 'N/A'],
[null, 'N/A'],
[undefined, 'N/A'],
[NaN, 'N/A'],
])(
'displays the not available label when the number is not finite',
(value, formattedValue) => {
expect(asInteger(value)).toBe(formattedValue);
}
);
it.each([
[0, '0'],
[0.005, '0'],
[1.23, '1'],
[12.34, '12'],
[123.45, '123'],
[1234.56, '1,235'],
[1234567.89, '1,234,568'],
])('displays the correct label when the number is finite', (value, formattedValue) => {
expect(asInteger(value)).toBe(formattedValue);
});
});
describe('asDecimalOrInteger', () => {
describe('with default threshold of 10', () => {
it('formats as integer when number equals to 0 ', () => {
expect(asDecimalOrInteger(0)).toEqual('0');
});
it('formats as integer when number is above or equals 10 ', () => {
expect(asDecimalOrInteger(10.123)).toEqual('10');
expect(asDecimalOrInteger(15.123)).toEqual('15');
});
it('formats as decimal when number is below 10 ', () => {
expect(asDecimalOrInteger(0.25435632645)).toEqual('0.3');
expect(asDecimalOrInteger(1)).toEqual('1.0');
expect(asDecimalOrInteger(3.374329704990765)).toEqual('3.4');
expect(asDecimalOrInteger(5)).toEqual('5.0');
expect(asDecimalOrInteger(9)).toEqual('9.0');
});
});
describe('with custom threshold of 1', () => {
it('formats as integer when number equals to 0 ', () => {
expect(asDecimalOrInteger(0, 1)).toEqual('0');
});
it('formats as integer when number is above or equals 1 ', () => {
expect(asDecimalOrInteger(1, 1)).toEqual('1');
expect(asDecimalOrInteger(1.123, 1)).toEqual('1');
expect(asDecimalOrInteger(3.374329704990765, 1)).toEqual('3');
expect(asDecimalOrInteger(5, 1)).toEqual('5');
expect(asDecimalOrInteger(9, 1)).toEqual('9');
expect(asDecimalOrInteger(10, 1)).toEqual('10');
expect(asDecimalOrInteger(10.123, 1)).toEqual('10');
expect(asDecimalOrInteger(15.123, 1)).toEqual('15');
});
it('formats as decimal when number is below 1 ', () => {
expect(asDecimalOrInteger(0.25435632645, 1)).toEqual('0.3');
});
});
it('returns fallback when valueNaN', () => {
expect(asDecimalOrInteger(NaN)).toEqual('N/A');
expect(asDecimalOrInteger(null)).toEqual('N/A');
expect(asDecimalOrInteger(undefined)).toEqual('N/A');
});
});
});

View file

@ -0,0 +1,47 @@
/*
* 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 { i18n } from '@kbn/i18n';
import { isFinite } from 'lodash';
import numeral from '@elastic/numeral';
import { Maybe } from '../../typings';
export const NOT_AVAILABLE_LABEL = i18n.translate(
'unifiedDocViewer.formatters.numeric.notAvailableLabel',
{
defaultMessage: 'N/A',
}
);
export function asDecimal(value: Maybe<number> | null) {
if (!isFinite(value)) {
return NOT_AVAILABLE_LABEL;
}
return numeral(value).format('0,0.0');
}
export function asInteger(value: Maybe<number> | null) {
if (!isFinite(value)) {
return NOT_AVAILABLE_LABEL;
}
return numeral(value).format('0,0');
}
export function asDecimalOrInteger(value: Maybe<number>, threshold = 10) {
if (!isFinite(value)) {
return NOT_AVAILABLE_LABEL;
}
if (value === null || value === undefined || value === 0 || value >= threshold) {
return asInteger(value);
}
return asDecimal(value);
}

View file

@ -0,0 +1,10 @@
/*
* 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".
*/
export * from './formatters';

View file

@ -0,0 +1,23 @@
/*
* 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 React from 'react';
import { EuiDelayRender, EuiSkeletonText } from '@elastic/eui';
import { dynamic } from '@kbn/shared-ux-utility';
export const UnifiedDocViewerTracesOverview = dynamic(
() => import('./doc_viewer_traces_overview'),
{
fallback: (
<EuiDelayRender delay={300}>
<EuiSkeletonText />
</EuiDelayRender>
),
}
);

View file

@ -33,4 +33,6 @@ export { UnifiedDocViewerFlyout } from './components/lazy_doc_viewer_flyout';
export type { LogsOverviewProps as UnifiedDocViewerLogsOverviewProps } from './components/doc_viewer_logs_overview/logs_overview';
export { UnifiedDocViewerLogsOverview } from './components/lazy_doc_viewer_logs_overview';
export { UnifiedDocViewerTracesOverview } from './components/lazy_doc_viewer_traces_overview';
export const plugin = () => new UnifiedDocViewerPublicPlugin();

View file

@ -1,9 +1,9 @@
{
"extends": "../../../../../tsconfig.base.json",
"compilerOptions": {
"outDir": "target/types",
"outDir": "target/types"
},
"include": [ "../../../../../typings/**/*", "common/**/*", "public/**/*", "server/**/*"],
"include": ["common/**/*", "public/**/*", "server/**/*"],
"kbn_references": [
"@kbn/kibana-react-plugin",
"@kbn/monaco",
@ -39,10 +39,8 @@
"@kbn/management-settings-ids",
"@kbn/apm-types",
"@kbn/event-stacktrace",
"@kbn/elastic-agent-utils",
"@kbn/data-view-utils"
],
"exclude": [
"target/**/*",
]
"exclude": ["target/**/*"]
}

View file

@ -167,7 +167,6 @@ export const LOGS_EXPLORER_FEEDBACK_LINK = 'https://ela.st/explorer-feedback';
export type {
ServiceOverviewParams,
ServiceOverviewLocator,
TransactionDetailsByNameParams,
TransactionDetailsByNameLocator,
AssetDetailsFlyoutLocator,
AssetDetailsFlyoutLocatorParams,
@ -195,6 +194,7 @@ export type {
export {
ServiceOverviewLocatorDefinition,
SERVICE_OVERVIEW_LOCATOR_ID,
DependencyOverviewLocatorDefinition,
TransactionDetailsByNameLocatorDefinition,
ASSET_DETAILS_FLYOUT_LOCATOR_ID,
AssetDetailsFlyoutLocatorDefinition,

View file

@ -0,0 +1,34 @@
/*
* 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 qs from 'query-string';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
import {
DEPENDENCY_OVERVIEW_LOCATOR_ID,
DependencyOverviewParams,
} from '@kbn/deeplinks-observability/locators';
export type DependencyOverviewLocator = LocatorPublic<DependencyOverviewParams>;
export class DependencyOverviewLocatorDefinition
implements LocatorDefinition<DependencyOverviewParams>
{
public readonly id = DEPENDENCY_OVERVIEW_LOCATOR_ID;
public readonly getLocation = async ({
rangeFrom,
rangeTo,
dependencyName,
environment,
}: DependencyOverviewParams) => {
const params = { rangeFrom, rangeTo, environment, dependencyName };
return {
app: 'apm',
path: `/dependencies/overview?${qs.stringify(params)}`,
state: {},
};
};
}

View file

@ -5,22 +5,18 @@
* 2.0.
*/
import qs from 'query-string';
import type { SerializableRecord } from '@kbn/utility-types';
import type { LocatorDefinition, LocatorPublic } from '@kbn/share-plugin/public';
export interface TransactionDetailsByNameParams extends SerializableRecord {
serviceName: string;
transactionName: string;
rangeFrom?: string;
rangeTo?: string;
}
import {
TRANSACTION_DETAILS_BY_NAME_LOCATOR,
TransactionDetailsByNameParams,
} from '@kbn/deeplinks-observability';
export type TransactionDetailsByNameLocator = LocatorPublic<TransactionDetailsByNameParams>;
export class TransactionDetailsByNameLocatorDefinition
implements LocatorDefinition<TransactionDetailsByNameParams>
{
public readonly id = 'TransactionDetailsByNameLocator';
public readonly id = TRANSACTION_DETAILS_BY_NAME_LOCATOR;
public readonly getLocation = async ({
rangeFrom,

View file

@ -6,6 +6,7 @@
*/
export * from './apm/service_overview_locator';
export * from './apm/dependency_overview_locator';
export * from './apm/transaction_details_by_name_locator';
export * from './apm/transaction_details_by_trace_id_locator';
export * from './apm/service_entity_locator';

View file

@ -49,6 +49,10 @@ import {
EntitiesInventoryLocatorDefinition,
} from '../common';
import { updateGlobalNavigation } from './services/update_global_navigation';
import {
DependencyOverviewLocator,
DependencyOverviewLocatorDefinition,
} from '../common/locators/apm/dependency_overview_locator';
export interface ObservabilitySharedSetup {
share: SharePluginSetup;
}
@ -80,6 +84,7 @@ interface ObservabilitySharedLocators {
};
apm: {
serviceOverview: ServiceOverviewLocator;
dependencyOverview: DependencyOverviewLocator;
transactionDetailsByName: TransactionDetailsByNameLocator;
transactionDetailsByTraceId: TransactionDetailsByTraceIdLocator;
serviceEntity: ServiceEntityLocator;
@ -154,6 +159,7 @@ export class ObservabilitySharedPlugin implements Plugin {
},
apm: {
serviceOverview: urlService.locators.create(new ServiceOverviewLocatorDefinition()),
dependencyOverview: urlService.locators.create(new DependencyOverviewLocatorDefinition()),
transactionDetailsByName: urlService.locators.create(
new TransactionDetailsByNameLocatorDefinition()
),