From 2f51a6a8558488d870f6fd01833e5aac6d4fabd1 Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Sun, 26 Oct 2025 10:48:44 +0200 Subject: [PATCH 1/5] add multi dimensions for managed metrics --- api/v1alpha1/managedmetric_types.go | 4 ++++ api/v1alpha1/zz_generated.deepcopy.go | 9 +++++++- .../metrics.openmcp.cloud_managedmetrics.yaml | 7 ++++++ internal/controller/datasink_utils.go | 6 +++++ .../controller/managedmetric_controller.go | 7 +----- .../metric_controller_helpers_test.go | 8 +++---- internal/orchestrator/managedhandler.go | 23 ++++++++++--------- internal/orchestrator/orchestrator.go | 3 +++ 8 files changed, 45 insertions(+), 22 deletions(-) diff --git a/api/v1alpha1/managedmetric_types.go b/api/v1alpha1/managedmetric_types.go index 98723e2..c3c5543 100644 --- a/api/v1alpha1/managedmetric_types.go +++ b/api/v1alpha1/managedmetric_types.go @@ -31,6 +31,10 @@ type ManagedMetricSpec struct { // Defines which managed resources to observe // +optional Target *GroupVersionKind `json:"target,omitempty"` + // Defines dimensions of the metric. All specified fields must be nested strings. Nested slices are not supported. + // If not specified, only status.conditions of the CR will be used as dimension. + // +optional + Dimensions map[string]string `json:"dimensions,omitempty"` // Define labels of your object to adapt filters of the query // +optional LabelSelector string `json:"labelSelector,omitempty"` diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e775681..e7ada3a 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha1 import ( - v1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" ) @@ -647,6 +647,13 @@ func (in *ManagedMetricSpec) DeepCopyInto(out *ManagedMetricSpec) { *out = new(GroupVersionKind) **out = **in } + if in.Dimensions != nil { + in, out := &in.Dimensions, &out.Dimensions + *out = make(map[string]string, len(*in)) + for key, val := range *in { + (*out)[key] = val + } + } out.Interval = in.Interval if in.DataSinkRef != nil { in, out := &in.DataSinkRef, &out.DataSinkRef diff --git a/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml b/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml index c8afc89..a5bdc61 100644 --- a/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml +++ b/cmd/metrics-operator/embedded/crds/metrics.openmcp.cloud_managedmetrics.yaml @@ -65,6 +65,13 @@ spec: description: Sets the description that will be used to identify the metric in Dynatrace(or other providers) type: string + dimensions: + additionalProperties: + type: string + description: |- + Defines dimensions of the metric. All specified fields must be nested strings. Nested slices are not supported. + If not specified, only status.conditions of the CR will be used as dimension. + type: object fieldSelector: description: Define fields of your object to adapt filters of the query diff --git a/internal/controller/datasink_utils.go b/internal/controller/datasink_utils.go index dce0e23..fdd4580 100644 --- a/internal/controller/datasink_utils.go +++ b/internal/controller/datasink_utils.go @@ -89,6 +89,12 @@ func (d *DataSinkCredentialsRetriever) GetDataSinkCredentials(ctx context.Contex return common.DataSinkCredentials{}, err } + localEnp := os.Getenv("LOCAL_DATASINK") + if localEnp != "" { + l.Info("Using LOCAL_DATASINK environment variable for DataSink endpoint.", "endpoint", localEnp) + dataSink.Spec.Connection.Endpoint = localEnp + } + // Extract endpoint from DataSink endpoint := dataSink.Spec.Connection.Endpoint diff --git a/internal/controller/managedmetric_controller.go b/internal/controller/managedmetric_controller.go index c1b4aed..1762017 100644 --- a/internal/controller/managedmetric_controller.go +++ b/internal/controller/managedmetric_controller.go @@ -20,7 +20,6 @@ import ( "context" "fmt" "net/url" - "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -309,11 +308,7 @@ func getClusterInfo(config *rest.Config) (string, error) { return "localhost", nil } - // Remove any prefix (like "kubernetes" or "kubernetes.default.svc") - parts := strings.Split(hostname, ".") - clusterName := parts[0] - - return clusterName, nil + return hostname, nil } // OrchestratorFactory is a function type for creating orchestrators diff --git a/internal/controller/metric_controller_helpers_test.go b/internal/controller/metric_controller_helpers_test.go index 6a776fa..a583749 100644 --- a/internal/controller/metric_controller_helpers_test.go +++ b/internal/controller/metric_controller_helpers_test.go @@ -45,22 +45,22 @@ func TestGetClusterInfo(t *testing.T) { { name: "KubernetesService", host: "https://kubernetes.default.svc:6443", - expectedName: "kubernetes", + expectedName: "kubernetes.default.svc", }, { name: "CustomClusterName", host: "https://my-cluster-api.example.com:6443", - expectedName: "my-cluster-api", + expectedName: "my-cluster-api.example.com", }, { name: "IPAddress", host: "https://192.168.1.1:6443", - expectedName: "192", // The function only extracts the first part of the IP address + expectedName: "192.168.1.1", // The function only extracts the first part of the IP address }, { name: "WithPath", host: "https://kubernetes.default.svc:6443/api", - expectedName: "kubernetes", + expectedName: "kubernetes.default.svc", }, } diff --git a/internal/orchestrator/managedhandler.go b/internal/orchestrator/managedhandler.go index 9948a7a..4fd62a4 100644 --- a/internal/orchestrator/managedhandler.go +++ b/internal/orchestrator/managedhandler.go @@ -5,7 +5,6 @@ import ( "fmt" "slices" "strconv" - "strings" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -59,25 +58,27 @@ func (h *ManagedHandler) sendStatusBasedMetricValue(ctx context.Context) (string // Create a new data point for each resource dataPoint := clientoptl.NewDataPoint() - // Add GVK dimensions from resource - gv, err := schema.ParseGroupVersion(cr.MangedResource.APIVersion) + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cr.MangedResource) if err != nil { return "", err } - dataPoint.AddDimension(KIND, cr.MangedResource.Kind) - dataPoint.AddDimension(GROUP, gv.Group) - dataPoint.AddDimension(VERSION, gv.Version) + + u := &unstructured.Unstructured{Object: objMap} + + for key, expr := range h.metric.Spec.Dimensions { + s, _, err := nestedPrimitiveValue(*u, expr) + if err != nil { + fmt.Printf("WARN: Could not parse expression '%s' for dimension field '%s'. Error: %v\n", key, expr, err) + continue + } + dataPoint.AddDimension(key, s) + } // Add cluster dimension if available if h.clusterName != nil { dataPoint.AddDimension(CLUSTER, *h.clusterName) } - // Add status conditions as dimensions - for typ, state := range cr.Status { - dataPoint.AddDimension(strings.ToLower(typ), strconv.FormatBool(state)) - } - // Set the value to 1 for each resource dataPoint.SetValue(1) diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 55a6851..15b8af0 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -29,6 +29,9 @@ const ( // APIVERSION Constant for k8s resource fields APIVERSION string = "apiVersion" + + // NAME Constant for k8s resource fields + CR_NAME string = "crName" ) // GenericHandler is used to monitor the metric From c819b9966a4c6cfb6d5470ecc7af70069a1f0e7d Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Mon, 27 Oct 2025 11:18:05 +0200 Subject: [PATCH 2/5] update datasink --- internal/controller/datasink_utils.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/controller/datasink_utils.go b/internal/controller/datasink_utils.go index fdd4580..33c31dc 100644 --- a/internal/controller/datasink_utils.go +++ b/internal/controller/datasink_utils.go @@ -91,10 +91,9 @@ func (d *DataSinkCredentialsRetriever) GetDataSinkCredentials(ctx context.Contex localEnp := os.Getenv("LOCAL_DATASINK") if localEnp != "" { - l.Info("Using LOCAL_DATASINK environment variable for DataSink endpoint.", "endpoint", localEnp) + l.Info("Overriding DataSink endpoint with LOCAL_ENDPOINT environment variable.", "endpoint", localEnp) dataSink.Spec.Connection.Endpoint = localEnp } - // Extract endpoint from DataSink endpoint := dataSink.Spec.Connection.Endpoint From 46afdd1d89b1a88fc48f06dd56edfd7da88db558 Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Mon, 27 Oct 2025 12:03:17 +0200 Subject: [PATCH 3/5] remove local testing --- internal/controller/datasink_utils.go | 5 ----- internal/orchestrator/orchestrator.go | 3 --- 2 files changed, 8 deletions(-) diff --git a/internal/controller/datasink_utils.go b/internal/controller/datasink_utils.go index 33c31dc..dce0e23 100644 --- a/internal/controller/datasink_utils.go +++ b/internal/controller/datasink_utils.go @@ -89,11 +89,6 @@ func (d *DataSinkCredentialsRetriever) GetDataSinkCredentials(ctx context.Contex return common.DataSinkCredentials{}, err } - localEnp := os.Getenv("LOCAL_DATASINK") - if localEnp != "" { - l.Info("Overriding DataSink endpoint with LOCAL_ENDPOINT environment variable.", "endpoint", localEnp) - dataSink.Spec.Connection.Endpoint = localEnp - } // Extract endpoint from DataSink endpoint := dataSink.Spec.Connection.Endpoint diff --git a/internal/orchestrator/orchestrator.go b/internal/orchestrator/orchestrator.go index 15b8af0..55a6851 100644 --- a/internal/orchestrator/orchestrator.go +++ b/internal/orchestrator/orchestrator.go @@ -29,9 +29,6 @@ const ( // APIVERSION Constant for k8s resource fields APIVERSION string = "apiVersion" - - // NAME Constant for k8s resource fields - CR_NAME string = "crName" ) // GenericHandler is used to monitor the metric From 0949c511749be187f0f08442c3bedcd430acb2f0 Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Mon, 27 Oct 2025 12:11:48 +0200 Subject: [PATCH 4/5] switch back to old hostname parsing logic --- internal/controller/managedmetric_controller.go | 8 +++++++- internal/controller/metric_controller_helpers_test.go | 8 ++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/internal/controller/managedmetric_controller.go b/internal/controller/managedmetric_controller.go index 1762017..0e48c5d 100644 --- a/internal/controller/managedmetric_controller.go +++ b/internal/controller/managedmetric_controller.go @@ -20,6 +20,7 @@ import ( "context" "fmt" "net/url" + "strings" "time" apierrors "k8s.io/apimachinery/pkg/api/errors" @@ -308,7 +309,12 @@ func getClusterInfo(config *rest.Config) (string, error) { return "localhost", nil } - return hostname, nil + // Remove any prefix (like "kubernetes" or "kubernetes.default.svc") + parts := strings.Split(hostname, ".") + clusterName := parts[0] + + return clusterName, nil + } // OrchestratorFactory is a function type for creating orchestrators diff --git a/internal/controller/metric_controller_helpers_test.go b/internal/controller/metric_controller_helpers_test.go index a583749..6a776fa 100644 --- a/internal/controller/metric_controller_helpers_test.go +++ b/internal/controller/metric_controller_helpers_test.go @@ -45,22 +45,22 @@ func TestGetClusterInfo(t *testing.T) { { name: "KubernetesService", host: "https://kubernetes.default.svc:6443", - expectedName: "kubernetes.default.svc", + expectedName: "kubernetes", }, { name: "CustomClusterName", host: "https://my-cluster-api.example.com:6443", - expectedName: "my-cluster-api.example.com", + expectedName: "my-cluster-api", }, { name: "IPAddress", host: "https://192.168.1.1:6443", - expectedName: "192.168.1.1", // The function only extracts the first part of the IP address + expectedName: "192", // The function only extracts the first part of the IP address }, { name: "WithPath", host: "https://kubernetes.default.svc:6443/api", - expectedName: "kubernetes.default.svc", + expectedName: "kubernetes", }, } From 248799318cfbabe7e3173f5fd62a07617c7e86fc Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Tue, 28 Oct 2025 05:30:38 +0200 Subject: [PATCH 5/5] fix generated files --- api/v1alpha1/zz_generated.deepcopy.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index e7ada3a..89b75c6 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -5,7 +5,7 @@ package v1alpha1 import ( - "k8s.io/apimachinery/pkg/apis/meta/v1" + v1 "k8s.io/apimachinery/pkg/apis/meta/v1" runtime "k8s.io/apimachinery/pkg/runtime" )