diff --git a/.github/dependabot.yml b/.github/dependabot.yml index d6afe9c5aa..be422efc08 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -178,6 +178,12 @@ updates: interval: "daily" open-pull-requests-limit: 20 + - package-ecosystem: nuget + directory: /test/test-applications/integrations/TestApplication.Plugins + schedule: + interval: "daily" + open-pull-requests-limit: 20 + - package-ecosystem: nuget directory: /test/test-applications/integrations/TestApplication.Smoke schedule: diff --git a/OpenTelemetry.AutoInstrumentation.sln b/OpenTelemetry.AutoInstrumentation.sln index 70a3e5f3c4..a048eba46a 100644 --- a/OpenTelemetry.AutoInstrumentation.sln +++ b/OpenTelemetry.AutoInstrumentation.sln @@ -127,6 +127,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.MongoDB", " EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.SqlClient", "test\test-applications\integrations\TestApplication.SqlClient\TestApplication.SqlClient.csproj", "{2120CFA2-9D16-45F0-A333-B0385C371509}" EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestApplication.Plugins", "test\test-applications\integrations\TestApplication.Plugins\TestApplication.Plugins.csproj", "{42BABA6C-1954-4F52-85C8-D336C00F06D9}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -503,6 +505,18 @@ Global {2120CFA2-9D16-45F0-A333-B0385C371509}.Release|x64.Build.0 = Release|x64 {2120CFA2-9D16-45F0-A333-B0385C371509}.Release|x86.ActiveCfg = Release|x86 {2120CFA2-9D16-45F0-A333-B0385C371509}.Release|x86.Build.0 = Release|x86 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|Any CPU.ActiveCfg = Debug|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|Any CPU.Build.0 = Debug|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|x64.ActiveCfg = Debug|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|x64.Build.0 = Debug|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|x86.ActiveCfg = Debug|x86 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Debug|x86.Build.0 = Debug|x86 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|Any CPU.ActiveCfg = Release|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|Any CPU.Build.0 = Release|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|x64.ActiveCfg = Release|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|x64.Build.0 = Release|x64 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|x86.ActiveCfg = Release|x86 + {42BABA6C-1954-4F52-85C8-D336C00F06D9}.Release|x86.Build.0 = Release|x86 EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -545,6 +559,7 @@ Global {6ACF2A48-427F-4EF2-81B5-EE9CDB0476C6} = {BAF8F246-3645-42AD-B1D0-0F7EAFBAB34A} {3B77F975-BF66-4218-9982-F48D3271DDFE} = {E409ADD3-9574-465C-AB09-4324D205CC7C} {2120CFA2-9D16-45F0-A333-B0385C371509} = {E409ADD3-9574-465C-AB09-4324D205CC7C} + {42BABA6C-1954-4F52-85C8-D336C00F06D9} = {E409ADD3-9574-465C-AB09-4324D205CC7C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {160A1D00-1F5B-40F8-A155-621B4459D78F} diff --git a/test/IntegrationTests/AspNetTests.cs b/test/IntegrationTests/AspNetTests.cs index 894a132bfa..9b39182d28 100644 --- a/test/IntegrationTests/AspNetTests.cs +++ b/test/IntegrationTests/AspNetTests.cs @@ -45,20 +45,17 @@ public async Task SubmitsTraces() { Assert.True(EnvironmentTools.IsWindowsAdministrator(), "This test requires Windows Administrator privileges."); - var agentPort = TcpPortProvider.GetOpenPort(); - var webPort = TcpPortProvider.GetOpenPort(); - - var testSettings = new TestSettings - { - TracesSettings = new TracesSettings { Port = agentPort } - }; - // Using "*" as host requires Administrator. This is needed to make the mock collector endpoint // accessible to the Windows docker container where the test application is executed by binding // the endpoint to all network interfaces. In order to do that it is necessary to open the port // on the firewall. - using var fwPort = FirewallHelper.OpenWinPort(agentPort, Output); - using var agent = new MockZipkinCollector(Output, agentPort, host: "*"); + using var agent = new MockZipkinCollector(Output, host: "*"); + using var fwPort = FirewallHelper.OpenWinPort(agent.Port, Output); + var testSettings = new TestSettings + { + TracesSettings = new TracesSettings { Port = agent.Port } + }; + var webPort = TcpPortProvider.GetOpenPort(); using var container = await StartContainerAsync(testSettings, webPort); var client = new HttpClient(); @@ -81,21 +78,19 @@ public async Task SubmitsTraces() [Trait("Containers", "Windows")] public async Task SubmitMetrics() { - var collectorPort = TcpPortProvider.GetOpenPort(); - var webPort = TcpPortProvider.GetOpenPort(); const int expectedMetricRequests = 1; - var testSettings = new TestSettings - { - MetricsSettings = new MetricsSettings { Port = collectorPort }, - }; - // Using "*" as host requires Administrator. This is needed to make the mock collector endpoint // accessible to the Windows docker container where the test application is executed by binding // the endpoint to all network interfaces. In order to do that it is necessary to open the port // on the firewall. - using var fwPort = FirewallHelper.OpenWinPort(collectorPort, Output); - using var collector = new MockCollector(Output, collectorPort, host: "*"); + using var collector = new MockMetricsCollector(Output, host: "*"); + using var fwPort = FirewallHelper.OpenWinPort(collector.Port, Output); + var testSettings = new TestSettings + { + MetricsSettings = new MetricsSettings { Port = collector.Port }, + }; + var webPort = TcpPortProvider.GetOpenPort(); using var container = await StartContainerAsync(testSettings, webPort); var client = new HttpClient(); diff --git a/test/IntegrationTests/GraphQLTests.cs b/test/IntegrationTests/GraphQLTests.cs index f0ed5c649b..243767ebef 100644 --- a/test/IntegrationTests/GraphQLTests.cs +++ b/test/IntegrationTests/GraphQLTests.cs @@ -82,10 +82,8 @@ public void SubmitsTraces() { SetEnvironmentVariable("OTEL_SERVICE_NAME", ServiceName); - int agentPort = TcpPortProvider.GetOpenPort(); int aspNetCorePort = TcpPortProvider.GetOpenPort(); - - using (var agent = new MockZipkinCollector(Output, agentPort)) + using (var agent = new MockZipkinCollector(Output)) using (Process process = StartTestApplication(agent.Port, arguments: null, packageVersion: string.Empty, aspNetCorePort: aspNetCorePort)) { if (process.HasExited) diff --git a/test/IntegrationTests/Helpers/EnvironmentHelper.cs b/test/IntegrationTests/Helpers/EnvironmentHelper.cs index 4e065e5642..940a953988 100644 --- a/test/IntegrationTests/Helpers/EnvironmentHelper.cs +++ b/test/IntegrationTests/Helpers/EnvironmentHelper.cs @@ -207,7 +207,7 @@ public void SetEnvironmentVariables( if (testSettings.MetricsSettings != null) { environmentVariables["OTEL_METRICS_EXPORTER"] = testSettings.MetricsSettings.Exporter; - environmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = $"http://127.0.0.1:{testSettings.MetricsSettings.Port}"; + environmentVariables["OTEL_EXPORTER_OTLP_ENDPOINT"] = $"http://localhost:{testSettings.MetricsSettings.Port}"; } // for ASP.NET Core test applications, set the server's port diff --git a/test/IntegrationTests/Helpers/MockCollector.cs b/test/IntegrationTests/Helpers/MockCollector.cs deleted file mode 100644 index d2f2925362..0000000000 --- a/test/IntegrationTests/Helpers/MockCollector.cs +++ /dev/null @@ -1,259 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Collections.Generic; -using System.Collections.Immutable; -using System.Collections.Specialized; -using System.Linq; -using System.Net; -using System.Text; -using System.Threading; -using Google.Protobuf; -using IntegrationTests.Helpers.Models; -using Opentelemetry.Proto.Collector.Metrics.V1; -using Xunit.Abstractions; - -namespace IntegrationTests.Helpers; - -public class MockCollector : IDisposable -{ - private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(20); - - private readonly ITestOutputHelper _output; - private readonly HttpListener _listener; - private readonly Thread _listenerThread; - - public MockCollector(ITestOutputHelper output, int port = 4318, int retries = 5, string host = "127.0.0.1") - { - _output = output; - - // try up to 5 consecutive ports before giving up - while (true) - { - // seems like we can't reuse a listener if it fails to start, - // so create a new listener each time we retry - var listener = new HttpListener(); - - try - { - listener.Start(); - string prefix = new UriBuilder("http", host, port).ToString(); - listener.Prefixes.Add(prefix); - - // successfully listening - Port = port; - _listener = listener; - - _listenerThread = new Thread(HandleHttpRequests); - _listenerThread.Start(); - - WriteOutput($"Running on port '{Port}'"); - - return; - } - catch (HttpListenerException) when (retries > 0) - { - // only catch the exception if there are retries left - port++; - retries--; - } - - // always close listener if exception is thrown, - // whether it was caught or not - listener.Close(); - - WriteOutput("Listener shut down. Could not find available port."); - } - } - - public event EventHandler> RequestReceived; - - public event EventHandler> RequestDeserialized; - - /// - /// Gets or sets a value indicating whether to skip deserialization of metrics. - /// - public bool ShouldDeserializeMetrics { get; set; } = true; - - /// - /// Gets the TCP port that this Agent is listening on. - /// Can be different from 's initialPort - /// parameter if listening on that port fails. - /// - public int Port { get; } - - /// - /// Gets the filters used to filter out metrics we don't want to look at for a test. - /// - public List> MetricFilters { get; private set; } = new List>(); - - public IImmutableList MetricsMessages { get; private set; } = ImmutableList.Empty; - - public IImmutableList RequestHeaders { get; private set; } = ImmutableList.Empty; - - /// - /// Wait for the given number of metric requests to appear. - /// - /// The expected number of metric requests. - /// The timeout - /// The list of metric requests. - public IImmutableList WaitForMetrics( - int count, - TimeSpan? timeout = null) - { - timeout ??= DefaultWaitTimeout; - var deadline = DateTime.Now.Add(timeout.Value); - - IImmutableList relevantMetricRequests = ImmutableList.Empty; - - while (DateTime.Now < deadline) - { - relevantMetricRequests = - MetricsMessages - .Where(m => MetricFilters.All(shouldReturn => shouldReturn(m))) - .ToImmutableList(); - - if (relevantMetricRequests.Count >= count) - { - break; - } - - Thread.Sleep(500); - } - - return relevantMetricRequests; - } - - public void Dispose() - { - WriteOutput($"Shutting down. Total metric requests received: '{MetricsMessages.Count}'"); - _listener?.Stop(); - } - - protected virtual void OnRequestReceived(HttpListenerContext context) - { - RequestReceived?.Invoke(this, new EventArgs(context)); - } - - protected virtual void OnRequestDeserialized(ExportMetricsServiceRequest metricsRequest) - { - RequestDeserialized?.Invoke(this, new EventArgs(metricsRequest)); - } - - private void AssertHeader( - NameValueCollection headers, - string headerKey, - Func assertion) - { - var header = headers.Get(headerKey); - - if (string.IsNullOrEmpty(header)) - { - throw new Exception($"Every submission to the agent should have a {headerKey} header."); - } - - if (!assertion(header)) - { - throw new Exception($"Failed assertion for {headerKey} on {header}"); - } - } - - private void HandleHttpRequests() - { - while (_listener.IsListening) - { - try - { - var ctx = _listener.GetContext(); - OnRequestReceived(ctx); - - if (ctx.Request.RawUrl.Equals("/healthz", StringComparison.OrdinalIgnoreCase)) - { - CreateHealthResponse(ctx); - - continue; - } - - if (ctx.Request.RawUrl.Equals("/v1/metrics", StringComparison.OrdinalIgnoreCase)) - { - if (ShouldDeserializeMetrics) - { - var metricsMessage = ExportMetricsServiceRequest.Parser.ParseFrom(ctx.Request.InputStream); - OnRequestDeserialized(metricsMessage); - - lock (this) - { - // we only need to lock when replacing the metric collection, - // not when reading it because it is immutable - MetricsMessages = MetricsMessages.Add(metricsMessage); - RequestHeaders = RequestHeaders.Add(new NameValueCollection(ctx.Request.Headers)); - } - } - - // NOTE: HttpStreamRequest doesn't support Transfer-Encoding: Chunked - // (Setting content-length avoids that) - ctx.Response.ContentType = "application/x-protobuf"; - ctx.Response.StatusCode = (int)HttpStatusCode.OK; - var responseMessage = new ExportMetricsServiceResponse(); - ctx.Response.ContentLength64 = responseMessage.CalculateSize(); - responseMessage.WriteTo(ctx.Response.OutputStream); - ctx.Response.Close(); - continue; - } - - // We received an unsupported request - ctx.Response.StatusCode = (int)HttpStatusCode.NotImplemented; - ctx.Response.Close(); - } - catch (HttpListenerException) - { - // listener was stopped, - // ignore to let the loop end and the method return - } - catch (ObjectDisposedException) - { - // the response has been already disposed. - } - catch (InvalidOperationException) - { - // this can occur when setting Response.ContentLength64, with the framework claiming that the response has already been submitted - // for now ignore, and we'll see if this introduces downstream issues - } - catch (Exception) when (!_listener.IsListening) - { - // we don't care about any exception when listener is stopped - } - } - } - - private void WriteOutput(string msg) - { - const string name = nameof(MockCollector); - - _output.WriteLine($"[{name}]: {msg}"); - } - - private void CreateHealthResponse(HttpListenerContext ctx) - { - ctx.Response.ContentType = "text/plain"; - var buffer = Encoding.UTF8.GetBytes("OK"); - ctx.Response.ContentLength64 = buffer.LongLength; - ctx.Response.OutputStream.Write(buffer, 0, buffer.Length); - ctx.Response.StatusCode = (int)HttpStatusCode.OK; - ctx.Response.Close(); - } -} diff --git a/test/IntegrationTests/Helpers/MockMetricsCollector.cs b/test/IntegrationTests/Helpers/MockMetricsCollector.cs new file mode 100644 index 0000000000..abed2b50ac --- /dev/null +++ b/test/IntegrationTests/Helpers/MockMetricsCollector.cs @@ -0,0 +1,174 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Collections.Specialized; +using System.Linq; +using System.Net; +using System.Text; +using System.Threading; +using Google.Protobuf; +using IntegrationTests.Helpers.Models; +using Opentelemetry.Proto.Collector.Metrics.V1; +using Xunit.Abstractions; + +namespace IntegrationTests.Helpers; + +public class MockMetricsCollector : IDisposable +{ + private static readonly TimeSpan DefaultWaitTimeout = TimeSpan.FromSeconds(20); + + private readonly ITestOutputHelper _output; + private readonly TestHttpListener _listener; + + public MockMetricsCollector(ITestOutputHelper output, string host = "localhost") + { + _output = output; + _listener = new(output, HandleHttpRequests, host); + } + + public event EventHandler> RequestReceived; + + public event EventHandler> RequestDeserialized; + + /// + /// Gets or sets a value indicating whether to skip deserialization of metrics. + /// + public bool ShouldDeserializeMetrics { get; set; } = true; + + /// + /// Gets the TCP port that this collector is listening on. + /// + public int Port { get => _listener.Port; } + + /// + /// Gets the filters used to filter out metrics we don't want to look at for a test. + /// + public List> MetricFilters { get; private set; } = new List>(); + + public IImmutableList MetricsMessages { get; private set; } = ImmutableList.Empty; + + public IImmutableList RequestHeaders { get; private set; } = ImmutableList.Empty; + + /// + /// Wait for the given number of metric requests to appear. + /// + /// The expected number of metric requests. + /// The timeout + /// The list of metric requests. + public IImmutableList WaitForMetrics( + int count, + TimeSpan? timeout = null) + { + timeout ??= DefaultWaitTimeout; + var deadline = DateTime.Now.Add(timeout.Value); + + IImmutableList relevantMetricRequests = ImmutableList.Empty; + + while (DateTime.Now < deadline) + { + relevantMetricRequests = + MetricsMessages + .Where(m => MetricFilters.All(shouldReturn => shouldReturn(m))) + .ToImmutableList(); + + if (relevantMetricRequests.Count >= count) + { + break; + } + + Thread.Sleep(500); + } + + return relevantMetricRequests; + } + + public void Dispose() + { + WriteOutput($"Shutting down. Total metric requests received: '{MetricsMessages.Count}'"); + _listener.Dispose(); + } + + protected virtual void OnRequestReceived(HttpListenerContext context) + { + RequestReceived?.Invoke(this, new EventArgs(context)); + } + + protected virtual void OnRequestDeserialized(ExportMetricsServiceRequest metricsRequest) + { + RequestDeserialized?.Invoke(this, new EventArgs(metricsRequest)); + } + + private void HandleHttpRequests(HttpListenerContext ctx) + { + OnRequestReceived(ctx); + + if (ctx.Request.RawUrl.Equals("/healthz", StringComparison.OrdinalIgnoreCase)) + { + CreateHealthResponse(ctx); + return; + } + + if (ctx.Request.RawUrl.Equals("/v1/metrics", StringComparison.OrdinalIgnoreCase)) + { + if (ShouldDeserializeMetrics) + { + var metricsMessage = ExportMetricsServiceRequest.Parser.ParseFrom(ctx.Request.InputStream); + OnRequestDeserialized(metricsMessage); + + lock (this) + { + // we only need to lock when replacing the metric collection, + // not when reading it because it is immutable + MetricsMessages = MetricsMessages.Add(metricsMessage); + RequestHeaders = RequestHeaders.Add(new NameValueCollection(ctx.Request.Headers)); + } + } + + // NOTE: HttpStreamRequest doesn't support Transfer-Encoding: Chunked + // (Setting content-length avoids that) + ctx.Response.ContentType = "application/x-protobuf"; + ctx.Response.StatusCode = (int)HttpStatusCode.OK; + var responseMessage = new ExportMetricsServiceResponse(); + ctx.Response.ContentLength64 = responseMessage.CalculateSize(); + responseMessage.WriteTo(ctx.Response.OutputStream); + ctx.Response.Close(); + return; + } + + // We received an unsupported request + ctx.Response.StatusCode = (int)HttpStatusCode.NotImplemented; + ctx.Response.Close(); + } + + private void CreateHealthResponse(HttpListenerContext ctx) + { + ctx.Response.ContentType = "text/plain"; + var buffer = Encoding.UTF8.GetBytes("OK"); + ctx.Response.ContentLength64 = buffer.LongLength; + ctx.Response.OutputStream.Write(buffer, 0, buffer.Length); + ctx.Response.StatusCode = (int)HttpStatusCode.OK; + ctx.Response.Close(); + } + + private void WriteOutput(string msg) + { + const string name = nameof(MockMetricsCollector); + _output.WriteLine($"[{name}]: {msg}"); + } +} diff --git a/test/IntegrationTests/Helpers/MockZipkinCollector.cs b/test/IntegrationTests/Helpers/MockZipkinCollector.cs index 72e1ca069e..40009b2f02 100644 --- a/test/IntegrationTests/Helpers/MockZipkinCollector.cs +++ b/test/IntegrationTests/Helpers/MockZipkinCollector.cs @@ -35,53 +35,12 @@ public class MockZipkinCollector : IDisposable private static readonly TimeSpan DefaultSpanWaitTimeout = TimeSpan.FromSeconds(20); private readonly ITestOutputHelper _output; - private readonly HttpListener _listener; - private readonly Thread _listenerThread; + private readonly TestHttpListener _listener; - public MockZipkinCollector(ITestOutputHelper output, int port = 9411, int retries = 5, string host = "localhost") + public MockZipkinCollector(ITestOutputHelper output, string host = "localhost") { _output = output; - - // try up to 5 consecutive ports before giving up - while (true) - { - // seems like we can't reuse a listener if it fails to start, - // so create a new listener each time we retry - var listener = new HttpListener(); - - try - { - listener.Start(); - - // See https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistenerprefixcollection.add?redirectedfrom=MSDN&view=net-6.0#remarks - // for info about the host value. - string prefix = new UriBuilder("http", host, port, "/api/v2/spans/").ToString(); - listener.Prefixes.Add(prefix); - - // successfully listening - Port = port; - _listener = listener; - - _listenerThread = new Thread(HandleHttpRequests); - _listenerThread.Start(); - - WriteOutput($"Running on port '{Port}'"); - - return; - } - catch (HttpListenerException) when (retries > 0) - { - // only catch the exception if there are retries left - port++; - retries--; - } - - // always close listener if exception is thrown, - // whether it was caught or not - listener.Close(); - - WriteOutput("Listener shut down. Could not find available port."); - } + _listener = new(output, HandleHttpRequests, host, "/api/v2/spans/"); } public event EventHandler> RequestReceived; @@ -94,11 +53,9 @@ public MockZipkinCollector(ITestOutputHelper output, int port = 9411, int retrie public bool ShouldDeserializeTraces { get; set; } = true; /// - /// Gets the TCP port that this Agent is listening on. - /// Can be different from 's initialPort - /// parameter if listening on that port fails. + /// Gets the TCP port that this collector is listening on. /// - public int Port { get; } + public int Port { get => _listener.Port; } /// /// Gets the filters used to filter out spans we don't want to look at for a test. @@ -161,7 +118,7 @@ public IImmutableList WaitForSpans( public void Dispose() { WriteOutput($"Shutting down. Total spans received: '{Spans.Count}'"); - _listener?.Stop(); + _listener.Dispose(); } protected virtual void OnRequestReceived(HttpListenerContext context) @@ -174,96 +131,43 @@ protected virtual void OnRequestDeserialized(IList trace) RequestDeserialized?.Invoke(this, new EventArgs>(trace)); } - private void AssertHeader( - NameValueCollection headers, - string headerKey, - Func assertion) + private void HandleHttpRequests(HttpListenerContext ctx) { - var header = headers.Get(headerKey); - - if (string.IsNullOrEmpty(header)) - { - throw new Exception($"Every submission to the agent should have a {headerKey} header."); - } - - if (!assertion(header)) + if (ctx.Request.RawUrl.Equals("/healthz", StringComparison.OrdinalIgnoreCase)) { - throw new Exception($"Failed assertion for {headerKey} on {header}"); + CreateHealthResponse(ctx); + return; } - } - private void HandleHttpRequests() - { - while (_listener.IsListening) + if (ShouldDeserializeTraces) { - try + using (var reader = new StreamReader(ctx.Request.InputStream)) { - var ctx = _listener.GetContext(); - OnRequestReceived(ctx); - - if (ctx.Request.RawUrl.Equals("/healthz", StringComparison.OrdinalIgnoreCase)) + var zspans = JsonConvert.DeserializeObject>(reader.ReadToEnd()); + if (zspans != null) { - CreateHealthResponse(ctx); + IList spans = zspans.ConvertAll(x => (IMockSpan)x); + OnRequestDeserialized(spans); - continue; - } - - if (ShouldDeserializeTraces) - { - using (var reader = new StreamReader(ctx.Request.InputStream)) + lock (this) { - var zspans = JsonConvert.DeserializeObject>(reader.ReadToEnd()); - if (zspans != null) - { - IList spans = zspans.ConvertAll(x => (IMockSpan)x); - OnRequestDeserialized(spans); - - lock (this) - { - // we only need to lock when replacing the span collection, - // not when reading it because it is immutable - Spans = Spans.AddRange(spans); - RequestHeaders = RequestHeaders.Add(new NameValueCollection(ctx.Request.Headers)); - } - } + // we only need to lock when replacing the span collection, + // not when reading it because it is immutable + Spans = Spans.AddRange(spans); + RequestHeaders = RequestHeaders.Add(new NameValueCollection(ctx.Request.Headers)); } } - - // NOTE: HttpStreamRequest doesn't support Transfer-Encoding: Chunked - // (Setting content-length avoids that) - - ctx.Response.ContentType = "application/json"; - var buffer = Encoding.UTF8.GetBytes("{}"); - ctx.Response.ContentLength64 = buffer.LongLength; - ctx.Response.OutputStream.Write(buffer, 0, buffer.Length); - ctx.Response.Close(); - } - catch (HttpListenerException) - { - // listener was stopped, - // ignore to let the loop end and the method return - } - catch (ObjectDisposedException) - { - // the response has been already disposed. - } - catch (InvalidOperationException) - { - // this can occur when setting Response.ContentLength64, with the framework claiming that the response has already been submitted - // for now ignore, and we'll see if this introduces downstream issues - } - catch (Exception) when (!_listener.IsListening) - { - // we don't care about any exception when listener is stopped } } - } - private void WriteOutput(string msg) - { - const string name = nameof(MockZipkinCollector); + // NOTE: HttpStreamRequest doesn't support Transfer-Encoding: Chunked + // (Setting content-length avoids that) - _output.WriteLine($"[{name}]: {msg}"); + ctx.Response.ContentType = "application/json"; + var buffer = Encoding.UTF8.GetBytes("{}"); + ctx.Response.ContentLength64 = buffer.LongLength; + ctx.Response.OutputStream.Write(buffer, 0, buffer.Length); + ctx.Response.Close(); } private void CreateHealthResponse(HttpListenerContext ctx) @@ -275,4 +179,10 @@ private void CreateHealthResponse(HttpListenerContext ctx) ctx.Response.StatusCode = (int)HttpStatusCode.OK; ctx.Response.Close(); } + + private void WriteOutput(string msg) + { + const string name = nameof(MockZipkinCollector); + _output.WriteLine($"[{name}]: {msg}"); + } } diff --git a/test/IntegrationTests/Helpers/Models/ProcessResult.cs b/test/IntegrationTests/Helpers/Models/ProcessResult.cs deleted file mode 100644 index 6f3d0c64d5..0000000000 --- a/test/IntegrationTests/Helpers/Models/ProcessResult.cs +++ /dev/null @@ -1,48 +0,0 @@ -// -// Copyright The OpenTelemetry Authors -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// - -using System; -using System.Diagnostics; - -namespace IntegrationTests.Helpers.Models; - -public class ProcessResult : IDisposable -{ - public ProcessResult( - Process process, - string standardOutput, - string standardError, - int exitCode) - { - Process = process; - StandardOutput = standardOutput; - StandardError = standardError; - ExitCode = exitCode; - } - - public Process Process { get; } - - public string StandardOutput { get; } - - public string StandardError { get; } - - public int ExitCode { get; } - - public void Dispose() - { - Process?.Dispose(); - } -} diff --git a/test/IntegrationTests/Helpers/TestHelper.cs b/test/IntegrationTests/Helpers/TestHelper.cs index 49368533ce..174d18275a 100644 --- a/test/IntegrationTests/Helpers/TestHelper.cs +++ b/test/IntegrationTests/Helpers/TestHelper.cs @@ -117,6 +117,10 @@ public async Task StartContainerAsync(TestSettings testSettings, int return new Container(container); } + /// + /// StartTestApplication starts the test application + // and returns the Process instance for further interaction. + /// public Process StartTestApplication(int traceAgentPort, string arguments, string packageVersion, int aspNetCorePort, string framework = "", bool enableStartupHook = true) { var testSettings = new TestSettings @@ -131,46 +135,32 @@ public Process StartTestApplication(int traceAgentPort, string arguments, string return StartTestApplication(testSettings); } - public ProcessResult RunTestApplicationAndWaitForExit(int traceAgentPort, string arguments = null, string packageVersion = "", string framework = "", int aspNetCorePort = 5000, bool enableStartupHook = true) + /// + /// RunTestApplication starts the test application, wait up to DefaultProcessTimeout. + /// Assertion exceptions are thrown if it timed out or the exit code is non-zero. + /// + public void RunTestApplication(int traceAgentPort = 0, int metricsAgentPort = 0, string arguments = null, string packageVersion = "", string framework = "", int aspNetCorePort = 5000, bool enableStartupHook = true) { var testSettings = new TestSettings { - TracesSettings = new TracesSettings { Port = traceAgentPort }, Arguments = arguments, PackageVersion = packageVersion, AspNetCorePort = aspNetCorePort, Framework = framework, EnableStartupHook = enableStartupHook }; - return RunTestApplicationAndWaitForExit(testSettings); - } - - public ProcessResult RunTestApplicationAndWaitForExit(TestSettings testSettings) - { - var process = StartTestApplication(testSettings); - var name = process.ProcessName; - using var helper = new ProcessHelper(process); - - bool processTimeout = !process.WaitForExit((int)DefaultProcessTimeout.TotalMilliseconds); - if (processTimeout) + if (traceAgentPort != 0) { - process.Kill(); + testSettings.TracesSettings = new() { Port = traceAgentPort }; } - var exitCode = process.ExitCode; - - Output.WriteLine($"ProcessName: " + name); - Output.WriteLine($"ProcessId: " + process.Id); - Output.WriteLine($"Exit Code: " + exitCode); - Output.WriteResult(helper); - - if (processTimeout) + if (metricsAgentPort != 0) { - throw new TimeoutException($"{name} ({process.Id}) did not exit within {DefaultProcessTimeout.TotalSeconds} sec"); + testSettings.MetricsSettings = new() { Port = metricsAgentPort }; } - return new ProcessResult(process, helper.StandardOutput, helper.ErrorOutput, exitCode); + RunTestApplication(testSettings); } protected void EnableDebugMode() @@ -183,6 +173,26 @@ protected void SetEnvironmentVariable(string key, string value) EnvironmentHelper.CustomEnvironmentVariables.Add(key, value); } + private void RunTestApplication(TestSettings testSettings) + { + using var process = StartTestApplication(testSettings); + Output.WriteLine($"ProcessName: " + process.ProcessName); + using var helper = new ProcessHelper(process); + + bool processTimeout = !process.WaitForExit((int)DefaultProcessTimeout.TotalMilliseconds); + if (processTimeout) + { + process.Kill(); + } + + Output.WriteLine($"ProcessId: " + process.Id); + Output.WriteLine($"Exit Code: " + process.ExitCode); + Output.WriteResult(helper); + + processTimeout.Should().BeFalse("Test application timed out"); + process.ExitCode.Should().Be(0, $"Test application exited with non-zero exit code"); + } + private Process StartTestApplication(TestSettings testSettings) { // get path to test application that the profiler will attach to diff --git a/test/IntegrationTests/Helpers/TestHttpListener.cs b/test/IntegrationTests/Helpers/TestHttpListener.cs new file mode 100644 index 0000000000..d97f678d09 --- /dev/null +++ b/test/IntegrationTests/Helpers/TestHttpListener.cs @@ -0,0 +1,120 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Net; +using System.Threading; +using Xunit.Abstractions; + +namespace IntegrationTests.Helpers; + +public class TestHttpListener : IDisposable +{ + private readonly ITestOutputHelper _output; + private readonly Action _requestHandler; + private readonly HttpListener _listener; + private readonly Thread _listenerThread; + + public TestHttpListener(ITestOutputHelper output, Action requestHandler, string host = "localhost", string sufix = "/") + { + _output = output; + _requestHandler = requestHandler; + + // try up to 5 consecutive ports before giving up + int retries = 4; + while (true) + { + // seems like we can't reuse a listener if it fails to start, + // so create a new listener each time we retry + _listener = new HttpListener(); + + try + { + _listener.Start(); + + // See https://docs.microsoft.com/en-us/dotnet/api/system.net.httplistenerprefixcollection.add?redirectedfrom=MSDN&view=net-6.0#remarks + // for info about the host value. + Port = TcpPortProvider.GetOpenPort(); + string prefix = new UriBuilder("http", host, Port, sufix).ToString(); + _listener.Prefixes.Add(prefix); + + // successfully listening + _listenerThread = new Thread(HandleHttpRequests); + _listenerThread.Start(); + WriteOutput($"Listening on '{prefix}'"); + + return; + } + catch (HttpListenerException) when (retries > 0) + { + retries--; + } + + // always close listener if exception is thrown, + // whether it was caught or not + _listener.Close(); + + WriteOutput("Listener shut down. Could not find available port."); + } + } + + /// + /// Gets the TCP port that this listener is listening on. + /// + public int Port { get; } + + public void Dispose() + { + WriteOutput($"Listener is shutting down."); + _listener.Stop(); + } + + private void HandleHttpRequests() + { + while (_listener.IsListening) + { + try + { + var ctx = _listener.GetContext(); + _requestHandler(ctx); + } + catch (HttpListenerException) + { + // listener was stopped, + // ignore to let the loop end and the method return + } + catch (ObjectDisposedException) + { + // the response has been already disposed. + } + catch (InvalidOperationException) + { + // this can occur when setting Response.ContentLength64, with the framework claiming that the response has already been submitted + // for now ignore, and we'll see if this introduces downstream issues + } + catch (Exception) when (!_listener.IsListening) + { + // we don't care about any exception when listener is stopped + } + } + } + + private void WriteOutput(string msg) + { + const string name = nameof(TestHttpListener); + _output.WriteLine($"[{name}]: {msg}"); + } +} diff --git a/test/IntegrationTests/HttpTests.cs b/test/IntegrationTests/HttpTests.cs index 6d981d1283..82aed7d065 100644 --- a/test/IntegrationTests/HttpTests.cs +++ b/test/IntegrationTests/HttpTests.cs @@ -42,13 +42,11 @@ public HttpTests(ITestOutputHelper output) [Trait("Category", "EndToEnd")] public void SubmitTraces() { - var agentPort = TcpPortProvider.GetOpenPort(); - using var agent = new MockZipkinCollector(Output, agentPort); + using var agent = new MockZipkinCollector(Output); const int expectedSpanCount = 3; - using var processResult = RunTestApplicationAndWaitForExit(agent.Port, enableStartupHook: true); - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); + RunTestApplication(agent.Port); var spans = agent.WaitForSpans(expectedSpanCount, TimeSpan.FromSeconds(5)); using (new AssertionScope()) @@ -91,19 +89,10 @@ public void SubmitTraces() [Trait("Category", "EndToEnd")] public void SubmitMetrics() { - var collectorPort = TcpPortProvider.GetOpenPort(); - using var collector = new MockCollector(Output, collectorPort); - const int expectedMetricRequests = 1; - var testSettings = new TestSettings - { - MetricsSettings = new MetricsSettings { Port = collectorPort }, - EnableStartupHook = true - }; - - using var processResult = RunTestApplicationAndWaitForExit(testSettings); - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); + using var collector = new MockMetricsCollector(Output); + RunTestApplication(metricsAgentPort: collector.Port); var metricRequests = collector.WaitForMetrics(expectedMetricRequests, TimeSpan.FromSeconds(5)); using (new AssertionScope()) diff --git a/test/IntegrationTests/MongoDBTests.cs b/test/IntegrationTests/MongoDBTests.cs index 2c5b23e386..99601d9ae0 100644 --- a/test/IntegrationTests/MongoDBTests.cs +++ b/test/IntegrationTests/MongoDBTests.cs @@ -43,49 +43,43 @@ public MongoDBTests(ITestOutputHelper output, MongoDBFixture mongoDB) [Trait("Containers", "Linux")] public void SubmitsTraces() { - int agentPort = TcpPortProvider.GetOpenPort(); + using var agent = new MockZipkinCollector(Output); + RunTestApplication(agent.Port, arguments: $"--mongo-db {_mongoDB.Port}"); + var spans = agent.WaitForSpans(3, TimeSpan.FromSeconds(5)); + Assert.True(spans.Count >= 3, $"Expecting at least 3 spans, only received {spans.Count}"); - using (var agent = new MockZipkinCollector(Output, agentPort)) - using (var processResult = RunTestApplicationAndWaitForExit(agent.Port, arguments: $"--mongo-db {_mongoDB.Port}", enableStartupHook: true)) - { - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); - - var spans = agent.WaitForSpans(3, TimeSpan.FromSeconds(5)); - Assert.True(spans.Count >= 3, $"Expecting at least 3 spans, only received {spans.Count}"); + var rootSpan = spans.Single(s => s.ParentId == null); - var rootSpan = spans.Single(s => s.ParentId == null); + // Check for manual trace + Assert.Equal("Main()", rootSpan.Name); + Assert.Null(rootSpan.Type); - // Check for manual trace - Assert.Equal("Main()", rootSpan.Name); - Assert.Null(rootSpan.Type); + int spansWithStatement = 0; - int spansWithStatement = 0; + foreach (var span in spans) + { + Assert.Equal("TestApplication.MongoDB", span.Service); - foreach (var span in spans) + if (Regex.IsMatch(span.Name, "employees\\.*")) { - Assert.Equal("TestApplication.MongoDB", span.Service); - - if (Regex.IsMatch(span.Name, "employees\\.*")) - { - Assert.Equal("mongodb", span.Tags["db.system"]); - Assert.Equal("test-db", span.Tags["db.name"]); - Assert.True("1.0.0.0" == span.Tags["otel.library.version"], span.ToString()); + Assert.Equal("mongodb", span.Tags["db.system"]); + Assert.Equal("test-db", span.Tags["db.name"]); + Assert.True("1.0.0.0" == span.Tags["otel.library.version"], span.ToString()); - if (span.Tags?.ContainsKey("db.statement") ?? false) - { - spansWithStatement++; - Assert.True(span.Tags?.ContainsKey("db.statement"), $"No db.statement found on span {span}"); - } - } - else + if (span.Tags?.ContainsKey("db.statement") ?? false) { - // These are manual (DiagnosticSource) traces - Assert.True("1.0.0" == span.Tags["otel.library.version"], span.ToString()); + spansWithStatement++; + Assert.True(span.Tags?.ContainsKey("db.statement"), $"No db.statement found on span {span}"); } } - - Assert.False(spansWithStatement == 0, "Extraction of the command failed on all spans"); + else + { + // These are manual (DiagnosticSource) traces + Assert.True("1.0.0" == span.Tags["otel.library.version"], span.ToString()); + } } + + Assert.False(spansWithStatement == 0, "Extraction of the command failed on all spans"); } } #endif diff --git a/test/IntegrationTests/PluginsTests.cs b/test/IntegrationTests/PluginsTests.cs new file mode 100644 index 0000000000..d91b1115e6 --- /dev/null +++ b/test/IntegrationTests/PluginsTests.cs @@ -0,0 +1,61 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using System; +using System.Collections.Generic; +using System.Linq; +using FluentAssertions; +using IntegrationTests.Helpers; +using IntegrationTests.Helpers.Models; +using Xunit; +using Xunit.Abstractions; + +namespace IntegrationTests; + +public class PluginsTests : TestHelper +{ + public PluginsTests(ITestOutputHelper output) + : base("Plugins", output) + { + } + + [Fact] + [Trait("Category", "EndToEnd")] + public void SubmitsTraces() + { + SetEnvironmentVariable("OTEL_DOTNET_AUTO_TRACES_PLUGINS", "TestApplication.Plugins.Plugin, TestApplication.Plugins, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + + using var collector = new MockZipkinCollector(Output); + RunTestApplication(collector.Port); + var spans = collector.WaitForSpans(1, TimeSpan.FromSeconds(5)); + + spans.Should().Contain(x => x.Name == "SayHello"); + } + + [Fact] + [Trait("Category", "EndToEnd")] + public void SubmitMetrics() + { + SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_PLUGINS", "TestApplication.Plugins.Plugin, TestApplication.Plugins, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null"); + + using var collector = new MockMetricsCollector(Output); + RunTestApplication(metricsAgentPort: collector.Port); + var metricRequests = collector.WaitForMetrics(1, TimeSpan.FromSeconds(5)); + + var metrics = metricRequests.Should().NotBeEmpty().And.Subject.First().ResourceMetrics.Should().ContainSingle().Subject.ScopeMetrics; + metrics.Should().Contain(x => x.Scope.Name == "MyCompany.MyProduct.MyLibrary"); + } +} diff --git a/test/IntegrationTests/SmokeTests.cs b/test/IntegrationTests/SmokeTests.cs index f8428cda5c..5115f384bf 100644 --- a/test/IntegrationTests/SmokeTests.cs +++ b/test/IntegrationTests/SmokeTests.cs @@ -33,8 +33,6 @@ public class SmokeTests : TestHelper { private const string ServiceName = "TestApplication.Smoke"; - private List _expectations = new List(); - public SmokeTests(ITestOutputHelper output) : base("Smoke", output) { @@ -120,19 +118,10 @@ public void ApplicationIsIncluded() public void SubmitMetrics() { SetEnvironmentVariable("OTEL_DOTNET_AUTO_METRICS_ADDITIONAL_SOURCES", "MyCompany.MyProduct.MyLibrary"); - var collectorPort = TcpPortProvider.GetOpenPort(); - using var collector = new MockCollector(Output, collectorPort); - const int expectedMetricRequests = 1; - var testSettings = new TestSettings - { - MetricsSettings = new MetricsSettings { Port = collectorPort }, - EnableStartupHook = true, - }; - - using var processResult = RunTestApplicationAndWaitForExit(testSettings); - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); + using var collector = new MockMetricsCollector(Output); + RunTestApplication(metricsAgentPort: collector.Port); var metricRequests = collector.WaitForMetrics(expectedMetricRequests, TimeSpan.FromSeconds(5)); using (new AssertionScope()) @@ -204,11 +193,12 @@ private static void AssertSpanExpectations(List expect private IImmutableList RunTestApplication(bool enableStartupHook = true) { + SetEnvironmentVariable("OTEL_DOTNET_AUTO_TRACES_ADDITIONAL_SOURCES", "MyCompany.MyProduct.MyLibrary"); + int agentPort = TcpPortProvider.GetOpenPort(); - using var agent = new MockZipkinCollector(Output, agentPort); - using var processResult = RunTestApplicationAndWaitForExit(agent.Port, enableStartupHook: enableStartupHook); + using var agent = new MockZipkinCollector(Output); + RunTestApplication(agent.Port, enableStartupHook: enableStartupHook); - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); return agent.WaitForSpans(2, TimeSpan.FromSeconds(5)); } } diff --git a/test/IntegrationTests/SqlClientTests.cs b/test/IntegrationTests/SqlClientTests.cs index 715469377b..145673827d 100644 --- a/test/IntegrationTests/SqlClientTests.cs +++ b/test/IntegrationTests/SqlClientTests.cs @@ -43,13 +43,11 @@ public SqlClientTests(ITestOutputHelper output, SqlClientFixture sqlClientFixtur [Trait("Containers", "Linux")] public void SubmitTraces() { - var agentPort = TcpPortProvider.GetOpenPort(); - using var agent = new MockZipkinCollector(Output, agentPort); + using var agent = new MockZipkinCollector(Output); const int expectedSpanCount = 8; - using var processResult = RunTestApplicationAndWaitForExit(agent.Port, arguments: $"{_sqlClientFixture.Password} {_sqlClientFixture.Port}", enableStartupHook: true); - Assert.True(processResult.ExitCode >= 0, $"Process exited with code {processResult.ExitCode} and exception: {processResult.StandardError}"); + RunTestApplication(agent.Port, arguments: $"{_sqlClientFixture.Password} {_sqlClientFixture.Port}"); var spans = agent.WaitForSpans(expectedSpanCount, TimeSpan.FromSeconds(5)); using (new AssertionScope()) diff --git a/test/test-applications/integrations/TestApplication.Plugins/Plugin.cs b/test/test-applications/integrations/TestApplication.Plugins/Plugin.cs new file mode 100644 index 0000000000..f5f6e351c0 --- /dev/null +++ b/test/test-applications/integrations/TestApplication.Plugins/Plugin.cs @@ -0,0 +1,34 @@ +// +// Copyright The OpenTelemetry Authors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// + +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; + +namespace TestApplication.Plugins +{ + public class Plugin + { + public TracerProviderBuilder ConfigureTracerProvider(TracerProviderBuilder builder) + { + return builder.AddSource(TestApplication.Smoke.Program.SourceName); + } + + public MeterProviderBuilder ConfigureMeterProvider(MeterProviderBuilder builder) + { + return builder.AddMeter(TestApplication.Smoke.Program.SourceName); + } + } +} diff --git a/test/test-applications/integrations/TestApplication.Plugins/TestApplication.Plugins.csproj b/test/test-applications/integrations/TestApplication.Plugins/TestApplication.Plugins.csproj new file mode 100644 index 0000000000..3dfb3c49e9 --- /dev/null +++ b/test/test-applications/integrations/TestApplication.Plugins/TestApplication.Plugins.csproj @@ -0,0 +1,14 @@ + + + + + + + + + + + + + + diff --git a/test/test-applications/integrations/TestApplication.Smoke/Program.cs b/test/test-applications/integrations/TestApplication.Smoke/Program.cs index a5ee56b4dd..a752b57d52 100644 --- a/test/test-applications/integrations/TestApplication.Smoke/Program.cs +++ b/test/test-applications/integrations/TestApplication.Smoke/Program.cs @@ -19,10 +19,12 @@ using System.Diagnostics.Metrics; using System.Net.Http; -namespace TestApplication.StartupHook +namespace TestApplication.Smoke { public class Program { + public static readonly string SourceName = "MyCompany.MyProduct.MyLibrary"; + public static void Main(string[] args) { EmitTraces(); @@ -31,7 +33,7 @@ public static void Main(string[] args) private static void EmitTraces() { - var myActivitySource = new ActivitySource("TestApplication.StartupHook", "1.0.0"); + var myActivitySource = new ActivitySource(SourceName, "1.0.0"); using (var activity = myActivitySource.StartActivity("SayHello")) { @@ -46,7 +48,7 @@ private static void EmitTraces() private static void EmitMetrics() { - var myMeter = new Meter("MyCompany.MyProduct.MyLibrary", "1.0"); + var myMeter = new Meter(SourceName, "1.0"); var myFruitCounter = myMeter.CreateCounter("MyFruitCounter"); myFruitCounter.Add(1, new KeyValuePair("name", "apple"));