Skip to content

Commit

Permalink
fix: topology edge filter support (#2388)
Browse files Browse the repository at this point in the history
  • Loading branch information
itssharmasandeep authored Sep 15, 2023
1 parent 17f4dca commit 46309ef
Show file tree
Hide file tree
Showing 3 changed files with 142 additions and 16 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
defaultSecondaryNodeMetricCategories
} from '../../../shared/dashboard/widgets/topology/metric/node-metric-category';
import { MetricAggregationType } from '../../../shared/graphql/model/metrics/metric-aggregation';
import { TopologyEdgeFilterConfig } from '../../../shared/dashboard/data/graphql/topology/topology-data-source.model';

export const getTopologyJson = (options?: TopologyJsonOptions): ModelJson => ({
type: 'topology-widget',
Expand All @@ -18,6 +19,7 @@ export const getTopologyJson = (options?: TopologyJsonOptions): ModelJson => ({
type: 'topology-data-source',
entity: 'SERVICE',
'downstream-entities': ['SERVICE', 'BACKEND'],
'edge-filter-config': options?.edgeFilterConfig,
'edge-metrics': {
type: 'topology-metrics',
primary: {
Expand Down Expand Up @@ -211,4 +213,5 @@ export const applicationFlowDefaultJson: DashboardDefaultConfiguration = {

export interface TopologyJsonOptions {
showBrush?: boolean;
edgeFilterConfig?: TopologyEdgeFilterConfig;
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,21 @@ import { TopologyMetricCategoryModel } from './metrics/topology-metric-category.
import { TopologyMetricWithCategoryModel } from './metrics/topology-metric-with-category.model';
import { TopologyMetricsModel } from './metrics/topology-metrics.model';
import { TopologyDataSourceModel } from './topology-data-source.model';
import { GraphQlFieldFilter } from '../../../../graphql/model/schema/filter/field/graphql-field-filter';
import { GraphQlOperatorType } from '../../../../graphql/model/schema/filter/graphql-filter';

describe('topology data source model', () => {
const testTimeRange = { startTime: new Date(1568907645141), endTime: new Date(1568911245141) };
let model!: TopologyDataSourceModel;
let totalQueries: number = 0;
let lastEmittedQuery: unknown;
let lastEmittedQueryRequestOption: GraphQlRequestOptions | undefined;

const filters = [
new GraphQlFieldFilter('service_id', GraphQlOperatorType.Equals, 'test-id'),
new GraphQlFieldFilter('backend_id', GraphQlOperatorType.Equals, 'test-backend-id')
];

const createCategoryModel = (
name: string,
minValue: number,
Expand Down Expand Up @@ -81,17 +89,24 @@ describe('topology data source model', () => {
model.entityType = ObservabilityEntityType.Service;
model.nodeMetricsModel = createTopologyMetricsModel('numCalls', MetricAggregationType.Average);
model.edgeMetricsModel = createTopologyMetricsModel('duration', MetricAggregationType.Average);
model.edgeFilterConfig = { entityType: ObservabilityEntityType.Backend, fields: ['backend_id'] };

model.api = mockApi as ModelApi;
model.query$.subscribe(query => {
lastEmittedQuery = query.buildRequest([]);
if (totalQueries === 0) {
// Without filters
lastEmittedQuery = query.buildRequest([]);
} else {
// With filters
lastEmittedQuery = query.buildRequest(filters);
}
lastEmittedQueryRequestOption = query.requestOptions;
totalQueries += 1;
});
model.getData();
});

test('builds expected request', () => {
model.getData();
test('builds expected request without filters', () => {
expect(lastEmittedQuery).toEqual({
requestType: ENTITY_TOPOLOGY_GQL_REQUEST,
rootNodeType: ObservabilityEntityType.Service,
Expand All @@ -102,6 +117,7 @@ describe('topology data source model', () => {
]
},
rootNodeFilters: [],
edgeFilters: [],
rootNodeLimit: 100,
timeRange: new GraphQlTimeRange(testTimeRange.startTime, testTimeRange.endTime),
downstreamNodeSpecifications: new Map<ObservabilityEntityType, TopologyNodeSpecification>([
Expand Down Expand Up @@ -147,4 +163,38 @@ describe('topology data source model', () => {
isolated: true
});
});

test('builds expected request with filters', () => {
expect(lastEmittedQuery).toEqual({
requestType: ENTITY_TOPOLOGY_GQL_REQUEST,
rootNodeType: ObservabilityEntityType.Service,
rootNodeSpecification: {
titleSpecification: expect.objectContaining({ name: 'name' }),
metricSpecifications: [
expect.objectContaining({ metric: 'numCalls', aggregation: MetricAggregationType.Average })
]
},
rootNodeFilters: [filters[0]],
edgeFilters: [filters[1]],
rootNodeLimit: 100,
timeRange: new GraphQlTimeRange(testTimeRange.startTime, testTimeRange.endTime),
downstreamNodeSpecifications: new Map<ObservabilityEntityType, TopologyNodeSpecification>([
[
ObservabilityEntityType.Backend,
{
titleSpecification: expect.objectContaining({ name: 'name' }),
metricSpecifications: [
expect.objectContaining({ metric: 'numCalls', aggregation: MetricAggregationType.Average })
]
}
]
]),
upstreamNodeSpecifications: new Map(),
edgeSpecification: {
metricSpecifications: [
expect.objectContaining({ metric: 'duration', aggregation: MetricAggregationType.Average })
]
}
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,8 @@ import {
Model,
ModelModelPropertyTypeInstance,
ModelProperty,
ModelPropertyType
ModelPropertyType,
PLAIN_OBJECT_PROPERTY
} from '@hypertrace/hyperdash';
import { uniq } from 'lodash-es';
import { Observable } from 'rxjs';
Expand All @@ -22,6 +23,9 @@ import {
} from '../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service';
import { GraphQlDataSourceModel } from '../graphql-data-source.model';
import { TopologyMetricsData, TopologyMetricsModel } from './metrics/topology-metrics.model';
import { GraphQlFieldFilter } from '../../../../graphql/model/schema/filter/field/graphql-field-filter';
import { AttributeExpression } from '../../../../graphql/model/attribute/attribute-expression';
import { GraphQlFilter } from '../../../../graphql/model/schema/filter/graphql-filter';

@Model({
type: 'topology-data-source'
Expand Down Expand Up @@ -79,6 +83,14 @@ export class TopologyDataSourceModel extends GraphQlDataSourceModel<TopologyData
})
public edgeMetricsModel!: TopologyMetricsModel;

@ModelProperty({
key: 'edge-filter-config',
type: {
key: PLAIN_OBJECT_PROPERTY.type
}
})
public edgeFilterConfig?: TopologyEdgeFilterConfig;

private readonly specBuilder: SpecificationBuilder = new SpecificationBuilder();
public readonly requestOptions: GraphQlRequestOptions = {
cacheability: GraphQlRequestCacheability.Cacheable,
Expand All @@ -90,20 +102,25 @@ export class TopologyDataSourceModel extends GraphQlDataSourceModel<TopologyData
metricSpecifications: this.getAllMetricSpecifications(this.edgeMetricsModel)
};

return this.query<EntityTopologyGraphQlQueryHandlerService>(
filters => ({
return this.query<EntityTopologyGraphQlQueryHandlerService>(filters => {
const topologyFilters = this.getTopologyFilters(filters);
const edgeFilterEntityType = this.edgeFilterConfig?.entityType;
const requiredEdgeEntityTypes =
topologyFilters.edges.length > 0 && edgeFilterEntityType !== undefined ? [edgeFilterEntityType] : undefined;

return {
requestType: ENTITY_TOPOLOGY_GQL_REQUEST,
rootNodeType: this.entityType,
rootNodeLimit: 100,
rootNodeSpecification: rootEntitySpec,
rootNodeFilters: filters,
rootNodeFilters: topologyFilters.nodes,
edgeSpecification: edgeSpec,
upstreamNodeSpecifications: this.buildUpstreamSpecifications(),
downstreamNodeSpecifications: this.buildDownstreamSpecifications(),
edgeFilters: topologyFilters.edges,
upstreamNodeSpecifications: this.buildUpstreamSpecifications(requiredEdgeEntityTypes),
downstreamNodeSpecifications: this.buildDownstreamSpecifications(requiredEdgeEntityTypes),
timeRange: this.getTimeRangeOrThrow()
}),
this.requestOptions
).pipe(
};
}, this.requestOptions).pipe(
map(nodes => ({
nodes: nodes,
nodeSpecification: rootEntitySpec,
Expand All @@ -119,14 +136,60 @@ export class TopologyDataSourceModel extends GraphQlDataSourceModel<TopologyData
);
}

private buildDownstreamSpecifications(): Map<ObservabilityEntityType, TopologyNodeSpecification> {
private getTopologyFilters(filters: GraphQlFilter[]): TopologyFilters {
const edgeFilterFields = this.edgeFilterConfig?.fields ?? [];
const edgeFilters: GraphQlFilter[] = [];
const nodeFilters: GraphQlFilter[] = [];

const isFieldFilter = (gqlFilter: GraphQlFilter): gqlFilter is GraphQlFieldFilter =>
'keyOrExpression' in gqlFilter && 'operator' in gqlFilter && 'value' in gqlFilter;

filters.forEach(gqlFilter => {
// Edge filter only supported for `GraphQlFieldFilter` for now
if (
isFieldFilter(gqlFilter) &&
edgeFilterFields.includes(this.getFieldFromExpression(gqlFilter.keyOrExpression))
) {
edgeFilters.push(gqlFilter);
} else {
nodeFilters.push(gqlFilter);
}
});

return {
nodes: nodeFilters,
edges: edgeFilters
};
}

private getFieldFromExpression(keyOrExpression: string | AttributeExpression): string {
return typeof keyOrExpression === 'string' ? keyOrExpression : keyOrExpression.key;
}

/**
* @param requiredEntityTypes If given, the function will return all the specs only for given required types
*/
private buildDownstreamSpecifications(
requiredEntityTypes?: string[]
): Map<ObservabilityEntityType, TopologyNodeSpecification> {
return new Map(
this.defaultedEntityTypeArray(this.downstreamEntityTypes).map(type => [type, this.buildEntitySpec()])
this.defaultedEntityTypeArray(this.downstreamEntityTypes)
.filter(entityType => (requiredEntityTypes === undefined ? true : requiredEntityTypes.includes(entityType)))
.map(type => [type, this.buildEntitySpec()])
);
}

private buildUpstreamSpecifications(): Map<ObservabilityEntityType, TopologyNodeSpecification> {
return new Map(this.defaultedEntityTypeArray(this.upstreamEntityTypes).map(type => [type, this.buildEntitySpec()]));
/**
* @param requiredEntityTypes If given, the function will return all the specs only for given required types
*/
private buildUpstreamSpecifications(
requiredEntityTypes?: string[]
): Map<ObservabilityEntityType, TopologyNodeSpecification> {
return new Map(
this.defaultedEntityTypeArray(this.upstreamEntityTypes)
.filter(entityType => (requiredEntityTypes === undefined ? true : requiredEntityTypes.includes(entityType)))
.map(type => [type, this.buildEntitySpec()])
);
}

private buildEntitySpec(): TopologyNodeSpecification {
Expand Down Expand Up @@ -160,3 +223,13 @@ export interface TopologyData {
nodeMetrics: TopologyMetricsData;
edgeMetrics: TopologyMetricsData;
}

export interface TopologyEdgeFilterConfig {
entityType: string;
fields: string[];
}

interface TopologyFilters {
nodes: GraphQlFilter[];
edges: GraphQlFilter[];
}

0 comments on commit 46309ef

Please sign in to comment.