diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..fbdc7ec --- /dev/null +++ b/.dockerignore @@ -0,0 +1,22 @@ +**/.dockerignore +**/.env +**/.gitignore +**/.project +**/.settings +**/.toolstarget +**/.vs +**/.vscode +**/.idea +**/*.*proj.user +**/*.dbmdl +**/*.jfm +**/azds.yaml +**/bin +**/charts +**/docker-compose* +**/Dockerfile* +**/node_modules +**/npm-debug.log +**/obj +**/secrets.dev.yaml +**/values.dev.yaml \ No newline at end of file diff --git a/Elastic.OpenTelemetry.sln b/Elastic.OpenTelemetry.sln index cba35c0..159ffe9 100644 --- a/Elastic.OpenTelemetry.sln +++ b/Elastic.OpenTelemetry.sln @@ -43,6 +43,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "ServiceDefaults", "examples EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Elastic.OpenTelemetry.AutoInstrumentationPlugin", "src\Elastic.OpenTelemetry.AutoInstrumentationPlugin\Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj", "{B61D749B-21E5-430D-B50D-CA02EBAA7F2F}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example.AutoInstrumentation", "examples\Example.AutoInstrumentation\Example.AutoInstrumentation.csproj", "{F3AA76EC-C7D8-42DA-947D-4376B6562772}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AutoInstrumentation.IntegrationTests", "tests\AutoInstrumentation.IntegrationTests\AutoInstrumentation.IntegrationTests.csproj", "{782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -101,6 +105,14 @@ Global {B61D749B-21E5-430D-B50D-CA02EBAA7F2F}.Debug|Any CPU.Build.0 = Debug|Any CPU {B61D749B-21E5-430D-B50D-CA02EBAA7F2F}.Release|Any CPU.ActiveCfg = Release|Any CPU {B61D749B-21E5-430D-B50D-CA02EBAA7F2F}.Release|Any CPU.Build.0 = Release|Any CPU + {F3AA76EC-C7D8-42DA-947D-4376B6562772}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {F3AA76EC-C7D8-42DA-947D-4376B6562772}.Debug|Any CPU.Build.0 = Debug|Any CPU + {F3AA76EC-C7D8-42DA-947D-4376B6562772}.Release|Any CPU.ActiveCfg = Release|Any CPU + {F3AA76EC-C7D8-42DA-947D-4376B6562772}.Release|Any CPU.Build.0 = Release|Any CPU + {782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -116,6 +128,8 @@ Global {206203BD-3EBA-4E9A-8881-1189D95AB037} = {4E95C87B-655B-4BC3-8F2A-DF06B7AAB7E9} {A3D1ED4D-863B-45D7-9829-305DD33B4CE5} = {4E95C87B-655B-4BC3-8F2A-DF06B7AAB7E9} {B61D749B-21E5-430D-B50D-CA02EBAA7F2F} = {E622CFF2-C6C4-40FB-BE42-7C4F2B38B75A} + {F3AA76EC-C7D8-42DA-947D-4376B6562772} = {4E95C87B-655B-4BC3-8F2A-DF06B7AAB7E9} + {782E4DC1-8186-4BAC-B2F4-89E6DF22A4DD} = {AAD39891-0B70-47FA-A212-43E1AAE5DF56} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {573B2B5F-8CBB-4D52-A55A-4E65E282AAFB} diff --git a/build/scripts/CommandLine.fs b/build/scripts/CommandLine.fs index 9529f0c..21fa4ce 100644 --- a/build/scripts/CommandLine.fs +++ b/build/scripts/CommandLine.fs @@ -22,6 +22,7 @@ type Build = | [] Test | [] Unit_Test + | [] Integrate | [] End_To_End | [] Format @@ -49,6 +50,7 @@ with | Build -> "Run build" | Unit_Test -> "alias to providing: test --test-suite=unit" + | Integrate -> "alias to providing: test --test-suite=integration" | End_To_End -> "alias to providing: test --test-suite=e2e" | Test -> "runs a clean build and then runs all the tests unless --test-suite is provided" | Release -> "runs build, tests, and create and validates the packages shy of publishing them" diff --git a/build/scripts/Targets.fs b/build/scripts/Targets.fs index 7420474..4f543f8 100644 --- a/build/scripts/Targets.fs +++ b/build/scripts/Targets.fs @@ -165,6 +165,7 @@ let Setup (parsed:ParseResults) = | Build -> Build.Cmd [Clean; CheckFormat] [] build | End_To_End -> Build.Cmd [] [Build] <| runTests E2E + | Integrate -> Build.Cmd [] [Build] <| runTests Integration | Unit_Test -> Build.Cmd [] [Build] <| runTests Unit | Test -> Build.Cmd [] [Build] test diff --git a/examples/Example.AutoInstrumentation/Dockerfile b/examples/Example.AutoInstrumentation/Dockerfile new file mode 100644 index 0000000..01665fc --- /dev/null +++ b/examples/Example.AutoInstrumentation/Dockerfile @@ -0,0 +1,86 @@ +ARG OTEL_VERSION=1.7.0 +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +ARG TARGETPLATFORM +ARG TARGETARCH +ARG TARGETVARIANT +RUN apt-get update && apt-get install -y unzip curl strace +# Would love to run as non root but TestContainers does not utilize buildkit like `docker build` does OOTB +#USER $APP_UID +WORKDIR /app +RUN chown app:app . + +FROM base AS otel +ARG OTEL_VERSION +# install OpenTelemetry .NET Automatic Instrumentation +# the following commented line does not work from TestContainers because it does not utilize buildkit which `docker build` does OOTB +#ADD --chown=$APP_UID --chmod=777 https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh otel-dotnet-auto-install.sh +ADD https://github.com/open-telemetry/opentelemetry-dotnet-instrumentation/releases/download/v${OTEL_VERSION}/otel-dotnet-auto-install.sh otel-dotnet-auto-install.sh +RUN chmod +x otel-dotnet-auto-install.sh +RUN OTEL_DOTNET_AUTO_HOME="/app/otel" sh otel-dotnet-auto-install.sh + + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build_example +WORKDIR /src +COPY ["examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj", "examples/Example.AutoInstrumentation/"] +RUN dotnet restore "examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj" +COPY . . +WORKDIR "/src/examples/Example.AutoInstrumentation" +RUN dotnet build "Example.AutoInstrumentation.csproj" -c Release -o /app/build_example + +FROM build_example AS publish_example +RUN dotnet publish "Example.AutoInstrumentation.csproj" -c Release -o /app/example /p:UseAppHost=false + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build_plugin +WORKDIR /src +COPY ["README.md", "."] +COPY ["LICENSE.txt", "."] +COPY ["NOTICE.txt", "."] +COPY ["src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj", "src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/"] +RUN dotnet restore "src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj" +COPY . . +WORKDIR "/src/src/Elastic.OpenTelemetry.AutoInstrumentationPlugin" +RUN dotnet build "Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj" -c release +RUN mkdir -p /app/temp +RUN cp -r /src/.artifacts/bin /app/temp + +FROM otel AS final +ARG TARGETPLATFORM +ARG TARGETARCH +ARG TARGETVARIANT +WORKDIR /app +COPY --from=publish_example /app/example /app/example +COPY --from=otel /app/otel /app/otel + +# This `RUN true` is a bit of magic that I don't care to understand +# https://github.com/moby/moby/issues/37965 +# Seems to relate to the fs driver on GitHub Actions failing to copy files from the `build_plugin` layer. +# Hence we copy the whole .artifacts/bin folder manually and copy the files to /app/otel/net +RUN true +COPY --from=build_plugin /app/temp /app/temp +RUN true +RUN ls -al /app/temp/bin +RUN mkdir -p /app/otel/net +RUN cp "/app/temp/bin/Elastic.OpenTelemetry.AutoInstrumentationPlugin/release_net8.0/Elastic.OpenTelemetry.AutoInstrumentationPlugin.dll" /app/otel/net/ +RUN cp "/app/temp/bin/Elastic.OpenTelemetry/release_net8.0/Elastic.OpenTelemetry.dll" /app/otel/net/ + +ENV CORECLR_ENABLE_PROFILING="1" +ENV CORECLR_PROFILER="{918728DD-259F-4A6A-AC2B-B85E1B658318}" +ENV CORECLR_PROFILER_PATH="/app/otel/linux-${TARGETARCH}/OpenTelemetry.AutoInstrumentation.Native.so" +ENV OTEL_DOTNET_AUTO_PLUGINS="Elastic.OpenTelemetry.AutoInstrumentationPlugin.ElasticAutoInstrumentationPlugin, Elastic.OpenTelemetry.AutoInstrumentationPlugin, Version=1.0.0.0, Culture=neutral, PublicKeyToken=069ca2728db333c1" + +ENV OTEL_TRACES_EXPORTER=none +ENV OTEL_METRICS_EXPORTER=none +ENV OTEL_LOGS_EXPORTER=none +ENV OTEL_SERVICE_NAME=ExampleInstrumentation + +ENV OTEL_LOG_LEVEL=info +ENV ELASTIC_OTEL_LOG_LEVEL=trace +ENV ELASTIC_OTEL_LOG_TARGETS=stdout +ENV OTEL_DOTNET_AUTO_LOG_DIRECTORY=/app/logs + +ENV OTEL_DOTNET_AUTO_HOME="/app/otel" +ENV DOTNET_ADDITIONAL_DEPS="/app/otel/AdditionalDeps" +ENV DOTNET_SHARED_STORE="/app/otel/store" +ENV DOTNET_STARTUP_HOOKS="/app/otel/net/OpenTelemetry.AutoInstrumentation.StartupHook.dll" + +ENTRYPOINT ["dotnet", "/app/example/Example.AutoInstrumentation.dll"] diff --git a/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj b/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj new file mode 100644 index 0000000..2a040bd --- /dev/null +++ b/examples/Example.AutoInstrumentation/Example.AutoInstrumentation.csproj @@ -0,0 +1,17 @@ + + + + Exe + net8.0 + enable + enable + Linux + + + + + .dockerignore + + + + diff --git a/examples/Example.AutoInstrumentation/Program.cs b/examples/Example.AutoInstrumentation/Program.cs new file mode 100644 index 0000000..29743a6 --- /dev/null +++ b/examples/Example.AutoInstrumentation/Program.cs @@ -0,0 +1,14 @@ +// 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 + +Console.WriteLine("Hello, World!"); + +var httpClient = new HttpClient(); +for (var i = 0; i < 10; i++) +{ + var response = await httpClient.GetAsync(new Uri("https://google.com")); + Console.Write($"\rSent {i + 1} requests, last response: {response.StatusCode}."); + await Task.Delay(TimeSpan.FromMilliseconds(100)); +} +Console.WriteLine(); diff --git a/src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj b/src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj index 0f457b7..7c6a754 100644 --- a/src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj +++ b/src/Elastic.OpenTelemetry.AutoInstrumentationPlugin/Elastic.OpenTelemetry.AutoInstrumentationPlugin.csproj @@ -1,8 +1,7 @@  - netstandard2.0;netstandard2.1;net8.0;net6.0;net462 - net8.0 + net8.0;net462 enable enable False diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs index 81b5e94..50a547b 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryBuilderOptions.cs @@ -28,13 +28,14 @@ public record ElasticOpenTelemetryBuilderOptions /// internal IServiceCollection? Services { get; init; } + private static readonly ElasticOpenTelemetryOptions DefaultDistroOptions = new(); /// /// Advanced options which can be used to finely-tune the behaviour of the Elastic /// distribution of OpenTelemetry. /// public ElasticOpenTelemetryOptions DistroOptions { - get => _elasticOpenTelemetryOptions ?? new(); + get => _elasticOpenTelemetryOptions ?? DefaultDistroOptions; init => _elasticOpenTelemetryOptions = value; } } diff --git a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs index 88cff96..0b59964 100644 --- a/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs +++ b/src/Elastic.OpenTelemetry/Configuration/ElasticOpenTelemetryOptions.cs @@ -116,14 +116,14 @@ public bool GlobalLogEnabled { get { - var isActive = (_logLevel.HasValue || !string.IsNullOrWhiteSpace(_logDirectory) || _logTargets.HasValue); - if (isActive) - { - if (_logLevel is LogLevel.None) - isActive = false; - else if (_logTargets is LogTargets.None) - isActive = false; - } + var isActive = _logLevel.HasValue || !string.IsNullOrWhiteSpace(_logDirectory) || _logTargets.HasValue; + if (!isActive) + return isActive; + + if (_logLevel is LogLevel.None) + isActive = false; + else if (_logTargets is LogTargets.None) + isActive = false; return isActive; } } diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs index 83ec309..6243819 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/CompositeLogger.cs @@ -19,6 +19,7 @@ internal sealed class CompositeLogger(ElasticOpenTelemetryBuilderOptions options public const string LogCategory = "Elastic.OpenTelemetry"; public FileLogger FileLogger { get; } = new(options.DistroOptions); + public StandardOutLogger ConsoleLogger { get; } = new(options.DistroOptions); private ILogger? _additionalLogger = options.Logger; private bool _isDisposed; @@ -26,13 +27,17 @@ internal sealed class CompositeLogger(ElasticOpenTelemetryBuilderOptions options public void Dispose() { _isDisposed = true; + if (_additionalLogger is IDisposable ad) + ad.Dispose(); FileLogger.Dispose(); } - public ValueTask DisposeAsync() + public async ValueTask DisposeAsync() { _isDisposed = true; - return FileLogger.DisposeAsync(); + if (_additionalLogger is IAsyncDisposable ad) + await ad.DisposeAsync().ConfigureAwait(false); + await FileLogger.DisposeAsync().ConfigureAwait(false); } public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) @@ -43,6 +48,9 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except if (FileLogger.IsEnabled(logLevel)) FileLogger.Log(logLevel, eventId, state, exception, formatter); + if (ConsoleLogger.IsEnabled(logLevel)) + ConsoleLogger.Log(logLevel, eventId, state, exception, formatter); + if (_additionalLogger == null) return; @@ -55,7 +63,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except public void SetAdditionalLogger(ILogger? logger) => _additionalLogger ??= logger; - public bool IsEnabled(LogLevel logLevel) => FileLogger.IsEnabled(logLevel) || (_additionalLogger?.IsEnabled(logLevel) ?? false); + public bool IsEnabled(LogLevel logLevel) => ConsoleLogger.IsEnabled(logLevel) || FileLogger.IsEnabled(logLevel) || (_additionalLogger?.IsEnabled(logLevel) ?? false); public IDisposable BeginScope(TState state) where TState : notnull => new CompositeDisposable(FileLogger.BeginScope(state), _additionalLogger?.BeginScope(state)); diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs index 927861f..19d49e6 100644 --- a/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/FileLogger.cs @@ -33,7 +33,7 @@ public FileLogger(ElasticOpenTelemetryOptions options) { _scopeProvider = new LoggerExternalScopeProvider(); _configuredLogLevel = options.LogLevel; - FileLoggingEnabled = options.GlobalLogEnabled; + FileLoggingEnabled = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.File); if (!FileLoggingEnabled) return; diff --git a/src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs b/src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs new file mode 100644 index 0000000..5c56217 --- /dev/null +++ b/src/Elastic.OpenTelemetry/Diagnostics/Logging/StandardOutLogger.cs @@ -0,0 +1,32 @@ +// 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 Elastic.OpenTelemetry.Configuration; +using Microsoft.Extensions.Logging; + +namespace Elastic.OpenTelemetry.Diagnostics.Logging; + +internal sealed class StandardOutLogger(ElasticOpenTelemetryOptions options) : ILogger +{ + private readonly LogLevel _configuredLogLevel = options.LogLevel; + + private readonly LoggerExternalScopeProvider _scopeProvider = new(); + + private bool StandardOutLoggingEnabled { get; } = options.GlobalLogEnabled && options.LogTargets.HasFlag(LogTargets.StdOut); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + // We skip logging for any log level higher (numerically) than the configured log level + if (!IsEnabled(logLevel)) + return; + + var logLine = LogFormatter.Format(logLevel, eventId, state, exception, formatter); + Console.WriteLine(logLine); + + } + + public bool IsEnabled(LogLevel logLevel) => StandardOutLoggingEnabled && _configuredLogLevel <= logLevel; + + public IDisposable BeginScope(TState state) where TState : notnull => _scopeProvider.Push(state); +} diff --git a/tests/AutoInstrumentation.IntegrationTests/AutoInstrumentation.IntegrationTests.csproj b/tests/AutoInstrumentation.IntegrationTests/AutoInstrumentation.IntegrationTests.csproj new file mode 100644 index 0000000..4fab745 --- /dev/null +++ b/tests/AutoInstrumentation.IntegrationTests/AutoInstrumentation.IntegrationTests.csproj @@ -0,0 +1,16 @@ + + + + net8.0 + enable + enable + Elastic.OpenTelemetry.AutoInstrumentation.IntegrationTests + xUnit1041 + + + + + + + + diff --git a/tests/AutoInstrumentation.IntegrationTests/ExampleApplicationContainer.cs b/tests/AutoInstrumentation.IntegrationTests/ExampleApplicationContainer.cs new file mode 100644 index 0000000..4914e20 --- /dev/null +++ b/tests/AutoInstrumentation.IntegrationTests/ExampleApplicationContainer.cs @@ -0,0 +1,73 @@ +// 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.Runtime.InteropServices; +using DotNet.Testcontainers; +using DotNet.Testcontainers.Builders; +using DotNet.Testcontainers.Configurations; +using DotNet.Testcontainers.Containers; +using DotNet.Testcontainers.Images; +using Nullean.Xunit.Partitions; +using Nullean.Xunit.Partitions.Sdk; +using Xunit; + +[assembly: TestFramework(Partition.TestFramework, Partition.Assembly)] + +namespace Elastic.OpenTelemetry.AutoInstrumentation.IntegrationTests; + +// ReSharper disable once ClassNeverInstantiated.Global +public class ExampleApplicationContainer : IPartitionLifetime +{ + private readonly IContainer _container; + private readonly IOutputConsumer _output; + private readonly IFutureDockerImage _image; + + // docker build -t example.autoinstrumentation:latest -f examples/Example.AutoInstrumentation/Dockerfile . \ + // && docker run -it --rm -p 5000:8080 --name autoin example.autoinstrumentation:latest + + public ExampleApplicationContainer() + { + ConsoleLogger.Instance.DebugLogLevelEnabled = true; + var directory = CommonDirectoryPath.GetSolutionDirectory(); + _image = new ImageFromDockerfileBuilder() + .WithDockerfileDirectory(directory, string.Empty) + .WithDockerfile("examples/Example.AutoInstrumentation/Dockerfile") + .WithLogger(ConsoleLogger.Instance) + .WithBuildArgument("TARGETARCH", RuntimeInformation.ProcessArchitecture switch + { + Architecture.Arm64 => "arm64", + Architecture.X64 => "x64", + Architecture.X86 => "x86", + _ => "unsupported" + }) + .Build(); + + _output = Consume.RedirectStdoutAndStderrToStream(new MemoryStream(), new MemoryStream()); + _container = new ContainerBuilder() + .WithImage(_image) + .WithPortBinding(5000, 8080) + .WithLogger(ConsoleLogger.Instance) + .WithOutputConsumer(_output) + .Build(); + + } + + public async Task InitializeAsync() + { + await _image.CreateAsync().ConfigureAwait(false); + + await _container.StartAsync().ConfigureAwait(false); + } + + public async Task DisposeAsync() => await _container.StopAsync().ConfigureAwait(false); + + public string FailureTestOutput() + { + _output.Stdout.Seek(0, SeekOrigin.Begin); + using var streamReader = new StreamReader(_output.Stdout, leaveOpen: true); + return streamReader.ReadToEnd(); + } + + public int? MaxConcurrency => null; +} diff --git a/tests/AutoInstrumentation.IntegrationTests/PluginLoaderTests.cs b/tests/AutoInstrumentation.IntegrationTests/PluginLoaderTests.cs new file mode 100644 index 0000000..25bffc0 --- /dev/null +++ b/tests/AutoInstrumentation.IntegrationTests/PluginLoaderTests.cs @@ -0,0 +1,37 @@ +// 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.Runtime.InteropServices; +using FluentAssertions; +using Nullean.Xunit.Partitions.Sdk; +using Xunit; + +namespace Elastic.OpenTelemetry.AutoInstrumentation.IntegrationTests; + +public class PluginLoaderTests(ExampleApplicationContainer exampleApplicationContainer) : IPartitionFixture +{ + + [NotWindowsCiFact] + public async Task ObserveDistributionPluginLoad() + { + await Task.Delay(TimeSpan.FromSeconds(3)); + var output = exampleApplicationContainer.FailureTestOutput(); + output.Should() + .NotBeNullOrWhiteSpace() + .And.Contain("Elastic OpenTelemetry Distribution:") + .And.Contain("ElasticOpenTelemetryBuilder initialized") + .And.Contain("Added 'Elastic.OpenTelemetry.Processors.ElasticCompatibilityProcessor'"); + + } + +} + +public class NotWindowsCiFact : FactAttribute +{ + public NotWindowsCiFact() + { + if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("CI"))) + Skip = "We can not run this test in a virtualized windows environment"; + } +}