diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTraceExporter.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTraceExporter.cs index c6b32c0c5f295..ccf1b42ef334d 100644 --- a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTraceExporter.cs +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTraceExporter.cs @@ -13,16 +13,20 @@ namespace OpenTelemetry.Exporter.AzureMonitor { public class AzureMonitorTraceExporter : ActivityExporter { - private readonly AzureMonitorTransmitter AzureMonitorTransmitter; + private readonly ITransmitter Transmitter; private readonly AzureMonitorExporterOptions options; private readonly string instrumentationKey; - public AzureMonitorTraceExporter(AzureMonitorExporterOptions options) + public AzureMonitorTraceExporter(AzureMonitorExporterOptions options) : this(options, new AzureMonitorTransmitter(options)) + { + } + + internal AzureMonitorTraceExporter(AzureMonitorExporterOptions options, ITransmitter transmitter) { this.options = options ?? throw new ArgumentNullException(nameof(options)); ConnectionString.ConnectionStringParser.GetValues(this.options.ConnectionString, out this.instrumentationKey, out _); - this.AzureMonitorTransmitter = new AzureMonitorTransmitter(options); + this.Transmitter = transmitter; } /// @@ -34,7 +38,7 @@ public override ExportResult Export(in Batch batch) // TODO: Handle return value, it can be converted as metrics. // TODO: Validate CancellationToken and async pattern here. - this.AzureMonitorTransmitter.TrackAsync(telemetryItems, false, CancellationToken.None).EnsureCompleted(); + this.Transmitter.TrackAsync(telemetryItems, false, CancellationToken.None).EnsureCompleted(); return ExportResult.Success; } catch (Exception ex) diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTransmitter.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTransmitter.cs index f8da74aedd24d..9ebf50a592a81 100644 --- a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTransmitter.cs +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/AzureMonitorTransmitter.cs @@ -16,7 +16,7 @@ namespace OpenTelemetry.Exporter.AzureMonitor /// /// This class encapsulates transmitting a collection of to the configured Ingestion Endpoint. /// - internal class AzureMonitorTransmitter + internal class AzureMonitorTransmitter : ITransmitter { private readonly ApplicationInsightsRestClient applicationInsightsRestClient; private readonly AzureMonitorExporterOptions options; diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/ITransmitter.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/ITransmitter.cs new file mode 100644 index 0000000000000..a88430585df0f --- /dev/null +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/ITransmitter.cs @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Generic; +using System.Threading; +using System.Threading.Tasks; + +using OpenTelemetry.Exporter.AzureMonitor.Models; + +namespace OpenTelemetry.Exporter.AzureMonitor +{ + internal interface ITransmitter + { + /// + /// Sent telemetry and return the number of items Accepted. + /// + ValueTask TrackAsync(IEnumerable telemetryItems, bool async, CancellationToken cancellationToken); + } +} diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/Properties/AssemblyInfo.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/Properties/AssemblyInfo.cs index e4d401bfc2169..fc00bbf242efc 100644 --- a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/Properties/AssemblyInfo.cs +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/src/Properties/AssemblyInfo.cs @@ -3,4 +3,8 @@ using System.Runtime.CompilerServices; -[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.AzureMonitor.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] \ No newline at end of file +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.AzureMonitor.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] +[assembly: InternalsVisibleTo("OpenTelemetry.Exporter.AzureMonitor.Integration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100d15ddcb29688295338af4b7686603fe614abd555e09efba8fb88ee09e1f7b1ccaeed2e8f823fa9eef3fdd60217fc012ea67d2479751a0b8c087a4185541b851bd8b16f8d91b840e51b1cb0ba6fe647997e57429265e85ef62d565db50a69ae1647d54d7bd855e4db3d8a91510e5bcbd0edfbbecaa20a7bd9ae74593daa7b11b4")] + +// Moq +[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/OpenTelemetryTests.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/OpenTelemetryTests.cs new file mode 100644 index 0000000000000..098a0a76bfbff --- /dev/null +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/OpenTelemetryTests.cs @@ -0,0 +1,72 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +using OpenTelemetry.Exporter.AzureMonitor.Integration.Tests.TestFramework; + +using Xunit; +using Xunit.Abstractions; + +namespace OpenTelemetry.Exporter.AzureMonitor.Integration.Tests +{ + public class OpenTelemetryTests : IClassFixture> + { + private readonly OpenTelemetryWebApplicationFactory factory; + private readonly ITestOutputHelper output; + + public OpenTelemetryTests(OpenTelemetryWebApplicationFactory factory, ITestOutputHelper output) + { + this.factory = factory; + this.output = output; + } + + /// + /// This test validates that when an app instrumented with the AzureMonitorExporter receives an HTTP request, + /// A TelemetryItem is created matching that request. + /// + [Fact] + public async Task ProofOfConcept() + { + string testValue = Guid.NewGuid().ToString(); + + // Arrange + var client = this.factory.CreateClient(); + + //// Act + var response = await client.GetAsync($"api/home/{testValue}"); + + // Shutdown + response.EnsureSuccessStatusCode(); + Task.Delay(100).Wait(); //TODO: HOW TO REMOVE THIS WAIT? + this.factory.ForceFlush(); + + // Assert + Assert.True(this.factory.TelemetryItems.Any(), "test project did not capture telemetry"); + + PrintTelemetryItems(this.factory.TelemetryItems); + var item = this.factory.TelemetryItems.Single(); + var baseData = (Models.RequestData)item.Data.BaseData; + Assert.True(baseData.Url.EndsWith(testValue), "it is expected that the recorded TelemetryItem matches the value of testValue."); + } + + /// + /// This uses the XUnit ITestOutputHelper to print details to the output of the test run. + /// + private void PrintTelemetryItems(IEnumerable telemetryItems) + { + foreach (var item in telemetryItems) + { + this.output.WriteLine(item.Name); + + if (item.Data.BaseData is Models.RequestData requestData) + { + this.output.WriteLine($"\t{requestData.Url}"); + } + } + } + } +} diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/Properties/AssemblyInfo.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000000..c712fb98d8578 --- /dev/null +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/Properties/AssemblyInfo.cs @@ -0,0 +1,5 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using Xunit; +[assembly: CollectionBehavior(CollectionBehavior.CollectionPerAssembly, DisableTestParallelization = true)] diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/MockTransmitter.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/MockTransmitter.cs new file mode 100644 index 0000000000000..bc54cfc9ff5e7 --- /dev/null +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/MockTransmitter.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; + +using OpenTelemetry.Exporter.AzureMonitor.Models; + +namespace OpenTelemetry.Exporter.AzureMonitor.Integration.Tests.TestFramework +{ + public class MockTransmitter : ITransmitter + { + public ConcurrentBag TelemetryItems = new ConcurrentBag(); + + public ValueTask TrackAsync(IEnumerable telemetryItems, bool async, CancellationToken cancellationToken) + { + foreach (var telemetryItem in telemetryItems) + { + this.TelemetryItems.Add(telemetryItem); + } + + return new ValueTask(Task.FromResult(telemetryItems.Count())); + } + } +} diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/OpenTelemetryWebApplicationFactory.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/OpenTelemetryWebApplicationFactory.cs new file mode 100644 index 0000000000000..73ed8a614172b --- /dev/null +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.Integration.Tests/TestFramework/OpenTelemetryWebApplicationFactory.cs @@ -0,0 +1,46 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System.Collections.Concurrent; + +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.Mvc.Testing; +using Microsoft.Extensions.DependencyInjection; + +using OpenTelemetry.Exporter.AzureMonitor.Models; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Exporter.AzureMonitor.Integration.Tests.TestFramework +{ + /// + /// This class implements and will configure the for OpenTelemetry and the . + /// Here we mock the to capture telemetry that would have been sent to the ingestion service. + /// We also mock the which would have been received from the ingestion service. + /// + /// Startup class from the application to be used during this test. + public class OpenTelemetryWebApplicationFactory : WebApplicationFactory where TStartup : class + { + public ConcurrentBag TelemetryItems => this.Transmitter.TelemetryItems; + + private const string EmptyConnectionString = "InstrumentationKey=00000000-0000-0000-0000-000000000000"; + private readonly MockTransmitter Transmitter = new MockTransmitter(); + private ActivityProcessor ActivityProcessor; + + protected override void ConfigureWebHost(IWebHostBuilder builder) + { + this.ActivityProcessor = new BatchExportActivityProcessor(new AzureMonitorTraceExporter( + options: new AzureMonitorExporterOptions + { + ConnectionString = EmptyConnectionString, + }, + transmitter: this.Transmitter)); + + builder.ConfigureServices(services => services.AddOpenTelemetryTracing((builder) => builder + .AddAspNetCoreInstrumentation() + .AddHttpClientInstrumentation() + .AddProcessor(this.ActivityProcessor))); + } + + public void ForceFlush() => this.ActivityProcessor.ForceFlush(); + } +} diff --git a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.UnitTest/AzureMonitorTraceExporterTests.cs b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.UnitTest/AzureMonitorTraceExporterTests.cs index 9768b20c5ff1b..cc37eeb7b76c6 100644 --- a/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.UnitTest/AzureMonitorTraceExporterTests.cs +++ b/sdk/monitor/OpenTelemetry.Exporter.AzureMonitor/tests/OpenTelemetry.Exporter.AzureMonitor.UnitTest/AzureMonitorTraceExporterTests.cs @@ -61,7 +61,7 @@ private void GetInternalFields(AzureMonitorTraceExporter exporter, out string ik .ToString(); var transmitter = typeof(AzureMonitorTraceExporter) - .GetField("AzureMonitorTransmitter", BindingFlags.Instance | BindingFlags.NonPublic) + .GetField("Transmitter", BindingFlags.Instance | BindingFlags.NonPublic) .GetValue(exporter); var serviceRestClient = typeof(AzureMonitorTransmitter)