diff --git a/config/sample-config.yaml b/config/sample-config.yaml index 80870395c..6047424e6 100644 --- a/config/sample-config.yaml +++ b/config/sample-config.yaml @@ -982,6 +982,126 @@ frontend: name: Flow RTT component: number hint: Specify a TCP smoothed Round Trip Time in nanoseconds. + scopes: + - id: cluster + name: Cluster + shortName: Cl + description: Cluster name or identifier + labels: + - K8S_ClusterName + feature: multiCluster + filter: cluster_name + stepInto: zone + - id: zone + name: Zone + shortName: AZ + description: Availability zone + labels: + - SrcK8S_Zone + - DstK8S_Zone + feature: zones + groups: + - clusters + filters: + - src_zone + - dst_zone + stepInto: host + - id: host + name: Node + description: Node on which the resources are running + labels: + - SrcK8S_HostName + - DstK8S_HostName + groups: + - clusters + - zones + - clusters+zones + filters: + - src_host_name + - dst_host_name + stepInto: resource + - id: namespace + name: Namespace + shortName: NS + description: Resource namespace + labels: + - SrcK8S_Namespace + - DstK8S_Namespace + groups: + - clusters + - clusters+zones + - clusters+hosts + - zones + - zones+hosts + - hosts + filters: + - src_namespace + - dst_namespace + stepInto: owner + - id: owner + name: Owner + shortName: Own + description: Controller owner, such as a Deployment + labels: + - SrcK8S_OwnerName + - SrcK8S_OwnerType + - DstK8S_OwnerName + - DstK8S_OwnerType + - SrcK8S_Namespace + - DstK8S_Namespace + groups: + - clusters + - clusters+zones + - clusters+hosts + - clusters+namespaces + - zones + - zones+hosts + - zones+namespaces + - hosts + - hosts+namespaces + - namespaces + filters: + - src_owner_name + - dst_owner_name + stepInto: resource + - id: resource + name: Resource + shortName: Res + description: Base resource, such as a Pod, a Service or a Node + labels: + - SrcK8S_Name + - SrcK8S_Type + - SrcK8S_OwnerName + - SrcK8S_OwnerType + - SrcK8S_Namespace + - SrcAddr + - SrcK8S_HostName + - DstK8S_Name + - DstK8S_Type + - DstK8S_OwnerName + - DstK8S_OwnerType + - DstK8S_Namespace + - DstAddr + - DstK8S_HostName + groups: + - clusters + - clusters+zones + - clusters+hosts + - clusters+namespaces + - clusters+owners + - zones + - zones+hosts + - zones+namespaces + - zones+owners + - hosts + - hosts+namespaces + - hosts+owners + - namespaces + - namespaces+owners + - owners + filters: + - src_resource + - dst_resource fields: - name: TimeFlowStartMs type: number diff --git a/pkg/config/config.go b/pkg/config/config.go index e64082a3e..c50de8ad9 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -77,7 +77,7 @@ type Column struct { Filter string `yaml:"filter,omitempty" json:"filter,omitempty"` Default bool `yaml:"default,omitempty" json:"default,omitempty"` Width int `yaml:"width,omitempty" json:"width,omitempty"` - Feature string `yaml:"feature" json:"feature"` + Feature string `yaml:"feature,omitempty" json:"feature,omitempty"` } type Filter struct { @@ -92,6 +92,19 @@ type Filter struct { Placeholder string `yaml:"placeholder,omitempty" json:"placeholder,omitempty"` } +type Scope struct { + ID string `yaml:"id" json:"id"` + Name string `yaml:"name" json:"name"` + ShortName string `yaml:"shortName" json:"shortName"` + Description string `yaml:"description" json:"description"` + Labels []string `yaml:"labels" json:"labels"` + Feature string `yaml:"feature,omitempty" json:"feature,omitempty"` + Groups []string `yaml:"groups,omitempty" json:"groups,omitempty"` + Filter string `yaml:"filter,omitempty" json:"filter,omitempty"` + Filters []string `yaml:"filters,omitempty" json:"filters,omitempty"` + StepInto string `yaml:"stepInto,omitempty" json:"stepInto,omitempty"` +} + type QuickFilter struct { Name string `yaml:"name" json:"name"` Filter map[string]string `yaml:"filter" json:"filter"` @@ -120,6 +133,7 @@ type Frontend struct { Panels []string `yaml:"panels" json:"panels"` Columns []Column `yaml:"columns" json:"columns"` Filters []Filter `yaml:"filters" json:"filters"` + Scopes []Scope `yaml:"scopes" json:"scopes"` QuickFilters []QuickFilter `yaml:"quickFilters" json:"quickFilters"` AlertNamespaces []string `yaml:"alertNamespaces" json:"alertNamespaces"` Sampling int `yaml:"sampling" json:"sampling"` @@ -166,7 +180,13 @@ func ReadFile(version, date, filename string) (*Config, error) { {ID: "SrcAddr", Name: "IP", Group: "Source", Field: "SrcAddr", Default: true, Width: 15}, {ID: "DstAddr", Name: "IP", Group: "Destination", Field: "DstAddr", Default: true, Width: 15}, }, - Filters: []Filter{}, + Filters: []Filter{}, + Scopes: []Scope{ + {ID: "host", Name: "Node", Labels: []string{"SrcK8S_HostName", "DstK8S_HostName"}}, + {ID: "namespace", Name: "Namespace", Labels: []string{"SrcK8S_Namespace", "DstK8S_Namespace"}}, + {ID: "owner", Name: "Owner", Labels: []string{"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"}}, + {ID: "resource", Name: "Resource", Labels: []string{"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"}}, + }, QuickFilters: []QuickFilter{}, Features: []string{}, Deduper: Deduper{ @@ -299,3 +319,16 @@ func (c *Config) GetAuthChecker() (auth.Checker, error) { } return auth.NewChecker(checkType, client.NewInCluster) } + +func (c *Frontend) GetAggregateKeyLabels() map[string][]string { + keyLabels := map[string][]string{ + "app": {"app"}, + "droppedState": {"PktDropLatestState"}, + "droppedCause": {"PktDropLatestDropCause"}, + "dnsRCode": {"DnsFlagsResponseCode"}, + } + for i := range c.Scopes { + keyLabels[c.Scopes[i].ID] = c.Scopes[i].Labels + } + return keyLabels +} diff --git a/pkg/handler/lokiclientmock/loki_client_mock.go b/pkg/handler/lokiclientmock/loki_client_mock.go index b101e6145..d8402cc79 100644 --- a/pkg/handler/lokiclientmock/loki_client_mock.go +++ b/pkg/handler/lokiclientmock/loki_client_mock.go @@ -37,7 +37,7 @@ func (o *LokiClientMock) Get(url string) ([]byte, int, error) { } else if strings.Contains(url, "by(K8S_ClusterName)") { path += "_cluster.json" } else if strings.Contains(url, "by(SrcK8S_Zone,DstK8S_Zone)") { - path += "zone.json" + path += "_zone.json" } else if strings.Contains(url, "by(SrcK8S_HostName,DstK8S_HostName)") { path += "_host.json" } else if strings.Contains(url, "by(SrcK8S_Namespace,DstK8S_Namespace)") { diff --git a/pkg/handler/topology.go b/pkg/handler/topology.go index 9c6d326a6..7843ffc3e 100644 --- a/pkg/handler/topology.go +++ b/pkg/handler/topology.go @@ -139,7 +139,7 @@ func (h *Handlers) extractTopologyQueryParams(params url.Values, ds constants.Da namespace, func(filters filters.SingleQuery) bool { // Do not expand if this is managed from prometheus - sr, _ := getEligiblePromMetric(h.PromInventory, filters, &in, namespace != "") + sr, _ := getEligiblePromMetric(h.Cfg.Frontend.GetAggregateKeyLabels(), h.PromInventory, filters, &in, namespace != "") return sr != nil && len(sr.Found) > 0 }, ) @@ -276,12 +276,12 @@ func buildTopologyQuery( qr *v1.Range, isDev bool, ) (string, *prometheus.Query, int, error) { - search, unsupportedReason := getEligiblePromMetric(promInventory, filters, in, isDev) + search, unsupportedReason := getEligiblePromMetric(cfg.Frontend.GetAggregateKeyLabels(), promInventory, filters, in, isDev) if unsupportedReason != "" { hlog.Debugf("Unsupported Prometheus query; reason: %s.", unsupportedReason) } else if search != nil && len(search.Found) > 0 { // Success, we can use Prometheus - qb := prometheus.NewQuery(in, qr, filters, search.Found) + qb := prometheus.NewQuery(cfg.Frontend.GetAggregateKeyLabels(), in, qr, filters, search.Found) q := qb.Build() return "", &q, http.StatusOK, nil } @@ -310,7 +310,7 @@ func buildTopologyQuery( "this request could not be performed with Prometheus metrics%s: it requires installing and enabling Loki", reason) } - qb, err := loki.NewTopologyQuery(&cfg.Loki, in) + qb, err := loki.NewTopologyQuery(&cfg.Loki, cfg.Frontend.GetAggregateKeyLabels(), in) if err != nil { return "", nil, http.StatusBadRequest, err } @@ -321,7 +321,7 @@ func buildTopologyQuery( return EncodeQuery(qb.Build()), nil, http.StatusOK, nil } -func getEligiblePromMetric(promInventory *prometheus.Inventory, filters filters.SingleQuery, in *loki.TopologyInput, isDev bool) (*prometheus.SearchResult, string) { +func getEligiblePromMetric(kl map[string][]string, promInventory *prometheus.Inventory, filters filters.SingleQuery, in *loki.TopologyInput, isDev bool) (*prometheus.SearchResult, string) { if in.DataSource != constants.DataSourceAuto && in.DataSource != constants.DataSourceProm { return nil, "" } @@ -332,7 +332,7 @@ func getEligiblePromMetric(promInventory *prometheus.Inventory, filters filters. return nil, fmt.Sprintf("RecordType not managed: %s", in.RecordType) } - labelsNeeded, _ := prometheus.GetLabelsAndFilter(in.Aggregate, in.Groups) + labelsNeeded, _ := prometheus.GetLabelsAndFilter(kl, in.Aggregate, in.Groups) fromFilters, unsupportedReason := prometheus.FiltersToLabels(filters) if unsupportedReason != "" { return nil, unsupportedReason diff --git a/pkg/loki/topology_query.go b/pkg/loki/topology_query.go index 3c046f22f..d90e3463c 100644 --- a/pkg/loki/topology_query.go +++ b/pkg/loki/topology_query.go @@ -13,28 +13,6 @@ const ( topologyDefaultLimit = "100" ) -var ( - aggregateKeyLabels = map[string][]string{ - "app": {"app"}, - "droppedState": {"PktDropLatestState"}, - "droppedCause": {"PktDropLatestDropCause"}, - "dnsRCode": {"DnsFlagsResponseCode"}, - "cluster": {"K8S_ClusterName"}, - "zone": {"SrcK8S_Zone", "DstK8S_Zone"}, - "host": {"SrcK8S_HostName", "DstK8S_HostName"}, - "namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"}, - "owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"}, - "resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"}, - } - groupKeyLabels = map[string][]string{ - "clusters": {"K8S_ClusterName"}, - "zones": {"SrcK8S_Zone", "DstK8S_Zone"}, - "hosts": {"SrcK8S_HostName", "DstK8S_HostName"}, - "namespaces": {"SrcK8S_Namespace", "DstK8S_Namespace"}, - "owners": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType"}, - } -) - type TopologyInput struct { Start string End string @@ -53,10 +31,11 @@ type TopologyInput struct { type TopologyQueryBuilder struct { *FlowQueryBuilder - topology *TopologyInput + topology *TopologyInput + aggregateKeyLabels map[string][]string } -func NewTopologyQuery(cfg *config.Loki, in *TopologyInput) (*TopologyQueryBuilder, error) { +func NewTopologyQuery(cfg *config.Loki, kl map[string][]string, in *TopologyInput) (*TopologyQueryBuilder, error) { var dedup bool var rt constants.RecordType if slices.Contains(constants.AnyConnectionType, string(in.RecordType)) { @@ -69,20 +48,21 @@ func NewTopologyQuery(cfg *config.Loki, in *TopologyInput) (*TopologyQueryBuilde fqb := NewFlowQueryBuilder(cfg, in.Start, in.End, in.Top, dedup, rt, in.PacketLoss) return &TopologyQueryBuilder{ - FlowQueryBuilder: fqb, - topology: in, + FlowQueryBuilder: fqb, + topology: in, + aggregateKeyLabels: kl, }, nil } -func GetLabelsAndFilter(aggregate, groups string) ([]string, string) { +func GetLabelsAndFilter(kl map[string][]string, aggregate, groups string) ([]string, string) { var fields []string var filter string - if fields = aggregateKeyLabels[aggregate]; fields == nil { + if fields = kl[aggregate]; fields == nil { fields = []string{aggregate} filter = aggregate } if groups != "" { - for gr, labels := range groupKeyLabels { + for gr, labels := range kl { if strings.Contains(groups, gr) { for _, label := range labels { if !slices.Contains(fields, label) { @@ -142,7 +122,7 @@ func (q *TopologyQueryBuilder) Build() string { top = topologyDefaultLimit } - labels, extraFilter := GetLabelsAndFilter(q.topology.Aggregate, q.topology.Groups) + labels, extraFilter := GetLabelsAndFilter(q.aggregateKeyLabels, q.topology.Aggregate, q.topology.Groups) if q.config.IsLabel(extraFilter) { extraFilter = "" } diff --git a/pkg/loki/topology_query_test.go b/pkg/loki/topology_query_test.go index 006daf955..cc2ac20cd 100644 --- a/pkg/loki/topology_query_test.go +++ b/pkg/loki/topology_query_test.go @@ -14,6 +14,19 @@ var lokiConfig = config.Loki{ Labels: []string{"SrcK8S_Namespace", "SrcK8S_OwnerName", "DstK8S_Namespace", "DstK8S_OwnerName", "FlowDirection"}, } +var aggregateKeyLabels = map[string][]string{ + "app": {"app"}, + "droppedState": {"PktDropLatestState"}, + "droppedCause": {"PktDropLatestDropCause"}, + "dnsRCode": {"DnsFlagsResponseCode"}, + "cluster": {"K8S_ClusterName"}, + "zone": {"SrcK8S_Zone", "DstK8S_Zone"}, + "host": {"SrcK8S_HostName", "DstK8S_HostName"}, + "namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"}, + "owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"}, + "resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"}, +} + func TestBuildTopologyQuery_SimpleAggregate(t *testing.T) { in := TopologyInput{ Start: "(start)", @@ -28,7 +41,7 @@ func TestBuildTopologyQuery_SimpleAggregate(t *testing.T) { Aggregate: "namespace", DedupMark: true, } - q, err := NewTopologyQuery(&lokiConfig, &in) + q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in) require.NoError(t, err) result := q.Build() assert.Equal( @@ -54,7 +67,7 @@ func TestBuildTopologyQuery_GroupsAndAggregate(t *testing.T) { Groups: "hosts", DedupMark: true, } - q, err := NewTopologyQuery(&lokiConfig, &in) + q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in) require.NoError(t, err) result := q.Build() assert.Equal( @@ -79,7 +92,7 @@ func TestBuildTopologyQuery_CustomAggregate(t *testing.T) { Aggregate: "SomeField", DedupMark: true, } - q, err := NewTopologyQuery(&lokiConfig, &in) + q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in) require.NoError(t, err) result := q.Build() assert.Equal( @@ -104,7 +117,7 @@ func TestBuildTopologyQuery_CustomLabelAggregate(t *testing.T) { Aggregate: "FlowDirection", DedupMark: true, } - q, err := NewTopologyQuery(&lokiConfig, &in) + q, err := NewTopologyQuery(&lokiConfig, aggregateKeyLabels, &in) require.NoError(t, err) result := q.Build() assert.Equal( diff --git a/pkg/prometheus/query.go b/pkg/prometheus/query.go index 68638ab59..bb5ad5ddf 100644 --- a/pkg/prometheus/query.go +++ b/pkg/prometheus/query.go @@ -10,10 +10,11 @@ import ( ) type QueryBuilder struct { - in *loki.TopologyInput - filters filters.SingleQuery - orMetrics []string - qRange v1.Range + aggregateKeyLabels map[string][]string + in *loki.TopologyInput + filters filters.SingleQuery + orMetrics []string + qRange v1.Range } type Query struct { @@ -21,17 +22,18 @@ type Query struct { PromQL string } -func NewQuery(in *loki.TopologyInput, qr *v1.Range, filters filters.SingleQuery, orMetrics []string) *QueryBuilder { +func NewQuery(kl map[string][]string, in *loki.TopologyInput, qr *v1.Range, filters filters.SingleQuery, orMetrics []string) *QueryBuilder { return &QueryBuilder{ - in: in, - filters: filters, - orMetrics: orMetrics, - qRange: *qr, + aggregateKeyLabels: kl, + in: in, + filters: filters, + orMetrics: orMetrics, + qRange: *qr, } } func (q *QueryBuilder) Build() Query { - labels, extraFilter := GetLabelsAndFilter(q.in.Aggregate, q.in.Groups) + labels, extraFilter := GetLabelsAndFilter(q.aggregateKeyLabels, q.in.Aggregate, q.in.Groups) if extraFilter != "" { q.filters = append(q.filters, filters.NewNotMatch(extraFilter, `""`)) } @@ -156,12 +158,12 @@ func appendFilteredMetric(sb *strings.Builder, metric string, filters filters.Si sb.WriteRune('}') } -func GetLabelsAndFilter(aggregate, groups string) ([]string, string) { +func GetLabelsAndFilter(kl map[string][]string, aggregate, groups string) ([]string, string) { if aggregate == "app" { // ignore app: it's a noop aggregation needed for Loki, not relevant in promQL return nil, "" } - return loki.GetLabelsAndFilter(aggregate, groups) + return loki.GetLabelsAndFilter(kl, aggregate, groups) } func QueryFilters(metric string, filters filters.SingleQuery) string { diff --git a/pkg/prometheus/query_test.go b/pkg/prometheus/query_test.go index a9cb64335..f3c96d336 100644 --- a/pkg/prometheus/query_test.go +++ b/pkg/prometheus/query_test.go @@ -14,6 +14,19 @@ import ( var qr = v1.Range{Start: time.Now().Add(-15 * time.Minute), End: time.Now(), Step: 30 * time.Second} +var kl = map[string][]string{ + "app": {"app"}, + "droppedState": {"PktDropLatestState"}, + "droppedCause": {"PktDropLatestDropCause"}, + "dnsRCode": {"DnsFlagsResponseCode"}, + "cluster": {"K8S_ClusterName"}, + "zone": {"SrcK8S_Zone", "DstK8S_Zone"}, + "host": {"SrcK8S_HostName", "DstK8S_HostName"}, + "namespace": {"SrcK8S_Namespace", "DstK8S_Namespace"}, + "owner": {"SrcK8S_OwnerName", "SrcK8S_OwnerType", "DstK8S_OwnerName", "DstK8S_OwnerType", "SrcK8S_Namespace", "DstK8S_Namespace"}, + "resource": {"SrcK8S_Name", "SrcK8S_Type", "SrcK8S_OwnerName", "SrcK8S_OwnerType", "SrcK8S_Namespace", "SrcAddr", "SrcK8S_HostName", "DstK8S_Name", "DstK8S_Type", "DstK8S_OwnerName", "DstK8S_OwnerType", "DstK8S_Namespace", "DstAddr", "DstK8S_HostName"}, +} + func TestBuildQuery_PromQLSimpleRateIgnoreApp(t *testing.T) { in := loki.TopologyInput{ Top: "50", @@ -25,7 +38,7 @@ func TestBuildQuery_PromQLSimpleRateIgnoreApp(t *testing.T) { Aggregate: "app", } f := filters.SingleQuery{} - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -45,7 +58,7 @@ func TestBuildQuery_PromQLSimpleRateNoFilter(t *testing.T) { Aggregate: "namespace", } f := filters.SingleQuery{} - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -70,7 +83,7 @@ func TestBuildQuery_PromQLSimpleRateAndFilter(t *testing.T) { Values: `"a"`, }, } - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -95,7 +108,7 @@ func TestBuildQuery_PromQLRateMultiFilter(t *testing.T) { Values: `"a","b"`, }, } - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -120,7 +133,7 @@ func TestBuildQuery_PromQLHistogramAverage(t *testing.T) { Values: `"a"`, }, } - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -145,7 +158,7 @@ func TestBuildQuery_PromQLHistogramP99(t *testing.T) { Values: `"a"`, }, } - q := NewQuery(&in, &qr, f, []string{"my_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"my_metric"}) result := q.Build() assert.Equal( t, @@ -165,7 +178,7 @@ func TestBuildQuery_PromQLByDNSResponseCode(t *testing.T) { Aggregate: "DnsFlagsResponseCode", } f := filters.SingleQuery{} - q := NewQuery(&in, &qr, f, []string{"netobserv_namespace_dns_latency_seconds_count"}) + q := NewQuery(kl, &in, &qr, f, []string{"netobserv_namespace_dns_latency_seconds_count"}) result := q.Build() assert.Equal( t, @@ -190,7 +203,7 @@ func TestBuildQuery_PromQLORMetrics(t *testing.T) { Values: `"a"`, }, } - q := NewQuery(&in, &qr, f, []string{"ingress_metric", "egress_metric"}) + q := NewQuery(kl, &in, &qr, f, []string{"ingress_metric", "egress_metric"}) result := q.Build() assert.Equal( t, diff --git a/pkg/server/server_test.go b/pkg/server/server_test.go index 457d83da0..3ca128909 100644 --- a/pkg/server/server_test.go +++ b/pkg/server/server_test.go @@ -336,6 +336,13 @@ func TestLokiConfigurationForTopology(t *testing.T) { authM := &authMock{} authM.MockGranted() + cfg, err := config.ReadFile("", "", "") + assert.Nil(t, err) + cfg.Frontend.Deduper = config.Deduper{ + Mark: true, + Merge: false, + } + // THAT is accessed behind the NOO console plugin backend backendRoutes := setupRoutes(context.TODO(), &config.Config{ Loki: config.Loki{ @@ -343,10 +350,7 @@ func TestLokiConfigurationForTopology(t *testing.T) { Timeout: config.Duration{Duration: time.Second}, Labels: []string{fields.SrcNamespace, fields.DstNamespace, fields.SrcOwnerName, fields.DstOwnerName, fields.SrcType, fields.DstType, fields.FlowDirection}, }, - Frontend: config.Frontend{Deduper: config.Deduper{ - Mark: true, - Merge: false, - }}, + Frontend: cfg.Frontend, }, authM) backendSvc := httptest.NewServer(backendRoutes) defer backendSvc.Close() @@ -394,6 +398,13 @@ func TestLokiConfigurationForTableHistogram(t *testing.T) { authM := &authMock{} authM.MockGranted() + cfg, err := config.ReadFile("", "", "") + assert.Nil(t, err) + cfg.Frontend.Deduper = config.Deduper{ + Mark: true, + Merge: false, + } + // THAT is accessed behind the NOO console plugin backend backendRoutes := setupRoutes(context.TODO(), &config.Config{ Loki: config.Loki{ @@ -401,10 +412,7 @@ func TestLokiConfigurationForTableHistogram(t *testing.T) { Timeout: config.Duration{Duration: time.Second}, Labels: []string{fields.SrcNamespace, fields.DstNamespace, fields.SrcOwnerName, fields.DstOwnerName, fields.SrcType, fields.DstType, fields.FlowDirection}, }, - Frontend: config.Frontend{Deduper: config.Deduper{ - Mark: true, - Merge: false, - }}, + Frontend: cfg.Frontend, }, authM) backendSvc := httptest.NewServer(backendRoutes) defer backendSvc.Close() diff --git a/web/locales/en/plugin__netobserv-plugin.json b/web/locales/en/plugin__netobserv-plugin.json index db26aec49..8462b37e7 100644 --- a/web/locales/en/plugin__netobserv-plugin.json +++ b/web/locales/en/plugin__netobserv-plugin.json @@ -6,10 +6,6 @@ "View alert details": "View alert details", "View health dashboard": "View health dashboard", "Name": "Name", - "Namespace": "Namespace", - "Node": "Node", - "Zone": "Zone", - "Cluster": "Cluster", "IP": "IP", "No information available for this content. Change scope to get more details.": "No information available for this content. Change scope to get more details.", "Cluster name": "Cluster name", @@ -38,6 +34,7 @@ "Add more filters or decrease limit / range to improve the query performance": "Add more filters or decrease limit / range to improve the query performance", "Unable to get {{item}}": "Unable to get {{item}}", "DNS Error": "DNS Error", + "Namespace": "Namespace", "Show related documentation": "Show related documentation", "Value": "Value", "Examples": "Examples", @@ -58,22 +55,6 @@ "JSON": "JSON", "Copy": "Copy", "Copied": "Copied", - "Clusters": "Clusters", - "Clusters + Nodes": "Clusters + Nodes", - "Clusters + Zones": "Clusters + Zones", - "Clusters + Namespaces": "Clusters + Namespaces", - "Clusters + Owners": "Clusters + Owners", - "Zones": "Zones", - "Zones + Nodes": "Zones + Nodes", - "Zones + Namespaces": "Zones + Namespaces", - "Zones + Owners": "Zones + Owners", - "Nodes": "Nodes", - "Nodes + Namespaces": "Nodes + Namespaces", - "Nodes + Owners": "Nodes + Owners", - "Namespaces": "Namespaces", - "Namespaces + Owners": "Namespaces + Owners", - "Owners": "Owners", - "None": "None", "3D": "3D", "BreadthFirst": "BreadthFirst", "Cola": "Cola", @@ -150,8 +131,6 @@ "1 hour": "1 hour", "2 hours": "2 hours", "1 day": "1 day", - "Owner": "Owner", - "Resource": "Resource", "Compact": "Compact", "Normal": "Normal", "Large": "Large", @@ -176,6 +155,7 @@ "M": "M", "S": "S", "XS": "XS", + "None": "None", "Step {{index}}/{{count}}": "Step {{index}}/{{count}}", "Step {{index}}/{{count}}_plural": "Step {{index}}/{{count}}", "Previous tip": "Previous tip", @@ -336,17 +316,6 @@ "Results": "Results", "Query summary": "Query summary", "Find in view": "Find in view", - "Res": "Res", - "Base resource, such as a Pod, a Service or a Node": "Base resource, such as a Pod, a Service or a Node", - "Own": "Own", - "Controller owner, such as a Deployment": "Controller owner, such as a Deployment", - "NS": "NS", - "Resource namespace": "Resource namespace", - "Node on which the resources are running": "Node on which the resources are running", - "AZ": "AZ", - "Availability zone": "Availability zone", - "Cl": "Cl", - "Cluster name or identifier": "Cluster name or identifier", "Show all graphs": "Show all graphs", "Focus on this graph": "Focus on this graph", "Show total": "Show total", diff --git a/web/src/api/loki.ts b/web/src/api/loki.ts index 5cd513f61..7a587d2e1 100644 --- a/web/src/api/loki.ts +++ b/web/src/api/loki.ts @@ -63,15 +63,16 @@ export interface NameAndType { export interface TopologyMetricPeer { id: string; addr?: string; - namespace?: string; owner?: NameAndType; resource?: NameAndType; - hostName?: string; - zone?: string; - clusterName?: string; resourceKind?: string; isAmbiguous: boolean; getDisplayName: (inclNamespace: boolean, disambiguate: boolean) => string | undefined; + // any FlowScope can appear here as optionnal field + [name: string]: unknown; + namespace?: string; + host?: string; + cluster?: string; } export type GenericMetric = { diff --git a/web/src/api/routes.ts b/web/src/api/routes.ts index 85072a600..dd295e0a7 100644 --- a/web/src/api/routes.ts +++ b/web/src/api/routes.ts @@ -169,6 +169,7 @@ export const getConfig = (): Promise => { : defaultConfig.portNaming.portNames }, filters: r.data.filters, + scopes: r.data.scopes, quickFilters: r.data.quickFilters, alertNamespaces: r.data.alertNamespaces, sampling: r.data.sampling, diff --git a/web/src/components/__tests-data__/scopes.ts b/web/src/components/__tests-data__/scopes.ts new file mode 100644 index 000000000..4320a0006 --- /dev/null +++ b/web/src/components/__tests-data__/scopes.ts @@ -0,0 +1,107 @@ +import { ScopeConfigDef } from '../../model/scope'; + +export const ScopeDefSample: ScopeConfigDef[] = [ + { + id: 'cluster', + name: 'Cluster', + shortName: 'Cl', + description: 'Cluster name or identifier', + labels: ['K8S_ClusterName'], + feature: 'multiCluster', + stepInto: 'zone' + }, + { + id: 'zone', + name: 'Zone', + shortName: 'AZ', + description: 'Availability zone', + labels: ['SrcK8S_Zone', 'DstK8S_Zone'], + feature: 'zones', + groups: ['clusters'], + stepInto: 'host' + }, + { + id: 'host', + name: 'Node', + shortName: 'Nd', + description: 'Node on which the resources are running', + labels: ['SrcK8S_HostName', 'DstK8S_HostName'], + groups: ['clusters', 'zones', 'clusters+zones'], + stepInto: 'resource' + }, + { + id: 'namespace', + name: 'Namespace', + shortName: 'NS', + description: 'Resource namespace', + labels: ['SrcK8S_Namespace', 'DstK8S_Namespace'], + groups: ['clusters', 'clusters+zones', 'clusters+hosts', 'zones', 'zones+hosts', 'hosts'], + stepInto: 'owner' + }, + { + id: 'owner', + name: 'Owner', + shortName: 'Own', + description: 'Controller owner, such as a Deployment', + labels: [ + 'SrcK8S_OwnerName', + 'SrcK8S_OwnerType', + 'DstK8S_OwnerName', + 'DstK8S_OwnerType', + 'SrcK8S_Namespace', + 'DstK8S_Namespace' + ], + groups: [ + 'clusters', + 'clusters+zones', + 'clusters+hosts', + 'clusters+namespaces', + 'zones', + 'zones+hosts', + 'zones+namespaces', + 'hosts', + 'hosts+namespaces', + 'namespaces' + ], + stepInto: 'resource' + }, + { + id: 'resource', + name: 'Resource', + shortName: 'Res', + description: 'Base resource, such as a Pod, a Service or a Node', + labels: [ + 'SrcK8S_Name', + 'SrcK8S_Type', + 'SrcK8S_OwnerName', + 'SrcK8S_OwnerType', + 'SrcK8S_Namespace', + 'SrcAddr', + 'SrcK8S_HostName', + 'DstK8S_Name', + 'DstK8S_Type', + 'DstK8S_OwnerName', + 'DstK8S_OwnerType', + 'DstK8S_Namespace', + 'DstAddr', + 'DstK8S_HostName' + ], + groups: [ + 'clusters', + 'clusters+zones', + 'clusters+hosts', + 'clusters+namespaces', + 'clusters+owners', + 'zones', + 'zones+hosts', + 'zones+namespaces', + 'zones+owners', + 'hosts', + 'hosts+namespaces', + 'hosts+owners', + 'namespaces', + 'namespaces+owners', + 'owners' + ] + } +]; diff --git a/web/src/components/drawer/element/element-fields.tsx b/web/src/components/drawer/element/element-fields.tsx index 6efc81819..ad0beabae 100644 --- a/web/src/components/drawer/element/element-fields.tsx +++ b/web/src/components/drawer/element/element-fields.tsx @@ -2,6 +2,7 @@ import { Text, TextContent, TextVariants } from '@patternfly/react-core'; import * as React from 'react'; import { useTranslation } from 'react-i18next'; import { Filter, FilterDefinition } from '../../../model/filters'; +import { getCustomScopes } from '../../../model/scope'; import { NodeData } from '../../../model/topology'; import { createPeer } from '../../../utils/metrics'; import { ElementField } from './element-field'; @@ -60,70 +61,26 @@ export const ElementFields: React.FC = ({ ); forceLabel = forceAsText = undefined; } - if (data.peer.namespace) { - fragments.push( - - ); - forceLabel = forceAsText = undefined; - } - if (data.peer.hostName) { - fragments.push( - - ); - forceLabel = forceAsText = undefined; - } - if (data.peer.zone) { - fragments.push( - - ); - forceLabel = forceAsText = undefined; - } - if (data.peer.clusterName) { - fragments.push( - - ); - forceLabel = forceAsText = undefined; - } + // add available fields from custom scopes + getCustomScopes().forEach(sc => { + const value = data.peer[sc.id] as string | undefined; + if (value) { + fragments.push( + + ); + forceLabel = forceAsText = undefined; + } + }); if (data.peer.addr) { fragments.push( = ({ const clusterName = React.useCallback( (d: NodeData) => { - if (!d.peer.clusterName) { + if (!d.peer.cluster) { return <>; } - const fields = createPeer({ clusterName: d.peer.clusterName }); + const fields = createPeer({ cluster: d.peer.cluster }); const isFiltered = isElementFiltered(fields, filters, filterDefinitions); return ( {t('Cluster name')} - {d.peer.clusterName} + {d.peer.cluster}