diff --git a/plugins/inputs/prometheus/k8s134_compatibility_test.go b/plugins/inputs/prometheus/k8s134_compatibility_test.go new file mode 100644 index 0000000000..953a406fb3 --- /dev/null +++ b/plugins/inputs/prometheus/k8s134_compatibility_test.go @@ -0,0 +1,218 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: MIT + +package prometheus + +import ( + "testing" +) + +func TestApplyK8s134Compatibility(t *testing.T) { + tests := []struct { + name string + input PrometheusMetricBatch + expected PrometheusMetricBatch + }{ + { + name: "apiserver_resource_objects renamed to apiserver_storage_objects with group", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_resource_objects", + tags: map[string]string{ + "resource": "pods", + "group": "v1", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_storage_objects", + tags: map[string]string{ + "resource": "pods.v1", + }, + }, + }, + }, + { + name: "apiserver_resource_objects renamed to apiserver_storage_objects without group", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_resource_objects", + tags: map[string]string{ + "resource": "nodes", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_storage_objects", + tags: map[string]string{ + "resource": "nodes", + }, + }, + }, + }, + { + name: "etcd_request metric gets type label", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_duration_seconds", + tags: map[string]string{ + "resource": "pods", + "group": "v1", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_duration_seconds", + tags: map[string]string{ + "resource": "pods", + "group": "v1", + "resource_prefix": "pods.v1", + "type": "pods.v1", + }, + }, + }, + }, + { + name: "apiserver_watch metric gets kind label", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_watch_events_total", + tags: map[string]string{ + "resource": "pods", + "group": "v1", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "apiserver_watch_events_total", + tags: map[string]string{ + "resource": "pods", + "group": "v1", + "resource_prefix": "pods.v1", + "kind": "pods", + }, + }, + }, + }, + { + name: "resource without group", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_total", + tags: map[string]string{ + "resource": "nodes", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_total", + tags: map[string]string{ + "resource": "nodes", + "resource_prefix": "nodes", + "type": "nodes", + }, + }, + }, + }, + { + name: "non-control-plane metric unchanged", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "some_other_metric", + tags: map[string]string{ + "label": "value", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "some_other_metric", + tags: map[string]string{ + "label": "value", + }, + }, + }, + }, + { + name: "empty group treated as missing", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_errors", + tags: map[string]string{ + "resource": "pods", + "group": "", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_errors", + tags: map[string]string{ + "resource": "pods", + "group": "", + "resource_prefix": "pods", + "type": "pods", + }, + }, + }, + }, + { + name: "metric without resource label unchanged", + input: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_duration_seconds", + tags: map[string]string{ + "operation": "get", + }, + }, + }, + expected: PrometheusMetricBatch{ + &PrometheusMetric{ + metricName: "etcd_request_duration_seconds", + tags: map[string]string{ + "operation": "get", + }, + }, + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := applyK8s134Compatibility(tt.input) + + if len(result) != len(tt.expected) { + t.Errorf("Expected %d metrics, got %d", len(tt.expected), len(result)) + return + } + + for i, metric := range result { + expected := tt.expected[i] + + if metric.metricName != expected.metricName { + t.Errorf("Expected metric name %s, got %s", expected.metricName, metric.metricName) + } + + for key, expectedValue := range expected.tags { + if actualValue, exists := metric.tags[key]; !exists { + t.Errorf("Expected tag %s to exist", key) + } else if actualValue != expectedValue { + t.Errorf("Expected tag %s=%s, got %s=%s", key, expectedValue, key, actualValue) + } + } + + // Check that no unexpected tags exist (except for the ones we know should be removed) + for key := range metric.tags { + if key == "group" && metric.metricName == "apiserver_storage_objects" { + t.Errorf("Group tag should be removed for apiserver_storage_objects") + } + } + } + }) + } +} diff --git a/plugins/inputs/prometheus/metrics_handler.go b/plugins/inputs/prometheus/metrics_handler.go index d515e015bf..eeca04b01d 100644 --- a/plugins/inputs/prometheus/metrics_handler.go +++ b/plugins/inputs/prometheus/metrics_handler.go @@ -5,6 +5,7 @@ package prometheus import ( "log" + "strings" "sync" "time" @@ -52,6 +53,9 @@ func (mh *metricsHandler) handle(pmb PrometheusMetricBatch) { // do calculation: calculate delta for counter pmb = mh.calculator.Calculate(pmb) + // Apply K8s 1.34 compatibility transformations + pmb = applyK8s134Compatibility(pmb) + // do merge: merge metrics which are sharing same tags metricMaterials := mergeMetrics(pmb) @@ -93,3 +97,50 @@ func (mh *metricsHandler) setEmfMetadata(mms []*metricMaterial) { } } } + +// applyK8s134Compatibility applies transformations for K8s 1.34 compatibility +// K8s 1.34 splits labels into group+resource and renames apiserver_storage_objects → apiserver_resource_objects +// We maintain pre-1.34 behavior with no user config changes +func applyK8s134Compatibility(pmb PrometheusMetricBatch) PrometheusMetricBatch { + for _, pm := range pmb { + // Rename: If metric name == apiserver_resource_objects, report it as apiserver_storage_objects + if pm.metricName == "apiserver_resource_objects" { + pm.metricName = "apiserver_storage_objects" + + // Set legacy resource = resource + (group != "" ? "." + group : ""); drop/ignore group + if resource, hasResource := pm.tags["resource"]; hasResource { + if group, hasGroup := pm.tags["group"]; hasGroup && group != "" { + pm.tags["resource"] = resource + "." + group + } + delete(pm.tags, "group") + } + } + + // Label shims for control-plane metrics that carry a resource label + if resource, hasResource := pm.tags["resource"]; hasResource { + group := pm.tags["group"] // treat missing group as "" + + // Add resource_prefix = resource + (group != "" ? "." + group : "") + if group != "" { + pm.tags["resource_prefix"] = resource + "." + group + } else { + pm.tags["resource_prefix"] = resource + } + + // For etcd_request metrics, set type = resource + (group != "" ? "." + group : "") + if strings.HasPrefix(pm.metricName, "etcd_request") { + if group != "" { + pm.tags["type"] = resource + "." + group + } else { + pm.tags["type"] = resource + } + } + + // For apiserver_watch_ metrics, set kind = resource + if strings.HasPrefix(pm.metricName, "apiserver_watch_") { + pm.tags["kind"] = resource + } + } + } + return pmb +}