Skip to content

Commit

Permalink
[META 388] Add support for Azure App Service Cloud metadata (#1083)
Browse files Browse the repository at this point in the history
This commit adds support for retrieving
cloud metadata about an Azure App Service
from the environment variables.
  • Loading branch information
russcam authored Dec 16, 2020
1 parent e7848d9 commit b43df67
Show file tree
Hide file tree
Showing 5 changed files with 236 additions and 4 deletions.
120 changes: 120 additions & 0 deletions src/Elastic.Apm/Cloud/AzureAppServiceMetadataProvider.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Provides cloud metadata for Microsoft Azure App Services
/// </summary>
public class AzureAppServiceMetadataProvider : ICloudMetadataProvider
{
internal const string Name = "azure-app-service";

private readonly IApmLogger _logger;
private readonly IDictionary _environmentVariables;

/// <summary>
/// Value of the form {subscription id}+{app service plan resource group}-{region}webspace
/// </summary>
/// <example>
/// f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace
/// </example>
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<Api.Cloud> GetMetadataAsync()
{
if (_environmentVariables is null)
{
_logger.Trace()?.Log("Unable to get {Provider} cloud metadata as no environment variables available", Provider);
return Task.FromResult<Api.Cloud>(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<Api.Cloud>(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<Api.Cloud>(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<Api.Cloud>(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;
}
}
3 changes: 3 additions & 0 deletions src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;
Expand All @@ -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));
Expand Down
31 changes: 31 additions & 0 deletions src/Elastic.Apm/Helpers/EnvironmentHelper.cs
Original file line number Diff line number Diff line change
@@ -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
{
/// <summary>
/// Gets Environment variables, catching and logging any exception that may be thrown.
/// </summary>
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;
}
}
}
Original file line number Diff line number Diff line change
@@ -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();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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]
Expand Down Expand Up @@ -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<AzureCloudMetadataProvider>();

providers.TryGetValue(AzureAppServiceMetadataProvider.Name, out provider).Should().BeTrue();
provider.Should().BeOfType<AzureAppServiceMetadataProvider>();
}
}
}

0 comments on commit b43df67

Please sign in to comment.