diff --git a/src/core/types/elasticsearch/search.ts b/src/core/types/elasticsearch/search.ts index 88d6cda3777dd..a54f5f3758ce3 100644 --- a/src/core/types/elasticsearch/search.ts +++ b/src/core/types/elasticsearch/search.ts @@ -48,7 +48,7 @@ type ValueTypeOfField = T extends Record type MaybeArray = T | T[]; -type Fields = Exclude['body']['fields'], undefined>; +type Fields = Required['body']>['fields']; type DocValueFields = MaybeArray; export type SearchHit< diff --git a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap index c4658ae2ac22c..2d1433324858b 100644 --- a/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap +++ b/x-pack/plugins/apm/common/__snapshots__/elasticsearch_fieldnames.test.ts.snap @@ -53,6 +53,8 @@ exports[`Error ERROR_EXC_TYPE 1`] = `undefined`; exports[`Error ERROR_GROUP_ID 1`] = `"grouping key"`; +exports[`Error ERROR_ID 1`] = `"error id"`; + exports[`Error ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Error ERROR_LOG_MESSAGE 1`] = `undefined`; @@ -298,6 +300,8 @@ exports[`Span ERROR_EXC_TYPE 1`] = `undefined`; exports[`Span ERROR_GROUP_ID 1`] = `undefined`; +exports[`Span ERROR_ID 1`] = `undefined`; + exports[`Span ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Span ERROR_LOG_MESSAGE 1`] = `undefined`; @@ -535,6 +539,8 @@ exports[`Transaction ERROR_EXC_TYPE 1`] = `undefined`; exports[`Transaction ERROR_GROUP_ID 1`] = `undefined`; +exports[`Transaction ERROR_ID 1`] = `undefined`; + exports[`Transaction ERROR_LOG_LEVEL 1`] = `undefined`; exports[`Transaction ERROR_LOG_MESSAGE 1`] = `undefined`; diff --git a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts index d1f07c28bc808..4a4cad5454c4b 100644 --- a/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts +++ b/x-pack/plugins/apm/common/elasticsearch_fieldnames.ts @@ -78,6 +78,7 @@ export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM = // Parent ID for a transaction or span export const PARENT_ID = 'parent.id'; +export const ERROR_ID = 'error.id'; export const ERROR_GROUP_ID = 'error.grouping_key'; export const ERROR_CULPRIT = 'error.culprit'; export const ERROR_LOG_LEVEL = 'error.log.level'; diff --git a/x-pack/plugins/apm/common/processor_event.ts b/x-pack/plugins/apm/common/processor_event.ts index 57705e7ed4ce0..fe0d9abfa0e51 100644 --- a/x-pack/plugins/apm/common/processor_event.ts +++ b/x-pack/plugins/apm/common/processor_event.ts @@ -4,6 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ +import * as t from 'io-ts'; export enum ProcessorEvent { transaction = 'transaction', @@ -12,6 +13,14 @@ export enum ProcessorEvent { span = 'span', profile = 'profile', } + +export const processorEventRt = t.union([ + t.literal(ProcessorEvent.transaction), + t.literal(ProcessorEvent.error), + t.literal(ProcessorEvent.metric), + t.literal(ProcessorEvent.span), + t.literal(ProcessorEvent.profile), +]); /** * Processor events that are searchable in the UI via the query bar. * diff --git a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx index 13aa3696eda42..e9525728bc3c5 100644 --- a/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/KeyValueTable/index.tsx @@ -4,7 +4,7 @@ * 2.0; you may not use this file except in compliance with the Elastic License * 2.0. */ - +import { castArray } from 'lodash'; import React, { TableHTMLAttributes } from 'react'; import { EuiTable, @@ -26,16 +26,32 @@ export function KeyValueTable({ return ( - {keyValuePairs.map(({ key, value }) => ( - - - {key} - - - - - - ))} + {keyValuePairs.map(({ key, value }) => { + const asArray = castArray(value); + const valueList = + asArray.length <= 1 ? ( + + ) : ( +
    + {asArray.map((val, index) => ( +
  • + +
  • + ))} +
+ ); + + return ( + + + {key} + + + {valueList} + + + ); + })}
); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx deleted file mode 100644 index f936941923e41..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/ErrorMetadata.test.tsx +++ /dev/null @@ -1,139 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { ErrorMetadata } from '.'; -import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -function getError() { - return { - labels: { someKey: 'labels value' }, - http: { someKey: 'http value' }, - host: { someKey: 'host value' }, - container: { someKey: 'container value' }, - service: { someKey: 'service value' }, - process: { someKey: 'process value' }, - agent: { someKey: 'agent value' }, - url: { someKey: 'url value' }, - user: { someKey: 'user value' }, - notIncluded: 'not included value', - error: { - id: '7efbc7056b746fcb', - notIncluded: 'error not included value', - custom: { - someKey: 'custom value', - }, - }, - } as unknown as APMError; -} - -describe('ErrorMetadata', () => { - it('should render a error with all sections', () => { - const error = getError(); - const output = render(, renderOptions); - - // sections - expectTextsInDocument(output, [ - 'Labels', - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'User', - 'Custom', - ]); - }); - - it('should render a error with all included dot notation keys', () => { - const error = getError(); - const output = render(, renderOptions); - - // included keys - expectTextsInDocument(output, [ - 'labels.someKey', - 'http.someKey', - 'host.someKey', - 'container.someKey', - 'service.someKey', - 'process.someKey', - 'agent.someKey', - 'url.someKey', - 'user.someKey', - 'error.custom.someKey', - ]); - - // excluded keys - expectTextsNotInDocument(output, ['notIncluded', 'error.notIncluded']); - }); - - it('should render a error with all included values', () => { - const error = getError(); - const output = render(, renderOptions); - - // included values - expectTextsInDocument(output, [ - 'labels value', - 'http value', - 'host value', - 'container value', - 'service value', - 'process value', - 'agent value', - 'url value', - 'user value', - 'custom value', - ]); - - // excluded values - expectTextsNotInDocument(output, [ - 'not included value', - 'error not included value', - ]); - }); - - it('should render a error with only the required sections', () => { - const error = {} as APMError; - const output = render(, renderOptions); - - // required sections should be found - expectTextsInDocument(output, ['Labels', 'User']); - - // optional sections should NOT be found - expectTextsNotInDocument(output, [ - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'Custom', - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx index 196a8706d5132..f6ffc34ecee02 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/index.tsx @@ -6,19 +6,41 @@ */ import React, { useMemo } from 'react'; -import { ERROR_METADATA_SECTIONS } from './sections'; import { APMError } from '../../../../../typings/es_schemas/ui/apm_error'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ProcessorEvent } from '../../../../../common/processor_event'; interface Props { error: APMError; } export function ErrorMetadata({ error }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(ERROR_METADATA_SECTIONS, error), - [error] + const { data: errorEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.error, + id: error.error.id, + }, + }, + }); + }, + [error.error.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(errorEvent?.metadata || {}), + [errorEvent?.metadata] + ); + + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts deleted file mode 100644 index 28a64ac36660e..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/ErrorMetadata/sections.ts +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - ERROR, - LABELS, - HTTP, - HOST, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - USER, - CUSTOM_ERROR, - TRACE, - TRANSACTION, -} from '../sections'; - -export const ERROR_METADATA_SECTIONS: Section[] = [ - { ...LABELS, required: true }, - TRACE, - TRANSACTION, - ERROR, - HTTP, - HOST, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - { ...USER, required: true }, - CUSTOM_ERROR, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx index 7ccde6a9a74d6..5d5976866ba24 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/MetadataTable.test.tsx @@ -11,7 +11,7 @@ import { MemoryRouter } from 'react-router-dom'; import { MetadataTable } from '.'; import { MockApmPluginContextWrapper } from '../../../context/apm_plugin/mock_apm_plugin_context'; import { expectTextsInDocument } from '../../../utils/testHelpers'; -import { SectionsWithRows } from './helper'; +import type { SectionDescriptor } from './types'; function Wrapper({ children }: { children?: ReactNode }) { return ( @@ -27,21 +27,20 @@ const renderOptions = { describe('MetadataTable', () => { it('shows sections', () => { - const sectionsWithRows = [ - { key: 'foo', label: 'Foo', required: true }, + const sections: SectionDescriptor[] = [ + { key: 'foo', label: 'Foo', required: true, properties: [] }, { key: 'bar', label: 'Bar', required: false, - properties: ['props.A', 'props.B'], - rows: [ - { key: 'props.A', value: 'A' }, - { key: 'props.B', value: 'B' }, + properties: [ + { field: 'props.A', value: ['A'] }, + { field: 'props.B', value: ['B'] }, ], }, - ] as unknown as SectionsWithRows; + ]; const output = render( - , + , renderOptions ); expectTextsInDocument(output, [ @@ -56,15 +55,17 @@ describe('MetadataTable', () => { }); describe('required sections', () => { it('shows "empty state message" if no data is available', () => { - const sectionsWithRows = [ + const sectionsWithRows: SectionDescriptor[] = [ { key: 'foo', label: 'Foo', required: true, + properties: [], }, - ] as unknown as SectionsWithRows; + ]; + const output = render( - , + , renderOptions ); expectTextsInDocument(output, ['Foo', 'No data available']); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx index d44464e2160d3..ed816b1c7a337 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.test.tsx @@ -12,7 +12,7 @@ import { expectTextsInDocument } from '../../../utils/testHelpers'; describe('Section', () => { it('shows "empty state message" if no data is available', () => { - const component = render(
); + const component = render(
); expectTextsInDocument(component, ['No data available']); }); }); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx index ff86083b8612d..03ae237f470c3 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/Section.tsx @@ -10,15 +10,21 @@ import { isEmpty } from 'lodash'; import { i18n } from '@kbn/i18n'; import { EuiText } from '@elastic/eui'; import { KeyValueTable } from '../KeyValueTable'; -import { KeyValuePair } from '../../../utils/flattenObject'; interface Props { - keyValuePairs: KeyValuePair[]; + properties: Array<{ field: string; value: string[] | number[] }>; } -export function Section({ keyValuePairs }: Props) { - if (!isEmpty(keyValuePairs)) { - return ; +export function Section({ properties }: Props) { + if (!isEmpty(properties)) { + return ( + ({ + key: property.field, + value: property.value, + }))} + /> + ); } return ( diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx deleted file mode 100644 index 46eaba1e9e11d..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/SpanMetadata.test.tsx +++ /dev/null @@ -1,107 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { SpanMetadata } from '.'; -import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -describe('SpanMetadata', () => { - describe('render', () => { - it('renders', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - id: '7efbc7056b746fcb', - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Message']); - }); - }); - describe('when a span is presented', () => { - it('renders the span', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - id: '7efbc7056b746fcb', - http: { - response: { status_code: 200 }, - }, - subtype: 'http', - type: 'external', - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent', 'Span', 'Message']); - }); - }); - describe('when there is no id inside span', () => { - it('does not show the section', () => { - const span = { - agent: { - ephemeral_id: 'ed8e3a4f-21d2-4a1f-bbc7-fa2064d94225', - name: 'java', - version: '1.9.1-SNAPSHOT', - }, - service: { - name: 'opbeans-java', - }, - span: { - http: { - response: { status_code: 200 }, - }, - subtype: 'http', - type: 'external', - }, - } as unknown as Span; - const output = render(, renderOptions); - expectTextsInDocument(output, ['Service', 'Agent']); - expectTextsNotInDocument(output, ['Span', 'Message']); - }); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx index feefcea9d38a0..bf5702b4acf3e 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/index.tsx @@ -6,19 +6,41 @@ */ import React, { useMemo } from 'react'; -import { SPAN_METADATA_SECTIONS } from './sections'; import { Span } from '../../../../../typings/es_schemas/ui/span'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; +import { ProcessorEvent } from '../../../../../common/processor_event'; interface Props { span: Span; } export function SpanMetadata({ span }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(SPAN_METADATA_SECTIONS, span), - [span] + const { data: spanEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.span, + id: span.span.id, + }, + }, + }); + }, + [span.span.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(spanEvent?.metadata || {}), + [spanEvent?.metadata] + ); + + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts deleted file mode 100644 index f19aef8e0bd8a..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/SpanMetadata/sections.ts +++ /dev/null @@ -1,29 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - AGENT, - SERVICE, - SPAN, - LABELS, - EVENT, - TRANSACTION, - TRACE, - MESSAGE_SPAN, -} from '../sections'; - -export const SPAN_METADATA_SECTIONS: Section[] = [ - LABELS, - TRACE, - TRANSACTION, - EVENT, - SPAN, - SERVICE, - MESSAGE_SPAN, - AGENT, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx deleted file mode 100644 index 08253f04777d9..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/TransactionMetadata.test.tsx +++ /dev/null @@ -1,164 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { render } from '@testing-library/react'; -import React, { ReactNode } from 'react'; -import { MemoryRouter } from 'react-router-dom'; -import { TransactionMetadata } from '.'; -import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { MockApmPluginContextWrapper } from '../../../../context/apm_plugin/mock_apm_plugin_context'; -import { - expectTextsInDocument, - expectTextsNotInDocument, -} from '../../../../utils/testHelpers'; - -function Wrapper({ children }: { children?: ReactNode }) { - return ( - - {children} - - ); -} - -const renderOptions = { - wrapper: Wrapper, -}; - -function getTransaction() { - return { - labels: { someKey: 'labels value' }, - http: { someKey: 'http value' }, - host: { someKey: 'host value' }, - container: { someKey: 'container value' }, - service: { someKey: 'service value' }, - process: { someKey: 'process value' }, - agent: { someKey: 'agent value' }, - url: { someKey: 'url value' }, - user: { someKey: 'user value' }, - notIncluded: 'not included value', - transaction: { - id: '7efbc7056b746fcb', - notIncluded: 'transaction not included value', - custom: { - someKey: 'custom value', - }, - message: { - age: { ms: 1577958057123 }, - queue: { name: 'queue name' }, - }, - }, - } as unknown as Transaction; -} - -describe('TransactionMetadata', () => { - it('should render a transaction with all sections', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // sections - expectTextsInDocument(output, [ - 'Labels', - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'User', - 'Custom', - 'Message', - ]); - }); - - it('should render a transaction with all included dot notation keys', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // included keys - expectTextsInDocument(output, [ - 'labels.someKey', - 'http.someKey', - 'host.someKey', - 'container.someKey', - 'service.someKey', - 'process.someKey', - 'agent.someKey', - 'url.someKey', - 'user.someKey', - 'transaction.custom.someKey', - 'transaction.message.age.ms', - 'transaction.message.queue.name', - ]); - - // excluded keys - expectTextsNotInDocument(output, [ - 'notIncluded', - 'transaction.notIncluded', - ]); - }); - - it('should render a transaction with all included values', () => { - const transaction = getTransaction(); - const output = render( - , - renderOptions - ); - - // included values - expectTextsInDocument(output, [ - 'labels value', - 'http value', - 'host value', - 'container value', - 'service value', - 'process value', - 'agent value', - 'url value', - 'user value', - 'custom value', - '1577958057123', - 'queue name', - ]); - - // excluded values - expectTextsNotInDocument(output, [ - 'not included value', - 'transaction not included value', - ]); - }); - - it('should render a transaction with only the required sections', () => { - const transaction = {} as Transaction; - const output = render( - , - renderOptions - ); - - // required sections should be found - expectTextsInDocument(output, ['Labels', 'User']); - - // optional sections should NOT be found - expectTextsNotInDocument(output, [ - 'HTTP', - 'Host', - 'Container', - 'Service', - 'Process', - 'Agent', - 'URL', - 'Custom', - 'Message', - ]); - }); -}); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx index b3a53472f0815..32c0101c73b4d 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/index.tsx @@ -6,19 +6,40 @@ */ import React, { useMemo } from 'react'; -import { TRANSACTION_METADATA_SECTIONS } from './sections'; import { Transaction } from '../../../../../typings/es_schemas/ui/transaction'; -import { getSectionsWithRows } from '../helper'; +import { getSectionsFromFields } from '../helper'; import { MetadataTable } from '..'; +import { ProcessorEvent } from '../../../../../common/processor_event'; +import { FETCH_STATUS, useFetcher } from '../../../../hooks/use_fetcher'; interface Props { transaction: Transaction; } export function TransactionMetadata({ transaction }: Props) { - const sectionsWithRows = useMemo( - () => getSectionsWithRows(TRANSACTION_METADATA_SECTIONS, transaction), - [transaction] + const { data: transactionEvent, status } = useFetcher( + (callApmApi) => { + return callApmApi({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.transaction, + id: transaction.transaction.id, + }, + }, + }); + }, + [transaction.transaction.id] + ); + + const sections = useMemo( + () => getSectionsFromFields(transactionEvent?.metadata || {}), + [transactionEvent?.metadata] + ); + return ( + ); - return ; } diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts deleted file mode 100644 index 2f4a3d3229857..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/TransactionMetadata/sections.ts +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { - Section, - TRANSACTION, - LABELS, - EVENT, - HTTP, - HOST, - CLIENT, - CONTAINER, - SERVICE, - PROCESS, - AGENT, - URL, - PAGE, - USER, - USER_AGENT, - CUSTOM_TRANSACTION, - MESSAGE_TRANSACTION, - TRACE, -} from '../sections'; - -export const TRANSACTION_METADATA_SECTIONS: Section[] = [ - { ...LABELS, required: true }, - TRACE, - TRANSACTION, - EVENT, - HTTP, - HOST, - CLIENT, - CONTAINER, - SERVICE, - PROCESS, - MESSAGE_TRANSACTION, - AGENT, - URL, - { ...PAGE, key: 'transaction.page' }, - { ...USER, required: true }, - USER_AGENT, - CUSTOM_TRANSACTION, -]; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts index 770b35e7d17f2..2e64c170437d8 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.test.ts @@ -5,62 +5,52 @@ * 2.0. */ -import { getSectionsWithRows, filterSectionsByTerm } from './helper'; -import { LABELS, HTTP, SERVICE } from './sections'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; +import { filterSectionsByTerm, getSectionsFromFields } from './helper'; describe('MetadataTable Helper', () => { - const sections = [ - { ...LABELS, required: true }, - HTTP, - { ...SERVICE, properties: ['environment'] }, - ]; - const apmDoc = { - http: { - headers: { - Connection: 'close', - Host: 'opbeans:3000', - request: { method: 'get' }, - }, - }, - service: { - framework: { name: 'express' }, - environment: 'production', - }, - } as unknown as Transaction; - const metadataItems = getSectionsWithRows(sections, apmDoc); + const fields = { + 'http.headers.Connection': ['close'], + 'http.headers.Host': ['opbeans:3000'], + 'http.headers.request.method': ['get'], + 'service.framework.name': ['express'], + 'service.environment': ['production'], + }; + + const metadataItems = getSectionsFromFields(fields); - it('returns flattened data and required section', () => { + it('returns flattened data', () => { expect(metadataItems).toEqual([ - { key: 'labels', label: 'Labels', required: true, rows: [] }, { key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' }, + label: 'http', + properties: [ + { field: 'http.headers.Connection', value: ['close'] }, + { field: 'http.headers.Host', value: ['opbeans:3000'] }, + { field: 'http.headers.request.method', value: ['get'] }, ], }, { key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }], + label: 'service', + properties: [ + { field: 'service.environment', value: ['production'] }, + { field: 'service.framework.name', value: ['express'] }, + ], }, ]); }); + describe('filter', () => { it('items by key', () => { const filteredItems = filterSectionsByTerm(metadataItems, 'http'); expect(filteredItems).toEqual([ { key: 'http', - label: 'HTTP', - rows: [ - { key: 'http.headers.Connection', value: 'close' }, - { key: 'http.headers.Host', value: 'opbeans:3000' }, - { key: 'http.headers.request.method', value: 'get' }, + label: 'http', + properties: [ + { field: 'http.headers.Connection', value: ['close'] }, + { field: 'http.headers.Host', value: ['opbeans:3000'] }, + { field: 'http.headers.request.method', value: ['get'] }, ], }, ]); @@ -71,9 +61,8 @@ describe('MetadataTable Helper', () => { expect(filteredItems).toEqual([ { key: 'service', - label: 'Service', - properties: ['environment'], - rows: [{ key: 'service.environment', value: 'production' }], + label: 'service', + properties: [{ field: 'service.environment', value: ['production'] }], }, ]); }); diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts index bd115c1c7c174..c9e0f2aa66745 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/helper.ts @@ -5,35 +5,52 @@ * 2.0. */ -import { get, pick, isEmpty } from 'lodash'; -import { Section } from './sections'; -import { Transaction } from '../../../../typings/es_schemas/ui/transaction'; -import { APMError } from '../../../../typings/es_schemas/ui/apm_error'; -import { Span } from '../../../../typings/es_schemas/ui/span'; -import { flattenObject, KeyValuePair } from '../../../utils/flattenObject'; - -export type SectionsWithRows = ReturnType; - -export const getSectionsWithRows = ( - sections: Section[], - apmDoc: Transaction | APMError | Span -) => { - return sections - .map((section) => { - const sectionData: Record = get(apmDoc, section.key); - const filteredData: Record | undefined = - section.properties - ? pick(sectionData, section.properties) - : sectionData; - - const rows: KeyValuePair[] = flattenObject(filteredData, section.key); - return { ...section, rows }; - }) - .filter(({ required, rows }) => required || !isEmpty(rows)); +import { isEmpty, groupBy, partition } from 'lodash'; +import type { SectionDescriptor } from './types'; + +const EXCLUDED_FIELDS = ['error.exception.stacktrace', 'span.stacktrace']; + +export const getSectionsFromFields = (fields: Record) => { + const rows = Object.keys(fields) + .filter( + (field) => !EXCLUDED_FIELDS.some((excluded) => field.startsWith(excluded)) + ) + .sort() + .map((field) => { + return { + section: field.split('.')[0], + field, + value: fields[field], + }; + }); + + const sections = Object.values(groupBy(rows, 'section')).map( + (rowsForSection) => { + const first = rowsForSection[0]; + + const section: SectionDescriptor = { + key: first.section, + label: first.section.toLowerCase(), + properties: rowsForSection.map((row) => ({ + field: row.field, + value: row.value, + })), + }; + + return section; + } + ); + + const [labelSections, otherSections] = partition( + sections, + (section) => section.key === 'labels' + ); + + return [...labelSections, ...otherSections]; }; export const filterSectionsByTerm = ( - sections: SectionsWithRows, + sections: SectionDescriptor[], searchTerm: string ) => { if (!searchTerm) { @@ -41,15 +58,16 @@ export const filterSectionsByTerm = ( } return sections .map((section) => { - const { rows = [] } = section; - const filteredRows = rows.filter(({ key, value }) => { - const valueAsString = String(value).toLowerCase(); + const { properties = [] } = section; + const filteredProps = properties.filter(({ field, value }) => { return ( - key.toLowerCase().includes(searchTerm) || - valueAsString.includes(searchTerm) + field.toLowerCase().includes(searchTerm) || + value.some((val: string | number) => + String(val).toLowerCase().includes(searchTerm) + ) ); }); - return { ...section, rows: filteredRows }; + return { ...section, properties: filteredProps }; }) - .filter(({ rows }) => !isEmpty(rows)); + .filter(({ properties }) => !isEmpty(properties)); }; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx index 45be525512d0a..248fa240fd557 100644 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/index.tsx @@ -19,18 +19,21 @@ import { i18n } from '@kbn/i18n'; import { isEmpty } from 'lodash'; import React, { useCallback } from 'react'; import { useHistory, useLocation } from 'react-router-dom'; +import { EuiLoadingSpinner } from '@elastic/eui'; import { useUrlParams } from '../../../context/url_params_context/use_url_params'; import { HeightRetainer } from '../HeightRetainer'; import { fromQuery, toQuery } from '../Links/url_helpers'; -import { filterSectionsByTerm, SectionsWithRows } from './helper'; +import { filterSectionsByTerm } from './helper'; import { Section } from './Section'; import { useApmPluginContext } from '../../../context/apm_plugin/use_apm_plugin_context'; +import { SectionDescriptor } from './types'; interface Props { - sections: SectionsWithRows; + sections: SectionDescriptor[]; + isLoading: boolean; } -export function MetadataTable({ sections }: Props) { +export function MetadataTable({ sections, isLoading }: Props) { const history = useHistory(); const location = useLocation(); const { urlParams } = useUrlParams(); @@ -77,6 +80,13 @@ export function MetadataTable({ sections }: Props) { /> + {isLoading && ( + + + + + + )} {filteredSections.map((section) => (
@@ -84,7 +94,7 @@ export function MetadataTable({ sections }: Props) {
{section.label}
-
+
))} diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts deleted file mode 100644 index efc2ef8bde66b..0000000000000 --- a/x-pack/plugins/apm/public/components/shared/MetadataTable/sections.ts +++ /dev/null @@ -1,173 +0,0 @@ -/* - * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one - * or more contributor license agreements. Licensed under the Elastic License - * 2.0; you may not use this file except in compliance with the Elastic License - * 2.0. - */ - -import { i18n } from '@kbn/i18n'; - -export interface Section { - key: string; - label: string; - required?: boolean; - properties?: string[]; -} - -export const LABELS: Section = { - key: 'labels', - label: i18n.translate('xpack.apm.metadataTable.section.labelsLabel', { - defaultMessage: 'Labels', - }), -}; - -export const EVENT: Section = { - key: 'event', - label: i18n.translate('xpack.apm.metadataTable.section.eventLabel', { - defaultMessage: 'event', - }), - properties: ['outcome'], -}; - -export const HTTP: Section = { - key: 'http', - label: i18n.translate('xpack.apm.metadataTable.section.httpLabel', { - defaultMessage: 'HTTP', - }), -}; - -export const HOST: Section = { - key: 'host', - label: i18n.translate('xpack.apm.metadataTable.section.hostLabel', { - defaultMessage: 'Host', - }), -}; - -export const CLIENT: Section = { - key: 'client', - label: i18n.translate('xpack.apm.metadataTable.section.clientLabel', { - defaultMessage: 'Client', - }), - properties: ['ip'], -}; - -export const CONTAINER: Section = { - key: 'container', - label: i18n.translate('xpack.apm.metadataTable.section.containerLabel', { - defaultMessage: 'Container', - }), -}; - -export const SERVICE: Section = { - key: 'service', - label: i18n.translate('xpack.apm.metadataTable.section.serviceLabel', { - defaultMessage: 'Service', - }), -}; - -export const PROCESS: Section = { - key: 'process', - label: i18n.translate('xpack.apm.metadataTable.section.processLabel', { - defaultMessage: 'Process', - }), -}; - -export const AGENT: Section = { - key: 'agent', - label: i18n.translate('xpack.apm.metadataTable.section.agentLabel', { - defaultMessage: 'Agent', - }), -}; - -export const URL: Section = { - key: 'url', - label: i18n.translate('xpack.apm.metadataTable.section.urlLabel', { - defaultMessage: 'URL', - }), -}; - -export const USER: Section = { - key: 'user', - label: i18n.translate('xpack.apm.metadataTable.section.userLabel', { - defaultMessage: 'User', - }), -}; - -export const USER_AGENT: Section = { - key: 'user_agent', - label: i18n.translate('xpack.apm.metadataTable.section.userAgentLabel', { - defaultMessage: 'User agent', - }), -}; - -export const PAGE: Section = { - key: 'page', - label: i18n.translate('xpack.apm.metadataTable.section.pageLabel', { - defaultMessage: 'Page', - }), -}; - -export const SPAN: Section = { - key: 'span', - label: i18n.translate('xpack.apm.metadataTable.section.spanLabel', { - defaultMessage: 'Span', - }), - properties: ['id'], -}; - -export const TRANSACTION: Section = { - key: 'transaction', - label: i18n.translate('xpack.apm.metadataTable.section.transactionLabel', { - defaultMessage: 'Transaction', - }), - properties: ['id'], -}; - -export const TRACE: Section = { - key: 'trace', - label: i18n.translate('xpack.apm.metadataTable.section.traceLabel', { - defaultMessage: 'Trace', - }), - properties: ['id'], -}; - -export const ERROR: Section = { - key: 'error', - label: i18n.translate('xpack.apm.metadataTable.section.errorLabel', { - defaultMessage: 'Error', - }), - properties: ['id'], -}; - -const customLabel = i18n.translate( - 'xpack.apm.metadataTable.section.customLabel', - { - defaultMessage: 'Custom', - } -); - -export const CUSTOM_ERROR: Section = { - key: 'error.custom', - label: customLabel, -}; -export const CUSTOM_TRANSACTION: Section = { - key: 'transaction.custom', - label: customLabel, -}; - -const messageLabel = i18n.translate( - 'xpack.apm.metadataTable.section.messageLabel', - { - defaultMessage: 'Message', - } -); - -export const MESSAGE_TRANSACTION: Section = { - key: 'transaction.message', - label: messageLabel, -}; - -export const MESSAGE_SPAN: Section = { - key: 'span.message', - label: messageLabel, -}; diff --git a/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts new file mode 100644 index 0000000000000..3ce7698460f30 --- /dev/null +++ b/x-pack/plugins/apm/public/components/shared/MetadataTable/types.ts @@ -0,0 +1,13 @@ +/* + * 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. + */ + +export interface SectionDescriptor { + key: string; + label: string; + required?: boolean; + properties: Array<{ field: string; value: string[] | number[] }>; +} diff --git a/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts new file mode 100644 index 0000000000000..97e2e1356363f --- /dev/null +++ b/x-pack/plugins/apm/server/lib/event_metadata/get_event_metadata.ts @@ -0,0 +1,66 @@ +/* + * 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 { QueryDslQueryContainer } from '@elastic/elasticsearch/api/types'; +import { + ERROR_ID, + SPAN_ID, + TRANSACTION_ID, +} from '../../../common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../common/processor_event'; +import type { APMEventClient } from '../helpers/create_es_client/create_apm_event_client'; + +export async function getEventMetadata({ + apmEventClient, + processorEvent, + id, +}: { + apmEventClient: APMEventClient; + processorEvent: ProcessorEvent; + id: string; +}) { + const filter: QueryDslQueryContainer[] = []; + + switch (processorEvent) { + case ProcessorEvent.error: + filter.push({ + term: { [ERROR_ID]: id }, + }); + break; + + case ProcessorEvent.transaction: + filter.push({ + term: { + [TRANSACTION_ID]: id, + }, + }); + break; + + case ProcessorEvent.span: + filter.push({ + term: { [SPAN_ID]: id }, + }); + break; + } + + const response = await apmEventClient.search('get_event_metadata', { + apm: { + events: [processorEvent], + }, + body: { + query: { + bool: { filter }, + }, + size: 1, + _source: false, + fields: [{ field: '*', include_unmapped: true }], + }, + terminate_after: 1, + }); + + return response.hits.hits[0].fields; +} diff --git a/x-pack/plugins/apm/server/routes/event_metadata.ts b/x-pack/plugins/apm/server/routes/event_metadata.ts new file mode 100644 index 0000000000000..8970ab8ffdeea --- /dev/null +++ b/x-pack/plugins/apm/server/routes/event_metadata.ts @@ -0,0 +1,44 @@ +/* + * 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 * as t from 'io-ts'; +import { createApmServerRouteRepository } from './create_apm_server_route_repository'; +import { createApmServerRoute } from './create_apm_server_route'; +import { getEventMetadata } from '../lib/event_metadata/get_event_metadata'; +import { processorEventRt } from '../../common/processor_event'; +import { setupRequest } from '../lib/helpers/setup_request'; + +const eventMetadataRoute = createApmServerRoute({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + options: { tags: ['access:apm'] }, + params: t.type({ + path: t.type({ + processorEvent: processorEventRt, + id: t.string, + }), + }), + handler: async (resources) => { + const setup = await setupRequest(resources); + + const { + path: { processorEvent, id }, + } = resources.params; + + const metadata = await getEventMetadata({ + apmEventClient: setup.apmEventClient, + processorEvent, + id, + }); + + return { + metadata, + }; + }, +}); + +export const eventMetadataRouteRepository = + createApmServerRouteRepository().add(eventMetadataRoute); diff --git a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts index 7aa520dd5b8a2..472e46fecfa10 100644 --- a/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts +++ b/x-pack/plugins/apm/server/routes/get_global_apm_server_route_repository.ts @@ -33,6 +33,7 @@ import { traceRouteRepository } from './traces'; import { transactionRouteRepository } from './transactions'; import { APMRouteHandlerResources } from './typings'; import { historicalDataRouteRepository } from './historical_data'; +import { eventMetadataRouteRepository } from './event_metadata'; import { suggestionsRouteRepository } from './suggestions'; const getTypedGlobalApmServerRouteRepository = () => { @@ -58,7 +59,8 @@ const getTypedGlobalApmServerRouteRepository = () => { .merge(apmFleetRouteRepository) .merge(backendsRouteRepository) .merge(fallbackToTransactionsRouteRepository) - .merge(historicalDataRouteRepository); + .merge(historicalDataRouteRepository) + .merge(eventMetadataRouteRepository); return repository; }; diff --git a/x-pack/plugins/translations/translations/ja-JP.json b/x-pack/plugins/translations/translations/ja-JP.json index dcbb8ce26ee4a..bdccd8ad87760 100644 --- a/x-pack/plugins/translations/translations/ja-JP.json +++ b/x-pack/plugins/translations/translations/ja-JP.json @@ -6432,24 +6432,6 @@ "xpack.apm.localFilters.titles.serviceName": "サービス名", "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "フィルター", - "xpack.apm.metadataTable.section.agentLabel": "エージェント", - "xpack.apm.metadataTable.section.clientLabel": "クライアント", - "xpack.apm.metadataTable.section.containerLabel": "コンテナー", - "xpack.apm.metadataTable.section.customLabel": "カスタム", - "xpack.apm.metadataTable.section.errorLabel": "エラー", - "xpack.apm.metadataTable.section.hostLabel": "ホスト", - "xpack.apm.metadataTable.section.httpLabel": "HTTP", - "xpack.apm.metadataTable.section.labelsLabel": "ラベル", - "xpack.apm.metadataTable.section.messageLabel": "メッセージ", - "xpack.apm.metadataTable.section.pageLabel": "ページ", - "xpack.apm.metadataTable.section.processLabel": "プロセス", - "xpack.apm.metadataTable.section.serviceLabel": "サービス", - "xpack.apm.metadataTable.section.spanLabel": "スパン", - "xpack.apm.metadataTable.section.traceLabel": "トレース", - "xpack.apm.metadataTable.section.transactionLabel": "トランザクション", - "xpack.apm.metadataTable.section.urlLabel": "URL", - "xpack.apm.metadataTable.section.userAgentLabel": "ユーザーエージェント", - "xpack.apm.metadataTable.section.userLabel": "ユーザー", "xpack.apm.metrics.transactionChart.machineLearningLabel": "機械学習:", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "ストリームには、平均レイテンシの想定境界が表示されます。赤色の垂直の注釈は、異常スコアが75以上の異常値を示します。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "フィルタリングで検索バーを使用しているときには、機械学習結果が表示されません", diff --git a/x-pack/plugins/translations/translations/zh-CN.json b/x-pack/plugins/translations/translations/zh-CN.json index 87c96d1efe48d..4be40151f7318 100644 --- a/x-pack/plugins/translations/translations/zh-CN.json +++ b/x-pack/plugins/translations/translations/zh-CN.json @@ -6483,24 +6483,6 @@ "xpack.apm.localFilters.titles.serviceName": "服务名称", "xpack.apm.localFilters.titles.transactionUrl": "URL", "xpack.apm.localFiltersTitle": "筛选", - "xpack.apm.metadataTable.section.agentLabel": "代理", - "xpack.apm.metadataTable.section.clientLabel": "客户端", - "xpack.apm.metadataTable.section.containerLabel": "容器", - "xpack.apm.metadataTable.section.customLabel": "定制", - "xpack.apm.metadataTable.section.errorLabel": "错误", - "xpack.apm.metadataTable.section.hostLabel": "主机", - "xpack.apm.metadataTable.section.httpLabel": "HTTP", - "xpack.apm.metadataTable.section.labelsLabel": "标签", - "xpack.apm.metadataTable.section.messageLabel": "消息", - "xpack.apm.metadataTable.section.pageLabel": "页", - "xpack.apm.metadataTable.section.processLabel": "进程", - "xpack.apm.metadataTable.section.serviceLabel": "服务", - "xpack.apm.metadataTable.section.spanLabel": "跨度", - "xpack.apm.metadataTable.section.traceLabel": "跟踪", - "xpack.apm.metadataTable.section.transactionLabel": "事务", - "xpack.apm.metadataTable.section.urlLabel": "URL", - "xpack.apm.metadataTable.section.userAgentLabel": "用户代理", - "xpack.apm.metadataTable.section.userLabel": "用户", "xpack.apm.metrics.transactionChart.machineLearningLabel": "Machine Learning", "xpack.apm.metrics.transactionChart.machineLearningTooltip": "流显示平均延迟的预期边界。红色垂直标注表示异常分数等于或大于 75 的异常。", "xpack.apm.metrics.transactionChart.machineLearningTooltip.withKuery": "使用搜索栏筛选时,Machine Learning 结果处于隐藏状态", diff --git a/x-pack/test/apm_api_integration/tests/index.ts b/x-pack/test/apm_api_integration/tests/index.ts index d402a74287f98..efe159b36e3d3 100644 --- a/x-pack/test/apm_api_integration/tests/index.ts +++ b/x-pack/test/apm_api_integration/tests/index.ts @@ -37,6 +37,10 @@ export default function apmApiIntegrationTests(providerContext: FtrProviderConte loadTestFile(require.resolve('./correlations/latency')); }); + describe('metadata/event_metadata', function () { + loadTestFile(require.resolve('./metadata/event_metadata')); + }); + describe('metrics_charts/metrics_charts', function () { loadTestFile(require.resolve('./metrics_charts/metrics_charts')); }); diff --git a/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts new file mode 100644 index 0000000000000..d979f0bad1ec6 --- /dev/null +++ b/x-pack/test/apm_api_integration/tests/metadata/event_metadata.ts @@ -0,0 +1,129 @@ +/* + * 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 expect from '@kbn/expect'; +import { PROCESSOR_EVENT } from '../../../../plugins/apm/common/elasticsearch_fieldnames'; +import { ProcessorEvent } from '../../../../plugins/apm/common/processor_event'; +import { FtrProviderContext } from '../../common/ftr_provider_context'; +import { registry } from '../../common/registry'; + +export default function ApiTest({ getService }: FtrProviderContext) { + const apmApiClient = getService('apmApiClient'); + const esClient = getService('es'); + + async function getLastDocId(processorEvent: ProcessorEvent) { + const response = await esClient.search<{ + [key: string]: { id: string }; + }>({ + index: ['apm-*'], + body: { + query: { + bool: { + filter: [{ term: { [PROCESSOR_EVENT]: processorEvent } }], + }, + }, + size: 1, + sort: { + '@timestamp': 'desc', + }, + }, + }); + + return response.body.hits.hits[0]._source![processorEvent].id; + } + + registry.when('Event metadata', { config: 'basic', archives: ['apm_8.0.0'] }, () => { + it('fetches transaction metadata', async () => { + const id = await getLastDocId(ProcessorEvent.transaction); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.transaction, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'transaction.name', + 'transaction.type', + 'service.name' + ); + }); + + it('fetches error metadata', async () => { + const id = await getLastDocId(ProcessorEvent.error); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.error, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'error.grouping_key', + 'error.grouping_name', + 'service.name' + ); + }); + + it('fetches span metadata', async () => { + const id = await getLastDocId(ProcessorEvent.span); + + const { body } = await apmApiClient.readUser({ + endpoint: 'GET /api/apm/event_metadata/{processorEvent}/{id}', + params: { + path: { + processorEvent: ProcessorEvent.span, + id, + }, + }, + }); + + expect(body).keys('metadata').ok(); + + expect( + Object.keys(body.metadata).filter((key) => { + return Array.isArray(body.metadata[key]); + }) + ); + + expect(body.metadata).keys( + '@timestamp', + 'agent.name', + 'span.name', + 'span.type', + 'service.name' + ); + }); + }); +}