From 2407b007fa883c400db93c8eaea959e715bac2bd 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 +++++ internal/orchestrator/managedhandler.go | 23 ++++++++++--------- internal/orchestrator/orchestrator.go | 3 +++ 6 files changed, 40 insertions(+), 12 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/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 90b84d2b572af42eaa5dc8ce75c437144231e60c 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 c3f6968602aed04016ca90b7f59508a61447a32f 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 222c8a403aeebc8a6e3c2b3b74da420020719169 Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Tue, 28 Oct 2025 05:30:38 +0200 Subject: [PATCH 4/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" ) From 166886ce4b7b6540c3027172f0f002b750cdc72e Mon Sep 17 00:00:00 2001 From: Jordan Raychev Date: Tue, 16 Dec 2025 14:11:40 +0200 Subject: [PATCH 5/5] keep old beheavior --- internal/orchestrator/managedhandler.go | 42 +++++++++++++++++++------ 1 file changed, 32 insertions(+), 10 deletions(-) diff --git a/internal/orchestrator/managedhandler.go b/internal/orchestrator/managedhandler.go index 4fd62a4..a1041b3 100644 --- a/internal/orchestrator/managedhandler.go +++ b/internal/orchestrator/managedhandler.go @@ -5,6 +5,7 @@ import ( "fmt" "slices" "strconv" + "strings" apiextensionsv1 "k8s.io/apiextensions-apiserver/pkg/apis/apiextensions/v1" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" @@ -58,20 +59,41 @@ func (h *ManagedHandler) sendStatusBasedMetricValue(ctx context.Context) (string // Create a new data point for each resource dataPoint := clientoptl.NewDataPoint() - objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cr.MangedResource) - if err != nil { - return "", err - } + // Preserve old logic so that if custom dimensions are not set, we use status.conditions + // as default dimensions + if h.metric.Spec.Dimensions == nil { + gv, err := schema.ParseGroupVersion(cr.MangedResource.APIVersion) + if err != nil { + return "", err + } - u := &unstructured.Unstructured{Object: objMap} + dataPoint.AddDimension(KIND, cr.MangedResource.Kind) + dataPoint.AddDimension(GROUP, gv.Group) + dataPoint.AddDimension(VERSION, gv.Version) - for key, expr := range h.metric.Spec.Dimensions { - s, _, err := nestedPrimitiveValue(*u, expr) + for typ, state := range cr.Status { + t := strings.ToLower(typ) + if t == "ready" || t == "synced" { + dataPoint.AddDimension(t, strconv.FormatBool(state)) + } + } + + } else { + objMap, err := runtime.DefaultUnstructuredConverter.ToUnstructured(&cr.MangedResource) if err != nil { - fmt.Printf("WARN: Could not parse expression '%s' for dimension field '%s'. Error: %v\n", key, expr, err) - continue + return "", err + } + + 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) } - dataPoint.AddDimension(key, s) } // Add cluster dimension if available