Skip to content

Commit 6e35969

Browse files
feat: add global filtering that applies aross all queries (#546)
1 parent c795786 commit 6e35969

File tree

10 files changed

+141
-18
lines changed

10 files changed

+141
-18
lines changed

projects/distributed-tracing/src/public-api.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ export * from './shared/graphql/model/schema/filter/field/graphql-field-filter';
6363
export * from './shared/graphql/model/schema/filter/id/graphql-id-filter';
6464

6565
export * from './shared/graphql/model/schema/filter/graphql-filter';
66+
export * from './shared/graphql/model/schema/filter/global-graphql-filter.service';
6667
export {
6768
GraphQlMetricAggregationType,
6869
convertToGraphQlMetricAggregationType
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import { createServiceFactory } from '@ngneat/spectator/jest';
2+
import { SPAN_SCOPE } from '../span';
3+
import { GraphQlFieldFilter } from './field/graphql-field-filter';
4+
import { GlobalGraphQlFilterService } from './global-graphql-filter.service';
5+
import { GraphQlOperatorType } from './graphql-filter';
6+
7+
describe('Global graphql filter serivce', () => {
8+
const firstFilter = new GraphQlFieldFilter('first', GraphQlOperatorType.Equals, 'first');
9+
const secondFilter = new GraphQlFieldFilter('second', GraphQlOperatorType.Equals, 'second');
10+
const thirdFilter = new GraphQlFieldFilter('third', GraphQlOperatorType.Equals, 'third');
11+
const createService = createServiceFactory({
12+
service: GlobalGraphQlFilterService
13+
});
14+
test('provides no filters by default', () => {
15+
const spectator = createService();
16+
17+
expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([]);
18+
});
19+
20+
test('provides filters from multiple matching rules', () => {
21+
const spectator = createService();
22+
spectator.service.setGlobalFilterRule(Symbol('first'), {
23+
filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : [])
24+
});
25+
spectator.service.setGlobalFilterRule(Symbol('second'), {
26+
filtersForScope: scope => (scope === 'fake' ? [secondFilter] : [])
27+
});
28+
spectator.service.setGlobalFilterRule(Symbol('third'), {
29+
filtersForScope: scope => (scope === SPAN_SCOPE ? [thirdFilter] : [])
30+
});
31+
expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter, thirdFilter]);
32+
});
33+
34+
test('allows removing global filter rules', () => {
35+
const spectator = createService();
36+
const filterRuleKey = Symbol('first');
37+
spectator.service.setGlobalFilterRule(filterRuleKey, {
38+
filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : [])
39+
});
40+
41+
expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter]);
42+
43+
spectator.service.clearGlobalFilterRule(filterRuleKey);
44+
expect(spectator.service.getGlobalFilters(SPAN_SCOPE)).toEqual([]);
45+
});
46+
47+
test('merges with local filters', () => {
48+
const spectator = createService();
49+
spectator.service.setGlobalFilterRule(Symbol('first'), {
50+
filtersForScope: scope => (scope === SPAN_SCOPE ? [firstFilter] : [])
51+
});
52+
53+
expect(spectator.service.mergeGlobalFilters(SPAN_SCOPE)).toEqual([firstFilter]);
54+
55+
expect(spectator.service.mergeGlobalFilters('fake', [secondFilter])).toEqual([secondFilter]);
56+
expect(spectator.service.mergeGlobalFilters(SPAN_SCOPE, [secondFilter])).toEqual([secondFilter, firstFilter]);
57+
});
58+
});
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
import { Injectable } from '@angular/core';
2+
import { GraphQlFilter } from './graphql-filter';
3+
4+
@Injectable({ providedIn: 'root' })
5+
export class GlobalGraphQlFilterService {
6+
private readonly filterRules: Map<symbol, FilterRule> = new Map();
7+
8+
public getGlobalFilters(scope: string): GraphQlFilter[] {
9+
return Array.from(this.filterRules.values()).flatMap(rule => rule.filtersForScope(scope));
10+
}
11+
12+
public mergeGlobalFilters(scope: string, localFilters: GraphQlFilter[] = []): GraphQlFilter[] {
13+
return [...localFilters, ...this.getGlobalFilters(scope)];
14+
}
15+
16+
public setGlobalFilterRule(ruleKey: symbol, rule: FilterRule): void {
17+
this.filterRules.set(ruleKey, rule);
18+
}
19+
20+
public clearGlobalFilterRule(ruleKey: symbol): void {
21+
this.filterRules.delete(ruleKey);
22+
}
23+
}
24+
25+
interface FilterRule {
26+
filtersForScope(scope: string): GraphQlFilter[];
27+
}

projects/distributed-tracing/src/shared/graphql/request/handlers/spans/spans-graphql-query-handler.service.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hype
44
import { Observable } from 'rxjs';
55
import { map } from 'rxjs/operators';
66
import { MetadataService } from '../../../../services/metadata/metadata.service';
7+
import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service';
78
import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter';
89
import { GraphQlSortBySpecification } from '../../../model/schema/sort/graphql-sort-by-specification';
910
import { Span, spanIdKey, SPAN_SCOPE } from '../../../model/schema/span';
@@ -19,7 +20,10 @@ export class SpansGraphQlQueryHandlerService implements GraphQlQueryHandler<Grap
1920
private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder();
2021
private readonly selectionBuilder: GraphQlSelectionBuilder = new GraphQlSelectionBuilder();
2122

22-
public constructor(private readonly metadataService: MetadataService) {}
23+
public constructor(
24+
private readonly metadataService: MetadataService,
25+
private readonly globalGraphQlFilterService: GlobalGraphQlFilterService
26+
) {}
2327

2428
public matchesRequest(request: unknown): request is GraphQlSpansRequest {
2529
return (
@@ -37,7 +41,7 @@ export class SpansGraphQlQueryHandlerService implements GraphQlQueryHandler<Grap
3741
this.argBuilder.forTimeRange(request.timeRange),
3842
...this.argBuilder.forOffset(request.offset),
3943
...this.argBuilder.forOrderBy(request.sort),
40-
...this.argBuilder.forFilters(request.filters)
44+
...this.argBuilder.forFilters(this.globalGraphQlFilterService.mergeGlobalFilters(SPAN_SCOPE, request.filters))
4145
],
4246
children: [
4347
{

projects/distributed-tracing/src/shared/graphql/request/handlers/traces/trace-graphql-query-handler.service.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@ import { Dictionary, TimeDuration, TimeRangeService, TimeUnit } from '@hypertrac
33
import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hypertrace/graphql-client';
44
import { isEmpty, isNil } from 'lodash-es';
55
import { GraphQlFieldFilter } from '../../../model/schema/filter/field/graphql-field-filter';
6+
import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service';
67
import { GraphQlFilter, GraphQlOperatorType } from '../../../model/schema/filter/graphql-filter';
78
import { GraphQlIdFilter } from '../../../model/schema/filter/id/graphql-id-filter';
8-
import { Span, spanIdKey } from '../../../model/schema/span';
9+
import { Span, spanIdKey, SPAN_SCOPE } from '../../../model/schema/span';
910
import { Specification } from '../../../model/schema/specifier/specification';
1011
import { GraphQlTimeRange } from '../../../model/schema/timerange/graphql-time-range';
1112
import { resolveTraceType, Trace, traceIdKey, TraceType, traceTypeKey } from '../../../model/schema/trace';
@@ -21,7 +22,10 @@ export class TraceGraphQlQueryHandlerService implements GraphQlQueryHandler<Grap
2122
private readonly argBuilder: GraphQlArgumentBuilder = new GraphQlArgumentBuilder();
2223
private readonly selectionBuilder: GraphQlSelectionBuilder = new GraphQlSelectionBuilder();
2324

24-
public constructor(private readonly timeRangeService: TimeRangeService) {}
25+
public constructor(
26+
private readonly timeRangeService: TimeRangeService,
27+
private readonly globalGraphQlFilterService: GlobalGraphQlFilterService
28+
) {}
2529

2630
public matchesRequest(request: unknown): request is GraphQlTraceRequest {
2731
return (
@@ -40,7 +44,11 @@ export class TraceGraphQlQueryHandlerService implements GraphQlQueryHandler<Grap
4044
this.argBuilder.forTraceType(resolveTraceType(request.traceType)),
4145
this.argBuilder.forLimit(1),
4246
this.argBuilder.forTimeRange(timeRange),
43-
...this.argBuilder.forFilters([this.buildTraceIdFilter(request)])
47+
...this.argBuilder.forFilters(
48+
this.globalGraphQlFilterService.mergeGlobalFilters(resolveTraceType(request.traceType), [
49+
this.buildTraceIdFilter(request)
50+
])
51+
)
4452
],
4553
children: [
4654
{
@@ -58,7 +66,9 @@ export class TraceGraphQlQueryHandlerService implements GraphQlQueryHandler<Grap
5866
arguments: [
5967
this.argBuilder.forLimit(request.spanLimit),
6068
this.argBuilder.forTimeRange(timeRange),
61-
...this.argBuilder.forFilters([...this.buildSpansFilter(request)])
69+
...this.argBuilder.forFilters(
70+
this.globalGraphQlFilterService.mergeGlobalFilters(SPAN_SCOPE, this.buildSpansFilter(request))
71+
)
6272
],
6373
children: [
6474
{

projects/distributed-tracing/src/shared/graphql/request/handlers/traces/traces-graphql-query-handler.service.ts

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { GraphQlHandlerType, GraphQlQueryHandler, GraphQlSelection } from '@hype
44
import { Observable } from 'rxjs';
55
import { map } from 'rxjs/operators';
66
import { MetadataService } from '../../../../services/metadata/metadata.service';
7+
import { GlobalGraphQlFilterService } from '../../../model/schema/filter/global-graphql-filter.service';
78
import { GraphQlFilter } from '../../../model/schema/filter/graphql-filter';
89
import { GraphQlSortBySpecification } from '../../../model/schema/sort/graphql-sort-by-specification';
910
import { Specification } from '../../../model/schema/specifier/specification';
@@ -18,7 +19,10 @@ export class TracesGraphQlQueryHandlerService implements GraphQlQueryHandler<Gra
1819
private readonly selectionBuilder: GraphQlSelectionBuilder = new GraphQlSelectionBuilder();
1920
public readonly type: GraphQlHandlerType.Query = GraphQlHandlerType.Query;
2021

21-
public constructor(private readonly metadataService: MetadataService) {}
22+
public constructor(
23+
private readonly metadataService: MetadataService,
24+
private readonly globalGraphQlFilterService: GlobalGraphQlFilterService
25+
) {}
2226

2327
public matchesRequest(request: unknown): request is GraphQlTracesRequest {
2428
return (
@@ -37,7 +41,9 @@ export class TracesGraphQlQueryHandlerService implements GraphQlQueryHandler<Gra
3741
this.argBuilder.forTimeRange(request.timeRange),
3842
...this.argBuilder.forOffset(request.offset),
3943
...this.argBuilder.forOrderBy(request.sort),
40-
...this.argBuilder.forFilters(request.filters)
44+
...this.argBuilder.forFilters(
45+
this.globalGraphQlFilterService.mergeGlobalFilters(resolveTraceType(request.traceType), request.filters)
46+
)
4147
],
4248
children: [
4349
{

projects/observability/src/shared/graphql/request/handlers/entities/query/entities-graphql-query-builder.service.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Inject, Injectable } from '@angular/core';
22
import { Dictionary, forkJoinSafeEmpty } from '@hypertrace/common';
33
import {
4+
GlobalGraphQlFilterService,
45
GraphQlFilter,
56
GraphQlSelectionBuilder,
67
GraphQlSortBySpecification,
@@ -27,7 +28,8 @@ export class EntitiesGraphqlQueryBuilderService {
2728

2829
public constructor(
2930
private readonly metadataService: MetadataService,
30-
@Inject(ENTITY_METADATA) private readonly entityMetetadata: EntityMetadataMap
31+
private readonly globalGraphQlFilterService: GlobalGraphQlFilterService,
32+
@Inject(ENTITY_METADATA) private readonly entityMetadata: EntityMetadataMap
3133
) {}
3234

3335
public buildRequestArguments(request: GraphQlEntitiesRequest): GraphQlArgument[] {
@@ -42,7 +44,9 @@ export class EntitiesGraphqlQueryBuilderService {
4244
}
4345

4446
protected buildFilters(request: GraphQlEntitiesRequest): GraphQlArgument[] {
45-
return this.argBuilder.forFilters(request.filters);
47+
return this.argBuilder.forFilters(
48+
this.globalGraphQlFilterService.mergeGlobalFilters(request.entityType, request.filters)
49+
);
4650
}
4751

4852
public buildRequestSpecifications(request: GraphQlEntitiesRequest): GraphQlSelection[] {
@@ -82,7 +86,7 @@ export class EntitiesGraphqlQueryBuilderService {
8286
}
8387

8488
public getRequestOptions(request: Pick<GraphQlEntitiesRequest, 'entityType'>): GraphQlRequestOptions {
85-
if (this.entityMetetadata.get(request.entityType)?.volatile) {
89+
if (this.entityMetadata.get(request.entityType)?.volatile) {
8690
return { cacheability: GraphQlRequestCacheability.NotCacheable };
8791
}
8892

projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.test.ts

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { FixedTimeRange, isEqualIgnoreFunctions } from '@hypertrace/common';
22
import { GraphQlTimeRange, MetricAggregationType, MetricHealth } from '@hypertrace/distributed-tracing';
33
import { GraphQlEnumArgument } from '@hypertrace/graphql-client';
4+
import { createServiceFactory } from '@ngneat/spectator/jest';
45
import { entityIdKey, entityTypeKey, ObservabilityEntityType } from '../../../../../model/schema/entity';
56
import { GraphQlIntervalUnit } from '../../../../../model/schema/interval/graphql-interval-unit';
67
import { ObservabilitySpecificationBuilder } from '../../../../builders/selections/observability-specification-builder';
@@ -15,7 +16,7 @@ import {
1516

1617
// tslint:disable: max-file-line-count
1718
describe('Entity topology graphql query handler', () => {
18-
const service = new EntityTopologyGraphQlQueryHandlerService();
19+
const createService = createServiceFactory({ service: EntityTopologyGraphQlQueryHandlerService });
1920

2021
const testTimeRange = GraphQlTimeRange.fromTimeRange(
2122
new FixedTimeRange(new Date(1568907645141), new Date(1568911245141))
@@ -229,14 +230,16 @@ describe('Entity topology graphql query handler', () => {
229230
});
230231

231232
test('only matches topology request', () => {
232-
expect(service.matchesRequest(buildTopologyRequest())).toBe(true);
233-
expect(service.matchesRequest({ requestType: 'other' })).toBe(false);
233+
const spectator = createService();
234+
expect(spectator.service.matchesRequest(buildTopologyRequest())).toBe(true);
235+
expect(spectator.service.matchesRequest({ requestType: 'other' })).toBe(false);
234236
});
235237

236238
test('builds expected request', () => {
239+
const spectator = createService();
237240
const request = buildTopologyRequest();
238241

239-
expect(service.convertRequest(request)).toEqual({
242+
expect(spectator.service.convertRequest(request)).toEqual({
240243
path: 'entities',
241244
arguments: [
242245
{ name: 'type', value: new GraphQlEnumArgument(ObservabilityEntityType.Service) },
@@ -413,6 +416,7 @@ describe('Entity topology graphql query handler', () => {
413416
});
414417
});
415418
test('correctly parses response', () => {
419+
const spectator = createService();
416420
const request = buildTopologyRequest();
417421
const serverResponse = buildTopologyResponse();
418422

@@ -544,7 +548,7 @@ describe('Entity topology graphql query handler', () => {
544548
serviceNode3.edges.push(service3ToBackendBEdge);
545549
backendNodeB.edges.push(service3ToBackendBEdge);
546550

547-
const actual = service.convertResponse(serverResponse, request);
551+
const actual = spectator.service.convertResponse(serverResponse, request);
548552
const expected = [serviceNode1, backendNodeA, serviceNode2, serviceNode3, backendNodeB];
549553
// Custom equality checking because built in doesn't do well with circular references + functions (which are checked by reference)
550554
expect(isEqualIgnoreFunctions(actual, expected)).toBe(true);

projects/observability/src/shared/graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable } from '@angular/core';
22
import { Dictionary } from '@hypertrace/common';
33
import {
4+
GlobalGraphQlFilterService,
45
GraphQlFilter,
56
GraphQlSelectionBuilder,
67
GraphQlTimeRange,
@@ -22,6 +23,7 @@ export class EntityTopologyGraphQlQueryHandlerService
2223
private readonly specBuilder: SpecificationBuilder = new SpecificationBuilder();
2324
private readonly argBuilder: GraphQlObservabilityArgumentBuilder = new GraphQlObservabilityArgumentBuilder();
2425
private readonly selectionBuilder: GraphQlSelectionBuilder = new GraphQlSelectionBuilder();
26+
public constructor(private readonly globalGraphQlFilterService: GlobalGraphQlFilterService) {}
2527

2628
public matchesRequest(request: unknown): request is GraphQlEntityTopologyRequest {
2729
return (
@@ -38,7 +40,9 @@ export class EntityTopologyGraphQlQueryHandlerService
3840
this.argBuilder.forEntityType(request.rootNodeType),
3941
this.argBuilder.forLimit(request.rootNodeLimit),
4042
this.argBuilder.forTimeRange(request.timeRange),
41-
...this.argBuilder.forFilters(request.rootNodeFilters)
43+
...this.argBuilder.forFilters(
44+
this.globalGraphQlFilterService.mergeGlobalFilters(request.rootNodeType, request.rootNodeFilters)
45+
)
4246
],
4347
children: [
4448
{

projects/observability/src/shared/graphql/request/handlers/explore/explore-graphql-query-handler.service.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Injectable } from '@angular/core';
22
import { DateCoercer, Dictionary, TimeDuration } from '@hypertrace/common';
33
import {
4+
GlobalGraphQlFilterService,
45
GraphQlFilter,
56
GraphQlSelectionBuilder,
67
GraphQlSortBySpecification,
@@ -29,6 +30,8 @@ export class ExploreGraphQlQueryHandlerService
2930
private readonly specBuilder: ExploreSpecificationBuilder = new ExploreSpecificationBuilder();
3031
private readonly dateCoercer: DateCoercer = new DateCoercer();
3132

33+
public constructor(private readonly globalGraphQlFilterService: GlobalGraphQlFilterService) {}
34+
3235
public matchesRequest(request: unknown): request is GraphQlExploreRequest {
3336
return (
3437
typeof request === 'object' &&
@@ -51,7 +54,9 @@ export class ExploreGraphQlQueryHandlerService
5154
this.argBuilder.forTimeRange(request.timeRange),
5255
...this.argBuilder.forOffset(request.offset),
5356
...this.argBuilder.forInterval(request.interval),
54-
...this.argBuilder.forFilters(request.filters),
57+
...this.argBuilder.forFilters(
58+
this.globalGraphQlFilterService.mergeGlobalFilters(request.context, request.filters)
59+
),
5560
...this.argBuilder.forGroupBy(request.groupBy),
5661
...this.argBuilder.forOrderBys(request.orderBy)
5762
],

0 commit comments

Comments
 (0)