From e6d69fdc1c6f254c4b1e46f955f5eaf54788bee7 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Wed, 21 Jan 2026 11:03:09 +0100 Subject: [PATCH 01/22] feat: bytter til Mimir service for Prometheus Co-authored-by: Carl Hedgren --- internal/thirdparty/promclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index ea1745fd1..1127a977f 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -51,7 +51,7 @@ func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, proms := map[string]promv1.API{} for _, cluster := range clusters { - client, err := api.NewClient(api.Config{Address: fmt.Sprintf("https://prometheus.%s.%s.cloud.nais.io", cluster, tenant)}) + client, err := api.NewClient(api.Config{Address: fmt.Sprintf("http://mimir-query-frontend", cluster, tenant)}) if err != nil { return nil, err } From 354fdb88d7156d6b490892dba05755850c778942 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Wed, 21 Jan 2026 12:00:40 +0100 Subject: [PATCH 02/22] fix: bruker strengen direkte --- internal/thirdparty/promclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 1127a977f..2d4091364 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -51,7 +51,7 @@ func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, proms := map[string]promv1.API{} for _, cluster := range clusters { - client, err := api.NewClient(api.Config{Address: fmt.Sprintf("http://mimir-query-frontend", cluster, tenant)}) + client, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) if err != nil { return nil, err } From 57e37b9000245f7c24419955a8529701fe0cd583 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 12:58:46 +0100 Subject: [PATCH 03/22] add more clients eventually we can :hohco: the old one Co-authored-by: Kyrre Havik --- internal/thirdparty/promclient/client.go | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 2d4091364..e687b9a84 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -10,6 +10,7 @@ import ( "github.com/prometheus/client_golang/api" promv1 "github.com/prometheus/client_golang/api/prometheus/v1" prom "github.com/prometheus/common/model" + "github.com/prometheus/prometheus/rules" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" ) @@ -44,14 +45,16 @@ func WithTime(t time.Time) QueryOption { type RealClient struct { prometheuses map[string]promv1.API + mimirMetrics promv1.API + mimirAlerts promv1.API log logrus.FieldLogger } func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { proms := map[string]promv1.API{} - for _, cluster := range clusters { - client, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) + client, err := api.NewClient(api.Config{Address: fmt.Sprintf("https://prometheus.%s.%s.cloud.nais.io", cluster, tenant)}) + if err != nil { return nil, err } @@ -59,8 +62,20 @@ func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, proms[cluster] = promv1.NewAPI(client) } + mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) + if err != nil { + return nil, err + } + + mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler"}) + if err != nil { + return nil, err + } + return &RealClient{ prometheuses: proms, + mimirMetrics: promv1.NewAPI(mimirMetrics), + mimirAlerts: promv1.NewAPI(mimirAlerts), log: log, }, nil } From 0e659c9324d57f849b65078573c34341ee700e5f Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 13:11:10 +0100 Subject: [PATCH 04/22] update queries Co-authored-by: Kyrre Havik --- internal/utilization/model.go | 8 ++--- internal/utilization/queries.go | 56 ++++++++++++++++----------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/internal/utilization/model.go b/internal/utilization/model.go index 65a39d2e9..b562f219c 100644 --- a/internal/utilization/model.go +++ b/internal/utilization/model.go @@ -133,7 +133,7 @@ type WorkloadUtilizationRecommendations struct { } func (w WorkloadUtilizationRecommendations) CPURequestCores(ctx context.Context) (float64, error) { - v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(cpuRequestRecommendation, w.workloadName, w.teamSlug, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) + v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(cpuRequestRecommendation, w.environmentName, w.teamSlug, w.workloadName, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) if err != nil { return 0, err } @@ -143,8 +143,8 @@ func (w WorkloadUtilizationRecommendations) CPURequestCores(ctx context.Context) return math.Max(cpuReq, minCPURequest), nil } -func (w WorkloadUtilizationRecommendations) MemoryRequestBytes(ctx context.Context) (int64, error) { - v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(memoryRequestRecommendation, w.workloadName, w.teamSlug, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) +func (w WorkloadUtilizationRecommendations) MemoryRequestBytes(ctx context.Context, env string) (int64, error) { + v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(memoryRequestRecommendation, w.environmentName, w.teamSlug, w.workloadName, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) if err != nil { return 0, err } @@ -154,7 +154,7 @@ func (w WorkloadUtilizationRecommendations) MemoryRequestBytes(ctx context.Conte } func (w WorkloadUtilizationRecommendations) MemoryLimitBytes(ctx context.Context) (int64, error) { - v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(memoryLimitRecommendation, w.workloadName, w.teamSlug, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) + v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(memoryLimitRecommendation, w.environmentName, w.teamSlug, w.workloadName, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) if err != nil { return 0, err } diff --git a/internal/utilization/queries.go b/internal/utilization/queries.go index 26814c457..1f7b3f28a 100644 --- a/internal/utilization/queries.go +++ b/internal/utilization/queries.go @@ -17,35 +17,35 @@ import ( ) const ( - appCPULimit = `max by (container, namespace) (kube_pod_container_resource_limits{namespace=%q, container=%q, resource="cpu", unit="core"})` - appCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{namespace=%q, container=%q, resource="cpu",unit="core"})` - appCPUUsage = `rate(container_cpu_usage_seconds_total{namespace=%q, container=%q}[5m])` - appCPUUsageAvg = `avg by (container, namespace) (rate(container_cpu_usage_seconds_total{namespace=%q, container=%q}[5m]))` - appMemoryLimit = `max by (container, namespace) (kube_pod_container_resource_limits{namespace=%q, container=%q, resource="memory", unit="byte"})` - appMemoryRequest = `max by (container, namespace) (kube_pod_container_resource_requests{namespace=%q, container=%q, resource="memory",unit="byte"})` - appMemoryUsage = `last_over_time(container_memory_working_set_bytes{namespace=%q, container=%q}[5m])` - appMemoryUsageAvg = `avg by (container, namespace) (last_over_time(container_memory_working_set_bytes{namespace=%q, container=%q}[5m]))` - instanceCPUUsage = `rate(container_cpu_usage_seconds_total{namespace=%q, container=%q, pod=%q}[5m])` - instanceMemoryUsage = `last_over_time(container_memory_working_set_bytes{namespace=%q, container=%q, pod=%q}[5m])` - teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` - teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPURequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPUUsage = `sum by (namespace, owner_kind) (rate(container_cpu_usage_seconds_total{namespace!~%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryRequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryUsage = `sum by (namespace, owner_kind) (container_memory_working_set_bytes{namespace!~%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + appCPULimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q,namespace=%q, container=%q, resource="cpu", unit="core"})` + appCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu",unit="core"})` + appCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` + appCPUUsageAvg = `avg by (container, namespace) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` + appMemoryLimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q, namespace=%q, container=%q, resource="memory", unit="byte"})` + appMemoryRequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="memory",unit="byte"})` + appMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` + appMemoryUsageAvg = `avg by (container, namespace) (last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` + instanceCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` + instanceMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` + teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` + teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsCPURequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsCPUUsage = `sum by (namespace, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace!~%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsMemoryRequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsMemoryUsage = `sum by (namespace, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace!~%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` cpuRequestRecommendation = `max( avg_over_time( - rate(container_cpu_usage_seconds_total{container=%q,namespace=%q}[5m])[1w:5m] + rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q,namespace=%q, container=%q}[5m])[1w:5m] ) and on () (hour() >= %d and hour() < %d and day_of_week() > 0 and day_of_week() < 6) )` memoryRequestRecommendation = `max( avg_over_time( - quantile_over_time(0.8, container_memory_working_set_bytes{container=%q,namespace=%q}[5m])[1w:5m] + quantile_over_time(0.8, container_memory_working_set_bytes{k8s_cluster_name=%q,namespace=%q,container=%q}[5m])[1w:5m] ) and on () time() >= (hour() >= %d and hour() < %d and day_of_week() > 0 and day_of_week() < 6) @@ -54,7 +54,7 @@ const ( max_over_time( quantile_over_time( 0.95, - container_memory_working_set_bytes{container=%q,namespace=%q}[5m] + container_memory_working_set_bytes{k8s_cluster_name=%q,namespace=%q, container=%q}[5m] )[1w:5m] ) and on () @@ -204,7 +204,7 @@ func WorkloadResourceRequest(ctx context.Context, env string, teamSlug slug.Slug c := fromContext(ctx).client - v, err := c.Query(ctx, env, fmt.Sprintf(q, teamSlug, workloadName)) + v, err := c.Query(ctx, env, fmt.Sprintf(q, env, teamSlug, workloadName)) if err != nil { return 0, err } @@ -219,7 +219,7 @@ func WorkloadResourceLimit(ctx context.Context, env string, teamSlug slug.Slug, c := fromContext(ctx).client - v, err := c.Query(ctx, env, fmt.Sprintf(q, teamSlug, workloadName)) + v, err := c.Query(ctx, env, fmt.Sprintf(q, env, teamSlug, workloadName)) if err != nil { return nil, err } @@ -239,7 +239,7 @@ func WorkloadResourceUsage(ctx context.Context, env string, teamSlug slug.Slug, c := fromContext(ctx).client - v, err := c.Query(ctx, env, fmt.Sprintf(q, teamSlug, workloadName)) + v, err := c.Query(ctx, env, fmt.Sprintf(q, env, teamSlug, workloadName)) if err != nil { return 0, err } @@ -247,20 +247,20 @@ func WorkloadResourceUsage(ctx context.Context, env string, teamSlug slug.Slug, return ensuredVal(v), nil } -func queryPrometheusRange(ctx context.Context, environmentName string, teamSlug slug.Slug, workloadName string, queryTemplate string, start time.Time, end time.Time, step int) ([]*UtilizationSample, error) { +func queryPrometheusRange(ctx context.Context, env string, teamSlug slug.Slug, workloadName string, queryTemplate string, start time.Time, end time.Time, step int) ([]*UtilizationSample, error) { c := fromContext(ctx).client // Format the query - query := fmt.Sprintf(queryTemplate, teamSlug, workloadName) + query := fmt.Sprintf(queryTemplate, env, teamSlug, workloadName) // Perform the query - v, warnings, err := c.QueryRange(ctx, environmentName, query, promv1.Range{Start: start, End: end, Step: time.Duration(step) * time.Second}) + v, warnings, err := c.QueryRange(ctx, env, query, promv1.Range{Start: start, End: end, Step: time.Duration(step) * time.Second}) if err != nil { return nil, err } if len(warnings) > 0 { fromContext(ctx).log.WithFields(logrus.Fields{ - "environment": environmentName, + "environment": env, "warnings": strings.Join(warnings, ", "), }).Warn("prometheus query warnings") } From 52350d4bb446a1f06ab9234208b6b8d1bebd37b6 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 13:36:51 +0100 Subject: [PATCH 05/22] queries are now over mimirs Co-authored-by: Kyrre Havik --- internal/thirdparty/promclient/client.go | 40 ++++++------------------ 1 file changed, 9 insertions(+), 31 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index e687b9a84..3f7de1d50 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -44,24 +44,12 @@ func WithTime(t time.Time) QueryOption { } type RealClient struct { - prometheuses map[string]promv1.API mimirMetrics promv1.API - mimirAlerts promv1.API + mimirRules promv1.API log logrus.FieldLogger } -func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { - proms := map[string]promv1.API{} - for _, cluster := range clusters { - client, err := api.NewClient(api.Config{Address: fmt.Sprintf("https://prometheus.%s.%s.cloud.nais.io", cluster, tenant)}) - - if err != nil { - return nil, err - } - - proms[cluster] = promv1.NewAPI(client) - } - +func New(tenant string, log logrus.FieldLogger) (*RealClient, error) { mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) if err != nil { return nil, err @@ -73,9 +61,8 @@ func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, } return &RealClient{ - prometheuses: proms, mimirMetrics: promv1.NewAPI(mimirMetrics), - mimirAlerts: promv1.NewAPI(mimirAlerts), + mimirRules: promv1.NewAPI(mimirAlerts), log: log, }, nil } @@ -87,7 +74,7 @@ func (c *RealClient) QueryAll(ctx context.Context, query string, opts ...QueryOp } wg := pool.NewWithResults[*result]().WithContext(ctx) - for env := range c.prometheuses { + for _, env := range []string{"dev"} { wg.Go(func(ctx context.Context) (*result, error) { v, err := c.Query(ctx, env, query, opts...) if err != nil { @@ -112,10 +99,7 @@ func (c *RealClient) QueryAll(ctx context.Context, query string, opts ...QueryOp } func (c *RealClient) Query(ctx context.Context, environmentName string, query string, opts ...QueryOption) (prom.Vector, error) { - client, ok := c.prometheuses[environmentName] - if !ok { - return nil, fmt.Errorf("no prometheus client for environment %s", environmentName) - } + client := c.mimirMetrics opt := &QueryOpts{ Time: time.Now().Add(-5 * time.Minute), @@ -145,19 +129,13 @@ func (c *RealClient) Query(ctx context.Context, environmentName string, query st } func (c *RealClient) QueryRange(ctx context.Context, environment string, query string, promRange promv1.Range) (prom.Value, promv1.Warnings, error) { - client, ok := c.prometheuses[environment] - if !ok { - return nil, nil, fmt.Errorf("no prometheus client for environment %s", environment) - } - + client := c.mimirMetrics return client.QueryRange(ctx, query, promRange) } func (c *RealClient) Rules(ctx context.Context, environment string, teamSlug slug.Slug) (promv1.RulesResult, error) { - api, ok := c.prometheuses[environment] - if !ok { - return promv1.RulesResult{}, fmt.Errorf("no prometheus client for environment %s", environment) - } + api := c.mimirRules + res, err := api.Rules(ctx) if err != nil { return promv1.RulesResult{}, err @@ -175,7 +153,7 @@ func (c *RealClient) RulesAll(ctx context.Context, teamSlug slug.Slug) (map[stri } wg := pool.NewWithResults[*item]().WithContext(ctx) - for env := range c.prometheuses { + for _, env := range []string{"dev"} { wg.Go(func(ctx context.Context) (*item, error) { res, err := c.Rules(ctx, env, teamSlug) if err != nil { From 2f9382ceb6921a34a8fe1a80313a5ab9a75f69d0 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 13:42:34 +0100 Subject: [PATCH 06/22] :hocho: unused Co-authored-by: Kyrre Havik --- internal/thirdparty/promclient/client.go | 1 - 1 file changed, 1 deletion(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 3f7de1d50..4533981dc 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -10,7 +10,6 @@ import ( "github.com/prometheus/client_golang/api" promv1 "github.com/prometheus/client_golang/api/prometheus/v1" prom "github.com/prometheus/common/model" - "github.com/prometheus/prometheus/rules" "github.com/sirupsen/logrus" "github.com/sourcegraph/conc/pool" ) From 95318942c87387f4bf2ba10ed534d9fd209aa36b Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 13:59:52 +0100 Subject: [PATCH 07/22] :hocho: unuseds Co-authored-by: Kyrre Havik --- internal/utilization/model.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/utilization/model.go b/internal/utilization/model.go index b562f219c..fc12479c0 100644 --- a/internal/utilization/model.go +++ b/internal/utilization/model.go @@ -143,7 +143,7 @@ func (w WorkloadUtilizationRecommendations) CPURequestCores(ctx context.Context) return math.Max(cpuReq, minCPURequest), nil } -func (w WorkloadUtilizationRecommendations) MemoryRequestBytes(ctx context.Context, env string) (int64, error) { +func (w WorkloadUtilizationRecommendations) MemoryRequestBytes(ctx context.Context) (int64, error) { v, err := w.client.Query(ctx, w.environmentName, fmt.Sprintf(memoryRequestRecommendation, w.environmentName, w.teamSlug, w.workloadName, w.start.Hour(), w.start.Add(time.Hour*12).Hour())) if err != nil { return 0, err From e8d40c83d0f11b9578f2988869a5cfa5977b1866 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 14:02:30 +0100 Subject: [PATCH 08/22] ci Co-authored-by: Kyrre Havik --- internal/thirdparty/promclient/client.go | 2 +- internal/utilization/model.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 4533981dc..15d83ccb2 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -48,7 +48,7 @@ type RealClient struct { log logrus.FieldLogger } -func New(tenant string, log logrus.FieldLogger) (*RealClient, error) { +func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) if err != nil { return nil, err diff --git a/internal/utilization/model.go b/internal/utilization/model.go index fc12479c0..c55cdd322 100644 --- a/internal/utilization/model.go +++ b/internal/utilization/model.go @@ -42,7 +42,7 @@ func (e UtilizationResourceType) String() string { return string(e) } -func (e *UtilizationResourceType) UnmarshalGQL(v interface{}) error { +func (e *UtilizationResourceType) UnmarshalGQL(v any) error { str, ok := v.(string) if !ok { return fmt.Errorf("enums must be strings") From 9d256b229990abd928649327108849995546ecd8 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Wed, 21 Jan 2026 14:56:35 +0100 Subject: [PATCH 09/22] service is 8080 --- internal/thirdparty/promclient/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 15d83ccb2..5397ef1d4 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -49,12 +49,12 @@ type RealClient struct { } func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { - mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend"}) + mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080"}) if err != nil { return nil, err } - mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler"}) + mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080"}) if err != nil { return nil, err } From a9728aff1a7ab8a77ad2a60d8e8279357977dc94 Mon Sep 17 00:00:00 2001 From: carl hedgren Date: Thu, 22 Jan 2026 10:10:30 +0100 Subject: [PATCH 10/22] use correct path this shouldn't fix anything --- internal/thirdparty/promclient/client.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 5397ef1d4..faa58089d 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -49,12 +49,12 @@ type RealClient struct { } func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { - mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080"}) + mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080/prometheus"}) if err != nil { return nil, err } - mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080"}) + mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080/prometheus"}) if err != nil { return nil, err } From ae0654aa80684f06a3652e9e0a62bf277cfe83d7 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 10:16:09 +0100 Subject: [PATCH 11/22] fix: manglet orgid som header for Mimir Co-authored-by: Terje Sannum Co-authored-by: Carl Hedgren --- internal/thirdparty/promclient/client.go | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index faa58089d..88603684e 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -3,6 +3,7 @@ package promclient import ( "context" "fmt" + "net/http" "strings" "time" @@ -48,13 +49,22 @@ type RealClient struct { log logrus.FieldLogger } +type mimirRoundTrip struct{} + +func (r mimirRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) { + req.Header.Set("X--Scope-OrgID", "nais") + return http.DefaultTransport.RoundTrip(req) +} + func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { - mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080/prometheus"}) + roundTrip := mimirRoundTrip{} + + mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080/prometheus", RoundTripper: roundTrip}) if err != nil { return nil, err } - mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080/prometheus"}) + mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080/prometheus", RoundTripper: roundTrip}) if err != nil { return nil, err } From dd0b7ee29a8455144c23c70043f0ce094050fda4 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 10:29:23 +0100 Subject: [PATCH 12/22] fix: feil headernavn Co-authored-by: Terje Sannum Co-authored-by: Carl Hedgren --- internal/thirdparty/promclient/client.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 88603684e..5eb5ecc64 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -52,7 +52,7 @@ type RealClient struct { type mimirRoundTrip struct{} func (r mimirRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set("X--Scope-OrgID", "nais") + req.Header.Set("X-Scope-OrgID", "nais") return http.DefaultTransport.RoundTrip(req) } From 746391746787762a49a9235cedfa1943a061b22c Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 10:50:35 +0100 Subject: [PATCH 13/22] =?UTF-8?q?fix:=20teamsqueries=20sender=20med=20milj?= =?UTF-8?q?=C3=B8,=20frontend=20tar=20seg=20av=20sortering?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Terje Sannum Co-authored-by: Carl Hedgren --- internal/utilization/queries.go | 24 +++++++++++++----------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/internal/utilization/queries.go b/internal/utilization/queries.go index 1f7b3f28a..ea42f1fde 100644 --- a/internal/utilization/queries.go +++ b/internal/utilization/queries.go @@ -17,7 +17,7 @@ import ( ) const ( - appCPULimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q,namespace=%q, container=%q, resource="cpu", unit="core"})` + appCPULimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu", unit="core"})` appCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu",unit="core"})` appCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` appCPUUsageAvg = `avg by (container, namespace) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` @@ -27,14 +27,16 @@ const ( appMemoryUsageAvg = `avg by (container, namespace) (last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` instanceCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` instanceMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` - teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` - teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPURequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPUUsage = `sum by (namespace, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace!~%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryRequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryUsage = `sum by (namespace, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace!~%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + + teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` + teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + + teamsCPURequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsCPUUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (rate(container_cpu_usage_seconds_total{namespace!~%q, container!~%q}[5m]) * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsMemoryRequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + teamsMemoryUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (container_memory_working_set_bytes{namespace!~%q, container!~%q} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` cpuRequestRecommendation = `max( avg_over_time( @@ -66,8 +68,8 @@ const ( ) var ( - ignoredContainers = strings.Join([]string{"elector", "linkerd-proxy", "cloudsql-proxy", "secure-logs-fluentd", "secure-logs-configmap-reload", "secure-logs-fluentbit", "wonderwall", "vks-sidecar"}, "|") + "||" // Adding "||" to the query filters data without container - ignoredNamespaces = strings.Join([]string{"kube-system", "nais-system", "cnrm-system", "configconnector-operator-system", "linkerd", "gke-mcs", "gke-managed-system", "kyverno", "default", "kube-node-lease", "kube-public"}, "|") + ignoredContainers = strings.Join([]string{"elector", "cloudsql-proxy", "secure-logs-fluentd", "secure-logs-configmap-reload", "secure-logs-fluentbit", "wonderwall", "vks-sidecar"}, "|") + "||" // Adding "||" to the query filters data without container + ignoredNamespaces = strings.Join([]string{"kube-system", "nais-system", "cnrm-system", "configconnector-operator-system", "gke-mcs", "gke-managed-system", "kyverno", "default", "kube-node-lease", "kube-public"}, "|") ) func ForInstance(ctx context.Context, env string, teamSlug slug.Slug, workloadName string, instanceName string, resourceType UtilizationResourceType) (*ApplicationInstanceUtilization, error) { From ff9610b6d321168edeb0408f82055fc9a68581b0 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 14:18:45 +0100 Subject: [PATCH 14/22] feat: ikke bruk egne queries, testene blir da feil over tid Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- .../thirdparty/promclient/fake/fake_test.go | 45 ++++++---------- internal/utilization/queries.go | 52 +++++++++---------- 2 files changed, 41 insertions(+), 56 deletions(-) diff --git a/internal/thirdparty/promclient/fake/fake_test.go b/internal/thirdparty/promclient/fake/fake_test.go index cf0e300e7..20d5c3aca 100644 --- a/internal/thirdparty/promclient/fake/fake_test.go +++ b/internal/thirdparty/promclient/fake/fake_test.go @@ -27,21 +27,6 @@ import ( "github.com/testcontainers/testcontainers-go/modules/postgres" ) -const ( - appCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{namespace=%q, container=%q, resource="cpu",unit="core"})` - appCPUUsage = `rate(container_cpu_usage_seconds_total{namespace=%q, container=%q}[5m])` - appMemoryRequest = `max by (container, namespace) (kube_pod_container_resource_requests{namespace=%q, container=%q, resource="memory",unit="byte"})` - appMemoryUsage = `last_over_time(container_memory_working_set_bytes{namespace=%q, container=%q}[5m])` - teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` - teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPURequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPUUsage = `sum by (namespace, owner_kind) (rate(container_cpu_usage_seconds_total{namespace!~%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryRequest = `sum by (namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryUsage = `sum by (namespace, owner_kind) (container_memory_working_set_bytes{namespace!~%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` -) - func TestFakeQuery(t *testing.T) { now := func() prom.Time { return prom.TimeFromUnix(100000) @@ -55,28 +40,28 @@ func TestFakeQuery(t *testing.T) { expected prom.Vector }{ "appCPURequest": { - query: appCPURequest, + query: utilization.AppCPURequest, args: []any{"team", "workload"}, expected: prom.Vector{ {Metric: prom.Metric{"container": "workload", "namespace": "team"}, Value: 0.15658213673311283, Timestamp: now()}, }, }, "appCPUUsage": { - query: appCPUUsage, + query: utilization.AppCPUUsage, args: []any{"team", "workload"}, expected: prom.Vector{ {Metric: prom.Metric{"container": "workload", "namespace": "team", "pod": "workload-1"}, Value: 0.15658213673311283, Timestamp: now()}, }, }, "appMemoryRequest": { - query: appMemoryRequest, + query: utilization.AppMemoryRequest, args: []any{"team", "workload"}, expected: prom.Vector{ {Metric: prom.Metric{"container": "workload", "namespace": "team"}, Value: 105283867, Timestamp: now()}, }, }, "appMemoryUsage": { - query: appMemoryUsage, + query: utilization.AppMemoryUsage, args: []any{"team", "workload"}, expected: prom.Vector{ {Metric: prom.Metric{"container": "workload", "namespace": "team", "pod": "workload-1"}, Value: 105283867, Timestamp: now()}, @@ -111,7 +96,7 @@ func TestFakeQueryAll(t *testing.T) { expected map[string]prom.Vector }{ "teamsMemoryRequest": { - query: teamsMemoryRequest, + query: utilization.TeamsMemoryRequest, args: []any{"", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}}, @@ -119,7 +104,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamsMemoryUsage": { - query: teamsMemoryUsage, + query: utilization.TeamsMemoryUsage, args: []any{"", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}}, @@ -127,7 +112,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamsCPURequest": { - query: teamsCPURequest, + query: utilization.TeamsCPURequest, args: []any{"", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}}, @@ -135,7 +120,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamsCPUUsage": { - query: teamsCPUUsage, + query: utilization.TeamsCPUUsage, args: []any{"", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}}, @@ -143,7 +128,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamMemoryRequest": { - query: teamMemoryRequest, + query: utilization.TeamMemoryRequest, args: []any{"team1", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"container": "app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 910654684, Timestamp: now()}}, @@ -151,7 +136,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamMemoryUsage": { - query: teamMemoryUsage, + query: utilization.TeamMemoryUsage, args: []any{"team2", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 910654684, Timestamp: now()}}, @@ -159,7 +144,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamCPURequest": { - query: teamCPURequest, + query: utilization.TeamCPURequest, args: []any{"team2", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}}, @@ -167,7 +152,7 @@ func TestFakeQueryAll(t *testing.T) { }, }, "teamCPUUsage": { - query: teamCPUUsage, + query: utilization.TeamCPUUsage, args: []any{"team1", ""}, expected: map[string]prom.Vector{ "dev": {&prom.Sample{Metric: prom.Metric{"container": "app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}}, @@ -250,7 +235,7 @@ func TestFakeQueryRange(t *testing.T) { expected prom.Matrix }{ "appMemoryUsage": { - query: appMemoryUsage, + query: utilization.AppMemoryUsage, rng: promv1.Range{Start: start.Add(-5 * time.Minute), End: start, Step: 15 * time.Second}, args: []any{"team1", "workload1"}, expected: prom.Matrix{ @@ -282,12 +267,12 @@ func TestFakeQueryRange(t *testing.T) { }, }, "appCPUUsage": { - query: appCPUUsage, + query: utilization.AppCPUUsage, rng: promv1.Range{Start: start.Add(-5 * time.Minute), End: start, Step: 15 * time.Second}, args: []any{"team1", "workload1"}, expected: prom.Matrix{ { - Metric: prom.Metric{"container": "workload1", "namespace": "team1", "pod": "workload1-1"}, + Metric: prom.Metric{"container": "workload1", "namespace": "team1", "k8s_cluster_name": "test", "pod": "workload1-1"}, Values: []prom.SamplePair{ {Value: 0.6805719573212468, Timestamp: 1609458900 * 1000}, {Value: 1.8199158760450043, Timestamp: 1609458915 * 1000}, diff --git a/internal/utilization/queries.go b/internal/utilization/queries.go index ea42f1fde..f99073b3e 100644 --- a/internal/utilization/queries.go +++ b/internal/utilization/queries.go @@ -18,25 +18,25 @@ import ( const ( appCPULimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu", unit="core"})` - appCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu",unit="core"})` - appCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` + AppCPURequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="cpu",unit="core"})` + AppCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` appCPUUsageAvg = `avg by (container, namespace) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` appMemoryLimit = `max by (container, namespace) (kube_pod_container_resource_limits{k8s_cluster_name=%q, namespace=%q, container=%q, resource="memory", unit="byte"})` - appMemoryRequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="memory",unit="byte"})` - appMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` + AppMemoryRequest = `max by (container, namespace) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container=%q, resource="memory",unit="byte"})` + AppMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m])` appMemoryUsageAvg = `avg by (container, namespace) (last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q}[5m]))` instanceCPUUsage = `rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` instanceMemoryUsage = `last_over_time(container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container=%q, pod=%q}[5m])` - teamCPURequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamCPUUsage = `sum by (container, owner_kind) (rate(container_cpu_usage_seconds_total{k8s_cluster_name=%q, namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` - teamMemoryRequest = `sum by (container, owner_kind) (kube_pod_container_resource_requests{k8s_cluster_name=%q, namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamMemoryUsage = `sum by (container, owner_kind) (container_memory_working_set_bytes{k8s_cluster_name=%q, namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamCPURequest = `sum by (k8s_cluster_name, container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="cpu",unit="core"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamCPUUsage = `sum by (k8s_cluster_name, container, owner_kind) (rate(container_cpu_usage_seconds_total{namespace=%q, container!~%q}[5m]) * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"} )` + TeamMemoryRequest = `sum by (k8s_cluster_name, container, owner_kind) (kube_pod_container_resource_requests{namespace=%q, container!~%q, resource="memory",unit="byte"} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamMemoryUsage = `sum by (k8s_cluster_name, container, owner_kind) (container_memory_working_set_bytes{namespace=%q, container!~%q} * on(pod,namespace) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPURequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsCPUUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (rate(container_cpu_usage_seconds_total{namespace!~%q, container!~%q}[5m]) * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryRequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` - teamsMemoryUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (container_memory_working_set_bytes{namespace!~%q, container!~%q} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamsCPURequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="cpu",unit="core"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamsCPUUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (rate(container_cpu_usage_seconds_total{namespace!~%q, container!~%q}[5m]) * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamsMemoryRequest = `sum by (k8s_cluster_name, namespace, owner_kind) (kube_pod_container_resource_requests{namespace!~%q, container!~%q, resource="memory",unit="byte"} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` + TeamsMemoryUsage = `sum by (k8s_cluster_name, namespace, owner_kind) (container_memory_working_set_bytes{namespace!~%q, container!~%q} * on(pod, namespace, k8s_cluster_name) group_left(owner_kind) kube_pod_owner{owner_kind="ReplicaSet"})` cpuRequestRecommendation = `max( avg_over_time( @@ -92,12 +92,12 @@ func ForInstance(ctx context.Context, env string, teamSlug slug.Slug, workloadNa } func ForTeams(ctx context.Context, resourceType UtilizationResourceType) ([]*TeamUtilizationData, error) { - reqQ := teamsMemoryRequest - usageQ := teamsMemoryUsage + reqQ := TeamsMemoryRequest + usageQ := TeamsMemoryUsage if resourceType == UtilizationResourceTypeCPU { - reqQ = teamsCPURequest - usageQ = teamsCPUUsage + reqQ = TeamsCPURequest + usageQ = TeamsCPUUsage } c := fromContext(ctx).client @@ -152,12 +152,12 @@ func ForTeams(ctx context.Context, resourceType UtilizationResourceType) ([]*Tea } func ForTeam(ctx context.Context, teamSlug slug.Slug, resourceType UtilizationResourceType) ([]*WorkloadUtilizationData, error) { - reqQ := teamMemoryRequest - usageQ := teamMemoryUsage + reqQ := TeamMemoryRequest + usageQ := TeamMemoryUsage if resourceType == UtilizationResourceTypeCPU { - reqQ = teamCPURequest - usageQ = teamCPUUsage + reqQ = TeamCPURequest + usageQ = TeamCPUUsage } c := fromContext(ctx).client @@ -199,9 +199,9 @@ func ForTeam(ctx context.Context, teamSlug slug.Slug, resourceType UtilizationRe } func WorkloadResourceRequest(ctx context.Context, env string, teamSlug slug.Slug, workloadName string, resourceType UtilizationResourceType) (float64, error) { - q := appMemoryRequest + q := AppMemoryRequest if resourceType == UtilizationResourceTypeCPU { - q = appCPURequest + q = AppCPURequest } c := fromContext(ctx).client @@ -314,17 +314,17 @@ func WorkloadResourceRecommendations(ctx context.Context, env string, teamSlug s } func WorkloadResourceUsageRange(ctx context.Context, env string, teamSlug slug.Slug, workloadName string, resourceType UtilizationResourceType, start time.Time, end time.Time, step int) ([]*UtilizationSample, error) { - queryTemplate := appMemoryUsage + queryTemplate := AppMemoryUsage if resourceType == UtilizationResourceTypeCPU { - queryTemplate = appCPUUsage + queryTemplate = AppCPUUsage } return queryPrometheusRange(ctx, env, teamSlug, workloadName, queryTemplate, start, end, step) } func WorkloadResourceRequestRange(ctx context.Context, env string, teamSlug slug.Slug, workloadName string, resourceType UtilizationResourceType, start time.Time, end time.Time, step int) ([]*UtilizationSample, error) { - queryTemplate := appMemoryRequest + queryTemplate := AppMemoryRequest if resourceType == UtilizationResourceTypeCPU { - queryTemplate = appCPURequest + queryTemplate = AppCPURequest } return queryPrometheusRange(ctx, env, teamSlug, workloadName, queryTemplate, start, end, step) } From 307a25bbca2dacb753b7cb4b7cd0ed3e3ef610bc Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 14:21:38 +0100 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20en=20sp=C3=B8rring=20inneholder?= =?UTF-8?q?=20alle=20metrics?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Før hadde vi en Prometheus per miljø, nå er alt samlet hos Mimir, og man spør bare en gang hvis man trenger flere miljøer. Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- internal/thirdparty/promclient/client.go | 33 +--------- internal/thirdparty/promclient/fake/fake.go | 36 ++++++----- .../thirdparty/promclient/fake/fake_test.go | 60 ++++++++----------- internal/utilization/client.go | 2 +- internal/utilization/queries.go | 50 +++++++--------- 5 files changed, 68 insertions(+), 113 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 5eb5ecc64..e8c71d254 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -17,7 +17,7 @@ import ( type QueryClient interface { Query(ctx context.Context, environment string, query string, opts ...QueryOption) (prom.Vector, error) - QueryAll(ctx context.Context, query string, opts ...QueryOption) (map[string]prom.Vector, error) + QueryAll(ctx context.Context, query string, opts ...QueryOption) (prom.Vector, error) QueryRange(ctx context.Context, environment string, query string, promRange promv1.Range) (prom.Value, promv1.Warnings, error) } @@ -76,35 +76,8 @@ func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, }, nil } -func (c *RealClient) QueryAll(ctx context.Context, query string, opts ...QueryOption) (map[string]prom.Vector, error) { - type result struct { - env string - vec prom.Vector - } - wg := pool.NewWithResults[*result]().WithContext(ctx) - - for _, env := range []string{"dev"} { - wg.Go(func(ctx context.Context) (*result, error) { - v, err := c.Query(ctx, env, query, opts...) - if err != nil { - c.log.WithError(err).Errorf("failed to query prometheus in %s", env) - return nil, err - } - return &result{env: env, vec: v}, nil - }) - } - - results, err := wg.Wait() - if err != nil { - return nil, err - } - - ret := map[string]prom.Vector{} - for _, res := range results { - ret[res.env] = res.vec - } - - return ret, nil +func (c *RealClient) QueryAll(ctx context.Context, query string, opts ...QueryOption) (prom.Vector, error) { + return c.Query(ctx, "query-all", query, opts...) } func (c *RealClient) Query(ctx context.Context, environmentName string, query string, opts ...QueryOption) (prom.Vector, error) { diff --git a/internal/thirdparty/promclient/fake/fake.go b/internal/thirdparty/promclient/fake/fake.go index 9b1e4c72f..204ea294d 100644 --- a/internal/thirdparty/promclient/fake/fake.go +++ b/internal/thirdparty/promclient/fake/fake.go @@ -45,16 +45,8 @@ func NewFakeClient(environments []string, random *rand.Rand, nowFunc func() prom } } -func (c *FakeClient) QueryAll(ctx context.Context, query string, opts ...promclient.QueryOption) (map[string]prom.Vector, error) { - ret := map[string]prom.Vector{} - for _, env := range c.environments { - v, err := c.Query(ctx, env, query, opts...) - if err != nil { - return nil, err - } - ret[env] = v - } - return ret, nil +func (c *FakeClient) QueryAll(ctx context.Context, query string, opts ...promclient.QueryOption) (prom.Vector, error) { + return c.Query(ctx, "query-all", query, opts...) } func (c *FakeClient) Query(ctx context.Context, environment string, query string, opts ...promclient.QueryOption) (prom.Vector, error) { @@ -71,6 +63,7 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string } var ( + cluster string teamSlug slug.Slug workload string unit string @@ -80,13 +73,14 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string switch expr := expr.(type) { case *parser.AggregateExpr: labelsToCreate = expr.Grouping - teamSlug, workload, unit, err = c.selector(expr.Expr) + cluster, teamSlug, workload, unit, err = c.selector(expr.Expr) case *parser.VectorSelector: for _, matcher := range expr.LabelMatchers { labelsToCreate = append(labelsToCreate, matcher.Name) } - teamSlug, workload, unit, err = c.selector(expr) + labelsToCreate = append(labelsToCreate, "k8s_cluster_name") + cluster, teamSlug, workload, unit, err = c.selector(expr) case *parser.Call: vectorSelector, ok := expr.Args[0].(*parser.VectorSelector) @@ -103,8 +97,8 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string for _, matcher := range vectorSelector.LabelMatchers { labelsToCreate = append(labelsToCreate, matcher.Name) } - labelsToCreate = append(labelsToCreate, "pod") - teamSlug, workload, unit, err = c.selector(expr) + labelsToCreate = append(labelsToCreate, []string{"pod", "k8s_cluster_name"}...) + cluster, teamSlug, workload, unit, err = c.selector(expr) default: return nil, fmt.Errorf("query: unexpected expression type %T", expr) @@ -117,6 +111,8 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string lbls := prom.Metric{} for _, label := range labelsToCreate { switch label { + case "k8s_cluster_name": + lbls["k8s_cluster_name"] = prom.LabelValue(cluster) case "namespace": lbls["namespace"] = prom.LabelValue(teamSlug) case "workload_namespace": @@ -223,7 +219,7 @@ func (c *FakeClient) QueryRange(ctx context.Context, environment string, query s return matrix, nil, nil } -func (c *FakeClient) selector(expr parser.Expr) (teamSlug slug.Slug, workload string, unit string, err error) { +func (c *FakeClient) selector(expr parser.Expr) (cluster string, teamSlug slug.Slug, workload string, unit string, err error) { switch expr := expr.(type) { case *parser.VectorSelector: for _, m := range expr.LabelMatchers { @@ -231,6 +227,8 @@ func (c *FakeClient) selector(expr parser.Expr) (teamSlug slug.Slug, workload st continue } switch m.Name { + case "k8s_cluster_name": + cluster = m.Value case "namespace": teamSlug = slug.Slug(m.Value) case "app", "container": @@ -256,9 +254,9 @@ func (c *FakeClient) selector(expr parser.Expr) (teamSlug slug.Slug, workload st return c.selector(expr.VectorSelector) case *parser.BinaryExpr: - teamSlug, workload, unit, err = c.selector(expr.LHS) + cluster, teamSlug, workload, unit, err = c.selector(expr.LHS) if err != nil { - return "", "", "", err + return "", "", "", "", err } case *parser.SubqueryExpr: @@ -268,10 +266,10 @@ func (c *FakeClient) selector(expr parser.Expr) (teamSlug slug.Slug, workload st // no-op default: - return "", "", "", fmt.Errorf("selector: unexpected expression type %T", expr) + return "", "", "", "", fmt.Errorf("selector: unexpected expression type %T", expr) } - return teamSlug, workload, unit, nil + return cluster, teamSlug, workload, unit, nil } func (c *FakeClient) Rules(ctx context.Context, environment string, teamSlug slug.Slug) (promv1.RulesResult, error) { diff --git a/internal/thirdparty/promclient/fake/fake_test.go b/internal/thirdparty/promclient/fake/fake_test.go index 20d5c3aca..687581ff7 100644 --- a/internal/thirdparty/promclient/fake/fake_test.go +++ b/internal/thirdparty/promclient/fake/fake_test.go @@ -19,6 +19,7 @@ import ( "github.com/nais/api/internal/kubernetes/fake" "github.com/nais/api/internal/kubernetes/watcher" "github.com/nais/api/internal/team" + "github.com/nais/api/internal/utilization" "github.com/nais/api/internal/workload/application" promv1 "github.com/prometheus/client_golang/api/prometheus/v1" prom "github.com/prometheus/common/model" @@ -73,7 +74,8 @@ func TestFakeQuery(t *testing.T) { t.Run(name, func(t *testing.T) { c := NewFakeClient([]string{"test", "dev"}, rand.New(rand.NewPCG(2, 2)), now) - res, err := c.Query(ctx, "unused", fmt.Sprintf(test.query, test.args...)) + args := append(test.args, "test") + res, err := c.Query(ctx, "unused", fmt.Sprintf(test.query, args...)) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -93,71 +95,59 @@ func TestFakeQueryAll(t *testing.T) { tests := map[string]struct { query string args []any - expected map[string]prom.Vector + expected prom.Vector }{ "teamsMemoryRequest": { query: utilization.TeamsMemoryRequest, args: []any{"", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 487676427, Timestamp: now()}}, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 313949612, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 750014392, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 487676427, Timestamp: now()}, }, }, "teamsMemoryUsage": { query: utilization.TeamsMemoryUsage, args: []any{"", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 487676427, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 487676427, Timestamp: now()}}, }, "teamsCPURequest": { query: utilization.TeamsCPURequest, args: []any{"", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, }, "teamsCPUUsage": { query: utilization.TeamsCPUUsage, args: []any{"", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, }, "teamMemoryRequest": { query: utilization.TeamMemoryRequest, args: []any{"team1", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"container": "app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 910654684, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"container": "app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 487676427, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 487676427, Timestamp: now()}}, }, "teamMemoryUsage": { query: utilization.TeamMemoryUsage, args: []any{"team2", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 910654684, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 487676427, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 487676427, Timestamp: now()}}, }, "teamCPURequest": { query: utilization.TeamCPURequest, args: []any{"team2", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"container": "team-app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, }, "teamCPUUsage": { query: utilization.TeamCPUUsage, args: []any{"team1", ""}, - expected: map[string]prom.Vector{ - "dev": {&prom.Sample{Metric: prom.Metric{"container": "app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}}, - "test": {&prom.Sample{Metric: prom.Metric{"container": "app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, - }, + expected: prom.Vector{ + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, }, } @@ -306,7 +296,9 @@ func TestFakeQueryRange(t *testing.T) { t.Run(name, func(t *testing.T) { c := NewFakeClient([]string{"test", "dev"}, rand.New(rand.NewPCG(1, 1)), now) - res, _, err := c.QueryRange(ctx, "test", fmt.Sprintf(test.query, test.args...), test.rng) + args := append([]any{"test"}, test.args...) + query := fmt.Sprintf(test.query, args...) + res, _, err := c.QueryRange(ctx, "test", fmt.Sprintf(test.query, args...), test.rng) if err != nil { t.Errorf("Expected no error, got %v", err) } diff --git a/internal/utilization/client.go b/internal/utilization/client.go index 9fba3dabe..472610e91 100644 --- a/internal/utilization/client.go +++ b/internal/utilization/client.go @@ -10,6 +10,6 @@ import ( type ResourceUsageClient interface { Query(ctx context.Context, environment string, query string, opts ...promclient.QueryOption) (prom.Vector, error) - QueryAll(ctx context.Context, query string, opts ...promclient.QueryOption) (map[string]prom.Vector, error) + QueryAll(ctx context.Context, query string, opts ...promclient.QueryOption) (prom.Vector, error) QueryRange(ctx context.Context, environment string, query string, promRange promv1.Range) (prom.Value, promv1.Warnings, error) } diff --git a/internal/utilization/queries.go b/internal/utilization/queries.go index f99073b3e..b4af11fcb 100644 --- a/internal/utilization/queries.go +++ b/internal/utilization/queries.go @@ -110,14 +110,12 @@ func ForTeams(ctx context.Context, resourceType UtilizationResourceType) ([]*Tea ret := []*TeamUtilizationData{} - for env, samples := range requested { - for _, sample := range samples { - ret = append(ret, &TeamUtilizationData{ - TeamSlug: slug.Slug(sample.Metric["namespace"]), - Requested: float64(sample.Value), - EnvironmentName: env, - }) - } + for _, sample := range requested { + ret = append(ret, &TeamUtilizationData{ + TeamSlug: slug.Slug(sample.Metric["namespace"]), + Requested: float64(sample.Value), + EnvironmentName: string(sample.Metric["k8s_cluster_name"]), + }) } used, err := c.QueryAll(ctx, fmt.Sprintf(usageQ, ignoredNamespaces, ignoredContainers), promclient.WithTime(queryTime)) @@ -138,12 +136,10 @@ func ForTeams(ctx context.Context, resourceType UtilizationResourceType) ([]*Tea } ret = filtered - for env, samples := range used { - for _, sample := range samples { - for _, data := range ret { - if data.TeamSlug == slug.Slug(sample.Metric["namespace"]) && data.EnvironmentName == env { - data.Used = float64(sample.Value) - } + for _, sample := range used { + for _, data := range ret { + if data.TeamSlug == slug.Slug(sample.Metric["namespace"]) && data.EnvironmentName == string(sample.Metric["k8s_cluster_name"]) { + data.Used = float64(sample.Value) } } } @@ -169,15 +165,13 @@ func ForTeam(ctx context.Context, teamSlug slug.Slug, resourceType UtilizationRe ret := []*WorkloadUtilizationData{} - for env, samples := range requested { - for _, sample := range samples { - ret = append(ret, &WorkloadUtilizationData{ - TeamSlug: teamSlug, - WorkloadName: string(sample.Metric["container"]), - EnvironmentName: env, - Requested: float64(sample.Value), - }) - } + for _, sample := range requested { + ret = append(ret, &WorkloadUtilizationData{ + TeamSlug: teamSlug, + WorkloadName: string(sample.Metric["container"]), + EnvironmentName: string(sample.Metric["k8s_cluster_name"]), + Requested: float64(sample.Value), + }) } used, err := c.QueryAll(ctx, fmt.Sprintf(usageQ, teamSlug, ignoredContainers)) @@ -185,12 +179,10 @@ func ForTeam(ctx context.Context, teamSlug slug.Slug, resourceType UtilizationRe return nil, err } - for env, samples := range used { - for _, sample := range samples { - for _, data := range ret { - if data.WorkloadName == string(sample.Metric["container"]) && data.EnvironmentName == env { - data.Used = float64(sample.Value) - } + for _, sample := range used { + for _, data := range ret { + if data.WorkloadName == string(sample.Metric["container"]) && data.EnvironmentName == string(sample.Metric["k8s_cluster_name"]) { + data.Used = float64(sample.Value) } } } From 9d7a55fab88c6d298fa27ab14442b9c033af54ce Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Mon, 26 Jan 2026 14:55:28 +0100 Subject: [PATCH 16/22] =?UTF-8?q?fix-ish:=20St=C3=B8tter=20et=20milj=C3=B8?= =?UTF-8?q?=20for=20noen=20tester?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- internal/thirdparty/promclient/fake/fake.go | 35 +++++++++---------- .../thirdparty/promclient/fake/fake_test.go | 3 +- 2 files changed, 17 insertions(+), 21 deletions(-) diff --git a/internal/thirdparty/promclient/fake/fake.go b/internal/thirdparty/promclient/fake/fake.go index 204ea294d..cc875d5bc 100644 --- a/internal/thirdparty/promclient/fake/fake.go +++ b/internal/thirdparty/promclient/fake/fake.go @@ -63,7 +63,6 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string } var ( - cluster string teamSlug slug.Slug workload string unit string @@ -73,14 +72,14 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string switch expr := expr.(type) { case *parser.AggregateExpr: labelsToCreate = expr.Grouping - cluster, teamSlug, workload, unit, err = c.selector(expr.Expr) + teamSlug, workload, unit, err = c.selector(expr.Expr) case *parser.VectorSelector: for _, matcher := range expr.LabelMatchers { labelsToCreate = append(labelsToCreate, matcher.Name) } labelsToCreate = append(labelsToCreate, "k8s_cluster_name") - cluster, teamSlug, workload, unit, err = c.selector(expr) + teamSlug, workload, unit, err = c.selector(expr) case *parser.Call: vectorSelector, ok := expr.Args[0].(*parser.VectorSelector) @@ -98,7 +97,7 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string labelsToCreate = append(labelsToCreate, matcher.Name) } labelsToCreate = append(labelsToCreate, []string{"pod", "k8s_cluster_name"}...) - cluster, teamSlug, workload, unit, err = c.selector(expr) + teamSlug, workload, unit, err = c.selector(expr) default: return nil, fmt.Errorf("query: unexpected expression type %T", expr) @@ -108,22 +107,22 @@ func (c *FakeClient) Query(ctx context.Context, environment string, query string } makeLabels := func() prom.Metric { - lbls := prom.Metric{} + labels := prom.Metric{} for _, label := range labelsToCreate { switch label { - case "k8s_cluster_name": - lbls["k8s_cluster_name"] = prom.LabelValue(cluster) case "namespace": - lbls["namespace"] = prom.LabelValue(teamSlug) + labels["namespace"] = prom.LabelValue(teamSlug) case "workload_namespace": - lbls["workload_namespace"] = prom.LabelValue(teamSlug) + labels["workload_namespace"] = prom.LabelValue(teamSlug) case "container": - lbls["container"] = prom.LabelValue(workload) + labels["container"] = prom.LabelValue(workload) case "pod": - lbls["pod"] = prom.LabelValue(fmt.Sprintf("%s-%s", workload, "1")) + labels["pod"] = prom.LabelValue(fmt.Sprintf("%s-%s", workload, "1")) } } - return lbls + // TODO: Støtte de spørringene som har et miljø satt, og QueryAll som svarer med flere miljøer + labels["k8s_cluster_name"] = prom.LabelValue(environment) + return labels } value := func() prom.SampleValue { @@ -219,7 +218,7 @@ func (c *FakeClient) QueryRange(ctx context.Context, environment string, query s return matrix, nil, nil } -func (c *FakeClient) selector(expr parser.Expr) (cluster string, teamSlug slug.Slug, workload string, unit string, err error) { +func (c *FakeClient) selector(expr parser.Expr) (teamSlug slug.Slug, workload string, unit string, err error) { switch expr := expr.(type) { case *parser.VectorSelector: for _, m := range expr.LabelMatchers { @@ -227,8 +226,6 @@ func (c *FakeClient) selector(expr parser.Expr) (cluster string, teamSlug slug.S continue } switch m.Name { - case "k8s_cluster_name": - cluster = m.Value case "namespace": teamSlug = slug.Slug(m.Value) case "app", "container": @@ -254,9 +251,9 @@ func (c *FakeClient) selector(expr parser.Expr) (cluster string, teamSlug slug.S return c.selector(expr.VectorSelector) case *parser.BinaryExpr: - cluster, teamSlug, workload, unit, err = c.selector(expr.LHS) + teamSlug, workload, unit, err = c.selector(expr.LHS) if err != nil { - return "", "", "", "", err + return "", "", "", err } case *parser.SubqueryExpr: @@ -266,10 +263,10 @@ func (c *FakeClient) selector(expr parser.Expr) (cluster string, teamSlug slug.S // no-op default: - return "", "", "", "", fmt.Errorf("selector: unexpected expression type %T", expr) + return "", "", "", fmt.Errorf("selector: unexpected expression type %T", expr) } - return cluster, teamSlug, workload, unit, nil + return teamSlug, workload, unit, nil } func (c *FakeClient) Rules(ctx context.Context, environment string, teamSlug slug.Slug) (promv1.RulesResult, error) { diff --git a/internal/thirdparty/promclient/fake/fake_test.go b/internal/thirdparty/promclient/fake/fake_test.go index 687581ff7..fadf0fe19 100644 --- a/internal/thirdparty/promclient/fake/fake_test.go +++ b/internal/thirdparty/promclient/fake/fake_test.go @@ -230,7 +230,7 @@ func TestFakeQueryRange(t *testing.T) { args: []any{"team1", "workload1"}, expected: prom.Matrix{ { - Metric: prom.Metric{"container": "workload1", "namespace": "team1", "pod": "workload1-1"}, + Metric: prom.Metric{"container": "workload1", "namespace": "team1", "k8s_cluster_name": "test", "pod": "workload1-1"}, Values: []prom.SamplePair{ {Value: 750014392, Timestamp: 1609458900 * 1000}, {Value: 487676427, Timestamp: 1609458915 * 1000}, @@ -297,7 +297,6 @@ func TestFakeQueryRange(t *testing.T) { c := NewFakeClient([]string{"test", "dev"}, rand.New(rand.NewPCG(1, 1)), now) args := append([]any{"test"}, test.args...) - query := fmt.Sprintf(test.query, args...) res, _, err := c.QueryRange(ctx, "test", fmt.Sprintf(test.query, args...), test.rng) if err != nil { t.Errorf("Expected no error, got %v", err) From bc4c8341eec63098c5c17920f1c553e7393fb19a Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 08:51:20 +0100 Subject: [PATCH 17/22] =?UTF-8?q?fix:=20tester=20for=20Mimir=20metrics=20k?= =?UTF-8?q?j=C3=B8rer?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Terje Sannum --- internal/thirdparty/promclient/fake/fake.go | 12 +++- .../thirdparty/promclient/fake/fake_test.go | 65 +++++++++++++------ 2 files changed, 57 insertions(+), 20 deletions(-) diff --git a/internal/thirdparty/promclient/fake/fake.go b/internal/thirdparty/promclient/fake/fake.go index cc875d5bc..36142d316 100644 --- a/internal/thirdparty/promclient/fake/fake.go +++ b/internal/thirdparty/promclient/fake/fake.go @@ -46,7 +46,17 @@ func NewFakeClient(environments []string, random *rand.Rand, nowFunc func() prom } func (c *FakeClient) QueryAll(ctx context.Context, query string, opts ...promclient.QueryOption) (prom.Vector, error) { - return c.Query(ctx, "query-all", query, opts...) + var vectors prom.Vector + for _, env := range c.environments { + vec, err := c.Query(ctx, env, query, opts...) + if err != nil { + return vec, err + } + + vectors = append(vec, vectors...) + } + + return vectors, nil } func (c *FakeClient) Query(ctx context.Context, environment string, query string, opts ...promclient.QueryOption) (prom.Vector, error) { diff --git a/internal/thirdparty/promclient/fake/fake_test.go b/internal/thirdparty/promclient/fake/fake_test.go index fadf0fe19..99c1c11f9 100644 --- a/internal/thirdparty/promclient/fake/fake_test.go +++ b/internal/thirdparty/promclient/fake/fake_test.go @@ -42,30 +42,30 @@ func TestFakeQuery(t *testing.T) { }{ "appCPURequest": { query: utilization.AppCPURequest, - args: []any{"team", "workload"}, + args: []any{"test", "team", "workload"}, expected: prom.Vector{ - {Metric: prom.Metric{"container": "workload", "namespace": "team"}, Value: 0.15658213673311283, Timestamp: now()}, + {Metric: prom.Metric{"container": "workload", "k8s_cluster_name": "test", "namespace": "team"}, Value: 0.15658213673311283, Timestamp: now()}, }, }, "appCPUUsage": { query: utilization.AppCPUUsage, - args: []any{"team", "workload"}, + args: []any{"test", "team", "workload"}, expected: prom.Vector{ - {Metric: prom.Metric{"container": "workload", "namespace": "team", "pod": "workload-1"}, Value: 0.15658213673311283, Timestamp: now()}, + {Metric: prom.Metric{"container": "workload", "k8s_cluster_name": "test", "namespace": "team", "pod": "workload-1"}, Value: 0.15658213673311283, Timestamp: now()}, }, }, "appMemoryRequest": { query: utilization.AppMemoryRequest, - args: []any{"team", "workload"}, + args: []any{"test", "team", "workload"}, expected: prom.Vector{ - {Metric: prom.Metric{"container": "workload", "namespace": "team"}, Value: 105283867, Timestamp: now()}, + {Metric: prom.Metric{"container": "workload", "k8s_cluster_name": "test", "namespace": "team"}, Value: 105283867, Timestamp: now()}, }, }, "appMemoryUsage": { query: utilization.AppMemoryUsage, - args: []any{"team", "workload"}, + args: []any{"test", "team", "workload"}, expected: prom.Vector{ - {Metric: prom.Metric{"container": "workload", "namespace": "team", "pod": "workload-1"}, Value: 105283867, Timestamp: now()}, + {Metric: prom.Metric{"container": "workload", "k8s_cluster_name": "test", "namespace": "team", "pod": "workload-1"}, Value: 105283867, Timestamp: now()}, }, }, } @@ -74,8 +74,7 @@ func TestFakeQuery(t *testing.T) { t.Run(name, func(t *testing.T) { c := NewFakeClient([]string{"test", "dev"}, rand.New(rand.NewPCG(2, 2)), now) - args := append(test.args, "test") - res, err := c.Query(ctx, "unused", fmt.Sprintf(test.query, args...)) + res, err := c.Query(ctx, "test", fmt.Sprintf(test.query, test.args...)) if err != nil { t.Errorf("Expected no error, got %v", err) } @@ -102,52 +101,80 @@ func TestFakeQueryAll(t *testing.T) { args: []any{"", ""}, expected: prom.Vector{ &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 313949612, Timestamp: now()}, - &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 910654684, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 750014392, Timestamp: now()}, - &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 487676427, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 487676427, Timestamp: now()}, }, }, "teamsMemoryUsage": { query: utilization.TeamsMemoryUsage, args: []any{"", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 487676427, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 313949612, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 910654684, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 750014392, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 487676427, Timestamp: now()}, + }, }, "teamsCPURequest": { query: utilization.TeamsCPURequest, args: []any{"", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}, + }, }, "teamsCPUUsage": { query: utilization.TeamsCPUUsage, args: []any{"", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team1"}, Value: 1.6575697128208544, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "namespace": "team2"}, Value: 1.6466714936466849, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team1"}, Value: 0.6805719573212468, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "namespace": "team2"}, Value: 1.8199158760450043, Timestamp: now()}, + }, }, "teamMemoryRequest": { query: utilization.TeamMemoryRequest, args: []any{"team1", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 487676427, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 313949612, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-second-dev"}, Value: 910654684, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 750014392, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-second-test"}, Value: 487676427, Timestamp: now()}, + }, }, "teamMemoryUsage": { query: utilization.TeamMemoryUsage, args: []any{"team2", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 313949612, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 910654684, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 750014392, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 487676427, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 313949612, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-second-dev"}, Value: 910654684, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 750014392, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-second-test"}, Value: 487676427, Timestamp: now()}, + }, }, "teamCPURequest": { query: utilization.TeamCPURequest, args: []any{"team2", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "team-app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "team-app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "team-app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}, + }, }, "teamCPUUsage": { query: utilization.TeamCPUUsage, args: []any{"team1", ""}, expected: prom.Vector{ - &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, &prom.Sample{Metric: prom.Metric{"container": "app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-name-dev"}, Value: 1.6575697128208544, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "dev", "container": "app-second-dev"}, Value: 1.6466714936466849, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-name-test"}, Value: 0.6805719573212468, Timestamp: now()}, + &prom.Sample{Metric: prom.Metric{"k8s_cluster_name": "test", "container": "app-second-test"}, Value: 1.8199158760450043, Timestamp: now()}, + }, }, } From e501bca9c4c38f664186362d27152791c566afa5 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 09:15:24 +0100 Subject: [PATCH 18/22] fix: manglet env for usage Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- internal/utilization/queries.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/utilization/queries.go b/internal/utilization/queries.go index b4af11fcb..e1b85ff85 100644 --- a/internal/utilization/queries.go +++ b/internal/utilization/queries.go @@ -81,7 +81,7 @@ func ForInstance(ctx context.Context, env string, teamSlug slug.Slug, workloadNa c := fromContext(ctx).client - current, err := c.Query(ctx, env, fmt.Sprintf(usageQ, teamSlug, workloadName, instanceName)) + current, err := c.Query(ctx, env, fmt.Sprintf(usageQ, env, teamSlug, workloadName, instanceName)) if err != nil { return nil, err } From c93198ade4a44de6f8757371ca06670e0cac9cfc Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 09:48:22 +0100 Subject: [PATCH 19/22] fix: riktig header verdi for scope Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- internal/thirdparty/promclient/client.go | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index e8c71d254..1c014245f 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -49,22 +49,22 @@ type RealClient struct { log logrus.FieldLogger } -type mimirRoundTrip struct{} +type mimirRoundTrip struct { + HeaderValue string +} func (r mimirRoundTrip) RoundTrip(req *http.Request) (*http.Response, error) { - req.Header.Set("X-Scope-OrgID", "nais") + req.Header.Set("X-Scope-OrgID", r.HeaderValue) return http.DefaultTransport.RoundTrip(req) } func New(clusters []string, tenant string, log logrus.FieldLogger) (*RealClient, error) { - roundTrip := mimirRoundTrip{} - - mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080/prometheus", RoundTripper: roundTrip}) + mimirMetrics, err := api.NewClient(api.Config{Address: "http://mimir-query-frontend:8080/prometheus", RoundTripper: mimirRoundTrip{HeaderValue: "nais"}}) if err != nil { return nil, err } - mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080/prometheus", RoundTripper: roundTrip}) + mimirAlerts, err := api.NewClient(api.Config{Address: "http://mimir-ruler:8080/prometheus", RoundTripper: mimirRoundTrip{HeaderValue: "tenant"}}) if err != nil { return nil, err } From 31b13245e463c8e59dd52c6caed56b598c141f63 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 10:30:17 +0100 Subject: [PATCH 20/22] debug: litt debugdata for fikk det ikke til lokalt --- internal/thirdparty/promclient/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 1c014245f..8d42e25ac 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -161,6 +161,7 @@ func filterRulesByTeam(in promv1.RulesResult, teamSlug slug.Slug) promv1.RulesRe out.Groups = make([]promv1.RuleGroup, 0, len(in.Groups)) for _, g := range in.Groups { + fmt.Printf("Alert rules: File: %s, Name: %s\n", g.File, g.Name) var filtered promv1.Rules for _, r := range g.Rules { if ar, ok := r.(promv1.AlertingRule); ok { From c1f1d600a8f3a710ad4ac65a71ea1c21561986f6 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 10:44:34 +0100 Subject: [PATCH 21/22] =?UTF-8?q?feat:=20henter=20namespace=20fra=20navnet?= =?UTF-8?q?=20p=C3=A5=20filen=20til=20regelen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- internal/thirdparty/promclient/client.go | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/internal/thirdparty/promclient/client.go b/internal/thirdparty/promclient/client.go index 8d42e25ac..991e0d152 100644 --- a/internal/thirdparty/promclient/client.go +++ b/internal/thirdparty/promclient/client.go @@ -161,16 +161,22 @@ func filterRulesByTeam(in promv1.RulesResult, teamSlug slug.Slug) promv1.RulesRe out.Groups = make([]promv1.RuleGroup, 0, len(in.Groups)) for _, g := range in.Groups { - fmt.Printf("Alert rules: File: %s, Name: %s\n", g.File, g.Name) + splittedFile := strings.Split(g.File, "/") + if len(splittedFile) < 2 { + continue + } + + namespace := splittedFile[1] + var filtered promv1.Rules - for _, r := range g.Rules { - if ar, ok := r.(promv1.AlertingRule); ok { - if string(ar.Labels["namespace"]) == teamSlug.String() || - string(ar.Labels["team"]) == teamSlug.String() { + if namespace == teamSlug.String() { + for _, r := range g.Rules { + if ar, ok := r.(promv1.AlertingRule); ok { filtered = append(filtered, ar) } } } + if len(filtered) > 0 { out.Groups = append(out.Groups, promv1.RuleGroup{ Name: g.Name, @@ -180,5 +186,6 @@ func filterRulesByTeam(in promv1.RulesResult, teamSlug slug.Slug) promv1.RulesRe }) } } + return out } From 4202d629149286b4b111dbe5c3b668c66ca602d2 Mon Sep 17 00:00:00 2001 From: Kyrre Havik Date: Tue, 27 Jan 2026 10:45:48 +0100 Subject: [PATCH 22/22] =?UTF-8?q?feat:=20Snakker=20n=C3=A5=20med=20Mimir?= =?UTF-8?q?=20via=20service?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Carl Hedgren Co-authored-by: Terje Sannum --- charts/Chart.yaml | 2 +- charts/templates/networkpolicy.yaml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/charts/Chart.yaml b/charts/Chart.yaml index 4e08f23be..c5359d842 100644 --- a/charts/Chart.yaml +++ b/charts/Chart.yaml @@ -2,6 +2,6 @@ apiVersion: v2 name: nais-api description: The all mighty Nais API type: application -version: v0.1.0 +version: v0.2.0 sources: - https://github.com/nais/api/tree/main/charts diff --git a/charts/templates/networkpolicy.yaml b/charts/templates/networkpolicy.yaml index f78113694..4712c5572 100644 --- a/charts/templates/networkpolicy.yaml +++ b/charts/templates/networkpolicy.yaml @@ -17,6 +17,10 @@ spec: podSelector: matchLabels: k8s-app: kube-dns + - to: + - podSelector: + matchLabels: + app.kubernetes.io/name: mimir - to: - podSelector: matchLabels: