From ad94fb9d505d1645f4278dbd5b2bdf3be06cfdf1 Mon Sep 17 00:00:00 2001 From: Norm Johanson Date: Mon, 14 Sep 2026 10:05:02 -0700 Subject: [PATCH] In Lambda managed instances (multi-concurrency) mode, emit a structured DEBUG log during init reporting the worker count and execution environment max concurrency when the JSON log format is enabled. --- .../9efcd50c-f1a0-4b57-90e1-20b54c382f2e.json | 11 ++++ .../Bootstrap/LambdaBootstrap.cs | 9 +++ .../Helpers/Utils.cs | 49 +++++++++++++- .../LambdaBootstrapMultiConcurrencyTests.cs | 65 +++++++++++++++++++ .../LogMessageFormatterTests.cs | 26 ++++++++ .../CapturingConsoleLoggerWriter.cs | 26 ++++++++ .../TestMultiConcurrencyRuntimeApiClient.cs | 2 +- .../UtilsTest.cs | 64 ++++++++++++++++++ 8 files changed, 250 insertions(+), 2 deletions(-) create mode 100644 .autover/changes/9efcd50c-f1a0-4b57-90e1-20b54c382f2e.json create mode 100644 Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/CapturingConsoleLoggerWriter.cs diff --git a/.autover/changes/9efcd50c-f1a0-4b57-90e1-20b54c382f2e.json b/.autover/changes/9efcd50c-f1a0-4b57-90e1-20b54c382f2e.json new file mode 100644 index 000000000..1703552ac --- /dev/null +++ b/.autover/changes/9efcd50c-f1a0-4b57-90e1-20b54c382f2e.json @@ -0,0 +1,11 @@ +{ + "Projects": [ + { + "Name": "Amazon.Lambda.RuntimeSupport", + "Type": "Patch", + "ChangelogMessages": [ + "In Lambda managed instances (multi-concurrency) mode, emit a structured DEBUG log during init reporting the worker count and execution environment max concurrency when the JSON log format is enabled." + ] + } + ] +} diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs index 87445c768..61012ac09 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Bootstrap/LambdaBootstrap.cs @@ -294,6 +294,15 @@ internal LambdaBootstrap(HttpClient httpClient, LambdaBootstrapHandler handler, var processingTasksCount = Utils.DetermineProcessingTaskCount(_environmentVariables, Environment.ProcessorCount); _logger.LogInformation($"Using {processingTasksCount} tasks for invoke processing loops"); + // In multi concurrency (Lambda managed instances) mode, emit a one time DEBUG log reporting the worker + // count and execution environment max concurrency for observability. This is a no-op unless the function + // is in multi concurrency mode with the JSON log format configured. + Utils.EmitWorkerPoolInitializingLog( + Client.ConsoleLogger, + _environmentVariables, + processingTasksCount, + Utils.GetMaxConcurrency(_environmentVariables)); + if (processingTasksCount == 1) { await ProcessingLoop(runOnce, cancellationToken); diff --git a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs index 1b99448b8..845814dcc 100644 --- a/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs +++ b/Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Utils.cs @@ -21,12 +21,59 @@ namespace Amazon.Lambda.RuntimeSupport.Helpers { internal static class Utils { + // The event name emitted in the worker pool initialization debug log. This matches the event name used by the + // Java and Python Lambda runtimes so the log is queryable consistently across languages. + internal const string WorkerPoolInitializingEvent = "runtime_worker_pool_initializing"; + + // The message template used for the worker pool initialization debug log. The named properties are emitted as + // top level fields in the structured JSON log (event, workerCount, executionEnvironmentMaxConcurrency). + internal const string WorkerPoolInitializingLogTemplate = "{event} workerCount={workerCount} executionEnvironmentMaxConcurrency={executionEnvironmentMaxConcurrency}"; + public static bool IsRunningNativeAot() { - // If dynamic code is not supported we are most likely running in an AOT environment. + // If dynamic code is not supported we are most likely running in an AOT environment. return !RuntimeFeature.IsDynamicCodeSupported; } + /// + /// Determines if the customer configured the Lambda function to use the JSON log format. This mirrors the + /// resolution done by : the .NET runtime specific environment variable is + /// checked first, falling back to the Lambda platform environment variable. + /// + internal static bool IsJsonLogFormat(IEnvironmentVariables environmentVariables) + { + var logFormat = environmentVariables.GetEnvironmentVariable(Constants.NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE); + if (string.IsNullOrEmpty(logFormat)) + { + logFormat = environmentVariables.GetEnvironmentVariable(Constants.LAMBDA_LOG_FORMAT_ENVIRONMENT_VARIABLE); + } + + return string.Equals(logFormat, Constants.LAMBDA_LOG_FORMAT_JSON, StringComparison.OrdinalIgnoreCase); + } + + /// + /// Emits a one time DEBUG log during init reporting the number of worker (processing) tasks and the execution + /// environment max concurrency. This is only emitted when the function is running in multi concurrency mode + /// (Lambda managed instances) and the JSON log format is configured. The DEBUG level means the log is only + /// surfaced when the function log level is set to Debug or Trace; that filtering is handled by the console + /// logger writer so it is not re-checked here. + /// + internal static void EmitWorkerPoolInitializingLog(IConsoleLoggerWriter consoleLogger, IEnvironmentVariables environmentVariables, int workerCount, int maxConcurrency) + { + if (consoleLogger == null) + return; + + if (!IsUsingMultiConcurrency(environmentVariables) || !IsJsonLogFormat(environmentVariables)) + return; + + consoleLogger.FormattedWriteLine( + LogLevelLoggerWriter.LogLevel.Debug.ToString(), + WorkerPoolInitializingLogTemplate, + WorkerPoolInitializingEvent, + workerCount, + maxConcurrency); + } + /// /// Determines if the Lambda function is running in multi concurrency mode. /// diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs index baf8854b3..06e7426cf 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LambdaBootstrapMultiConcurrencyTests.cs @@ -3,9 +3,11 @@ using System; using System.Collections.Concurrent; using System.Collections.Generic; +using System.Linq; using System.Threading; using System.Threading.Tasks; using Amazon.Lambda.Core; +using Amazon.Lambda.RuntimeSupport.Helpers; using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; using Amazon.Lambda.Serialization.Json; using Xunit; @@ -197,6 +199,69 @@ public async Task ThreadPoolStarvation_BlockingHandlers_AllInvocationsDequeued() } } + [Theory] + [InlineData(true, 1)] // Multi concurrency + JSON log format => emitted exactly once. + [InlineData(false, 0)] // Multi concurrency without JSON log format => not emitted. + public async Task WorkerPoolInitializingLog_EmissionGatedByJsonLogFormat(bool jsonLogFormat, int expectedEmissions) + { + TestEnvironmentVariables environmentVariables = new TestEnvironmentVariables(); + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, "2"); + if (jsonLogFormat) + { + environmentVariables.SetEnvironmentVariable( + Amazon.Lambda.RuntimeSupport.Bootstrap.Constants.NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE, "Json"); + } + + var capturingLogger = new CapturingConsoleLoggerWriter(); + var testRuntimeApiClient = new TestMultiConcurrencyRuntimeApiClient(environmentVariables, + new TestMultiConcurrencyRuntimeApiClient.InvocationEvent + { + Headers = CreateDefaultHeaders("request1", "trace1"), + FunctionInput = CreateFunctionInput(new SleepTimeEvent(0, 0)) + }) + { + ConsoleLogger = capturingLogger + }; + + var handler = HandlerWrapper.GetHandlerWrapper((SleepTimeEvent sleepTime, ILambdaContext context) => { }, _serializer).Handler; + + var lambdaBootstrap = new LambdaBootstrap( + httpClient: null, + handler: handler, + initializer: null, + ownsHttpClient: true, + environmentVariables: environmentVariables); + lambdaBootstrap.Client = testRuntimeApiClient; + + try + { + CancellationTokenSource cts = new CancellationTokenSource(); + cts.CancelAfter(TimeSpan.FromSeconds(3)); + await lambdaBootstrap.RunAsync(cts.Token); + } + catch (OperationCanceledException) + { + // Expected when the cancellation token is triggered. + } + + var workerPoolWrites = capturingLogger.Writes + .Where(w => w.Args != null && w.Args.Length > 0 && Equals(w.Args[0], Helpers.Utils.WorkerPoolInitializingEvent)) + .ToList(); + + Assert.Equal(expectedEmissions, workerPoolWrites.Count); + + if (expectedEmissions > 0) + { + var write = workerPoolWrites[0]; + Assert.Equal(LogLevelLoggerWriter.LogLevel.Debug.ToString(), write.Level); + Assert.Equal(Helpers.Utils.WorkerPoolInitializingLogTemplate, write.Message); + // Worker count defaults to the max concurrency (2) and max concurrency is 2. + Assert.Equal(2, write.Args[1]); + Assert.Equal(2, write.Args[2]); + } + } + private Dictionary> CreateDefaultHeaders(string requestId, string traceId) { return new Dictionary> diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs index 6bd724de5..936a8cb7c 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/LogMessageFormatterTests.cs @@ -80,6 +80,32 @@ public void FormatJsonWithNoMessageProperties() Assert.Equal("Simple Log Message", doc.RootElement.GetProperty("message").GetString()); } + [Fact] + public void FormatWorkerPoolInitializingEvent() + { + var timestamp = DateTime.UtcNow; + var formattedTimestamp = timestamp.ToString("yyyy-MM-ddTHH:mm:ss.fffZ"); + + var formatter = new JsonLogMessageFormatter(); + var state = new MessageState() + { + Level = Helpers.LogLevelLoggerWriter.LogLevel.Debug, + MessageTemplate = Utils.WorkerPoolInitializingLogTemplate, + // Use distinct worker count and max concurrency values to confirm they are reported as separate fields. + MessageArguments = new object[] { Utils.WorkerPoolInitializingEvent, 3, 10 }, + TimeStamp = timestamp + }; + + var json = formatter.FormatMessage(state); + var doc = JsonDocument.Parse(json); + + Assert.Equal(formattedTimestamp, doc.RootElement.GetProperty("timestamp").GetString()); + Assert.Equal("Debug", doc.RootElement.GetProperty("level").GetString()); + Assert.Equal("runtime_worker_pool_initializing", doc.RootElement.GetProperty("event").GetString()); + Assert.Equal(3, doc.RootElement.GetProperty("workerCount").GetInt32()); + Assert.Equal(10, doc.RootElement.GetProperty("executionEnvironmentMaxConcurrency").GetInt32()); + } + [Fact] public void FormatTenantId() { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/CapturingConsoleLoggerWriter.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/CapturingConsoleLoggerWriter.cs new file mode 100644 index 000000000..dd95320d5 --- /dev/null +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/CapturingConsoleLoggerWriter.cs @@ -0,0 +1,26 @@ +// Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +// SPDX-License-Identifier: Apache-2.0 + +using System; +using System.Collections.Generic; +using Amazon.Lambda.RuntimeSupport.Helpers; + +namespace Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers +{ + /// + /// Capturing implementation of used by tests to assert what was logged + /// without touching the real console writers. + /// + internal class CapturingConsoleLoggerWriter : IConsoleLoggerWriter + { + public List<(string Level, string Message, object[] Args)> Writes { get; } = new(); + + public void SetRuntimeHeaders(IRuntimeApiHeaders runtimeApiHeaders) { } + + public void FormattedWriteLine(string message) => Writes.Add((null, message, null)); + + public void FormattedWriteLine(string level, string message, params object[] args) => Writes.Add((level, message, args)); + + public void FormattedWriteLine(string level, Exception exception, string message, params object[] args) => Writes.Add((level, message, args)); + } +} diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs index 4d7f4493c..eb95afaa3 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/TestHelpers/TestMultiConcurrencyRuntimeApiClient.cs @@ -32,7 +32,7 @@ public TestMultiConcurrencyRuntimeApiClient(IEnvironmentVariables environmentVar ConsoleLogger = new LogLevelLoggerWriter(environmentVariables); } - public IConsoleLoggerWriter ConsoleLogger { get; } + public IConsoleLoggerWriter ConsoleLogger { get; set; } public class InvocationEvent { diff --git a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs index a41e809ac..627d043ec 100644 --- a/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs +++ b/Libraries/test/Amazon.Lambda.RuntimeSupport.Tests/Amazon.Lambda.RuntimeSupport.UnitTests/UtilsTest.cs @@ -5,12 +5,76 @@ using Amazon.Lambda.RuntimeSupport.Helpers; using Xunit; using Amazon.Lambda.RuntimeSupport.Bootstrap; +using Amazon.Lambda.RuntimeSupport.UnitTests.TestHelpers; namespace Amazon.Lambda.RuntimeSupport.UnitTests; public class UtilsTest { + [Theory] + // .NET runtime specific variable takes precedence. + [InlineData("Json", null, true)] + [InlineData("json", null, true)] + [InlineData("Text", null, false)] + // Falls back to the Lambda platform variable when the .NET one is not set. + [InlineData(null, "Json", true)] + [InlineData(null, "Text", false)] + // The .NET variable wins over the platform variable. + [InlineData("Text", "Json", false)] + [InlineData("Json", "Text", true)] + [InlineData(null, null, false)] + public void IsJsonLogFormat(string ricLogFormat, string lambdaLogFormat, bool expected) + { + var envVars = new TestEnvironmentVariables(); + if (ricLogFormat != null) + envVars.SetEnvironmentVariable(Constants.NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE, ricLogFormat); + if (lambdaLogFormat != null) + envVars.SetEnvironmentVariable(Constants.LAMBDA_LOG_FORMAT_ENVIRONMENT_VARIABLE, lambdaLogFormat); + + Assert.Equal(expected, Utils.IsJsonLogFormat(envVars)); + } + + [Fact] + public void EmitWorkerPoolInitializingLog_WhenMultiConcurrencyAndJson_EmitsOnce() + { + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, "10"); + envVars.SetEnvironmentVariable(Constants.NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE, "Json"); + var logger = new CapturingConsoleLoggerWriter(); + + // Use a worker count that differs from the max concurrency to confirm the two values are reported distinctly. + Utils.EmitWorkerPoolInitializingLog(logger, envVars, workerCount: 3, maxConcurrency: 10); + + var write = Assert.Single(logger.Writes); + Assert.Equal(LogLevelLoggerWriter.LogLevel.Debug.ToString(), write.Level); + Assert.Equal(Utils.WorkerPoolInitializingLogTemplate, write.Message); + Assert.Equal(new object[] { Utils.WorkerPoolInitializingEvent, 3, 10 }, write.Args); + } + + [Fact] + public void EmitWorkerPoolInitializingLog_WhenMultiConcurrencyButNotJson_DoesNotEmit() + { + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable(Constants.ENVIRONMENT_VARIABLE_AWS_LAMBDA_MAX_CONCURRENCY, "10"); + var logger = new CapturingConsoleLoggerWriter(); + + Utils.EmitWorkerPoolInitializingLog(logger, envVars, workerCount: 10, maxConcurrency: 10); + + Assert.Empty(logger.Writes); + } + + [Fact] + public void EmitWorkerPoolInitializingLog_WhenNotMultiConcurrency_DoesNotEmit() + { + var envVars = new TestEnvironmentVariables(); + envVars.SetEnvironmentVariable(Constants.NET_RIC_LOG_FORMAT_ENVIRONMENT_VARIABLE, "Json"); + var logger = new CapturingConsoleLoggerWriter(); + + Utils.EmitWorkerPoolInitializingLog(logger, envVars, workerCount: 1, maxConcurrency: 0); + + Assert.Empty(logger.Writes); + } [Theory] [InlineData("5", true)] [InlineData("", false)]