Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 30 additions & 9 deletions src/OpenTelemetry.Resources.Azure/AppServiceResourceDetector.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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 (subscriptionId is { Length: > 0 })
{
attributeList.Add(new(ResourceSemanticConventions.AttributeCloudAccount, subscriptionId));
}

var azureResourceUri = GetAzureResourceURI(websiteSiteName, websiteResourceGroup, subscriptionId);
if (azureResourceUri != null)
{
attributeList.Add(new(ResourceSemanticConventions.AttributeCloudResourceId, azureResourceUri));
Expand Down Expand Up @@ -63,20 +76,28 @@ 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;
// WEBSITE_OWNER_NAME typically has the form
// "<subscription-id>+<resource-group>-<region>webspace".
// See https://learn.microsoft.com/azure/app-service/reference-app-settings#app-environment
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;
}
}
4 changes: 4 additions & 0 deletions src/OpenTelemetry.Resources.Azure/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/OpenTelemetry.Resources.Azure/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}". |
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, object>(ResourceSemanticConventions.AttributeCloudAccount, "testtestSubscriptionId"), resource.Attributes);
Assert.Contains(new KeyValuePair<string, object>(ResourceSemanticConventions.AttributeCloudResourceId, expectedResourceUri), resource.Attributes);
Assert.Contains(new KeyValuePair<string, object>(ResourceAttributeConstants.AzureResourceGroupName, "testResourceGroup"), resource.Attributes);
Assert.Contains(new KeyValuePair<string, object>(ResourceSemanticConventions.AttributeServiceName, "sitename"), resource.Attributes);

foreach (var kvp in AppServiceResourceDetector.AppServiceResourceAttributes)
Expand All @@ -114,6 +116,42 @@ public void AppServiceResourceDetectorReturnsResourceWithAttributes()
}
}

[Fact]
public void AppServiceResourceDetectorUsesUnseparatedOwnerNameAsCloudAccount()
{
var resource = DetectAppServiceResource("subscription-without-separator", null);

Assert.Contains(
new KeyValuePair<string, object>(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<string, object>(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()
{
Expand Down Expand Up @@ -277,4 +315,19 @@ public void CustomServiceNameAfterAppServiceDetectorOverridesDetectorValue()
resource.Attributes);
}
}

private static Resource DetectAppServiceResource(string? websiteOwnerName, string? websiteResourceGroup)
{
var environment = new Dictionary<string, string?>
{
[ResourceAttributeConstants.AppServiceSiteNameEnvVar] = "sitename",
[ResourceAttributeConstants.AppServiceOwnerNameEnvVar] = websiteOwnerName,
[ResourceAttributeConstants.AppServiceResourceGroupEnvVar] = websiteResourceGroup,
};

using (EnvironmentVariableScope.Create(environment))
{
return ResourceBuilder.CreateEmpty().AddAzureAppServiceDetector().Build();
}
}
}