diff --git a/src/Elastic.Apm/Cloud/AzureAppServiceMetadataProvider.cs b/src/Elastic.Apm/Cloud/AzureAppServiceMetadataProvider.cs
new file mode 100644
index 000000000..c500f2ed0
--- /dev/null
+++ b/src/Elastic.Apm/Cloud/AzureAppServiceMetadataProvider.cs
@@ -0,0 +1,120 @@
+// Licensed to Elasticsearch B.V under
+// one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Collections;
+using System.Threading.Tasks;
+using Elastic.Apm.Api;
+using Elastic.Apm.Logging;
+
+namespace Elastic.Apm.Cloud
+{
+ ///
+ /// Provides cloud metadata for Microsoft Azure App Services
+ ///
+ public class AzureAppServiceMetadataProvider : ICloudMetadataProvider
+ {
+ internal const string Name = "azure-app-service";
+
+ private readonly IApmLogger _logger;
+ private readonly IDictionary _environmentVariables;
+
+ ///
+ /// Value of the form {subscription id}+{app service plan resource group}-{region}webspace
+ ///
+ ///
+ /// f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace
+ ///
+ internal static readonly string WebsiteOwnerName = "WEBSITE_OWNER_NAME";
+
+ internal static readonly string WebsiteResourceGroup = "WEBSITE_RESOURCE_GROUP";
+
+ internal static readonly string WebsiteSiteName = "WEBSITE_SITE_NAME";
+
+ internal static readonly string WebsiteInstanceId = "WEBSITE_INSTANCE_ID";
+
+ private static readonly string Webspace = "webspace";
+
+ public AzureAppServiceMetadataProvider(IApmLogger logger, IDictionary environmentVariables)
+ {
+ _logger = logger;
+ _environmentVariables = environmentVariables;
+ }
+
+ public string Provider { get; } = Name;
+
+ public Task GetMetadataAsync()
+ {
+ if (_environmentVariables is null)
+ {
+ _logger.Trace()?.Log("Unable to get {Provider} cloud metadata as no environment variables available", Provider);
+ return Task.FromResult(null);
+ }
+
+ var websiteOwnerName = GetEnvironmentVariable(WebsiteOwnerName);
+ var websiteResourceGroup = GetEnvironmentVariable(WebsiteResourceGroup);
+ var websiteSiteName = GetEnvironmentVariable(WebsiteSiteName);
+ var websiteInstanceId = GetEnvironmentVariable(WebsiteInstanceId);
+
+ bool NullOrEmptyVariable(string key, string value)
+ {
+ if (!string.IsNullOrEmpty(value)) return false;
+
+ _logger.Trace()?.Log(
+ "Unable to get {Provider} cloud metadata as no {EnvironmentVariable} environment variable",
+ Provider,
+ key);
+
+ return true;
+ }
+
+ if (NullOrEmptyVariable(WebsiteOwnerName, websiteOwnerName) ||
+ NullOrEmptyVariable(WebsiteResourceGroup, websiteResourceGroup) ||
+ NullOrEmptyVariable(WebsiteSiteName, websiteSiteName) ||
+ NullOrEmptyVariable(WebsiteInstanceId, websiteInstanceId))
+ return Task.FromResult(null);
+
+ var websiteOwnerNameParts = websiteOwnerName.Split('+');
+ if (websiteOwnerNameParts.Length != 2)
+ {
+ _logger.Trace()?.Log(
+ "Unable to get {Provider} cloud metadata as {EnvironmentVariable} does not contain expected format",
+ Provider,
+ WebsiteOwnerName);
+ return Task.FromResult(null);
+ }
+
+ var subscriptionId = websiteOwnerNameParts[0];
+ var lastHyphenIndex = websiteOwnerNameParts[1].LastIndexOf('-');
+ if (lastHyphenIndex == -1)
+ {
+ _logger.Trace()?.Log(
+ "Unable to get {Provider} cloud metadata as {EnvironmentVariable} does not contain expected format",
+ Provider,
+ WebsiteOwnerName);
+ return Task.FromResult(null);
+ }
+
+ var index = lastHyphenIndex + 1;
+
+ var region = websiteOwnerNameParts[1].EndsWith(Webspace)
+ ? websiteOwnerNameParts[1].Substring(index, websiteOwnerNameParts[1].Length - (index + Webspace.Length))
+ : websiteOwnerNameParts[1].Substring(index);
+
+ return Task.FromResult(new Api.Cloud
+ {
+ Account = new CloudAccount { Id = subscriptionId },
+ Instance = new CloudInstance { Id = websiteInstanceId, Name = websiteSiteName },
+ Project = new CloudProject { Name = websiteResourceGroup },
+ Provider = "azure",
+ Region = region
+ });
+ }
+
+ private string GetEnvironmentVariable(string key) =>
+ _environmentVariables.Contains(key)
+ ? _environmentVariables[key]?.ToString()
+ : null;
+ }
+}
diff --git a/src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs b/src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs
index 614b77e4e..ac66f5d7c 100644
--- a/src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs
+++ b/src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs
@@ -6,6 +6,7 @@
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
+using Elastic.Apm.Helpers;
using Elastic.Apm.Logging;
using static Elastic.Apm.Config.ConfigConsts;
@@ -30,6 +31,7 @@ public CloudMetadataProviderCollection(string cloudProvider, IApmLogger logger)
break;
case SupportedValues.CloudProviderAzure:
Add(new AzureCloudMetadataProvider(logger));
+ Add(new AzureAppServiceMetadataProvider(logger, EnvironmentHelper.GetEnvironmentVariables(logger)));
break;
case SupportedValues.CloudProviderNone:
break;
@@ -40,6 +42,7 @@ public CloudMetadataProviderCollection(string cloudProvider, IApmLogger logger)
Add(new AwsCloudMetadataProvider(logger));
Add(new GcpCloudMetadataProvider(logger));
Add(new AzureCloudMetadataProvider(logger));
+ Add(new AzureAppServiceMetadataProvider(logger, EnvironmentHelper.GetEnvironmentVariables(logger)));
break;
default:
throw new ArgumentException($"Unknown cloud provider {cloudProvider}", nameof(cloudProvider));
diff --git a/src/Elastic.Apm/Helpers/EnvironmentHelper.cs b/src/Elastic.Apm/Helpers/EnvironmentHelper.cs
new file mode 100644
index 000000000..28a0a35f2
--- /dev/null
+++ b/src/Elastic.Apm/Helpers/EnvironmentHelper.cs
@@ -0,0 +1,31 @@
+// Licensed to Elasticsearch B.V under
+// one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System;
+using System.Collections;
+using Elastic.Apm.Logging;
+
+namespace Elastic.Apm.Helpers
+{
+ ///
+ /// Gets Environment variables, catching and logging any exception that may be thrown.
+ ///
+ internal static class EnvironmentHelper
+ {
+ public static IDictionary GetEnvironmentVariables(IApmLogger logger)
+ {
+ try
+ {
+ return Environment.GetEnvironmentVariables();
+ }
+ catch (Exception e)
+ {
+ logger.Debug()?.LogException(e, "Error while getting environment variables");
+ }
+
+ return null;
+ }
+ }
+}
diff --git a/test/Elastic.Apm.Tests/Cloud/AzureAppServiceMetadataProviderTests.cs b/test/Elastic.Apm.Tests/Cloud/AzureAppServiceMetadataProviderTests.cs
new file mode 100644
index 000000000..87f459f00
--- /dev/null
+++ b/test/Elastic.Apm.Tests/Cloud/AzureAppServiceMetadataProviderTests.cs
@@ -0,0 +1,74 @@
+// Licensed to Elasticsearch B.V under
+// one or more agreements.
+// Elasticsearch B.V licenses this file to you under the Apache 2.0 License.
+// See the LICENSE file in the project root for more information
+
+using System.Collections;
+using System.Threading.Tasks;
+using Elastic.Apm.Cloud;
+using Elastic.Apm.Tests.Mocks;
+using FluentAssertions;
+using Xunit;
+
+namespace Elastic.Apm.Tests.Cloud
+{
+ public class AzureAppServiceMetadataProviderTests
+ {
+ [Fact]
+ public async Task GetMetadataAsync_Returns_Expected_Cloud_Metadata()
+ {
+ var environmentVariables = new Hashtable
+ {
+ { AzureAppServiceMetadataProvider.WebsiteInstanceId, "instance_id" },
+ { AzureAppServiceMetadataProvider.WebsiteOwnerName, "f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace" },
+ { AzureAppServiceMetadataProvider.WebsiteSiteName, "site_name" },
+ { AzureAppServiceMetadataProvider.WebsiteResourceGroup, "resource_group" },
+ };
+
+ var provider = new AzureAppServiceMetadataProvider(new NoopLogger(), environmentVariables);
+ var metadata = await provider.GetMetadataAsync();
+
+ metadata.Should().NotBeNull();
+ metadata.Account.Should().NotBeNull();
+ metadata.Account.Id.Should().Be("f5940f10-2e30-3e4d-a259-63451ba6dae4");
+ metadata.Provider.Should().Be("azure");
+ metadata.Instance.Should().NotBeNull();
+ metadata.Instance.Id.Should().Be("instance_id");
+ metadata.Instance.Name.Should().Be("site_name");
+ metadata.Project.Should().NotBeNull();
+ metadata.Project.Name.Should().Be("resource_group");
+ metadata.Region.Should().Be("AustraliaEast");
+ }
+
+ [Theory]
+ [InlineData(null, "f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace", "site_name", "resource_group")]
+ [InlineData("instance_id", null, "site_name", "resource_group")]
+ [InlineData("instance_id", "f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace", null, "resource_group")]
+ [InlineData("instance_id", "f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace", "site_name", null)]
+ public async Task GetMetadataAsync_Returns_Null_When_Expected_EnvironmentVariable_Is_Missing(
+ string instanceId, string ownerName, string siteName, string resourceGroup)
+ {
+ var environmentVariables = new Hashtable
+ {
+ { AzureAppServiceMetadataProvider.WebsiteInstanceId, instanceId },
+ { AzureAppServiceMetadataProvider.WebsiteOwnerName, ownerName },
+ { AzureAppServiceMetadataProvider.WebsiteSiteName, siteName },
+ { AzureAppServiceMetadataProvider.WebsiteResourceGroup, resourceGroup },
+ };
+
+ var provider = new AzureAppServiceMetadataProvider(new NoopLogger(), environmentVariables);
+ var metadata = await provider.GetMetadataAsync();
+
+ metadata.Should().BeNull();
+ }
+
+ [Fact]
+ public async Task GetMetadataAsync_Returns_Null_When_EnvironmentVariables_Is_Null()
+ {
+ var provider = new AzureAppServiceMetadataProvider(new NoopLogger(), null);
+ var metadata = await provider.GetMetadataAsync();
+
+ metadata.Should().BeNull();
+ }
+ }
+}
diff --git a/test/Elastic.Apm.Tests/Cloud/CloudMetadataProviderCollectionTests.cs b/test/Elastic.Apm.Tests/Cloud/CloudMetadataProviderCollectionTests.cs
index 70bce09b0..ffae9fa6e 100644
--- a/test/Elastic.Apm.Tests/Cloud/CloudMetadataProviderCollectionTests.cs
+++ b/test/Elastic.Apm.Tests/Cloud/CloudMetadataProviderCollectionTests.cs
@@ -19,11 +19,12 @@ public void DefaultCloudProvider_Registers_Aws_Gcp_Azure_Providers()
{
var providers = new CloudMetadataProviderCollection(DefaultValues.CloudProvider, new NoopLogger());
- providers.Count.Should().Be(3);
+ providers.Count.Should().Be(4);
providers.TryGetValue(AwsCloudMetadataProvider.Name, out _).Should().BeTrue();
providers.TryGetValue(GcpCloudMetadataProvider.Name, out _).Should().BeTrue();
providers.TryGetValue(AzureCloudMetadataProvider.Name, out _).Should().BeTrue();
- providers.Select(p => p.Provider).Should().Equal("aws", "gcp", "azure");
+ providers.TryGetValue(AzureAppServiceMetadataProvider.Name, out _).Should().BeTrue();
+ providers.Select(p => p.Provider).Should().Equal("aws", "gcp", "azure", "azure-app-service");
}
[Fact]
@@ -52,12 +53,15 @@ public void CloudProvider_Gcp_Should_Register_Gcp_Provider()
}
[Fact]
- public void CloudProvider_Azure_Should_Register_Azure_Provider()
+ public void CloudProvider_Azure_Should_Register_Azure_Providers()
{
var providers = new CloudMetadataProviderCollection(SupportedValues.CloudProviderAzure, new NoopLogger());
- providers.Count.Should().Be(1);
+ providers.Count.Should().Be(2);
providers.TryGetValue(SupportedValues.CloudProviderAzure, out var provider).Should().BeTrue();
provider.Should().BeOfType();
+
+ providers.TryGetValue(AzureAppServiceMetadataProvider.Name, out provider).Should().BeTrue();
+ provider.Should().BeOfType();
}
}
}