From f9bb2a99637d7794c7ebe23d65da6ad4b4f010d4 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:15:55 -0500 Subject: [PATCH 01/10] docs: reorder sections in README.md - Moved project description below badges for improved structure and readability. --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1e5c705f..9a210213 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,12 @@ # aws-lambda-host -A modern .NET framework for building AWS Lambda functions using familiar ASP.NET Core patterns. - [![Main Build](https://github.com/j-d-ha/aws-lambda-host/actions/workflows/main-build.yaml/badge.svg)](https://github.com/j-d-ha/aws-lambda-host/actions/workflows/main-build.yaml) [![codecov](https://codecov.io/gh/j-d-ha/aws-lambda-host/graph/badge.svg?token=BWORPTQ0UK)](https://codecov.io/gh/j-d-ha/aws-lambda-host) [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=j-d-ha_aws-lambda-host&metric=alert_status&token=9fb519975d91379dcfbc6c13a4bd4207131af6e3)](https://sonarcloud.io/summary/new_code?id=j-d-ha_aws-lambda-host) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) +A modern .NET framework for building AWS Lambda functions using familiar ASP.NET Core patterns. + > ⚠️ **Development Status**: This project is actively under development and not yet > production-ready. Breaking changes may occur in future versions. Use at your own discretion in > production environments. From ed3966912bc671717faa3b089acc9731fd67fd93 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:23:13 -0500 Subject: [PATCH 02/10] refactor: simplify LambdaOpenTelemetryAdapters methods - Removed redundant IServiceProvider parameter from tracer methods. - Improved null checks using `ArgumentNullException.ThrowIfNull`. - Consolidated and simplified code structure for clarity and maintainability. --- .../LambdaOpenTelemetryAdapters.cs | 353 +++++++++--------- 1 file changed, 174 insertions(+), 179 deletions(-) diff --git a/src/AwsLambda.Host.OpenTelemetry/LambdaOpenTelemetryAdapters.cs b/src/AwsLambda.Host.OpenTelemetry/LambdaOpenTelemetryAdapters.cs index dfe32a86..b23c9f7c 100644 --- a/src/AwsLambda.Host.OpenTelemetry/LambdaOpenTelemetryAdapters.cs +++ b/src/AwsLambda.Host.OpenTelemetry/LambdaOpenTelemetryAdapters.cs @@ -11,202 +11,197 @@ namespace Microsoft.Extensions.DependencyInjection; /// public static class LambdaOpenTelemetryServiceProviderExtensions { - /// - /// Creates a middleware function that traces Lambda invocations with both event and response - /// types. - /// - /// The type of the Lambda event expected in the context. - /// The type of the Lambda response expected in the context. - /// - /// The service provider containing the configured - /// . - /// - /// A middleware function that wraps the Lambda invocation with OpenTelemetry tracing. - /// - /// - /// Important: These methods are primarily intended to be used by source generators - /// and interceptors. Direct usage is not recommended. Source generation and interception are - /// the primary use cases for automatic tracing integration. - /// - /// - /// Uses the registered to wrap Lambda invocations with - /// distributed tracing capabilities through AWS Lambda instrumentation. This method is a - /// wrapper around from - /// the - /// OpenTelemetry.Instrumentation.AWSLambda - /// NuGet package. - /// - /// - /// The context must contain an event of type and the handler - /// must set a response of type . - /// - /// - /// TracerProvider Registration Required: A instance - /// must be registered in the dependency injection container before calling these methods. - /// Failure to register a will result in an - /// being thrown at startup. - /// - /// - /// - /// Thrown if the context event is not of type - /// or if the context response is not of type - /// , or if a instance is not - /// registered in the dependency injection container. - /// - public static Func GetOpenTelemetryTracer< - TEvent, - TResponse - >(this IServiceProvider services) + extension(IServiceProvider services) { - var tracerProvider = services.GetRequiredService(); - - return next => + /// + /// Creates a middleware function that traces Lambda invocations with both event and response + /// types. + /// + /// The type of the Lambda event expected in the context. + /// The type of the Lambda response expected in the context. + /// A middleware function that wraps the Lambda invocation with OpenTelemetry tracing. + /// + /// + /// Important: These methods are primarily intended to be used by source generators + /// and interceptors. Direct usage is not recommended. Source generation and interception are + /// the primary use cases for automatic tracing integration. + /// + /// + /// Uses the registered to wrap Lambda invocations with + /// distributed tracing capabilities through AWS Lambda instrumentation. This method is a + /// wrapper around from + /// the + /// OpenTelemetry.Instrumentation.AWSLambda + /// NuGet package. + /// + /// + /// The context must contain an event of type and the handler + /// must set a response of type . + /// + /// + /// TracerProvider Registration Required: A instance + /// must be registered in the dependency injection container before calling these methods. + /// Failure to register a will result in an + /// being thrown at startup. + /// + /// + /// + /// Thrown if the context event is not of type + /// or if the context response is not of type + /// , or if a instance is not + /// registered in the dependency injection container. + /// + public Func GetOpenTelemetryTracer< + TEvent, + TResponse + >() { - return async context => + ArgumentNullException.ThrowIfNull(services); + + var tracerProvider = services.GetRequiredService(); + + return next => { - if (context.Event is not TEvent inputEvent) - throw new InvalidOperationException( - $"Lambda event of type '{typeof(TEvent).FullName}' is not available in the context." - ); + return async context => + { + if (context.Event is not TEvent inputEvent) + throw new InvalidOperationException( + $"Lambda event of type '{typeof(TEvent).FullName}' is not available in the context." + ); - await AWSLambdaWrapper.TraceAsync( - tracerProvider, - async Task (_, _) => - { - await next(context); - - if (context.Response is not TResponse result) - throw new InvalidOperationException( - $"Lambda response of type '{typeof(TResponse).FullName}' is not available in the context." - ); - - return result; - }, - inputEvent, - context - ); - }; - }; - } + await AWSLambdaWrapper.TraceAsync( + tracerProvider, + async Task (_, _) => + { + await next(context); - /// Creates a middleware function that traces Lambda invocations with only a response type. - /// The type of the Lambda response expected in the context. - /// - /// - /// - /// - /// - /// The event - /// type is not relevant or known when using this overload. - /// - /// - /// Thrown if the context response is not of type - /// . - /// - public static Func< - LambdaInvocationDelegate, - LambdaInvocationDelegate - > GetOpenTelemetryTracerNoEvent(this IServiceProvider services) - { - var tracerProvider = services.GetRequiredService(); + if (context.Response is not TResponse result) + throw new InvalidOperationException( + $"Lambda response of type '{typeof(TResponse).FullName}' is not available in the context." + ); - return next => + return result; + }, + inputEvent, + context + ); + }; + }; + } + + /// Creates a middleware function that traces Lambda invocations with only a response type. + /// The type of the Lambda response expected in the context. + /// + /// + /// The event + /// type is not relevant or known when using this overload. + /// + /// + /// Thrown if the context response is not of type + /// . + /// + public Func< + LambdaInvocationDelegate, + LambdaInvocationDelegate + > GetOpenTelemetryTracerNoEvent() { - return async context => + ArgumentNullException.ThrowIfNull(services); + + var tracerProvider = services.GetRequiredService(); + + return next => { - await AWSLambdaWrapper.TraceAsync( - tracerProvider, - async Task (object? _, ILambdaContext _) => - { - await next(context); - - if (context.Response is not TResponse result) - throw new InvalidOperationException( - $"Lambda response of type '{typeof(TResponse).FullName}' is not available in the context." - ); - - return result; - }, - null, - context - ); - }; - }; - } + return async context => + { + await AWSLambdaWrapper.TraceAsync( + tracerProvider, + async Task (object? _, ILambdaContext _) => + { + await next(context); - /// Creates a middleware function that traces Lambda invocations with only an event type. - /// The type of the Lambda event expected in the context. - /// - /// - /// - /// - /// - /// The - /// response type is not relevant or known when using this overload. - /// - /// - /// Thrown if the context event is not of type - /// . - /// - public static Func< - LambdaInvocationDelegate, - LambdaInvocationDelegate - > GetOpenTelemetryTracerNoResponse(this IServiceProvider services) - { - var tracerProvider = services.GetRequiredService(); + if (context.Response is not TResponse result) + throw new InvalidOperationException( + $"Lambda response of type '{typeof(TResponse).FullName}' is not available in the context." + ); + + return result; + }, + null, + context + ); + }; + }; + } - return next => + /// Creates a middleware function that traces Lambda invocations with only an event type. + /// The type of the Lambda event expected in the context. + /// + /// + /// The + /// response type is not relevant or known when using this overload. + /// + /// + /// Thrown if the context event is not of type + /// . + /// + public Func< + LambdaInvocationDelegate, + LambdaInvocationDelegate + > GetOpenTelemetryTracerNoResponse() { - return async context => + ArgumentNullException.ThrowIfNull(services); + + var tracerProvider = services.GetRequiredService(); + + return next => { - if (context.Event is not TEvent inputEvent) - throw new InvalidOperationException( - $"Lambda event of type '{typeof(TEvent).FullName}' is not available in the context." - ); + return async context => + { + if (context.Event is not TEvent inputEvent) + throw new InvalidOperationException( + $"Lambda event of type '{typeof(TEvent).FullName}' is not available in the context." + ); - await AWSLambdaWrapper.TraceAsync( - tracerProvider, - async Task (_, _) => await next(context), - inputEvent, - context - ); + await AWSLambdaWrapper.TraceAsync( + tracerProvider, + async Task (_, _) => await next(context), + inputEvent, + context + ); + }; }; - }; - } - - /// - /// Creates a middleware function that traces Lambda invocations without specific event or - /// response types. - /// - /// - /// - /// - /// - /// - /// Neither - /// event nor response types are relevant or known when using this overload. - /// - public static Func< - LambdaInvocationDelegate, - LambdaInvocationDelegate - > GetOpenTelemetryTracerNoEventNoResponse(this IServiceProvider services) - { - var tracerProvider = services.GetRequiredService(); + } - return next => + /// + /// Creates a middleware function that traces Lambda invocations without specific event or + /// response types. + /// + /// + /// + /// Neither + /// event nor response types are relevant or known when using this overload. + /// + public Func< + LambdaInvocationDelegate, + LambdaInvocationDelegate + > GetOpenTelemetryTracerNoEventNoResponse() { - return async context => + ArgumentNullException.ThrowIfNull(services); + + var tracerProvider = services.GetRequiredService(); + + return next => { - await AWSLambdaWrapper.TraceAsync( - tracerProvider, - async Task (object? _, ILambdaContext _) => await next(context), - null, - context - ); + return async context => + { + await AWSLambdaWrapper.TraceAsync( + tracerProvider, + async Task (object? _, ILambdaContext _) => await next(context), + null, + context + ); + }; }; - }; + } } } From 951667f992b63eabeb0a1f83cec975e175ffeff2 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:23:20 -0500 Subject: [PATCH 03/10] feat(tests): add unit tests for OpenTelemetry integration - Added `AwsLambda.Host.OpenTelemetry.UnitTests` project to solution. - Included initial test class `LambdaOpenTelemetryServiceProviderExtensionsTests` with a placeholder test. - Configured multiple target frameworks for the test project. - Added necessary dependencies to support testing, mocking, and coverage tools. --- AwsLambda.Host.sln | 7 ++++++ ...Lambda.Host.OpenTelemetry.UnitTests.csproj | 22 +++++++++++++++++++ ...TelemetryServiceProviderExtensionsTests.cs | 9 ++++++++ 3 files changed, 38 insertions(+) create mode 100644 tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj create mode 100644 tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs diff --git a/AwsLambda.Host.sln b/AwsLambda.Host.sln index 6d17617c..6e5b1833 100644 --- a/AwsLambda.Host.sln +++ b/AwsLambda.Host.sln @@ -49,6 +49,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.Envelopes.Ap EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.Envelopes.UnitTests", "tests\AwsLambda.Host.Envelopes.UnitTests\AwsLambda.Host.Envelopes.UnitTests.csproj", "{0BBBEF49-6478-4394-9BD7-B9AFCA280B3D}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AwsLambda.Host.OpenTelemetry.UnitTests", "tests\AwsLambda.Host.OpenTelemetry.UnitTests\AwsLambda.Host.OpenTelemetry.UnitTests.csproj", "{B8FD7488-07A9-4CA0-8F40-483ECE109159}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -111,6 +113,10 @@ Global {0BBBEF49-6478-4394-9BD7-B9AFCA280B3D}.Debug|Any CPU.Build.0 = Debug|Any CPU {0BBBEF49-6478-4394-9BD7-B9AFCA280B3D}.Release|Any CPU.ActiveCfg = Release|Any CPU {0BBBEF49-6478-4394-9BD7-B9AFCA280B3D}.Release|Any CPU.Build.0 = Release|Any CPU + {B8FD7488-07A9-4CA0-8F40-483ECE109159}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B8FD7488-07A9-4CA0-8F40-483ECE109159}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B8FD7488-07A9-4CA0-8F40-483ECE109159}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B8FD7488-07A9-4CA0-8F40-483ECE109159}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(NestedProjects) = preSolution {6B8CD8FF-832E-9F88-FB36-2173461F6678} = {B36A84DF-456D-A817-6EDD-3EC3E7F6E11F} @@ -128,5 +134,6 @@ Global {8E3037D3-BEB0-4A0B-A09A-CF31F50D0508} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359} {83E4EBD0-EE1C-422D-A22C-BC6A1895A879} = {1C3C52D9-2936-4A0C-A9C8-F330F22B8359} {0BBBEF49-6478-4394-9BD7-B9AFCA280B3D} = {0AB3BF05-4346-4AA6-1389-037BE0695223} + {B8FD7488-07A9-4CA0-8F40-483ECE109159} = {0AB3BF05-4346-4AA6-1389-037BE0695223} EndGlobalSection EndGlobal diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj new file mode 100644 index 00000000..c146bae7 --- /dev/null +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj @@ -0,0 +1,22 @@ + + + net8.0;net9.0;net10.0 + enable + enable + false + false + + + + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs new file mode 100644 index 00000000..73103bc0 --- /dev/null +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs @@ -0,0 +1,9 @@ +using Xunit; + +namespace AwsLambda.Host.OpenTelemetry.UnitTests; + +public class LambdaOpenTelemetryServiceProviderExtensionsTests +{ + [Fact] + public void Test1() { } +} From 1634772e7ff91607881f5b70bd1d7ba2e880e486 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:34:26 -0500 Subject: [PATCH 04/10] feat(tests): expand LambdaOpenTelemetry extensions test coverage - Added comprehensive unit tests for `LambdaOpenTelemetryServiceProviderExtensions` methods. - Verified behavior for various scenarios, including null checks, invalid types, and valid middleware flows. - Introduced helper models (`TestEvent`, `TestResponse`) to support test scenarios. - Updated project file to include necessary dependencies and project references. - Modified target frameworks and enabled `LangVersion=preview` for testing. --- ...Lambda.Host.OpenTelemetry.UnitTests.csproj | 7 +- ...TelemetryServiceProviderExtensionsTests.cs | 355 +++++++++++++++++- 2 files changed, 359 insertions(+), 3 deletions(-) diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj index c146bae7..fb315248 100644 --- a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj @@ -1,10 +1,11 @@  - net8.0;net9.0;net10.0 + net9.0;net10.0 enable enable false false + preview @@ -19,4 +20,8 @@ + + + + diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs index 73103bc0..3d130662 100644 --- a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs @@ -1,9 +1,360 @@ -using Xunit; +using AutoFixture; +using AwesomeAssertions; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using NSubstitute; +using OpenTelemetry.Trace; +using Xunit; namespace AwsLambda.Host.OpenTelemetry.UnitTests; +[TestSubject(typeof(LambdaOpenTelemetryServiceProviderExtensions))] public class LambdaOpenTelemetryServiceProviderExtensionsTests { + private readonly Fixture _fixture = new(); + private readonly IServiceProvider _serviceProvider = Substitute.For(); + private readonly TracerProvider _tracerProvider = Substitute.For(); + + public LambdaOpenTelemetryServiceProviderExtensionsTests() => + _serviceProvider.GetService(typeof(TracerProvider)).Returns(_tracerProvider); + + #region GetOpenTelemetryTracer + + [Fact] + public void GetOpenTelemetryTracer_WithNullServiceProvider_ThrowsArgumentNullException() + { + // Arrange + IServiceProvider? nullProvider = null; + + // Act + var action = () => nullProvider!.GetOpenTelemetryTracer(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracer_WithoutTracerProvider_ThrowsInvalidOperationException() + { + // Arrange + _serviceProvider.GetService(typeof(TracerProvider)).Returns(null); + + // Act + var action = () => _serviceProvider.GetOpenTelemetryTracer(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracer_ReturnsMiddlewareFunction() + { + // Act + var middleware = _serviceProvider.GetOpenTelemetryTracer(); + + // Assert + middleware.Should().NotBeNull(); + middleware.Should().BeOfType>(); + } + + [Fact] + public async Task GetOpenTelemetryTracer_WithIncorrectEventType_ThrowsInvalidOperationException() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracer(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var context = Substitute.For(); + context.Event.Returns(new object()); // Wrong type + context.Response.Returns(new TestResponse()); + + // Act + var action = async () => await wrappedDelegate(context); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task GetOpenTelemetryTracer_WithIncorrectResponseType_ThrowsInvalidOperationException() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracer(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var context = Substitute.For(); + context.Event.Returns(new TestEvent()); + context.Response.Returns(new object()); // Wrong type + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + var action = async () => await wrappedDelegate(context); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task GetOpenTelemetryTracer_WithValidEventAndResponse_CallsNextDelegate() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracer(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var testEvent = new TestEvent(); + var testResponse = new TestResponse(); + var context = Substitute.For(); + context.Event.Returns(testEvent); + context.Response.Returns(testResponse); + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + await wrappedDelegate(context); + + // Assert + await nextDelegate.Received(1)(Arg.Any()); + } + + #endregion + + #region GetOpenTelemetryTracerNoEvent + + [Fact] + public void GetOpenTelemetryTracerNoEvent_WithNullServiceProvider_ThrowsArgumentNullException() + { + // Arrange + IServiceProvider? nullProvider = null; + + // Act + var action = () => nullProvider!.GetOpenTelemetryTracerNoEvent(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracerNoEvent_WithoutTracerProvider_ThrowsInvalidOperationException() + { + // Arrange + _serviceProvider.GetService(typeof(TracerProvider)).Returns(null); + + // Act + var action = () => _serviceProvider.GetOpenTelemetryTracerNoEvent(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracerNoEvent_ReturnsMiddlewareFunction() + { + // Act + var middleware = _serviceProvider.GetOpenTelemetryTracerNoEvent(); + + // Assert + middleware.Should().NotBeNull(); + middleware.Should().BeOfType>(); + } + + [Fact] + public async Task GetOpenTelemetryTracerNoEvent_WithIncorrectResponseType_ThrowsInvalidOperationException() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracerNoEvent(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var context = Substitute.For(); + context.Event.Returns(new object()); + context.Response.Returns(new object()); // Wrong type + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + var action = async () => await wrappedDelegate(context); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task GetOpenTelemetryTracerNoEvent_WithValidResponse_CallsNextDelegate() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracerNoEvent(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var testResponse = new TestResponse(); + var context = Substitute.For(); + context.Event.Returns(new object()); + context.Response.Returns(testResponse); + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + await wrappedDelegate(context); + + // Assert + await nextDelegate.Received(1)(Arg.Any()); + } + + #endregion + + #region GetOpenTelemetryTracerNoResponse + + [Fact] + public void GetOpenTelemetryTracerNoResponse_WithNullServiceProvider_ThrowsArgumentNullException() + { + // Arrange + IServiceProvider? nullProvider = null; + + // Act + var action = () => nullProvider!.GetOpenTelemetryTracerNoResponse(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracerNoResponse_WithoutTracerProvider_ThrowsInvalidOperationException() + { + // Arrange + _serviceProvider.GetService(typeof(TracerProvider)).Returns(null); + + // Act + var action = () => _serviceProvider.GetOpenTelemetryTracerNoResponse(); + + // Assert + action.Should().ThrowExactly(); + } + [Fact] - public void Test1() { } + public void GetOpenTelemetryTracerNoResponse_ReturnsMiddlewareFunction() + { + // Act + var middleware = _serviceProvider.GetOpenTelemetryTracerNoResponse(); + + // Assert + middleware.Should().NotBeNull(); + middleware.Should().BeOfType>(); + } + + [Fact] + public async Task GetOpenTelemetryTracerNoResponse_WithIncorrectEventType_ThrowsInvalidOperationException() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracerNoResponse(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var context = Substitute.For(); + context.Event.Returns(new object()); // Wrong type + context.Response.Returns(new object()); + + // Act + var action = async () => await wrappedDelegate(context); + + // Assert + await action.Should().ThrowAsync(); + } + + [Fact] + public async Task GetOpenTelemetryTracerNoResponse_WithValidEvent_CallsNextDelegate() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracerNoResponse(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var testEvent = new TestEvent(); + var context = Substitute.For(); + context.Event.Returns(testEvent); + context.Response.Returns(new object()); + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + await wrappedDelegate(context); + + // Assert + await nextDelegate.Received(1)(Arg.Any()); + } + + #endregion + + #region GetOpenTelemetryTracerNoEventNoResponse + + [Fact] + public void GetOpenTelemetryTracerNoEventNoResponse_WithNullServiceProvider_ThrowsArgumentNullException() + { + // Arrange + IServiceProvider? nullProvider = null; + + // Act + var action = () => nullProvider!.GetOpenTelemetryTracerNoEventNoResponse(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracerNoEventNoResponse_WithoutTracerProvider_ThrowsInvalidOperationException() + { + // Arrange + _serviceProvider.GetService(typeof(TracerProvider)).Returns(null); + + // Act + var action = () => _serviceProvider.GetOpenTelemetryTracerNoEventNoResponse(); + + // Assert + action.Should().ThrowExactly(); + } + + [Fact] + public void GetOpenTelemetryTracerNoEventNoResponse_ReturnsMiddlewareFunction() + { + // Act + var middleware = _serviceProvider.GetOpenTelemetryTracerNoEventNoResponse(); + + // Assert + middleware.Should().NotBeNull(); + middleware.Should().BeOfType>(); + } + + [Fact] + public async Task GetOpenTelemetryTracerNoEventNoResponse_WithAnyEventAndResponse_CallsNextDelegate() + { + // Arrange + var middleware = _serviceProvider.GetOpenTelemetryTracerNoEventNoResponse(); + var nextDelegate = Substitute.For(); + var wrappedDelegate = middleware(nextDelegate); + + var context = Substitute.For(); + context.Event.Returns(new object()); + context.Response.Returns(new object()); + + nextDelegate(Arg.Any()).Returns(Task.CompletedTask); + + // Act + await wrappedDelegate(context); + + // Assert + await nextDelegate.Received(1)(Arg.Any()); + } + + #endregion + + #region Test Helpers + + private class TestEvent { } + + private class TestResponse { } + + #endregion } From b1f6d1b8e1be9ddaf8e20d2485a8a386ef193a7f Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:35:59 -0500 Subject: [PATCH 05/10] refactor(lambda-host): simplify and restructure OpenTelemetry shutdown extensions - Reorganized `OnShutdownOpenTelemetryExtensions` methods for improved readability and maintainability. - Removed redundant XML documentation in favor of concise summaries. - Consolidated method chaining to streamline extension behavior. - Enhanced exception handling with `ArgumentNullException.ThrowIfNull`. - Simplified `RunForceFlush` logic with consistent logging and timeout handling. --- .../OnShutdownOpenTelemetryExtensions.cs | 277 +++++++++--------- 1 file changed, 135 insertions(+), 142 deletions(-) diff --git a/src/AwsLambda.Host.OpenTelemetry/OnShutdownOpenTelemetryExtensions.cs b/src/AwsLambda.Host.OpenTelemetry/OnShutdownOpenTelemetryExtensions.cs index bae9a456..5984946b 100644 --- a/src/AwsLambda.Host.OpenTelemetry/OnShutdownOpenTelemetryExtensions.cs +++ b/src/AwsLambda.Host.OpenTelemetry/OnShutdownOpenTelemetryExtensions.cs @@ -14,159 +14,152 @@ public static class OnShutdownOpenTelemetryExtensions { private const string LogCategory = "AwsLambda.Host.OpenTelemetry"; - /// - /// Registers shutdown handlers to force flush both OpenTelemetry tracers and meters on Lambda - /// shutdown. - /// - /// The instance. - /// - /// The timeout in milliseconds for flush operations. Defaults to - /// . - /// - /// The same instance for method chaining. - /// - /// - /// This method registers shutdown handlers that force flush both tracer and meter providers - /// to ensure all telemetry data is exported before the Lambda container stops. - /// - /// - /// The flush operations respect Lambda's timeout constraints and log warnings if they cannot - /// complete within the allocated shutdown time. - /// - /// - public static ILambdaApplication OnShutdownFlushOpenTelemetry( - this ILambdaApplication application, - int timeoutMilliseconds = Timeout.Infinite - ) + extension(ILambdaApplication application) { - ArgumentNullException.ThrowIfNull(application); - - application.OnShutdownFlushMeter(timeoutMilliseconds); + /// + /// Registers shutdown handlers to force flush both OpenTelemetry tracers and meters on Lambda + /// shutdown. + /// + /// + /// The timeout in milliseconds for flush operations. Defaults to + /// . + /// + /// The same instance for method chaining. + /// + /// + /// This method registers shutdown handlers that force flush both tracer and meter providers + /// to ensure all telemetry data is exported before the Lambda container stops. + /// + /// + /// The flush operations respect Lambda's timeout constraints and log warnings if they cannot + /// complete within the allocated shutdown time. + /// + /// + public ILambdaApplication OnShutdownFlushOpenTelemetry( + int timeoutMilliseconds = Timeout.Infinite + ) + { + ArgumentNullException.ThrowIfNull(application); - application.OnShutdownFlushTracer(timeoutMilliseconds); + application.OnShutdownFlushMeter(timeoutMilliseconds); - return application; - } - - /// - /// Registers a shutdown handler to force flush the OpenTelemetry tracer provider on Lambda - /// shutdown. - /// - /// The instance. - /// - /// The timeout in milliseconds for the flush operation. Defaults to - /// . - /// - /// The same instance for method chaining. - /// - /// - /// This method registers a shutdown handler that force flushes the tracer provider to ensure - /// all distributed traces are exported before Lambda shutdown completes. - /// - /// - /// If no is registered in the dependency injection container, - /// this method safely returns without error. - /// - /// - public static ILambdaApplication OnShutdownFlushTracer( - this ILambdaApplication application, - int timeoutMilliseconds = Timeout.Infinite - ) - { - ArgumentNullException.ThrowIfNull(application); - - var tracerProvider = application.Services.GetRequiredService(); - var logger = - application.Services.GetService()?.CreateLogger(LogCategory) - ?? NullLogger.Instance; - - application.OnShutdown( - (_, cancellationToken) => - RunForceFlush( - "tracer", - tracerProvider.ForceFlush, - timeoutMilliseconds, - logger, - cancellationToken - ) - ); - - return application; - } + application.OnShutdownFlushTracer(timeoutMilliseconds); - /// - /// Registers a shutdown handler to force flush the OpenTelemetry meter provider on Lambda - /// shutdown. - /// - /// The instance. - /// - /// The timeout in milliseconds for the flush operation. Defaults to - /// . - /// - /// The same instance for method chaining. - /// - /// - /// This method registers a shutdown handler that force flushes the meter provider to ensure - /// all metrics are exported before Lambda shutdown completes. - /// - /// - /// If no is registered in the dependency injection container, - /// this method safely returns without error. - /// - /// - public static ILambdaApplication OnShutdownFlushMeter( - this ILambdaApplication application, - int timeoutMilliseconds = Timeout.Infinite - ) - { - ArgumentNullException.ThrowIfNull(application); - - var meterProvider = application.Services.GetRequiredService(); - var logger = - application.Services.GetService()?.CreateLogger(LogCategory) - ?? NullLogger.Instance; - - application.OnShutdown( - (_, cancellationToken) => - RunForceFlush( - "meter", - meterProvider.ForceFlush, - timeoutMilliseconds, - logger, - cancellationToken - ) - ); - - return application; - } + return application; + } - /// Executes a force flush operation with timeout handling and logging. - private static async Task RunForceFlush( - string providerName, - Func flusher, - int timeoutMilliseconds, - ILogger logger, - CancellationToken cancellationToken - ) - { - var flusherTask = Task.Run(() => flusher(timeoutMilliseconds), cancellationToken); + /// + /// Registers a shutdown handler to force flush the OpenTelemetry tracer provider on Lambda + /// shutdown. + /// + /// + /// The timeout in milliseconds for the flush operation. Defaults to + /// . + /// + /// The same instance for method chaining. + /// + /// + /// This method registers a shutdown handler that force flushes the tracer provider to ensure + /// all distributed traces are exported before Lambda shutdown completes. + /// + /// + /// If no is registered in the dependency injection container, + /// this method safely returns without error. + /// + /// + public ILambdaApplication OnShutdownFlushTracer(int timeoutMilliseconds = Timeout.Infinite) + { + ArgumentNullException.ThrowIfNull(application); + + var tracerProvider = application.Services.GetRequiredService(); + var logger = + application.Services.GetService()?.CreateLogger(LogCategory) + ?? NullLogger.Instance; + + application.OnShutdown( + (_, cancellationToken) => + RunForceFlush( + "tracer", + tracerProvider.ForceFlush, + timeoutMilliseconds, + logger, + cancellationToken + ) + ); - await Task.WhenAny(flusherTask, Task.Delay(Timeout.Infinite, cancellationToken)); + return application; + } - if (flusherTask.Status != TaskStatus.RanToCompletion) + /// + /// Registers a shutdown handler to force flush the OpenTelemetry meter provider on Lambda + /// shutdown. + /// + /// + /// The timeout in milliseconds for the flush operation. Defaults to + /// . + /// + /// The same instance for method chaining. + /// + /// + /// This method registers a shutdown handler that force flushes the meter provider to ensure + /// all metrics are exported before Lambda shutdown completes. + /// + /// + /// If no is registered in the dependency injection container, + /// this method safely returns without error. + /// + /// + public ILambdaApplication OnShutdownFlushMeter(int timeoutMilliseconds = Timeout.Infinite) { - logger.LogWarning( - "OpenTelemetry {ProviderName} provider force flush failed to complete within allocated time", - providerName + ArgumentNullException.ThrowIfNull(application); + + var meterProvider = application.Services.GetRequiredService(); + var logger = + application.Services.GetService()?.CreateLogger(LogCategory) + ?? NullLogger.Instance; + + application.OnShutdown( + (_, cancellationToken) => + RunForceFlush( + "meter", + meterProvider.ForceFlush, + timeoutMilliseconds, + logger, + cancellationToken + ) ); - return; + return application; } - logger.LogInformation( - "OpenTelemetry {ProviderName} provider force flush {Status}", - providerName, - flusherTask.Result ? "succeeded" : "failed" - ); + /// Executes a force flush operation with timeout handling and logging. + private static async Task RunForceFlush( + string providerName, + Func flusher, + int timeoutMilliseconds, + ILogger logger, + CancellationToken cancellationToken + ) + { + var flusherTask = Task.Run(() => flusher(timeoutMilliseconds), cancellationToken); + + await Task.WhenAny(flusherTask, Task.Delay(Timeout.Infinite, cancellationToken)); + + if (flusherTask.Status != TaskStatus.RanToCompletion) + { + logger.LogWarning( + "OpenTelemetry {ProviderName} provider force flush failed to complete within allocated time", + providerName + ); + + return; + } + + logger.LogInformation( + "OpenTelemetry {ProviderName} provider force flush {Status}", + providerName, + flusherTask.Result ? "succeeded" : "failed" + ); + } } } From e94128a341a6a5276ae1d066aa00d05238648814 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sat, 15 Nov 2025 15:59:36 -0500 Subject: [PATCH 06/10] refactor(tests): improve readability in LambdaOpenTelemetryServiceProviderExtensionsTests - Removed unused `AutoFixture` dependency. - Reorganized long method declarations to improve readability. - Standardized formatting across `TestEvent` and `TestResponse` helper classes. - Enhanced code consistency in test case definitions and constructor initialization. --- .../LambdaOpenTelemetryServiceProviderExtensionsTests.cs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs index 3d130662..93f5ed03 100644 --- a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/LambdaOpenTelemetryServiceProviderExtensionsTests.cs @@ -1,5 +1,4 @@ -using AutoFixture; -using AwesomeAssertions; +using AwesomeAssertions; using JetBrains.Annotations; using Microsoft.Extensions.DependencyInjection; using NSubstitute; @@ -11,7 +10,6 @@ namespace AwsLambda.Host.OpenTelemetry.UnitTests; [TestSubject(typeof(LambdaOpenTelemetryServiceProviderExtensions))] public class LambdaOpenTelemetryServiceProviderExtensionsTests { - private readonly Fixture _fixture = new(); private readonly IServiceProvider _serviceProvider = Substitute.For(); private readonly TracerProvider _tracerProvider = Substitute.For(); From 70a4cccbe6a2904d5f74578cf2d7d89d3fe91b6a Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 16 Nov 2025 10:19:22 -0500 Subject: [PATCH 07/10] feat(tests): add unit tests for OpenTelemetry shutdown extensions - Added `OnShutdownFlushTracer` tests to validate different shutdown scenarios for tracer providers. - Added `OnShutdownFlushMeter` tests to validate meter provider force flush behaviors on shutdown. - Added `OnShutdownFlushOpenTelemetry` tests to cover combined tracer and meter flush scenarios. - Verified logging messages for successful, failed, and timed-out flush operations. - Introduced helper classes and mocks (`MockOptions`, `MockProcessor`, `MockMetricReader`) for test setup. --- .../OnShutdownOpenTelemetryExtensionsTests.cs | 635 ++++++++++++++++++ 1 file changed, 635 insertions(+) create mode 100644 tests/AwsLambda.Host.OpenTelemetry.UnitTests/OnShutdownOpenTelemetryExtensionsTests.cs diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/OnShutdownOpenTelemetryExtensionsTests.cs b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/OnShutdownOpenTelemetryExtensionsTests.cs new file mode 100644 index 00000000..69dadef4 --- /dev/null +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/OnShutdownOpenTelemetryExtensionsTests.cs @@ -0,0 +1,635 @@ +using System.Diagnostics; +using AwesomeAssertions; +using JetBrains.Annotations; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Logging; +using NSubstitute; +using OpenTelemetry; +using OpenTelemetry.Metrics; +using OpenTelemetry.Trace; +using Xunit; + +namespace AwsLambda.Host.OpenTelemetry.UnitTests; + +[TestSubject(typeof(OnShutdownOpenTelemetryExtensions))] +public class OnShutdownOpenTelemetryExtensionsTests +{ + #region OnShutdownFlushTracer Tests + + [Fact] + public void OnShutdownFlushTracer_ThrowsOnNullILambdaApplication() + { + // Arrange + ILambdaApplication mockApp = null!; + + // Act + Action act = () => mockApp.OnShutdownFlushTracer(); + + // Assert + act.Should().ThrowExactly(); + } + + [Fact] + public void OnShutdownFlushTracer_ThrowsOnNoTracerProviderRegistered() + { + // Arrange + var mockApp = Substitute.For(); + var mockServiceProvider = Substitute.For(); + mockServiceProvider.GetService(typeof(TracerProvider)).Returns(null); + mockApp.Services.Returns(mockServiceProvider); + + // Act + Action act = () => mockApp.OnShutdownFlushTracer(); + + // Assert + act.Should() + .ThrowExactly() + .WithMessage( + "No service for type 'OpenTelemetry.Trace.TracerProvider' has been registered." + ); + } + + [Fact] + public void OnShutdownFlushTracer_ShouldNotThrowOnNoILoggerFactoryRegistered() + { + // Arrange + var mockApp = Substitute.For(); + var mockServiceProvider = Substitute.For(); + mockServiceProvider + .GetService(typeof(TracerProvider)) + .Returns(Substitute.For()); + mockServiceProvider.GetService(typeof(ILoggerFactory)).Returns(null); + mockApp.Services.Returns(mockServiceProvider); + + // Act + Action act = () => mockApp.OnShutdownFlushTracer(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public async Task OnShutdownFlushTracer_RegistersShutdownHandler_AndSuccessfullyFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp(a => capturedShutdownAction = a); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushTracer(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush succeeded", + } + ); + } + + [Fact] + public async Task OnShutdownFlushTracer_RegistersShutdownHandler_AndFailedFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp( + a => capturedShutdownAction = a, + options => options.TracerShouldSucceed = false + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushTracer(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush failed", + } + ); + } + + [Fact] + public async Task OnShutdownFlushTracer_RegistersShutdownHandler_AndCanceledFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp( + a => capturedShutdownAction = a, + options => options.TracerDelay = TimeSpan.FromMilliseconds(10) + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushTracer(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, new CancellationToken(true)); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Warning, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush failed to complete within allocated time", + } + ); + } + + #endregion + + #region OnShutdownFlushMeter Tests + + [Fact] + public void OnShutdownFlushMeter_ThrowsOnNullILambdaApplication() + { + // Arrange + ILambdaApplication mockApp = null!; + + // Act + Action act = () => mockApp.OnShutdownFlushMeter(); + + // Assert + act.Should().ThrowExactly(); + } + + [Fact] + public void OnShutdownFlushMeter_ThrowsOnNoMeterProviderRegistered() + { + // Arrange + var mockApp = Substitute.For(); + var mockServiceProvider = Substitute.For(); + mockServiceProvider.GetService(typeof(MeterProvider)).Returns(null); + mockApp.Services.Returns(mockServiceProvider); + + // Act + Action act = () => mockApp.OnShutdownFlushMeter(); + + // Assert + act.Should() + .ThrowExactly() + .WithMessage( + "No service for type 'OpenTelemetry.Metrics.MeterProvider' has been registered." + ); + } + + [Fact] + public void OnShutdownFlushMeter_ShouldNotThrowOnNoILoggerFactoryRegistered() + { + // Arrange + var mockApp = Substitute.For(); + var mockServiceProvider = Substitute.For(); + mockServiceProvider + .GetService(typeof(MeterProvider)) + .Returns(Substitute.For()); + mockServiceProvider.GetService(typeof(ILoggerFactory)).Returns(null); + mockApp.Services.Returns(mockServiceProvider); + + // Act + Action act = () => mockApp.OnShutdownFlushMeter(); + + // Assert + act.Should().NotThrow(); + } + + [Fact] + public async Task OnShutdownFlushMeter_RegistersShutdownHandler_AndSuccessfullyFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp(a => capturedShutdownAction = a); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushMeter(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush succeeded", + } + ); + } + + [Fact] + public async Task OnShutdownFlushMeter_RegistersShutdownHandler_AndFailedFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp( + a => capturedShutdownAction = a, + options => options.MeterShouldSucceed = false + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushMeter(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush failed", + } + ); + } + + [Fact] + public async Task OnShutdownFlushMeter_RegistersShutdownHandler_AndCanceledFlushes() + { + // Arrange + LambdaShutdownDelegate? capturedShutdownAction = null; + var mockApp = CreateMockApp( + a => capturedShutdownAction = a, + options => options.MeterDelay = TimeSpan.FromMilliseconds(10) + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushMeter(); + + capturedShutdownAction.Should().NotBeNull(); + await capturedShutdownAction.Invoke(mockServiceProvider, new CancellationToken(true)); + + // Assert + result.Should().Be(mockApp); + mockApp.Received(1).OnShutdown(Arg.Any()); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Warning, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush failed to complete within allocated time", + } + ); + } + + #endregion + + #region OnShutdownFlushOpenTelemetry Tests + + [Fact] + public void OnShutdownFlushOpenTelemetry_ThrowsOnNullILambdaApplication() + { + // Arrange + ILambdaApplication mockApp = null!; + + // Act + Action act = () => mockApp.OnShutdownFlushOpenTelemetry(); + + // Assert + act.Should().ThrowExactly(); + } + + [Fact] + public void OnShutdownFlushOpenTelemetry_ReturnsApplicationForChaining() + { + // Arrange + var mockApp = CreateMockApp(_ => { }); + + // Act + var result = mockApp.OnShutdownFlushOpenTelemetry(); + + // Assert + result.Should().Be(mockApp); + } + + [Fact] + public void OnShutdownFlushOpenTelemetry_RegistersBothFlushHandlers() + { + // Arrange + var mockApp = CreateMockApp(_ => { }); + + // Act + mockApp.OnShutdownFlushOpenTelemetry(); + + // Assert + mockApp.Received(2).OnShutdown(Arg.Any()); + } + + [Fact] + public void OnShutdownFlushOpenTelemetry_PassesTimeoutToHandlers() + { + // Arrange + var mockApp = CreateMockApp(_ => { }); + const int customTimeout = 5000; + + // Act - Should not throw when passing custom timeout + Action act = () => mockApp.OnShutdownFlushOpenTelemetry(customTimeout); + + // Assert + act.Should().NotThrow(); + mockApp.Received(2).OnShutdown(Arg.Any()); + } + + [Fact] + public async Task OnShutdownFlushOpenTelemetry_BothFlushesBothSucceed() + { + // Arrange + var shutdownActions = new List(); + var mockApp = CreateMockApp(a => shutdownActions.Add(a)); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushOpenTelemetry(); + + shutdownActions.Should().HaveCount(2); + await shutdownActions[0].Invoke(mockServiceProvider, CancellationToken.None); + await shutdownActions[1].Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush succeeded", + } + ) + .And.ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush succeeded", + } + ); + } + + [Fact] + public async Task OnShutdownFlushOpenTelemetry_TracerFailsButMeterSucceeds() + { + // Arrange + var shutdownActions = new List(); + var mockApp = CreateMockApp( + a => shutdownActions.Add(a), + options => options.TracerShouldSucceed = false + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushOpenTelemetry(); + + shutdownActions.Should().HaveCount(2); + await shutdownActions[0].Invoke(mockServiceProvider, CancellationToken.None); + await shutdownActions[1].Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush succeeded", + } + ) + .And.ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush failed", + } + ); + } + + [Fact] + public async Task OnShutdownFlushOpenTelemetry_MeterFailsButTracerSucceeds() + { + // Arrange + var shutdownActions = new List(); + var mockApp = CreateMockApp( + a => shutdownActions.Add(a), + options => options.MeterShouldSucceed = false + ); + + var mockServiceProvider = Substitute.For(); + + // Act + var result = mockApp.OnShutdownFlushOpenTelemetry(); + + shutdownActions.Should().HaveCount(2); + await shutdownActions[0].Invoke(mockServiceProvider, CancellationToken.None); + await shutdownActions[1].Invoke(mockServiceProvider, CancellationToken.None); + + // Assert + result.Should().Be(mockApp); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush failed", + } + ) + .And.ContainEquivalentOf( + new + { + Level = LogLevel.Information, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush succeeded", + } + ); + } + + [Fact] + public async Task OnShutdownFlushOpenTelemetry_BothFlushesTimeout() + { + // Arrange + var shutdownActions = new List(); + var mockApp = CreateMockApp( + a => shutdownActions.Add(a), + options => + { + options.MeterDelay = TimeSpan.FromMilliseconds(10); + options.TracerDelay = TimeSpan.FromMilliseconds(10); + } + ); + + var mockServiceProvider = Substitute.For(); + var cancellationToken = new CancellationToken(true); + + // Act + var result = mockApp.OnShutdownFlushOpenTelemetry(); + + shutdownActions.Should().HaveCount(2); + await shutdownActions[0].Invoke(mockServiceProvider, cancellationToken); + await shutdownActions[1].Invoke(mockServiceProvider, cancellationToken); + + // Assert + result.Should().Be(mockApp); + mockApp + .Services.GetFakeLogCollector() + .GetSnapshot() + .Should() + .ContainEquivalentOf( + new + { + Level = LogLevel.Warning, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry meter provider force flush failed to complete within allocated time", + } + ) + .And.ContainEquivalentOf( + new + { + Level = LogLevel.Warning, + Category = "AwsLambda.Host.OpenTelemetry", + Message = "OpenTelemetry tracer provider force flush failed to complete within allocated time", + } + ); + } + + #endregion + + #region Test Helpers + + private static ILambdaApplication CreateMockApp( + Action onShutdown, + Action? configureOptions = null + ) + { + var options = new MockOptions(); + configureOptions?.Invoke(options); + + var mockApp = Substitute.For(); + + mockApp.OnShutdown(Arg.Do(onShutdown)).Returns(mockApp); + + var serviceCollection = new ServiceCollection(); + + serviceCollection + .AddOpenTelemetry() + .WithTracing(builder => + { + builder.AddProcessor( + new MockProcessor(options.TracerDelay, options.TracerShouldSucceed) + ); + }) + .WithMetrics(builder => + { + builder.AddReader( + new MockMetricReader(options.MeterDelay, options.MeterShouldSucceed) + ); + }); + + serviceCollection.AddLogging(builder => + { + builder.AddFakeLogging(); + }); + + var serviceProvider = serviceCollection.BuildServiceProvider(); + + mockApp.Services.Returns(serviceProvider); + + return mockApp; + } + + private class MockOptions + { + public TimeSpan TracerDelay { get; set; } = TimeSpan.Zero; + public bool TracerShouldSucceed { get; set; } = true; + public TimeSpan MeterDelay { get; set; } = TimeSpan.Zero; + public bool MeterShouldSucceed { get; set; } = true; + } + + private class MockProcessor(TimeSpan delay, bool shouldSucceed) : BaseProcessor + { + protected override bool OnForceFlush(int timeoutMilliseconds) + { + Thread.Sleep(delay); + return shouldSucceed; + } + } + + private class MockMetricReader(TimeSpan delay, bool shouldSucceed) : MetricReader + { + protected override bool OnCollect(int timeoutMilliseconds) + { + Thread.Sleep(delay); + return shouldSucceed; + } + } + + #endregion +} From 402957adddf37d3e0fa17c7d2e029444074dffb6 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 16 Nov 2025 10:19:32 -0500 Subject: [PATCH 08/10] feat(tests): update dependencies and enhance test projects - Added `NSubstitute.Analyzers.CSharp` to all test projects for improved mocking verification. - Integrated `OpenTelemetry.Exporter.InMemory` into OpenTelemetry test project for in-memory tracing. - Added `Microsoft.Extensions.Diagnostics.Testing` to support diagnostic testing scenarios. - Updated `Directory.Packages.props` to include new package versions for consistent versioning. --- Directory.Packages.props | 3 +++ .../AwsLambda.Host.Envelopes.UnitTests.csproj | 4 ++++ .../AwsLambda.Host.OpenTelemetry.UnitTests.csproj | 9 ++++++++- .../AwsLambda.Host.SourceGenerators.UnitTests.csproj | 4 ++++ .../AwsLambda.Host.UnitTests.csproj | 4 ++++ 5 files changed, 23 insertions(+), 1 deletion(-) diff --git a/Directory.Packages.props b/Directory.Packages.props index e8683930..3f38325a 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -19,6 +19,8 @@ Include="Microsoft.CodeAnalysis.CSharp.SourceGenerators.Testing" Version="1.1.2" /> + + @@ -36,6 +38,7 @@ + diff --git a/tests/AwsLambda.Host.Envelopes.UnitTests/AwsLambda.Host.Envelopes.UnitTests.csproj b/tests/AwsLambda.Host.Envelopes.UnitTests/AwsLambda.Host.Envelopes.UnitTests.csproj index 40de6638..a9e17b22 100644 --- a/tests/AwsLambda.Host.Envelopes.UnitTests/AwsLambda.Host.Envelopes.UnitTests.csproj +++ b/tests/AwsLambda.Host.Envelopes.UnitTests/AwsLambda.Host.Envelopes.UnitTests.csproj @@ -13,6 +13,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive diff --git a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj index fb315248..c2523950 100644 --- a/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj +++ b/tests/AwsLambda.Host.OpenTelemetry.UnitTests/AwsLambda.Host.OpenTelemetry.UnitTests.csproj @@ -1,11 +1,11 @@  net9.0;net10.0 + preview enable enable false false - preview @@ -14,11 +14,18 @@ + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + all runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/AwsLambda.Host.SourceGenerators.UnitTests/AwsLambda.Host.SourceGenerators.UnitTests.csproj b/tests/AwsLambda.Host.SourceGenerators.UnitTests/AwsLambda.Host.SourceGenerators.UnitTests.csproj index 433c48a3..e116a4d2 100644 --- a/tests/AwsLambda.Host.SourceGenerators.UnitTests/AwsLambda.Host.SourceGenerators.UnitTests.csproj +++ b/tests/AwsLambda.Host.SourceGenerators.UnitTests/AwsLambda.Host.SourceGenerators.UnitTests.csproj @@ -18,6 +18,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + diff --git a/tests/AwsLambda.Host.UnitTests/AwsLambda.Host.UnitTests.csproj b/tests/AwsLambda.Host.UnitTests/AwsLambda.Host.UnitTests.csproj index 3f31afad..027c845a 100644 --- a/tests/AwsLambda.Host.UnitTests/AwsLambda.Host.UnitTests.csproj +++ b/tests/AwsLambda.Host.UnitTests/AwsLambda.Host.UnitTests.csproj @@ -15,6 +15,10 @@ + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + all runtime; build; native; contentfiles; analyzers; buildtransitive From 9ab9577310cfafb89103b70093b31166d8b41ab5 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 16 Nov 2025 10:21:20 -0500 Subject: [PATCH 09/10] feat(dependabot): update schedules to daily and add npm and dotnet-tools support - Changed Dependabot update schedule from weekly to daily for nuget and GitHub Actions ecosystems. - Added configurations for npm and dotnet-tools ecosystems with daily updates. - Defined minor-and-patch groupings for consistent dependency management. --- .github/dependabot.yml | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index a94f508a..310c7bad 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -3,7 +3,7 @@ updates: - package-ecosystem: "nuget" directory: "/" schedule: - interval: "weekly" + interval: "daily" open-pull-requests-limit: 10 groups: minor-and-patch: @@ -14,7 +14,29 @@ updates: - package-ecosystem: "github-actions" directory: "/" schedule: - interval: "weekly" + interval: "daily" + open-pull-requests-limit: 10 + groups: + minor-and-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "daily" + open-pull-requests-limit: 10 + groups: + minor-and-patch: + update-types: + - "minor" + - "patch" + + - package-ecosystem: "dotnet-tools" + directory: ".config" + schedule: + interval: "daily" open-pull-requests-limit: 10 groups: minor-and-patch: From 1da70de850d81ee476ce1fb00c550728c0903904 Mon Sep 17 00:00:00 2001 From: Jonas Ha Date: Sun, 16 Nov 2025 10:25:28 -0500 Subject: [PATCH 10/10] feat(dependabot): update dotnet configuration and schedule - Changed `dotnet-tools` ecosystem to `dotnet-sdk` in Dependabot config. - Updated directory to `/` for `dotnet-sdk`. - Modified update schedule from daily to weekly for better control. --- .github/dependabot.yml | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 310c7bad..4d2775bd 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -33,13 +33,7 @@ updates: - "minor" - "patch" - - package-ecosystem: "dotnet-tools" - directory: ".config" + - package-ecosystem: "dotnet-sdk" + directory: "/" schedule: - interval: "daily" - open-pull-requests-limit: 10 - groups: - minor-and-patch: - update-types: - - "minor" - - "patch" + interval: "weekly" \ No newline at end of file