From 05ea9b78740d07b5971a837a5baeb88671cfe6a9 Mon Sep 17 00:00:00 2001 From: "E. Lewis" <7704925+Lewis-E@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:20:28 -0400 Subject: [PATCH 1/4] Add Azure App Service account and resource group attributes Signed-off-by: E. Lewis <7704925+Lewis-E@users.noreply.github.com> --- .../AppServiceResourceDetector.cs | 36 ++++++++++++++----- .../CHANGELOG.md | 4 +++ .../ResourceAttributeConstants.cs | 1 + .../AzureResourceDetectorTests.cs | 2 ++ 4 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs index 583c36a485..f4dbf082bc 100644 --- a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs +++ b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs @@ -35,7 +35,20 @@ public Resource Detect() new(ResourceSemanticConventions.AttributeCloudPlatform, ResourceAttributeConstants.AzureAppServicePlatformValue), }; - var azureResourceUri = GetAzureResourceURI(websiteSiteName); + var websiteResourceGroup = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceResourceGroupEnvVar); + if (!string.IsNullOrEmpty(websiteResourceGroup)) + { + attributeList.Add(new(ResourceAttributeConstants.AzureResourceGroupName, websiteResourceGroup)); + } + + var websiteOwnerName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceOwnerNameEnvVar); + var subscriptionId = GetSubscriptionId(websiteOwnerName); + if (!string.IsNullOrEmpty(subscriptionId)) + { + attributeList.Add(new(ResourceSemanticConventions.AttributeCloudAccount, subscriptionId)); + } + + var azureResourceUri = GetAzureResourceURI(websiteSiteName, websiteResourceGroup, subscriptionId); if (azureResourceUri != null) { attributeList.Add(new(ResourceSemanticConventions.AttributeCloudResourceId, azureResourceUri)); @@ -63,20 +76,25 @@ public Resource Detect() return Resource.Empty; } - private static string? GetAzureResourceURI(string websiteSiteName) + private static string? GetAzureResourceURI(string websiteSiteName, string? websiteResourceGroup, string? subscriptionId) + { + return string.IsNullOrEmpty(websiteResourceGroup) || string.IsNullOrEmpty(subscriptionId) + ? null + : $"/subscriptions/{subscriptionId}/resourceGroups/{websiteResourceGroup}/providers/Microsoft.Web/sites/{websiteSiteName}"; + } + + private static string? GetSubscriptionId(string? websiteOwnerName) { - var websiteResourceGroup = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceResourceGroupEnvVar); - var websiteOwnerName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceOwnerNameEnvVar) ?? string.Empty; + if (websiteOwnerName == null) + { + return null; + } #if NET var index = websiteOwnerName.IndexOf('+', StringComparison.Ordinal); #else var index = websiteOwnerName.IndexOf("+", StringComparison.Ordinal); #endif - var subscriptionId = index > 0 ? websiteOwnerName.Substring(0, index) : websiteOwnerName; - - return string.IsNullOrEmpty(websiteResourceGroup) || string.IsNullOrEmpty(subscriptionId) - ? null - : $"/subscriptions/{subscriptionId}/resourceGroups/{websiteResourceGroup}/providers/Microsoft.Web/sites/{websiteSiteName}"; + return index > 0 ? websiteOwnerName.Substring(0, index) : websiteOwnerName; } } diff --git a/src/OpenTelemetry.Resources.Azure/CHANGELOG.md b/src/OpenTelemetry.Resources.Azure/CHANGELOG.md index a3577c8bd4..dcc564a71f 100644 --- a/src/OpenTelemetry.Resources.Azure/CHANGELOG.md +++ b/src/OpenTelemetry.Resources.Azure/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +* Add `cloud.account.id` and `azure.resource_group.name` attributes to the Azure + App Service resource detector. + ([#5138](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/5138)) + ## 1.18.0-beta.1 Released 2026-Aug-21 diff --git a/src/OpenTelemetry.Resources.Azure/ResourceAttributeConstants.cs b/src/OpenTelemetry.Resources.Azure/ResourceAttributeConstants.cs index 25c1393de1..40585cfe9a 100644 --- a/src/OpenTelemetry.Resources.Azure/ResourceAttributeConstants.cs +++ b/src/OpenTelemetry.Resources.Azure/ResourceAttributeConstants.cs @@ -7,6 +7,7 @@ internal sealed class ResourceAttributeConstants { // AppService resource attributes internal const string AzureAppServiceStamp = "azure.app.service.stamp"; + internal const string AzureResourceGroupName = "azure.resource_group.name"; // Azure VM resource attributes internal const string AzureVmScaleSetName = "azure.vm.scaleset.name"; diff --git a/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs b/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs index f7e95157ed..2d1cc2d14f 100644 --- a/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs +++ b/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs @@ -104,7 +104,9 @@ public void AppServiceResourceDetectorReturnsResourceWithAttributes() Assert.StartsWith("https://opentelemetry.io/schemas/", resource.SchemaUrl); var expectedResourceUri = "/subscriptions/testtestSubscriptionId/resourceGroups/testResourceGroup/providers/Microsoft.Web/sites/sitename"; + Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeCloudAccount, "testtestSubscriptionId"), resource.Attributes); Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeCloudResourceId, expectedResourceUri), resource.Attributes); + Assert.Contains(new KeyValuePair(ResourceAttributeConstants.AzureResourceGroupName, "testResourceGroup"), resource.Attributes); Assert.Contains(new KeyValuePair(ResourceSemanticConventions.AttributeServiceName, "sitename"), resource.Attributes); foreach (var kvp in AppServiceResourceDetector.AppServiceResourceAttributes) From f1af9069641604e3e89a728a36e103c5ee51f98b Mon Sep 17 00:00:00 2001 From: "E. Lewis" <7704925+Lewis-E@users.noreply.github.com> Date: Thu, 27 Aug 2026 15:33:22 -0400 Subject: [PATCH 2/4] Document and test App Service identity attributes Signed-off-by: E. Lewis <7704925+Lewis-E@users.noreply.github.com> --- src/OpenTelemetry.Resources.Azure/README.md | 2 + .../AzureResourceDetectorTests.cs | 51 +++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/OpenTelemetry.Resources.Azure/README.md b/src/OpenTelemetry.Resources.Azure/README.md index 8a8211d3cc..c32bf29fac 100644 --- a/src/OpenTelemetry.Resources.Azure/README.md +++ b/src/OpenTelemetry.Resources.Azure/README.md @@ -51,6 +51,8 @@ using var loggerFactory = LoggerFactory.Create(builder => | Attribute | Description | | --------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | azure.app.service.stamp | The specific "stamp" cluster within Azure where the App Service is running, e.g., "waws-prod-sn1-001". | +| azure.resource_group.name | The Azure resource group from `WEBSITE_RESOURCE_GROUP`. Emitted when the environment variable is non-empty. | +| cloud.account.id | The Azure subscription ID parsed from `WEBSITE_OWNER_NAME`. Emitted when the environment variable is non-empty. | | cloud.platform | The cloud platform. Here, it's always "azure_app_service". | | cloud.provider | The cloud service provider. In this context, it's always "azure". | | cloud.resource_id | The Azure Resource Manager URI uniquely identifying the Azure App Service. Typically in the format "/subscriptions/{subscriptionId}/resourceGroups/{groupName}/providers/Microsoft.Web/sites/{siteName}". | diff --git a/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs b/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs index 2d1cc2d14f..bde686d3db 100644 --- a/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs +++ b/test/OpenTelemetry.Resources.Azure.Tests/AzureResourceDetectorTests.cs @@ -116,6 +116,42 @@ public void AppServiceResourceDetectorReturnsResourceWithAttributes() } } + [Fact] + public void AppServiceResourceDetectorUsesUnseparatedOwnerNameAsCloudAccount() + { + var resource = DetectAppServiceResource("subscription-without-separator", null); + + Assert.Contains( + new KeyValuePair(ResourceSemanticConventions.AttributeCloudAccount, "subscription-without-separator"), + resource.Attributes); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceAttributeConstants.AzureResourceGroupName); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceSemanticConventions.AttributeCloudResourceId); + } + + [Fact] + public void AppServiceResourceDetectorEmitsResourceGroupWithoutCloudAccount() + { + var resource = DetectAppServiceResource(null, "testResourceGroup"); + + Assert.Contains( + new KeyValuePair(ResourceAttributeConstants.AzureResourceGroupName, "testResourceGroup"), + resource.Attributes); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceSemanticConventions.AttributeCloudAccount); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceSemanticConventions.AttributeCloudResourceId); + } + + [Theory] + [InlineData("", null)] + [InlineData(null, "")] + public void AppServiceResourceDetectorSuppressesEmptyIdentityAttributes(string? websiteOwnerName, string? websiteResourceGroup) + { + var resource = DetectAppServiceResource(websiteOwnerName, websiteResourceGroup); + + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceSemanticConventions.AttributeCloudAccount); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceAttributeConstants.AzureResourceGroupName); + Assert.DoesNotContain(resource.Attributes, attribute => attribute.Key == ResourceSemanticConventions.AttributeCloudResourceId); + } + [Fact] public void TestAzureVmResourceDetector() { @@ -279,4 +315,19 @@ public void CustomServiceNameAfterAppServiceDetectorOverridesDetectorValue() resource.Attributes); } } + + private static Resource DetectAppServiceResource(string? websiteOwnerName, string? websiteResourceGroup) + { + var environment = new Dictionary + { + [ResourceAttributeConstants.AppServiceSiteNameEnvVar] = "sitename", + [ResourceAttributeConstants.AppServiceOwnerNameEnvVar] = websiteOwnerName, + [ResourceAttributeConstants.AppServiceResourceGroupEnvVar] = websiteResourceGroup, + }; + + using (EnvironmentVariableScope.Create(environment)) + { + return ResourceBuilder.CreateEmpty().AddAzureAppServiceDetector().Build(); + } + } } From a7f16e8dbfa63bae3a2065560cf6c31eb8692a8b Mon Sep 17 00:00:00 2001 From: "E. Lewis" <7704925+Lewis-E@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:49:29 -0400 Subject: [PATCH 3/4] Fix App Service subscription ID nullability Signed-off-by: E. Lewis <7704925+Lewis-E@users.noreply.github.com> --- src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs index f4dbf082bc..2253c82fbd 100644 --- a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs +++ b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs @@ -43,7 +43,7 @@ public Resource Detect() var websiteOwnerName = Environment.GetEnvironmentVariable(ResourceAttributeConstants.AppServiceOwnerNameEnvVar); var subscriptionId = GetSubscriptionId(websiteOwnerName); - if (!string.IsNullOrEmpty(subscriptionId)) + if (subscriptionId is { Length: > 0 }) { attributeList.Add(new(ResourceSemanticConventions.AttributeCloudAccount, subscriptionId)); } From 2e8ce7b7a967937b94bb06a2a2f7403f272d898a Mon Sep 17 00:00:00 2001 From: "E. Lewis" <7704925+Lewis-E@users.noreply.github.com> Date: Fri, 28 Aug 2026 10:53:36 -0400 Subject: [PATCH 4/4] Document App Service owner name format Signed-off-by: E. Lewis <7704925+Lewis-E@users.noreply.github.com> --- .../AppServiceResourceDetector.cs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs index 2253c82fbd..65f0fd6049 100644 --- a/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs +++ b/src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs @@ -85,6 +85,9 @@ public Resource Detect() private static string? GetSubscriptionId(string? websiteOwnerName) { + // WEBSITE_OWNER_NAME typically has the form + // "+-webspace". + // See https://learn.microsoft.com/azure/app-service/reference-app-settings#app-environment if (websiteOwnerName == null) { return null;