diff --git a/projects/components/src/download-json/download-json.component.test.ts b/projects/components/src/download-json/download-json.component.test.ts index 6f1b6466e..c5f7fd56c 100644 --- a/projects/components/src/download-json/download-json.component.test.ts +++ b/projects/components/src/download-json/download-json.component.test.ts @@ -9,7 +9,7 @@ import { IconComponent } from '../icon/icon.component'; import { DownloadJsonComponent } from './download-json.component'; import { DownloadJsonModule } from './download-json.module'; -describe('Button Component', () => { +describe('Download Json Component', () => { let spectator: Spectator; const mockElement = document.createElement('a'); const createElementSpy = jest.fn().mockReturnValue(mockElement); @@ -53,8 +53,7 @@ describe('Button Component', () => { spyOn(spectator.component, 'triggerDownload'); expect(spectator.component.dataLoading).toBe(false); - expect(spectator.component.fileName).toBe('download'); - expect(spectator.component.tooltip).toBe('Download Json'); + expect(spectator.component.fileName).toBe('download.json'); const element = spectator.query('.download-json'); expect(element).toExist(); diff --git a/projects/components/src/download-json/download-json.component.ts b/projects/components/src/download-json/download-json.component.ts index 186855eb6..42120fd19 100644 --- a/projects/components/src/download-json/download-json.component.ts +++ b/projects/components/src/download-json/download-json.component.ts @@ -12,7 +12,7 @@ import { NotificationService } from '../notification/notification.service'; changeDetection: ChangeDetectionStrategy.OnPush, styleUrls: ['./download-json.component.scss'], template: ` -
+
; @Input() - public fileName: string = 'download'; - - @Input() - public tooltip: string = 'Download Json'; + public fileName: string = 'download.json'; public dataLoading: boolean = false; private readonly dlJsonAnchorElement: HTMLAnchorElement; @@ -72,7 +69,7 @@ export class DownloadJsonComponent { 'href', `data:text/json;charset=utf-8,${encodeURIComponent(data)}` ); - this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', `${this.fileName}.json`); + this.renderer.setAttribute(this.dlJsonAnchorElement, 'download', this.fileName); this.renderer.setAttribute(this.dlJsonAnchorElement, 'display', 'none'); this.dlJsonAnchorElement.click(); } diff --git a/projects/components/src/download-json/download-json.module.ts b/projects/components/src/download-json/download-json.module.ts index 801f24056..f731e0897 100644 --- a/projects/components/src/download-json/download-json.module.ts +++ b/projects/components/src/download-json/download-json.module.ts @@ -3,12 +3,11 @@ import { NgModule } from '@angular/core'; import { ButtonModule } from '../button/button.module'; import { IconModule } from '../icon/icon.module'; import { NotificationModule } from '../notification/notification.module'; -import { TooltipModule } from '../tooltip/tooltip.module'; import { DownloadJsonComponent } from './download-json.component'; @NgModule({ declarations: [DownloadJsonComponent], - imports: [CommonModule, ButtonModule, NotificationModule, IconModule, TooltipModule], + imports: [CommonModule, ButtonModule, NotificationModule, IconModule], exports: [DownloadJsonComponent] }) export class DownloadJsonModule {} 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 96d7893d3..64abd18ed 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 @@ -43,6 +43,13 @@ import { TraceDetails, TraceDetailService } from './trace-detail.service';
+ +
@@ -60,6 +67,7 @@ export class TraceDetailPageComponent { public static readonly TRACE_ID_PARAM_NAME: string = 'id'; public readonly traceDetails$: Observable; + public readonly exportSpans$: Observable; public constructor( private readonly subscriptionLifecycle: SubscriptionLifecycle, @@ -67,6 +75,7 @@ export class TraceDetailPageComponent { private readonly traceDetailService: TraceDetailService ) { this.traceDetails$ = this.traceDetailService.fetchTraceDetails(); + this.exportSpans$ = this.traceDetailService.fetchExportSpans(); } public onDashboardReady(dashboard: Dashboard): 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 e441735d0..541fb3332 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 @@ -4,10 +4,12 @@ import { RouterModule } from '@angular/router'; import { FormattingModule, TraceRoute } from '@hypertrace/common'; import { CopyShareableLinkToClipboardModule, + DownloadJsonModule, IconModule, LabelModule, LoadAsyncModule, - SummaryValueModule + SummaryValueModule, + TooltipModule } from '@hypertrace/components'; import { NavigableDashboardModule } from '../../shared/dashboard/dashboard-wrapper/navigable-dashboard.module'; import { TracingDashboardModule } from '../../shared/dashboard/tracing-dashboard.module'; @@ -30,9 +32,11 @@ const ROUTE_CONFIG: TraceRoute[] = [ TracingDashboardModule, IconModule, SummaryValueModule, + TooltipModule, LoadAsyncModule, FormattingModule, CopyShareableLinkToClipboardModule, + DownloadJsonModule, NavigableDashboardModule.withDefaultDashboards(traceDetailDashboard) ] }) 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 5d6a80881..1efc4bd24 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 @@ -7,6 +7,10 @@ import { Observable, Subject } from 'rxjs'; import { map, shareReplay, switchMap, takeUntil } from 'rxjs/operators'; import { Trace, traceIdKey, TraceType, traceTypeKey } from '../../shared/graphql/model/schema/trace'; import { SpecificationBuilder } from '../../shared/graphql/request/builders/specification/specification-builder'; +import { + ExportSpansGraphQlQueryHandlerService, + EXPORT_SPANS_GQL_REQUEST +} from '../../shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; import { TraceGraphQlQueryHandlerService, TRACE_GQL_REQUEST @@ -82,6 +86,21 @@ export class TraceDetailService implements OnDestroy { ); } + public fetchExportSpans(): Observable { + return this.routeIds$.pipe( + switchMap(routeIds => + this.graphQlQueryService.query({ + requestType: EXPORT_SPANS_GQL_REQUEST, + traceId: routeIds.traceId, + timestamp: this.dateCoercer.coerce(routeIds.startTime), + limit: 1000 + }) + ), + 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 327e12e1f..a7b4aedd8 100644 --- a/projects/distributed-tracing/src/public-api.ts +++ b/projects/distributed-tracing/src/public-api.ts @@ -38,6 +38,7 @@ export * from './shared/dashboard/widgets/table/table-widget-view-toggle.model'; export * from './shared/services/filter-builder/graphql-filter-builder.service'; // Handlers +export * from './shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/traces/trace-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/traces/traces-graphql-query-handler.service'; export * from './shared/graphql/request/handlers/spans/span-graphql-query-handler.service'; diff --git a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts index f7ece9781..ba8a69527 100644 --- a/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts +++ b/projects/distributed-tracing/src/shared/dashboard/data/graphql/graphql-handler-configuration.ts @@ -1,10 +1,12 @@ import { SpanGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/span-graphql-query-handler.service'; import { SpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/spans/spans-graphql-query-handler.service'; +import { ExportSpansGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/export-spans-graphql-query-handler.service'; import { TraceGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/trace-graphql-query-handler.service'; import { TracesGraphQlQueryHandlerService } from '../../../graphql/request/handlers/traces/traces-graphql-query-handler.service'; import { MetadataGraphQlQueryHandlerService } from '../../../services/metadata/handler/metadata-graphql-query-handler.service'; export const GRAPHQL_DATA_SOURCE_HANDLER_PROVIDERS = [ + ExportSpansGraphQlQueryHandlerService, TracesGraphQlQueryHandlerService, TraceGraphQlQueryHandlerService, SpansGraphQlQueryHandlerService, diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts new file mode 100644 index 000000000..8b9357b23 --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.test.ts @@ -0,0 +1,124 @@ +import { FixedTimeRange, TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common'; +import { GraphQlEnumArgument } from '@hypertrace/graphql-client'; +import { createServiceFactory, mockProvider } from '@ngneat/spectator/jest'; +import { GraphQlFilterType } from '../../../model/schema/filter/graphql-filter'; +import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range'; +import { TRACE_SCOPE } from '../../../model/schema/trace'; +import { + ExportSpansGraphQlQueryHandlerService, + EXPORT_SPANS_GQL_REQUEST, + GraphQlExportSpansRequest +} from './export-spans-graphql-query-handler.service'; + +describe('ExportSpansGraphQlQueryHandlerService', () => { + const createService = createServiceFactory({ + service: ExportSpansGraphQlQueryHandlerService, + providers: [ + mockProvider(TimeRangeService, { + getCurrentTimeRange: jest + .fn() + .mockReturnValue(new FixedTimeRange(new Date(1568907645141), new Date(1568911245141))) + }) + ] + }); + + const testTimeRange = GraphQlTimeRange.fromTimeRange( + new FixedTimeRange(new Date(1568907645141), new Date(1568911245141)) + ); + const buildRequest = (timestamp?: Date): GraphQlExportSpansRequest => ({ + requestType: EXPORT_SPANS_GQL_REQUEST, + traceId: 'test-id', + timestamp: timestamp, + limit: 1 + }); + + test('matches request', () => { + const spectator = createService(); + expect(spectator.service.matchesRequest(buildRequest())).toBe(true); + expect(spectator.service.matchesRequest({ requestType: 'other' })).toBe(false); + }); + + test('produces expected graphql', () => { + const spectator = createService(); + const expected = spectator.service.convertRequest(buildRequest()); + expect(expected).toEqual({ + path: 'exportSpans', + arguments: [ + { + name: 'limit', + value: 1 + }, + { + name: 'between', + value: { + startTime: new Date(testTimeRange.from), + endTime: new Date(testTimeRange.to) + } + }, + { + name: 'filterBy', + value: [ + { + operator: new GraphQlEnumArgument('EQUALS'), + value: 'test-id', + type: new GraphQlEnumArgument(GraphQlFilterType.Id), + idType: new GraphQlEnumArgument(TRACE_SCOPE) + } + ] + } + ], + children: [ + { + path: 'result' + } + ] + }); + }); + + test('produces expected graphql with timestamp', () => { + const spectator = createService(); + const traceTimestamp = new Date(new TimeDuration(30, TimeUnit.Minute).toMillis()); + const expected = spectator.service.convertRequest(buildRequest(traceTimestamp)); + expect(expected).toEqual({ + path: 'exportSpans', + arguments: [ + { + name: 'limit', + value: 1 + }, + { + name: 'between', + value: { + startTime: new Date(0), + endTime: new Date(traceTimestamp.getTime() * 2) + } + }, + { + name: 'filterBy', + value: [ + { + operator: new GraphQlEnumArgument('EQUALS'), + value: 'test-id', + type: new GraphQlEnumArgument(GraphQlFilterType.Id), + idType: new GraphQlEnumArgument(TRACE_SCOPE) + } + ] + } + ], + children: [ + { + path: 'result' + } + ] + }); + }); + + test('converts response', () => { + const spectator = createService(); + const exportSpansResponse = { + result: '{}' + }; + + expect(spectator.service.convertResponse(exportSpansResponse)).toEqual('{}'); + }); +}); diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts new file mode 100644 index 000000000..9931ea8a3 --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/traces/export-spans-graphql-query-handler.service.ts @@ -0,0 +1,81 @@ +import { Injectable } from '@angular/core'; +import { TimeDuration, TimeRangeService, TimeUnit } from '@hypertrace/common'; +import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hypertrace/graphql-client'; +import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service'; +import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter'; +import { GraphQlIdFilter } from '../../../model/schema/filter/id/graphql-id-filter'; +import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range'; +import { resolveTraceType, TraceType } from '../../../model/schema/trace'; +import { GraphQlArgumentBuilder } from '../../builders/argument/graphql-argument-builder'; + +@Injectable({ providedIn: 'root' }) +export class ExportSpansGraphQlQueryHandlerService + implements GraphQlQueryHandler { + public readonly type: GraphQlHandlerType.Query = GraphQlHandlerType.Query; + private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder(); + + public constructor( + private readonly timeRangeService: TimeRangeService, + private readonly globalGraphQlFilterService: GlobalGraphQlFilterService + ) {} + + public matchesRequest(request: unknown): request is GraphQlExportSpansRequest { + return ( + typeof request === 'object' && + request !== null && + (request as Partial).requestType === EXPORT_SPANS_GQL_REQUEST + ); + } + + public convertRequest(request: GraphQlExportSpansRequest): GraphQlSelection { + const timeRange = this.buildTimeRange(request.timestamp); + + return { + path: 'exportSpans', + arguments: [ + this.argBuilder.forLimit(request.limit), + this.argBuilder.forTimeRange(timeRange), + ...this.argBuilder.forFilters( + this.globalGraphQlFilterService.mergeGlobalFilters(resolveTraceType(request.traceType), [ + this.buildTraceIdFilter(request) + ]) + ) + ], + children: [ + { + path: 'result' + } + ] + }; + } + + public convertResponse(response: ExportSpansResponse): string | undefined { + return response.result; + } + + private buildTraceIdFilter(request: GraphQlExportSpansRequest): GraphQlFilter { + return new GraphQlIdFilter(request.traceId, resolveTraceType(request.traceType)); + } + + protected buildTimeRange(timestamp?: Date): GraphQlTimeRange { + const duration = new TimeDuration(30, TimeUnit.Minute); + + return timestamp + ? new GraphQlTimeRange(timestamp.getTime() - duration.toMillis(), timestamp.getTime() + duration.toMillis()) + : GraphQlTimeRange.fromTimeRange(this.timeRangeService.getCurrentTimeRange()); + } +} + +export const EXPORT_SPANS_GQL_REQUEST = Symbol('GraphQL Export Spans Request'); + +export interface GraphQlExportSpansRequest { + requestType: typeof EXPORT_SPANS_GQL_REQUEST; + traceType?: TraceType; + traceId: string; + limit: number; + timestamp?: Date; +} + +export interface ExportSpansResponse { + result: string; +}