diff --git a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.test.ts index df335e527..0dcdf1b1c 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.test.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.test.ts @@ -1,9 +1,8 @@ -import { ColorService, TimeDuration, TimeUnit } from '@hypertrace/common'; +import { ColorService, FixedTimeRange, TimeDuration, TimeUnit } from '@hypertrace/common'; import { createModelFactory } from '@hypertrace/dashboards/testing'; import { AttributeMetadataType, GraphQlQueryEventService, - GraphQlTimeRange, MetadataService, MetricAggregationType } from '@hypertrace/distributed-tracing'; @@ -29,6 +28,8 @@ import { ExploreCartesianDataSourceModel, ExplorerData } from './explore-cartesi describe('Explore cartesian data source model', () => { const testInterval = new TimeDuration(5, TimeUnit.Minute); + const endTime = new Date('2021-05-11T00:35:00.000Z'); + const startTime = new Date(endTime.getTime() - 2 * testInterval.toMillis()); const modelFactory = createModelFactory({ providers: [ @@ -74,7 +75,7 @@ describe('Explore cartesian data source model', () => { beforeEach(() => { model = modelFactory(TestExploreCartesianDataSourceModel, { api: { - getTimeRange: jest.fn().mockReturnValue(new GraphQlTimeRange(2, 3)) + getTimeRange: () => new FixedTimeRange(startTime, endTime) } }).model; }); @@ -100,14 +101,14 @@ describe('Explore cartesian data source model', () => { value: 10, type: AttributeMetadataType.Number }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { value: 15, type: AttributeMetadataType.Number }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime } ] }, @@ -122,11 +123,15 @@ describe('Explore cartesian data source model', () => { type: CartesianSeriesVisualizationType.Line, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 10 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 15 } ] @@ -231,7 +236,7 @@ describe('Explore cartesian data source model', () => { value: 'first', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { @@ -242,7 +247,7 @@ describe('Explore cartesian data source model', () => { value: 'first', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime }, { 'sum(foo)': { @@ -253,7 +258,7 @@ describe('Explore cartesian data source model', () => { value: 'second', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { @@ -264,7 +269,7 @@ describe('Explore cartesian data source model', () => { value: 'second', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime } ] }, @@ -279,11 +284,15 @@ describe('Explore cartesian data source model', () => { type: CartesianSeriesVisualizationType.Area, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 10 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 15 } ] @@ -294,11 +303,15 @@ describe('Explore cartesian data source model', () => { type: CartesianSeriesVisualizationType.Area, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 20 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 25 } ] diff --git a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.ts index b2a98ffdc..cb81ef0c2 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-cartesian-data-source.model.ts @@ -1,5 +1,10 @@ import { ColorService, forkJoinSafeEmpty, RequireBy, TimeDuration } from '@hypertrace/common'; -import { GraphQlDataSourceModel, GraphQlFilter, MetadataService } from '@hypertrace/distributed-tracing'; +import { + GraphQlDataSourceModel, + GraphQlFilter, + GraphQlTimeRange, + MetadataService +} from '@hypertrace/distributed-tracing'; import { ModelInject } from '@hypertrace/hyperdash-angular'; import { isEmpty } from 'lodash-es'; import { NEVER, Observable, of } from 'rxjs'; @@ -36,21 +41,27 @@ export abstract class ExploreCartesianDataSourceModel extends GraphQlDataSourceM protected fetchResults(interval: TimeDuration | 'AUTO'): Observable> { const requestState = this.buildRequestState(interval); - + const timeRange = this.getTimeRangeOrThrow(); if (requestState === undefined) { return NEVER; } return this.query(inheritedFilters => - this.buildExploreRequest(requestState, this.getFilters(inheritedFilters)) - ).pipe(mergeMap(response => this.mapResponseData(requestState, response))); + this.buildExploreRequest(requestState, this.getFilters(inheritedFilters), timeRange) + ).pipe( + mergeMap(response => + this.mapResponseData(requestState, response, requestState.interval as TimeDuration, timeRange) + ) + ); } protected mapResponseData( requestState: ExploreRequestState, - response: GraphQlExploreResponse + response: GraphQlExploreResponse, + interval: TimeDuration, + timeRange: GraphQlTimeRange ): Observable> { - return this.getAllData(requestState, response).pipe( + return this.getAllData(requestState, response, interval, timeRange).pipe( map(explorerResults => ({ series: explorerResults, bands: [] @@ -58,21 +69,30 @@ export abstract class ExploreCartesianDataSourceModel extends GraphQlDataSourceM ); } - protected buildExploreRequest(requestState: ExploreRequestState, filters: GraphQlFilter[]): GraphQlExploreRequest { + protected buildExploreRequest( + requestState: ExploreRequestState, + filters: GraphQlFilter[], + timeRange: GraphQlTimeRange + ): GraphQlExploreRequest { return { requestType: EXPLORE_GQL_REQUEST, selections: requestState.series.map(series => series.specification), context: requestState.context, limit: requestState.resultLimit, - timeRange: this.getTimeRangeOrThrow(), + timeRange: timeRange, interval: requestState.interval as TimeDuration, filters: filters, groupBy: requestState.groupBy }; } - private getAllData(request: ExploreRequestState, response: GraphQlExploreResponse): Observable { - return this.buildAllSeries(request, new ExploreResult(response)); + private getAllData( + request: ExploreRequestState, + response: GraphQlExploreResponse, + interval?: TimeDuration, + timeRange?: GraphQlTimeRange + ): Observable { + return this.buildAllSeries(request, new ExploreResult(response, interval, timeRange)); } protected buildAllSeries(request: ExploreRequestState, result: ExploreResult): Observable { diff --git a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-result.ts b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-result.ts index ddde7646b..247394685 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/explore/explore-result.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/explore/explore-result.ts @@ -1,4 +1,5 @@ -import { MetricAggregationType } from '@hypertrace/distributed-tracing'; +import { TimeDuration } from '@hypertrace/common'; +import { GraphQlTimeRange, MetricAggregationType } from '@hypertrace/distributed-tracing'; import { groupBy } from 'lodash-es'; import { MetricTimeseriesInterval } from '../../../../graphql/model/metric/metric-timeseries'; import { ExploreSpecification } from '../../../../graphql/model/schema/specifications/explore-specification'; @@ -15,7 +16,11 @@ export class ExploreResult { private readonly specBuilder: ExploreSpecificationBuilder = new ExploreSpecificationBuilder(); - public constructor(private readonly response: GraphQlExploreResponse) {} + public constructor( + private readonly response: GraphQlExploreResponse, + private readonly interval?: TimeDuration, + private readonly timeRange?: GraphQlTimeRange + ) {} public getTimeSeriesData(metricKey: string, aggregation: MetricAggregationType): MetricTimeseriesInterval[] { return this.extractTimeseriesForSpec(this.specBuilder.exploreSpecificationForKey(metricKey, aggregation)); @@ -42,7 +47,7 @@ export class ExploreResult { return new Map( Object.entries(groupedResults).map(([concatenatedGroupNames, results]) => [ concatenatedGroupNames.split(','), - results.map(result => this.resultToTimeseriesInterval(result, spec)) + this.resultsToTimeseriesIntervals(results, spec) ]) ); } @@ -52,7 +57,7 @@ export class ExploreResult { } private extractTimeseriesForSpec(spec: ExploreSpecification): MetricTimeseriesInterval[] { - return this.resultsContainingSpec(spec).map(result => this.resultToTimeseriesInterval(result, spec)); + return this.resultsToTimeseriesIntervals(this.resultsContainingSpec(spec), spec); } private resultToGroupData( @@ -72,6 +77,43 @@ export class ExploreResult { .map(name => (name === ExploreResult.OTHER_SERVER_GROUP_NAME ? ExploreResult.OTHER_UI_GROUP_NAME : name)); } + private resultsToTimeseriesIntervals( + results: GraphQlExploreResult[], + spec: ExploreSpecification + ): MetricTimeseriesInterval[] { + if (this.interval !== undefined && this.timeRange !== undefined) { + // This should add missing data to array + + // Add all intervals + const buckets = []; + const intervalDuration = this.interval.toMillis(); + const startTime = Math.floor(this.timeRange.from.valueOf() / intervalDuration) * intervalDuration; + const endTime = Math.ceil(this.timeRange.to.valueOf() / intervalDuration) * intervalDuration; + + for (let timestamp = startTime; timestamp <= endTime; timestamp = timestamp + intervalDuration) { + buckets.push(timestamp); + } + + const resultBucketMap: Map = new Map( + results + .map(result => this.resultToTimeseriesInterval(result, spec)) + .map(metric => [metric.timestamp.getTime(), metric]) + ); + + const metrics = buckets.map( + timestamp => + resultBucketMap.get(timestamp) ?? { + value: 0, + timestamp: new Date(timestamp) + } + ); + + return metrics; + } + + return results.map(result => this.resultToTimeseriesInterval(result, spec)); + } + private resultToTimeseriesInterval( result: GraphQlExploreResult, spec: ExploreSpecification diff --git a/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.test.ts index 887bcd24c..9ca630f37 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.test.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.test.ts @@ -1,9 +1,8 @@ -import { ColorService, TimeDuration, TimeUnit } from '@hypertrace/common'; +import { ColorService, FixedTimeRange, TimeDuration, TimeUnit } from '@hypertrace/common'; import { createModelFactory } from '@hypertrace/dashboards/testing'; import { AttributeMetadataType, GraphQlQueryEventService, - GraphQlTimeRange, MetadataService, MetricAggregationType } from '@hypertrace/distributed-tracing'; @@ -27,6 +26,8 @@ import { ExplorerVisualizationCartesianDataSourceModel } from './explorer-visual describe('Explorer Visualization cartesian data source model', () => { const testInterval = new TimeDuration(5, TimeUnit.Minute); + const endTime = new Date('2021-05-11T00:35:00.000Z'); + const startTime = new Date(endTime.getTime() - 2 * testInterval.toMillis()); const modelFactory = createModelFactory({ providers: [ @@ -90,14 +91,14 @@ describe('Explorer Visualization cartesian data source model', () => { beforeEach(() => { model = modelFactory(ExplorerVisualizationCartesianDataSourceModel, { api: { - getTimeRange: jest.fn().mockReturnValue(new GraphQlTimeRange(2, 3)) + getTimeRange: () => new FixedTimeRange(startTime, endTime) } }).model; }); test('can build timeseries data', () => { model.request = buildVisualizationRequest({ - interval: 'AUTO', + interval: new TimeDuration(5, TimeUnit.Minute), groupBy: undefined, series: [ { @@ -119,14 +120,14 @@ describe('Explorer Visualization cartesian data source model', () => { value: 10, type: AttributeMetadataType.Number }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { value: 15, type: AttributeMetadataType.Number }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime } ] }, @@ -141,11 +142,15 @@ describe('Explorer Visualization cartesian data source model', () => { type: CartesianSeriesVisualizationType.Line, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 10 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 15 } ] @@ -224,7 +229,7 @@ describe('Explorer Visualization cartesian data source model', () => { test('can build grouped timeseries data', () => { model.request = buildVisualizationRequest({ - interval: 'AUTO', + interval: new TimeDuration(5, TimeUnit.Minute), groupBy: { keys: ['baz'], limit: 5 @@ -253,7 +258,7 @@ describe('Explorer Visualization cartesian data source model', () => { value: 'first', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { @@ -264,7 +269,7 @@ describe('Explorer Visualization cartesian data source model', () => { value: 'first', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime }, { 'sum(foo)': { @@ -275,7 +280,7 @@ describe('Explorer Visualization cartesian data source model', () => { value: 'second', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(0) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: startTime }, { 'sum(foo)': { @@ -286,7 +291,7 @@ describe('Explorer Visualization cartesian data source model', () => { value: 'second', type: AttributeMetadataType.String }, - [GQL_EXPLORE_RESULT_INTERVAL_KEY]: new Date(1) + [GQL_EXPLORE_RESULT_INTERVAL_KEY]: endTime } ] }, @@ -301,11 +306,15 @@ describe('Explorer Visualization cartesian data source model', () => { type: CartesianSeriesVisualizationType.Area, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 10 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 15 } ] @@ -316,11 +325,15 @@ describe('Explorer Visualization cartesian data source model', () => { type: CartesianSeriesVisualizationType.Area, data: [ { - timestamp: new Date(0), + timestamp: startTime, value: 20 }, { - timestamp: new Date(1), + timestamp: new Date('2021-05-11T00:30:00.000Z'), + value: 0 + }, + { + timestamp: endTime, value: 25 } ] diff --git a/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.ts index 5d98ff51e..907ef0621 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/explorer-visualization/explorer-visualization-cartesian-data-source.model.ts @@ -1,4 +1,5 @@ -import { GraphQlFilter } from '@hypertrace/distributed-tracing'; +import { TimeDuration } from '@hypertrace/common'; +import { GraphQlFilter, GraphQlTimeRange } from '@hypertrace/distributed-tracing'; import { Model } from '@hypertrace/hyperdash'; import { NEVER, Observable } from 'rxjs'; import { mergeMap, switchMap } from 'rxjs/operators'; @@ -24,21 +25,28 @@ export class ExplorerVisualizationCartesianDataSourceModel extends ExploreCartes } return this.request.exploreQuery$.pipe( - switchMap(exploreRequest => - this.query(inheritedFilters => - this.appendFilters(exploreRequest, this.getFilters(inheritedFilters)) - ).pipe(mergeMap(response => this.mapResponseData(this.request!, response))) - ) + switchMap(exploreRequest => { + const timeRange = this.getTimeRangeOrThrow(); + + return this.query(inheritedFilters => + this.appendFilters(exploreRequest, this.getFilters(inheritedFilters), timeRange) + ).pipe( + mergeMap(response => + this.mapResponseData(this.request!, response, exploreRequest.interval as TimeDuration, timeRange) + ) + ); + }) ); } protected appendFilters( request: Omit, - filters: GraphQlFilter[] + filters: GraphQlFilter[], + timeRange: GraphQlTimeRange ): GraphQlExploreRequest { return { ...request, - timeRange: this.getTimeRangeOrThrow(), + timeRange: timeRange, filters: [...(request.filters ?? []), ...filters] }; } diff --git a/projects/observability/src/shared/dashboard/widgets/charts/cartesian-widget/cartesian-widget-renderer.component.ts b/projects/observability/src/shared/dashboard/widgets/charts/cartesian-widget/cartesian-widget-renderer.component.ts index 362bab47a..0e3298da3 100644 --- a/projects/observability/src/shared/dashboard/widgets/charts/cartesian-widget/cartesian-widget-renderer.component.ts +++ b/projects/observability/src/shared/dashboard/widgets/charts/cartesian-widget/cartesian-widget-renderer.component.ts @@ -59,12 +59,13 @@ export class CartesianWidgetRendererComponent extends Interacti tap(fetcher => { this.fetcher = fetcher; const defaultInterval = this.model.defaultInterval?.getDuration(); - const intervalOptions = this.buildIntervalOptions(); - this.selectedInterval = this.getBestIntervalMatch(intervalOptions, this.selectedInterval ?? defaultInterval); if (this.intervalSupported()) { + this.selectedInterval = this.getBestIntervalMatch(intervalOptions, this.selectedInterval ?? defaultInterval); this.intervalOptions = intervalOptions; // The only thing this flag controls is whether options are available (and thus, the selector) + } else { + this.selectedInterval = this.getBestIntervalMatch(intervalOptions, defaultInterval); } }), switchMap(() => this.buildDataObservable())