Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[META 388] Add support for Azure App Service Cloud metadata #1083

Merged
merged 2 commits into from
Dec 16, 2020
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
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>();
}
}
}