From a13a115f291a1cd392183b880bd532a42bf9b0ae Mon Sep 17 00:00:00 2001 From: Joseph Sirianni Date: Mon, 29 Jan 2024 14:13:37 -0500 Subject: [PATCH] [processor/resourcedetection] Detect Azure Kubernetes Service cluster name (#29328) **Description:** Added best effort support for detecting Azure Kubernetes Service cluster name: `k8s.cluster.name`. The cluster name can be extracted from the cluster's "resource group name" which is retrieved using existing functionality. The `parseClusterName` function has comments explaining the limitations. **Link to tracking Issue:** https://github.com/open-telemetry/opentelemetry-collector-contrib/issues/26794 **Testing:** I added unit tests for each scenario, and have tested against live AKS clusters that fit each scenario. I am happy to spin these up if anyone has any questions. Added `k8s.cluster.name` to the list of AKS resource attributes. --- ...detectionprocessor-azure-cluster-name.yaml | 27 ++++++++++ .../resourcedetectionprocessor/README.md | 29 +++++++++++ .../internal/azure/aks/aks.go | 44 +++++++++++++++-- .../internal/azure/aks/aks_test.go | 49 +++++++++++++++++++ .../aks/internal/metadata/generated_config.go | 8 ++- .../metadata/generated_config_test.go | 10 ++-- .../internal/metadata/generated_resource.go | 7 +++ .../metadata/generated_resource_test.go | 8 ++- .../internal/metadata/testdata/config.yaml | 4 ++ .../internal/azure/aks/metadata.yaml | 6 ++- 10 files changed, 180 insertions(+), 12 deletions(-) create mode 100644 .chloggen/resourcedetectionprocessor-azure-cluster-name.yaml diff --git a/.chloggen/resourcedetectionprocessor-azure-cluster-name.yaml b/.chloggen/resourcedetectionprocessor-azure-cluster-name.yaml new file mode 100644 index 000000000000..168388405a27 --- /dev/null +++ b/.chloggen/resourcedetectionprocessor-azure-cluster-name.yaml @@ -0,0 +1,27 @@ +# Use this changelog template to create an entry for release notes. + +# One of 'breaking', 'deprecation', 'new_component', 'enhancement', 'bug_fix' +change_type: enhancement + +# The name of the component, or a single word describing the area of concern, (e.g. filelogreceiver) +component: processor/resourcedetectionprocessor + +# A brief description of the change. Surround your text with quotes ("") if it needs to start with a backtick (`). +note: Detect Azure cluster name from IMDS metadata + +# Mandatory: One or more tracking issues related to the change. You can use the PR number here if no issue exists. +issues: [26794] + +# (Optional) One or more lines of additional information to render under the primary note. +# These lines will be padded with 2 spaces and then inserted directly into the document. +# Use pipe (|) for multiline entries. +subtext: + +# If your change doesn't affect end users or the exported elements of any package, +# you should instead start your pull request title with [chore] or use the "Skip Changelog" label. +# Optional: The change log or logs in which this entry should be included. +# e.g. '[user]' or '[user, api]' +# Include 'user' if the change is relevant to end users. +# Include 'api' if there is a change to a library API. +# Default: '[user]' +change_logs: [] diff --git a/processor/resourcedetectionprocessor/README.md b/processor/resourcedetectionprocessor/README.md index 691fc84d4c8f..05a75bdc8bf4 100644 --- a/processor/resourcedetectionprocessor/README.md +++ b/processor/resourcedetectionprocessor/README.md @@ -400,6 +400,7 @@ processors: * cloud.provider ("azure") * cloud.platform ("azure_aks") + * k8s.cluster.name ```yaml processors: @@ -409,6 +410,34 @@ processors: override: false ``` +#### Cluster Name + +Cluster name detection is disabled by default, and can be enabled with the +following configuration: + +```yaml +processors: + resourcedetection/aks: + detectors: [aks] + timeout: 2s + override: false + aks: + resource_attributes: + k8s.cluster.name: true +``` + +Azure AKS cluster name is derived from the Azure Instance Metadata Service's (IMDS) infrastructure resource group field. This field contains the resource group and name of the cluster, separated by underscores. e.g: `MC___`. + +Example: + - Resource group: my-resource-group + - Cluster name: my-cluster + - Location: eastus + - Generated name: MC_my-resource-group_my-cluster_eastus + +The cluster name is detected if it does not contain underscores and if a custom infrastructure resource group name was not used. + +If accurate parsing cannot be performed, the infrastructure resource group value is returned. This value can be used to uniquely identify the cluster, as Azure will not allow users to create multiple clusters with the same infrastructure resource group name. + ### Consul Queries a [consul agent](https://www.consul.io/docs/agent) and reads its' [configuration endpoint](https://www.consul.io/api-docs/agent#read-configuration) to retrieve the following resource attributes: diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/aks.go b/processor/resourcedetectionprocessor/internal/azure/aks/aks.go index 9bdfb1e690d6..0619ab2708ec 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/aks.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/aks.go @@ -6,6 +6,7 @@ package aks // import "github.com/open-telemetry/opentelemetry-collector-contrib import ( "context" "os" + "strings" "go.opentelemetry.io/collector/pdata/pcommon" "go.opentelemetry.io/collector/processor" @@ -42,8 +43,9 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem return res, "", nil } + m, err := d.provider.Metadata(ctx) // If we can't get a response from the metadata endpoint, we're not running in Azure - if !azureMetadataAvailable(ctx, d.provider) { + if err != nil { return res, "", nil } @@ -54,6 +56,9 @@ func (d *Detector) Detect(ctx context.Context) (resource pcommon.Resource, schem if d.resourceAttributes.CloudPlatform.Enabled { attrs.PutStr(conventions.AttributeCloudPlatform, conventions.AttributeCloudPlatformAzureAKS) } + if d.resourceAttributes.K8sClusterName.Enabled { + attrs.PutStr(conventions.AttributeK8SClusterName, parseClusterName(m.ResourceGroupName)) + } return res, conventions.SchemaURL, nil } @@ -62,7 +67,38 @@ func onK8s() bool { return os.Getenv(kubernetesServiceHostEnvVar) != "" } -func azureMetadataAvailable(ctx context.Context, p azure.Provider) bool { - _, err := p.Metadata(ctx) - return err == nil +// parseClusterName parses the cluster name from the infrastructure +// resource group name. AKS IMDS returns the resource group name in +// the following formats: +// +// 1. Generated group: MC___ +// - Example: +// - Resource group: my-resource-group +// - Cluster name: my-cluster +// - Location: eastus +// - Generated name: MC_my-resource-group_my-cluster_eastus +// +// 2. Custom group: custom-infra-resource-group-name +// +// When using the generated infrastructure resource group, the resource +// group will include the cluster name. If the cluster's resource group +// or cluster name contains underscores, parsing will fall back on the +// unparsed infrastructure resource group name. +// +// When using a custom infrastructure resource group, the resource group name +// does not contain the cluster name. The custom infrastructure resource group +// name is returned instead. +// +// It is safe to use the infrastructure resource group name as a unique identifier +// because Azure will not allow the user to create multiple AKS clusters with the same +// infrastructure resource group name. +func parseClusterName(resourceGroup string) string { + // Code inspired by https://github.com/open-telemetry/opentelemetry-collector-contrib/blob/main/exporter/datadogexporter/internal/hostmetadata/internal/azure/provider.go#L36 + splitAll := strings.Split(resourceGroup, "_") + + if len(splitAll) == 4 && strings.ToLower(splitAll[0]) == "mc" { + return splitAll[len(splitAll)-2] + } + + return resourceGroup } diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/aks_test.go b/processor/resourcedetectionprocessor/internal/azure/aks/aks_test.go index 4ce567164f9b..56a6ccf51d01 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/aks_test.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/aks_test.go @@ -64,3 +64,52 @@ func mockProvider() *azure.MockProvider { mp.On("Metadata").Return(&azure.ComputeMetadata{}, nil) return mp } + +func TestParseClusterName(t *testing.T) { + cases := []struct { + name string + resourceGroup string + expected string + }{ + { + name: "Return cluster name", + resourceGroup: "MC_myResourceGroup_AKSCluster_eastus", + expected: "AKSCluster", + }, + { + name: "Return resource group name, resource group contains underscores", + resourceGroup: "MC_Resource_Group_AKSCluster_eastus", + expected: "MC_Resource_Group_AKSCluster_eastus", + }, + { + name: "Return resource group name, cluster name contains underscores", + resourceGroup: "MC_myResourceGroup_AKS_Cluster_eastus", + expected: "MC_myResourceGroup_AKS_Cluster_eastus", + }, + { + name: "Custom infrastructure resource group name, return resource group name", + resourceGroup: "infra-group_name", + expected: "infra-group_name", + }, + { + name: "Custom infrastructure resource group name with four underscores, return resource group name", + resourceGroup: "dev_infra_group_name", + expected: "dev_infra_group_name", + }, + // This case is unlikely because it would require the user to create + // a custom infrastructure resource group with the MC prefix and the + // correct number of underscores. + { + name: "Custom infrastructure resource group name with MC prefix", + resourceGroup: "MC_group_name_location", + expected: "name", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + actual := parseClusterName(tc.resourceGroup) + assert.Equal(t, tc.expected, actual) + }) + } +} diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config.go b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config.go index 5b9acb9f0fef..2eb05eb09483 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config.go @@ -25,8 +25,9 @@ func (rac *ResourceAttributeConfig) Unmarshal(parser *confmap.Conf) error { // ResourceAttributesConfig provides config for resourcedetectionprocessor/aks resource attributes. type ResourceAttributesConfig struct { - CloudPlatform ResourceAttributeConfig `mapstructure:"cloud.platform"` - CloudProvider ResourceAttributeConfig `mapstructure:"cloud.provider"` + CloudPlatform ResourceAttributeConfig `mapstructure:"cloud.platform"` + CloudProvider ResourceAttributeConfig `mapstructure:"cloud.provider"` + K8sClusterName ResourceAttributeConfig `mapstructure:"k8s.cluster.name"` } func DefaultResourceAttributesConfig() ResourceAttributesConfig { @@ -37,5 +38,8 @@ func DefaultResourceAttributesConfig() ResourceAttributesConfig { CloudProvider: ResourceAttributeConfig{ Enabled: true, }, + K8sClusterName: ResourceAttributeConfig{ + Enabled: false, + }, } } diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config_test.go b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config_test.go index 9ce16e7f0d6a..fa542527d5fb 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config_test.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_config_test.go @@ -25,15 +25,17 @@ func TestResourceAttributesConfig(t *testing.T) { { name: "all_set", want: ResourceAttributesConfig{ - CloudPlatform: ResourceAttributeConfig{Enabled: true}, - CloudProvider: ResourceAttributeConfig{Enabled: true}, + CloudPlatform: ResourceAttributeConfig{Enabled: true}, + CloudProvider: ResourceAttributeConfig{Enabled: true}, + K8sClusterName: ResourceAttributeConfig{Enabled: true}, }, }, { name: "none_set", want: ResourceAttributesConfig{ - CloudPlatform: ResourceAttributeConfig{Enabled: false}, - CloudProvider: ResourceAttributeConfig{Enabled: false}, + CloudPlatform: ResourceAttributeConfig{Enabled: false}, + CloudProvider: ResourceAttributeConfig{Enabled: false}, + K8sClusterName: ResourceAttributeConfig{Enabled: false}, }, }, } diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource.go b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource.go index aff8c18f53ad..b4286d831a6b 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource.go @@ -35,6 +35,13 @@ func (rb *ResourceBuilder) SetCloudProvider(val string) { } } +// SetK8sClusterName sets provided value as "k8s.cluster.name" attribute. +func (rb *ResourceBuilder) SetK8sClusterName(val string) { + if rb.config.K8sClusterName.Enabled { + rb.res.Attributes().PutStr("k8s.cluster.name", val) + } +} + // Emit returns the built resource and resets the internal builder state. func (rb *ResourceBuilder) Emit() pcommon.Resource { r := rb.res diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource_test.go b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource_test.go index 40fc980e81bc..a467659755bc 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource_test.go +++ b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/generated_resource_test.go @@ -15,6 +15,7 @@ func TestResourceBuilder(t *testing.T) { rb := NewResourceBuilder(cfg) rb.SetCloudPlatform("cloud.platform-val") rb.SetCloudProvider("cloud.provider-val") + rb.SetK8sClusterName("k8s.cluster.name-val") res := rb.Emit() assert.Equal(t, 0, rb.Emit().Attributes().Len()) // Second call should return empty Resource @@ -23,7 +24,7 @@ func TestResourceBuilder(t *testing.T) { case "default": assert.Equal(t, 2, res.Attributes().Len()) case "all_set": - assert.Equal(t, 2, res.Attributes().Len()) + assert.Equal(t, 3, res.Attributes().Len()) case "none_set": assert.Equal(t, 0, res.Attributes().Len()) return @@ -41,6 +42,11 @@ func TestResourceBuilder(t *testing.T) { if ok { assert.EqualValues(t, "cloud.provider-val", val.Str()) } + val, ok = res.Attributes().Get("k8s.cluster.name") + assert.Equal(t, test == "all_set", ok) + if ok { + assert.EqualValues(t, "k8s.cluster.name-val", val.Str()) + } }) } } diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/testdata/config.yaml b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/testdata/config.yaml index d00b63470c51..1b7d4c7eda41 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/testdata/config.yaml +++ b/processor/resourcedetectionprocessor/internal/azure/aks/internal/metadata/testdata/config.yaml @@ -5,9 +5,13 @@ all_set: enabled: true cloud.provider: enabled: true + k8s.cluster.name: + enabled: true none_set: resource_attributes: cloud.platform: enabled: false cloud.provider: enabled: false + k8s.cluster.name: + enabled: false diff --git a/processor/resourcedetectionprocessor/internal/azure/aks/metadata.yaml b/processor/resourcedetectionprocessor/internal/azure/aks/metadata.yaml index c0065862472b..c5c1af7fbc15 100644 --- a/processor/resourcedetectionprocessor/internal/azure/aks/metadata.yaml +++ b/processor/resourcedetectionprocessor/internal/azure/aks/metadata.yaml @@ -10,4 +10,8 @@ resource_attributes: cloud.platform: description: The cloud.platform type: string - enabled: true \ No newline at end of file + enabled: true + k8s.cluster.name: + description: The k8s.cluster.name parsed from the Azure Instance Metadata Service's infrastructure resource group field + type: string + enabled: false