diff --git a/projects/distributed-tracing/src/pages/trace-detail/logs/trace-logs.component.ts b/projects/distributed-tracing/src/pages/trace-detail/logs/trace-logs.component.ts new file mode 100644 index 000000000..ea34f9136 --- /dev/null +++ b/projects/distributed-tracing/src/pages/trace-detail/logs/trace-logs.component.ts @@ -0,0 +1,24 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { LogEventsTableViewType } from '../../../shared/components/log-events/log-events-table.component'; +import { LogEvent } from '../../../shared/dashboard/widgets/waterfall/waterfall/waterfall-chart'; +import { TraceDetailService } from './../trace-detail.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + ` +}) +export class TraceLogsComponent { + public readonly logEvents$: Observable; + + public constructor(private readonly traceDetailService: TraceDetailService) { + this.logEvents$ = this.traceDetailService.fetchLogEvents(); + } +} diff --git a/projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.component.ts b/projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.component.ts new file mode 100644 index 000000000..2df86724c --- /dev/null +++ b/projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.component.ts @@ -0,0 +1,38 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { TraceDetailService } from './../trace-detail.service'; +import { traceSequenceDashboard } from './trace-sequence.dashboard'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + ` +}) +export class TraceSequenceComponent { + public readonly traceVariables$: Observable; + + public constructor(private readonly traceDetailService: TraceDetailService) { + this.traceVariables$ = this.traceDetailService.fetchTraceDetails().pipe( + map(details => ({ + traceId: details.id, + startTime: details.startTime, + spanId: details.entrySpanId + })) + ); + } +} + +interface TraceDetailVariables { + traceId: string; + startTime?: string | number; + spanId?: string; +} diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.dashboard.ts b/projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.dashboard.ts similarity index 74% rename from projects/distributed-tracing/src/pages/trace-detail/trace-detail.dashboard.ts rename to projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.dashboard.ts index bd6770239..8d4d32acd 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.dashboard.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/sequence/trace-sequence.dashboard.ts @@ -1,7 +1,7 @@ -import { DashboardDefaultConfiguration } from '../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module'; +import { DashboardDefaultConfiguration } from '../../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module'; -export const traceDetailDashboard: DashboardDefaultConfiguration = { - location: 'TRACE_DETAIL', +export const traceSequenceDashboard: DashboardDefaultConfiguration = { + location: 'TRACE_SEQUENCE', json: { type: 'container-widget', layout: { diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.scss b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.scss index 628045ccb..491d2f9b1 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.scss +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.scss @@ -45,4 +45,8 @@ .summary-value { margin-right: 24px; } + + .tabs { + margin-top: 16px; + } } diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts index 64abd18ed..0cfae196b 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.component.ts @@ -2,11 +2,8 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; import { NavigationService, SubscriptionLifecycle } from '@hypertrace/common'; import { IconSize } from '@hypertrace/components'; - -import { Dashboard } from '@hypertrace/hyperdash'; import { Observable } from 'rxjs'; - -import { traceDetailDashboard } from './trace-detail.dashboard'; +import { LogEvent } from '../../shared/dashboard/widgets/waterfall/waterfall/waterfall-chart'; import { TraceDetails, TraceDetailService } from './trace-detail.service'; @Component({ styleUrls: ['./trace-detail.page.component.scss'], @@ -53,13 +50,16 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service'; - - + + Sequence + + Logs + + + +
+ +
` }) @@ -68,25 +68,15 @@ export class TraceDetailPageComponent { public readonly traceDetails$: Observable; public readonly exportSpans$: Observable; + public readonly logEvents$: Observable; public constructor( - private readonly subscriptionLifecycle: SubscriptionLifecycle, private readonly navigationService: NavigationService, private readonly traceDetailService: TraceDetailService ) { this.traceDetails$ = this.traceDetailService.fetchTraceDetails(); this.exportSpans$ = this.traceDetailService.fetchExportSpans(); - } - - public onDashboardReady(dashboard: Dashboard): void { - this.subscriptionLifecycle.add( - this.traceDetails$.subscribe(traceDetails => { - dashboard.setVariable('traceId', traceDetails.id); - dashboard.setVariable('spanId', traceDetails.entrySpanId); - dashboard.setVariable('startTime', traceDetails.startTime); - dashboard.refresh(); - }) - ); + this.logEvents$ = this.traceDetailService.fetchLogEvents(); } public onClickBack(): void { diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts index 541fb3332..576bb1def 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.page.module.ts @@ -8,23 +8,42 @@ import { IconModule, LabelModule, LoadAsyncModule, + NavigableTabModule, SummaryValueModule, TooltipModule } from '@hypertrace/components'; +import { LogEventsTableModule } from '../../shared/components/log-events/log-events-table.module'; import { NavigableDashboardModule } from '../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module'; import { TracingDashboardModule } from '../../shared/dashboard/tracing-dashboard.module'; -import { traceDetailDashboard } from './trace-detail.dashboard'; +import { TraceLogsComponent } from './logs/trace-logs.component'; +import { TraceSequenceComponent } from './sequence/trace-sequence.component'; +import { traceSequenceDashboard } from './sequence/trace-sequence.dashboard'; import { TraceDetailPageComponent } from './trace-detail.page.component'; const ROUTE_CONFIG: TraceRoute[] = [ { path: `:${TraceDetailPageComponent.TRACE_ID_PARAM_NAME}`, - component: TraceDetailPageComponent + component: TraceDetailPageComponent, + children: [ + { + path: '', + redirectTo: 'sequence', + pathMatch: 'full' + }, + { + path: 'sequence', + component: TraceSequenceComponent + }, + { + path: 'logs', + component: TraceLogsComponent + } + ] } ]; @NgModule({ - declarations: [TraceDetailPageComponent], + declarations: [TraceDetailPageComponent, TraceSequenceComponent, TraceLogsComponent], imports: [ RouterModule.forChild(ROUTE_CONFIG), CommonModule, @@ -37,7 +56,9 @@ const ROUTE_CONFIG: TraceRoute[] = [ FormattingModule, CopyShareableLinkToClipboardModule, DownloadJsonModule, - NavigableDashboardModule.withDefaultDashboards(traceDetailDashboard) + NavigableDashboardModule.withDefaultDashboards(traceSequenceDashboard), + NavigableTabModule, + LogEventsTableModule ] }) export class TraceDetailPageModule {} diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.test.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.test.ts index 234a68953..39723895b 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.test.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.test.ts @@ -9,7 +9,7 @@ import { } from '@hypertrace/common'; import { GraphQlRequestService } from '@hypertrace/graphql-client'; import { runFakeRxjs } from '@hypertrace/test-utils'; -import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { AttributeMetadataType } from '../../shared/graphql/model/metadata/attribute-metadata'; import { spanIdKey } from '../../shared/graphql/model/schema/span'; @@ -18,63 +18,62 @@ import { MetadataService } from '../../shared/services/metadata/metadata.service import { TraceDetailService } from './trace-detail.service'; describe('TraceDetailService', () => { - let spectator: SpectatorService; - const createService = createServiceFactory({ - service: TraceDetailService, - providers: [ - mockProvider(ActivatedRoute, { - paramMap: of({ - get: (param: string) => { - if (param === 'startTime') { - return 1608151401295; - } + service: TraceDetailService + }); - return 'test-id'; - } - }) - }), - mockProvider(TimeRangeService, { - getTimeRangeAndChanges: jest.fn().mockReturnValue(of(new RelativeTimeRange(new TimeDuration(1, TimeUnit.Hour)))) - }), - mockProvider(GraphQlRequestService, { - query: jest.fn().mockReturnValue( - of({ - [traceTypeKey]: TRACE_SCOPE, - [traceIdKey]: 'test-id', - startTime: 1576364117792, - duration: 20, - spans: [ - { - [spanIdKey]: 'test-id', - serviceName: 'test service', - displaySpanName: 'egress' + test('should fetch and map trace details', () => { + const spectator = createService({ + providers: [ + mockProvider(ActivatedRoute, { + paramMap: of({ + get: (param: string) => { + if (param === 'startTime') { + return 1608151401295; } - ] - }) - ) - }), - mockProvider(MetadataService, { - getAttribute: (scope: string, attributeKey: string) => - of({ - name: attributeKey, - displayName: 'Latency', - units: 'ms', - type: AttributeMetadataType.Number, - scope: scope, - onlySupportsAggregation: false, - onlySupportsGrouping: false, - allowedAggregations: [] - }) - }) - ] - }); - beforeEach(() => { - spectator = createService(); - }); + return 'test-id'; + } + }) + }), + mockProvider(TimeRangeService, { + getTimeRangeAndChanges: jest + .fn() + .mockReturnValue(of(new RelativeTimeRange(new TimeDuration(1, TimeUnit.Hour)))) + }), + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue( + of({ + [traceTypeKey]: TRACE_SCOPE, + [traceIdKey]: 'test-id', + startTime: 1576364117792, + duration: 20, + spans: [ + { + [spanIdKey]: 'test-id', + serviceName: 'test service', + displaySpanName: 'egress' + } + ] + }) + ) + }), + mockProvider(MetadataService, { + getAttribute: (scope: string, attributeKey: string) => + of({ + name: attributeKey, + displayName: 'Latency', + units: 'ms', + type: AttributeMetadataType.Number, + scope: scope, + onlySupportsAggregation: false, + onlySupportsGrouping: false, + allowedAggregations: [] + }) + }) + ] + }); - test('should map fetch and map trace details', () => { runFakeRxjs(({ expectObservable }) => { expectObservable(spectator.service.fetchTraceDetails()).toBe('(x|)', { x: { @@ -90,4 +89,90 @@ describe('TraceDetailService', () => { }); }); }); + + test('should fetch export spans', () => { + const spectator = createService({ + providers: [ + mockProvider(ActivatedRoute, { + paramMap: of({ + get: (param: string) => { + if (param === 'startTime') { + return 1608151401295; + } + + return 'test-id'; + } + }) + }), + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue(of('result')) + }) + ] + }); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.service.fetchExportSpans()).toBe('(x|)', { + x: 'result' + }); + }); + }); + + test('should fetch and map logEvents', () => { + const spectator = createService({ + providers: [ + mockProvider(ActivatedRoute, { + paramMap: of({ + get: (param: string) => { + if (param === 'startTime') { + return 1608151401295; + } + + return 'test-id'; + } + }) + }), + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue( + of({ + spans: [ + { + logEvents: { + results: [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary' + } + ] + }, + serviceName: 'service', + displaySpanName: 'span', + protocolName: 'protocol', + startTime: 1608151401295 + } + ] + }) + ) + }) + ] + }); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.service.fetchLogEvents()).toBe('(x|)', { + x: [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary', + $$spanName: { + serviceName: 'service', + protocolName: 'protocol', + apiName: 'span' + }, + spanStartTime: 1608151401295 + } + ] + }); + }); + }); }); diff --git a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts index 1efc4bd24..cc45f88d0 100644 --- a/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts +++ b/projects/distributed-tracing/src/pages/trace-detail/trace-detail.service.ts @@ -1,10 +1,10 @@ import { Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { DateCoercer, DateFormatMode, DateFormatter, ReplayObservable } from '@hypertrace/common'; - import { GraphQlRequestService } from '@hypertrace/graphql-client'; import { Observable, Subject } from 'rxjs'; import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators'; +import { LogEvent } from '../../shared/dashboard/widgets/waterfall/waterfall/waterfall-chart'; import { Trace, traceIdKey, TraceType, traceTypeKey } from '../../shared/graphql/model/schema/trace'; import { SpecificationBuilder } from '../../shared/graphql/request/builders/specification/specification-builder'; import { @@ -15,6 +15,7 @@ import { TraceGraphQlQueryHandlerService, TRACE_GQL_REQUEST } from '../../shared/graphql/request/handlers/traces/trace-graphql-query-handler.service'; +import { LogEventsService } from '../../shared/services/log-events/log-events.service'; import { MetadataService } from '../../shared/services/metadata/metadata.service'; @Injectable() @@ -32,7 +33,8 @@ export class TraceDetailService implements OnDestroy { public constructor( route: ActivatedRoute, private readonly metadataService: MetadataService, - private readonly graphQlQueryService: GraphQlRequestService + private readonly graphQlQueryService: GraphQlRequestService, + private readonly logEventsService: LogEventsService ) { this.routeIds$ = route.paramMap.pipe( map(paramMap => ({ @@ -101,6 +103,17 @@ export class TraceDetailService implements OnDestroy { ); } + public fetchLogEvents(): Observable { + return this.routeIds$.pipe( + switchMap(routeIds => + this.logEventsService.getLogEventsGqlResponseForTrace(routeIds.traceId, routeIds.startTime) + ), + map(trace => this.logEventsService.mapLogEvents(trace)), + takeUntil(this.destroyed$), + shareReplay(1) + ); + } + private fetchTrace(traceId: string, spanId?: string, startTime?: string | number): Observable { return this.graphQlQueryService.query({ requestType: TRACE_GQL_REQUEST, diff --git a/projects/distributed-tracing/src/public-api.ts b/projects/distributed-tracing/src/public-api.ts index a65c188ae..e900e2bda 100644 --- a/projects/distributed-tracing/src/public-api.ts +++ b/projects/distributed-tracing/src/public-api.ts @@ -7,6 +7,8 @@ export * from './shared/graphql/model/schema/enriched-attribute'; // Pages export * from './pages/trace-detail/trace-detail.page.module'; +export * from './pages/trace-detail/logs/trace-logs.component'; +export * from './pages/trace-detail/sequence/trace-sequence.component'; export * from './pages/spans/span-list.page.module'; // Builders @@ -51,6 +53,10 @@ export * from './shared/icons/tracing-icon-library.module'; // Interaction Handler - Deprecated export { InteractionHandler } from './shared/dashboard/interaction/interaction-handler'; +// Log Events Table +export * from './shared/components/log-events/log-events-table.component'; +export * from './shared/components/log-events/log-events-table.module'; + // Metadata export * from './shared/services/metadata/metadata.service'; export * from './shared/services/metadata/metadata.service.module'; @@ -83,6 +89,7 @@ export * from './shared/graphql/model/schema/trace'; // Services export * from './pages/trace-detail/trace-detail.service'; +export * from './shared/services/log-events/log-events.service'; // Span Detail export { SpanData } from './shared/components/span-detail/span-data'; @@ -104,7 +111,7 @@ export * from './shared/components/table/tracing-table-cell-type'; // Waterfall export { WaterfallData } from './shared/dashboard/widgets/waterfall/waterfall/waterfall-chart'; export { TraceWaterfallDataSourceModel } from './shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model'; -export { traceDetailDashboard } from './pages/trace-detail/trace-detail.dashboard'; +export { traceSequenceDashboard } from './pages/trace-detail/sequence/trace-sequence.dashboard'; export { TraceDetailPageComponent } from './pages/trace-detail/trace-detail.page.component'; export { LogEvent } from './shared/dashboard/widgets/waterfall/waterfall/waterfall-chart'; diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts index f8a5c53ab..ac311b73d 100644 --- a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.test.ts @@ -29,14 +29,14 @@ describe('LogEventsTableComponent', () => { test('should render data correctly for sheet view', fakeAsync(() => { spectator = createHost( - ``, + ``, { hostProps: { logEvents: [ { attributes: { attr1: 1, attr2: 2 }, summary: 'test', timestamp: '2021-04-30T12:23:57.889149Z' } ], logEventsTableViewType: LogEventsTableViewType.Condensed, - spanStartTime: 1619785437887 + startTime: 1619785437887 } } ); @@ -52,7 +52,7 @@ describe('LogEventsTableComponent', () => { id: 'summary' }) ]); - expect(spectator.query(TableComponent)!.pageable).toBe(false); + expect(spectator.query(TableComponent)!.pageable).toBe(true); expect(spectator.query(TableComponent)!.detailContent).not.toBeNull(); runFakeRxjs(({ expectObservable }) => { diff --git a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts index 92debd388..ce6b22552 100644 --- a/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts +++ b/projects/distributed-tracing/src/shared/components/log-events/log-events-table.component.ts @@ -13,6 +13,7 @@ import { import { LogEvent } from '@hypertrace/distributed-tracing'; import { isEmpty } from 'lodash-es'; import { Observable, of } from 'rxjs'; +import { WaterfallTableCellType } from '../../dashboard/widgets/waterfall/waterfall/span-name/span-name-cell-type'; export const enum LogEventsTableViewType { Condensed = 'condensed', @@ -28,7 +29,7 @@ export const enum LogEventsTableViewType { ({ ...logEvent, timestamp: this.dateCoercer.coerce(logEvent.timestamp), - baseTimestamp: this.dateCoercer.coerce(this.spanStartTime) + baseTimestamp: this.dateCoercer.coerce(logEvent.spanStartTime) })), totalCount: this.logEvents.length }), @@ -93,29 +91,58 @@ export class LogEventsTableComponent implements OnChanges { } private getTableColumnConfigs(): TableColumnConfig[] { - if (this.logEventsTableViewType === LogEventsTableViewType.Condensed) { - return [ - { - id: 'timestamp', - name: 'timestamp', - title: 'Timestamp', - display: CoreTableCellRendererType.RelativeTimestamp, - visible: true, - width: '150px', - sortable: false, - filterable: false - }, - { - id: 'summary', - name: 'summary', - title: 'Summary', - visible: true, - sortable: false, - filterable: false - } - ]; + switch (this.logEventsTableViewType) { + case LogEventsTableViewType.Condensed: + return [ + { + id: 'timestamp', + name: 'timestamp', + title: 'Timestamp', + display: CoreTableCellRendererType.RelativeTimestamp, + visible: true, + width: '150px', + sortable: false, + filterable: false + }, + { + id: 'summary', + name: 'summary', + title: 'Summary', + visible: true, + sortable: false, + filterable: false + } + ]; + case LogEventsTableViewType.Detailed: + return [ + { + id: 'timestamp', + title: 'Timestamp', + display: CoreTableCellRendererType.RelativeTimestamp, + visible: true, + width: '150px', + sortable: false, + filterable: false + }, + { + id: 'summary', + title: 'Summary', + visible: true, + sortable: false, + filterable: false + }, + { + id: '$$spanName', + title: 'Span', + display: WaterfallTableCellType.SpanName, + visible: true, + width: '30%', + sortable: false, + filterable: false + } + ]; + default: + return []; } - - return []; } } diff --git a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts index 13f988aa9..44c67aaf1 100644 --- a/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts +++ b/projects/distributed-tracing/src/shared/components/span-detail/span-detail.component.ts @@ -47,11 +47,8 @@ import { SpanDetailTab } from './span-detail-tab'; - - + + diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.test.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.test.ts index 97b1a9b74..a02b3ae74 100644 --- a/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.test.ts +++ b/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.test.ts @@ -4,6 +4,7 @@ import { mockProvider } from '@ngneat/spectator/jest'; import { Observable, of } from 'rxjs'; import { map, take } from 'rxjs/operators'; import { Trace, traceIdKey, traceTypeKey, TRACE_SCOPE } from '../../../../graphql/model/schema/trace'; +import { LogEventsService } from '../../../../services/log-events/log-events.service'; import { MetadataService } from '../../../../services/metadata/metadata.service'; import { WaterfallData } from '../../../widgets/waterfall/waterfall/waterfall-chart'; import { GraphQlQueryEventService } from '../graphql-query-event.service'; @@ -21,7 +22,10 @@ describe('Trace Waterfall data source model', () => { }) ) }), - mockProvider(GraphQlQueryEventService) + mockProvider(GraphQlQueryEventService), + mockProvider(LogEventsService, { + getLogEventsWithSpanStartTime: jest.fn().mockReturnValue([]) + }) ] }); @@ -210,7 +214,8 @@ describe('Trace Waterfall data source model', () => { displaySpanName: 'Span Name 1', serviceName: 'Service Name 1', type: SpanType.Entry, - spanTags: {} + spanTags: {}, + logEvents: [] }, { [spanIdKey]: 'second-id', @@ -221,7 +226,8 @@ describe('Trace Waterfall data source model', () => { displaySpanName: 'Span Name 2', serviceName: 'Service Name 2', type: SpanType.Exit, - spanTags: {} + spanTags: {}, + logEvents: [] } ] }) @@ -241,7 +247,8 @@ describe('Trace Waterfall data source model', () => { serviceName: 'Service Name 1', protocolName: undefined, spanType: SpanType.Entry, - tags: {} + tags: {}, + logEvents: [] }, { id: 'second-id', @@ -257,7 +264,8 @@ describe('Trace Waterfall data source model', () => { serviceName: 'Service Name 2', protocolName: undefined, spanType: SpanType.Exit, - tags: {} + tags: {}, + logEvents: [] } ] }); diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.ts index 30fccf333..615c28296 100644 --- a/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.ts +++ b/projects/distributed-tracing/src/shared/dashboard/data/graphql/waterfall/trace-waterfall-data-source.model.ts @@ -12,6 +12,7 @@ import { TraceGraphQlQueryHandlerService, TRACE_GQL_REQUEST } from '../../../../graphql/request/handlers/traces/trace-graphql-query-handler.service'; +import { LogEventsService } from '../../../../services/log-events/log-events.service'; import { MetadataService } from '../../../../services/metadata/metadata.service'; import { LogEvent, WaterfallData } from '../../../widgets/waterfall/waterfall/waterfall-chart'; import { GraphQlDataSourceModel } from '../graphql-data-source.model'; @@ -44,6 +45,9 @@ export class TraceWaterfallDataSourceModel extends GraphQlDataSourceModel, errorCount: span.errorCount as number, - logEvents: ((span.logEvents as Dictionary) ?? {}).results + logEvents: this.logEventsService.getLogEventsWithSpanStartTime( + span.logEvents as Dictionary, + span.startTime as number + ) })); } } diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/span-name/span-name-table-cell-renderer.component.scss b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/span-name/span-name-table-cell-renderer.component.scss index 874d26418..848088827 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/span-name/span-name-table-cell-renderer.component.scss +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/span-name/span-name-table-cell-renderer.component.scss @@ -7,12 +7,12 @@ .span-title { display: grid; - grid-template-columns: 3px min-content min-content min-content min-content auto; + grid-template-columns: repeat(5, min-content) auto; grid-template-rows: 1fr; column-gap: 4px; .color-bar { - width: 100%; + width: 3px; height: 16px; border-radius: 3px; align-self: center; diff --git a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/waterfall-chart.ts b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/waterfall-chart.ts index c5077dc3a..d9b1ca14d 100644 --- a/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/waterfall-chart.ts +++ b/projects/distributed-tracing/src/shared/dashboard/widgets/waterfall/waterfall/waterfall-chart.ts @@ -40,6 +40,7 @@ export interface WaterfallChartState { } export interface LogEvent { + spanStartTime?: number; attributes: Dictionary; timestamp: string; summary: string; diff --git a/projects/distributed-tracing/src/shared/services/log-events/log-events.service.test.ts b/projects/distributed-tracing/src/shared/services/log-events/log-events.service.test.ts new file mode 100644 index 000000000..e0f597cb4 --- /dev/null +++ b/projects/distributed-tracing/src/shared/services/log-events/log-events.service.test.ts @@ -0,0 +1,116 @@ +import { Dictionary } from '@hypertrace/common'; +import { GraphQlRequestService } from '@hypertrace/graphql-client'; +import { runFakeRxjs } from '@hypertrace/test-utils'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; +import { Observable, of } from 'rxjs'; +import { LogEvent } from '../../dashboard/widgets/waterfall/waterfall/waterfall-chart'; +import { spanIdKey } from '../../graphql/model/schema/span'; +import { Trace, traceIdKey, traceTypeKey } from '../../graphql/model/schema/trace'; +import { LogEventsService } from './log-events.service'; + +describe('Log Events Service', () => { + const mockTrace: Trace = { + [traceIdKey]: 'test-id', + [traceTypeKey]: 'API_TRACE', + spans: [] + }; + + const mockTraceWithLogEvents: Trace = { + [traceIdKey]: 'test-id', + [traceTypeKey]: 'TRACE', + spans: [ + { + [spanIdKey]: 'test-span', + logEvents: { + results: [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary' + } + ] + }, + serviceName: 'service', + displaySpanName: 'span', + protocolName: 'protocol', + startTime: 1608151401295 + } + ] + }; + + const createService = createServiceFactory({ + service: LogEventsService, + providers: [ + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue(of({})) + }) + ] + }); + + const expectSingleValueObservable = (observable: Observable, value: T): void => { + runFakeRxjs(({ expectObservable }) => { + expectObservable(observable).toBe('(x|)', { x: value }); + }); + }; + + test('should return empty array for zero log Events', () => { + const spectator = createService(); + expect(spectator.service.getLogEventsWithSpanStartTime({ results: [] }, 1608151401295)).toMatchObject([]); + }); + + test('should return array with start time for log Events', () => { + const spectator = createService(); + const logEvents: Dictionary = { + results: [ + { + timestamp: 'time', + attributes: {}, + summary: 'summary' + } + ] + }; + const logEventsWithSpanStartTime = [ + { + timestamp: 'time', + attributes: {}, + summary: 'summary', + spanStartTime: 1608151401295 + } + ]; + expect(spectator.service.getLogEventsWithSpanStartTime(logEvents, 1608151401295)).toMatchObject( + logEventsWithSpanStartTime + ); + }); + + test('should return trace and spans', () => { + const spectator = createService({ + providers: [ + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue(of(mockTrace)) + }) + ] + }); + expectSingleValueObservable( + spectator.service.getLogEventsGqlResponseForTrace('test', '1608151401295', 'API_TRACE'), + mockTrace + ); + }); + + test('should map log events', () => { + const spectator = createService(); + const mappedLogEvents = [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary', + $$spanName: { + serviceName: 'service', + apiName: 'span', + protocolName: 'protocol' + }, + spanStartTime: 1608151401295 + } + ]; + expect(spectator.service.mapLogEvents(mockTraceWithLogEvents)).toMatchObject(mappedLogEvents); + }); +}); diff --git a/projects/distributed-tracing/src/shared/services/log-events/log-events.service.ts b/projects/distributed-tracing/src/shared/services/log-events/log-events.service.ts new file mode 100644 index 000000000..f19c8028d --- /dev/null +++ b/projects/distributed-tracing/src/shared/services/log-events/log-events.service.ts @@ -0,0 +1,79 @@ +import { Injectable } from '@angular/core'; +import { DateCoercer, Dictionary } from '@hypertrace/common'; +import { GraphQlRequestService } from '@hypertrace/graphql-client'; +import { ObservabilityTraceType } from '@hypertrace/observability'; +import { isEmpty } from 'lodash-es'; +import { Observable } from 'rxjs'; +import { LogEvent } from '../../dashboard/widgets/waterfall/waterfall/waterfall-chart'; +import { Span } from '../../graphql/model/schema/span'; +import { Trace, TraceType, traceTypeKey } from '../../graphql/model/schema/trace'; +import { SpecificationBuilder } from '../../graphql/request/builders/specification/specification-builder'; +import { + TraceGraphQlQueryHandlerService, + TRACE_GQL_REQUEST +} from '../../graphql/request/handlers/traces/trace-graphql-query-handler.service'; + +@Injectable({ providedIn: 'root' }) +export class LogEventsService { + private readonly specificationBuilder: SpecificationBuilder = new SpecificationBuilder(); + private readonly dateCoercer: DateCoercer = new DateCoercer(); + private readonly logEventProperties: string[] = ['attributes', 'timestamp', 'summary']; + private readonly spanPropertiesForTrace: string[] = ['startTime', 'serviceName', 'displaySpanName', 'protocolName']; + private readonly spanPropertiesForApiTrace: string[] = [ + 'startTime', + 'displayEntityName', + 'displaySpanName', + 'protocolName' + ]; + + public constructor(private readonly graphQlQueryService: GraphQlRequestService) {} + + public getLogEventsWithSpanStartTime(logEventsObject: Dictionary, startTime: number): LogEvent[] { + if (isEmpty(logEventsObject) || isEmpty(logEventsObject.results)) { + return []; + } + + return logEventsObject.results.map(logEvent => ({ + ...logEvent, + spanStartTime: startTime + })); + } + + public getLogEventsGqlResponseForTrace( + traceId: string, + startTime?: string, + traceType?: TraceType + ): Observable { + return this.graphQlQueryService.query({ + requestType: TRACE_GQL_REQUEST, + traceType: traceType, + traceId: traceId, + timestamp: this.dateCoercer.coerce(startTime), + traceProperties: [], + spanProperties: (traceType === ObservabilityTraceType.Api + ? this.spanPropertiesForApiTrace + : this.spanPropertiesForTrace + ).map(property => this.specificationBuilder.attributeSpecificationForKey(property)), + logEventProperties: this.logEventProperties.map(property => + this.specificationBuilder.attributeSpecificationForKey(property) + ), + spanLimit: 1000 + }); + } + + public mapLogEvents(trace: Trace): LogEvent[] { + return (trace.spans ?? []) + .map((span: Span) => + (span.logEvents as Dictionary).results.map(logEvent => ({ + ...logEvent, + $$spanName: { + serviceName: trace[traceTypeKey] === ObservabilityTraceType.Api ? span.displayEntityName : span.serviceName, + protocolName: span.protocolName, + apiName: span.displaySpanName + }, + spanStartTime: span.startTime as number + })) + ) + .flat(); + } +} diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.scss b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.scss index 01e0c2494..9a4ce3371 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.scss +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.scss @@ -47,4 +47,8 @@ .summary-value { margin-right: 24px; } + + .tabs { + margin-top: 8px; + } } diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.ts index 64d612848..7ad23db4f 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.component.ts @@ -2,8 +2,7 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { IconType } from '@hypertrace/assets-library'; import { NavigationService, SubscriptionLifecycle } from '@hypertrace/common'; import { ButtonRole, ButtonStyle, IconSize } from '@hypertrace/components'; - -import { Dashboard, ModelJson } from '@hypertrace/hyperdash'; +import { LogEvent } from '@hypertrace/distributed-tracing'; import { Observable } from 'rxjs'; import { ApiTraceDetails, ApiTraceDetailService } from './api-trace-detail.service'; @@ -53,13 +52,15 @@ import { ApiTraceDetails, ApiTraceDetailService } from './api-trace-detail.servi + + Sequence + + Logs + + +
- - +
` @@ -68,45 +69,14 @@ export class ApiTraceDetailPageComponent { public static readonly TRACE_ID_PARAM_NAME: string = 'id'; public readonly traceDetails$: Observable; - - public readonly defaultJson: ModelJson = { - type: 'container-widget', - layout: { - type: 'auto-container-layout', - 'enable-style': false - }, - children: [ - { - type: 'waterfall-widget', - title: 'Sequence Diagram', - data: { - type: 'api-trace-waterfall-data-source', - // tslint:disable-next-line: no-invalid-template-strings - 'trace-id': '${traceId}', - // tslint:disable-next-line: no-invalid-template-strings - 'start-time': '${startTime}' - } - } - ] - }; + public readonly logEvents$: Observable; public constructor( - private readonly subscriptionLifecycle: SubscriptionLifecycle, protected readonly navigationService: NavigationService, private readonly apiTraceDetailService: ApiTraceDetailService ) { this.traceDetails$ = this.apiTraceDetailService.fetchTraceDetails(); - } - - public onDashboardReady(dashboard: Dashboard): void { - this.subscriptionLifecycle.add( - this.traceDetails$.subscribe(traceDetails => { - dashboard.setVariable('traceId', traceDetails.id); - dashboard.setVariable('traceType', traceDetails.type); - dashboard.setVariable('startTime', traceDetails.startTime); - dashboard.refresh(); - }) - ); + this.logEvents$ = this.apiTraceDetailService.fetchLogEvents(); } public onClickBack(): void { diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts index 328463a6b..c5a636468 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.page.module.ts @@ -8,20 +8,40 @@ import { IconModule, LabelModule, LoadAsyncModule, + NavigableTabModule, SummaryValueModule } from '@hypertrace/components'; +import { LogEventsTableModule, NavigableDashboardModule } from '@hypertrace/distributed-tracing'; import { ObservabilityDashboardModule } from '../../shared/dashboard/observability-dashboard.module'; import { ApiTraceDetailPageComponent } from './api-trace-detail.page.component'; +import { ApiTraceLogsComponent } from './logs/api-trace-logs.component'; +import { ApiTraceSequenceComponent } from './sequence/api-trace-sequence.component'; +import { apiTraceSequenceDashboard } from './sequence/api-trace-sequence.dashboard'; const ROUTE_CONFIG: TraceRoute[] = [ { path: `:${ApiTraceDetailPageComponent.TRACE_ID_PARAM_NAME}`, - component: ApiTraceDetailPageComponent + component: ApiTraceDetailPageComponent, + children: [ + { + path: '', + redirectTo: 'sequence', + pathMatch: 'full' + }, + { + path: 'sequence', + component: ApiTraceSequenceComponent + }, + { + path: 'logs', + component: ApiTraceLogsComponent + } + ] } ]; @NgModule({ - declarations: [ApiTraceDetailPageComponent], + declarations: [ApiTraceDetailPageComponent, ApiTraceSequenceComponent, ApiTraceLogsComponent], imports: [ RouterModule.forChild(ROUTE_CONFIG), CommonModule, @@ -32,7 +52,10 @@ const ROUTE_CONFIG: TraceRoute[] = [ LoadAsyncModule, FormattingModule, ButtonModule, - CopyShareableLinkToClipboardModule + CopyShareableLinkToClipboardModule, + NavigableTabModule, + LogEventsTableModule, + NavigableDashboardModule.withDefaultDashboards(apiTraceSequenceDashboard) ] }) export class ApiTraceDetailPageModule {} diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.test.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.test.ts index b263e4257..02623c3e0 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.test.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.test.ts @@ -16,14 +16,12 @@ import { } from '@hypertrace/distributed-tracing'; import { GraphQlRequestService } from '@hypertrace/graphql-client'; import { runFakeRxjs } from '@hypertrace/test-utils'; -import { createServiceFactory, mockProvider, SpectatorService } from '@ngneat/spectator/jest'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; import { of } from 'rxjs'; import { ObservabilityTraceType } from '../../shared/graphql/model/schema/observability-traces'; import { ApiTraceDetailService } from './api-trace-detail.service'; describe('Api TraceDetailService', () => { - let spectator: SpectatorService; - const createService = createServiceFactory({ service: ApiTraceDetailService, providers: [ @@ -65,11 +63,8 @@ describe('Api TraceDetailService', () => { ] }); - beforeEach(() => { - spectator = createService(); - }); - test('should map fetch and map trace details', () => { + const spectator = createService(); runFakeRxjs(({ expectObservable }) => { expectObservable(spectator.service.fetchTraceDetails()).toBe('(x|)', { x: { @@ -87,6 +82,7 @@ describe('Api TraceDetailService', () => { }); test('should build correct query request with timestamp', () => { + const spectator = createService(); const graphqlService = spectator.inject(GraphQlRequestService); graphqlService.query.mockClear(); @@ -121,4 +117,58 @@ describe('Api TraceDetailService', () => { spanLimit: 1 }); }); + + test('should fetch and map logEvents', () => { + const spectator = createService({ + providers: [ + mockProvider(ActivatedRoute, { + paramMap: of({ + get: (key: string) => (key === 'id' ? 'test-id' : '1576364117792') + }) + }), + mockProvider(GraphQlRequestService, { + query: jest.fn().mockReturnValue( + of({ + [traceTypeKey]: 'API_TRACE', + spans: [ + { + logEvents: { + results: [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary' + } + ] + }, + displayEntityName: 'service', + displaySpanName: 'span', + protocolName: 'protocol', + startTime: 1608151401295 + } + ] + }) + ) + }) + ] + }); + + runFakeRxjs(({ expectObservable }) => { + expectObservable(spectator.service.fetchLogEvents()).toBe('(x|)', { + x: [ + { + timestamp: 'time', + attributes: undefined, + summary: 'summary', + $$spanName: { + serviceName: 'service', + protocolName: 'protocol', + apiName: 'span' + }, + spanStartTime: 1608151401295 + } + ] + }); + }); + }); }); diff --git a/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.ts b/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.ts index b33293a08..fc5e24c5c 100644 --- a/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.ts +++ b/projects/observability/src/pages/api-trace-detail/api-trace-detail.service.ts @@ -3,6 +3,8 @@ import { ActivatedRoute } from '@angular/router'; import { DateCoercer, DateFormatMode, DateFormatter, ReplayObservable } from '@hypertrace/common'; import { AttributeMetadata, + LogEvent, + LogEventsService, MetadataService, SpecificationBuilder, Trace, @@ -32,7 +34,8 @@ export class ApiTraceDetailService implements OnDestroy { public constructor( route: ActivatedRoute, private readonly metadataService: MetadataService, - private readonly graphQlQueryService: GraphQlRequestService + private readonly graphQlQueryService: GraphQlRequestService, + private readonly logEventsService: LogEventsService ) { this.routeIds$ = route.paramMap.pipe( map(paramMap => ({ @@ -66,6 +69,21 @@ export class ApiTraceDetailService implements OnDestroy { ); } + public fetchLogEvents(): Observable { + return this.routeIds$.pipe( + switchMap(routeIds => + this.logEventsService.getLogEventsGqlResponseForTrace( + routeIds.traceId, + routeIds.startTime, + ObservabilityTraceType.Api + ) + ), + map(trace => this.logEventsService.mapLogEvents(trace)), + takeUntil(this.destroyed$), + shareReplay(1) + ); + } + protected constructTraceDetails(trace: Trace, durationAttribute: AttributeMetadata): ApiTraceDetails { return { id: trace[traceIdKey], diff --git a/projects/observability/src/pages/api-trace-detail/logs/api-trace-logs.component.ts b/projects/observability/src/pages/api-trace-detail/logs/api-trace-logs.component.ts new file mode 100644 index 000000000..1baf2d1b0 --- /dev/null +++ b/projects/observability/src/pages/api-trace-detail/logs/api-trace-logs.component.ts @@ -0,0 +1,23 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { LogEvent, LogEventsTableViewType } from '@hypertrace/distributed-tracing'; +import { Observable } from 'rxjs'; +import { ApiTraceDetailService } from './../api-trace-detail.service'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + + ` +}) +export class ApiTraceLogsComponent { + public readonly logEvents$: Observable; + + public constructor(private readonly apiTraceDetailService: ApiTraceDetailService) { + this.logEvents$ = this.apiTraceDetailService.fetchLogEvents(); + } +} diff --git a/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.component.ts b/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.component.ts new file mode 100644 index 000000000..0a0ec4407 --- /dev/null +++ b/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.component.ts @@ -0,0 +1,35 @@ +import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { ApiTraceDetailService } from './../api-trace-detail.service'; +import { apiTraceSequenceDashboard } from './api-trace-sequence.dashboard'; + +@Component({ + changeDetection: ChangeDetectionStrategy.OnPush, + template: ` + + + ` +}) +export class ApiTraceSequenceComponent { + public readonly traceVariables$: Observable; + + public constructor(private readonly apiTraceDetailService: ApiTraceDetailService) { + this.traceVariables$ = this.apiTraceDetailService.fetchTraceDetails().pipe( + map(details => ({ + traceId: details.id, + startTime: details.startTime + })) + ); + } +} + +interface ApiTraceDetailVariables { + traceId: string; + startTime?: string | number; +} diff --git a/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.dashboard.ts b/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.dashboard.ts new file mode 100644 index 000000000..0b127c5c5 --- /dev/null +++ b/projects/observability/src/pages/api-trace-detail/sequence/api-trace-sequence.dashboard.ts @@ -0,0 +1,25 @@ +import { DashboardDefaultConfiguration } from '@hypertrace/distributed-tracing'; + +export const apiTraceSequenceDashboard: DashboardDefaultConfiguration = { + location: 'API_TRACE_SEQUENCE', + json: { + type: 'container-widget', + layout: { + type: 'auto-container-layout', + 'enable-style': false + }, + children: [ + { + type: 'waterfall-widget', + title: 'Sequence Diagram', + data: { + type: 'api-trace-waterfall-data-source', + // tslint:disable-next-line: no-invalid-template-strings + 'trace-id': '${traceId}', + // tslint:disable-next-line: no-invalid-template-strings + 'start-time': '${startTime}' + } + } + ] + } +}; diff --git a/projects/observability/src/public-api.ts b/projects/observability/src/public-api.ts index 462c84681..c9010ab30 100644 --- a/projects/observability/src/public-api.ts +++ b/projects/observability/src/public-api.ts @@ -10,6 +10,7 @@ export * from './shared/graphql/model/schema/filter/entity/graphql-entity-filter export * from './shared/graphql/model/schema/neighbor'; export * from './shared/graphql/model/schema/specifications/entity-specification'; export * from './shared/graphql/model/schema/specifications/explore-specification'; +export * from './shared/graphql/model/schema/observability-traces'; // Builders export * from './shared/graphql/request/builders/argument/graphql-observability-argument-builder'; @@ -116,6 +117,9 @@ export * from './pages/apis/backend-detail/traces/backend-trace-list.module'; export * from './pages/explorer/explorer.module'; +export * from './pages/api-trace-detail/logs/api-trace-logs.component'; +export * from './pages/api-trace-detail/sequence/api-trace-sequence.component'; +export * from './pages/api-trace-detail/sequence/api-trace-sequence.dashboard'; export * from './pages/api-trace-detail/api-trace-detail.page.module'; // Icon Types diff --git a/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.test.ts index 425196294..33ae7156a 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.test.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.test.ts @@ -1,6 +1,7 @@ import { createModelFactory } from '@hypertrace/dashboards/testing'; import { GraphQlQueryEventService, + LogEventsService, MetadataService, spanIdKey, SpanType, @@ -27,7 +28,10 @@ describe('Api Trace Waterfall data source model', () => { }) ) }), - mockProvider(GraphQlQueryEventService) + mockProvider(GraphQlQueryEventService), + mockProvider(LogEventsService, { + getLogEventsWithSpanStartTime: jest.fn().mockReturnValue([]) + }) ] }); diff --git a/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.ts index 1298fa4e7..07a77ec4f 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/waterfall/api-trace-waterfall-data-source.model.ts @@ -3,6 +3,7 @@ import { AttributeMetadata, GraphQlDataSourceModel, LogEvent, + LogEventsService, MetadataService, Span, spanIdKey, @@ -42,6 +43,9 @@ export class ApiTraceWaterfallDataSourceModel extends GraphQlDataSourceModel, errorCount: span.errorCount as number, - logEvents: ((span.logEvents as Dictionary) ?? {}).results ?? [] + logEvents: this.logEventsService.getLogEventsWithSpanStartTime( + span.logEvents as Dictionary, + span.startTime as number + ) }; } }