diff --git a/packages/kbn-apm-synthtrace/index.ts b/packages/kbn-apm-synthtrace/index.ts index 170c5ed6206c1..1ff59bdd7d16a 100644 --- a/packages/kbn-apm-synthtrace/index.ts +++ b/packages/kbn-apm-synthtrace/index.ts @@ -8,6 +8,7 @@ export { timerange } from './src/lib/timerange'; export { apm } from './src/lib/apm'; +export { dedot } from './src/lib/utils/dedot'; export { stackMonitoring } from './src/lib/stack_monitoring'; export { observer } from './src/lib/agent_config'; export { cleanWriteTargets } from './src/lib/utils/clean_write_targets'; diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts b/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts index 334c0f296851d..216397f1e1b40 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/apm_error.ts @@ -27,4 +27,10 @@ export class ApmError extends Serializable { ); return [data]; } + + timestamp(value: number) { + const ret = super.timestamp(value); + this.fields['timestamp.us'] = value * 1000; + return ret; + } } diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts b/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts index 0cfe5940405a2..b74604c39c242 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/base_span.ts @@ -88,4 +88,10 @@ export class BaseSpan extends Serializable { }); return this; } + + override timestamp(timestamp: number) { + const ret = super.timestamp(timestamp); + this.fields['timestamp.us'] = timestamp * 1000; + return ret; + } } diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts index f69e54b3e300b..9a7fff73b64a7 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/instance.ts @@ -20,24 +20,49 @@ export type SpanParams = { } & ApmFields; export class Instance extends Entity { - transaction({ - transactionName, - transactionType = 'request', - }: { - transactionName: string; - transactionType?: string; - }) { + transaction( + ...options: + | [{ transactionName: string; transactionType?: string }] + | [string] + | [string, string] + ) { + let transactionName: string; + let transactionType: string | undefined; + if (options.length === 2) { + transactionName = options[0]; + transactionType = options[1]; + } else if (typeof options[0] === 'string') { + transactionName = options[0]; + } else { + transactionName = options[0].transactionName; + transactionType = options[0].transactionType; + } + return new Transaction({ ...this.fields, 'transaction.name': transactionName, - 'transaction.type': transactionType, + 'transaction.type': transactionType || 'request', }); } - span({ spanName, spanType, spanSubtype, ...apmFields }: SpanParams) { + span(...options: [string, string] | [string, string, string] | [SpanParams]) { + let spanName: string; + let spanType: string; + let spanSubtype: string; + let fields: ApmFields; + + if (options.length === 3 || options.length === 2) { + spanName = options[0]; + spanType = options[1]; + spanSubtype = options[2] || 'unknown'; + fields = {}; + } else { + ({ spanName, spanType, spanSubtype = 'unknown', ...fields } = options[0]); + } + return new Span({ ...this.fields, - ...apmFields, + ...fields, 'span.name': spanName, 'span.type': spanType, 'span.subtype': spanSubtype, diff --git a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts index 0939535a87135..1925c0cdcfd13 100644 --- a/packages/kbn-apm-synthtrace/src/lib/apm/service.ts +++ b/packages/kbn-apm-synthtrace/src/lib/apm/service.ts @@ -20,17 +20,18 @@ export class Service extends Entity { } } -export function service({ - name, - environment, - agentName, -}: { - name: string; - environment: string; - agentName: string; -}) { +export function service(name: string, environment: string, agentName: string): Service; + +export function service(options: { name: string; environment: string; agentName: string }): Service; + +export function service( + ...args: [{ name: string; environment: string; agentName: string }] | [string, string, string] +) { + const [serviceName, environment, agentName] = + args.length === 1 ? [args[0].name, args[0].environment, args[0].agentName] : args; + return new Service({ - 'service.name': name, + 'service.name': serviceName, 'service.environment': environment, 'agent.name': agentName, }); diff --git a/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts b/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts index 0d7d0ff5dfa51..84f0dbb0a62bf 100644 --- a/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts +++ b/packages/kbn-apm-synthtrace/src/lib/stream_processor.ts @@ -187,10 +187,7 @@ export class StreamProcessor { document['service.node.name'] = document['service.node.name'] || document['container.id'] || document['host.name']; document['ecs.version'] = '1.4'; - // TODO this non standard field should not be enriched here - if (document['processor.event'] !== 'metric') { - document['timestamp.us'] = document['@timestamp']! * 1000; - } + return document; } diff --git a/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts b/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts index 4f38a7025f3b5..5d0f57fb5840b 100644 --- a/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts +++ b/packages/kbn-apm-synthtrace/src/lib/utils/dedot.ts @@ -13,4 +13,5 @@ export function dedot(source: Record, target: Record) const val = source[key as keyof typeof source]; set(target, key, val); } + return target; } diff --git a/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts b/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts index afafcc0c49665..edb20c4768ee5 100644 --- a/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/apm_events_to_elasticsearch_output.test.ts @@ -59,9 +59,6 @@ describe('output apm events to elasticsearch', () => { "name": "instance-a", }, }, - "timestamp": Object { - "us": 1609455600000000, - }, } `); }); diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts index a278997ecdf73..a14ae076e8186 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/01_simple_trace.test.ts @@ -84,6 +84,7 @@ describe('simple trace', () => { 'service.environment': 'production', 'service.name': 'opbeans-java', 'service.node.name': 'instance-1', + 'timestamp.us': 1609459200000000, 'trace.id': '00000000000000000000000000000241', 'transaction.duration.us': 1000000, 'transaction.id': '0000000000000240', @@ -113,6 +114,7 @@ describe('simple trace', () => { 'span.name': 'GET apm-*/_search', 'span.subtype': 'elasticsearch', 'span.type': 'db', + 'timestamp.us': 1609459200050000, 'trace.id': '00000000000000000000000000000301', 'transaction.id': '0000000000000300', }); diff --git a/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap b/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap index 1a5fca39e9fd9..8b3306d2d3a4b 100644 --- a/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap +++ b/packages/kbn-apm-synthtrace/src/test/scenarios/__snapshots__/01_simple_trace.test.ts.snap @@ -13,6 +13,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459200000000, "trace.id": "00000000000000000000000000000001", "transaction.duration.us": 1000000, "transaction.id": "0000000000000000", @@ -37,6 +38,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459200050000, "trace.id": "00000000000000000000000000000001", "transaction.id": "0000000000000000", }, @@ -51,6 +53,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459260000000, "trace.id": "00000000000000000000000000000005", "transaction.duration.us": 1000000, "transaction.id": "0000000000000004", @@ -75,6 +78,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459260050000, "trace.id": "00000000000000000000000000000005", "transaction.id": "0000000000000004", }, @@ -89,6 +93,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459320000000, "trace.id": "00000000000000000000000000000009", "transaction.duration.us": 1000000, "transaction.id": "0000000000000008", @@ -113,6 +118,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459320050000, "trace.id": "00000000000000000000000000000009", "transaction.id": "0000000000000008", }, @@ -127,6 +133,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459380000000, "trace.id": "00000000000000000000000000000013", "transaction.duration.us": 1000000, "transaction.id": "0000000000000012", @@ -151,6 +158,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459380050000, "trace.id": "00000000000000000000000000000013", "transaction.id": "0000000000000012", }, @@ -165,6 +173,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459440000000, "trace.id": "00000000000000000000000000000017", "transaction.duration.us": 1000000, "transaction.id": "0000000000000016", @@ -189,6 +198,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459440050000, "trace.id": "00000000000000000000000000000017", "transaction.id": "0000000000000016", }, @@ -203,6 +213,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459500000000, "trace.id": "00000000000000000000000000000021", "transaction.duration.us": 1000000, "transaction.id": "0000000000000020", @@ -227,6 +238,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459500050000, "trace.id": "00000000000000000000000000000021", "transaction.id": "0000000000000020", }, @@ -241,6 +253,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459560000000, "trace.id": "00000000000000000000000000000025", "transaction.duration.us": 1000000, "transaction.id": "0000000000000024", @@ -265,6 +278,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459560050000, "trace.id": "00000000000000000000000000000025", "transaction.id": "0000000000000024", }, @@ -279,6 +293,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459620000000, "trace.id": "00000000000000000000000000000029", "transaction.duration.us": 1000000, "transaction.id": "0000000000000028", @@ -303,6 +318,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459620050000, "trace.id": "00000000000000000000000000000029", "transaction.id": "0000000000000028", }, @@ -317,6 +333,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459680000000, "trace.id": "00000000000000000000000000000033", "transaction.duration.us": 1000000, "transaction.id": "0000000000000032", @@ -341,6 +358,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459680050000, "trace.id": "00000000000000000000000000000033", "transaction.id": "0000000000000032", }, @@ -355,6 +373,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459740000000, "trace.id": "00000000000000000000000000000037", "transaction.duration.us": 1000000, "transaction.id": "0000000000000036", @@ -379,6 +398,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459740050000, "trace.id": "00000000000000000000000000000037", "transaction.id": "0000000000000036", }, @@ -393,6 +413,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459800000000, "trace.id": "00000000000000000000000000000041", "transaction.duration.us": 1000000, "transaction.id": "0000000000000040", @@ -417,6 +438,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459800050000, "trace.id": "00000000000000000000000000000041", "transaction.id": "0000000000000040", }, @@ -431,6 +453,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459860000000, "trace.id": "00000000000000000000000000000045", "transaction.duration.us": 1000000, "transaction.id": "0000000000000044", @@ -455,6 +478,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459860050000, "trace.id": "00000000000000000000000000000045", "transaction.id": "0000000000000044", }, @@ -469,6 +493,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459920000000, "trace.id": "00000000000000000000000000000049", "transaction.duration.us": 1000000, "transaction.id": "0000000000000048", @@ -493,6 +518,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459920050000, "trace.id": "00000000000000000000000000000049", "transaction.id": "0000000000000048", }, @@ -507,6 +533,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609459980000000, "trace.id": "00000000000000000000000000000053", "transaction.duration.us": 1000000, "transaction.id": "0000000000000052", @@ -531,6 +558,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609459980050000, "trace.id": "00000000000000000000000000000053", "transaction.id": "0000000000000052", }, @@ -545,6 +573,7 @@ Array [ "service.environment": "production", "service.name": "opbeans-java", "service.node.name": "instance-1", + "timestamp.us": 1609460040000000, "trace.id": "00000000000000000000000000000057", "transaction.duration.us": 1000000, "transaction.id": "0000000000000056", @@ -569,6 +598,7 @@ Array [ "span.name": "GET apm-*/_search", "span.subtype": "elasticsearch", "span.type": "db", + "timestamp.us": 1609460040050000, "trace.id": "00000000000000000000000000000057", "transaction.id": "0000000000000056", }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts index 41df488839358..22b2a5de751f5 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/schema.ts @@ -546,6 +546,10 @@ export const stackManagementSchema: MakeSchemaFrom = { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, }, + 'observability:apmEnableCriticalPath': { + type: 'boolean', + _meta: { description: 'Non-default value of setting.' }, + }, 'observability:enableInfrastructureHostsView': { type: 'boolean', _meta: { description: 'Non-default value of setting.' }, diff --git a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts index 2bd59dc69084f..6957323103545 100644 --- a/src/plugins/kibana_usage_collection/server/collectors/management/types.ts +++ b/src/plugins/kibana_usage_collection/server/collectors/management/types.ts @@ -147,6 +147,7 @@ export interface UsageStats { 'observability:apmServiceGroupMaxNumberOfServices': number; 'observability:apmServiceInventoryOptimizedSorting': boolean; 'observability:apmTraceExplorerTab': boolean; + 'observability:apmEnableCriticalPath': boolean; 'securitySolution:enableGroupedNav': boolean; 'securitySolution:showRelatedIntegrations': boolean; 'visualization:visualize:legacyGaugeChartsLibrary': boolean; diff --git a/src/plugins/telemetry/schema/oss_plugins.json b/src/plugins/telemetry/schema/oss_plugins.json index 1a97586dffa62..14db4bca74d4a 100644 --- a/src/plugins/telemetry/schema/oss_plugins.json +++ b/src/plugins/telemetry/schema/oss_plugins.json @@ -8864,6 +8864,12 @@ "description": "Non-default value of setting." } }, + "observability:apmEnableCriticalPath": { + "type": "boolean", + "_meta": { + "description": "Non-default value of setting." + } + }, "observability:enableInfrastructureHostsView": { "type": "boolean", "_meta": { diff --git a/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts b/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts new file mode 100644 index 0000000000000..38d1b0a3da1ca --- /dev/null +++ b/x-pack/plugins/apm/common/critical_path/get_critical_path.test.ts @@ -0,0 +1,274 @@ +/* + * 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, ApmFields, dedot } from '@kbn/apm-synthtrace'; +import { getWaterfall } from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; +import { Span } from '../../typings/es_schemas/ui/span'; +import { Transaction } from '../../typings/es_schemas/ui/transaction'; +import { getCriticalPath } from './get_critical_path'; + +describe('getCriticalPath', () => { + function getCriticalPathFromEvents(events: ApmFields[]) { + const waterfall = getWaterfall( + { + traceDocs: events.map( + (event) => dedot(event, {}) as Transaction | Span + ), + errorDocs: [], + exceedsMax: false, + linkedChildrenOfSpanCountBySpanId: {}, + }, + events[0]['transaction.id']! + ); + + return { + waterfall, + criticalPath: getCriticalPath(waterfall), + }; + } + it('adds the only active span to the critical path', () => { + const service = apm.service('a', 'development', 'java').instance('a'); + + const { + criticalPath: { segments }, + waterfall, + } = getCriticalPathFromEvents( + service + .transaction('/service-a') + .timestamp(1) + .duration(100) + .children( + service.span('foo', 'external', 'db').duration(100).timestamp(1) + ) + .serialize() + ); + + expect(segments).toEqual([ + { self: false, duration: 100000, item: waterfall.items[0], offset: 0 }, + { self: false, duration: 100000, item: waterfall.items[1], offset: 0 }, + { self: true, duration: 100000, item: waterfall.items[1], offset: 0 }, + ]); + }); + + it('adds the span that ended last', () => { + const service = apm.service('a', 'development', 'java').instance('a'); + + const { + criticalPath: { segments }, + waterfall, + } = getCriticalPathFromEvents( + service + .transaction('/service-a') + .timestamp(1) + .duration(100) + .children( + service.span('foo', 'external', 'db').duration(99).timestamp(1), + service.span('bar', 'external', 'db').duration(100).timestamp(1) + ) + .serialize() + ); + + const longerSpan = waterfall.items.find( + (item) => (item.doc as Span).span?.name === 'bar' + ); + + expect(segments).toEqual([ + { self: false, duration: 100000, item: waterfall.items[0], offset: 0 }, + { + self: false, + duration: 100000, + item: longerSpan, + offset: 0, + }, + { self: true, duration: 100000, item: longerSpan, offset: 0 }, + ]); + }); + + it('adds segment for uninstrumented gaps in the parent', () => { + const service = apm.service('a', 'development', 'java').instance('a'); + + const { + criticalPath: { segments }, + waterfall, + } = getCriticalPathFromEvents( + service + .transaction('/service-a') + .timestamp(1) + .duration(100) + .children( + service.span('foo', 'external', 'db').duration(50).timestamp(11) + ) + .serialize() + ); + + expect( + segments.map((segment) => ({ + self: segment.self, + duration: segment.duration, + id: segment.item.id, + offset: segment.offset, + })) + ).toEqual([ + { self: false, duration: 100000, id: waterfall.items[0].id, offset: 0 }, + { + self: true, + duration: 40000, + id: waterfall.items[0].id, + offset: 60000, + }, + { + self: false, + duration: 50000, + id: waterfall.items[1].id, + offset: 10000, + }, + { + self: true, + duration: 50000, + id: waterfall.items[1].id, + offset: 10000, + }, + { + self: true, + duration: 10000, + offset: 0, + id: waterfall.items[0].id, + }, + ]); + }); + + it('only considers a single child to be active at the same time', () => { + const service = apm.service('a', 'development', 'java').instance('a'); + + const { + criticalPath: { segments }, + waterfall, + } = getCriticalPathFromEvents( + service + .transaction('s1') + .timestamp(1) + .duration(100) + .children( + service.span('s2', 'external', 'db').duration(1).timestamp(1), + service.span('s3', 'external', 'db').duration(1).timestamp(2), + service.span('s4', 'external', 'db').duration(98).timestamp(3), + service + .span('s5', 'external', 'db') + .duration(98) + .timestamp(1) + .children( + service.span('s6', 'external', 'db').duration(30).timestamp(5), + service.span('s7', 'external', 'db').duration(30).timestamp(35) + ) + ) + .serialize() + ); + + const [_s1, s2, _s5, _s6, _s7, s3, s4] = waterfall.items; + + expect( + segments + .map((segment) => ({ + self: segment.self, + duration: segment.duration, + id: segment.item.id, + offset: segment.offset, + })) + .filter((segment) => segment.self) + .map((segment) => segment.id) + ).toEqual([s4.id, s3.id, s2.id]); + }); + + // https://www.uber.com/en-NL/blog/crisp-critical-path-analysis-for-microservice-architectures/ + it('correctly returns the critical path for the CRISP example', () => { + const service = apm.service('a', 'development', 'java').instance('a'); + + const { + criticalPath: { segments }, + waterfall, + } = getCriticalPathFromEvents( + service + .transaction('s1') + .timestamp(1) + .duration(100) + .children( + service.span('s2', 'external', 'db').duration(25).timestamp(6), + service + .span('s3', 'external', 'db') + .duration(50) + .timestamp(41) + .children( + service.span('s4', 'external', 'db').duration(20).timestamp(61), + service.span('s5', 'external', 'db').duration(30).timestamp(51) + ) + ) + .serialize() + ); + + const [s1, s2, s3, s5, _s4] = waterfall.items; + + expect( + segments + .map((segment) => ({ + self: segment.self, + duration: segment.duration, + id: segment.item.id, + offset: segment.offset, + })) + .filter((segment) => segment.self) + ).toEqual([ + // T9-T10 + { + self: true, + duration: 10000, + id: s1.id, + offset: 90000, + }, + // T8-T9 + { + self: true, + duration: 10000, + id: s3.id, + offset: 80000, + }, + // T5-T8 + { + self: true, + duration: s5.duration, + id: s5.id, + offset: s5.offset, + }, + // T4-T5 + { + self: true, + duration: 10000, + id: s3.id, + offset: 40000, + }, + // T3-T4 + { + self: true, + duration: 10000, + id: s1.id, + offset: 30000, + }, + // T2-T3 + { + self: true, + duration: 25000, + id: s2.id, + offset: 5000, + }, + // T1-T2 + { + duration: 5000, + id: s1.id, + offset: 0, + self: true, + }, + ]); + }); +}); diff --git a/x-pack/plugins/apm/common/critical_path/get_critical_path.ts b/x-pack/plugins/apm/common/critical_path/get_critical_path.ts new file mode 100644 index 0000000000000..c517548bf3d1f --- /dev/null +++ b/x-pack/plugins/apm/common/critical_path/get_critical_path.ts @@ -0,0 +1,134 @@ +/* + * 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 type { + IWaterfall, + IWaterfallSpanOrTransaction, +} from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; +import { CriticalPath, CriticalPathSegment } from './types'; + +export function getCriticalPath(waterfall: IWaterfall): CriticalPath { + const segments: CriticalPathSegment[] = []; + + function scan({ + item, + start, + end, + }: { + item: IWaterfallSpanOrTransaction; + start: number; + end: number; + }): void { + segments.push({ + self: false, + duration: end - start, + item, + offset: start, + }); + const directChildren = waterfall.childrenByParentId[item.id]; + + if (directChildren && directChildren.length > 0) { + // We iterate over all the item's direct children. The one that + // ends last is the first item in the array. + const orderedChildren = directChildren.concat().sort((a, b) => { + const endTimeA = a.offset + a.skew + a.duration; + const endTimeB = b.offset + b.skew + b.duration; + return endTimeB - endTimeA; + }); + + // For each point in time, determine what child is on the critical path. + // We start scanning at the end. Once we've decided what the child on the + // critical path is, scan its children, from the start time of that span + // until the end. The next scan time is the start time of the child that was + // on the critical path. + let scanTime = end; + + orderedChildren.forEach((child) => { + const normalizedChildStart = Math.max(child.offset + child.skew, start); + const childEnd = child.offset + child.skew + child.duration; + + // if a span ends before the current scan time, use the current + // scan time as when the child ended. We don't want to scan further + // than the scan time. This prevents overlap in the critical path. + const normalizedChildEnd = Math.min(childEnd, scanTime); + + const isOnCriticalPath = !( + // A span/tx is NOT on the critical path if: + // - The start time is equal to or greater than the current scan time. + // Otherwise, spans that started at the same time will all contribute to + // the critical path, but we only want one to contribute. + // - The span/tx ends before the start of the initial scan period. + // - The span ends _after_ the current scan time. + + ( + normalizedChildStart >= scanTime || + normalizedChildEnd < start || + childEnd > scanTime + ) + ); + + if (!isOnCriticalPath) { + return; + } + + if (normalizedChildEnd < scanTime - 1000) { + // This span is on the critical path, but it ended before the scan time. + // This means that there is a gap, so we add a segment to the critical path + // for the _parent_. There's a slight offset because we don't want really small + // segments that can be reasonably attributed to clock skew. + segments.push({ + item, + duration: scanTime - normalizedChildEnd, + offset: normalizedChildEnd, + self: true, + }); + } + + // scan this child for the period we're considering it to be on the critical path + scan({ + start: normalizedChildStart, + end: childEnd, + item: child, + }); + + // set the scan time to the start of the span, and scan the next child + scanTime = normalizedChildStart; + }); + + // there's an unattributed gap at the start, so add a segment for the parent as well + if (scanTime > start) { + segments.push({ + item, + offset: start, + duration: scanTime - start, + self: true, + }); + } + } else { + // for the entire scan period, add this item to the critical path + segments.push({ + item, + offset: start, + duration: end - start, + self: true, + }); + } + } + + if (waterfall.entryWaterfallTransaction) { + const start = + waterfall.entryWaterfallTransaction.skew + + waterfall.entryWaterfallTransaction.offset; + scan({ + item: waterfall.entryWaterfallTransaction, + start, + end: start + waterfall.entryWaterfallTransaction.duration, + }); + } + + return { segments }; +} diff --git a/x-pack/plugins/apm/common/critical_path/types.ts b/x-pack/plugins/apm/common/critical_path/types.ts new file mode 100644 index 0000000000000..56f3db04e866f --- /dev/null +++ b/x-pack/plugins/apm/common/critical_path/types.ts @@ -0,0 +1,19 @@ +/* + * Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one + * or more contributor license agreements. Licensed under the Elastic License + * 2.0; you may not use this file except in compliance with the Elastic License + * 2.0. + */ + +import type { IWaterfallSpanOrTransaction } from '../../public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_helpers/waterfall_helpers'; + +export interface CriticalPathSegment { + item: IWaterfallSpanOrTransaction; + offset: number; + duration: number; + self: boolean; +} + +export interface CriticalPath { + segments: CriticalPathSegment[]; +} diff --git a/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx b/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx index 4cfb1a3ba9c06..792f3f0aece25 100644 --- a/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx +++ b/x-pack/plugins/apm/public/components/app/dependency_detail_operations/dependency_detail_operations_list/index.tsx @@ -41,6 +41,7 @@ function OperationLink({ spanName }: { spanName: string }) { {...query} spanName={spanName} detailTab={TransactionTab.timeline} + showCriticalPath={false} /> } /> diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx index 0cea6233edf95..4dde4af56ccf3 100644 --- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx +++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/dependency_operation_detail_trace_list.tsx @@ -101,6 +101,7 @@ export function DependencyOperationDetailTraceList({ traceId, transactionId, transactionType, + showCriticalPath: false, }, }) : router.link('/link-to/trace/{traceId}', { diff --git a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx index 742f6e27b9be3..9acd060f5fe68 100644 --- a/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/dependency_operation_detail_view/index.tsx @@ -48,6 +48,7 @@ export function DependencyOperationDetailView() { detailTab, sortField = '@timestamp', sortDirection = 'desc', + showCriticalPath, }, } = useApmParams('/dependencies/operation'); @@ -199,6 +200,14 @@ export function DependencyOperationDetailView() { waterfallItemId={waterfallItemId} detailTab={detailTab} selectedSample={selectedSample || null} + showCriticalPath={showCriticalPath} + onShowCriticalPathChange={(nextShowCriticalPath) => { + push(history, { + query: { + showCriticalPath: nextShowCriticalPath ? 'true' : 'false', + }, + }); + }} /> diff --git a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx index 220f276f62152..4b41099240f54 100644 --- a/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx +++ b/x-pack/plugins/apm/public/components/app/error_group_details/detail_view/index.tsx @@ -99,6 +99,7 @@ export function DetailView({ errorGroup, urlParams, kuery }: Props) { const traceExplorerLink = router.link('/traces/explorer', { query: { ...query, + showCriticalPath: false, query: `${ERROR_GROUP_ID}:${groupId}`, type: TraceSearchType.kql, traceId: '', diff --git a/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx index 4bcc8bb8ca53b..add6c48fef8e5 100644 --- a/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx +++ b/x-pack/plugins/apm/public/components/app/service_map/popover/edge_contents.tsx @@ -58,6 +58,7 @@ export function EdgeContents({ elementData }: ContentsProps) { traceId: '', transactionId: '', detailTab: TransactionTab.timeline, + showCriticalPath: false, }, }); diff --git a/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx b/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx index cf3306ad0d376..1b6c35adffc04 100644 --- a/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_explorer/index.tsx @@ -37,6 +37,7 @@ export function TraceExplorer() { transactionId, waterfallItemId, detailTab, + showCriticalPath, }, } = useApmParams('/traces/explorer'); @@ -158,6 +159,14 @@ export function TraceExplorer() { waterfallFetchResult.waterfall.entryWaterfallTransaction?.doc .service.name } + showCriticalPath={showCriticalPath} + onShowCriticalPathChange={(nextShowCriticalPath) => { + push(history, { + query: { + showCriticalPath: nextShowCriticalPath ? 'true' : 'false', + }, + }); + }} /> diff --git a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx index 1093a74f6bc2f..4c176527d49f6 100644 --- a/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx +++ b/x-pack/plugins/apm/public/components/app/trace_overview/index.tsx @@ -44,6 +44,7 @@ export function TraceOverview({ children }: { children: React.ReactElement }) { traceId: '', transactionId: '', detailTab: TransactionTab.timeline, + showCriticalPath: false, }, }); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx index 0cb5fa49117c5..72bf5a048e9e7 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/distribution/index.tsx @@ -21,7 +21,7 @@ import { useApmParams } from '../../../../hooks/use_apm_params'; import { useTimeRange } from '../../../../hooks/use_time_range'; import { DurationDistributionChartWithScrubber } from '../../../shared/charts/duration_distribution_chart_with_scrubber'; import { HeightRetainer } from '../../../shared/height_retainer'; -import { fromQuery, toQuery } from '../../../shared/links/url_helpers'; +import { fromQuery, push, toQuery } from '../../../shared/links/url_helpers'; import { TransactionTab } from '../waterfall_with_summary/transaction_tabs'; import { useTransactionDistributionChartData } from './use_transaction_distribution_chart_data'; import { TraceSamplesFetchResult } from '../../../../hooks/use_transaction_trace_samples_fetcher'; @@ -43,7 +43,7 @@ export function TransactionDistribution({ const { traceId, transactionId } = urlParams; const { - query: { rangeFrom, rangeTo }, + query: { rangeFrom, rangeTo, showCriticalPath }, } = useApmParams('/services/{serviceName}/transactions/view'); const { start, end } = useTimeRange({ rangeFrom, rangeTo }); @@ -119,6 +119,14 @@ export function TransactionDistribution({ waterfallFetchResult={waterfallFetchResult} traceSamplesFetchStatus={traceSamplesFetchResult.status} traceSamples={traceSamplesFetchResult.data?.traceSamples} + showCriticalPath={showCriticalPath} + onShowCriticalPathChange={(nextShowCriticalPath) => { + push(history, { + query: { + showCriticalPath: nextShowCriticalPath ? 'true' : 'false', + }, + }); + }} /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx index 537ada31df0e5..4ef0bb54319a0 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/index.tsx @@ -34,6 +34,8 @@ interface Props { serviceName?: string; waterfallItemId?: string; detailTab?: TransactionTab; + showCriticalPath: boolean; + onShowCriticalPathChange: (showCriticalPath: boolean) => void; selectedSample?: TSample | null; } @@ -47,6 +49,8 @@ export function WaterfallWithSummary({ serviceName, waterfallItemId, detailTab, + showCriticalPath, + onShowCriticalPathChange, selectedSample, }: Props) { const [sampleActivePage, setSampleActivePage] = useState(0); @@ -171,6 +175,8 @@ export function WaterfallWithSummary({ onTabClick={onTabClick} waterfall={waterfallFetchResult.waterfall} isLoading={isLoading} + showCriticalPath={showCriticalPath} + onShowCriticalPathChange={onShowCriticalPathChange} /> ); diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx index e3fdaeea24846..85e8b36942936 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/transaction_tabs.tsx @@ -22,6 +22,8 @@ interface Props { serviceName?: string; waterfallItemId?: string; onTabClick: (tab: TransactionTab) => void; + showCriticalPath: boolean; + onShowCriticalPathChange: (showCriticalPath: boolean) => void; } export function TransactionTabs({ @@ -32,6 +34,8 @@ export function TransactionTabs({ waterfallItemId, serviceName, onTabClick, + showCriticalPath, + onShowCriticalPathChange, }: Props) { const tabs = [timelineTab, metadataTab, logsTab]; const currentTab = tabs.find(({ key }) => key === detailTab) ?? timelineTab; @@ -64,6 +68,8 @@ export function TransactionTabs({ serviceName={serviceName} waterfall={waterfall} transaction={transaction} + showCriticalPath={showCriticalPath} + onShowCriticalPathChange={onShowCriticalPathChange} /> )} @@ -104,16 +110,22 @@ function TimelineTabContent({ waterfall, waterfallItemId, serviceName, + showCriticalPath, + onShowCriticalPathChange, }: { waterfallItemId?: string; serviceName?: string; waterfall: IWaterfall; + showCriticalPath: boolean; + onShowCriticalPathChange: (showCriticalPath: boolean) => void; }) { return ( ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx index 2dd74aeae3eef..b9f149c32e491 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/index.tsx @@ -5,26 +5,36 @@ * 2.0. */ -import React from 'react'; +import { EuiFlexGroup, EuiFlexItem, EuiSwitch } from '@elastic/eui'; +import { i18n } from '@kbn/i18n'; import { keyBy } from 'lodash'; +import React from 'react'; +import { useCriticalPathFeatureEnabledSetting } from '../../../../../hooks/use_critical_path_feature_enabled_setting'; +import { TechnicalPreviewBadge } from '../../../../shared/technical_preview_badge'; +import { Waterfall } from './waterfall'; import { IWaterfall, WaterfallLegendType, } from './waterfall/waterfall_helpers/waterfall_helpers'; -import { Waterfall } from './waterfall'; import { WaterfallLegends } from './waterfall_legends'; interface Props { waterfallItemId?: string; serviceName?: string; waterfall: IWaterfall; + showCriticalPath: boolean; + onShowCriticalPathChange: (showCriticalPath: boolean) => void; } export function WaterfallContainer({ serviceName, waterfallItemId, waterfall, + showCriticalPath, + onShowCriticalPathChange, }: Props) { + const isCriticalPathFeatureEnabled = useCriticalPathFeatureEnabledSetting(); + if (!waterfall) { return null; } @@ -74,9 +84,40 @@ export function WaterfallContainer({ }); return ( -
- - -
+ + {isCriticalPathFeatureEnabled ? ( + + + + {i18n.translate('xpack.apm.waterfall.showCriticalPath', { + defaultMessage: 'Show critical path', + })} + + + + + + } + checked={showCriticalPath} + onChange={(event) => { + onShowCriticalPathChange(event.target.checked); + }} + /> + + ) : null} + + + + + + + ); } diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx index c0932e041de1a..3b996bfb3cdd1 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/accordion_waterfall.tsx @@ -15,12 +15,16 @@ import { } from '@elastic/eui'; import React, { Dispatch, SetStateAction, useEffect, useState } from 'react'; import { euiStyled } from '@kbn/kibana-react-plugin/common'; +import { groupBy } from 'lodash'; +import { transparentize } from 'polished'; import { Margins } from '../../../../../shared/charts/timeline'; import { IWaterfall, IWaterfallSpanOrTransaction, } from './waterfall_helpers/waterfall_helpers'; import { WaterfallItem } from './waterfall_item'; +import { getCriticalPath } from '../../../../../../../common/critical_path/get_critical_path'; +import { useTheme } from '../../../../../../hooks/use_theme'; interface AccordionWaterfallProps { isOpen: boolean; @@ -32,6 +36,7 @@ interface AccordionWaterfallProps { waterfall: IWaterfall; timelineMargins: Margins; onClickWaterfallItem: (item: IWaterfallSpanOrTransaction) => void; + showCriticalPath: boolean; } const ACCORDION_HEIGHT = '48px'; @@ -85,8 +90,11 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { setMaxLevel, timelineMargins, onClickWaterfallItem, + showCriticalPath, } = props; + const theme = useTheme(); + const [isOpen, setIsOpen] = useState(props.isOpen); const [nextLevel] = useState(level + 1); @@ -94,7 +102,26 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { setMaxLevel(nextLevel); }, [nextLevel, setMaxLevel]); - const children = waterfall.childrenByParentId[item.id] || []; + let children = waterfall.childrenByParentId[item.id] || []; + + const criticalPath = showCriticalPath + ? getCriticalPath(waterfall) + : undefined; + + const criticalPathSegmentsById = groupBy( + criticalPath?.segments, + (segment) => segment.item.id + ); + + let displayedColor = item.color; + + if (showCriticalPath) { + children = children.filter( + (child) => criticalPathSegmentsById[child.id]?.length + ); + displayedColor = transparentize(0.5, item.color); + } + const errorCount = waterfall.getErrorCount(item.id); // To indent the items creating the parent/child tree @@ -131,7 +158,7 @@ export function AccordionWaterfall(props: AccordionWaterfallProps) { { onClickWaterfallItem(item); }} + segments={criticalPathSegmentsById[item.id] + ?.filter((segment) => segment.self) + .map((segment) => ({ + color: theme.eui.euiColorAccent, + left: + (segment.offset - item.offset - item.skew) / item.duration, + width: segment.duration / item.duration, + }))} /> diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx index 04c3734eebaff..d117cb2d982c1 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/index.tsx @@ -52,8 +52,14 @@ const WaterfallItemsContainer = euiStyled.div` interface Props { waterfallItemId?: string; waterfall: IWaterfall; + showCriticalPath: boolean; } -export function Waterfall({ waterfall, waterfallItemId }: Props) { + +export function Waterfall({ + waterfall, + waterfallItemId, + showCriticalPath, +}: Props) { const history = useHistory(); const [isAccordionOpen, setIsAccordionOpen] = useState(true); const itemContainerHeight = 58; // TODO: This is a nasty way to calculate the height of the svg element. A better approach should be found @@ -119,6 +125,7 @@ export function Waterfall({ waterfall, waterfallItemId }: Props) { onClickWaterfallItem={(item: IWaterfallItem) => toggleFlyout({ history, item }) } + showCriticalPath={showCriticalPath} /> )} diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx index 0f03b430152f0..8057ee3a32b7d 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall/waterfall_item.tsx @@ -83,6 +83,31 @@ const ItemText = euiStyled.span` } `; +const CriticalPathItemBar = euiStyled.div` + box-sizing: border-box; + position: relative; + height: ${({ theme }) => theme.eui.euiSizeS}; + top : ${({ theme }) => theme.eui.euiSizeS}; + min-width: 2px; + background-color: transparent; + display: flex; + flex-direction: row; +`; + +const CriticalPathItemSegment = euiStyled.div<{ + left: number; + width: number; + color: string; +}>` + box-sizing: border-box; + position: absolute; + height: ${({ theme }) => theme.eui.euiSizeS}; + left: ${(props) => props.left * 100}%; + width: ${(props) => props.width * 100}%; + min-width: 2px; + background-color: ${(props) => props.color}; +`; + interface IWaterfallItemProps { timelineMargins: Margins; totalDuration?: number; @@ -92,6 +117,11 @@ interface IWaterfallItemProps { isSelected: boolean; errorCount: number; marginLeftLevel: number; + segments?: Array<{ + left: number; + width: number; + color: string; + }>; onClick: () => unknown; } @@ -194,6 +224,7 @@ export function WaterfallItem({ errorCount, marginLeftLevel, onClick, + segments, }: IWaterfallItemProps) { const [widthFactor, setWidthFactor] = useState(1); const waterfallItemRef: React.RefObject = useRef(null); @@ -217,7 +248,9 @@ export function WaterfallItem({ 100; const isCompositeSpan = item.docType === 'span' && item.doc.span.composite; + const itemBarStyle = getItemBarStyle(item, color, width, left); + const isServerlessColdstart = item.docType === 'transaction' && item.doc.faas?.coldstart; @@ -237,7 +270,19 @@ export function WaterfallItem({ style={itemBarStyle} color={isCompositeSpan ? 'transparent' : color} type={item.docType} - /> + > + {segments?.length ? ( + + {segments?.map((segment) => ( + + ))} + + ) : null} + diff --git a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx index a10518ab58e4c..0a08dcb166048 100644 --- a/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx +++ b/x-pack/plugins/apm/public/components/app/transaction_details/waterfall_with_summary/waterfall_container/waterfall_container.stories.tsx @@ -8,6 +8,7 @@ import { Meta, Story } from '@storybook/react'; import React, { ComponentProps } from 'react'; import { MemoryRouter } from 'react-router-dom'; +import { noop } from 'lodash'; import { MockApmPluginContextWrapper } from '../../../../../context/apm_plugin/mock_apm_plugin_context'; import { WaterfallContainer } from '.'; import { getWaterfall } from './waterfall/waterfall_helpers/waterfall_helpers'; @@ -59,6 +60,8 @@ export const Example: Story = ({ serviceName={serviceName} waterfallItemId={waterfallItemId} waterfall={waterfall} + showCriticalPath={false} + onShowCriticalPathChange={noop} /> ); }; @@ -76,6 +79,8 @@ export const WithErrors: Story = ({ serviceName={serviceName} waterfallItemId={waterfallItemId} waterfall={waterfall} + showCriticalPath={false} + onShowCriticalPathChange={noop} /> ); }; @@ -93,6 +98,8 @@ export const ChildStartsBeforeParent: Story = ({ serviceName={serviceName} waterfallItemId={waterfallItemId} waterfall={waterfall} + showCriticalPath={false} + onShowCriticalPathChange={noop} /> ); }; @@ -110,6 +117,8 @@ export const InferredSpans: Story = ({ serviceName={serviceName} waterfallItemId={waterfallItemId} waterfall={waterfall} + showCriticalPath={false} + onShowCriticalPathChange={noop} /> ); }; @@ -127,6 +136,8 @@ export const ManyChildrenWithSameLength: Story = ({ serviceName={serviceName} waterfallItemId={waterfallItemId} waterfall={waterfall} + showCriticalPath={false} + onShowCriticalPathChange={noop} /> ); }; diff --git a/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx b/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx index 01109eedba483..7b0d93d7550e1 100644 --- a/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/dependencies.tsx @@ -79,6 +79,7 @@ export const dependencies = { t.literal(TransactionTab.metadata), t.literal(TransactionTab.logs), ]), + showCriticalPath: toBooleanRt, }), t.partial({ spanId: t.string, @@ -91,6 +92,7 @@ export const dependencies = { defaults: { query: { detailTab: TransactionTab.timeline, + showCriticalPath: '', }, }, element: , diff --git a/x-pack/plugins/apm/public/components/routing/home/index.tsx b/x-pack/plugins/apm/public/components/routing/home/index.tsx index 51a68488f9d81..36ead4f7b36c7 100644 --- a/x-pack/plugins/apm/public/components/routing/home/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/home/index.tsx @@ -213,6 +213,7 @@ export const home = { t.literal(TransactionTab.metadata), t.literal(TransactionTab.logs), ]), + showCriticalPath: toBooleanRt, }), }), defaults: { @@ -223,6 +224,7 @@ export const home = { traceId: '', transactionId: '', detailTab: TransactionTab.timeline, + showCriticalPath: '', }, }, }, diff --git a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx index fdd1aedfa0022..7cc2f7b113fe9 100644 --- a/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx +++ b/x-pack/plugins/apm/public/components/routing/service_detail/index.tsx @@ -180,6 +180,7 @@ export const serviceDetail = { t.type({ transactionName: t.string, comparisonEnabled: toBooleanRt, + showCriticalPath: toBooleanRt, }), t.partial({ traceId: t.string, @@ -188,6 +189,11 @@ export const serviceDetail = { offsetRt, ]), }), + defaults: { + query: { + showCriticalPath: '', + }, + }, }, '/services/{serviceName}/transactions': { element: , diff --git a/x-pack/plugins/apm/public/hooks/use_critical_path_feature_enabled_setting.ts b/x-pack/plugins/apm/public/hooks/use_critical_path_feature_enabled_setting.ts new file mode 100644 index 0000000000000..29c6d10ea2d69 --- /dev/null +++ b/x-pack/plugins/apm/public/hooks/use_critical_path_feature_enabled_setting.ts @@ -0,0 +1,18 @@ +/* + * 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 { enableCriticalPath } from '@kbn/observability-plugin/common'; +import { useApmPluginContext } from '../context/apm_plugin/use_apm_plugin_context'; + +export function useCriticalPathFeatureEnabledSetting() { + const { core } = useApmPluginContext(); + + const isCriticalPathFeatureEnabled = + core.uiSettings.get(enableCriticalPath); + + return isCriticalPathFeatureEnabled; +} diff --git a/x-pack/plugins/observability/common/index.ts b/x-pack/plugins/observability/common/index.ts index 3c64645f9b1e8..123c0639f2d49 100644 --- a/x-pack/plugins/observability/common/index.ts +++ b/x-pack/plugins/observability/common/index.ts @@ -27,6 +27,7 @@ export { enableInfrastructureHostsView, enableServiceMetrics, enableAwsLambdaMetrics, + enableCriticalPath, } from './ui_settings_keys'; export { diff --git a/x-pack/plugins/observability/common/ui_settings_keys.ts b/x-pack/plugins/observability/common/ui_settings_keys.ts index f41e492d25050..ab1684c2e5bfe 100644 --- a/x-pack/plugins/observability/common/ui_settings_keys.ts +++ b/x-pack/plugins/observability/common/ui_settings_keys.ts @@ -22,3 +22,4 @@ export const apmLabsButton = 'observability:apmLabsButton'; export const enableInfrastructureHostsView = 'observability:enableInfrastructureHostsView'; export const enableAwsLambdaMetrics = 'observability:enableAwsLambdaMetrics'; export const enableServiceMetrics = 'observability:apmEnableServiceMetrics'; +export const enableCriticalPath = 'observability:apmEnableCriticalPath'; diff --git a/x-pack/plugins/observability/server/ui_settings.ts b/x-pack/plugins/observability/server/ui_settings.ts index f272db404b7b8..e979bd6a7fb11 100644 --- a/x-pack/plugins/observability/server/ui_settings.ts +++ b/x-pack/plugins/observability/server/ui_settings.ts @@ -25,6 +25,7 @@ import { enableInfrastructureHostsView, enableServiceMetrics, enableAwsLambdaMetrics, + enableCriticalPath, } from '../common/ui_settings_keys'; const technicalPreviewLabel = i18n.translate( @@ -309,4 +310,21 @@ export const uiSettings: Record = { type: 'boolean', showInLabs: true, }, + [enableCriticalPath]: { + category: [observabilityFeatureId], + name: i18n.translate('xpack.observability.enableCriticalPath', { + defaultMessage: 'Critical path', + }), + description: i18n.translate('xpack.observability.enableCriticalPathDescription', { + defaultMessage: '{technicalPreviewLabel} Optionally display the critical path of a trace.', + values: { + technicalPreviewLabel: `[${technicalPreviewLabel}]`, + }, + }), + schema: schema.boolean(), + value: false, + requiresPageReload: true, + type: 'boolean', + showInLabs: true, + }, };