diff --git a/projects/distributed-tracing/src/public-api.ts b/projects/distributed-tracing/src/public-api.ts index 1e2267f03..e3cebcf67 100644 --- a/projects/distributed-tracing/src/public-api.ts +++ b/projects/distributed-tracing/src/public-api.ts @@ -63,6 +63,7 @@ export * from './shared/graphql/model/schema/filter/field/graphql-field-filter'; export * from './shared/graphql/model/schema/filter/id/graphql-id-filter'; export * from './shared/graphql/model/schema/filter/graphql-filter'; +export * from './shared/graphql/model/schema/filter/global-graphql-filter.service'; export { GraphQlMetricAggregationType, convertToGraphQlMetricAggregationType diff --git a/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.test.ts b/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.test.ts new file mode 100644 index 000000000..d312260dd --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.test.ts @@ -0,0 +1,58 @@ +import { createServiceFactory } from '@ngneat/spectator/jest'; +import { SPAN_SCOPE } from '../span'; +import { GraphQlFieldFilter } from './field/graphql-field-filter'; +import { GlobalGraphQlFilterService } from './global-graphql-filter.service'; +import { GraphQlOperatorType } from './graphql-filter'; + +describe('Global graphql filter serivce', () => { + const firstFilter = new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'first'); + const secondFilter = new GraphQlFieldFilter('second', GraphQlOperatorType.Equals, 'second'); + const thirdFilter = new GraphQlFieldFilter('third', GraphQlOperatorType.Equals, 'third'); + const createService = createServiceFactory({ + service: GlobalGraphQlFilterService + }); + test('provides no filters by default', () => { + const spectator = createService(); + + expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([]); + }); + + test('provides filters from multiple matching rules', () => { + const spectator = createService(); + spectator.service.setGlobalFilterRule(Symbol('first'), { + filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : []) + }); + spectator.service.setGlobalFilterRule(Symbol('second'), { + filtersForScope: scope => (scope === 'fake' ? [secondFilter] : []) + }); + spectator.service.setGlobalFilterRule(Symbol('third'), { + filtersForScope: scope => (scope === SPAN_SCOPE ? [thirdFilter] : []) + }); + expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter, thirdFilter]); + }); + + test('allows removing global filter rules', () => { + const spectator = createService(); + const filterRuleKey = Symbol('first'); + spectator.service.setGlobalFilterRule(filterRuleKey, { + filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : []) + }); + + expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter]); + + spectator.service.clearGlobalFilterRule(filterRuleKey); + expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([]); + }); + + test('merges with local filters', () => { + const spectator = createService(); + spectator.service.setGlobalFilterRule(Symbol('first'), { + filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : []) + }); + + expect(spectator.service.mergeGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter]); + + expect(spectator.service.mergeGlobalFilters('fake', [secondFilter])).toEqual([secondFilter]); + expect(spectator.service.mergeGlobalFilters(SPAN_SCOPE, [secondFilter])).toEqual([secondFilter, firstFilter]); + }); +}); diff --git a/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.ts b/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.ts new file mode 100644 index 000000000..bef63ed62 --- /dev/null +++ b/projects/distributed-tracing/src/shared/graphql/model/schema/filter/global-graphql-filter.service.ts @@ -0,0 +1,27 @@ +import { Injectable } from '@angular/core'; +import { GraphQlFilter } from './graphql-filter'; + +@Injectable({ providedIn: 'root' }) +export class GlobalGraphQlFilterService { + private readonly filterRules: Map = new Map(); + + public getGlobalFilters(scope: string): GraphQlFilter[] { + return Array.from(this.filterRules.values()).flatMap(rule => rule.filtersForScope(scope)); + } + + public mergeGlobalFilters(scope: string, localFilters: GraphQlFilter[] = []): GraphQlFilter[] { + return [...localFilters, ...this.getGlobalFilters(scope)]; + } + + public setGlobalFilterRule(ruleKey: symbol, rule: FilterRule): void { + this.filterRules.set(ruleKey, rule); + } + + public clearGlobalFilterRule(ruleKey: symbol): void { + this.filterRules.delete(ruleKey); + } +} + +interface FilterRule { + filtersForScope(scope: string): GraphQlFilter[]; +} diff --git a/projects/distributed-tracing/src/shared/graphql/request/handlers/spans/spans-graphql-query-handler.service.ts b/projects/distributed-tracing/src/shared/graphql/request/handlers/spans/spans-graphql-query-handler.service.ts index ced995bd4..1a013a02f 100644 --- a/projects/distributed-tracing/src/shared/graphql/request/handlers/spans/spans-graphql-query-handler.service.ts +++ b/projects/distributed-tracing/src/shared/graphql/request/handlers/spans/spans-graphql-query-handler.service.ts @@ -4,6 +4,7 @@ import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hype import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { MetadataService } from '../../../../services/metadata/metadata.service'; +import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service'; import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter'; import { GraphQlSortBySpecification } from '../../../model/schema/sort/graphql-sort-by-specification'; import { Span, spanIdKey, SPAN_SCOPE } from '../../../model/schema/span'; @@ -19,7 +20,10 @@ export class SpansGraphQlQueryHandlerService implements GraphQlQueryHandler): GraphQlRequestOptions { - if (this.entityMetetadata.get(request.entityType)?.volatile) { + if (this.entityMetadata.get(request.entityType)?.volatile) { return { cacheability: GraphQlRequestCacheability.NotCacheable }; } diff --git a/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.test.ts b/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.test.ts index fbfee31de..c3d04f7c8 100644 --- a/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.test.ts +++ b/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.test.ts @@ -1,6 +1,7 @@ import { FixedTimeRange, isEqualIgnoreFunctions } from '@hypertrace/common'; import { GraphQlTimeRange, MetricAggregationType, MetricHealth } from '@hypertrace/distributed-tracing'; import { GraphQlEnumArgument } from '@hypertrace/graphql-client'; +import { createServiceFactory } from '@ngneat/spectator/jest'; import { entityIdKey, entityTypeKey, ObservabilityEntityType } from '../../../../../model/schema/entity'; import { GraphQlIntervalUnit } from '../../../../../model/schema/interval/graphql-interval-unit'; import { ObservabilitySpecificationBuilder } from '../../../../builders/selections/observability-specification-builder'; @@ -15,7 +16,7 @@ import { // tslint:disable: max-file-line-count describe('Entity topology graphql query handler', () => { - const service = new EntityTopologyGraphQlQueryHandlerService(); + const createService = createServiceFactory({ service: EntityTopologyGraphQlQueryHandlerService }); const testTimeRange = GraphQlTimeRange.fromTimeRange( new FixedTimeRange(new Date(1568907645141), new Date(1568911245141)) @@ -229,14 +230,16 @@ describe('Entity topology graphql query handler', () => { }); test('only matches topology request', () => { - expect(service.matchesRequest(buildTopologyRequest())).toBe(true); - expect(service.matchesRequest({ requestType: 'other' })).toBe(false); + const spectator = createService(); + expect(spectator.service.matchesRequest(buildTopologyRequest())).toBe(true); + expect(spectator.service.matchesRequest({ requestType: 'other' })).toBe(false); }); test('builds expected request', () => { + const spectator = createService(); const request = buildTopologyRequest(); - expect(service.convertRequest(request)).toEqual({ + expect(spectator.service.convertRequest(request)).toEqual({ path: 'entities', arguments: [ { name: 'type', value: new GraphQlEnumArgument(ObservabilityEntityType.Service) }, @@ -413,6 +416,7 @@ describe('Entity topology graphql query handler', () => { }); }); test('correctly parses response', () => { + const spectator = createService(); const request = buildTopologyRequest(); const serverResponse = buildTopologyResponse(); @@ -544,7 +548,7 @@ describe('Entity topology graphql query handler', () => { serviceNode3.edges.push(service3ToBackendBEdge); backendNodeB.edges.push(service3ToBackendBEdge); - const actual = service.convertResponse(serverResponse, request); + const actual = spectator.service.convertResponse(serverResponse, request); const expected = [serviceNode1, backendNodeA, serviceNode2, serviceNode3, backendNodeB]; // Custom equality checking because built in doesn't do well with circular references + functions (which are checked by reference) expect(isEqualIgnoreFunctions(actual, expected)).toBe(true); diff --git a/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.ts b/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.ts index 211bff80e..4290b86e7 100644 --- a/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.ts +++ b/projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { Dictionary } from '@hypertrace/common'; import { + GlobalGraphQlFilterService, GraphQlFilter, GraphQlSelectionBuilder, GraphQlTimeRange, @@ -22,6 +23,7 @@ export class EntityTopologyGraphQlQueryHandlerService private readonly specBuilder: SpecificationBuilder = new SpecificationBuilder(); private readonly argBuilder: GraphQlObservabilityArgumentBuilder = new GraphQlObservabilityArgumentBuilder(); private readonly selectionBuilder: GraphQlSelectionBuilder = new GraphQlSelectionBuilder(); + public constructor(private readonly globalGraphQlFilterService: GlobalGraphQlFilterService) {} public matchesRequest(request: unknown): request is GraphQlEntityTopologyRequest { return ( @@ -38,7 +40,9 @@ export class EntityTopologyGraphQlQueryHandlerService this.argBuilder.forEntityType(request.rootNodeType), this.argBuilder.forLimit(request.rootNodeLimit), this.argBuilder.forTimeRange(request.timeRange), - ...this.argBuilder.forFilters(request.rootNodeFilters) + ...this.argBuilder.forFilters( + this.globalGraphQlFilterService.mergeGlobalFilters(request.rootNodeType, request.rootNodeFilters) + ) ], children: [ { diff --git a/projects/observability/src/shared/graphql/request/handlers/explore/explore-graphql-query-handler.service.ts b/projects/observability/src/shared/graphql/request/handlers/explore/explore-graphql-query-handler.service.ts index 25cda2b79..85b2a0fc6 100644 --- a/projects/observability/src/shared/graphql/request/handlers/explore/explore-graphql-query-handler.service.ts +++ b/projects/observability/src/shared/graphql/request/handlers/explore/explore-graphql-query-handler.service.ts @@ -1,6 +1,7 @@ import { Injectable } from '@angular/core'; import { DateCoercer, Dictionary, TimeDuration } from '@hypertrace/common'; import { + GlobalGraphQlFilterService, GraphQlFilter, GraphQlSelectionBuilder, GraphQlSortBySpecification, @@ -29,6 +30,8 @@ export class ExploreGraphQlQueryHandlerService private readonly specBuilder: ExploreSpecificationBuilder = new ExploreSpecificationBuilder(); private readonly dateCoercer: DateCoercer = new DateCoercer(); + public constructor(private readonly globalGraphQlFilterService: GlobalGraphQlFilterService) {} + public matchesRequest(request: unknown): request is GraphQlExploreRequest { return ( typeof request === 'object' && @@ -51,7 +54,9 @@ export class ExploreGraphQlQueryHandlerService this.argBuilder.forTimeRange(request.timeRange), ...this.argBuilder.forOffset(request.offset), ...this.argBuilder.forInterval(request.interval), - ...this.argBuilder.forFilters(request.filters), + ...this.argBuilder.forFilters( + this.globalGraphQlFilterService.mergeGlobalFilters(request.context, request.filters) + ), ...this.argBuilder.forGroupBy(request.groupBy), ...this.argBuilder.forOrderBys(request.orderBy) ],