From b1282545de533dc95ed2df7f25a6e1335bd653ef Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Thu, 15 Jul 2021 15:28:48 -0700 Subject: [PATCH 1/5] feat: adding topology custom metric model and service --- .../metrics/topology-metric-category.model.ts | 74 +++++++++++++++++ .../topology-metric-with-category.model.ts | 58 ++++++++++++++ .../metrics/topology-metrics.model.ts | 48 +++++++++++ .../topology/metric/edge-metric-category.ts | 80 +++++++++++++++++++ .../topology/metric/node-metric-category.ts | 80 +++++++++++++++++++ ...gy-data-source-model-properties.service.ts | 45 +++++++++++ 6 files changed, 385 insertions(+) create mode 100644 projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metrics.model.ts create mode 100644 projects/observability/src/shared/dashboard/widgets/topology/metric/edge-metric-category.ts create mode 100644 projects/observability/src/shared/dashboard/widgets/topology/metric/node-metric-category.ts create mode 100644 projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.ts diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.ts new file mode 100644 index 000000000..d844453d0 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.ts @@ -0,0 +1,74 @@ +import { Color } from '@hypertrace/common'; +import { BOOLEAN_PROPERTY, Model, ModelProperty, NUMBER_PROPERTY, STRING_PROPERTY } from '@hypertrace/hyperdash'; +import { kebabCase, uniqueId } from 'lodash-es'; + +@Model({ + type: 'topology-metric-category' +}) +export class TopologyMetricCategoryModel implements TopologyMetricCategoryData { + @ModelProperty({ + key: 'name', + required: true, + type: STRING_PROPERTY.type + }) + public name!: string; + + @ModelProperty({ + key: 'minValue', + required: true, + type: NUMBER_PROPERTY.type + }) + public minValue!: number; + + @ModelProperty({ + key: 'maxValue', + required: false, + type: NUMBER_PROPERTY.type + }) + public maxValue?: number; + + @ModelProperty({ + key: 'fillColor', + required: true, + type: STRING_PROPERTY.type + }) + public fillColor!: Color; + + @ModelProperty({ + key: 'strokeColor', + required: true, + type: STRING_PROPERTY.type + }) + public strokeColor!: Color; + + @ModelProperty({ + key: 'focusColor', + required: true, + type: STRING_PROPERTY.type + }) + public focusColor!: Color; + + @ModelProperty({ + key: 'highestPrecedence', + required: false, + type: BOOLEAN_PROPERTY.type + }) + public highestPrecedence?: boolean = false; + + private readonly id: string = uniqueId(); + + public getCategoryClassName(): string { + return `${kebabCase(this.name)}-${this.id}`; + } +} + +export interface TopologyMetricCategoryData { + name: string; + minValue: number; + maxValue?: number; + fillColor: string; + strokeColor: string; + focusColor: string; + highestPrecedence?: boolean; + getCategoryClassName(): string; +} diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.ts new file mode 100644 index 000000000..644ff2afe --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.ts @@ -0,0 +1,58 @@ +import { Dictionary } from '@hypertrace/common'; +import { MetricAggregation } from '@hypertrace/distributed-tracing'; +import { + ARRAY_PROPERTY, + Model, + ModelModelPropertyTypeInstance, + ModelProperty, + ModelPropertyType +} from '@hypertrace/hyperdash'; +import { MetricAggregationSpecificationModel } from '../../specifiers/metric-aggregation-specification.model'; +import { MetricAggregationSpecification } from './../../../../../graphql/model/schema/specifications/metric-aggregation-specification'; +import { TopologyMetricCategoryData, TopologyMetricCategoryModel } from './topology-metric-category.model'; + +@Model({ + type: 'topology-metric-with-category' +}) +export class TopologyMetricWithCategoryModel implements TopologyMetricWithCategoryData { + @ModelProperty({ + key: 'specification', + required: true, + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ModelPropertyType.TYPE, + defaultModelClass: MetricAggregationSpecificationModel + } as ModelModelPropertyTypeInstance + }) + public specification!: MetricAggregationSpecificationModel; + + @ModelProperty({ + key: 'categories', + required: false, + type: ARRAY_PROPERTY.type + }) + public categories: TopologyMetricCategoryModel[] = []; + + public extractAndGetDataCategoryForMetric(data: Dictionary): TopologyMetricCategoryData | undefined { + const aggregation = this.extractDataForMetric(data); + + return aggregation !== undefined ? this.getDataCategoryForMetric(aggregation.value) : undefined; + } + + public extractDataForMetric(data: Dictionary): MetricAggregation | undefined { + return data[this.specification.resultAlias()] as MetricAggregation | undefined; + } + + private getDataCategoryForMetric(value: number): TopologyMetricCategoryData | undefined { + return this.categories.find( + category => value >= category.minValue && (category.maxValue !== undefined ? value < category.maxValue : true) + ); + } +} + +export interface TopologyMetricWithCategoryData { + specification: MetricAggregationSpecification; + categories: TopologyMetricCategoryData[]; + extractDataForMetric(data: Dictionary): MetricAggregation | undefined; + extractAndGetDataCategoryForMetric(data: Dictionary): TopologyMetricCategoryData | undefined; +} diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metrics.model.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metrics.model.ts new file mode 100644 index 000000000..934912632 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metrics.model.ts @@ -0,0 +1,48 @@ +import { + ARRAY_PROPERTY, + Model, + ModelModelPropertyTypeInstance, + ModelProperty, + ModelPropertyType +} from '@hypertrace/hyperdash'; +import { TopologyMetricWithCategoryData, TopologyMetricWithCategoryModel } from './topology-metric-with-category.model'; + +@Model({ + type: 'topology-metrics' +}) +export class TopologyMetricsModel implements TopologyMetricsData { + @ModelProperty({ + key: 'primary', + required: true, + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ModelPropertyType.TYPE, + defaultModelClass: TopologyMetricWithCategoryModel + } as ModelModelPropertyTypeInstance + }) + public primary!: TopologyMetricWithCategoryModel; + + @ModelProperty({ + key: 'secondary', + required: false, + // tslint:disable-next-line: no-object-literal-type-assertion + type: { + key: ModelPropertyType.TYPE, + defaultModelClass: TopologyMetricWithCategoryModel + } as ModelModelPropertyTypeInstance + }) + public secondary?: TopologyMetricWithCategoryModel; + + @ModelProperty({ + key: 'others', + required: false, + type: ARRAY_PROPERTY.type + }) + public others?: TopologyMetricWithCategoryModel[]; +} + +export interface TopologyMetricsData { + primary: TopologyMetricWithCategoryData; + secondary?: TopologyMetricWithCategoryData; + others?: TopologyMetricWithCategoryData[]; +} diff --git a/projects/observability/src/shared/dashboard/widgets/topology/metric/edge-metric-category.ts b/projects/observability/src/shared/dashboard/widgets/topology/metric/edge-metric-category.ts new file mode 100644 index 000000000..be98800a0 --- /dev/null +++ b/projects/observability/src/shared/dashboard/widgets/topology/metric/edge-metric-category.ts @@ -0,0 +1,80 @@ +import { Color } from '@hypertrace/common'; +import { TopologyMetricCategoryData } from '../../../data/graphql/topology/metrics/topology-metric-category.model'; + +const enum PrimaryEdgeMetricCategoryValueType { + LessThan20 = 'less-than-20', + From20To100 = 'from-20-to-100', + From100To500 = 'from-100-to-500', + From500To1000 = 'from-500-to-1000', + GreaterThanOrEqualTo1000 = 'greater-than-or-equal-to-1000', + NotSpecified = 'not-specified' +} + +const enum SecondaryEdgeMetricCategoryValueType { + LessThan5 = 'less-than-5', + GreaterThanOrEqualTo5 = 'greater-than-or-equal-to-5', + NotSpecified = 'not-specified' +} + +export const defaultPrimaryEdgeMetricCategories: Omit[] = [ + { + name: PrimaryEdgeMetricCategoryValueType.LessThan20, + minValue: 0, + maxValue: 20, + fillColor: Color.BlueGray1, + strokeColor: Color.BlueGray1, + focusColor: Color.BlueGray1 + }, + { + name: PrimaryEdgeMetricCategoryValueType.From20To100, + minValue: 20, + maxValue: 100, + fillColor: Color.BlueGray2, + strokeColor: Color.BlueGray2, + focusColor: Color.BlueGray2 + }, + { + name: PrimaryEdgeMetricCategoryValueType.From100To500, + minValue: 100, + maxValue: 500, + fillColor: Color.BlueGray3, + strokeColor: Color.BlueGray3, + focusColor: Color.BlueGray3 + }, + { + name: PrimaryEdgeMetricCategoryValueType.From500To1000, + minValue: 500, + maxValue: 1000, + fillColor: Color.BlueGray4, + strokeColor: Color.BlueGray4, + focusColor: Color.BlueGray4 + }, + { + name: PrimaryEdgeMetricCategoryValueType.GreaterThanOrEqualTo1000, + minValue: 1000, + maxValue: undefined, + fillColor: Color.BlueGray4, + strokeColor: Color.BlueGray4, + focusColor: Color.BlueGray4 + } +]; + +export const defaultSecondaryEdgeMetricCategories: Omit[] = [ + { + name: SecondaryEdgeMetricCategoryValueType.LessThan5, + minValue: 0, + maxValue: 5, + fillColor: Color.Gray2, + strokeColor: Color.Gray2, + focusColor: Color.Gray2 + }, + { + name: SecondaryEdgeMetricCategoryValueType.GreaterThanOrEqualTo5, + minValue: 5, + maxValue: undefined, + fillColor: Color.Red5, + strokeColor: Color.Red5, + focusColor: Color.Red5, + highestPrecedence: true + } +]; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/metric/node-metric-category.ts b/projects/observability/src/shared/dashboard/widgets/topology/metric/node-metric-category.ts new file mode 100644 index 000000000..688f9a313 --- /dev/null +++ b/projects/observability/src/shared/dashboard/widgets/topology/metric/node-metric-category.ts @@ -0,0 +1,80 @@ +import { Color } from '@hypertrace/common'; +import { TopologyMetricCategoryData } from '../../../data/graphql/topology/metrics/topology-metric-category.model'; + +const enum PrimaryNodeMetricCategoryValueType { + LessThan20 = 'less-than-20', + From20To100 = 'from-20-to-100', + From100To500 = 'from-100-to-500', + From500To1000 = 'from-500-to-1000', + GreaterThanOrEqualTo1000 = 'greater-than-or-equal-to-1000', + NotSpecified = 'not-specified' +} + +const enum SecondaryNodeMetricCategoryValueType { + LessThan5 = 'less-than-5', + GreaterThanOrEqualTo5 = 'greater-than-or-equal-to-5', + NotSpecified = 'not-specified' +} + +export const defaultPrimaryNodeMetricCategories: Omit[] = [ + { + name: PrimaryNodeMetricCategoryValueType.LessThan20, + minValue: 0, + maxValue: 20, + fillColor: Color.BlueGray1, + strokeColor: Color.BlueGray1, + focusColor: Color.Blue4 + }, + { + name: PrimaryNodeMetricCategoryValueType.From20To100, + minValue: 20, + maxValue: 100, + fillColor: Color.BlueGray2, + strokeColor: Color.BlueGray2, + focusColor: Color.Blue4 + }, + { + name: PrimaryNodeMetricCategoryValueType.From100To500, + minValue: 100, + maxValue: 500, + fillColor: Color.BlueGray3, + strokeColor: Color.BlueGray3, + focusColor: Color.Blue4 + }, + { + name: PrimaryNodeMetricCategoryValueType.From500To1000, + minValue: 500, + maxValue: 1000, + fillColor: Color.BlueGray4, + strokeColor: Color.BlueGray4, + focusColor: Color.Blue4 + }, + { + name: PrimaryNodeMetricCategoryValueType.GreaterThanOrEqualTo1000, + minValue: 1000, + maxValue: undefined, + fillColor: Color.BlueGray4, + strokeColor: Color.BlueGray4, + focusColor: Color.Blue4 + } +]; + +export const defaultSecondaryNodeMetricCategories: Omit[] = [ + { + name: SecondaryNodeMetricCategoryValueType.LessThan5, + minValue: 0, + maxValue: 5, + fillColor: Color.Gray2, + strokeColor: Color.Gray2, + focusColor: Color.Blue4 + }, + { + name: SecondaryNodeMetricCategoryValueType.GreaterThanOrEqualTo5, + minValue: 5, + maxValue: undefined, + fillColor: Color.Red1, + strokeColor: Color.Red5, + focusColor: Color.Red5, + highestPrecedence: true + } +]; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.ts new file mode 100644 index 000000000..941652b03 --- /dev/null +++ b/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.ts @@ -0,0 +1,45 @@ +import { Injectable } from '@angular/core'; +import { TopologyMetricCategoryData } from '../../data/graphql/topology/metrics/topology-metric-category.model'; +import { TopologyMetricWithCategoryData } from '../../data/graphql/topology/metrics/topology-metric-with-category.model'; +import { TopologyMetricsData } from '../../data/graphql/topology/metrics/topology-metrics.model'; + +@Injectable() +export class TopologyDataSourceModelPropertiesService { + private nodeMetrics?: TopologyMetricsData; + private edgeMetrics?: TopologyMetricsData; + + public setModelProperties(nodeMetrics: TopologyMetricsData, edgeMetrics: TopologyMetricsData): void { + this.nodeMetrics = nodeMetrics; + this.edgeMetrics = edgeMetrics; + } + + public getPrimaryNodeMetric(): TopologyMetricWithCategoryData | undefined { + return this.nodeMetrics?.primary; + } + + public getSecondaryNodeMetric(): TopologyMetricWithCategoryData | undefined { + return this.nodeMetrics?.secondary; + } + + public getPrimaryEdgeMetric(): TopologyMetricWithCategoryData | undefined { + return this.edgeMetrics?.primary; + } + + public getSecondaryEdgeMetric(): TopologyMetricWithCategoryData | undefined { + return this.edgeMetrics?.secondary; + } + + public getAllNodeCategories(): TopologyMetricCategoryData[] { + return [this.nodeMetrics?.primary, this.nodeMetrics?.secondary, this.nodeMetrics?.others] + .flat() + .map(categoryData => categoryData?.categories ?? []) + .flat(); + } + + public getAllEdgeCategories(): TopologyMetricCategoryData[] { + return [this.edgeMetrics?.primary, this.edgeMetrics?.secondary, this.edgeMetrics?.others] + .flat() + .map(categoryData => categoryData?.categories ?? []) + .flat(); + } +} From bf79eff38240e70f8043326e3b9879f837d0531c Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Thu, 15 Jul 2021 15:30:59 -0700 Subject: [PATCH 2/5] refactor: migrating to new topology custom metric --- .../topology/application-flow.component.ts | 248 +++++++++++++----- ...bservability-graphql-data-source.module.ts | 6 + .../topology/topology-data-source.model.ts | 50 ++-- .../curved/entity-edge-curve-renderer.scss | 62 +---- .../entity-edge-curve-renderer.service.ts | 121 ++++----- .../topology/metric/metric-category.ts | 71 ----- .../api-node-box-renderer.service.ts | 2 +- .../backend-node-box-renderer.service.ts | 2 +- .../node/box/entity-node-box-renderer.scss | 44 ---- .../box/entity-node-box-renderer.service.ts | 83 ++++-- .../service-node-box-renderer.service.ts | 2 +- ...topology-widget-renderer.component.test.ts | 20 +- .../topology-widget-renderer.component.ts | 15 +- 13 files changed, 373 insertions(+), 353 deletions(-) delete mode 100644 projects/observability/src/shared/dashboard/widgets/topology/metric/metric-category.ts diff --git a/projects/observability/src/pages/apis/topology/application-flow.component.ts b/projects/observability/src/pages/apis/topology/application-flow.component.ts index d0344d1d6..938ec8bd8 100644 --- a/projects/observability/src/pages/apis/topology/application-flow.component.ts +++ b/projects/observability/src/pages/apis/topology/application-flow.component.ts @@ -1,6 +1,14 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; import { MetricAggregationType } from '@hypertrace/distributed-tracing'; import { ModelJson } from '@hypertrace/hyperdash'; +import { + defaultPrimaryEdgeMetricCategories, + defaultSecondaryEdgeMetricCategories +} from './../../../shared/dashboard/widgets/topology/metric/edge-metric-category'; +import { + defaultPrimaryNodeMetricCategories, + defaultSecondaryNodeMetricCategories +} from './../../../shared/dashboard/widgets/topology/metric/node-metric-category'; @Component({ changeDetection: ChangeDetectionStrategy.OnPush, @@ -26,78 +34,182 @@ export class ApplicationFlowComponent { type: 'topology-data-source', entity: 'SERVICE', 'downstream-entities': ['SERVICE', 'BACKEND'], - 'node-metrics': [ - { - type: 'percentile-latency-metric-aggregation', - 'display-name': 'P99 Latency' + 'edge-metrics': { + type: 'topology-metrics', + primary: { + type: 'topology-metric-with-category', + specification: { + type: 'percentile-latency-metric-aggregation', + 'display-name': 'P99 Latency' + }, + categories: [ + { + type: 'topology-metric-category', + ...defaultPrimaryEdgeMetricCategories[0] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryEdgeMetricCategories[1] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryEdgeMetricCategories[2] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryEdgeMetricCategories[3] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryEdgeMetricCategories[4] + } + ] }, - { - type: 'metric-aggregation', - metric: 'duration', - aggregation: MetricAggregationType.P50, - 'display-name': 'P50 Latency' + secondary: { + type: 'topology-metric-with-category', + specification: { + type: 'error-percentage-metric-aggregation', + aggregation: MetricAggregationType.Average, + 'display-name': 'Error %' + }, + categories: [ + { + type: 'topology-metric-category', + ...defaultSecondaryEdgeMetricCategories[0] + }, + { + type: 'topology-metric-category', + ...defaultSecondaryEdgeMetricCategories[1] + } + ] }, - { - type: 'error-percentage-metric-aggregation', - aggregation: MetricAggregationType.Average, - 'display-name': 'Error %' + others: [ + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'duration', + aggregation: MetricAggregationType.P50, + 'display-name': 'P50 Latency' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'errorCount', + aggregation: MetricAggregationType.Sum, + 'display-name': 'Error Count' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'numCalls', + aggregation: MetricAggregationType.AvgrateSecond, + 'display-name': 'Call Rate/sec' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'numCalls', + aggregation: MetricAggregationType.Sum, + 'display-name': 'Call Count' + } + } + ] + }, + 'node-metrics': { + type: 'topology-metrics', + primary: { + type: 'topology-metric-with-category', + specification: { + type: 'percentile-latency-metric-aggregation', + 'display-name': 'P99 Latency' + }, + categories: [ + { + type: 'topology-metric-category', + ...defaultPrimaryNodeMetricCategories[0] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryNodeMetricCategories[1] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryNodeMetricCategories[2] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryNodeMetricCategories[3] + }, + { + type: 'topology-metric-category', + ...defaultPrimaryNodeMetricCategories[4] + } + ] }, - - { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: MetricAggregationType.Sum, - 'display-name': 'Error Count' - }, - { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: MetricAggregationType.AvgrateSecond, - 'display-name': 'Call Rate/sec' - }, - { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: MetricAggregationType.Sum, - 'display-name': 'Call Count' - } - ], - 'edge-metrics': [ - { - type: 'percentile-latency-metric-aggregation', - 'display-name': 'P99 Latency' - }, - { - type: 'metric-aggregation', - metric: 'duration', - aggregation: MetricAggregationType.P50, - 'display-name': 'P50 Latency' - }, - { - type: 'error-percentage-metric-aggregation', - aggregation: MetricAggregationType.Average, - 'display-name': 'Error %' - }, - - { - type: 'metric-aggregation', - metric: 'errorCount', - aggregation: MetricAggregationType.Sum, - 'display-name': 'Error Count' - }, - { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: MetricAggregationType.AvgrateSecond, - 'display-name': 'Call Rate/sec' + secondary: { + type: 'topology-metric-with-category', + specification: { + type: 'error-percentage-metric-aggregation', + aggregation: MetricAggregationType.Average, + 'display-name': 'Error %' + }, + categories: [ + { + type: 'topology-metric-category', + ...defaultSecondaryNodeMetricCategories[0] + }, + { + type: 'topology-metric-category', + ...defaultSecondaryNodeMetricCategories[1] + } + ] }, - { - type: 'metric-aggregation', - metric: 'numCalls', - aggregation: MetricAggregationType.Sum, - 'display-name': 'Call Count' - } - ] + others: [ + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'duration', + aggregation: MetricAggregationType.P50, + 'display-name': 'P50 Latency' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'errorCount', + aggregation: MetricAggregationType.Sum, + 'display-name': 'Error Count' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'numCalls', + aggregation: MetricAggregationType.AvgrateSecond, + 'display-name': 'Call Rate/sec' + } + }, + { + type: 'topology-metric-with-category', + specification: { + type: 'metric-aggregation', + metric: 'numCalls', + aggregation: MetricAggregationType.Sum, + 'display-name': 'Call Count' + } + } + ] + } } } ] diff --git a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts index e0e2f9690..95faf3401 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/observability-graphql-data-source.module.ts @@ -28,6 +28,9 @@ import { PercentileLatencyAggregationSpecificationModel } from './specifiers/per import { EntityTableDataSourceModel } from './table/entity/entity-table-data-source.model'; import { ExploreTableDataSourceModel } from './table/explore/explore-table-data-source.model'; import { InteractionsTableDataSourceModel } from './table/interactions/interactions-table-data-source.model'; +import { TopologyMetricCategoryModel } from './topology/metrics/topology-metric-category.model'; +import { TopologyMetricWithCategoryModel } from './topology/metrics/topology-metric-with-category.model'; +import { TopologyMetricsModel } from './topology/metrics/topology-metrics.model'; import { TopologyDataSourceModel } from './topology/topology-data-source.model'; import { TraceMetricAggregationDataSourceModel } from './trace/aggregation/trace-metric-aggregation-data-source.model'; import { TraceDonutDataSourceModel } from './trace/donut/trace-donut-data-source.model'; @@ -53,6 +56,9 @@ import { ApiTraceWaterfallDataSourceModel } from './waterfall/api-trace-waterfal EntityMetricTimeseriesDataSourceModel, EntityMetricAggregationDataSourceModel, TopologyDataSourceModel, + TopologyMetricsModel, + TopologyMetricWithCategoryModel, + TopologyMetricCategoryModel, EntityTableDataSourceModel, InteractionsTableDataSourceModel, EntityAttributeDataSourceModel, diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.ts index 2cf33f001..faefa584b 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.ts @@ -1,7 +1,13 @@ import { ArrayPropertyTypeInstance, EnumPropertyTypeInstance, ENUM_TYPE } from '@hypertrace/dashboards'; import { GraphQlDataSourceModel, SpecificationBuilder } from '@hypertrace/distributed-tracing'; import { GraphQlRequestCacheability, GraphQlRequestOptions } from '@hypertrace/graphql-client'; -import { ARRAY_PROPERTY, Model, ModelProperty, ModelPropertyType } from '@hypertrace/hyperdash'; +import { + ARRAY_PROPERTY, + Model, + ModelModelPropertyTypeInstance, + ModelProperty, + ModelPropertyType +} from '@hypertrace/hyperdash'; import { uniq } from 'lodash-es'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; @@ -14,7 +20,7 @@ import { TopologyEdgeSpecification, TopologyNodeSpecification } from '../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; -import { EntityMetricAggregationDataSourceModel } from '../entity/aggregation/entity-metric-aggregation-data-source.model'; +import { TopologyMetricsData, TopologyMetricsModel } from './metrics/topology-metrics.model'; @Model({ type: 'topology-data-source' @@ -61,27 +67,21 @@ export class TopologyDataSourceModel extends GraphQlDataSourceModel { const rootEntitySpec = this.buildEntitySpec(); const edgeSpec = { - metricSpecifications: this.edgeMetricSpecifications + metricSpecifications: this.getAllMetricSpecifications(this.edgeMetricsModel) }; return this.query( @@ -116,7 +116,9 @@ export class TopologyDataSourceModel extends GraphQlDataSourceModel _.specification) : []) + ]; + } } export interface TopologyData { @@ -151,4 +161,6 @@ export interface TopologyData { nodeTypes: ObservabilityEntityType[]; nodeSpecification: TopologyNodeSpecification; edgeSpecification: TopologyEdgeSpecification; + nodeMetrics: TopologyMetricsData; + edgeMetrics: TopologyMetricsData; } diff --git a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.scss b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.scss index cb316fac5..855f71e51 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.scss +++ b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.scss @@ -18,7 +18,7 @@ stroke-width: 2; stroke-linecap: round; stroke-linejoin: round; - stroke: currentColor; + stroke: $gray-3; fill: none; @include chart-small-regular; @@ -34,7 +34,6 @@ .edge-path { stroke-dasharray: 2, 2; stroke-width: 1px; - stroke: $gray-3; } &.background { @@ -50,7 +49,6 @@ &.focused { .entity-edge-metric-bubble { @include show; - fill: currentColor; } .entity-edge-metric-value { @@ -65,35 +63,6 @@ .edge-path { stroke-dasharray: none; stroke-width: 1.5px; - stroke: currentColor; - } - - &.less-than-20-category { - color: $blue-gray-1; - } - - &.from-20-to-100-category { - color: $blue-gray-2; - } - - &.from-100-to-500-category { - color: $blue-gray-3; - } - - &.from-500-to-1000-category { - color: $blue-gray-4; - } - - &.greater-than-or-equal-to-1000-category { - color: $blue-gray-5; - } - - &.not-specified-category { - color: lightgray; - } - - &.greater-than-or-equal-to-5-error-category { - color: $red-5; } } } @@ -101,35 +70,6 @@ .entity-edge-arrow { stroke-linecap: round; stroke-linejoin: round; - fill: currentColor; - - &.less-than-20-category { - color: $blue-gray-1; - } - - &.from-20-to-100-category { - color: $blue-gray-2; - } - - &.from-100-to-500-category { - color: $blue-gray-3; - } - - &.from-500-to-1000-category { - color: $blue-gray-4; - } - - &.greater-than-or-equal-to-1000-category { - color: $blue-gray-5; - } - - &.not-specified-category { - color: lightgray; - } - - &.greater-than-or-equal-to-5-error-category { - color: $red-5; - } } } } diff --git a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts index a60671e7f..8230c9d4b 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts @@ -1,11 +1,8 @@ import { Injectable, Renderer2 } from '@angular/core'; -import { DomElementMeasurerService, NumericFormatter, selector } from '@hypertrace/common'; +import { Color, DomElementMeasurerService, NumericFormatter, selector } from '@hypertrace/common'; +import { MetricAggregation } from '@hypertrace/distributed-tracing'; import { select, Selection } from 'd3-selection'; import { linkHorizontal } from 'd3-shape'; -import { - ErrorPercentageMetricAggregation, - ErrorPercentageMetricValueCategory -} from '../../../../..//graphql/model/schema/specifications/error-percentage-aggregation-specification'; import { TopologyEdgePositionInformation, TopologyEdgeRenderDelegate @@ -19,19 +16,12 @@ import { import { D3UtilService } from '../../../../../components/utils/d3/d3-util.service'; import { SvgUtilService } from '../../../../../components/utils/svg/svg-util.service'; import { MetricAggregationSpecification } from '../../../../../graphql/model/schema/specifications/metric-aggregation-specification'; -import { PercentileLatencyMetricAggregation } from '../../../../../graphql/model/schema/specifications/percentile-latency-aggregation-specification'; import { EntityEdge } from '../../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; -import { - allErrorPercentageMetricCategories, - allLatencyMetricCategories, - getErrorPercentageCategoryClass, - getErrorPercentageMetric, - getLatencyCategoryClass, - getLatencyMetric -} from '../../metric/metric-category'; +import { TopologyMetricCategoryData } from '../../../../data/graphql/topology/metrics/topology-metric-category.model'; +import { TopologyDataSourceModelPropertiesService } from '../../topology-data-source-model-properties.service'; import { VisibilityUpdater } from '../../visibility-updater'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegate { private readonly edgeClass: string = 'entity-edge'; private readonly edgeLineClass: string = 'entity-edge-line'; @@ -44,7 +34,8 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat public constructor( private readonly domElementMeasurerService: DomElementMeasurerService, private readonly svgUtils: SvgUtilService, - private readonly d3Utils: D3UtilService + private readonly d3Utils: D3UtilService, + private readonly topologyDataSourceModelPropertiesService: TopologyDataSourceModelPropertiesService ) {} public matches(edge: TopologyEdge & Partial): edge is EntityEdge { @@ -90,13 +81,17 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat domRenderer: Renderer2 ): void { const selection = this.d3Utils.select(element, domRenderer); - const metricSpecifications = state.dataSpecifiers?.map(specifier => specifier.value); + + const primaryMetric = this.topologyDataSourceModelPropertiesService.getPrimaryEdgeMetric(); + const secondaryMetric = this.topologyDataSourceModelPropertiesService.getSecondaryEdgeMetric(); this.updateEdgeMetric( selection, state.visibility, - getLatencyMetric(edge.data, metricSpecifications), - getErrorPercentageMetric(edge.data, metricSpecifications) + primaryMetric?.extractAndGetDataCategoryForMetric(edge.data), + secondaryMetric?.extractAndGetDataCategoryForMetric(edge.data), + primaryMetric?.extractDataForMetric(edge.data), + secondaryMetric?.extractDataForMetric(edge.data) ); this.visibilityUpdater.updateVisibility(selection, state.visibility); @@ -107,22 +102,42 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat protected updateEdgeMetric( selection: Selection, visibility: TopologyElementVisibility, - latencyMetric?: PercentileLatencyMetricAggregation, - errorPercentageMetric?: ErrorPercentageMetricAggregation + primaryMetricCategory?: TopologyMetricCategoryData, + secondaryMetricCategory?: TopologyMetricCategoryData, + primaryMetricAggregation?: MetricAggregation, + secondaryMetricAggregation?: MetricAggregation ): void { - const edgeCategoryClass = this.getEdgeCategoryClass(latencyMetric, errorPercentageMetric); - selection.classed(this.getAllCategoryClasses().join(' '), false).classed(edgeCategoryClass, true); + const edgeFocusedCategory = this.isEmphasizedOrFocused(visibility) + ? this.getEdgeFocusedCategory(primaryMetricCategory, secondaryMetricCategory) + : undefined; + + selection + .select(selector(this.edgeLineClass)) + .select('.edge-path') + .attr('stroke', edgeFocusedCategory?.strokeColor ?? Color.Gray3); + + selection + .select(selector(this.edgeMetricBubbleClass)) + .attr('fill', edgeFocusedCategory?.fillColor ?? '') + .attr('stroke', edgeFocusedCategory?.strokeColor ?? 'none'); selection .select(selector(this.edgeMetricValueClass)) - .text(this.getMetricValueString(latencyMetric, errorPercentageMetric)); + .text( + this.getMetricValueString( + primaryMetricAggregation, + secondaryMetricAggregation, + primaryMetricCategory, + secondaryMetricCategory + ) + ); selection .select(selector(this.edgeLineClass)) .attr( 'marker-end', - visibility === TopologyElementVisibility.Emphasized || visibility === TopologyElementVisibility.Focused - ? `url(#${this.getMarkerIdForCategory(edgeCategoryClass)})` + this.isEmphasizedOrFocused(visibility) + ? `url(#${this.getMarkerIdForCategory(edgeFocusedCategory?.getCategoryClassName() ?? '')})` : 'none' ); } @@ -143,22 +158,26 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat } private defineArrowMarkersIfNeeded(edgeElement: SVGGElement, domRenderer: Renderer2): void { + const allEdgeCategories = this.topologyDataSourceModelPropertiesService.getAllEdgeCategories(); this.d3Utils .select(this.svgUtils.addDefinitionDeclarationToSvgIfNotExists(edgeElement, domRenderer), domRenderer) .selectAll('marker') - .data(this.getAllCategoryClasses()) + .data(allEdgeCategories) .enter() .append('marker') - .attr('id', category => this.getMarkerIdForCategory(category)) + .attr('id', category => this.getMarkerIdForCategory(category.getCategoryClassName())) .attr('viewBox', '0 0 10 10') .attr('refX', 5) .attr('refY', 5) .attr('markerWidth', 12) .attr('markerHeight', 12) .attr('orient', 'auto-start-reverse') + .attr('fill', category => category.fillColor) .append('path') .classed(this.edgeArrowClass, true) - .each((category, index, elements) => this.d3Utils.select(elements[index], domRenderer).classed(category, true)) + .each((category, index, elements) => + this.d3Utils.select(elements[index], domRenderer).classed(category.getCategoryClassName(), true) + ) .attr('d', 'M2,2 L5,5 L2,8'); } @@ -227,44 +246,28 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat return 2; } - protected getAllCategoryClasses(): string[] { - return [ - ...allLatencyMetricCategories.map(getLatencyCategoryClass), - ...allErrorPercentageMetricCategories.map(getErrorPercentageCategoryClass) - ]; + private isEmphasizedOrFocused(visibility: TopologyElementVisibility): boolean { + return visibility === TopologyElementVisibility.Emphasized || visibility === TopologyElementVisibility.Focused; } - private getEdgeCategoryClass( - latencyMetric?: PercentileLatencyMetricAggregation, - errorPercentageMetric?: ErrorPercentageMetricAggregation - ): string { - if (errorPercentageMetric?.category === ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5) { - return getErrorPercentageCategoryClass(errorPercentageMetric?.category); - } - - if (latencyMetric) { - return getLatencyCategoryClass(latencyMetric.category); - } - - return ''; + private getEdgeFocusedCategory( + primaryMetricCategory?: TopologyMetricCategoryData, + secondaryMetricCategory?: TopologyMetricCategoryData + ): TopologyMetricCategoryData | undefined { + return secondaryMetricCategory?.highestPrecedence ? secondaryMetricCategory : primaryMetricCategory; } private getMetricValueString( - latencyMetric?: PercentileLatencyMetricAggregation, - errorPercentageMetric?: ErrorPercentageMetricAggregation + primaryMetricAggregation?: MetricAggregation, + secondaryMetricAggregation?: MetricAggregation, + primaryMetricCategory?: TopologyMetricCategoryData, + secondaryMetricCategory?: TopologyMetricCategoryData ): string { - if ( - errorPercentageMetric && - (errorPercentageMetric.category === ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 || !latencyMetric) - ) { - return this.formattedMetricValue(errorPercentageMetric.value, errorPercentageMetric.units); - } - - if (latencyMetric) { - return this.formattedMetricValue(latencyMetric.value, latencyMetric.units); + if (secondaryMetricCategory?.highestPrecedence === true || !primaryMetricCategory) { + return this.formattedMetricValue(secondaryMetricAggregation!.value, secondaryMetricAggregation?.units); } - return '-'; + return this.formattedMetricValue(primaryMetricAggregation!.value, primaryMetricAggregation?.units); } private formattedMetricValue(valueToShow: number, unit?: string): string { diff --git a/projects/observability/src/shared/dashboard/widgets/topology/metric/metric-category.ts b/projects/observability/src/shared/dashboard/widgets/topology/metric/metric-category.ts deleted file mode 100644 index 243ed62f4..000000000 --- a/projects/observability/src/shared/dashboard/widgets/topology/metric/metric-category.ts +++ /dev/null @@ -1,71 +0,0 @@ -import { Dictionary } from '@hypertrace/common'; -import { MetricAggregation, MetricAggregationType } from '@hypertrace/distributed-tracing'; -import { - ErrorPercentageMetricAggregation, - ErrorPercentageMetricValueCategory -} from '../../../../graphql/model/schema/specifications/error-percentage-aggregation-specification'; -import { MetricAggregationSpecification } from '../../../../graphql/model/schema/specifications/metric-aggregation-specification'; -import { - PercentileLatencyMetricAggregation, - PercentileLatencyMetricValueCategory -} from '../../../../graphql/model/schema/specifications/percentile-latency-aggregation-specification'; - -export const getLatencyMetric = ( - data: Dictionary, - metricSpecifications?: MetricAggregationSpecification[] -): PercentileLatencyMetricAggregation | undefined => { - const latencySpecification = metricSpecifications?.find( - specification => specification.name === 'p99Latency' && specification.aggregation === MetricAggregationType.P99 - ); - - return getTopologyMetric(data, latencySpecification); -}; - -export const getErrorPercentageMetric = ( - data: Dictionary, - metricSpecifications?: MetricAggregationSpecification[] -): ErrorPercentageMetricAggregation | undefined => { - const errorSpecification = metricSpecifications?.find( - specification => - specification.name === 'errorPercentage' && specification.aggregation === MetricAggregationType.Average - ); - - return getTopologyMetric(data, errorSpecification); -}; - -export const getAllCategoryClasses = () => [ - ...allLatencyMetricCategories.map(getLatencyCategoryClass), - ...allErrorPercentageMetricCategories.map(getErrorPercentageCategoryClass) -]; - -export const getLatencyCategoryClass = (category?: PercentileLatencyMetricValueCategory): string => - category !== undefined ? `${category}-category` : ''; - -export const getErrorPercentageCategoryClass = (category?: ErrorPercentageMetricValueCategory): string => - category !== undefined ? `${category}-error-category` : ''; - -export const allLatencyMetricCategories = [ - PercentileLatencyMetricValueCategory.LessThan20, - PercentileLatencyMetricValueCategory.From20To100, - PercentileLatencyMetricValueCategory.From100To500, - PercentileLatencyMetricValueCategory.From500To1000, - PercentileLatencyMetricValueCategory.GreaterThanOrEqualTo1000, - PercentileLatencyMetricValueCategory.NotSpecified -]; - -export const allErrorPercentageMetricCategories = [ - ErrorPercentageMetricValueCategory.LessThan5, - ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5, - ErrorPercentageMetricValueCategory.NotSpecified -]; - -const getTopologyMetric = ( - data: Dictionary, - spec?: MetricAggregationSpecification -): T | undefined => { - if (!spec) { - return undefined; - } - - return data[spec.resultAlias()] as T; -}; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/node/box/api-node-renderer/api-node-box-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/node/box/api-node-renderer/api-node-box-renderer.service.ts index e1bae7654..81f682d0a 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/node/box/api-node-renderer/api-node-box-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/node/box/api-node-renderer/api-node-box-renderer.service.ts @@ -5,7 +5,7 @@ import { entityTypeKey, ObservabilityEntityType } from '../../../../../../graphq import { EntityNode } from '../../../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; import { EntityNodeBoxRendererService } from '../entity-node-box-renderer.service'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class ApiNodeBoxRendererService extends EntityNodeBoxRendererService { public matches(node: TopologyNode & Partial): node is EntityNode { return this.isEntityNode(node) && node.data[entityTypeKey] === ObservabilityEntityType.Api; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/node/box/backend-node-renderer/backend-node-box-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/node/box/backend-node-renderer/backend-node-box-renderer.service.ts index 0108d435e..012b31139 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/node/box/backend-node-renderer/backend-node-box-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/node/box/backend-node-renderer/backend-node-box-renderer.service.ts @@ -5,7 +5,7 @@ import { entityTypeKey, ObservabilityEntityType } from '../../../../../../graphq import { EntityNode } from '../../../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; import { EntityNodeBoxRendererService } from '../entity-node-box-renderer.service'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class BackendNodeBoxRendererService extends EntityNodeBoxRendererService { public matches(node: TopologyNode & Partial): node is EntityNode { return this.isEntityNode(node) && node.data[entityTypeKey] === ObservabilityEntityType.Backend; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.scss b/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.scss index 241c595c7..1539df693 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.scss +++ b/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.scss @@ -45,7 +45,6 @@ .entity-outer-band { filter: url(#entity-node-dropshadow-filter); fill: white; - stroke: $blue-4; stroke-width: 1px; } @@ -86,49 +85,6 @@ color: $gray-4; fill: currentColor; } - - &.less-than-20-category { - .metric-category { - fill: $blue-gray-1; - } - } - - &.from-20-to-100-category { - .metric-category { - fill: $blue-gray-2; - } - } - - &.from-100-to-500-category { - .metric-category { - fill: $blue-gray-3; - } - } - - &.from-500-to-1000-category { - .metric-category { - fill: $blue-gray-4; - } - } - - &.greater-than-or-equal-to-1000-category { - .metric-category { - fill: $blue-gray-5; - } - } - - &.not-specified-category { - .metric-category { - fill: lightgray; - } - } - - &.greater-than-or-equal-to-5-error-category { - .entity-outer-band { - fill: $red-1; - stroke: $red-5; - } - } } } } diff --git a/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.service.ts index d484c6de9..209392247 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/node/box/entity-node-box-renderer.service.ts @@ -1,32 +1,32 @@ import { Injectable, Renderer2 } from '@angular/core'; -import { DomElementMeasurerService, NumericFormatter, selector } from '@hypertrace/common'; +import { Color, DomElementMeasurerService, NumericFormatter, selector } from '@hypertrace/common'; import { MetricAggregation, MetricHealth } from '@hypertrace/distributed-tracing'; import { select, Selection } from 'd3-selection'; import { Observable, Subject } from 'rxjs'; import { filter, takeUntil } from 'rxjs/operators'; import { TopologyNodeRendererDelegate } from '../../../../../components/topology/renderers/node/topology-node-renderer.service'; -import { TopologyCoordinates, TopologyNode, TopologyNodeState } from '../../../../../components/topology/topology'; +import { + TopologyCoordinates, + TopologyElementVisibility, + TopologyNode, + TopologyNodeState +} from '../../../../../components/topology/topology'; import { D3UtilService } from '../../../../../components/utils/d3/d3-util.service'; import { SvgUtilService } from '../../../../../components/utils/svg/svg-util.service'; import { Entity } from '../../../../../graphql/model/schema/entity'; -import { ErrorPercentageMetricAggregation } from '../../../../../graphql/model/schema/specifications/error-percentage-aggregation-specification'; import { MetricAggregationSpecification } from '../../../../../graphql/model/schema/specifications/metric-aggregation-specification'; -import { PercentileLatencyMetricAggregation } from '../../../../../graphql/model/schema/specifications/percentile-latency-aggregation-specification'; import { EntityNode } from '../../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; import { EntityIconLookupService } from '../../../../../services/entity/entity-icon-lookup.service'; import { EntityNavigationService } from '../../../../../services/navigation/entity/entity-navigation.service'; -import { - getAllCategoryClasses, - getErrorPercentageCategoryClass, - getErrorPercentageMetric, - getLatencyCategoryClass, - getLatencyMetric -} from '../../metric/metric-category'; +import { TopologyMetricCategoryData } from '../../../../data/graphql/topology/metrics/topology-metric-category.model'; +import { TopologyDataSourceModelPropertiesService } from '../../topology-data-source-model-properties.service'; import { VisibilityUpdater } from '../../visibility-updater'; @Injectable() // Annotate so parameters still provide metadata for children export abstract class EntityNodeBoxRendererService implements TopologyNodeRendererDelegate { private readonly entityMetricClass: string = 'entity-metric-value'; + private readonly entityOuterBandClass: string = 'entity-outer-band'; + private readonly metricCategoryClass: string = 'metric-category'; private readonly dropshadowFilterId: string = 'entity-node-dropshadow-filter'; private readonly nodeSelectionMap: WeakMap< EntityNode, @@ -41,7 +41,8 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender private readonly domElementMeasurerService: DomElementMeasurerService, private readonly svgUtils: SvgUtilService, protected readonly d3Utils: D3UtilService, - private readonly entityIconLookupService: EntityIconLookupService + private readonly entityIconLookupService: EntityIconLookupService, + private readonly topologyDataSourceModelPropertiesService: TopologyDataSourceModelPropertiesService ) {} public abstract matches(node: TopologyNode & Partial): node is EntityNode; @@ -101,13 +102,14 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender domElementRenderer: Renderer2 ): void { const elementSelection = this.d3Utils.select(element, domElementRenderer); - const metricSpecifications = state.dataSpecifiers?.map(specifier => specifier.value); - - this.updateNodeMetric( - elementSelection, - getLatencyMetric(node.data, metricSpecifications), - getErrorPercentageMetric(node.data, metricSpecifications) - ); + const primaryMetricCategory = this.topologyDataSourceModelPropertiesService + .getPrimaryNodeMetric() + ?.extractAndGetDataCategoryForMetric(node.data); + const secondaryMetricCategory = this.topologyDataSourceModelPropertiesService + .getSecondaryNodeMetric() + ?.extractAndGetDataCategoryForMetric(node.data); + + this.updateNodeMetric(elementSelection, state.visibility, primaryMetricCategory, secondaryMetricCategory); this.visibilityUpdater.updateVisibility(elementSelection, state.visibility); elementSelection.classed('dragging', state.dragging); @@ -127,14 +129,35 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender protected updateNodeMetric( selection: Selection, - latencyMetric?: PercentileLatencyMetricAggregation, - errorPercentageMetric?: ErrorPercentageMetricAggregation + visibility: TopologyElementVisibility, + primaryMetric?: TopologyMetricCategoryData, + secondaryMetric?: TopologyMetricCategoryData ): void { selection - .classed(getAllCategoryClasses().join(' '), false) - .classed(getLatencyCategoryClass(latencyMetric?.category), true) - .classed(getErrorPercentageCategoryClass(errorPercentageMetric?.category), true) + .classed(primaryMetric?.getCategoryClassName() ?? '', true) + .classed(secondaryMetric?.getCategoryClassName() ?? '', true) .select(selector(this.entityMetricClass)); + + // For primary category + selection.select(selector(this.metricCategoryClass)).attr('fill', primaryMetric?.fillColor!); + + // For secondary category + selection + .select(selector(this.entityOuterBandClass)) + .style('fill', () => { + if (visibility === TopologyElementVisibility.Focused || visibility === TopologyElementVisibility.Emphasized) { + return this.focusedOrEmphasizedColor(); + } + + return secondaryMetric?.fillColor ?? ''; + }) + .style('stroke', () => { + if (visibility === TopologyElementVisibility.Focused) { + return secondaryMetric?.highestPrecedence === true ? secondaryMetric?.focusColor : primaryMetric?.focusColor!; + } + + return secondaryMetric?.strokeColor ?? ''; + }); } protected isEntityNode(node: TopologyNode & Partial): node is EntityNode { @@ -155,7 +178,7 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender protected addOuterBand(nodeSelection: Selection): void { nodeSelection .append('rect') - .classed('entity-outer-band', true) + .classed(this.entityOuterBandClass, true) .attr('x', 0) .attr('y', 0) .attr('width', this.boxWidth()) @@ -168,7 +191,7 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender protected addMetricCategory(nodeSelection: Selection): void { nodeSelection .append('circle') - .classed('metric-category', true) + .classed(this.metricCategoryClass, true) .attr('transform', `translate(${this.getPadding()}, ${(this.boxHeight() - this.metricCategoryWidth()) / 2})`) .attr('cx', '4') .attr('cy', '4') @@ -246,6 +269,14 @@ export abstract class EntityNodeBoxRendererService implements TopologyNodeRender .each((_datum, index, groups) => this.svgUtils.truncateText(groups[index], 30)); } + protected focusedOrEmphasizedColor(): string { + return Color.White; + } + + protected focusedBandColor(): string { + return Color.Blue4; + } + protected boxWidth(): number { return 240; } diff --git a/projects/observability/src/shared/dashboard/widgets/topology/node/box/service-node-renderer/service-node-box-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/node/box/service-node-renderer/service-node-box-renderer.service.ts index d99ed8471..02955ed56 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/node/box/service-node-renderer/service-node-box-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/node/box/service-node-renderer/service-node-box-renderer.service.ts @@ -5,7 +5,7 @@ import { entityTypeKey, ObservabilityEntityType } from '../../../../../../graphq import { EntityNode } from '../../../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; import { EntityNodeBoxRendererService } from '../entity-node-box-renderer.service'; -@Injectable({ providedIn: 'root' }) +@Injectable() export class ServiceNodeBoxRendererService extends EntityNodeBoxRendererService { public matches(node: TopologyNode & Partial): node is EntityNode { return this.isEntityNode(node) && node.data[entityTypeKey] === ObservabilityEntityType.Service; diff --git a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts index a475b3a7f..696608f37 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts @@ -81,7 +81,25 @@ describe('Topology Widget renderer', () => { nodes: mockResponse, nodeSpecification: nodeSpec, edgeSpecification: edgeSpec, - nodeTypes: uniq(mockResponse.map(node => node.data[entityTypeKey])) + nodeTypes: uniq(mockResponse.map(node => node.data[entityTypeKey])), + nodeMetrics: { + primary: { + specification: nodeSpec.metricSpecifications[0] + }, + secondary: { + specification: nodeSpec.metricSpecifications[1] + }, + others: [nodeSpec.metricSpecifications[2], nodeSpec.metricSpecifications[3]] + }, + edgeMetrics: { + primary: { + specification: edgeSpec.metricSpecifications[0] + }, + secondary: { + specification: edgeSpec.metricSpecifications[1] + }, + others: [edgeSpec.metricSpecifications[2], edgeSpec.metricSpecifications[3]] + } }) ), showLegend: true diff --git a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.ts b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.ts index 51ecb7cb5..28303feb8 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.ts @@ -20,6 +20,7 @@ import { ApiNodeBoxRendererService } from './node/box/api-node-renderer/api-node import { BackendNodeBoxRendererService } from './node/box/backend-node-renderer/backend-node-box-renderer.service'; import { ServiceNodeBoxRendererService } from './node/box/service-node-renderer/service-node-box-renderer.service'; import { TopologyEntityTooltipComponent } from './tooltip/topology-entity-tooltip.component'; +import { TopologyDataSourceModelPropertiesService } from './topology-data-source-model-properties.service'; import { TopologyWidgetModel } from './topology-widget.model'; @Renderer({ modelClass: TopologyWidgetModel }) @@ -30,7 +31,16 @@ import { TopologyWidgetModel } from './topology-widget.model'; './edge/curved/entity-edge-curve-renderer.scss', './topology-widget-renderer.component.scss' ], - providers: [TopologyNodeRendererService, TopologyEdgeRendererService, TopologyTooltipRendererService], + providers: [ + TopologyNodeRendererService, + TopologyEdgeRendererService, + TopologyTooltipRendererService, + EntityEdgeCurveRendererService, + ApiNodeBoxRendererService, + BackendNodeBoxRendererService, + ServiceNodeBoxRendererService, + TopologyDataSourceModelPropertiesService + ], changeDetection: ChangeDetectionStrategy.OnPush, template: `
@@ -81,6 +91,7 @@ export class TopologyWidgetRendererComponent extends WidgetRenderer ({ From dd26de0cd42b1bb10fe307ec3aa11a9154c5728a Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Thu, 15 Jul 2021 18:33:34 -0700 Subject: [PATCH 3/5] refactor: adding test --- .../topology-metric-category.model.test.ts | 22 +++ ...opology-metric-with-category.model.test.ts | 72 ++++++++++ ...ta-source-model-properties.service.test.ts | 125 ++++++++++++++++++ 3 files changed, 219 insertions(+) create mode 100644 projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.test.ts create mode 100644 projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.test.ts create mode 100644 projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.test.ts diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.test.ts new file mode 100644 index 000000000..6c09a13b2 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-category.model.test.ts @@ -0,0 +1,22 @@ +import { Color } from '@hypertrace/common'; +import { createModelFactory } from '@hypertrace/dashboards/testing'; +import { TopologyMetricCategoryModel } from './topology-metric-category.model'; + +describe('Topology Metric with category model', () => { + const modelFactory = createModelFactory(); + + test('provides category name correctly', () => { + const spectator = modelFactory(TopologyMetricCategoryModel, { + properties: { + name: 'test name', + minValue: 0, + maxValue: 10, + fillColor: Color.Blue2, + strokeColor: Color.Blue3, + focusColor: Color.Blue4 + } + }); + + expect(spectator.model.getCategoryClassName()).toContain('test-name'); + }); +}); diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.test.ts new file mode 100644 index 000000000..4d496b2f1 --- /dev/null +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/metrics/topology-metric-with-category.model.test.ts @@ -0,0 +1,72 @@ +import { Color } from '@hypertrace/common'; +import { createModelFactory } from '@hypertrace/dashboards/testing'; +import { MetricAggregationType } from '@hypertrace/distributed-tracing'; +import { MetricAggregationSpecificationModel } from '../../specifiers/metric-aggregation-specification.model'; +import { TopologyMetricCategoryModel } from './topology-metric-category.model'; +import { TopologyMetricWithCategoryModel } from './topology-metric-with-category.model'; + +describe('Topology Metric with category model', () => { + const modelFactory = createModelFactory(); + + const createCategoryModel = ( + name: string, + minValue: number, + fillColor: Color, + strokeColor: Color, + focusColor: Color, + maxValue?: number + ): TopologyMetricCategoryModel => { + const categoryModel = new TopologyMetricCategoryModel(); + categoryModel.name = name; + categoryModel.minValue = minValue; + categoryModel.maxValue = maxValue; + categoryModel.fillColor = fillColor; + categoryModel.strokeColor = strokeColor; + categoryModel.focusColor = focusColor; + + return categoryModel; + }; + test('provides category name correctly', () => { + const specification = new MetricAggregationSpecificationModel(); + specification.metric = 'metric-name'; + specification.aggregation = MetricAggregationType.Average; + specification.modelOnInit(); + + const categories = [ + createCategoryModel('first', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10), + createCategoryModel('second', 10, Color.Red1, Color.Red3, Color.Red4, 50), + createCategoryModel('third', 50, Color.Blue2, Color.Blue3, Color.Blue4) + ]; + + const spectator = modelFactory(TopologyMetricWithCategoryModel, { + properties: { + specification: specification, + categories: categories + } + }); + + expect( + spectator.model.extractAndGetDataCategoryForMetric({ + [specification.resultAlias()]: { value: 50 } + }) + ).toEqual(categories[2]); + + expect( + spectator.model.extractAndGetDataCategoryForMetric({ + [specification.resultAlias()]: { value: 5 } + }) + ).toEqual(categories[0]); + + expect( + spectator.model.extractAndGetDataCategoryForMetric({ + [specification.resultAlias()]: { value: 22 } + }) + ).toEqual(categories[1]); + + expect( + spectator.model.extractAndGetDataCategoryForMetric({ + [specification.resultAlias()]: { value: -10 } + }) + ).toEqual(undefined); + }); +}); diff --git a/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.test.ts b/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.test.ts new file mode 100644 index 000000000..02c494f5f --- /dev/null +++ b/projects/observability/src/shared/dashboard/widgets/topology/topology-data-source-model-properties.service.test.ts @@ -0,0 +1,125 @@ +import { Color } from '@hypertrace/common'; +import { MetricAggregationType } from '@hypertrace/distributed-tracing'; +import { MetricAggregationSpecificationModel } from '@hypertrace/observability'; +import { createServiceFactory } from '@ngneat/spectator/jest'; +import { TopologyMetricCategoryModel } from '../../data/graphql/topology/metrics/topology-metric-category.model'; +import { TopologyMetricWithCategoryModel } from '../../data/graphql/topology/metrics/topology-metric-with-category.model'; +import { TopologyMetricsData } from '../../data/graphql/topology/metrics/topology-metrics.model'; +import { TopologyDataSourceModelPropertiesService } from './topology-data-source-model-properties.service'; + +describe('TopologyDataSourceModelPropertiesService', () => { + const createService = createServiceFactory({ + service: TopologyDataSourceModelPropertiesService + }); + + const createCategoryModel = ( + name: string, + minValue: number, + fillColor: Color, + strokeColor: Color, + focusColor: Color, + maxValue?: number + ): TopologyMetricCategoryModel => { + const categoryModel = new TopologyMetricCategoryModel(); + categoryModel.name = name; + categoryModel.minValue = minValue; + categoryModel.maxValue = maxValue; + categoryModel.fillColor = fillColor; + categoryModel.strokeColor = strokeColor; + categoryModel.focusColor = focusColor; + + return categoryModel; + }; + + const createSpecificationModel = (metric: string, aggregation: MetricAggregationType) => { + const specification = new MetricAggregationSpecificationModel(); + specification.metric = metric; + specification.aggregation = aggregation; + + specification.modelOnInit(); + + return specification; + }; + + const createMetricWithCategory = ( + spec: MetricAggregationSpecificationModel, + categories: TopologyMetricCategoryModel[] + ) => { + const model = new TopologyMetricWithCategoryModel(); + model.specification = spec; + model.categories = categories; + + return model; + }; + + test('should return correct results', () => { + const spectator = createService(); + + const nodePrimary = createMetricWithCategory( + createSpecificationModel('node-metric-1', MetricAggregationType.Average), + [ + createCategoryModel('node-first-1', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10), + createCategoryModel('node-second-1', 10, Color.Red1, Color.Red3, Color.Red4, 50), + createCategoryModel('node-third-1', 50, Color.Blue2, Color.Blue3, Color.Blue4) + ] + ); + + const nodeSecondary = createMetricWithCategory( + createSpecificationModel('node-metric-2', MetricAggregationType.Average), + [ + createCategoryModel('node-first-2', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10), + createCategoryModel('node-second-2', 10, Color.Red1, Color.Red3, Color.Red4, 50), + createCategoryModel('node-third-2', 50, Color.Blue2, Color.Blue3, Color.Blue4) + ] + ); + + const nodeOthers = [ + createMetricWithCategory(createSpecificationModel('node-metric-5', MetricAggregationType.Average), [ + createCategoryModel('node-others-2', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10) + ]) + ]; + + const nodeMetrics: TopologyMetricsData = { + primary: nodePrimary, + secondary: nodeSecondary, + others: nodeOthers + }; + + const edgePrimary = createMetricWithCategory( + createSpecificationModel('edge-metric-3', MetricAggregationType.Average), + [ + createCategoryModel('edge-first-1', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10), + createCategoryModel('edge-second-1', 10, Color.Red1, Color.Red3, Color.Red4, 50), + createCategoryModel('edge-third-1', 50, Color.Blue2, Color.Blue3, Color.Blue4) + ] + ); + + const edgeSecondary = createMetricWithCategory( + createSpecificationModel('metric-4', MetricAggregationType.Average), + [ + createCategoryModel('edge-first-2', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10), + createCategoryModel('edge-second-2', 10, Color.Red1, Color.Red3, Color.Red4, 50), + createCategoryModel('edge-third-2', 50, Color.Blue2, Color.Blue3, Color.Blue4) + ] + ); + + const edgeOthers = [ + createMetricWithCategory(createSpecificationModel('metric-4', MetricAggregationType.Average), [ + createCategoryModel('edge-others-2', 0, Color.Blue2, Color.Blue3, Color.Blue4, 10) + ]) + ]; + + const edgeMetrics: TopologyMetricsData = { + primary: edgePrimary, + secondary: edgeSecondary, + others: edgeOthers + }; + spectator.service.setModelProperties(nodeMetrics, edgeMetrics); + + expect(spectator.service.getPrimaryNodeMetric()).toEqual(nodePrimary); + expect(spectator.service.getSecondaryNodeMetric()).toEqual(nodeSecondary); + + expect(spectator.service.getPrimaryEdgeMetric()).toEqual(edgePrimary); + expect(spectator.service.getSecondaryEdgeMetric()).toEqual(edgeSecondary); + }); +}); From 43b5ed5f9defae52f936aa036996cec664e2d793 Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Thu, 15 Jul 2021 21:32:48 -0700 Subject: [PATCH 4/5] refactor: adding tests --- .../topology-data-source.model.test.ts | 66 +++- .../entity-edge-curve-renderer.service.ts | 8 +- ...topology-widget-renderer.component.test.ts | 318 ++++++++++++------ 3 files changed, 281 insertions(+), 111 deletions(-) diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts index e4c6192f0..86cfe87b9 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts @@ -1,4 +1,4 @@ -import { isEqualIgnoreFunctions } from '@hypertrace/common'; +import { Color, isEqualIgnoreFunctions } from '@hypertrace/common'; import { GraphQlTimeRange, MetricAggregationType } from '@hypertrace/distributed-tracing'; import { GraphQlRequestCacheability, GraphQlRequestOptions } from '@hypertrace/graphql-client'; import { ModelApi } from '@hypertrace/hyperdash'; @@ -8,6 +8,10 @@ import { ENTITY_TOPOLOGY_GQL_REQUEST, TopologyNodeSpecification } from '../../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; +import { MetricAggregationSpecificationModel } from '../specifiers/metric-aggregation-specification.model'; +import { TopologyMetricCategoryModel } from './metrics/topology-metric-category.model'; +import { TopologyMetricWithCategoryModel } from './metrics/topology-metric-with-category.model'; +import { TopologyMetricsModel } from './metrics/topology-metrics.model'; import { TopologyDataSourceModel } from './topology-data-source.model'; describe('topology data source model', () => { @@ -17,6 +21,57 @@ describe('topology data source model', () => { let lastEmittedQuery: unknown; let lastEmittedQueryRequestOption: GraphQlRequestOptions | undefined; + const createCategoryModel = ( + name: string, + minValue: number, + fillColor: Color, + strokeColor: Color, + focusColor: Color, + maxValue?: number + ): TopologyMetricCategoryModel => { + const categoryModel = new TopologyMetricCategoryModel(); + categoryModel.name = name; + categoryModel.minValue = minValue; + categoryModel.maxValue = maxValue; + categoryModel.fillColor = fillColor; + categoryModel.strokeColor = strokeColor; + categoryModel.focusColor = focusColor; + + return categoryModel; + }; + + const createSpecificationModel = (metric: string, aggregation: MetricAggregationType) => { + const specification = new MetricAggregationSpecificationModel(); + specification.metric = metric; + specification.aggregation = aggregation; + + specification.modelOnInit(); + + return specification; + }; + + const createMetricWithCategory = ( + spec: MetricAggregationSpecificationModel, + categories: TopologyMetricCategoryModel[] + ) => { + const withCategoryModel = new TopologyMetricWithCategoryModel(); + withCategoryModel.specification = spec; + withCategoryModel.categories = categories; + + return withCategoryModel; + }; + + const createTopologyMetricsModel = (metric: string, aggregation: MetricAggregationType) => { + const primary = createMetricWithCategory(createSpecificationModel(metric, aggregation), [ + createCategoryModel(metric, 0, Color.Blue2, Color.Blue3, Color.Blue4, 10) + ]); + + const metricsModel: TopologyMetricsModel = new TopologyMetricsModel(); + metricsModel.primary = primary; + + return metricsModel; + }; + beforeEach(() => { const mockApi: Partial = { getTimeRange: jest.fn(() => testTimeRange) @@ -25,12 +80,9 @@ describe('topology data source model', () => { model.downstreamEntityTypes = [ObservabilityEntityType.Api, ObservabilityEntityType.Backend]; model.upstreamEntityTypes = [ObservabilityEntityType.Service]; model.entityType = ObservabilityEntityType.Service; - model.nodeMetricSpecifications = [ - specBuilder.metricAggregationSpecForKey('numCalls', MetricAggregationType.Average) - ]; - model.edgeMetricSpecifications = [ - specBuilder.metricAggregationSpecForKey('duration', MetricAggregationType.Average) - ]; + model.nodeMetricsModel = createTopologyMetricsModel('numCalls', MetricAggregationType.Average); + model.edgeMetricsModel = createTopologyMetricsModel('duration', MetricAggregationType.Average); + model.api = mockApi as ModelApi; model.query$.subscribe(query => { lastEmittedQuery = query.buildRequest([]); diff --git a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts index 8230c9d4b..da93fea4b 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/edge/curved/entity-edge-curve-renderer.service.ts @@ -263,11 +263,13 @@ export class EntityEdgeCurveRendererService implements TopologyEdgeRenderDelegat primaryMetricCategory?: TopologyMetricCategoryData, secondaryMetricCategory?: TopologyMetricCategoryData ): string { - if (secondaryMetricCategory?.highestPrecedence === true || !primaryMetricCategory) { - return this.formattedMetricValue(secondaryMetricAggregation!.value, secondaryMetricAggregation?.units); + if (secondaryMetricAggregation && (secondaryMetricCategory?.highestPrecedence === true || !primaryMetricCategory)) { + return this.formattedMetricValue(secondaryMetricAggregation.value, secondaryMetricAggregation?.units); } - return this.formattedMetricValue(primaryMetricAggregation!.value, primaryMetricAggregation?.units); + return primaryMetricAggregation + ? this.formattedMetricValue(primaryMetricAggregation.value, primaryMetricAggregation?.units) + : '-'; } private formattedMetricValue(valueToShow: number, unit?: string): string { diff --git a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts index 696608f37..fa56182a6 100644 --- a/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts +++ b/projects/observability/src/shared/dashboard/widgets/topology/topology-widget-renderer.component.test.ts @@ -3,9 +3,10 @@ import { HttpClientTestingModule } from '@angular/common/http/testing'; import { Renderer2 } from '@angular/core'; import { discardPeriodicTasks, fakeAsync, flush, TestBed } from '@angular/core/testing'; import { IconLibraryTestingModule, IconRegistryService } from '@hypertrace/assets-library'; -import { DomElementMeasurerService, selector } from '@hypertrace/common'; +import { Color, DomElementMeasurerService, selector } from '@hypertrace/common'; import { mockDashboardWidgetProviders } from '@hypertrace/dashboards/testing'; import { MetricAggregationType, MetricHealth } from '@hypertrace/distributed-tracing'; +import { MetricAggregationSpecificationModel } from '@hypertrace/observability'; import { addWidthAndHeightToSvgElForTest } from '@hypertrace/test-utils'; import { createComponentFactory, mockProvider, Spectator } from '@ngneat/spectator/jest'; import { uniq } from 'lodash-es'; @@ -23,6 +24,9 @@ import { } from '../../../graphql/request/handlers/entities/query/topology/entity-topology-graphql-query-handler.service'; import { ObservabilityIconLibraryModule } from '../../../icons/observability-icon-library.module'; import { ObservabilityIconType } from '../../../icons/observability-icon-type'; +import { TopologyMetricCategoryModel } from '../../data/graphql/topology/metrics/topology-metric-category.model'; +import { TopologyMetricWithCategoryModel } from '../../data/graphql/topology/metrics/topology-metric-with-category.model'; +import { TopologyMetricsModel } from './../../data/graphql/topology/metrics/topology-metrics.model'; import { BackendNodeBoxRendererService } from './node/box/backend-node-renderer/backend-node-box-renderer.service'; import { TopologyWidgetRendererComponent } from './topology-widget-renderer.component'; import { TopologyWidgetModule } from './topology-widget.module'; @@ -30,22 +34,70 @@ import { TopologyWidgetModule } from './topology-widget.module'; describe('Topology Widget renderer', () => { let mockResponse: EntityNode[] = []; const specBuilder = new ObservabilitySpecificationBuilder(); - const nodeSpec = { - titleSpecification: specBuilder.attributeSpecificationForKey('name'), - metricSpecifications: [ - specBuilder.metricAggregationSpecForKey('metric1', MetricAggregationType.Average), - specBuilder.metricAggregationSpecForKey('metric2', MetricAggregationType.Max), - specBuilder.metricAggregationSpecForLatency(MetricAggregationType.P99, 'p99Latency'), - specBuilder.metricAggregationSpecForErrorPercentage(MetricAggregationType.Average) - ] + + const createCategoryModel = ( + name: string, + minValue: number, + fillColor: Color, + strokeColor: Color, + focusColor: Color, + maxValue?: number + ): TopologyMetricCategoryModel => { + const categoryModel = new TopologyMetricCategoryModel(); + categoryModel.name = name; + categoryModel.minValue = minValue; + categoryModel.maxValue = maxValue; + categoryModel.fillColor = fillColor; + categoryModel.strokeColor = strokeColor; + categoryModel.focusColor = focusColor; + + return categoryModel; }; - const edgeSpec = { - metricSpecifications: [ - specBuilder.metricAggregationSpecForKey('metric3', MetricAggregationType.Sum), - specBuilder.metricAggregationSpecForKey('metric4', MetricAggregationType.Min), - specBuilder.metricAggregationSpecForLatency(MetricAggregationType.P99, 'p99Latency'), - specBuilder.metricAggregationSpecForErrorPercentage(MetricAggregationType.Average) - ] + + const createSpecificationModel = (metric: string, aggregation: MetricAggregationType) => { + const specification = new MetricAggregationSpecificationModel(); + specification.metric = metric; + specification.aggregation = aggregation; + + specification.modelOnInit(); + + return specification; + }; + + const createMetricWithCategory = ( + spec: MetricAggregationSpecificationModel, + categories: TopologyMetricCategoryModel[] + ) => { + const model = new TopologyMetricWithCategoryModel(); + model.specification = spec; + model.categories = categories; + + return model; + }; + + const createTopologyMetricsModel = (prefix: string) => { + const primary = createMetricWithCategory( + createSpecificationModel(`${prefix}-metric-1`, MetricAggregationType.Average), + [createCategoryModel(`${prefix}-first-1`, 0, Color.Blue2, Color.Blue3, Color.Blue4, 10)] + ); + + const secondary = createMetricWithCategory( + createSpecificationModel(`${prefix}-metric-2`, MetricAggregationType.Average), + [createCategoryModel(`${prefix}-first-2`, 0, Color.Blue2, Color.Blue3, Color.Blue4, 10)] + ); + + const others = [ + createMetricWithCategory(createSpecificationModel(`${prefix}-metric-3`, MetricAggregationType.Average), [ + createCategoryModel(`${prefix}-others-2`, 0, Color.Blue2, Color.Blue3, Color.Blue4, 10) + ]) + ]; + + const metricsModel: TopologyMetricsModel = new TopologyMetricsModel(); + metricsModel.primary = primary; + metricsModel.secondary = secondary; + metricsModel.others = others; + + return metricsModel; }; const findNodeWithTypeAndName = ( @@ -75,6 +127,24 @@ describe('Topology Widget renderer', () => { return node!; }; + const getSpecifications = (metricsModel: TopologyMetricsModel) => + [ + metricsModel.primary.specification, + metricsModel.secondary?.specification ?? [], + (metricsModel.others ?? []).map(model => model.specification) + ].flat(); + + const nodeMetrics = createTopologyMetricsModel('topo'); + const edgeMetrics = createTopologyMetricsModel('topo'); + + const nodeSpec = { + titleSpecification: specBuilder.attributeSpecificationForKey('name'), + metricSpecifications: getSpecifications(nodeMetrics) + }; + const edgeSpec = { + metricSpecifications: getSpecifications(edgeMetrics) + }; + const mockModel = { getData: jest.fn(() => of({ @@ -82,24 +152,8 @@ describe('Topology Widget renderer', () => { nodeSpecification: nodeSpec, edgeSpecification: edgeSpec, nodeTypes: uniq(mockResponse.map(node => node.data[entityTypeKey])), - nodeMetrics: { - primary: { - specification: nodeSpec.metricSpecifications[0] - }, - secondary: { - specification: nodeSpec.metricSpecifications[1] - }, - others: [nodeSpec.metricSpecifications[2], nodeSpec.metricSpecifications[3]] - }, - edgeMetrics: { - primary: { - specification: edgeSpec.metricSpecifications[0] - }, - secondary: { - specification: edgeSpec.metricSpecifications[1] - }, - others: [edgeSpec.metricSpecifications[2], edgeSpec.metricSpecifications[3]] - } + nodeMetrics: nodeMetrics, + edgeMetrics: edgeMetrics }) ), showLegend: true @@ -172,7 +226,7 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'avg(metric1)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.Healthy } @@ -185,7 +239,7 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Api, name: 'Api 2', - 'avg(metric1)': { + 'avg(topo-metric-1)': { value: 456, health: MetricHealth.Warning } @@ -197,7 +251,11 @@ describe('Topology Widget renderer', () => { data: { [entityIdKey]: '3', [entityTypeKey]: ObservabilityEntityType.Backend, - name: 'Backend 3' + name: 'Backend 3', + 'avg(topo-metric-3)': { + value: 456, + health: MetricHealth.Warning + } }, specification: nodeSpec } @@ -229,17 +287,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -256,16 +314,16 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 2', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -282,16 +340,16 @@ describe('Topology Widget renderer', () => { fromNode: mockResponse[0], toNode: mockResponse[1], data: { - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'sum(metric3)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -325,17 +383,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -352,17 +410,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 2', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -379,17 +437,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '3', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 3', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -406,16 +464,16 @@ describe('Topology Widget renderer', () => { fromNode: mockResponse[0], toNode: mockResponse[1], data: { - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 0, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.LessThan5 }, - 'sum(metric3)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -433,16 +491,16 @@ describe('Topology Widget renderer', () => { fromNode: mockResponse[1], toNode: mockResponse[2], data: { - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 1, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.LessThan5 }, - 'sum(metric3)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -505,17 +563,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -532,22 +590,49 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 2', - 'p99(duration)': { - value: 234, + 'avg(topo-metric-1)': { + value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { - value: 456, + 'avg(topo-metric-2)': { + value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { - value: 567, + 'avg(topo-metric-3)': { + value: 345, health: MetricHealth.NotSpecified }, 'max(metric2)': { - value: 789, + value: 456, + health: MetricHealth.NotSpecified + } + }, + specification: nodeSpec + }, + { + edges: [], + data: { + [entityIdKey]: '3', + [entityTypeKey]: ObservabilityEntityType.Service, + name: 'Service 3', + 'avg(topo-metric-1)': { + value: 123, + health: MetricHealth.NotSpecified, + category: PercentileLatencyMetricValueCategory.From100To500 + }, + 'avg(topo-metric-2)': { + value: 234, + health: MetricHealth.NotSpecified, + category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 + }, + 'avg(topo-metric-3)': { + value: 345, + health: MetricHealth.NotSpecified + }, + 'avg(metric2)': { + value: 456, health: MetricHealth.NotSpecified } }, @@ -559,16 +644,16 @@ describe('Topology Widget renderer', () => { fromNode: mockResponse[0], toNode: mockResponse[1], data: { - 'p99(duration)': { - value: 321, + 'avg(topo-metric-1)': { + value: 0, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.LessThan5 }, - 'sum(metric3)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -582,12 +667,40 @@ describe('Topology Widget renderer', () => { mockResponse[0].edges.push(edge0To1); mockResponse[1].edges.push(edge0To1); + const edge1To2: EntityEdge = { + fromNode: mockResponse[1], + toNode: mockResponse[2], + data: { + 'avg(topo-metric-1)': { + value: 1, + health: MetricHealth.NotSpecified + }, + 'avg(topo-metric-2)': { + value: 234, + health: MetricHealth.NotSpecified, + category: ErrorPercentageMetricValueCategory.LessThan5 + }, + 'avg(topo-metric-3)': { + value: 345, + health: MetricHealth.NotSpecified + }, + 'min(metric4)': { + value: 456, + health: MetricHealth.NotSpecified + } + }, + specification: edgeSpec + }; + + mockResponse[1].edges.push(edge1To2); + mockResponse[2].edges.push(edge1To2); + const spectator = createComponent(); spectator.tick(); // Can't use normal angular querying against svgs const getService = findNodeWithTypeAndName.bind(undefined, spectator, ObservabilityEntityType.Service, 'Service 1'); - const getEdge = findEdgeWithMetricValue.bind(undefined, spectator, 321); + const getEdge = findEdgeWithMetricValue.bind(undefined, spectator, 0); spectator.dispatchMouseEvent(getService(), 'mouseenter'); spectator.tick(500); // Trigger popup @@ -600,12 +713,15 @@ describe('Topology Widget renderer', () => { }); let metricRowElements = container.querySelectorAll('.metric-row'); - expect(metricRowElements.length).toBe(4); - expect(metricRowElements[0].querySelector('.metric-label')).toContainText('Metric1'); - expect(metricRowElements[0].querySelector('.metric-value')).toContainText('345'); + expect(metricRowElements.length).toBe(3); + expect(metricRowElements[0].querySelector('.metric-label')).toContainText('Topo-metric-1'); + expect(metricRowElements[0].querySelector('.metric-value')).toContainText('123'); + + expect(metricRowElements[1].querySelector('.metric-label')).toContainText('Topo-metric-2'); + expect(metricRowElements[1].querySelector('.metric-value')).toContainText('234'); - expect(metricRowElements[1].querySelector('.metric-label')).toContainText('Max. Metric2'); - expect(metricRowElements[1].querySelector('.metric-value')).toContainText('456'); + expect(metricRowElements[2].querySelector('.metric-label')).toContainText('Topo-metric-3'); + expect(metricRowElements[2].querySelector('.metric-value')).toContainText('345'); spectator.dispatchMouseEvent(getService(), 'mouseleave'); expect(spectator.query('.tooltip-container', { root: true })).not.toExist(); @@ -619,11 +735,11 @@ describe('Topology Widget renderer', () => { text: `Service 1Service 2` }); metricRowElements = container.querySelectorAll('.metric-row'); - expect(metricRowElements[0].querySelector('.metric-label')).toContainText('Sum Metric3'); - expect(metricRowElements[0].querySelector('.metric-value')).toContainText('345'); + expect(metricRowElements[0].querySelector('.metric-label')).toContainText('Topo-metric-1'); + expect(metricRowElements[0].querySelector('.metric-value')).toContainText('0'); - expect(metricRowElements[1].querySelector('.metric-label')).toContainText('Min. Metric4'); - expect(metricRowElements[1].querySelector('.metric-value')).toContainText('456'); + expect(metricRowElements[1].querySelector('.metric-label')).toContainText('Topo-metric-2'); + expect(metricRowElements[1].querySelector('.metric-value')).toContainText('234'); spectator.dispatchMouseEvent(getEdge(), 'mouseleave'); expect(spectator.query('.tooltip-container', { root: true })).not.toExist(); @@ -637,17 +753,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -664,16 +780,16 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 2', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -690,16 +806,16 @@ describe('Topology Widget renderer', () => { fromNode: mockResponse[0], toNode: mockResponse[1], data: { - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.LessThan5 }, - 'sum(metric3)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -718,7 +834,7 @@ describe('Topology Widget renderer', () => { // Can't use normal angular querying against svgs const getService = findNodeWithTypeAndName.bind(undefined, spectator, ObservabilityEntityType.Service, 'Service 1'); - const getEdge = findEdgeWithMetricValue.bind(undefined, spectator, 123); + const getEdge = findEdgeWithMetricValue.bind(undefined, spectator, 234); const getCloseButton = () => spectator.query('.hide-tooltip-button', { root: true })!; const getTooltip = () => spectator.query('.tooltip-container', { root: true })!; @@ -753,17 +869,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '1', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 1', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 123, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 234, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 345, health: MetricHealth.NotSpecified }, @@ -780,17 +896,17 @@ describe('Topology Widget renderer', () => { [entityIdKey]: '2', [entityTypeKey]: ObservabilityEntityType.Service, name: 'Service 2', - 'p99(duration)': { + 'avg(topo-metric-1)': { value: 234, health: MetricHealth.NotSpecified, category: PercentileLatencyMetricValueCategory.From100To500 }, - 'avg(errorCount)_avg(numCalls)': { + 'avg(topo-metric-2)': { value: 456, health: MetricHealth.NotSpecified, category: ErrorPercentageMetricValueCategory.GreaterThanOrEqualTo5 }, - 'avg(metric1)': { + 'avg(topo-metric-3)': { value: 567, health: MetricHealth.NotSpecified }, From f1865d86386cb64146f6e39c98078997fa0ca81d Mon Sep 17 00:00:00 2001 From: anandtiwary <52081890+anandtiwary@users.noreply.github.com> Date: Fri, 16 Jul 2021 15:32:12 -0700 Subject: [PATCH 5/5] refactor: fixing test --- .../topology-data-source.model.test.ts | 94 ++++++++++--------- 1 file changed, 50 insertions(+), 44 deletions(-) diff --git a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts index 86cfe87b9..c23e32803 100644 --- a/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts +++ b/projects/observability/src/shared/dashboard/data/graphql/topology/topology-data-source.model.test.ts @@ -1,9 +1,8 @@ -import { Color, isEqualIgnoreFunctions } from '@hypertrace/common'; +import { Color } from '@hypertrace/common'; import { GraphQlTimeRange, MetricAggregationType } from '@hypertrace/distributed-tracing'; import { GraphQlRequestCacheability, GraphQlRequestOptions } from '@hypertrace/graphql-client'; import { ModelApi } from '@hypertrace/hyperdash'; import { ObservabilityEntityType } from '../../../../graphql/model/schema/entity'; -import { ObservabilitySpecificationBuilder } from '../../../../graphql/request/builders/selections/observability-specification-builder'; import { ENTITY_TOPOLOGY_GQL_REQUEST, TopologyNodeSpecification @@ -15,7 +14,6 @@ import { TopologyMetricsModel } from './metrics/topology-metrics.model'; import { TopologyDataSourceModel } from './topology-data-source.model'; describe('topology data source model', () => { - const specBuilder = new ObservabilitySpecificationBuilder(); const testTimeRange = { startTime: new Date(1568907645141), endTime: new Date(1568911245141) }; let model!: TopologyDataSourceModel; let lastEmittedQuery: unknown; @@ -93,47 +91,55 @@ describe('topology data source model', () => { test('builds expected request', () => { model.getData(); - expect( - isEqualIgnoreFunctions(lastEmittedQuery, { - requestType: ENTITY_TOPOLOGY_GQL_REQUEST, - rootNodeType: ObservabilityEntityType.Service, - rootNodeSpecification: { - titleSpecification: specBuilder.attributeSpecificationForKey('name'), - metricSpecifications: [specBuilder.metricAggregationSpecForKey('numCalls', MetricAggregationType.Average)] - }, - rootNodeFilters: [], - rootNodeLimit: 100, - timeRange: new GraphQlTimeRange(testTimeRange.startTime, testTimeRange.endTime), - downstreamNodeSpecifications: new Map([ - [ - ObservabilityEntityType.Api, - { - titleSpecification: specBuilder.attributeSpecificationForKey('name'), - metricSpecifications: [specBuilder.metricAggregationSpecForKey('numCalls', MetricAggregationType.Average)] - } - ], - [ - ObservabilityEntityType.Backend, - { - titleSpecification: specBuilder.attributeSpecificationForKey('name'), - metricSpecifications: [specBuilder.metricAggregationSpecForKey('numCalls', MetricAggregationType.Average)] - } - ] - ]), - upstreamNodeSpecifications: new Map([ - [ - ObservabilityEntityType.Service, - { - titleSpecification: specBuilder.attributeSpecificationForKey('name'), - metricSpecifications: [specBuilder.metricAggregationSpecForKey('numCalls', MetricAggregationType.Average)] - } - ] - ]), - edgeSpecification: { - metricSpecifications: [specBuilder.metricAggregationSpecForKey('duration', MetricAggregationType.Average)] - } - }) - ).toBe(true); + 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: [], + rootNodeLimit: 100, + timeRange: new GraphQlTimeRange(testTimeRange.startTime, testTimeRange.endTime), + downstreamNodeSpecifications: new Map([ + [ + ObservabilityEntityType.Api, + { + titleSpecification: expect.objectContaining({ name: 'name' }), + metricSpecifications: [ + expect.objectContaining({ metric: 'numCalls', aggregation: MetricAggregationType.Average }) + ] + } + ], + [ + ObservabilityEntityType.Backend, + { + titleSpecification: expect.objectContaining({ name: 'name' }), + metricSpecifications: [ + expect.objectContaining({ metric: 'numCalls', aggregation: MetricAggregationType.Average }) + ] + } + ] + ]), + upstreamNodeSpecifications: new Map([ + [ + ObservabilityEntityType.Service, + { + titleSpecification: expect.objectContaining({ name: 'name' }), + metricSpecifications: [ + expect.objectContaining({ metric: 'numCalls', aggregation: MetricAggregationType.Average }) + ] + } + ] + ]), + edgeSpecification: { + metricSpecifications: [ + expect.objectContaining({ metric: 'duration', aggregation: MetricAggregationType.Average }) + ] + } + }); expect(lastEmittedQueryRequestOption).toEqual({ cacheability: GraphQlRequestCacheability.Cacheable,