Skip to content

Commit d338dc8

Browse files
authored
Merge pull request #980 from dcabib/fix/log-sampling-random-generation-951
fix(logging): Fix sampling when log level is above Debug
2 parents cbf75a4 + 7cdd0a7 commit d338dc8

File tree

13 files changed

+1111
-56
lines changed

13 files changed

+1111
-56
lines changed

docs/core/logging.md

Lines changed: 33 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -778,12 +778,16 @@ custom keys can be persisted across invocations. If you want all custom keys to
778778

779779
## Sampling debug logs
780780

781-
You can dynamically set a percentage of your logs to **DEBUG** level via env var `POWERTOOLS_LOGGER_SAMPLE_RATE` or
782-
via `SamplingRate` parameter on attribute.
781+
Use sampling when you want to dynamically change your log level to **DEBUG** based on a **percentage of the Lambda function invocations**.
783782

784-
!!! info
785-
Configuration on environment variable is given precedence over sampling rate configuration on attribute, provided it's
786-
in valid value range.
783+
You can use values ranging from `0.0` to `1` (100%) when setting `POWERTOOLS_LOGGER_SAMPLE_RATE` env var, or `SamplingRate` parameter in Logger.
784+
785+
???+ tip "Tip: When is this useful?"
786+
Log sampling allows you to capture debug information for a fraction of your requests, helping you diagnose rare or intermittent issues without increasing the overall verbosity of your logs.
787+
788+
Example: Imagine an e-commerce checkout process where you want to understand rare payment gateway errors. With 10% sampling, you'll log detailed information for a small subset of transactions, making troubleshooting easier without generating excessive logs.
789+
790+
The sampling decision happens automatically with each invocation when using `Logger` decorator. When not using the decorator, you're in charge of refreshing it via `RefreshSampleRateCalculation` method. Skipping both may lead to unexpected sampling results.
787791

788792
=== "Sampling via attribute parameter"
789793

@@ -802,6 +806,30 @@ via `SamplingRate` parameter on attribute.
802806
}
803807
```
804808

809+
=== "Sampling Logger.Configure"
810+
811+
```c# hl_lines="5-10 16"
812+
public class Function
813+
{
814+
public Function()
815+
{
816+
Logger.Configure(options =>
817+
{
818+
options.MinimumLogLevel = LogLevel.Information;
819+
options.LoggerOutputCase = LoggerOutputCase.CamelCase;
820+
options.SamplingRate = 0.1; // 10% sampling
821+
});
822+
}
823+
824+
public async Task<APIGatewayProxyResponse> FunctionHandler
825+
(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
826+
{
827+
Logger.RefreshSampleRateCalculation();
828+
...
829+
}
830+
}
831+
```
832+
805833
=== "Sampling via environment variable"
806834

807835
```yaml hl_lines="8"

libraries/src/AWS.Lambda.Powertools.Logging/Internal/Helpers/LoggerFactoryHelper.cs

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ internal static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigu
2121
config.Service = configuration.Service;
2222
config.TimestampFormat = configuration.TimestampFormat;
2323
config.MinimumLogLevel = configuration.MinimumLogLevel;
24+
config.InitialLogLevel = configuration.InitialLogLevel;
2425
config.SamplingRate = configuration.SamplingRate;
2526
config.LoggerOutputCase = configuration.LoggerOutputCase;
2627
config.LogLevelKey = configuration.LogLevelKey;
@@ -32,8 +33,14 @@ internal static ILoggerFactory CreateAndConfigureFactory(PowertoolsLoggerConfigu
3233
config.LogEvent = configuration.LogEvent;
3334
});
3435

35-
// Use current filter level or level from config
36-
if (configuration.MinimumLogLevel != LogLevel.None)
36+
// When sampling is enabled, set the factory minimum level to Debug
37+
// so that all logs can reach our PowertoolsLogger for dynamic filtering
38+
if (configuration.SamplingRate > 0)
39+
{
40+
builder.AddFilter(null, LogLevel.Debug);
41+
builder.SetMinimumLevel(LogLevel.Debug);
42+
}
43+
else if (configuration.MinimumLogLevel != LogLevel.None)
3744
{
3845
builder.AddFilter(null, configuration.MinimumLogLevel);
3946
builder.SetMinimumLevel(configuration.MinimumLogLevel);

libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLogger.cs

Lines changed: 33 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,18 @@ internal void EndScope()
8080
public bool IsEnabled(LogLevel logLevel)
8181
{
8282
var config = _currentConfig();
83+
return IsEnabledForConfig(logLevel, config);
84+
}
8385

86+
/// <summary>
87+
/// Determines whether the specified log level is enabled for a specific configuration.
88+
/// </summary>
89+
/// <param name="logLevel">The log level.</param>
90+
/// <param name="config">The configuration to check against.</param>
91+
/// <returns>bool.</returns>
92+
[MethodImpl(MethodImplOptions.AggressiveInlining)]
93+
private bool IsEnabledForConfig(LogLevel logLevel, PowertoolsLoggerConfiguration config)
94+
{
8495
//if Buffering is enabled and the log level is below the buffer threshold, skip logging only if bellow error
8596
if (logLevel <= config.LogBuffering?.BufferAtLogLevel
8697
&& config.LogBuffering?.BufferAtLogLevel != LogLevel.Error
@@ -116,19 +127,38 @@ public bool IsEnabled(LogLevel logLevel)
116127
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception exception,
117128
Func<TState, Exception, string> formatter)
118129
{
119-
if (!IsEnabled(logLevel))
130+
var config = _currentConfig();
131+
if (config.SamplingRate > 0)
132+
{
133+
var samplingActivated = config.RefreshSampleRateCalculation(out double samplerValue);
134+
if (samplingActivated)
135+
{
136+
config.LogOutput.WriteLine($"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {config.SamplingRate}, Sampler Value: {samplerValue}.");
137+
}
138+
}
139+
140+
// Use the same config reference for IsEnabled check to ensure we see the updated MinimumLogLevel
141+
if (!IsEnabledForConfig(logLevel, config))
120142
{
121143
return;
122144
}
123145

124-
_currentConfig().LogOutput.WriteLine(LogEntryString(logLevel, state, exception, formatter));
146+
config.LogOutput.WriteLine(LogEntryString(logLevel, state, exception, formatter));
125147
}
126148

127149
internal void LogLine(string message)
128150
{
129151
_currentConfig().LogOutput.WriteLine(message);
130152
}
131153

154+
private void LogDebug(string message)
155+
{
156+
if (IsEnabled(LogLevel.Debug))
157+
{
158+
Log(LogLevel.Debug, new EventId(), message, null, (msg, ex) => msg);
159+
}
160+
}
161+
132162
internal string LogEntryString<TState>(LogLevel logLevel, TState state, Exception exception,
133163
Func<TState, Exception, string> formatter)
134164
{
@@ -662,4 +692,4 @@ private string ExtractParameterName(string key)
662692
? nameWithPossibleFormat.Substring(0, colonIndex)
663693
: nameWithPossibleFormat;
664694
}
665-
}
695+
}

libraries/src/AWS.Lambda.Powertools.Logging/Internal/PowertoolsLoggerProvider.cs

Lines changed: 29 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,10 @@ public PowertoolsLoggerProvider(
2424
{
2525
_powertoolsConfigurations = powertoolsConfigurations;
2626
_currentConfig = config;
27-
27+
2828
// Set execution environment
2929
_powertoolsConfigurations.SetExecutionEnvironment(this);
30-
30+
3131
// Apply environment configurations if available
3232
ConfigureFromEnvironment();
3333
}
@@ -58,47 +58,41 @@ public void ConfigureFromEnvironment()
5858
_currentConfig.LoggerOutputCase = loggerOutputCase;
5959
}
6060

61-
// Set log level from environment ONLY if not explicitly set
6261
var minLogLevel = lambdaLogLevelEnabled ? lambdaLogLevel : logLevel;
63-
_currentConfig.MinimumLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel;
62+
var effectiveLogLevel = minLogLevel != LogLevel.None ? minLogLevel : LoggingConstants.DefaultLogLevel;
63+
64+
// Only set InitialLogLevel if it hasn't been explicitly configured
65+
if (_currentConfig.InitialLogLevel == LogLevel.Information)
66+
{
67+
_currentConfig.InitialLogLevel = effectiveLogLevel;
68+
}
69+
70+
_currentConfig.MinimumLogLevel = effectiveLogLevel;
71+
6472
_currentConfig.XRayTraceId = _powertoolsConfigurations.XRayTraceId;
6573
_currentConfig.LogEvent = _powertoolsConfigurations.LoggerLogEvent;
66-
74+
6775
// Configure the log level key based on output case
6876
_currentConfig.LogLevelKey = _powertoolsConfigurations.LambdaLogLevelEnabled() &&
6977
_currentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase
7078
? "LogLevel"
7179
: LoggingConstants.KeyLogLevel;
72-
80+
7381
ProcessSamplingRate(_currentConfig, _powertoolsConfigurations);
7482
_environmentConfigured = true;
7583
}
76-
84+
7785
/// <summary>
7886
/// Process sampling rate configuration
7987
/// </summary>
8088
private void ProcessSamplingRate(PowertoolsLoggerConfiguration config, IPowertoolsConfigurations configurations)
8189
{
82-
var samplingRate = config.SamplingRate > 0
83-
? config.SamplingRate
90+
var samplingRate = config.SamplingRate > 0
91+
? config.SamplingRate
8492
: configurations.LoggerSampleRate;
85-
93+
8694
samplingRate = ValidateSamplingRate(samplingRate, config);
8795
config.SamplingRate = samplingRate;
88-
89-
// Only notify if sampling is configured
90-
if (samplingRate > 0)
91-
{
92-
double sample = config.GetRandom();
93-
94-
// Instead of changing log level, just indicate sampling status
95-
if (sample <= samplingRate)
96-
{
97-
config.LogOutput.WriteLine(
98-
$"Changed log level to DEBUG based on Sampling configuration. Sampling Rate: {samplingRate}, Sampler Value: {sample}.");
99-
config.MinimumLogLevel = LogLevel.Debug;
100-
}
101-
}
10296
}
10397

10498
/// <summary>
@@ -129,18 +123,27 @@ public virtual ILogger CreateLogger(string categoryName)
129123
}
130124

131125
internal PowertoolsLoggerConfiguration GetCurrentConfig() => _currentConfig;
132-
126+
133127
public void UpdateConfiguration(PowertoolsLoggerConfiguration config)
134128
{
135129
_currentConfig = config;
136-
130+
137131
// Apply environment configurations if available
138132
if (_powertoolsConfigurations != null && !_environmentConfigured)
139133
{
140134
ConfigureFromEnvironment();
141135
}
142136
}
143137

138+
/// <summary>
139+
/// Refresh the sampling calculation and update the minimum log level if needed
140+
/// </summary>
141+
/// <returns>True if debug sampling was enabled, false otherwise</returns>
142+
internal bool RefreshSampleRateCalculation()
143+
{
144+
return _currentConfig.RefreshSampleRateCalculation();
145+
}
146+
144147
public virtual void Dispose()
145148
{
146149
_loggers.Clear();
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
namespace AWS.Lambda.Powertools.Logging;
2+
3+
public static partial class Logger
4+
{
5+
/// <summary>
6+
/// Refresh the sampling calculation and update the minimum log level if needed
7+
/// </summary>
8+
/// <returns>True if debug sampling was enabled, false otherwise</returns>
9+
public static bool RefreshSampleRateCalculation()
10+
{
11+
return _config.RefreshSampleRateCalculation();
12+
}
13+
}

libraries/src/AWS.Lambda.Powertools.Logging/Logger.cs

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ namespace AWS.Lambda.Powertools.Logging;
1212
/// </summary>
1313
public static partial class Logger
1414
{
15+
private static PowertoolsLoggerConfiguration _config;
1516
private static ILogger _loggerInstance;
1617
private static readonly object Lock = new object();
1718

@@ -58,9 +59,9 @@ public static void Configure(Action<PowertoolsLoggerConfiguration> configure)
5859
{
5960
lock (Lock)
6061
{
61-
var config = new PowertoolsLoggerConfiguration();
62-
configure(config);
63-
_loggerInstance = LoggerFactoryHelper.CreateAndConfigureFactory(config).CreatePowertoolsLogger();
62+
_config = new PowertoolsLoggerConfiguration();
63+
configure(_config);
64+
_loggerInstance = LoggerFactoryHelper.CreateAndConfigureFactory(_config).CreatePowertoolsLogger();
6465
}
6566
}
6667

libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerConfiguration.cs

Lines changed: 67 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -313,22 +313,84 @@ private PowertoolsLoggingSerializer InitializeSerializer()
313313
internal string XRayTraceId { get; set; }
314314
internal bool LogEvent { get; set; }
315315

316-
internal double Random { get; set; } = GetSafeRandom();
316+
internal int SamplingRefreshCount { get; set; } = 0;
317+
internal LogLevel InitialLogLevel { get; set; } = LogLevel.Information;
317318

318319
/// <summary>
319320
/// Gets random number
320321
/// </summary>
321322
/// <returns>System.Double.</returns>
322323
internal virtual double GetRandom()
323324
{
324-
return Random;
325+
return GetSafeRandom();
325326
}
326-
327+
328+
/// <summary>
329+
/// Refresh the sampling calculation and update the minimum log level if needed
330+
/// </summary>
331+
/// <returns>True if debug sampling was enabled, false otherwise</returns>
332+
internal bool RefreshSampleRateCalculation()
333+
{
334+
return RefreshSampleRateCalculation(out _);
335+
}
336+
337+
/// <summary>
338+
/// Refresh the sampling calculation and update the minimum log level if needed
339+
/// </summary>
340+
/// <param name="samplerValue"></param>
341+
/// <returns>True if debug sampling was enabled, false otherwise</returns>
342+
internal bool RefreshSampleRateCalculation(out double samplerValue)
343+
{
344+
samplerValue = 0.0;
345+
346+
if (SamplingRate <= 0)
347+
return false;
348+
349+
// Increment counter at the beginning for proper cold start protection
350+
SamplingRefreshCount++;
351+
352+
// Skip first call for cold start protection
353+
if (SamplingRefreshCount == 1)
354+
{
355+
return false;
356+
}
357+
358+
var shouldEnableDebugSampling = ShouldEnableDebugSampling(out samplerValue);
359+
360+
if (shouldEnableDebugSampling && MinimumLogLevel > LogLevel.Debug)
361+
{
362+
MinimumLogLevel = LogLevel.Debug;
363+
return true;
364+
}
365+
else if (!shouldEnableDebugSampling)
366+
{
367+
MinimumLogLevel = InitialLogLevel;
368+
}
369+
370+
return shouldEnableDebugSampling;
371+
}
372+
373+
374+
internal bool ShouldEnableDebugSampling()
375+
{
376+
return ShouldEnableDebugSampling(out _);
377+
}
378+
379+
internal bool ShouldEnableDebugSampling(out double samplerValue)
380+
{
381+
samplerValue = 0.0;
382+
if (SamplingRate <= 0) return false;
383+
384+
samplerValue = GetRandom();
385+
return samplerValue <= SamplingRate;
386+
}
387+
327388
internal static double GetSafeRandom()
328389
{
329390
var randomGenerator = RandomNumberGenerator.Create();
330-
byte[] data = new byte[16];
391+
byte[] data = new byte[4];
331392
randomGenerator.GetBytes(data);
332-
return BitConverter.ToDouble(data);
393+
uint randomUInt = BitConverter.ToUInt32(data, 0);
394+
return (double)randomUInt / uint.MaxValue;
333395
}
334396
}

libraries/src/AWS.Lambda.Powertools.Logging/PowertoolsLoggerExtensions.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -257,4 +257,13 @@ public static void ClearBuffer(this ILogger logger)
257257
// Direct call to the buffer manager to avoid any recursion
258258
LogBufferManager.ClearCurrentBuffer();
259259
}
260+
261+
/// <summary>
262+
/// Refresh the sampling calculation and update the minimum log level if needed
263+
/// </summary>
264+
/// <returns></returns>
265+
public static bool RefreshSampleRateCalculation(this ILogger logger)
266+
{
267+
return Logger.RefreshSampleRateCalculation();
268+
}
260269
}

0 commit comments

Comments
 (0)