Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 79 additions & 22 deletions libraries/src/AWS.Lambda.Powertools.Common/Core/ConsoleWrapper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ public class ConsoleWrapper : IConsoleWrapper
private static bool _override;
private static TextWriter _testOutputStream;
private static bool _inTestMode = false;
private static StreamWriter _stdoutWriter;
private static StreamWriter _stderrWriter;
private static readonly object _lock = new object();

/// <inheritdoc />
public void WriteLine(string message)
Expand Down Expand Up @@ -47,12 +50,7 @@ public void Error(string message)
}
else
{
if (!_override)
{
var errorOutput = new StreamWriter(Console.OpenStandardError());
errorOutput.AutoFlush = true;
Console.SetError(errorOutput);
}
EnsureStderrOutput();
Console.Error.WriteLine(message);
}
}
Expand All @@ -78,6 +76,37 @@ private static void EnsureConsoleOutput()
}
}

private static void EnsureStderrOutput()
{
EnsureStderrOutput(() => Console.OpenStandardError());
}

internal static void EnsureStderrOutput(Func<Stream> standardErrorOpener)
{
if (_inTestMode) return;

var isLambda = !string.IsNullOrEmpty(Environment.GetEnvironmentVariable("AWS_LAMBDA_FUNCTION_NAME"));
if (!isLambda) return;

lock (_lock)
{
if (_stderrWriter != null) return;

try
{
_stderrWriter = new StreamWriter(standardErrorOpener())
{
AutoFlush = true
};
Console.SetError(_stderrWriter);
}
catch (Exception)
{
// Degraded functionality is better than crash
}
}
}
Comment thread
hjgraca marked this conversation as resolved.

private static bool ShouldOverrideConsole()
{
// Don't override if we're in test mode
Expand All @@ -96,13 +125,27 @@ internal static bool HasLambdaReInterceptedConsole()

internal static bool HasLambdaReInterceptedConsole(Func<TextWriter> consoleOutAccessor)
{
// Lambda might re-intercept console between init and handler execution
// Lambda might re-intercept console between init and handler execution.
// We need to detect when Lambda replaces our writer with its own,
// but NOT trigger on the SyncTextWriter wrapper that Console.SetOut
// always applies around our StreamWriter — that's still ours.
try
{
var currentOut = consoleOutAccessor();
// Check if current output stream looks like it might be Lambda's wrapper
var typeName = currentOut.GetType().FullName ?? "";
return typeName.Contains("Lambda") || typeName == "System.IO.TextWriter+SyncTextWriter";

// If it explicitly contains "Lambda", Lambda has re-intercepted
if (typeName.Contains("Lambda"))
return true;

// If we have a cached writer, check if Console.Out still wraps it.
// Console.SetOut wraps in SyncTextWriter, so seeing SyncTextWriter
// does NOT mean Lambda re-intercepted — it's our own writer wrapped.
// Only if _stdoutWriter is null (never set) do we need to override.
lock (_lock)
{
return _stdoutWriter == null;
}
}
catch
{
Expand All @@ -117,20 +160,32 @@ internal static void OverrideLambdaLogger()

internal static void OverrideLambdaLogger(Func<Stream> standardOutputOpener)
{
try
lock (_lock)
{
// Force override of LambdaLogger
var standardOutput = new StreamWriter(standardOutputOpener())
try
{
AutoFlush = true
};
Console.SetOut(standardOutput);
_override = true;
}
catch (Exception)
{
// Log the failure but don't throw - degraded functionality is better than crash
_override = false;
// Reuse existing writer if we already have one — avoids FD leak
if (_stdoutWriter != null)
{
// Re-set Console.Out in case Lambda replaced it
Console.SetOut(_stdoutWriter);
_override = true;
return;
}

// First time: create a single long-lived writer for stdout
_stdoutWriter = new StreamWriter(standardOutputOpener())
{
AutoFlush = true
};
Console.SetOut(_stdoutWriter);
_override = true;
}
catch (Exception)
{
// Log the failure but don't throw - degraded functionality is better than crash
_override = false;
}
}
}

Expand All @@ -147,6 +202,8 @@ public static void ResetForTest()
_override = false;
_inTestMode = false;
_testOutputStream = null;
_stdoutWriter = null;
_stderrWriter = null;
}

/// <summary>
Expand All @@ -157,4 +214,4 @@ public static void ClearOutputResetFlag()
// This method is kept for backward compatibility but no longer needed
// since we removed the _outputResetPerformed flag
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ internal class PowertoolsLoggingSerializer
{
private JsonSerializerOptions _currentOptions;
private LoggerOutputCase _currentOutputCase;
private JsonSerializerOptions _jsonOptions;
private volatile JsonSerializerOptions _jsonOptions;
private readonly object _lock = new();

private readonly ConcurrentBag<JsonSerializerContext> _additionalContexts = new();
Expand Down Expand Up @@ -59,11 +59,9 @@ internal void ConfigureNamingPolicy(LoggerOutputCase loggerOutputCase)
{
_currentOutputCase = loggerOutputCase;

// Only rebuild options if they already exist
if (_jsonOptions != null)
{
SetOutputCase();
}
// Force a full rebuild on next access instead of mutating existing options,
// because JsonSerializerOptions becomes read-only after first serialization.
_jsonOptions = null;
}
}
}
Expand Down Expand Up @@ -175,27 +173,29 @@ private void BuildJsonSerializerOptions(JsonSerializerOptions options = null)
{
lock (_lock)
{
// Create a completely new options instance regardless
_jsonOptions = new JsonSerializerOptions();
// Build into a local variable so _jsonOptions is never visible in a partially-configured state.
// Another thread doing a lock-free read in GetSerializerOptions() could see a non-null _jsonOptions,
// use it for serialization (making it read-only), and then subsequent mutations here would throw.
var newOptions = new JsonSerializerOptions();

// Copy any properties from the original options if provided
if (options != null)
{
// Copy standard properties
_jsonOptions.DefaultIgnoreCondition = options.DefaultIgnoreCondition;
_jsonOptions.PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive;
_jsonOptions.PropertyNamingPolicy = options.PropertyNamingPolicy;
_jsonOptions.DictionaryKeyPolicy = options.DictionaryKeyPolicy;
_jsonOptions.WriteIndented = options.WriteIndented;
_jsonOptions.ReferenceHandler = options.ReferenceHandler;
_jsonOptions.MaxDepth = options.MaxDepth;
_jsonOptions.IgnoreReadOnlyFields = options.IgnoreReadOnlyFields;
_jsonOptions.IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties;
_jsonOptions.IncludeFields = options.IncludeFields;
_jsonOptions.NumberHandling = options.NumberHandling;
_jsonOptions.ReadCommentHandling = options.ReadCommentHandling;
_jsonOptions.UnknownTypeHandling = options.UnknownTypeHandling;
_jsonOptions.AllowTrailingCommas = options.AllowTrailingCommas;
newOptions.DefaultIgnoreCondition = options.DefaultIgnoreCondition;
newOptions.PropertyNameCaseInsensitive = options.PropertyNameCaseInsensitive;
newOptions.PropertyNamingPolicy = options.PropertyNamingPolicy;
newOptions.DictionaryKeyPolicy = options.DictionaryKeyPolicy;
newOptions.WriteIndented = options.WriteIndented;
newOptions.ReferenceHandler = options.ReferenceHandler;
newOptions.MaxDepth = options.MaxDepth;
newOptions.IgnoreReadOnlyFields = options.IgnoreReadOnlyFields;
newOptions.IgnoreReadOnlyProperties = options.IgnoreReadOnlyProperties;
newOptions.IncludeFields = options.IncludeFields;
newOptions.NumberHandling = options.NumberHandling;
newOptions.ReadCommentHandling = options.ReadCommentHandling;
newOptions.UnknownTypeHandling = options.UnknownTypeHandling;
newOptions.AllowTrailingCommas = options.AllowTrailingCommas;

// Handle type resolver extraction without setting it yet
if (options.TypeInfoResolver != null)
Expand All @@ -211,49 +211,52 @@ private void BuildJsonSerializerOptions(JsonSerializerOptions options = null)
}

// Set output case and other properties
SetOutputCase();
AddConverters();
_jsonOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
_jsonOptions.PropertyNameCaseInsensitive = true;
SetOutputCase(newOptions);
AddConverters(newOptions);
newOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping;
newOptions.PropertyNameCaseInsensitive = true;

// Set TypeInfoResolver last, as this makes options read-only
if (!RuntimeFeatureWrapper.IsDynamicCodeSupported)
{
_jsonOptions.TypeInfoResolver = GetCompositeResolver();
newOptions.TypeInfoResolver = GetCompositeResolver();
}

// Publish fully-configured options in a single atomic assignment
_jsonOptions = newOptions;
}
}

internal void SetOutputCase()
internal void SetOutputCase(JsonSerializerOptions target)
{
switch (_currentOutputCase)
{
case LoggerOutputCase.CamelCase:
_jsonOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
_jsonOptions.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
target.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
target.DictionaryKeyPolicy = JsonNamingPolicy.CamelCase;
break;
case LoggerOutputCase.PascalCase:
_jsonOptions.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance;
_jsonOptions.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance;
target.PropertyNamingPolicy = PascalCaseNamingPolicy.Instance;
target.DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance;
break;
default: // Snake case
// If is default (Not Set) and JsonOptions provided with DictionaryKeyPolicy or PropertyNamingPolicy, use it
_jsonOptions.DictionaryKeyPolicy ??= JsonNamingPolicy.SnakeCaseLower;
_jsonOptions.PropertyNamingPolicy ??= JsonNamingPolicy.SnakeCaseLower;
target.DictionaryKeyPolicy ??= JsonNamingPolicy.SnakeCaseLower;
target.PropertyNamingPolicy ??= JsonNamingPolicy.SnakeCaseLower;
break;
}
}

private void AddConverters()
private static void AddConverters(JsonSerializerOptions target)
{
_jsonOptions.Converters.Add(new ByteArrayConverter());
_jsonOptions.Converters.Add(new ExceptionConverter());
_jsonOptions.Converters.Add(new MemoryStreamConverter());
_jsonOptions.Converters.Add(new ConstantClassConverter());
_jsonOptions.Converters.Add(new DateOnlyConverter());
_jsonOptions.Converters.Add(new TimeOnlyConverter());

_jsonOptions.Converters.Add(new LogLevelJsonConverter());
target.Converters.Add(new ByteArrayConverter());
target.Converters.Add(new ExceptionConverter());
target.Converters.Add(new MemoryStreamConverter());
target.Converters.Add(new ConstantClassConverter());
target.Converters.Add(new DateOnlyConverter());
target.Converters.Add(new TimeOnlyConverter());

target.Converters.Add(new LogLevelJsonConverter());
}

internal void SetOptions(JsonSerializerOptions options)
Expand Down
Loading
Loading