Skip to content

Commit

Permalink
Add Azure App Service Metadata gherkin spec (#1221)
Browse files Browse the repository at this point in the history
This commit adds the scenario implementations
for testing the collection of Azure App Service Metadata.

EnvironmentHelper is changed to IEnvironmentVariables,
to introduce a test seam for environment variables, to isolate
the manipulation of environment variables to tests.

Update AzureAppServiceMetadataProvider to handle the
additional test case.

Co-authored-by: Russ Cam <[email protected]>
Co-authored-by: Gergely Kalapos <[email protected]>
  • Loading branch information
3 people authored Mar 30, 2021
1 parent 26ebf44 commit d9c163f
Show file tree
Hide file tree
Showing 9 changed files with 303 additions and 40 deletions.
12 changes: 7 additions & 5 deletions src/Elastic.Apm/Cloud/AzureAppServiceMetadataProvider.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
// 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 System.Threading.Tasks;
using Elastic.Apm.Api;
Expand Down Expand Up @@ -86,6 +87,11 @@ bool NullOrEmptyVariable(string key, string value)
}

var subscriptionId = websiteOwnerNameParts[0];

var webspaceIndex = websiteOwnerNameParts[1].LastIndexOf(Webspace, StringComparison.Ordinal);
if (webspaceIndex != -1)
websiteOwnerNameParts[1] = websiteOwnerNameParts[1].Substring(0, webspaceIndex);

var lastHyphenIndex = websiteOwnerNameParts[1].LastIndexOf('-');
if (lastHyphenIndex == -1)
{
Expand All @@ -96,11 +102,7 @@ bool NullOrEmptyVariable(string key, string value)
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);
var region = websiteOwnerNameParts[1].Substring(lastHyphenIndex + 1);

return Task.FromResult(new Api.Cloud
{
Expand Down
11 changes: 9 additions & 2 deletions src/Elastic.Apm/Cloud/CloudMetadataProviderCollection.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,14 @@ public class CloudMetadataProviderCollection : KeyedCollection<string, ICloudMet
protected override string GetKeyForItem(ICloudMetadataProvider item) => item.Provider;

public CloudMetadataProviderCollection(string cloudProvider, IApmLogger logger)
: this(cloudProvider, logger, new EnvironmentVariables(logger))
{
}

internal CloudMetadataProviderCollection(string cloudProvider, IApmLogger logger, IEnvironmentVariables environmentVariables)
{
environmentVariables ??= new EnvironmentVariables(logger);

switch (cloudProvider?.ToLowerInvariant())
{
case SupportedValues.CloudProviderAws:
Expand All @@ -31,7 +38,7 @@ public CloudMetadataProviderCollection(string cloudProvider, IApmLogger logger)
break;
case SupportedValues.CloudProviderAzure:
Add(new AzureCloudMetadataProvider(logger));
Add(new AzureAppServiceMetadataProvider(logger, EnvironmentHelper.GetEnvironmentVariables(logger)));
Add(new AzureAppServiceMetadataProvider(logger, environmentVariables.GetEnvironmentVariables()));
break;
case SupportedValues.CloudProviderNone:
break;
Expand All @@ -42,7 +49,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)));
Add(new AzureAppServiceMetadataProvider(logger, environmentVariables.GetEnvironmentVariables()));
break;
default:
throw new ArgumentException($"Unknown cloud provider {cloudProvider}", nameof(cloudProvider));
Expand Down
31 changes: 0 additions & 31 deletions src/Elastic.Apm/Helpers/EnvironmentHelper.cs

This file was deleted.

35 changes: 35 additions & 0 deletions src/Elastic.Apm/Helpers/EnvironmentVariables.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// 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
{
internal interface IEnvironmentVariables
{
IDictionary GetEnvironmentVariables();
}

internal sealed class EnvironmentVariables : IEnvironmentVariables
{
private readonly IApmLogger _logger;
public EnvironmentVariables(IApmLogger logger) => _logger = logger.Scoped(nameof(EnvironmentVariables));

public IDictionary GetEnvironmentVariables()
{
try
{
return Environment.GetEnvironmentVariables();
}
catch (Exception e)
{
_logger.Error()?.LogException(e, "could not get environment variables");
return null;
}
}
}
}
5 changes: 3 additions & 2 deletions src/Elastic.Apm/Report/PayloadSenderV2.cs
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ public PayloadSenderV2(
IApmServerInfo apmServerInfo,
HttpMessageHandler httpMessageHandler = null,
string dbgName = null,
bool isEnabled = true
bool isEnabled = true,
IEnvironmentVariables environmentVariables = null
)
: base(isEnabled, logger, ThisClassName, service, config, httpMessageHandler)
{
Expand All @@ -76,7 +77,7 @@ public PayloadSenderV2(

System = system;

_cloudMetadataProviderCollection = new CloudMetadataProviderCollection(config.CloudProvider, _logger);
_cloudMetadataProviderCollection = new CloudMetadataProviderCollection(config.CloudProvider, _logger, environmentVariables);
_apmServerInfo = apmServerInfo;
_metadata = new Metadata { Service = service, System = System };
foreach (var globalLabelKeyValue in config.GlobalLabels) _metadata.Labels.Add(globalLabelKeyValue.Key, globalLabelKeyValue.Value);
Expand Down
152 changes: 152 additions & 0 deletions test/Elastic.Apm.Feature.Tests/CloudProviderSteps.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
// 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.Generic;
using System.IO;
using System.Net;
using System.Net.Http;
using System.Threading;
using Elastic.Apm.Api;
using Elastic.Apm.Logging;
using Elastic.Apm.Report;
using Elastic.Apm.Tests.Utilities;
using Elastic.Apm.Tests.Utilities.XUnit;
using FluentAssertions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using RichardSzalay.MockHttp;
using TechTalk.SpecFlow;
using Xunit.Abstractions;
using static Elastic.Apm.BackendComm.BackendCommUtils.ApmServerEndpoints;
using MockHttpMessageHandler = RichardSzalay.MockHttp.MockHttpMessageHandler;

namespace Elastic.Apm.Feature.Tests
{
[Binding]
public class CloudProviderSteps
{
private static readonly JsonSerializer _serializer = new JsonSerializer();
private readonly ScenarioContext _scenarioContext;

public CloudProviderSteps(ScenarioContext scenarioContext) => _scenarioContext = scenarioContext;

[Given(@"^an instrumented application is configured to collect cloud provider metadata for (.*?)$")]
public void AgentWithCloudMetadata(string cloudProvider)
{
var output = _scenarioContext.ScenarioContainer.Resolve<ITestOutputHelper>();
var logger = new XUnitLogger(LogLevel.Trace, output);
var config = new MockConfigSnapshot(logger, cloudProvider: cloudProvider);

var payloadCollector = new PayloadCollector();
_scenarioContext.Set(payloadCollector);

var handler = new MockHttpMessageHandler();
handler.When(BuildIntakeV2EventsAbsoluteUrl(config.ServerUrl).AbsoluteUri)
.Respond(r =>
{
var json = r.Content.ReadAsStringAsync().Result;
if (json.Contains("\"metadata\""))
{
payloadCollector.Request = ParseJObjects(json);
payloadCollector.WaitHandle.Set();
}

return new HttpResponseMessage(HttpStatusCode.OK);
});

var environmentVariables = new TestEnvironmentVariables();
_scenarioContext.Set(environmentVariables);

var payloadSender = new PayloadSenderV2(
logger,
config,
Service.GetDefaultService(config, new NoopLogger()),
new Api.System(),
MockApmServerInfo.Version710,
handler,
environmentVariables: environmentVariables);

var lazyAgent = new Lazy<ApmAgent>(() =>
new ApmAgent(new TestAgentComponents(logger, config, payloadSender)));

_scenarioContext.Set(lazyAgent);
}

[Given("^the following environment variables are present$")]
public void EnvironmentVariablesSet(Table table)
{
var environmentVariables = _scenarioContext.Get<TestEnvironmentVariables>();

foreach(var row in table.Rows)
environmentVariables[row[0]] = row[1];
}

[When("^cloud metadata is collected$")]
public void CollectCloudMetadata()
{
var lazyAgent = _scenarioContext.Get<Lazy<ApmAgent>>();
// create the agent and capture a transaction to send metadata
var agent = lazyAgent.Value;
agent.Tracer.CaptureTransaction("Transaction", "feature", () => { });

var payloadCollector = _scenarioContext.Get<PayloadCollector>();

// wait for the wait handle to be signalled
var timeout = TimeSpan.FromSeconds(30);
if (!payloadCollector.WaitHandle.Wait(timeout))
throw new Exception($"Did not receive payload within {timeout}");
}

[Then("^cloud metadata is not null$")]
public void CloudMetadataIsNotNull()
{
var payloadCollector = _scenarioContext.Get<PayloadCollector>();

payloadCollector.Request.Should().NotBeNull();
var cloudMetadata = payloadCollector.Request[0]["metadata"]["cloud"];
cloudMetadata.Should().NotBeNull();
}

[Then("^cloud metadata is null$")]
public void CloudMetadataIsNull()
{
var payloadCollector = _scenarioContext.Get<PayloadCollector>();

payloadCollector.Request.Should().NotBeNull();
var cloudMetadata = payloadCollector.Request[0]["metadata"]["cloud"];
cloudMetadata.Should().BeNull();
}

[Then("^cloud metadata '(.*?)' is '(.*?)'$")]
public void CloudMetadataKeyEqualsValue(string key, string value)
{
var payloadCollector = _scenarioContext.Get<PayloadCollector>();
var token = payloadCollector.Request[0].SelectToken($"metadata.cloud.{key}");

token.Should().NotBeNull();
token.Value<string>().Should().Be(value);
}

private class PayloadCollector
{
public ManualResetEventSlim WaitHandle { get; }

public PayloadCollector() => WaitHandle = new ManualResetEventSlim(false);

public List<JObject> Request { get; set; }
}

private static List<JObject> ParseJObjects(string json)
{
var jObjects = new List<JObject>();
using var stringReader = new StringReader(json);
using var jsonReader = new JsonTextReader(stringReader) { SupportMultipleContent = true };
while (jsonReader.Read())
jObjects.Add(_serializer.Deserialize<JObject>(jsonReader));
return jObjects;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
<ItemGroup>
<PackageReference Include="FluentAssertions" Version="5.6.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.3" />
<PackageReference Include="RichardSzalay.MockHttp" Version="6.0.0" />
<PackageReference Include="SpecFlow.Tools.MsBuild.Generation" Version="3.5.5" />
<PackageReference Include="SpecFlow.xUnit" Version="3.5.5" />
<PackageReference Include="xunit" Version="2.4.1" />
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
Feature: Extracting Metadata for Azure App Service

Background:
Given an instrumented application is configured to collect cloud provider metadata for azure

Scenario Outline: Azure App Service with all environment variables present in expected format
Given the following environment variables are present
| name | value |
| WEBSITE_OWNER_NAME | <WEBSITE_OWNER_NAME> |
| WEBSITE_RESOURCE_GROUP | resource_group |
| WEBSITE_SITE_NAME | site_name |
| WEBSITE_INSTANCE_ID | instance_id |
When cloud metadata is collected
Then cloud metadata is not null
And cloud metadata 'account.id' is 'f5940f10-2e30-3e4d-a259-63451ba6dae4'
And cloud metadata 'provider' is 'azure'
And cloud metadata 'instance.id' is 'instance_id'
And cloud metadata 'instance.name' is 'site_name'
And cloud metadata 'project.name' is 'resource_group'
And cloud metadata 'region' is 'AustraliaEast'
Examples:
| WEBSITE_OWNER_NAME |
| f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace |
| f5940f10-2e30-3e4d-a259-63451ba6dae4+appsvc_linux_australiaeast-AustraliaEastwebspace-Linux |

# WEBSITE_OWNER_NAME is expected to include a + character
Scenario: WEBSITE_OWNER_NAME environment variable not expected format
Given the following environment variables are present
| name | value |
| WEBSITE_OWNER_NAME | f5940f10-2e30-3e4d-a259-63451ba6dae4-elastic-apm-AustraliaEastwebspace |
| WEBSITE_RESOURCE_GROUP | resource_group |
| WEBSITE_SITE_NAME | site_name |
| WEBSITE_INSTANCE_ID | instance_id |
When cloud metadata is collected
Then cloud metadata is null

Scenario: Missing WEBSITE_OWNER_NAME environment variable
Given the following environment variables are present
| name | value |
| WEBSITE_RESOURCE_GROUP | resource_group |
| WEBSITE_SITE_NAME | site_name |
| WEBSITE_INSTANCE_ID | instance_id |
When cloud metadata is collected
Then cloud metadata is null

Scenario: Missing WEBSITE_RESOURCE_GROUP environment variable
Given the following environment variables are present
| name | value |
| WEBSITE_OWNER_NAME | f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace |
| WEBSITE_SITE_NAME | site_name |
| WEBSITE_INSTANCE_ID | instance_id |
When cloud metadata is collected
Then cloud metadata is null

Scenario: Missing WEBSITE_SITE_NAME environment variable
Given the following environment variables are present
| name | value |
| WEBSITE_OWNER_NAME | f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace |
| WEBSITE_RESOURCE_GROUP | resource_group |
| WEBSITE_INSTANCE_ID | instance_id |
When cloud metadata is collected
Then cloud metadata is null

Scenario: Missing WEBSITE_INSTANCE_ID environment variable
Given the following environment variables are present
| name | value |
| WEBSITE_OWNER_NAME | f5940f10-2e30-3e4d-a259-63451ba6dae4+elastic-apm-AustraliaEastwebspace |
| WEBSITE_RESOURCE_GROUP | resource_group |
| WEBSITE_SITE_NAME | site_name |
When cloud metadata is collected
Then cloud metadata is null
Loading

0 comments on commit d9c163f

Please sign in to comment.