From 365c3f9ae3063b8e6115f2b460053986ad537d66 Mon Sep 17 00:00:00 2001 From: Hector Hernandez <39923391+hectorhdzg@users.noreply.github.com> Date: Wed, 7 Jun 2023 09:19:41 -0700 Subject: [PATCH] [Azure Monitor] OpenTelemetry Exporter Add support for Kubernetes resource attributes (#26104) Use Kubernetes resource attributes when present, to support AKS customers --- .../CHANGELOG.md | 1 + .../src/utils/common.ts | 86 ++++++++++++--- .../test/internal/commonUtils.test.ts | 100 ++++++++++++++++++ 3 files changed, 172 insertions(+), 15 deletions(-) create mode 100644 sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts diff --git a/sdk/monitor/monitor-opentelemetry-exporter/CHANGELOG.md b/sdk/monitor/monitor-opentelemetry-exporter/CHANGELOG.md index ca89796d91b1..6321a395382c 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/CHANGELOG.md +++ b/sdk/monitor/monitor-opentelemetry-exporter/CHANGELOG.md @@ -9,6 +9,7 @@ - Add AiCloudRole and AiCloudRoleInstance to OTel Resource event. - Add OTel resource metric envelope. - Add OpenTelemetry Log Exporter +- Use Kubernetes resource attributes to populate cloud role and role instance. ## 1.0.0-beta.12 (2023-04-04) diff --git a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts index 4301adaa6002..e7cb86250bc0 100644 --- a/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts +++ b/sdk/monitor/monitor-opentelemetry-exporter/src/utils/common.ts @@ -17,21 +17,8 @@ export function createTagsFromResource(resource: Resource): Tags { const context = getInstance(); const tags: Tags = { ...context.tags }; if (resource && resource.attributes) { - const serviceName = resource.attributes[SemanticResourceAttributes.SERVICE_NAME]; - const serviceNamespace = resource.attributes[SemanticResourceAttributes.SERVICE_NAMESPACE]; - if (serviceName) { - if (serviceNamespace) { - tags[KnownContextTagKeys.AiCloudRole] = `${serviceNamespace}.${serviceName}`; - } else { - tags[KnownContextTagKeys.AiCloudRole] = String(serviceName); - } - } - const serviceInstanceId = resource.attributes[SemanticResourceAttributes.SERVICE_INSTANCE_ID]; - if (serviceInstanceId) { - tags[KnownContextTagKeys.AiCloudRoleInstance] = String(serviceInstanceId); - } else { - tags[KnownContextTagKeys.AiCloudRoleInstance] = os && os.hostname(); - } + tags[KnownContextTagKeys.AiCloudRole] = getCloudRole(resource); + tags[KnownContextTagKeys.AiCloudRoleInstance] = getCloudRoleInstance(resource); const endUserId = resource.attributes[SemanticAttributes.ENDUSER_ID]; if (endUserId) { tags[KnownContextTagKeys.AiUserId] = String(endUserId); @@ -40,6 +27,75 @@ export function createTagsFromResource(resource: Resource): Tags { return tags; } +function getCloudRole(resource: Resource): string { + let cloudRole = ""; + // Service attributes + const serviceName = resource.attributes[SemanticResourceAttributes.SERVICE_NAME]; + const serviceNamespace = resource.attributes[SemanticResourceAttributes.SERVICE_NAMESPACE]; + if (serviceName) { + // Custom Service name provided by customer is highest precedence + if (!String(serviceName).startsWith("unknown_service")) { + if (serviceNamespace) { + return `${serviceNamespace}.${serviceName}`; + } else { + return String(serviceName); + } + } else { + // Service attributes will be only used if K8S attributes are not present + if (serviceNamespace) { + cloudRole = `${serviceNamespace}.${serviceName}`; + } else { + cloudRole = String(serviceName); + } + } + } + // Kubernetes attributes should take precedence + const kubernetesDeploymentName = + resource.attributes[SemanticResourceAttributes.K8S_DEPLOYMENT_NAME]; + if (kubernetesDeploymentName) { + return String(kubernetesDeploymentName); + } + const kuberneteReplicasetName = + resource.attributes[SemanticResourceAttributes.K8S_REPLICASET_NAME]; + if (kuberneteReplicasetName) { + return String(kuberneteReplicasetName); + } + const kubernetesStatefulSetName = + resource.attributes[SemanticResourceAttributes.K8S_STATEFULSET_NAME]; + if (kubernetesStatefulSetName) { + return String(kubernetesStatefulSetName); + } + const kubernetesJobName = resource.attributes[SemanticResourceAttributes.K8S_JOB_NAME]; + if (kubernetesJobName) { + return String(kubernetesJobName); + } + const kubernetesCronjobName = resource.attributes[SemanticResourceAttributes.K8S_CRONJOB_NAME]; + if (kubernetesCronjobName) { + return String(kubernetesCronjobName); + } + const kubernetesDaemonsetName = + resource.attributes[SemanticResourceAttributes.K8S_DAEMONSET_NAME]; + if (kubernetesDaemonsetName) { + return String(kubernetesDaemonsetName); + } + return cloudRole; +} + +function getCloudRoleInstance(resource: Resource): string { + // Kubernetes attributes should take precedence + const kubernetesPodName = resource.attributes[SemanticResourceAttributes.K8S_POD_NAME]; + if (kubernetesPodName) { + return String(kubernetesPodName); + } + // Service attributes + const serviceInstanceId = resource.attributes[SemanticResourceAttributes.SERVICE_INSTANCE_ID]; + if (serviceInstanceId) { + return String(serviceInstanceId); + } + // Default + return os && os.hostname(); +} + export function isSqlDB(dbSystem: string) { return ( dbSystem === DbSystemValues.DB2 || diff --git a/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts b/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts new file mode 100644 index 000000000000..122f68f095f2 --- /dev/null +++ b/sdk/monitor/monitor-opentelemetry-exporter/test/internal/commonUtils.test.ts @@ -0,0 +1,100 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT license. +import os from "os"; +import * as assert from "assert"; +import { Resource } from "@opentelemetry/resources"; +import { Tags } from "../../src/types"; +import { createTagsFromResource } from "../../src/utils/common"; + +describe("commonUtils.ts", () => { + describe("#createTagsFromResource", () => { + it("default values", () => { + let resource: Resource = Resource.EMPTY; + const tags: Tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], ""); + assert.strictEqual(tags["ai.cloud.roleInstance"], os.hostname()); + assert.strictEqual(tags["ai.user.id"], undefined); + }); + + it("should create Tags using custom Service attributes", () => { + let resource = new Resource({ + "service.name": "testServiceName", + "service.namespace": "testServiceNamespace", + "service.instance.id": "testServiceInstanceId", + "k8s.deployment.name": "testK8sDeployment", + "k8s.replicaset.name": "testK8sReplicaset", + "k8s.statefulset.name": "testK8sStatefulSet", + "k8s.job.name": "testK8sJob", + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + "k8s.pod.name": "testK8sPod", + }); + let tags: Tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testServiceNamespace.testServiceName"); + assert.strictEqual(tags["ai.cloud.roleInstance"], "testK8sPod"); + + resource = new Resource({ + "service.name": "testServiceName", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testServiceName"); + }); + + it("should create Tags using Kubernetes attributes if available", () => { + let resource = new Resource({ + "k8s.deployment.name": "testK8sDeployment", + "k8s.replicaset.name": "testK8sReplicaset", + "k8s.statefulset.name": "testK8sStatefulSet", + "k8s.job.name": "testK8sJob", + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + "k8s.pod.name": "testK8sPod", + }); + let tags: Tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sDeployment"); + assert.strictEqual(tags["ai.cloud.roleInstance"], "testK8sPod"); + + resource = new Resource({ + "k8s.replicaset.name": "testK8sReplicaset", + "k8s.statefulset.name": "testK8sStatefulSet", + "k8s.job.name": "testK8sJob", + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sReplicaset"); + resource = new Resource({ + "k8s.statefulset.name": "testK8sStatefulSet", + "k8s.job.name": "testK8sJob", + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sStatefulSet"); + resource = new Resource({ + "k8s.job.name": "testK8sJob", + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sJob"); + resource = new Resource({ + "k8s.cronjob.name": "testK8sCronJob", + "k8s.daemonset.name": "testK8sDaemonset", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sCronJob"); + resource = new Resource({ + "k8s.daemonset.name": "testK8sDaemonset", + }); + tags = createTagsFromResource(resource); + assert.strictEqual(tags["ai.cloud.role"], "testK8sDaemonset"); + }); + + it("should create Tags using default Resource", () => { + let resource = Resource.default(); + let tags: Tags = createTagsFromResource(resource); + assert.ok(tags["ai.cloud.role"].startsWith("unknown_service"), "wrong ai.cloud.role"); + }); + }); +});