mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
[APM] Span link (#126630)
* adding span links data generation * fixing span links synthtrace * adding span links * span links route * fixing span links new scenario * adding span links * improved tab structure * span links table * adding span links data generation * fixing span links synthtrace * adding span links * span links route * fixing span links new scenario * adding span links * span links table * improved tab structure * adjusting table * fixing ts issue * filtering data within timerange * fixing ts * fixing ci * disabling select option when no link available * adding api tests * fixing tests * e2e tests * fixing too_many_nested_clauses issue * refactoring apis * api tests * fixing e2e tests * fixing links * renaming link * fixing tests * addressing PR comments * fixing test * fixing ci * fixing ci * addressing pr comments * passing processor event to incoming links API * updating api tests * renaming incoming and outgoing * wrapping type into details property * renaming incoming/outgoing * pr comments * adding processor event to query * renaming * new API tests * import fix * renaming * adding e2e tests * addressing pr changes * changing link * Adding filter on children fetch * renaming services on test * renaming Co-authored-by: Boris Kirov <borisasenovkirov@gmail.com> Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
3f5197a598
commit
d3b9f3285e
43 changed files with 3354 additions and 1247 deletions
|
@ -85,6 +85,10 @@ export type ApmFields = Fields &
|
|||
'span.destination.service.response_time.count': number;
|
||||
'span.self_time.count': number;
|
||||
'span.self_time.sum.us': number;
|
||||
'span.links': Array<{
|
||||
trace: { id: string };
|
||||
span: { id: string };
|
||||
}>;
|
||||
'cloud.provider': string;
|
||||
'cloud.project.name': string;
|
||||
'cloud.service.name': string;
|
||||
|
|
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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 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 or the Server
|
||||
* Side Public License, v 1.
|
||||
*/
|
||||
|
||||
import { compact, shuffle } from 'lodash';
|
||||
import { apm, ApmFields, EntityArrayIterable, timerange } from '../..';
|
||||
import { generateLongId, generateShortId } from '../../lib/utils/generate_id';
|
||||
import { Scenario } from '../scenario';
|
||||
|
||||
function generateExternalSpanLinks() {
|
||||
// randomly creates external span links 0 - 10
|
||||
return Array(Math.floor(Math.random() * 11))
|
||||
.fill(0)
|
||||
.map(() => ({ span: { id: generateLongId() }, trace: { id: generateShortId() } }));
|
||||
}
|
||||
|
||||
function getSpanLinksFromEvents(events: ApmFields[]) {
|
||||
return compact(
|
||||
events.map((event) => {
|
||||
const spanId = event['span.id'];
|
||||
return spanId ? { span: { id: spanId }, trace: { id: event['trace.id']! } } : undefined;
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const scenario: Scenario<ApmFields> = async () => {
|
||||
return {
|
||||
generate: ({ from, to }) => {
|
||||
const producerInternalOnlyInstance = apm
|
||||
.service('producer-internal-only', 'production', 'go')
|
||||
.instance('instance-a');
|
||||
const producerInternalOnlyEvents = timerange(
|
||||
new Date('2022-04-25T19:00:00.000Z'),
|
||||
new Date('2022-04-25T19:01:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerInternalOnlyInstance
|
||||
.transaction('Transaction A')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerInternalOnlyInstance
|
||||
.span('Span A', 'custom')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const producerInternalOnlyApmFields = producerInternalOnlyEvents.toArray();
|
||||
const spanASpanLink = getSpanLinksFromEvents(producerInternalOnlyApmFields);
|
||||
|
||||
const producerConsumerInstance = apm
|
||||
.service('producer-consumer', 'production', 'java')
|
||||
.instance('instance-b');
|
||||
const producerConsumerEvents = timerange(from, to)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerConsumerInstance
|
||||
.transaction('Transaction B')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerConsumerInstance
|
||||
.span('Span B', 'external')
|
||||
.defaults({
|
||||
'span.links': shuffle([...generateExternalSpanLinks(), ...spanASpanLink]),
|
||||
})
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(900)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const producerConsumerApmFields = producerConsumerEvents.toArray();
|
||||
const spanBSpanLink = getSpanLinksFromEvents(producerConsumerApmFields);
|
||||
|
||||
const consumerInstance = apm.service('consumer', 'production', 'ruby').instance('instance-c');
|
||||
const consumerEvents = timerange(from, to)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return consumerInstance
|
||||
.transaction('Transaction C')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
consumerInstance
|
||||
.span('Span C', 'external')
|
||||
.defaults({ 'span.links': spanBSpanLink })
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(900)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
return new EntityArrayIterable(producerInternalOnlyApmFields)
|
||||
.merge(consumerEvents)
|
||||
.merge(new EntityArrayIterable(producerConsumerApmFields));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default scenario;
|
|
@ -203,6 +203,12 @@ exports[`Error SPAN_DURATION 1`] = `undefined`;
|
|||
|
||||
exports[`Error SPAN_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_LINKS 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_LINKS_SPAN_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_LINKS_TRACE_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Error SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
@ -446,6 +452,12 @@ exports[`Span SPAN_DURATION 1`] = `1337`;
|
|||
|
||||
exports[`Span SPAN_ID 1`] = `"span id"`;
|
||||
|
||||
exports[`Span SPAN_LINKS 1`] = `undefined`;
|
||||
|
||||
exports[`Span SPAN_LINKS_SPAN_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span SPAN_LINKS_TRACE_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Span SPAN_NAME 1`] = `"span name"`;
|
||||
|
||||
exports[`Span SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
@ -703,6 +715,12 @@ exports[`Transaction SPAN_DURATION 1`] = `undefined`;
|
|||
|
||||
exports[`Transaction SPAN_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_LINKS 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_LINKS_SPAN_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_LINKS_TRACE_ID 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_NAME 1`] = `undefined`;
|
||||
|
||||
exports[`Transaction SPAN_SELF_TIME_SUM 1`] = `undefined`;
|
||||
|
|
|
@ -74,6 +74,10 @@ export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT =
|
|||
export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM =
|
||||
'span.destination.service.response_time.sum.us';
|
||||
|
||||
export const SPAN_LINKS = 'span.links';
|
||||
export const SPAN_LINKS_TRACE_ID = 'span.links.trace.id';
|
||||
export const SPAN_LINKS_SPAN_ID = 'span.links.span.id';
|
||||
|
||||
// Parent ID for a transaction or span
|
||||
export const PARENT_ID = 'parent.id';
|
||||
|
||||
|
|
24
x-pack/plugins/apm/common/span_links.ts
Normal file
24
x-pack/plugins/apm/common/span_links.ts
Normal file
|
@ -0,0 +1,24 @@
|
|||
/*
|
||||
* 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 { AgentName } from '../typings/es_schemas/ui/fields/agent';
|
||||
import { Environment } from './environment_rt';
|
||||
|
||||
export interface SpanLinkDetails {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
details?: {
|
||||
agentName: AgentName;
|
||||
serviceName: string;
|
||||
duration: number;
|
||||
environment: Environment;
|
||||
transactionId?: string;
|
||||
spanName?: string;
|
||||
spanSubtype?: string;
|
||||
spanType?: string;
|
||||
};
|
||||
}
|
File diff suppressed because it is too large
Load diff
|
@ -628,8 +628,7 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
},
|
||||
"aliases": {},
|
||||
"index": ".ml-config",
|
||||
"mappings": {
|
||||
"_meta": {
|
||||
|
@ -15510,6 +15509,26 @@
|
|||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"links": {
|
||||
"properties": {
|
||||
"span": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -20620,6 +20639,26 @@
|
|||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"links": {
|
||||
"properties": {
|
||||
"span": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,326 @@
|
|||
/*
|
||||
* 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 { apm, EntityArrayIterable, timerange } from '@elastic/apm-synthtrace';
|
||||
import { synthtrace } from '../../../../synthtrace';
|
||||
import { SpanLink } from '../../../../../typings/es_schemas/raw/fields/span_links';
|
||||
|
||||
function getProducerInternalOnly() {
|
||||
const producerInternalOnlyInstance = apm
|
||||
.service('producer-internal-only', 'production', 'go')
|
||||
.instance('instance a');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:00:00.000Z'),
|
||||
new Date('2022-01-01T00:01:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerInternalOnlyInstance
|
||||
.transaction(`Transaction A`)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerInternalOnlyInstance
|
||||
.span(`Span A`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionA = apmFields.find(
|
||||
(item) => item['processor.event'] === 'transaction'
|
||||
);
|
||||
const spanA = apmFields.find((item) => item['processor.event'] === 'span');
|
||||
|
||||
const ids =
|
||||
spanA && transactionA
|
||||
? {
|
||||
transactionAId: transactionA['transaction.id']!,
|
||||
traceId: spanA['trace.id']!,
|
||||
spanAId: spanA['span.id']!,
|
||||
}
|
||||
: {};
|
||||
const spanASpanLink = spanA
|
||||
? { trace: { id: spanA['trace.id']! }, span: { id: spanA['span.id']! } }
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ids,
|
||||
spanASpanLink,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getProducerExternalOnly() {
|
||||
const producerExternalOnlyInstance = apm
|
||||
.service('producer-external-only', 'production', 'java')
|
||||
.instance('instance b');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:02:00.000Z'),
|
||||
new Date('2022-01-01T00:03:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerExternalOnlyInstance
|
||||
.transaction(`Transaction B`)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerExternalOnlyInstance
|
||||
.span(`Span B`, 'external', 'http')
|
||||
.defaults({
|
||||
'span.links': [
|
||||
{ trace: { id: 'trace#1' }, span: { id: 'span#1' } },
|
||||
],
|
||||
})
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success(),
|
||||
producerExternalOnlyInstance
|
||||
.span(`Span B.1`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionB = apmFields.find(
|
||||
(item) => item['processor.event'] === 'transaction'
|
||||
);
|
||||
const spanB = apmFields.find(
|
||||
(item) =>
|
||||
item['processor.event'] === 'span' && item['span.name'] === 'Span B'
|
||||
);
|
||||
const ids =
|
||||
spanB && transactionB
|
||||
? {
|
||||
traceId: spanB['trace.id']!,
|
||||
transactionBId: transactionB['transaction.id']!,
|
||||
spanBId: spanB['span.id']!,
|
||||
}
|
||||
: {};
|
||||
|
||||
const spanBSpanLink = spanB
|
||||
? {
|
||||
trace: { id: spanB['trace.id']! },
|
||||
span: { id: spanB['span.id']! },
|
||||
}
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
ids,
|
||||
spanBSpanLink,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getProducerConsumer({
|
||||
producerInternalOnlySpanASpanLink,
|
||||
}: {
|
||||
producerInternalOnlySpanASpanLink?: SpanLink;
|
||||
}) {
|
||||
const producerConsumerInstance = apm
|
||||
.service('producer-consumer', 'production', 'ruby')
|
||||
.instance('instance c');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:04:00.000Z'),
|
||||
new Date('2022-01-01T00:05:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerConsumerInstance
|
||||
.transaction(`Transaction C`)
|
||||
.defaults({
|
||||
'span.links': producerInternalOnlySpanASpanLink
|
||||
? [producerInternalOnlySpanASpanLink]
|
||||
: [],
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerConsumerInstance
|
||||
.span(`Span C`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionC = apmFields.find(
|
||||
(item) => item['processor.event'] === 'transaction'
|
||||
);
|
||||
const transactionCSpanLink = transactionC
|
||||
? {
|
||||
trace: { id: transactionC['trace.id']! },
|
||||
span: { id: transactionC['transaction.id']! },
|
||||
}
|
||||
: undefined;
|
||||
const spanC = apmFields.find(
|
||||
(item) =>
|
||||
item['processor.event'] === 'span' || item['span.name'] === 'Span C'
|
||||
);
|
||||
const spanCSpanLink = spanC
|
||||
? {
|
||||
trace: { id: spanC['trace.id']! },
|
||||
span: { id: spanC['span.id']! },
|
||||
}
|
||||
: undefined;
|
||||
const ids =
|
||||
spanC && transactionC
|
||||
? {
|
||||
traceId: transactionC['trace.id']!,
|
||||
transactionCId: transactionC['transaction.id']!,
|
||||
spanCId: spanC['span.id']!,
|
||||
}
|
||||
: {};
|
||||
return {
|
||||
transactionCSpanLink,
|
||||
spanCSpanLink,
|
||||
ids,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getConsumerMultiple({
|
||||
producerInternalOnlySpanASpanLink,
|
||||
producerExternalOnlySpanBSpanLink,
|
||||
producerConsumerSpanCSpanLink,
|
||||
producerConsumerTransactionCSpanLink,
|
||||
}: {
|
||||
producerInternalOnlySpanASpanLink?: SpanLink;
|
||||
producerExternalOnlySpanBSpanLink?: SpanLink;
|
||||
producerConsumerSpanCSpanLink?: SpanLink;
|
||||
producerConsumerTransactionCSpanLink?: SpanLink;
|
||||
}) {
|
||||
const consumerMultipleInstance = apm
|
||||
.service('consumer-multiple', 'production', 'nodejs')
|
||||
.instance('instance d');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:06:00.000Z'),
|
||||
new Date('2022-01-01T00:07:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return consumerMultipleInstance
|
||||
.transaction(`Transaction D`)
|
||||
.defaults({
|
||||
'span.links':
|
||||
producerInternalOnlySpanASpanLink && producerConsumerSpanCSpanLink
|
||||
? [
|
||||
producerInternalOnlySpanASpanLink,
|
||||
producerConsumerSpanCSpanLink,
|
||||
]
|
||||
: [],
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
consumerMultipleInstance
|
||||
.span(`Span E`, 'external', 'http')
|
||||
.defaults({
|
||||
'span.links':
|
||||
producerExternalOnlySpanBSpanLink &&
|
||||
producerConsumerTransactionCSpanLink
|
||||
? [
|
||||
producerExternalOnlySpanBSpanLink,
|
||||
producerConsumerTransactionCSpanLink,
|
||||
]
|
||||
: [],
|
||||
})
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
const apmFields = events.toArray();
|
||||
const transactionD = apmFields.find(
|
||||
(item) => item['processor.event'] === 'transaction'
|
||||
);
|
||||
const spanE = apmFields.find((item) => item['processor.event'] === 'span');
|
||||
|
||||
const ids =
|
||||
transactionD && spanE
|
||||
? {
|
||||
traceId: transactionD['trace.id']!,
|
||||
transactionDId: transactionD['transaction.id']!,
|
||||
spanEId: spanE['span.id']!,
|
||||
}
|
||||
: {};
|
||||
|
||||
return {
|
||||
ids,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Data ingestion summary:
|
||||
*
|
||||
* producer-internal-only (go)
|
||||
* --Transaction A
|
||||
* ----Span A
|
||||
*
|
||||
* producer-external-only (java)
|
||||
* --Transaction B
|
||||
* ----Span B
|
||||
* ------span.links=external link
|
||||
* ----Span B1
|
||||
*
|
||||
* producer-consumer (ruby)
|
||||
* --Transaction C
|
||||
* ------span.links=producer-internal-only / Span A
|
||||
* ----Span C
|
||||
*
|
||||
* consumer-multiple (nodejs)
|
||||
* --Transaction D
|
||||
* ------span.links= producer-consumer / Span C | producer-internal-only / Span A
|
||||
* ----Span E
|
||||
* ------span.links= producer-external-only / Span B | producer-consumer / Transaction C
|
||||
*/
|
||||
export async function generateSpanLinksData() {
|
||||
const producerInternalOnly = getProducerInternalOnly();
|
||||
const producerExternalOnly = getProducerExternalOnly();
|
||||
const producerConsumer = getProducerConsumer({
|
||||
producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink,
|
||||
});
|
||||
const producerMultiple = getConsumerMultiple({
|
||||
producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink,
|
||||
producerConsumerSpanCSpanLink: producerConsumer.spanCSpanLink,
|
||||
producerConsumerTransactionCSpanLink: producerConsumer.transactionCSpanLink,
|
||||
producerExternalOnlySpanBSpanLink: producerExternalOnly.spanBSpanLink,
|
||||
});
|
||||
|
||||
await synthtrace.index(
|
||||
new EntityArrayIterable(producerInternalOnly.apmFields).merge(
|
||||
new EntityArrayIterable(producerExternalOnly.apmFields),
|
||||
new EntityArrayIterable(producerConsumer.apmFields),
|
||||
new EntityArrayIterable(producerMultiple.apmFields)
|
||||
)
|
||||
);
|
||||
|
||||
return {
|
||||
producerInternalOnlyIds: producerInternalOnly.ids,
|
||||
producerExternalOnlyIds: producerExternalOnly.ids,
|
||||
producerConsumerIds: producerConsumer.ids,
|
||||
producerMultipleIds: producerMultiple.ids,
|
||||
};
|
||||
}
|
|
@ -0,0 +1,301 @@
|
|||
/*
|
||||
* 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 url from 'url';
|
||||
import { synthtrace } from '../../../../synthtrace';
|
||||
import { generateSpanLinksData } from './generate_span_links_data';
|
||||
|
||||
const start = '2022-01-01T00:00:00.000Z';
|
||||
const end = '2022-01-01T00:15:00.000Z';
|
||||
|
||||
function getServiceInventoryUrl({ serviceName }: { serviceName: string }) {
|
||||
return url.format({
|
||||
pathname: `/app/apm/services/${serviceName}`,
|
||||
query: {
|
||||
rangeFrom: start,
|
||||
rangeTo: end,
|
||||
environment: 'ENVIRONMENT_ALL',
|
||||
kuery: '',
|
||||
serviceGroup: '',
|
||||
transactionType: 'request',
|
||||
comparisonEnabled: true,
|
||||
offset: '1d',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('Span links', () => {
|
||||
beforeEach(() => {
|
||||
cy.loginAsReadOnlyUser();
|
||||
});
|
||||
|
||||
describe('when data is loaded', () => {
|
||||
let ids: Awaited<ReturnType<typeof generateSpanLinksData>>;
|
||||
before(async () => {
|
||||
ids = await generateSpanLinksData();
|
||||
});
|
||||
|
||||
after(async () => {
|
||||
await synthtrace.clean();
|
||||
});
|
||||
|
||||
describe('span links count on trace waterfall', () => {
|
||||
it('Shows two children and no parents on producer-internal-only Span A', () => {
|
||||
cy.visit(
|
||||
getServiceInventoryUrl({ serviceName: 'producer-internal-only' })
|
||||
);
|
||||
cy.contains('Transaction A').click();
|
||||
cy.contains('2 Span links');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerInternalOnlyIds.spanAId}"]`
|
||||
).realHover();
|
||||
cy.contains('2 Span links found');
|
||||
cy.contains('2 incoming');
|
||||
cy.contains('0 outgoing');
|
||||
});
|
||||
|
||||
it('Shows one parent and one children on producer-external-only Span B', () => {
|
||||
cy.visit(
|
||||
getServiceInventoryUrl({ serviceName: 'producer-external-only' })
|
||||
);
|
||||
cy.contains('Transaction B').click();
|
||||
cy.contains('2 Span links');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerExternalOnlyIds.spanBId}"]`
|
||||
).realHover();
|
||||
cy.contains('2 Span links found');
|
||||
cy.contains('1 incoming');
|
||||
cy.contains('1 outgoing');
|
||||
});
|
||||
|
||||
it('Shows one parent and one children on producer-consumer Transaction C', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' }));
|
||||
cy.contains('Transaction C').click();
|
||||
cy.contains('2 Span links');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.transactionCId}"]`
|
||||
).realHover();
|
||||
cy.contains('2 Span links found');
|
||||
cy.contains('1 incoming');
|
||||
cy.contains('1 outgoing');
|
||||
});
|
||||
|
||||
it('Shows no parent and one children on producer-consumer Span C', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' }));
|
||||
cy.contains('Transaction C').click();
|
||||
cy.contains('1 Span link');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerConsumerIds.spanCId}"]`
|
||||
).realHover();
|
||||
cy.contains('1 Span link found');
|
||||
cy.contains('1 incoming');
|
||||
cy.contains('0 outgoing');
|
||||
});
|
||||
|
||||
it('Shows two parents and one children on consumer-multiple Transaction D', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' }));
|
||||
cy.contains('Transaction D').click();
|
||||
cy.contains('2 Span links');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.transactionDId}"]`
|
||||
).realHover();
|
||||
cy.contains('2 Span links found');
|
||||
cy.contains('0 incoming');
|
||||
cy.contains('2 outgoing');
|
||||
});
|
||||
|
||||
it('Shows two parents and one children on consumer-multiple Span E', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' }));
|
||||
cy.contains('Transaction D').click();
|
||||
cy.contains('2 Span links');
|
||||
cy.get(
|
||||
`[data-test-subj="spanLinksBadge_${ids.producerMultipleIds.spanEId}"]`
|
||||
).realHover();
|
||||
cy.contains('2 Span links found');
|
||||
cy.contains('0 incoming');
|
||||
cy.contains('2 outgoing');
|
||||
});
|
||||
});
|
||||
|
||||
describe('span link flyout', () => {
|
||||
it('Shows children details on producer-internal-only Span A', () => {
|
||||
cy.visit(
|
||||
getServiceInventoryUrl({ serviceName: 'producer-internal-only' })
|
||||
);
|
||||
cy.contains('Transaction A').click();
|
||||
cy.contains('Span A').click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
cy.contains('producer-consumer')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-consumer/overview');
|
||||
cy.contains('Transaction C')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`/link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}`
|
||||
);
|
||||
cy.contains('consumer-multiple')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/consumer-multiple/overview');
|
||||
cy.contains('Transaction D')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}`
|
||||
);
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
|
||||
'contain.text',
|
||||
'Outgoing links (0)'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows children and parents details on producer-external-only Span B', () => {
|
||||
cy.visit(
|
||||
getServiceInventoryUrl({ serviceName: 'producer-external-only' })
|
||||
);
|
||||
cy.contains('Transaction B').click();
|
||||
cy.contains('Span B').click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
|
||||
cy.contains('consumer-multiple')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/consumer-multiple/overview');
|
||||
cy.contains('Span E')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}`
|
||||
);
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').select(
|
||||
'Outgoing links (1)'
|
||||
);
|
||||
cy.contains('Unknown');
|
||||
cy.contains('trace#1-span#1');
|
||||
});
|
||||
|
||||
it('Shows children and parents details on producer-consumer Transaction C', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' }));
|
||||
cy.contains('Transaction C').click();
|
||||
cy.get(
|
||||
`[aria-controls="${ids.producerConsumerIds.transactionCId}"]`
|
||||
).click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
|
||||
cy.contains('consumer-multiple')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/consumer-multiple/overview');
|
||||
cy.contains('Span E')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.spanEId}`
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').select(
|
||||
'Outgoing links (1)'
|
||||
);
|
||||
cy.contains('producer-internal-only')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-internal-only/overview');
|
||||
cy.contains('Span A')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}`
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows children and parents details on producer-consumer Span C', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'producer-consumer' }));
|
||||
cy.contains('Transaction C').click();
|
||||
cy.contains('Span C').click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
|
||||
cy.contains('consumer-multiple')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/consumer-multiple/overview');
|
||||
cy.contains('Transaction D')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerMultipleIds.transactionDId}?waterfallItemId=${ids.producerMultipleIds.transactionDId}`
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
|
||||
'contain.text',
|
||||
'Outgoing links (0)'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows children and parents details on consumer-multiple Transaction D', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' }));
|
||||
cy.contains('Transaction D').click();
|
||||
cy.get(
|
||||
`[aria-controls="${ids.producerMultipleIds.transactionDId}"]`
|
||||
).click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
|
||||
cy.contains('producer-consumer')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-consumer/overview');
|
||||
cy.contains('Span C')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.spanCId}`
|
||||
);
|
||||
|
||||
cy.contains('producer-internal-only')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-internal-only/overview');
|
||||
cy.contains('Span A')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerInternalOnlyIds.transactionAId}?waterfallItemId=${ids.producerInternalOnlyIds.spanAId}`
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
|
||||
'contain.text',
|
||||
'Incoming links (0)'
|
||||
);
|
||||
});
|
||||
|
||||
it('Shows children and parents details on consumer-multiple Span E', () => {
|
||||
cy.visit(getServiceInventoryUrl({ serviceName: 'consumer-multiple' }));
|
||||
cy.contains('Transaction D').click();
|
||||
cy.contains('Span E').click();
|
||||
cy.get('[data-test-subj="spanLinksTab"]').click();
|
||||
|
||||
cy.contains('producer-external-only')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-external-only/overview');
|
||||
cy.contains('Span B')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerExternalOnlyIds.transactionBId}?waterfallItemId=${ids.producerExternalOnlyIds.spanBId}`
|
||||
);
|
||||
|
||||
cy.contains('producer-consumer')
|
||||
.should('have.attr', 'href')
|
||||
.and('include', '/services/producer-consumer/overview');
|
||||
cy.contains('Transaction C')
|
||||
.should('have.attr', 'href')
|
||||
.and(
|
||||
'include',
|
||||
`link-to/transaction/${ids.producerConsumerIds.transactionCId}?waterfallItemId=${ids.producerConsumerIds.transactionCId}`
|
||||
);
|
||||
|
||||
cy.get('[data-test-subj="spanLinkTypeSelect"]').should(
|
||||
'contain.text',
|
||||
'Incoming links (0)'
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -34,7 +34,7 @@ describe('getRedirectToTransactionDetailPageUrl', () => {
|
|||
|
||||
it('formats url correctly', () => {
|
||||
expect(url).toBe(
|
||||
'/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A00%3A00.000Z&rangeTo=2020-01-01T00%3A05%3A00.000Z'
|
||||
'/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A00%3A00.000Z&rangeTo=2020-01-01T00%3A05%3A00.000Z&waterfallItemId='
|
||||
);
|
||||
});
|
||||
});
|
||||
|
@ -48,7 +48,7 @@ describe('getRedirectToTransactionDetailPageUrl', () => {
|
|||
|
||||
it('uses timerange provided', () => {
|
||||
expect(url).toBe(
|
||||
'/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A02%3A00.000Z&rangeTo=2020-01-01T00%3A17%3A59.999Z'
|
||||
'/services/opbeans-node/transactions/view?traceId=trace_id&transactionId=transaction_id&transactionName=transaction_name&transactionType=request&rangeFrom=2020-01-01T00%3A02%3A00.000Z&rangeTo=2020-01-01T00%3A17%3A59.999Z&waterfallItemId='
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -12,10 +12,12 @@ export const getRedirectToTransactionDetailPageUrl = ({
|
|||
transaction,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
waterfallItemId,
|
||||
}: {
|
||||
transaction: Transaction;
|
||||
rangeFrom?: string;
|
||||
rangeTo?: string;
|
||||
waterfallItemId?: string;
|
||||
}) => {
|
||||
return format({
|
||||
pathname: `/services/${transaction.service.name}/transactions/view`,
|
||||
|
@ -37,6 +39,7 @@ export const getRedirectToTransactionDetailPageUrl = ({
|
|||
diff: transaction.transaction.duration.us / 1000,
|
||||
direction: 'up',
|
||||
}),
|
||||
waterfallItemId,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
|
|
@ -122,7 +122,7 @@ describe('TraceLink', () => {
|
|||
const component = shallow(<TraceLink />);
|
||||
|
||||
expect(component.prop('to')).toEqual(
|
||||
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now'
|
||||
'/services/foo/transactions/view?traceId=123&transactionId=456&transactionName=bar&transactionType=GET&rangeFrom=now-24h&rangeTo=now&waterfallItemId='
|
||||
);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -10,12 +10,14 @@ import { useLegacyUrlParams } from '../../../context/url_params_context/use_url_
|
|||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { APIReturnType } from '../../../services/rest/create_call_apm_api';
|
||||
import { getWaterfall } from './waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
|
||||
const INITIAL_DATA = {
|
||||
const INITIAL_DATA: APIReturnType<'GET /internal/apm/traces/{traceId}'> = {
|
||||
errorDocs: [],
|
||||
traceDocs: [],
|
||||
exceedsMax: false,
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
};
|
||||
|
||||
export function useWaterfallFetcher() {
|
||||
|
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* 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 { EuiBadge, EuiFlexGroup, EuiFlexItem, EuiToolTip } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers';
|
||||
|
||||
type Props = SpanLinksCount & { id: string };
|
||||
|
||||
export function SpanLinksBadge({ linkedParents, linkedChildren, id }: Props) {
|
||||
if (!linkedParents && !linkedChildren) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const total = linkedParents + linkedChildren;
|
||||
return (
|
||||
<EuiToolTip
|
||||
title={i18n.translate('xpack.apm.waterfall.spanLinks.tooltip.title', {
|
||||
defaultMessage:
|
||||
'{total} {total, plural, one {Span link} other {Span links}} found',
|
||||
values: { total },
|
||||
})}
|
||||
content={
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.apm.waterfall.spanLinks.tooltip.linkedChildren',
|
||||
{
|
||||
defaultMessage: '{linkedChildren} incoming',
|
||||
values: { linkedChildren },
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate(
|
||||
'xpack.apm.waterfall.spanLinks.tooltip.linkedParents',
|
||||
{
|
||||
defaultMessage: '{linkedParents} outgoing',
|
||||
values: { linkedParents },
|
||||
}
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
}
|
||||
>
|
||||
<EuiBadge data-test-subj={`spanLinksBadge_${id}`}>
|
||||
{i18n.translate('xpack.apm.waterfall.spanLinks.badge', {
|
||||
defaultMessage:
|
||||
'{total} {total, plural, one {Span link} other {Span links}}',
|
||||
values: { total },
|
||||
})}
|
||||
</EuiBadge>
|
||||
</EuiToolTip>
|
||||
);
|
||||
}
|
|
@ -20,24 +20,27 @@ import {
|
|||
EuiToolTip,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { Fragment } from 'react';
|
||||
import { isEmpty } from 'lodash';
|
||||
import { euiStyled } from '@kbn/kibana-react-plugin/common';
|
||||
import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/summary/composite_span_duration_summary_item';
|
||||
import { isEmpty } from 'lodash';
|
||||
import React, { Fragment } from 'react';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import { DiscoverSpanLink } from '../../../../../../shared/links/discover_links/discover_span_link';
|
||||
import { SpanMetadata } from '../../../../../../shared/metadata_table/span_metadata';
|
||||
import { getSpanLinksTabContent } from '../../../../../../shared/span_links/span_links_tab_content';
|
||||
import { Stacktrace } from '../../../../../../shared/stacktrace';
|
||||
import { Summary } from '../../../../../../shared/summary';
|
||||
import { CompositeSpanDurationSummaryItem } from '../../../../../../shared/summary/composite_span_duration_summary_item';
|
||||
import { DurationSummaryItem } from '../../../../../../shared/summary/duration_summary_item';
|
||||
import { HttpInfoSummaryItem } from '../../../../../../shared/summary/http_info_summary_item';
|
||||
import { TimestampTooltip } from '../../../../../../shared/timestamp_tooltip';
|
||||
import { ResponsiveFlyout } from '../responsive_flyout';
|
||||
import { SyncBadge } from '../badge/sync_badge';
|
||||
import { FailureBadge } from '../failure_badge';
|
||||
import { ResponsiveFlyout } from '../responsive_flyout';
|
||||
import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers';
|
||||
import { SpanDatabase } from './span_db';
|
||||
import { StickySpanProperties } from './sticky_span_properties';
|
||||
import { FailureBadge } from '../failure_badge';
|
||||
import { ProcessorEvent } from '../../../../../../../../common/processor_event';
|
||||
|
||||
function formatType(type: string) {
|
||||
switch (type) {
|
||||
|
@ -86,6 +89,7 @@ interface Props {
|
|||
parentTransaction?: Transaction;
|
||||
totalDuration?: number;
|
||||
onClose: () => void;
|
||||
spanLinksCount: SpanLinksCount;
|
||||
}
|
||||
|
||||
export function SpanFlyout({
|
||||
|
@ -93,6 +97,7 @@ export function SpanFlyout({
|
|||
parentTransaction,
|
||||
totalDuration,
|
||||
onClose,
|
||||
spanLinksCount,
|
||||
}: Props) {
|
||||
if (!span) {
|
||||
return null;
|
||||
|
@ -107,6 +112,13 @@ export function SpanFlyout({
|
|||
const spanHttpUrl = span.url?.original || span.span?.http?.url?.original;
|
||||
const spanHttpMethod = span.http?.request?.method || span.span?.http?.method;
|
||||
|
||||
const spanLinksTabContent = getSpanLinksTabContent({
|
||||
spanLinksCount,
|
||||
traceId: span.trace.id,
|
||||
spanId: span.span.id,
|
||||
processorEvent: ProcessorEvent.span,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<ResponsiveFlyout onClose={onClose} size="m" ownFocus={true}>
|
||||
|
@ -254,6 +266,7 @@ export function SpanFlyout({
|
|||
},
|
||||
]
|
||||
: []),
|
||||
...(spanLinksTabContent ? [spanLinksTabContent] : []),
|
||||
]}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
|
|
|
@ -10,19 +10,23 @@ import {
|
|||
EuiFlexItem,
|
||||
EuiFlyoutBody,
|
||||
EuiFlyoutHeader,
|
||||
EuiHorizontalRule,
|
||||
EuiPortal,
|
||||
EuiSpacer,
|
||||
EuiTabbedContent,
|
||||
EuiTitle,
|
||||
EuiHorizontalRule,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { ProcessorEvent } from '../../../../../../../../common/processor_event';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/transaction_action_menu';
|
||||
import { TransactionMetadata } from '../../../../../../shared/metadata_table/transaction_metadata';
|
||||
import { getSpanLinksTabContent } from '../../../../../../shared/span_links/span_links_tab_content';
|
||||
import { TransactionSummary } from '../../../../../../shared/summary/transaction_summary';
|
||||
import { TransactionActionMenu } from '../../../../../../shared/transaction_action_menu/transaction_action_menu';
|
||||
import { FlyoutTopLevelProperties } from '../flyout_top_level_properties';
|
||||
import { ResponsiveFlyout } from '../responsive_flyout';
|
||||
import { TransactionMetadata } from '../../../../../../shared/metadata_table/transaction_metadata';
|
||||
import { SpanLinksCount } from '../waterfall_helpers/waterfall_helpers';
|
||||
import { DroppedSpansWarning } from './dropped_spans_warning';
|
||||
|
||||
interface Props {
|
||||
|
@ -30,21 +34,7 @@ interface Props {
|
|||
transaction?: Transaction;
|
||||
errorCount?: number;
|
||||
rootTransactionDuration?: number;
|
||||
}
|
||||
|
||||
function TransactionPropertiesTable({
|
||||
transaction,
|
||||
}: {
|
||||
transaction: Transaction;
|
||||
}) {
|
||||
return (
|
||||
<div>
|
||||
<EuiTitle size="s">
|
||||
<h4>Metadata</h4>
|
||||
</EuiTitle>
|
||||
<TransactionMetadata transaction={transaction} />
|
||||
</div>
|
||||
);
|
||||
spanLinksCount: SpanLinksCount;
|
||||
}
|
||||
|
||||
export function TransactionFlyout({
|
||||
|
@ -52,11 +42,19 @@ export function TransactionFlyout({
|
|||
onClose,
|
||||
errorCount = 0,
|
||||
rootTransactionDuration,
|
||||
spanLinksCount,
|
||||
}: Props) {
|
||||
if (!transactionDoc) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const spanLinksTabContent = getSpanLinksTabContent({
|
||||
spanLinksCount,
|
||||
traceId: transactionDoc.trace.id,
|
||||
spanId: transactionDoc.transaction.id,
|
||||
processorEvent: ProcessorEvent.transaction,
|
||||
});
|
||||
|
||||
return (
|
||||
<EuiPortal>
|
||||
<ResponsiveFlyout onClose={onClose} ownFocus={true} maxWidth={false}>
|
||||
|
@ -94,7 +92,26 @@ export function TransactionFlyout({
|
|||
/>
|
||||
<EuiHorizontalRule margin="m" />
|
||||
<DroppedSpansWarning transactionDoc={transactionDoc} />
|
||||
<TransactionPropertiesTable transaction={transactionDoc} />
|
||||
<EuiTabbedContent
|
||||
tabs={[
|
||||
{
|
||||
id: 'metadata',
|
||||
name: i18n.translate(
|
||||
'xpack.apm.propertiesTable.tabs.metadataLabel',
|
||||
{
|
||||
defaultMessage: 'Metadata',
|
||||
}
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<TransactionMetadata transaction={transactionDoc} />
|
||||
</>
|
||||
),
|
||||
},
|
||||
...(spanLinksTabContent ? [spanLinksTabContent] : []),
|
||||
]}
|
||||
/>
|
||||
</EuiFlyoutBody>
|
||||
</ResponsiveFlyout>
|
||||
</EuiPortal>
|
||||
|
|
|
@ -45,6 +45,7 @@ export function WaterfallFlyout({
|
|||
span={currentItem.doc}
|
||||
parentTransaction={parentTransaction}
|
||||
onClose={() => toggleFlyout({ history })}
|
||||
spanLinksCount={currentItem.spanLinksCount}
|
||||
/>
|
||||
);
|
||||
case 'transaction':
|
||||
|
@ -56,6 +57,7 @@ export function WaterfallFlyout({
|
|||
waterfall.rootTransaction?.transaction.duration.us
|
||||
}
|
||||
errorCount={waterfall.getErrorCount(currentItem.id)}
|
||||
spanLinksCount={currentItem.spanLinksCount}
|
||||
/>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -190,18 +190,38 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -389,18 +409,38 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"mySpanIdD": Array [
|
||||
|
@ -516,12 +556,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"myTransactionId1": Array [
|
||||
|
@ -596,9 +648,17 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"myTransactionId2": Array [
|
||||
|
@ -751,15 +811,31 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"root": Array [
|
||||
|
@ -797,6 +873,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -835,6 +915,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"errorItems": Array [
|
||||
Object {
|
||||
|
@ -907,6 +991,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
|
@ -948,6 +1036,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1020,9 +1112,17 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1136,12 +1236,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1292,15 +1404,31 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1488,18 +1616,38 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1687,18 +1835,38 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId1",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"legends": Array [
|
||||
|
@ -1869,12 +2037,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -1994,12 +2174,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"mySpanIdD": Array [
|
||||
|
@ -2047,6 +2239,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"myTransactionId2": Array [
|
||||
|
@ -2131,9 +2327,17 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
|
@ -2182,6 +2386,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"errorItems": Array [],
|
||||
"getErrorCount": [Function],
|
||||
|
@ -2230,6 +2438,10 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2312,9 +2524,17 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2434,12 +2654,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2559,12 +2791,24 @@ Object {
|
|||
"parent": undefined,
|
||||
"parentId": "mySpanIdD",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "myTransactionId2",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "mySpanIdA",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
],
|
||||
"legends": Array [
|
||||
|
@ -2687,6 +2931,10 @@ Array [
|
|||
"offset": 0,
|
||||
"parent": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2740,9 +2988,17 @@ Array [
|
|||
"offset": 0,
|
||||
"parent": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "a",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2796,9 +3052,17 @@ Array [
|
|||
"offset": 0,
|
||||
"parent": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "a",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2877,12 +3141,24 @@ Array [
|
|||
"offset": 0,
|
||||
"parent": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "a",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "b",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
Object {
|
||||
"color": "",
|
||||
|
@ -2989,15 +3265,31 @@ Array [
|
|||
"offset": 0,
|
||||
"parent": undefined,
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "a",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "b",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
"parentId": "c",
|
||||
"skew": 0,
|
||||
"spanLinksCount": Object {
|
||||
"linkedChildren": 0,
|
||||
"linkedParents": 0,
|
||||
},
|
||||
},
|
||||
]
|
||||
`;
|
||||
|
|
|
@ -133,6 +133,7 @@ describe('waterfall_helpers', () => {
|
|||
traceDocs: hits,
|
||||
errorDocs,
|
||||
exceedsMax: false,
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
};
|
||||
const waterfall = getWaterfall(apiResp, entryTransactionId);
|
||||
const { apiResponse, ...waterfallRest } = waterfall;
|
||||
|
@ -151,6 +152,7 @@ describe('waterfall_helpers', () => {
|
|||
traceDocs: hits,
|
||||
errorDocs,
|
||||
exceedsMax: false,
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
};
|
||||
const waterfall = getWaterfall(apiResp, entryTransactionId);
|
||||
|
||||
|
@ -236,6 +238,7 @@ describe('waterfall_helpers', () => {
|
|||
traceDocs: traceItems,
|
||||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
|
@ -342,6 +345,7 @@ describe('waterfall_helpers', () => {
|
|||
traceDocs: traceItems,
|
||||
errorDocs: [],
|
||||
exceedsMax: false,
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
},
|
||||
entryTransactionId
|
||||
);
|
||||
|
@ -404,6 +408,10 @@ describe('waterfall_helpers', () => {
|
|||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedChildren: 0,
|
||||
linkedParents: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
docType: 'span',
|
||||
|
@ -426,6 +434,10 @@ describe('waterfall_helpers', () => {
|
|||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedChildren: 0,
|
||||
linkedParents: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
docType: 'span',
|
||||
|
@ -448,6 +460,10 @@ describe('waterfall_helpers', () => {
|
|||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedChildren: 0,
|
||||
linkedParents: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
docType: 'transaction',
|
||||
|
@ -464,6 +480,10 @@ describe('waterfall_helpers', () => {
|
|||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedChildren: 0,
|
||||
linkedParents: 0,
|
||||
},
|
||||
},
|
||||
{
|
||||
docType: 'transaction',
|
||||
|
@ -481,6 +501,10 @@ describe('waterfall_helpers', () => {
|
|||
skew: 0,
|
||||
legendValues,
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedChildren: 0,
|
||||
linkedParents: 0,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
|
|
|
@ -7,10 +7,11 @@
|
|||
|
||||
import { euiPaletteColorBlind } from '@elastic/eui';
|
||||
import { first, flatten, groupBy, isEmpty, sortBy, uniq } from 'lodash';
|
||||
import { APIReturnType } from '../../../../../../../services/rest/create_call_apm_api';
|
||||
import { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
import { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
||||
import { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import type { APIReturnType } from '../../../../../../../services/rest/create_call_apm_api';
|
||||
import type { APMError } from '../../../../../../../../typings/es_schemas/ui/apm_error';
|
||||
import type { Span } from '../../../../../../../../typings/es_schemas/ui/span';
|
||||
import type { Transaction } from '../../../../../../../../typings/es_schemas/ui/transaction';
|
||||
import { ProcessorEvent } from '../../../../../../../../common/processor_event';
|
||||
|
||||
type TraceAPIResponse = APIReturnType<'GET /internal/apm/traces/{traceId}'>;
|
||||
|
||||
|
@ -20,6 +21,11 @@ interface IWaterfallGroup {
|
|||
|
||||
const ROOT_ID = 'root';
|
||||
|
||||
export interface SpanLinksCount {
|
||||
linkedChildren: number;
|
||||
linkedParents: number;
|
||||
}
|
||||
|
||||
export enum WaterfallLegendType {
|
||||
ServiceName = 'serviceName',
|
||||
SpanType = 'spanType',
|
||||
|
@ -48,6 +54,7 @@ interface IWaterfallSpanItemBase<TDocument, TDoctype>
|
|||
*/
|
||||
duration: number;
|
||||
legendValues: Record<WaterfallLegendType, string>;
|
||||
spanLinksCount: SpanLinksCount;
|
||||
}
|
||||
|
||||
interface IWaterfallItemBase<TDocument, TDoctype> {
|
||||
|
@ -93,13 +100,17 @@ function getLegendValues(transactionOrSpan: Transaction | Span) {
|
|||
return {
|
||||
[WaterfallLegendType.ServiceName]: transactionOrSpan.service.name,
|
||||
[WaterfallLegendType.SpanType]:
|
||||
'span' in transactionOrSpan
|
||||
? transactionOrSpan.span.subtype || transactionOrSpan.span.type
|
||||
transactionOrSpan.processor.event === ProcessorEvent.span
|
||||
? (transactionOrSpan as Span).span.subtype ||
|
||||
(transactionOrSpan as Span).span.type
|
||||
: '',
|
||||
};
|
||||
}
|
||||
|
||||
function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
|
||||
function getTransactionItem(
|
||||
transaction: Transaction,
|
||||
linkedChildrenCount: number = 0
|
||||
): IWaterfallTransaction {
|
||||
return {
|
||||
docType: 'transaction',
|
||||
doc: transaction,
|
||||
|
@ -110,10 +121,17 @@ function getTransactionItem(transaction: Transaction): IWaterfallTransaction {
|
|||
skew: 0,
|
||||
legendValues: getLegendValues(transaction),
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedParents: transaction.span?.links?.length ?? 0,
|
||||
linkedChildren: linkedChildrenCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function getSpanItem(span: Span): IWaterfallSpan {
|
||||
function getSpanItem(
|
||||
span: Span,
|
||||
linkedChildrenCount: number = 0
|
||||
): IWaterfallSpan {
|
||||
return {
|
||||
docType: 'span',
|
||||
doc: span,
|
||||
|
@ -124,6 +142,10 @@ function getSpanItem(span: Span): IWaterfallSpan {
|
|||
skew: 0,
|
||||
legendValues: getLegendValues(span),
|
||||
color: '',
|
||||
spanLinksCount: {
|
||||
linkedParents: span.span.links?.length ?? 0,
|
||||
linkedChildren: linkedChildrenCount,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -265,14 +287,26 @@ const getWaterfallDuration = (waterfallItems: IWaterfallItem[]) =>
|
|||
0
|
||||
);
|
||||
|
||||
const getWaterfallItems = (items: TraceAPIResponse['traceDocs']) =>
|
||||
const getWaterfallItems = (
|
||||
items: TraceAPIResponse['traceDocs'],
|
||||
linkedChildrenOfSpanCountBySpanId: TraceAPIResponse['linkedChildrenOfSpanCountBySpanId']
|
||||
) =>
|
||||
items.map((item) => {
|
||||
const docType: 'span' | 'transaction' = item.processor.event;
|
||||
switch (docType) {
|
||||
case 'span':
|
||||
return getSpanItem(item as Span);
|
||||
case 'span': {
|
||||
const span = item as Span;
|
||||
return getSpanItem(
|
||||
span,
|
||||
linkedChildrenOfSpanCountBySpanId[span.span.id]
|
||||
);
|
||||
}
|
||||
case 'transaction':
|
||||
return getTransactionItem(item as Transaction);
|
||||
const transaction = item as Transaction;
|
||||
return getTransactionItem(
|
||||
transaction,
|
||||
linkedChildrenOfSpanCountBySpanId[transaction.transaction.id]
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -396,7 +430,8 @@ export function getWaterfall(
|
|||
const errorCountByParentId = getErrorCountByParentId(apiResponse.errorDocs);
|
||||
|
||||
const waterfallItems: IWaterfallSpanOrTransaction[] = getWaterfallItems(
|
||||
apiResponse.traceDocs
|
||||
apiResponse.traceDocs,
|
||||
apiResponse.linkedChildrenOfSpanCountBySpanId
|
||||
);
|
||||
|
||||
const childrenByParentId = getChildrenGroupedByParentId(
|
||||
|
|
|
@ -19,6 +19,7 @@ import { asDuration } from '../../../../../../../common/utils/formatters';
|
|||
import { Margins } from '../../../../../shared/charts/timeline';
|
||||
import { TruncateWithTooltip } from '../../../../../shared/truncate_with_tooltip';
|
||||
import { SyncBadge } from './badge/sync_badge';
|
||||
import { SpanLinksBadge } from './badge/span_links_badge';
|
||||
import { ColdStartBadge } from './badge/cold_start_badge';
|
||||
import { IWaterfallSpanOrTransaction } from './waterfall_helpers/waterfall_helpers';
|
||||
import { FailureBadge } from './failure_badge';
|
||||
|
@ -237,6 +238,11 @@ export function WaterfallItem({
|
|||
agentName={item.doc.agent.name}
|
||||
/>
|
||||
)}
|
||||
<SpanLinksBadge
|
||||
linkedParents={item.spanLinksCount.linkedParents}
|
||||
linkedChildren={item.spanLinksCount.linkedChildren}
|
||||
id={item.id}
|
||||
/>
|
||||
{isServerlessColdstart && <ColdStartBadge />}
|
||||
</ItemText>
|
||||
</Container>
|
||||
|
|
|
@ -530,6 +530,7 @@ export const simpleTrace = {
|
|||
],
|
||||
exceedsMax: false,
|
||||
errorDocs: [],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
} as TraceAPIResponse;
|
||||
|
||||
export const manyChildrenWithSameLength = {
|
||||
|
@ -4126,6 +4127,7 @@ export const manyChildrenWithSameLength = {
|
|||
},
|
||||
},
|
||||
],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
} as TraceAPIResponse;
|
||||
|
||||
export const traceWithErrors = {
|
||||
|
@ -4716,6 +4718,7 @@ export const traceWithErrors = {
|
|||
},
|
||||
},
|
||||
],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
} as unknown as TraceAPIResponse;
|
||||
|
||||
export const traceChildStartBeforeParent = {
|
||||
|
@ -5211,6 +5214,7 @@ export const traceChildStartBeforeParent = {
|
|||
],
|
||||
exceedsMax: false,
|
||||
errorDocs: [],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
} as TraceAPIResponse;
|
||||
|
||||
export const inferredSpans = {
|
||||
|
@ -5865,4 +5869,5 @@ export const inferredSpans = {
|
|||
],
|
||||
exceedsMax: false,
|
||||
errorDocs: [],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
} as TraceAPIResponse;
|
||||
|
|
|
@ -21,7 +21,7 @@ const CentralizedContainer = euiStyled.div`
|
|||
export function TransactionLink() {
|
||||
const {
|
||||
path: { transactionId },
|
||||
query: { rangeFrom, rangeTo },
|
||||
query: { rangeFrom, rangeTo, waterfallItemId },
|
||||
} = useApmParams('/link-to/transaction/{transactionId}');
|
||||
|
||||
const { data = { transaction: null }, status } = useFetcher(
|
||||
|
@ -46,6 +46,7 @@ export function TransactionLink() {
|
|||
transaction: data.transaction,
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
waterfallItemId,
|
||||
})}
|
||||
/>
|
||||
);
|
||||
|
|
|
@ -43,6 +43,7 @@ const apmRoutes = {
|
|||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
waterfallItemId: t.string,
|
||||
}),
|
||||
}),
|
||||
]),
|
||||
|
|
153
x-pack/plugins/apm/public/components/shared/span_links/index.tsx
Normal file
153
x-pack/plugins/apm/public/components/shared/span_links/index.tsx
Normal file
|
@ -0,0 +1,153 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiLoadingSpinner,
|
||||
EuiSelect,
|
||||
EuiSelectOption,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo, useState } from 'react';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { FETCH_STATUS, useFetcher } from '../../../hooks/use_fetcher';
|
||||
import { useTimeRange } from '../../../hooks/use_time_range';
|
||||
import { SpanLinksCount } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { KueryBar } from '../kuery_bar';
|
||||
import { SpanLinksCallout } from './span_links_callout';
|
||||
import { SpanLinksTable } from './span_links_table';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
import { useLocalStorage } from '../../../hooks/use_local_storage';
|
||||
|
||||
interface Props {
|
||||
spanLinksCount: SpanLinksCount;
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
processorEvent: ProcessorEvent;
|
||||
}
|
||||
|
||||
type LinkType = 'children' | 'parents';
|
||||
|
||||
export function SpanLinks({
|
||||
spanLinksCount,
|
||||
traceId,
|
||||
spanId,
|
||||
processorEvent,
|
||||
}: Props) {
|
||||
const {
|
||||
query: { rangeFrom, rangeTo },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const { start, end } = useTimeRange({ rangeFrom, rangeTo });
|
||||
|
||||
const [selectedLinkType, setSelectedLinkType] = useState<LinkType>(
|
||||
spanLinksCount.linkedChildren ? 'children' : 'parents'
|
||||
);
|
||||
|
||||
const [spanLinksCalloutDismissed, setSpanLinksCalloutDismissed] =
|
||||
useLocalStorage('apm.spanLinksCalloutDismissed', false);
|
||||
|
||||
const [kuery, setKuery] = useState('');
|
||||
|
||||
const { data, status } = useFetcher(
|
||||
(callApmApi) => {
|
||||
if (selectedLinkType === 'children') {
|
||||
return callApmApi(
|
||||
'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children',
|
||||
{
|
||||
params: {
|
||||
path: { traceId, spanId },
|
||||
query: { kuery, start, end },
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
return callApmApi(
|
||||
'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents',
|
||||
{
|
||||
params: {
|
||||
path: { traceId, spanId },
|
||||
query: { kuery, start, end, processorEvent },
|
||||
},
|
||||
}
|
||||
);
|
||||
},
|
||||
[selectedLinkType, kuery, traceId, spanId, start, end, processorEvent]
|
||||
);
|
||||
|
||||
const selectOptions: EuiSelectOption[] = useMemo(
|
||||
() => [
|
||||
{
|
||||
value: 'children',
|
||||
text: i18n.translate('xpack.apm.spanLinks.combo.childrenLinks', {
|
||||
defaultMessage: 'Incoming links ({linkedChildren})',
|
||||
values: { linkedChildren: spanLinksCount.linkedChildren },
|
||||
}),
|
||||
disabled: !spanLinksCount.linkedChildren,
|
||||
},
|
||||
{
|
||||
value: 'parents',
|
||||
text: i18n.translate('xpack.apm.spanLinks.combo.parentsLinks', {
|
||||
defaultMessage: 'Outgoing links ({linkedParents})',
|
||||
values: { linkedParents: spanLinksCount.linkedParents },
|
||||
}),
|
||||
disabled: !spanLinksCount.linkedParents,
|
||||
},
|
||||
],
|
||||
[spanLinksCount]
|
||||
);
|
||||
|
||||
if (
|
||||
!data ||
|
||||
status === FETCH_STATUS.LOADING ||
|
||||
status === FETCH_STATUS.NOT_INITIATED
|
||||
) {
|
||||
return (
|
||||
<div style={{ display: 'flex', justifyContent: 'center' }}>
|
||||
<EuiLoadingSpinner />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="m">
|
||||
{!spanLinksCalloutDismissed && (
|
||||
<EuiFlexItem>
|
||||
<SpanLinksCallout
|
||||
dismissCallout={() => {
|
||||
setSpanLinksCalloutDismissed(true);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup gutterSize="s">
|
||||
<EuiFlexItem>
|
||||
<KueryBar
|
||||
onSubmit={(value) => {
|
||||
setKuery(value);
|
||||
}}
|
||||
value={kuery}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiSelect
|
||||
data-test-subj="spanLinkTypeSelect"
|
||||
options={selectOptions}
|
||||
value={selectedLinkType}
|
||||
onChange={(e) => {
|
||||
setSelectedLinkType(e.target.value as LinkType);
|
||||
}}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<SpanLinksTable items={data.spanLinksDetails} />
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* 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 { EuiButton, EuiCallOut } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { FormattedMessage } from '@kbn/i18n-react';
|
||||
|
||||
interface Props {
|
||||
dismissCallout: () => void;
|
||||
}
|
||||
|
||||
export function SpanLinksCallout({ dismissCallout }: Props) {
|
||||
return (
|
||||
<EuiCallOut
|
||||
title={i18n.translate('xpack.apm.spanLinks.callout.title', {
|
||||
defaultMessage: 'Span links',
|
||||
})}
|
||||
iconType="iInCircle"
|
||||
>
|
||||
<p>
|
||||
<FormattedMessage
|
||||
id="xpack.apm.spanLinks.callout.description"
|
||||
defaultMessage="A Link is a pointer from the current span to another span in the same trace or in a different trace. For example. this can be used in batching operations, where a single batch handler processes multiple requests from different traces or when the handler receives a request from a different project."
|
||||
/>
|
||||
</p>
|
||||
<EuiButton
|
||||
onClick={() => {
|
||||
dismissCallout();
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.apm.spanLinks.callout.dimissButton', {
|
||||
defaultMessage: 'Dismiss',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiCallOut>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { EuiNotificationBadge, EuiSpacer } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { SpanLinks } from '.';
|
||||
import { SpanLinksCount } from '../../app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers';
|
||||
import { ProcessorEvent } from '../../../../common/processor_event';
|
||||
|
||||
interface Props {
|
||||
spanLinksCount: SpanLinksCount;
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
processorEvent: ProcessorEvent;
|
||||
}
|
||||
|
||||
export function getSpanLinksTabContent({
|
||||
spanLinksCount,
|
||||
traceId,
|
||||
spanId,
|
||||
processorEvent,
|
||||
}: Props) {
|
||||
if (!spanLinksCount.linkedChildren && !spanLinksCount.linkedParents) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
id: 'span_links',
|
||||
'data-test-subj': 'spanLinksTab',
|
||||
name: (
|
||||
<>
|
||||
{i18n.translate('xpack.apm.propertiesTable.tabs.spanLinks', {
|
||||
defaultMessage: 'Span links',
|
||||
})}
|
||||
</>
|
||||
),
|
||||
append: (
|
||||
<EuiNotificationBadge color="subdued">
|
||||
{spanLinksCount.linkedChildren + spanLinksCount.linkedParents}
|
||||
</EuiNotificationBadge>
|
||||
),
|
||||
content: (
|
||||
<>
|
||||
<EuiSpacer size="m" />
|
||||
<SpanLinks
|
||||
spanLinksCount={spanLinksCount}
|
||||
traceId={traceId}
|
||||
spanId={spanId}
|
||||
processorEvent={processorEvent}
|
||||
/>
|
||||
</>
|
||||
),
|
||||
};
|
||||
}
|
|
@ -0,0 +1,234 @@
|
|||
/*
|
||||
* 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 {
|
||||
EuiBasicTableColumn,
|
||||
EuiButtonEmpty,
|
||||
EuiButtonIcon,
|
||||
EuiCopy,
|
||||
EuiFlexGroup,
|
||||
EuiFlexItem,
|
||||
EuiIcon,
|
||||
EuiInMemoryTable,
|
||||
EuiLink,
|
||||
EuiPopover,
|
||||
EuiText,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState } from 'react';
|
||||
import { SpanLinkDetails } from '../../../../common/span_links';
|
||||
import { asDuration } from '../../../../common/utils/formatters';
|
||||
import { useApmParams } from '../../../hooks/use_apm_params';
|
||||
import { useApmRouter } from '../../../hooks/use_apm_router';
|
||||
import { ServiceLink } from '../service_link';
|
||||
import { getSpanIcon } from '../span_icon/get_span_icon';
|
||||
|
||||
interface Props {
|
||||
items: SpanLinkDetails[];
|
||||
}
|
||||
|
||||
export function SpanLinksTable({ items }: Props) {
|
||||
const router = useApmRouter();
|
||||
const {
|
||||
query: { rangeFrom, rangeTo, comparisonEnabled },
|
||||
} = useApmParams('/services/{serviceName}/transactions/view');
|
||||
const [idActionMenuOpen, setIdActionMenuOpen] = useState<
|
||||
string | undefined
|
||||
>();
|
||||
|
||||
const columns: Array<EuiBasicTableColumn<SpanLinkDetails>> = [
|
||||
{
|
||||
field: 'serviceName',
|
||||
name: i18n.translate('xpack.apm.spanLinks.table.serviceName', {
|
||||
defaultMessage: 'Service name',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (_, { details }) => {
|
||||
if (details) {
|
||||
return (
|
||||
<ServiceLink
|
||||
serviceName={details.serviceName}
|
||||
agentName={details.agentName}
|
||||
query={{
|
||||
rangeFrom,
|
||||
rangeTo,
|
||||
kuery: '',
|
||||
serviceGroup: '',
|
||||
comparisonEnabled,
|
||||
environment: details.environment || 'ENVIRONMENT_ALL',
|
||||
}}
|
||||
/>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<EuiFlexGroup alignItems="center" gutterSize="xs" responsive={false}>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="stopSlash" size="m" color="subdued" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
{i18n.translate('xpack.apm.spanLinks.table.serviceName.unknown', {
|
||||
defaultMessage: 'Unknown',
|
||||
})}
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'spanId',
|
||||
name: i18n.translate('xpack.apm.spanLinks.table.span', {
|
||||
defaultMessage: 'Span',
|
||||
}),
|
||||
sortable: true,
|
||||
render: (_, { spanId, traceId, details }) => {
|
||||
if (details && details.transactionId) {
|
||||
return (
|
||||
<EuiFlexGroup
|
||||
alignItems="center"
|
||||
gutterSize="xs"
|
||||
responsive={false}
|
||||
>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon
|
||||
type={getSpanIcon(details.spanType, details.spanSubtype)}
|
||||
size="l"
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
href={router.link('/link-to/transaction/{transactionId}', {
|
||||
path: { transactionId: details.transactionId },
|
||||
query: { waterfallItemId: spanId },
|
||||
})}
|
||||
>
|
||||
{details.spanName}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
||||
return `${traceId}-${spanId}`;
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'duration',
|
||||
name: i18n.translate('xpack.apm.spanLinks.table.spanDuration', {
|
||||
defaultMessage: 'Span duration',
|
||||
}),
|
||||
sortable: true,
|
||||
width: '150',
|
||||
render: (_, { details }) => {
|
||||
return (
|
||||
<EuiText size="s" color="subdued">
|
||||
{asDuration(details?.duration)}
|
||||
</EuiText>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
field: 'actions',
|
||||
name: 'Actions',
|
||||
width: '100',
|
||||
render: (_, { spanId, traceId, details }) => {
|
||||
const id = `${traceId}:${spanId}`;
|
||||
return (
|
||||
<EuiPopover
|
||||
button={
|
||||
<EuiButtonIcon
|
||||
aria-label="Edit"
|
||||
iconType="boxesHorizontal"
|
||||
onClick={() => {
|
||||
setIdActionMenuOpen(id);
|
||||
}}
|
||||
/>
|
||||
}
|
||||
isOpen={idActionMenuOpen === id}
|
||||
closePopover={() => {
|
||||
setIdActionMenuOpen(undefined);
|
||||
}}
|
||||
>
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{details?.transactionId && (
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
href={router.link('/link-to/transaction/{transactionId}', {
|
||||
path: { transactionId: details.transactionId },
|
||||
})}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.spanLinks.table.actions.goToTraceDetails',
|
||||
{ defaultMessage: 'Go to trace' }
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiCopy textToCopy={traceId}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
copy();
|
||||
setIdActionMenuOpen(undefined);
|
||||
}}
|
||||
flush="both"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.spanLinks.table.actions.copyParentTraceId',
|
||||
{ defaultMessage: 'Copy parent trace id' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
{details?.transactionId && (
|
||||
<EuiFlexItem>
|
||||
<EuiLink
|
||||
href={router.link('/link-to/transaction/{transactionId}', {
|
||||
path: { transactionId: details.transactionId },
|
||||
query: { waterfallItemId: spanId },
|
||||
})}
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.spanLinks.table.actions.goToSpanDetails',
|
||||
{ defaultMessage: 'Go to span details' }
|
||||
)}
|
||||
</EuiLink>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
<EuiFlexItem>
|
||||
<EuiCopy textToCopy={spanId}>
|
||||
{(copy) => (
|
||||
<EuiButtonEmpty
|
||||
onClick={() => {
|
||||
copy();
|
||||
setIdActionMenuOpen(undefined);
|
||||
}}
|
||||
flush="both"
|
||||
>
|
||||
{i18n.translate(
|
||||
'xpack.apm.spanLinks.table.actions.copySpanId',
|
||||
{ defaultMessage: 'Copy span id' }
|
||||
)}
|
||||
</EuiButtonEmpty>
|
||||
)}
|
||||
</EuiCopy>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</EuiPopover>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<EuiInMemoryTable
|
||||
items={items}
|
||||
columns={columns}
|
||||
sorting={true}
|
||||
pagination={true}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -37,6 +37,7 @@ import { historicalDataRouteRepository } from '../historical_data/route';
|
|||
import { eventMetadataRouteRepository } from '../event_metadata/route';
|
||||
import { suggestionsRouteRepository } from '../suggestions/route';
|
||||
import { agentKeysRouteRepository } from '../agent_keys/route';
|
||||
import { spanLinksRouteRepository } from '../span_links/route';
|
||||
|
||||
function getTypedGlobalApmServerRouteRepository() {
|
||||
const repository = {
|
||||
|
@ -67,6 +68,7 @@ function getTypedGlobalApmServerRouteRepository() {
|
|||
...historicalDataRouteRepository,
|
||||
...eventMetadataRouteRepository,
|
||||
...agentKeysRouteRepository,
|
||||
...spanLinksRouteRepository,
|
||||
};
|
||||
|
||||
return repository;
|
||||
|
|
|
@ -0,0 +1,141 @@
|
|||
/*
|
||||
* 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 { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { isEmpty } from 'lodash';
|
||||
import {
|
||||
PROCESSOR_EVENT,
|
||||
SPAN_ID,
|
||||
SPAN_LINKS,
|
||||
SPAN_LINKS_TRACE_ID,
|
||||
SPAN_LINKS_SPAN_ID,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import type { SpanRaw } from '../../../typings/es_schemas/raw/span_raw';
|
||||
import type { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { getBufferedTimerange } from './utils';
|
||||
|
||||
async function fetchLinkedChildrenOfSpan({
|
||||
traceId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
spanId,
|
||||
}: {
|
||||
traceId: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
spanId?: string;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const { startWithBuffer, endWithBuffer } = getBufferedTimerange({
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const response = await apmEventClient.search(
|
||||
'fetch_linked_children_of_span',
|
||||
{
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
_source: [SPAN_LINKS, TRACE_ID, SPAN_ID, PROCESSOR_EVENT, TRANSACTION_ID],
|
||||
body: {
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...rangeQuery(startWithBuffer, endWithBuffer),
|
||||
{ term: { [SPAN_LINKS_TRACE_ID]: traceId } },
|
||||
...(spanId ? [{ term: { [SPAN_LINKS_SPAN_ID]: spanId } }] : []),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
// Filter out documents that don't have any span.links that match the combination of traceId and spanId
|
||||
return response.hits.hits.filter(({ _source: source }) => {
|
||||
const spanLinks = source.span?.links?.filter((spanLink) => {
|
||||
return (
|
||||
spanLink.trace.id === traceId &&
|
||||
(spanId ? spanLink.span.id === spanId : true)
|
||||
);
|
||||
});
|
||||
return !isEmpty(spanLinks);
|
||||
});
|
||||
}
|
||||
|
||||
function getSpanId(source: TransactionRaw | SpanRaw) {
|
||||
return source.processor.event === ProcessorEvent.span
|
||||
? (source as SpanRaw).span.id
|
||||
: (source as TransactionRaw).transaction?.id;
|
||||
}
|
||||
|
||||
export async function getLinkedChildrenCountBySpanId({
|
||||
traceId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
traceId: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const linkedChildren = await fetchLinkedChildrenOfSpan({
|
||||
traceId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
return linkedChildren.reduce<Record<string, number>>(
|
||||
(acc, { _source: source }) => {
|
||||
source.span?.links?.forEach((link) => {
|
||||
// Ignores span links that don't belong to this trace
|
||||
if (link.trace.id === traceId) {
|
||||
acc[link.span.id] = (acc[link.span.id] || 0) + 1;
|
||||
}
|
||||
});
|
||||
return acc;
|
||||
},
|
||||
{}
|
||||
);
|
||||
}
|
||||
|
||||
export async function getLinkedChildrenOfSpan({
|
||||
traceId,
|
||||
spanId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const linkedChildren = await fetchLinkedChildrenOfSpan({
|
||||
traceId,
|
||||
spanId,
|
||||
setup,
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
return linkedChildren.map(({ _source: source }) => {
|
||||
return {
|
||||
trace: { id: source.trace.id },
|
||||
span: { id: getSpanId(source) },
|
||||
};
|
||||
});
|
||||
}
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* 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 { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import {
|
||||
SPAN_ID,
|
||||
SPAN_LINKS,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
PROCESSOR_EVENT,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { SpanRaw } from '../../../typings/es_schemas/raw/span_raw';
|
||||
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
|
||||
export async function getLinkedParentsOfSpan({
|
||||
setup,
|
||||
traceId,
|
||||
spanId,
|
||||
start,
|
||||
end,
|
||||
processorEvent,
|
||||
}: {
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
setup: Setup;
|
||||
start: number;
|
||||
end: number;
|
||||
processorEvent: ProcessorEvent;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const response = await apmEventClient.search('get_linked_parents_of_span', {
|
||||
apm: {
|
||||
events: [processorEvent],
|
||||
},
|
||||
_source: [SPAN_LINKS],
|
||||
body: {
|
||||
size: 1,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...rangeQuery(start, end),
|
||||
{ term: { [TRACE_ID]: traceId } },
|
||||
{ exists: { field: SPAN_LINKS } },
|
||||
{ term: { [PROCESSOR_EVENT]: processorEvent } },
|
||||
...(processorEvent === ProcessorEvent.transaction
|
||||
? [{ term: { [TRANSACTION_ID]: spanId } }]
|
||||
: [{ term: { [SPAN_ID]: spanId } }]),
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const source = response.hits.hits?.[0]?._source as TransactionRaw | SpanRaw;
|
||||
|
||||
return source?.span?.links || [];
|
||||
}
|
|
@ -0,0 +1,212 @@
|
|||
/*
|
||||
* 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 { kqlQuery, rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { chunk, compact, isEmpty, keyBy } from 'lodash';
|
||||
import {
|
||||
SERVICE_NAME,
|
||||
SPAN_ID,
|
||||
SPAN_NAME,
|
||||
TRACE_ID,
|
||||
TRANSACTION_ID,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
SPAN_DURATION,
|
||||
PROCESSOR_EVENT,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
AGENT_NAME,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { Environment } from '../../../common/environment_rt';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { SpanLinkDetails } from '../../../common/span_links';
|
||||
import { SpanLink } from '../../../typings/es_schemas/raw/fields/span_links';
|
||||
import { SpanRaw } from '../../../typings/es_schemas/raw/span_raw';
|
||||
import { TransactionRaw } from '../../../typings/es_schemas/raw/transaction_raw';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { getBufferedTimerange } from './utils';
|
||||
|
||||
async function fetchSpanLinksDetails({
|
||||
setup,
|
||||
kuery,
|
||||
spanLinks,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
setup: Setup;
|
||||
kuery: string;
|
||||
spanLinks: SpanLink[];
|
||||
start: number;
|
||||
end: number;
|
||||
}) {
|
||||
const { apmEventClient } = setup;
|
||||
|
||||
const { startWithBuffer, endWithBuffer } = getBufferedTimerange({
|
||||
start,
|
||||
end,
|
||||
});
|
||||
|
||||
const response = await apmEventClient.search('get_span_links_details', {
|
||||
apm: {
|
||||
events: [ProcessorEvent.span, ProcessorEvent.transaction],
|
||||
},
|
||||
_source: [
|
||||
TRACE_ID,
|
||||
SPAN_ID,
|
||||
TRANSACTION_ID,
|
||||
SERVICE_NAME,
|
||||
SPAN_NAME,
|
||||
TRANSACTION_NAME,
|
||||
TRANSACTION_DURATION,
|
||||
SPAN_DURATION,
|
||||
PROCESSOR_EVENT,
|
||||
SPAN_SUBTYPE,
|
||||
SPAN_TYPE,
|
||||
AGENT_NAME,
|
||||
],
|
||||
body: {
|
||||
size: 1000,
|
||||
query: {
|
||||
bool: {
|
||||
filter: [
|
||||
...rangeQuery(startWithBuffer, endWithBuffer),
|
||||
...kqlQuery(kuery),
|
||||
{
|
||||
bool: {
|
||||
should: spanLinks.map((item) => {
|
||||
return {
|
||||
bool: {
|
||||
filter: [
|
||||
{ term: { [TRACE_ID]: item.trace.id } },
|
||||
{
|
||||
bool: {
|
||||
should: [
|
||||
{ term: { [SPAN_ID]: item.span.id } },
|
||||
{ term: { [TRANSACTION_ID]: item.span.id } },
|
||||
],
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
}),
|
||||
minimum_should_match: 1,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const spanIdsMap = keyBy(spanLinks, 'span.id');
|
||||
|
||||
return response.hits.hits.filter(({ _source: source }) => {
|
||||
// The above query might return other spans from the same transaction because siblings spans share the same transaction.id
|
||||
// so, if it is a span we need to guarantee that the span.id is the same as the span links ids
|
||||
if (source.processor.event === ProcessorEvent.span) {
|
||||
const span = source as SpanRaw;
|
||||
const hasSpanId = spanIdsMap[span.span.id] || false;
|
||||
return hasSpanId;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function getSpanLinksDetails({
|
||||
setup,
|
||||
spanLinks,
|
||||
kuery,
|
||||
start,
|
||||
end,
|
||||
}: {
|
||||
setup: Setup;
|
||||
spanLinks: SpanLink[];
|
||||
kuery: string;
|
||||
start: number;
|
||||
end: number;
|
||||
}): Promise<SpanLinkDetails[]> {
|
||||
if (!spanLinks.length) {
|
||||
return [];
|
||||
}
|
||||
|
||||
// chunk span links to avoid too_many_nested_clauses problem
|
||||
const spanLinksChunks = chunk(spanLinks, 500);
|
||||
const chunkedResponses = await Promise.all(
|
||||
spanLinksChunks.map((spanLinksChunk) =>
|
||||
fetchSpanLinksDetails({
|
||||
setup,
|
||||
kuery,
|
||||
spanLinks: spanLinksChunk,
|
||||
start,
|
||||
end,
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
const linkedSpans = chunkedResponses.flat();
|
||||
|
||||
// Creates a map for all span links details found
|
||||
const spanLinksDetailsMap = linkedSpans.reduce<
|
||||
Record<string, SpanLinkDetails>
|
||||
>((acc, { _source: source }) => {
|
||||
const commonDetails = {
|
||||
serviceName: source.service.name,
|
||||
agentName: source.agent.name,
|
||||
environment: source.service.environment as Environment,
|
||||
transactionId: source.transaction?.id,
|
||||
};
|
||||
|
||||
if (source.processor.event === ProcessorEvent.transaction) {
|
||||
const transaction = source as TransactionRaw;
|
||||
const key = `${transaction.trace.id}:${transaction.transaction.id}`;
|
||||
acc[key] = {
|
||||
traceId: source.trace.id,
|
||||
spanId: transaction.transaction.id,
|
||||
details: {
|
||||
...commonDetails,
|
||||
spanName: transaction.transaction.name,
|
||||
duration: transaction.transaction.duration.us,
|
||||
},
|
||||
};
|
||||
} else {
|
||||
const span = source as SpanRaw;
|
||||
const key = `${span.trace.id}:${span.span.id}`;
|
||||
acc[key] = {
|
||||
traceId: source.trace.id,
|
||||
spanId: span.span.id,
|
||||
details: {
|
||||
...commonDetails,
|
||||
spanName: span.span.name,
|
||||
duration: span.span.duration.us,
|
||||
spanSubtype: span.span.subtype,
|
||||
spanType: span.span.type,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return acc;
|
||||
}, {});
|
||||
|
||||
// It's important to keep the original order of the span links,
|
||||
// so loops trough the original list merging external links and links with details.
|
||||
// external links are links that the details were not found in the ES query.
|
||||
return compact(
|
||||
spanLinks.map((item) => {
|
||||
const key = `${item.trace.id}:${item.span.id}`;
|
||||
const details = spanLinksDetailsMap[key];
|
||||
if (details) {
|
||||
return details;
|
||||
}
|
||||
|
||||
// When kuery is not set, returns external links, if not hides this item.
|
||||
return isEmpty(kuery)
|
||||
? { traceId: item.trace.id, spanId: item.span.id }
|
||||
: undefined;
|
||||
})
|
||||
);
|
||||
}
|
103
x-pack/plugins/apm/server/routes/span_links/route.ts
Normal file
103
x-pack/plugins/apm/server/routes/span_links/route.ts
Normal file
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* 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 { setupRequest } from '../../lib/helpers/setup_request';
|
||||
import { createApmServerRoute } from '../apm_routes/create_apm_server_route';
|
||||
import { getSpanLinksDetails } from './get_span_links_details';
|
||||
import { getLinkedChildrenOfSpan } from './get_linked_children';
|
||||
import { kueryRt, rangeRt } from '../default_api_types';
|
||||
import { SpanLinkDetails } from '../../../common/span_links';
|
||||
import { processorEventRt } from '../../../common/processor_event';
|
||||
import { getLinkedParentsOfSpan } from './get_linked_parents';
|
||||
|
||||
const linkedParentsRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
traceId: t.string,
|
||||
spanId: t.string,
|
||||
}),
|
||||
query: t.intersection([
|
||||
kueryRt,
|
||||
rangeRt,
|
||||
t.type({ processorEvent: processorEventRt }),
|
||||
]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
spanLinksDetails: SpanLinkDetails[];
|
||||
}> => {
|
||||
const {
|
||||
params: { query, path },
|
||||
} = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const linkedParents = await getLinkedParentsOfSpan({
|
||||
setup,
|
||||
traceId: path.traceId,
|
||||
spanId: path.spanId,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
processorEvent: query.processorEvent,
|
||||
});
|
||||
|
||||
return {
|
||||
spanLinksDetails: await getSpanLinksDetails({
|
||||
setup,
|
||||
spanLinks: linkedParents,
|
||||
kuery: query.kuery,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const linkedChildrenRoute = createApmServerRoute({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children',
|
||||
params: t.type({
|
||||
path: t.type({
|
||||
traceId: t.string,
|
||||
spanId: t.string,
|
||||
}),
|
||||
query: t.intersection([kueryRt, rangeRt]),
|
||||
}),
|
||||
options: { tags: ['access:apm'] },
|
||||
handler: async (
|
||||
resources
|
||||
): Promise<{
|
||||
spanLinksDetails: SpanLinkDetails[];
|
||||
}> => {
|
||||
const {
|
||||
params: { query, path },
|
||||
} = resources;
|
||||
const setup = await setupRequest(resources);
|
||||
const linkedChildren = await getLinkedChildrenOfSpan({
|
||||
setup,
|
||||
traceId: path.traceId,
|
||||
spanId: path.spanId,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
});
|
||||
|
||||
return {
|
||||
spanLinksDetails: await getSpanLinksDetails({
|
||||
setup,
|
||||
spanLinks: linkedChildren,
|
||||
kuery: query.kuery,
|
||||
start: query.start,
|
||||
end: query.end,
|
||||
}),
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const spanLinksRouteRepository = {
|
||||
...linkedParentsRoute,
|
||||
...linkedChildrenRoute,
|
||||
};
|
23
x-pack/plugins/apm/server/routes/span_links/utils.ts
Normal file
23
x-pack/plugins/apm/server/routes/span_links/utils.ts
Normal 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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import moment from 'moment';
|
||||
|
||||
export function getBufferedTimerange({
|
||||
start,
|
||||
end,
|
||||
bufferSize = 4,
|
||||
}: {
|
||||
start: number;
|
||||
end: number;
|
||||
bufferSize?: number;
|
||||
}) {
|
||||
return {
|
||||
startWithBuffer: moment(start).subtract(bufferSize, 'days').valueOf(),
|
||||
endWithBuffer: moment(end).add(bufferSize, 'days').valueOf(),
|
||||
};
|
||||
}
|
|
@ -10,15 +10,16 @@ import {
|
|||
Sort,
|
||||
} from '@elastic/elasticsearch/lib/api/typesWithBodyKey';
|
||||
import { rangeQuery } from '@kbn/observability-plugin/server';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import {
|
||||
ERROR_LOG_LEVEL,
|
||||
PARENT_ID,
|
||||
SPAN_DURATION,
|
||||
TRACE_ID,
|
||||
TRANSACTION_DURATION,
|
||||
SPAN_DURATION,
|
||||
PARENT_ID,
|
||||
ERROR_LOG_LEVEL,
|
||||
} from '../../../common/elasticsearch_fieldnames';
|
||||
import { ProcessorEvent } from '../../../common/processor_event';
|
||||
import { Setup } from '../../lib/helpers/setup_request';
|
||||
import { getLinkedChildrenCountBySpanId } from '../span_links/get_linked_children';
|
||||
|
||||
export async function getTraceItems(
|
||||
traceId: string,
|
||||
|
@ -74,12 +75,21 @@ export async function getTraceItems(
|
|||
},
|
||||
});
|
||||
|
||||
const errorResponse = await errorResponsePromise;
|
||||
const traceResponse = await traceResponsePromise;
|
||||
const [errorResponse, traceResponse, linkedChildrenOfSpanCountBySpanId] =
|
||||
await Promise.all([
|
||||
errorResponsePromise,
|
||||
traceResponsePromise,
|
||||
getLinkedChildrenCountBySpanId({ traceId, setup, start, end }),
|
||||
]);
|
||||
|
||||
const exceedsMax = traceResponse.hits.total.value > maxTraceItems;
|
||||
const traceDocs = traceResponse.hits.hits.map((hit) => hit._source);
|
||||
const errorDocs = errorResponse.hits.hits.map((hit) => hit._source);
|
||||
|
||||
return { exceedsMax, traceDocs, errorDocs };
|
||||
return {
|
||||
exceedsMax,
|
||||
traceDocs,
|
||||
errorDocs,
|
||||
linkedChildrenOfSpanCountBySpanId,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -82,6 +82,7 @@ const tracesByIdRoute = createApmServerRoute({
|
|||
errorDocs: Array<
|
||||
import('./../../../typings/es_schemas/ui/apm_error').APMError
|
||||
>;
|
||||
linkedChildrenOfSpanCountBySpanId: Record<string, number>;
|
||||
}> => {
|
||||
const setup = await setupRequest(resources);
|
||||
const { params } = resources;
|
||||
|
|
|
@ -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; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export interface SpanLink {
|
||||
trace: { id: string };
|
||||
span: { id: string };
|
||||
}
|
|
@ -8,6 +8,7 @@
|
|||
import { APMBaseDoc } from './apm_base_doc';
|
||||
import { EventOutcome } from './fields/event_outcome';
|
||||
import { Http } from './fields/http';
|
||||
import { SpanLink } from './fields/span_links';
|
||||
import { Stackframe } from './fields/stackframe';
|
||||
import { TimestampUs } from './fields/timestamp_us';
|
||||
import { Url } from './fields/url';
|
||||
|
@ -63,6 +64,7 @@ export interface SpanRaw extends APMBaseDoc {
|
|||
sum: { us: number };
|
||||
compression_strategy: string;
|
||||
};
|
||||
links?: SpanLink[];
|
||||
};
|
||||
timestamp: TimestampUs;
|
||||
transaction?: {
|
||||
|
|
|
@ -20,6 +20,7 @@ import { Url } from './fields/url';
|
|||
import { User } from './fields/user';
|
||||
import { UserAgent } from './fields/user_agent';
|
||||
import { Faas } from './fields/faas';
|
||||
import { SpanLink } from './fields/span_links';
|
||||
|
||||
interface Processor {
|
||||
name: 'transaction';
|
||||
|
@ -71,4 +72,7 @@ export interface TransactionRaw extends APMBaseDoc {
|
|||
user_agent?: UserAgent;
|
||||
cloud?: Cloud;
|
||||
faas?: Faas;
|
||||
span?: {
|
||||
links?: SpanLink[];
|
||||
};
|
||||
}
|
||||
|
|
|
@ -628,8 +628,7 @@
|
|||
{
|
||||
"type": "index",
|
||||
"value": {
|
||||
"aliases": {
|
||||
},
|
||||
"aliases": {},
|
||||
"index": ".ml-config",
|
||||
"mappings": {
|
||||
"_meta": {
|
||||
|
@ -15510,6 +15509,26 @@
|
|||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"links": {
|
||||
"properties": {
|
||||
"span": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -20620,6 +20639,26 @@
|
|||
"type": {
|
||||
"ignore_above": 1024,
|
||||
"type": "keyword"
|
||||
},
|
||||
"links": {
|
||||
"properties": {
|
||||
"span": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
},
|
||||
"trace": {
|
||||
"properties": {
|
||||
"id": {
|
||||
"type": "keyword",
|
||||
"ignore_above": 1024
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
|
|
@ -0,0 +1,300 @@
|
|||
/*
|
||||
* 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 { apm, timerange } from '@elastic/apm-synthtrace';
|
||||
import { SpanLink } from '@kbn/apm-plugin/typings/es_schemas/raw/fields/span_links';
|
||||
import uuid from 'uuid';
|
||||
|
||||
function getProducerInternalOnly() {
|
||||
const producerInternalOnlyInstance = apm
|
||||
.service('producer-internal-only', 'production', 'go')
|
||||
.instance('instance a');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:00:00.000Z'),
|
||||
new Date('2022-01-01T00:01:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerInternalOnlyInstance
|
||||
.transaction(`Transaction A`)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerInternalOnlyInstance
|
||||
.span(`Span A`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionA = apmFields.find((item) => item['processor.event'] === 'transaction');
|
||||
const spanA = apmFields.find((item) => item['processor.event'] === 'span');
|
||||
|
||||
const ids = {
|
||||
transactionAId: transactionA?.['transaction.id']!,
|
||||
traceId: spanA?.['trace.id']!,
|
||||
spanAId: spanA?.['span.id']!,
|
||||
};
|
||||
const spanASpanLink = {
|
||||
trace: { id: spanA?.['trace.id']! },
|
||||
span: { id: spanA?.['span.id']! },
|
||||
};
|
||||
|
||||
return {
|
||||
ids,
|
||||
spanASpanLink,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getProducerExternalOnly() {
|
||||
const producerExternalOnlyInstance = apm
|
||||
.service('producer-external-only', 'production', 'java')
|
||||
.instance('instance b');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:02:00.000Z'),
|
||||
new Date('2022-01-01T00:03:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerExternalOnlyInstance
|
||||
.transaction(`Transaction B`)
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerExternalOnlyInstance
|
||||
.span(`Span B`, 'external', 'http')
|
||||
.defaults({
|
||||
'span.links': [{ trace: { id: 'trace#1' }, span: { id: 'span#1' } }],
|
||||
})
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success(),
|
||||
producerExternalOnlyInstance
|
||||
.span(`Span B.1`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionB = apmFields.find((item) => item['processor.event'] === 'transaction');
|
||||
const spanB = apmFields.find(
|
||||
(item) => item['processor.event'] === 'span' && item['span.name'] === 'Span B'
|
||||
);
|
||||
const ids = {
|
||||
traceId: spanB?.['trace.id']!,
|
||||
transactionBId: transactionB?.['transaction.id']!,
|
||||
spanBId: spanB?.['span.id']!,
|
||||
};
|
||||
|
||||
const spanBSpanLink = {
|
||||
trace: { id: spanB?.['trace.id']! },
|
||||
span: { id: spanB?.['span.id']! },
|
||||
};
|
||||
|
||||
const transactionBSpanLink = {
|
||||
trace: { id: transactionB?.['trace.id']! },
|
||||
span: { id: transactionB?.['transaction.id']! },
|
||||
};
|
||||
|
||||
return {
|
||||
ids,
|
||||
spanBSpanLink,
|
||||
transactionBSpanLink,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getProducerConsumer({
|
||||
producerInternalOnlySpanASpanLink,
|
||||
producerExternalOnlySpanBLink,
|
||||
producerExternalOnlyTransactionBLink,
|
||||
}: {
|
||||
producerInternalOnlySpanASpanLink: SpanLink;
|
||||
producerExternalOnlySpanBLink: SpanLink;
|
||||
producerExternalOnlyTransactionBLink: SpanLink;
|
||||
}) {
|
||||
const externalTraceId = uuid.v4();
|
||||
|
||||
const producerConsumerInstance = apm
|
||||
.service('producer-consumer', 'production', 'ruby')
|
||||
.instance('instance c');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:04:00.000Z'),
|
||||
new Date('2022-01-01T00:05:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return producerConsumerInstance
|
||||
.transaction(`Transaction C`)
|
||||
.defaults({
|
||||
'span.links': [
|
||||
producerInternalOnlySpanASpanLink,
|
||||
producerExternalOnlyTransactionBLink,
|
||||
{ trace: { id: externalTraceId }, span: { id: producerExternalOnlySpanBLink.span.id } },
|
||||
],
|
||||
})
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
producerConsumerInstance
|
||||
.span(`Span C`, 'external', 'http')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
|
||||
const apmFields = events.toArray();
|
||||
const transactionC = apmFields.find((item) => item['processor.event'] === 'transaction');
|
||||
const transactionCSpanLink = {
|
||||
trace: { id: transactionC?.['trace.id']! },
|
||||
span: { id: transactionC?.['transaction.id']! },
|
||||
};
|
||||
const spanC = apmFields.find(
|
||||
(item) => item['processor.event'] === 'span' || item['span.name'] === 'Span C'
|
||||
);
|
||||
const spanCSpanLink = {
|
||||
trace: { id: spanC?.['trace.id']! },
|
||||
span: { id: spanC?.['span.id']! },
|
||||
};
|
||||
const ids = {
|
||||
traceId: transactionC?.['trace.id']!,
|
||||
transactionCId: transactionC?.['transaction.id']!,
|
||||
spanCId: spanC?.['span.id']!,
|
||||
externalTraceId,
|
||||
};
|
||||
return {
|
||||
transactionCSpanLink,
|
||||
spanCSpanLink,
|
||||
ids,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
function getConsumerMultiple({
|
||||
producerInternalOnlySpanALink,
|
||||
producerExternalOnlySpanBLink,
|
||||
producerConsumerSpanCLink,
|
||||
producerConsumerTransactionCLink,
|
||||
}: {
|
||||
producerInternalOnlySpanALink: SpanLink;
|
||||
producerExternalOnlySpanBLink: SpanLink;
|
||||
producerConsumerSpanCLink: SpanLink;
|
||||
producerConsumerTransactionCLink: SpanLink;
|
||||
}) {
|
||||
const consumerMultipleInstance = apm
|
||||
.service('consumer-multiple', 'production', 'nodejs')
|
||||
.instance('instance d');
|
||||
|
||||
const events = timerange(
|
||||
new Date('2022-01-01T00:06:00.000Z'),
|
||||
new Date('2022-01-01T00:07:00.000Z')
|
||||
)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return consumerMultipleInstance
|
||||
.transaction(`Transaction D`)
|
||||
.defaults({ 'span.links': [producerInternalOnlySpanALink, producerConsumerSpanCLink] })
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.success()
|
||||
.children(
|
||||
consumerMultipleInstance
|
||||
.span(`Span E`, 'external', 'http')
|
||||
.defaults({
|
||||
'span.links': [producerExternalOnlySpanBLink, producerConsumerTransactionCLink],
|
||||
})
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(100)
|
||||
.success()
|
||||
);
|
||||
});
|
||||
const apmFields = events.toArray();
|
||||
const transactionD = apmFields.find((item) => item['processor.event'] === 'transaction');
|
||||
const spanE = apmFields.find((item) => item['processor.event'] === 'span');
|
||||
|
||||
const ids = {
|
||||
traceId: transactionD?.['trace.id']!,
|
||||
transactionDId: transactionD?.['transaction.id']!,
|
||||
spanEId: spanE?.['span.id']!,
|
||||
};
|
||||
|
||||
return {
|
||||
ids,
|
||||
apmFields,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Data ingestion summary:
|
||||
*
|
||||
* producer-internal-only (go)
|
||||
* --Transaction A
|
||||
* ----Span A
|
||||
*
|
||||
* producer-external-only (java)
|
||||
* --Transaction B
|
||||
* ----Span B
|
||||
* ------span.links=external link
|
||||
* ----Span B1
|
||||
*
|
||||
* producer-consumer (ruby)
|
||||
* --Transaction C
|
||||
* ------span.links=Service A / Span A
|
||||
* ------span.links=Service B / Transaction B
|
||||
* ------span.links=External ID / Span B
|
||||
* ----Span C
|
||||
*
|
||||
* consumer-multiple (nodejs)
|
||||
* --Transaction D
|
||||
* ------span.links= Service C / Span C | Service A / Span A
|
||||
* ----Span E
|
||||
* ------span.links= Service B / Span B | Service C / Transaction C
|
||||
*/
|
||||
export function generateSpanLinksData() {
|
||||
const producerInternalOnly = getProducerInternalOnly();
|
||||
const producerExternalOnly = getProducerExternalOnly();
|
||||
const producerConsumer = getProducerConsumer({
|
||||
producerInternalOnlySpanASpanLink: producerInternalOnly.spanASpanLink,
|
||||
producerExternalOnlySpanBLink: producerExternalOnly.spanBSpanLink,
|
||||
producerExternalOnlyTransactionBLink: producerExternalOnly.transactionBSpanLink,
|
||||
});
|
||||
const producerMultiple = getConsumerMultiple({
|
||||
producerInternalOnlySpanALink: producerInternalOnly.spanASpanLink,
|
||||
producerExternalOnlySpanBLink: producerExternalOnly.spanBSpanLink,
|
||||
producerConsumerSpanCLink: producerConsumer.spanCSpanLink,
|
||||
producerConsumerTransactionCLink: producerConsumer.transactionCSpanLink,
|
||||
});
|
||||
return {
|
||||
apmFields: {
|
||||
producerInternalOnly: producerInternalOnly.apmFields,
|
||||
producerExternalOnly: producerExternalOnly.apmFields,
|
||||
producerConsumer: producerConsumer.apmFields,
|
||||
producerMultiple: producerMultiple.apmFields,
|
||||
},
|
||||
ids: {
|
||||
producerInternalOnly: producerInternalOnly.ids,
|
||||
producerExternalOnly: producerExternalOnly.ids,
|
||||
producerConsumer: producerConsumer.ids,
|
||||
producerMultiple: producerMultiple.ids,
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,496 @@
|
|||
/*
|
||||
* 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 { EntityArrayIterable } from '@elastic/apm-synthtrace';
|
||||
import { ProcessorEvent } from '@kbn/apm-plugin/common/processor_event';
|
||||
import expect from '@kbn/expect';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { generateSpanLinksData } from './data_generator';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
registry.when(
|
||||
'contains linked children',
|
||||
{ config: 'basic', archives: ['apm_mappings_only_8.0.0'] },
|
||||
() => {
|
||||
let ids: ReturnType<typeof generateSpanLinksData>['ids'];
|
||||
|
||||
before(async () => {
|
||||
const spanLinksData = generateSpanLinksData();
|
||||
|
||||
ids = spanLinksData.ids;
|
||||
|
||||
await synthtraceEsClient.index(
|
||||
new EntityArrayIterable(spanLinksData.apmFields.producerInternalOnly).merge(
|
||||
new EntityArrayIterable(spanLinksData.apmFields.producerExternalOnly),
|
||||
new EntityArrayIterable(spanLinksData.apmFields.producerConsumer),
|
||||
new EntityArrayIterable(spanLinksData.apmFields.producerMultiple)
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('Span links count on traces', () => {
|
||||
async function fetchTraces({ traceId }: { traceId: string }) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId },
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
describe('producer-internal-only trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
before(async () => {
|
||||
const tracesResponse = await fetchTraces({ traceId: ids.producerInternalOnly.traceId });
|
||||
traces = tracesResponse.body;
|
||||
});
|
||||
|
||||
it('contains two children link on Span A', () => {
|
||||
expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(1);
|
||||
expect(
|
||||
traces.linkedChildrenOfSpanCountBySpanId[ids.producerInternalOnly.spanAId]
|
||||
).to.equal(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('producer-external-only trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
before(async () => {
|
||||
const tracesResponse = await fetchTraces({ traceId: ids.producerExternalOnly.traceId });
|
||||
traces = tracesResponse.body;
|
||||
});
|
||||
|
||||
it('contains two children link on Span B', () => {
|
||||
expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2);
|
||||
expect(
|
||||
traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.spanBId]
|
||||
).to.equal(1);
|
||||
expect(
|
||||
traces.linkedChildrenOfSpanCountBySpanId[ids.producerExternalOnly.transactionBId]
|
||||
).to.equal(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('producer-consumer trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
before(async () => {
|
||||
const tracesResponse = await fetchTraces({ traceId: ids.producerConsumer.traceId });
|
||||
traces = tracesResponse.body;
|
||||
});
|
||||
|
||||
it('contains one children link on transaction C and two on span C', () => {
|
||||
expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(2);
|
||||
expect(
|
||||
traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.transactionCId]
|
||||
).to.equal(1);
|
||||
expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerConsumer.spanCId]).to.equal(
|
||||
1
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('consumer-multiple trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
before(async () => {
|
||||
const tracesResponse = await fetchTraces({ traceId: ids.producerMultiple.traceId });
|
||||
traces = tracesResponse.body;
|
||||
});
|
||||
|
||||
it('contains no children', () => {
|
||||
expect(Object.values(traces.linkedChildrenOfSpanCountBySpanId).length).to.equal(0);
|
||||
expect(
|
||||
traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.transactionDId]
|
||||
).to.equal(undefined);
|
||||
expect(traces.linkedChildrenOfSpanCountBySpanId[ids.producerMultiple.spanEId]).to.equal(
|
||||
undefined
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Span links details', () => {
|
||||
async function fetchChildrenAndParentsDetails({
|
||||
kuery,
|
||||
traceId,
|
||||
spanId,
|
||||
processorEvent,
|
||||
}: {
|
||||
kuery: string;
|
||||
traceId: string;
|
||||
spanId: string;
|
||||
processorEvent: ProcessorEvent;
|
||||
}) {
|
||||
const [childrenLinksResponse, parentsLinksResponse] = await Promise.all([
|
||||
await apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/children',
|
||||
params: {
|
||||
path: { traceId, spanId },
|
||||
query: {
|
||||
kuery,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
apmApiClient.readUser({
|
||||
endpoint: 'GET /internal/apm/traces/{traceId}/span_links/{spanId}/parents',
|
||||
params: {
|
||||
path: { traceId, spanId },
|
||||
query: {
|
||||
kuery,
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
processorEvent,
|
||||
},
|
||||
},
|
||||
}),
|
||||
]);
|
||||
|
||||
return {
|
||||
childrenLinks: childrenLinksResponse.body,
|
||||
parentsLinks: parentsLinksResponse.body,
|
||||
};
|
||||
}
|
||||
|
||||
describe('producer-internal-only span links details', () => {
|
||||
let transactionALinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
let spanALinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
before(async () => {
|
||||
const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all(
|
||||
[
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerInternalOnly.traceId,
|
||||
spanId: ids.producerInternalOnly.transactionAId,
|
||||
processorEvent: ProcessorEvent.transaction,
|
||||
}),
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerInternalOnly.traceId,
|
||||
spanId: ids.producerInternalOnly.spanAId,
|
||||
processorEvent: ProcessorEvent.span,
|
||||
}),
|
||||
]
|
||||
);
|
||||
transactionALinksDetails = transactionALinksDetailsResponse;
|
||||
spanALinksDetails = spanALinksDetailsResponse;
|
||||
});
|
||||
|
||||
it('returns no links for transaction A', () => {
|
||||
expect(transactionALinksDetails.childrenLinks.spanLinksDetails).to.eql([]);
|
||||
expect(transactionALinksDetails.parentsLinks.spanLinksDetails).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns no parents on Span A', () => {
|
||||
expect(spanALinksDetails.parentsLinks.spanLinksDetails).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns two children on Span A', () => {
|
||||
expect(spanALinksDetails.childrenLinks.spanLinksDetails.length).to.eql(2);
|
||||
const serviceCDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find(
|
||||
(childDetails) => {
|
||||
return (
|
||||
childDetails.traceId === ids.producerConsumer.traceId &&
|
||||
childDetails.spanId === ids.producerConsumer.transactionCId
|
||||
);
|
||||
}
|
||||
);
|
||||
expect(serviceCDetails?.details).to.eql({
|
||||
serviceName: 'producer-consumer',
|
||||
agentName: 'ruby',
|
||||
transactionId: ids.producerConsumer.transactionCId,
|
||||
spanName: 'Transaction C',
|
||||
duration: 1000000,
|
||||
});
|
||||
|
||||
const serviceDDetails = spanALinksDetails.childrenLinks.spanLinksDetails.find(
|
||||
(childDetails) => {
|
||||
return (
|
||||
childDetails.traceId === ids.producerMultiple.traceId &&
|
||||
childDetails.spanId === ids.producerMultiple.transactionDId
|
||||
);
|
||||
}
|
||||
);
|
||||
expect(serviceDDetails?.details).to.eql({
|
||||
serviceName: 'consumer-multiple',
|
||||
agentName: 'nodejs',
|
||||
transactionId: ids.producerMultiple.transactionDId,
|
||||
spanName: 'Transaction D',
|
||||
duration: 1000000,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('producer-external-only span links details', () => {
|
||||
let transactionBLinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
let spanBLinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
before(async () => {
|
||||
const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all(
|
||||
[
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerExternalOnly.traceId,
|
||||
spanId: ids.producerExternalOnly.transactionBId,
|
||||
processorEvent: ProcessorEvent.transaction,
|
||||
}),
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerExternalOnly.traceId,
|
||||
spanId: ids.producerExternalOnly.spanBId,
|
||||
processorEvent: ProcessorEvent.span,
|
||||
}),
|
||||
]
|
||||
);
|
||||
transactionBLinksDetails = transactionALinksDetailsResponse;
|
||||
spanBLinksDetails = spanALinksDetailsResponse;
|
||||
});
|
||||
|
||||
it('returns producer-consumer as children of transaction B', () => {
|
||||
expect(transactionBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
|
||||
});
|
||||
|
||||
it('returns no parent for transaction B', () => {
|
||||
expect(transactionBLinksDetails.parentsLinks.spanLinksDetails).to.eql([]);
|
||||
});
|
||||
|
||||
it('returns external parent on Span B', () => {
|
||||
expect(spanBLinksDetails.parentsLinks.spanLinksDetails.length).to.be(1);
|
||||
expect(spanBLinksDetails.parentsLinks.spanLinksDetails).to.eql([
|
||||
{ traceId: 'trace#1', spanId: 'span#1' },
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns consumer-multiple as child on Span B', () => {
|
||||
expect(spanBLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
|
||||
expect(spanBLinksDetails.childrenLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerMultiple.traceId,
|
||||
spanId: ids.producerMultiple.spanEId,
|
||||
details: {
|
||||
serviceName: 'consumer-multiple',
|
||||
agentName: 'nodejs',
|
||||
transactionId: ids.producerMultiple.transactionDId,
|
||||
spanName: 'Span E',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('producer-consumer span links details', () => {
|
||||
let transactionCLinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
let spanCLinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
before(async () => {
|
||||
const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all(
|
||||
[
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerConsumer.traceId,
|
||||
spanId: ids.producerConsumer.transactionCId,
|
||||
processorEvent: ProcessorEvent.transaction,
|
||||
}),
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerConsumer.traceId,
|
||||
spanId: ids.producerConsumer.spanCId,
|
||||
processorEvent: ProcessorEvent.span,
|
||||
}),
|
||||
]
|
||||
);
|
||||
transactionCLinksDetails = transactionALinksDetailsResponse;
|
||||
spanCLinksDetails = spanALinksDetailsResponse;
|
||||
});
|
||||
|
||||
it('returns producer-internal-only Span A, producer-external-only Transaction B, and External link as parents of Transaction C', () => {
|
||||
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(3);
|
||||
expect(transactionCLinksDetails.parentsLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerInternalOnly.traceId,
|
||||
spanId: ids.producerInternalOnly.spanAId,
|
||||
details: {
|
||||
serviceName: 'producer-internal-only',
|
||||
agentName: 'go',
|
||||
transactionId: ids.producerInternalOnly.transactionAId,
|
||||
spanName: 'Span A',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
{
|
||||
traceId: ids.producerExternalOnly.traceId,
|
||||
spanId: ids.producerExternalOnly.transactionBId,
|
||||
details: {
|
||||
serviceName: 'producer-external-only',
|
||||
agentName: 'java',
|
||||
transactionId: ids.producerExternalOnly.transactionBId,
|
||||
duration: 1000000,
|
||||
spanName: 'Transaction B',
|
||||
},
|
||||
},
|
||||
{
|
||||
traceId: ids.producerConsumer.externalTraceId,
|
||||
spanId: ids.producerExternalOnly.spanBId,
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns consumer-multiple Span E as child of Transaction C', () => {
|
||||
expect(transactionCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
|
||||
expect(transactionCLinksDetails.childrenLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerMultiple.traceId,
|
||||
spanId: ids.producerMultiple.spanEId,
|
||||
details: {
|
||||
serviceName: 'consumer-multiple',
|
||||
agentName: 'nodejs',
|
||||
transactionId: ids.producerMultiple.transactionDId,
|
||||
spanName: 'Span E',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no child on Span C', () => {
|
||||
expect(spanCLinksDetails.parentsLinks.spanLinksDetails.length).to.be(0);
|
||||
});
|
||||
|
||||
it('returns consumer-multiple as Child on producer-consumer', () => {
|
||||
expect(spanCLinksDetails.childrenLinks.spanLinksDetails.length).to.be(1);
|
||||
expect(spanCLinksDetails.childrenLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerMultiple.traceId,
|
||||
spanId: ids.producerMultiple.transactionDId,
|
||||
details: {
|
||||
serviceName: 'consumer-multiple',
|
||||
agentName: 'nodejs',
|
||||
transactionId: ids.producerMultiple.transactionDId,
|
||||
spanName: 'Transaction D',
|
||||
duration: 1000000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('consumer-multiple span links details', () => {
|
||||
let transactionDLinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
let spanELinksDetails: Awaited<ReturnType<typeof fetchChildrenAndParentsDetails>>;
|
||||
before(async () => {
|
||||
const [transactionALinksDetailsResponse, spanALinksDetailsResponse] = await Promise.all(
|
||||
[
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerMultiple.traceId,
|
||||
spanId: ids.producerMultiple.transactionDId,
|
||||
processorEvent: ProcessorEvent.transaction,
|
||||
}),
|
||||
fetchChildrenAndParentsDetails({
|
||||
kuery: '',
|
||||
traceId: ids.producerMultiple.traceId,
|
||||
spanId: ids.producerMultiple.spanEId,
|
||||
processorEvent: ProcessorEvent.span,
|
||||
}),
|
||||
]
|
||||
);
|
||||
transactionDLinksDetails = transactionALinksDetailsResponse;
|
||||
spanELinksDetails = spanALinksDetailsResponse;
|
||||
});
|
||||
|
||||
it('returns producer-internal-only Span A and producer-consumer Span C as parents of Transaction D', () => {
|
||||
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
|
||||
expect(transactionDLinksDetails.parentsLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerInternalOnly.traceId,
|
||||
spanId: ids.producerInternalOnly.spanAId,
|
||||
details: {
|
||||
serviceName: 'producer-internal-only',
|
||||
agentName: 'go',
|
||||
transactionId: ids.producerInternalOnly.transactionAId,
|
||||
spanName: 'Span A',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
{
|
||||
traceId: ids.producerConsumer.traceId,
|
||||
spanId: ids.producerConsumer.spanCId,
|
||||
details: {
|
||||
serviceName: 'producer-consumer',
|
||||
agentName: 'ruby',
|
||||
transactionId: ids.producerConsumer.transactionCId,
|
||||
spanName: 'Span C',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no children on Transaction D', () => {
|
||||
expect(transactionDLinksDetails.childrenLinks.spanLinksDetails.length).to.be(0);
|
||||
});
|
||||
|
||||
it('returns producer-external-only Span B and producer-consumer Transaction C as parents of Span E', () => {
|
||||
expect(spanELinksDetails.parentsLinks.spanLinksDetails.length).to.be(2);
|
||||
|
||||
expect(spanELinksDetails.parentsLinks.spanLinksDetails).to.eql([
|
||||
{
|
||||
traceId: ids.producerExternalOnly.traceId,
|
||||
spanId: ids.producerExternalOnly.spanBId,
|
||||
details: {
|
||||
serviceName: 'producer-external-only',
|
||||
agentName: 'java',
|
||||
transactionId: ids.producerExternalOnly.transactionBId,
|
||||
spanName: 'Span B',
|
||||
duration: 100000,
|
||||
spanSubtype: 'http',
|
||||
spanType: 'external',
|
||||
},
|
||||
},
|
||||
{
|
||||
traceId: ids.producerConsumer.traceId,
|
||||
spanId: ids.producerConsumer.transactionCId,
|
||||
details: {
|
||||
serviceName: 'producer-consumer',
|
||||
agentName: 'ruby',
|
||||
transactionId: ids.producerConsumer.transactionCId,
|
||||
spanName: 'Transaction C',
|
||||
duration: 1000000,
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
it('returns no children on Span E', () => {
|
||||
expect(spanELinksDetails.childrenLinks.spanLinksDetails.length).to.be(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
|
@ -4,84 +4,120 @@
|
|||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { apm, EntityArrayIterable, timerange } from '@elastic/apm-synthtrace';
|
||||
import expect from '@kbn/expect';
|
||||
import archives_metadata from '../../common/fixtures/es_archiver/archives_metadata';
|
||||
import { FtrProviderContext } from '../../common/ftr_provider_context';
|
||||
import { SupertestReturnType } from '../../common/apm_api_supertest';
|
||||
|
||||
export default function ApiTest({ getService }: FtrProviderContext) {
|
||||
const registry = getService('registry');
|
||||
const apmApiClient = getService('apmApiClient');
|
||||
const synthtraceEsClient = getService('synthtraceEsClient');
|
||||
|
||||
const archiveName = 'apm_8.0.0';
|
||||
const metadata = archives_metadata[archiveName];
|
||||
const { start, end } = metadata;
|
||||
const start = new Date('2022-01-01T00:00:00.000Z').getTime();
|
||||
const end = new Date('2022-01-01T00:15:00.000Z').getTime() - 1;
|
||||
|
||||
async function fetchTraces({
|
||||
traceId,
|
||||
query,
|
||||
}: {
|
||||
traceId: string;
|
||||
query: { start: string; end: string; _inspect?: boolean };
|
||||
}) {
|
||||
return await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId },
|
||||
query,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
registry.when('Trace does not exist', { config: 'basic', archives: [] }, () => {
|
||||
it('handles empty state', async () => {
|
||||
const response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: 'foo' },
|
||||
query: { start, end },
|
||||
const response = await fetchTraces({
|
||||
traceId: 'foo',
|
||||
query: {
|
||||
start: new Date(start).toISOString(),
|
||||
end: new Date(end).toISOString(),
|
||||
},
|
||||
});
|
||||
|
||||
expect(response.status).to.be(200);
|
||||
expect(response.body).to.eql({ exceedsMax: false, traceDocs: [], errorDocs: [] });
|
||||
expect(response.body).to.eql({
|
||||
exceedsMax: false,
|
||||
traceDocs: [],
|
||||
errorDocs: [],
|
||||
linkedChildrenOfSpanCountBySpanId: {},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
registry.when('Trace exists', { config: 'basic', archives: [archiveName] }, () => {
|
||||
let response: SupertestReturnType<`GET /internal/apm/traces/{traceId}`>;
|
||||
registry.when('Trace exists', { config: 'basic', archives: ['apm_mappings_only_8.0.0'] }, () => {
|
||||
let serviceATraceId: string;
|
||||
before(async () => {
|
||||
response = await apmApiClient.readUser({
|
||||
endpoint: `GET /internal/apm/traces/{traceId}`,
|
||||
params: {
|
||||
path: { traceId: '64d0014f7530df24e549dd17cc0a8895' },
|
||||
query: { start, end },
|
||||
},
|
||||
const instanceJava = apm.service('synth-apple', 'production', 'java').instance('instance-b');
|
||||
const events = timerange(start, end)
|
||||
.interval('1m')
|
||||
.rate(1)
|
||||
.generator((timestamp) => {
|
||||
return [
|
||||
instanceJava
|
||||
.transaction('GET /apple 🍏')
|
||||
.timestamp(timestamp)
|
||||
.duration(1000)
|
||||
.failure()
|
||||
.errors(
|
||||
instanceJava
|
||||
.error('[ResponseError] index_not_found_exception')
|
||||
.timestamp(timestamp + 50)
|
||||
)
|
||||
.children(
|
||||
instanceJava
|
||||
.span('get_green_apple_🍏', 'db', 'elasticsearch')
|
||||
.timestamp(timestamp + 50)
|
||||
.duration(900)
|
||||
.success()
|
||||
),
|
||||
];
|
||||
});
|
||||
const entities = events.toArray();
|
||||
serviceATraceId = entities.slice(0, 1)[0]['trace.id']!;
|
||||
|
||||
await synthtraceEsClient.index(new EntityArrayIterable(entities));
|
||||
});
|
||||
|
||||
after(() => synthtraceEsClient.clean());
|
||||
|
||||
describe('return trace', () => {
|
||||
let traces: Awaited<ReturnType<typeof fetchTraces>>['body'];
|
||||
before(async () => {
|
||||
const response = await fetchTraces({
|
||||
traceId: serviceATraceId,
|
||||
query: { start: new Date(start).toISOString(), end: new Date(end).toISOString() },
|
||||
});
|
||||
expect(response.status).to.eql(200);
|
||||
traces = response.body;
|
||||
});
|
||||
it('returns some errors', () => {
|
||||
expect(traces.errorDocs.length).to.be.greaterThan(0);
|
||||
expect(traces.errorDocs[0].error.exception?.[0].message).to.eql(
|
||||
'[ResponseError] index_not_found_exception'
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
it('returns the correct status code', async () => {
|
||||
expect(response.status).to.be(200);
|
||||
});
|
||||
|
||||
it('returns the correct number of buckets', async () => {
|
||||
expectSnapshot(response.body.errorDocs.map((doc) => doc.error?.exception?.[0]?.message))
|
||||
.toMatchInline(`
|
||||
Array [
|
||||
"Test CaptureError",
|
||||
"Uncaught Error: Test Error in dashboard",
|
||||
]
|
||||
`);
|
||||
expectSnapshot(
|
||||
response.body.traceDocs.map((doc) =>
|
||||
'span' in doc ? `${doc.span.name} (span)` : `${doc.transaction.name} (transaction)`
|
||||
)
|
||||
).toMatchInline(`
|
||||
Array [
|
||||
"/dashboard (transaction)",
|
||||
"GET /api/stats (transaction)",
|
||||
"APIRestController#topProducts (transaction)",
|
||||
"Parsing the document, executing sync. scripts (span)",
|
||||
"GET /api/products/top (span)",
|
||||
"GET /api/stats (span)",
|
||||
"Requesting and receiving the document (span)",
|
||||
"SELECT FROM customers (span)",
|
||||
"SELECT FROM order_lines (span)",
|
||||
"http://opbeans-frontend:3000/static/css/main.7bd7c5e8.css (span)",
|
||||
"SELECT FROM products (span)",
|
||||
"SELECT FROM orders (span)",
|
||||
"SELECT FROM order_lines (span)",
|
||||
"Making a connection to the server (span)",
|
||||
"Fire \\"load\\" event (span)",
|
||||
"empty query (span)",
|
||||
]
|
||||
`);
|
||||
expectSnapshot(response.body.exceedsMax).toMatchInline(`false`);
|
||||
it('returns some trace docs', () => {
|
||||
expect(traces.traceDocs.length).to.be.greaterThan(0);
|
||||
expect(
|
||||
traces.traceDocs.map((item) => {
|
||||
if (item.span && 'name' in item.span) {
|
||||
return item.span.name;
|
||||
}
|
||||
if (item.transaction && 'name' in item.transaction) {
|
||||
return item.transaction.name;
|
||||
}
|
||||
})
|
||||
).to.eql(['GET /apple 🍏', 'get_green_apple_🍏']);
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue