Skip to content

Commit 53bb027

Browse files
authored
Merge pull request #516 from hjgraca/lambda-log-level
feat: New log level
2 parents fe4ef8b + fcb180c commit 53bb027

File tree

9 files changed

+443
-38
lines changed

9 files changed

+443
-38
lines changed

docs/core/logging.md

+45
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,51 @@ Here is an example using the AWS SAM [Globals section](https://docs.aws.amazon.c
6969
| **POWERTOOLS_LOGGER_LOG_EVENT** | Logs incoming event | `false` |
7070
| **POWERTOOLS_LOGGER_SAMPLE_RATE** | Debug log sampling | `0` |
7171

72+
73+
### Using AWS Lambda Advanced Logging Controls (ALC)
74+
75+
With [AWS Lambda Advanced Logging Controls (ALC)](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-advanced), you can control the output format of your logs as either TEXT or JSON and specify the minimum accepted log level for your application. Regardless of the output format setting in Lambda, Powertools for AWS Lambda will always output JSON formatted logging messages.
76+
77+
When you have this feature enabled, log messages that don’t meet the configured log level are discarded by Lambda. For example, if you set the minimum log level to WARN, you will only receive WARN and ERROR messages in your AWS CloudWatch Logs, all other log levels will be discarded by Lambda.
78+
79+
!!! warning "When using AWS Lambda Advanced Logging Controls (ALC)"
80+
- When Powertools Logger output is set to `PascalCase` **`Level`** property name will be replaced by **`LogLevel`** as a property name.
81+
- ALC takes precedence over **`POWERTOOLS_LOG_LEVEL`** and when setting it in code using **`[Logging(LogLevel = )]`**
82+
83+
```mermaid
84+
sequenceDiagram
85+
title Lambda ALC allows WARN logs only
86+
participant Lambda service
87+
participant Lambda function
88+
participant Application Logger
89+
90+
Note over Lambda service: AWS_LAMBDA_LOG_LEVEL="WARN"
91+
Lambda service->>Lambda function: Invoke (event)
92+
Lambda function->>Lambda function: Calls handler
93+
Lambda function->>Application Logger: Logger.Warning("Something happened")
94+
Lambda function-->>Application Logger: Logger.Debug("Something happened")
95+
Lambda function-->>Application Logger: Logger.Information("Something happened")
96+
97+
Lambda service->>Lambda service: DROP INFO and DEBUG logs
98+
99+
Lambda service->>CloudWatch Logs: Ingest error logs
100+
```
101+
102+
Logger will automatically listen for the AWS_LAMBDA_LOG_FORMAT and AWS_LAMBDA_LOG_LEVEL environment variables, and change behaviour if they’re found to ensure as much compatibility as possible.
103+
104+
**Priority of log level settings in Powertools for AWS Lambda**
105+
106+
When the Advanced Logging Controls feature is enabled, we are unable to increase the minimum log level below the AWS_LAMBDA_LOG_LEVEL environment variable value, see [AWS Lambda service documentation](https://docs.aws.amazon.com/lambda/latest/dg/monitoring-cloudwatchlogs.html#monitoring-cloudwatchlogs-log-level) for more details.
107+
108+
We prioritise log level settings in this order:
109+
110+
1. AWS_LAMBDA_LOG_LEVEL environment variable
111+
2. Setting the log level in code using `[Logging(LogLevel = )]`
112+
3. POWERTOOLS_LOG_LEVEL environment variable
113+
114+
In the event you have set POWERTOOLS_LOG_LEVEL to a level lower than the ACL setting, Powertools for AWS Lambda will output a warning log message informing you that your messages will be discarded by Lambda.
115+
116+
72117
## Standard structured keys
73118

74119
Your logs will always include the following keys to your structured logging:

libraries/src/AWS.Lambda.Powertools.Common/Core/Constants.cs

+5
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,11 @@ internal static class Constants
5454
/// Constant for POWERTOOLS_LOG_LEVEL environment variable
5555
/// </summary>
5656
internal const string LogLevelNameEnv = "POWERTOOLS_LOG_LEVEL";
57+
58+
/// <summary>
59+
/// Constant for POWERTOOLS_LOG_LEVEL environment variable
60+
/// </summary>
61+
internal const string AWSLambdaLogLevelNameEnv = "AWS_LAMBDA_LOG_LEVEL";
5762

5863
/// <summary>
5964
/// Constant for POWERTOOLS_LOGGER_SAMPLE_RATE environment variable

libraries/src/AWS.Lambda.Powertools.Common/Core/IPowertoolsConfigurations.cs

+7-1
Original file line numberDiff line numberDiff line change
@@ -57,10 +57,16 @@ public interface IPowertoolsConfigurations
5757
string MetricsNamespace { get; }
5858

5959
/// <summary>
60-
/// Gets the log level.
60+
/// Gets the Powertools log level.
6161
/// </summary>
6262
/// <value>The log level.</value>
6363
string LogLevel { get; }
64+
65+
/// <summary>
66+
/// Gets the AWS Lambda log level.
67+
/// </summary>
68+
/// <value>The log level.</value>
69+
string AWSLambdaLogLevel { get; }
6470

6571
/// <summary>
6672
/// Gets the logger sample rate.

libraries/src/AWS.Lambda.Powertools.Common/Core/PowertoolsConfigurations.cs

+5-6
Original file line numberDiff line numberDiff line change
@@ -147,12 +147,11 @@ public bool GetEnvironmentVariableOrDefault(string variable, bool defaultValue)
147147
public string MetricsNamespace =>
148148
GetEnvironmentVariable(Constants.MetricsNamespaceEnv);
149149

150-
/// <summary>
151-
/// Gets the log level.
152-
/// </summary>
153-
/// <value>The log level.</value>
154-
public string LogLevel =>
155-
GetEnvironmentVariable(Constants.LogLevelNameEnv);
150+
/// <inheritdoc />
151+
public string LogLevel => GetEnvironmentVariable(Constants.LogLevelNameEnv);
152+
153+
/// <inheritdoc />
154+
public string AWSLambdaLogLevel => GetEnvironmentVariable(Constants.AWSLambdaLogLevelNameEnv);
156155

157156
/// <summary>
158157
/// Gets the logger sample rate.

libraries/src/AWS.Lambda.Powertools.Common/Core/SystemWrapper.cs

+18-8
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
/*
22
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3-
*
3+
*
44
* Licensed under the Apache License, Version 2.0 (the "License").
55
* You may not use this file except in compliance with the License.
66
* A copy of the License is located at
7-
*
7+
*
88
* http://aws.amazon.com/apache2.0
9-
*
9+
*
1010
* or in the "license" file accompanying this file. This file is distributed
1111
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
1212
* express or implied. See the License for the specific language governing
1313
* permissions and limitations under the License.
1414
*/
1515

1616
using System;
17+
using System.IO;
1718
using System.Text;
1819

1920
namespace AWS.Lambda.Powertools.Common;
@@ -39,6 +40,14 @@ public SystemWrapper(IPowertoolsEnvironment powertoolsEnvironment)
3940
{
4041
_powertoolsEnvironment = powertoolsEnvironment;
4142
_instance ??= this;
43+
44+
// Clear AWS SDK Console injected parameters StdOut and StdErr
45+
var standardOutput = new StreamWriter(Console.OpenStandardOutput());
46+
standardOutput.AutoFlush = true;
47+
Console.SetOut(standardOutput);
48+
var errordOutput = new StreamWriter(Console.OpenStandardError());
49+
errordOutput.AutoFlush = true;
50+
Console.SetError(errordOutput);
4251
}
4352

4453
/// <summary>
@@ -97,21 +106,21 @@ public void SetExecutionEnvironment<T>(T type)
97106
var envValue = new StringBuilder();
98107
var currentEnvValue = GetEnvironmentVariable(envName);
99108
var assemblyName = ParseAssemblyName(_powertoolsEnvironment.GetAssemblyName(type));
100-
109+
101110
// If there is an existing execution environment variable add the annotations package as a suffix.
102-
if(!string.IsNullOrEmpty(currentEnvValue))
111+
if (!string.IsNullOrEmpty(currentEnvValue))
103112
{
104113
// Avoid duplication - should not happen since the calling Instances are Singletons - defensive purposes
105114
if (currentEnvValue.Contains(assemblyName))
106115
{
107116
return;
108117
}
109-
118+
110119
envValue.Append($"{currentEnvValue} ");
111120
}
112121

113122
var assemblyVersion = _powertoolsEnvironment.GetAssemblyVersion(type);
114-
123+
115124
envValue.Append($"{assemblyName}/{assemblyVersion}");
116125

117126
SetEnvironmentVariable(envName, envValue.ToString());
@@ -127,13 +136,14 @@ private string ParseAssemblyName(string assemblyName)
127136
{
128137
try
129138
{
130-
var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal)+1);
139+
var parsedName = assemblyName.Substring(assemblyName.LastIndexOf(".", StringComparison.Ordinal) + 1);
131140
return $"{Constants.FeatureContextIdentifier}/{parsedName}";
132141
}
133142
catch
134143
{
135144
//NOOP
136145
}
146+
137147
return $"{Constants.FeatureContextIdentifier}/{assemblyName}";
138148
}
139149
}

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

+23
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System;
17+
using System.Collections.Generic;
1718
using AWS.Lambda.Powertools.Common;
1819
using Microsoft.Extensions.Logging;
1920

@@ -42,6 +43,18 @@ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsCo
4243
return LoggingConstants.DefaultLogLevel;
4344
}
4445

46+
internal static LogLevel GetLambdaLogLevel(this IPowertoolsConfigurations powertoolsConfigurations)
47+
{
48+
AwsLogLevelMapper.TryGetValue((powertoolsConfigurations.AWSLambdaLogLevel ?? "").Trim().ToUpper(), out var awsLogLevel);
49+
50+
if (Enum.TryParse(awsLogLevel, true, out LogLevel result))
51+
{
52+
return result;
53+
}
54+
55+
return LogLevel.None;
56+
}
57+
4558
internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations,
4659
LoggerOutputCase? loggerOutputCase = null)
4760
{
@@ -53,4 +66,14 @@ internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurati
5366

5467
return LoggingConstants.DefaultLoggerOutputCase;
5568
}
69+
70+
private static Dictionary<string, string> AwsLogLevelMapper = new()
71+
{
72+
{ "TRACE", "TRACE" },
73+
{ "DEBUG", "DEBUG" },
74+
{ "INFO", "INFORMATION" },
75+
{ "WARN", "WARNING" },
76+
{ "ERROR", "ERROR" },
77+
{ "FATAL", "CRITICAL" }
78+
};
5679
}

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

+34-17
Original file line numberDiff line numberDiff line change
@@ -50,17 +50,16 @@ internal sealed class PowertoolsLogger : ILogger
5050
/// The system wrapper
5151
/// </summary>
5252
private readonly ISystemWrapper _systemWrapper;
53-
54-
/// <summary>
55-
/// The current configuration
56-
/// </summary>
57-
private LoggerConfiguration _currentConfig;
5853

5954
/// <summary>
6055
/// The JsonSerializer options
6156
/// </summary>
6257
private JsonSerializerOptions _jsonSerializerOptions;
6358

59+
private LogLevel _lambdaLogLevel;
60+
private LogLevel _logLevel;
61+
private bool _lambdaLogLevelEnabled;
62+
6463
/// <summary>
6564
/// Initializes a new instance of the <see cref="PowertoolsLogger" /> class.
6665
/// </summary>
@@ -78,14 +77,17 @@ public PowertoolsLogger(
7877
powertoolsConfigurations, systemWrapper, getCurrentConfig);
7978

8079
_powertoolsConfigurations.SetExecutionEnvironment(this);
80+
CurrentConfig = GetCurrentConfig();
81+
82+
if (_lambdaLogLevelEnabled && _logLevel < _lambdaLogLevel)
83+
{
84+
var message =
85+
$"Current log level ({_logLevel}) does not match AWS Lambda Advanced Logging Controls minimum log level ({_lambdaLogLevel}). This can lead to data loss, consider adjusting them.";
86+
this.LogWarning(message);
87+
}
8188
}
8289

83-
/// <summary>
84-
/// Sets the current configuration.
85-
/// </summary>
86-
/// <value>The current configuration.</value>
87-
private LoggerConfiguration CurrentConfig =>
88-
_currentConfig ??= GetCurrentConfig();
90+
private LoggerConfiguration CurrentConfig { get; set; }
8991

9092
/// <summary>
9193
/// Sets the minimum level.
@@ -255,8 +257,15 @@ private Dictionary<string, object> GetLogEntry(LogLevel logLevel, DateTime times
255257
}
256258
}
257259

260+
var keyLogLevel = LoggingConstants.KeyLogLevel;
261+
// If ALC is enabled and PascalCase we need to convert Level to LogLevel for it to be parsed and sent to CW
262+
if (_lambdaLogLevelEnabled && CurrentConfig.LoggerOutputCase == LoggerOutputCase.PascalCase)
263+
{
264+
keyLogLevel = "LogLevel";
265+
}
266+
258267
logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o"));
259-
logEntry.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString());
268+
logEntry.TryAdd(keyLogLevel, logLevel.ToString());
260269
logEntry.TryAdd(LoggingConstants.KeyService, Service);
261270
logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name);
262271
logEntry.TryAdd(LoggingConstants.KeyMessage, message);
@@ -361,7 +370,7 @@ private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, objec
361370
/// </summary>
362371
internal void ClearConfig()
363372
{
364-
_currentConfig = null;
373+
CurrentConfig = null;
365374
}
366375

367376
/// <summary>
@@ -371,14 +380,22 @@ internal void ClearConfig()
371380
private LoggerConfiguration GetCurrentConfig()
372381
{
373382
var currConfig = _getCurrentConfig();
374-
var minimumLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel);
383+
_logLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel);
375384
var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate;
376385
var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase);
377-
386+
_lambdaLogLevel = _powertoolsConfigurations.GetLambdaLogLevel();
387+
_lambdaLogLevelEnabled = _lambdaLogLevel != LogLevel.None;
388+
389+
var minLogLevel = _logLevel;
390+
if (_lambdaLogLevelEnabled)
391+
{
392+
minLogLevel = _lambdaLogLevel;
393+
}
394+
378395
var config = new LoggerConfiguration
379396
{
380397
Service = currConfig?.Service,
381-
MinimumLevel = minimumLevel,
398+
MinimumLevel = minLogLevel,
382399
SamplingRate = samplingRate,
383400
LoggerOutputCase = loggerOutputCase
384401
};
@@ -388,7 +405,7 @@ private LoggerConfiguration GetCurrentConfig()
388405

389406
if (samplingRate.Value < 0 || samplingRate.Value > 1)
390407
{
391-
if (minimumLevel is LogLevel.Debug or LogLevel.Trace)
408+
if (minLogLevel is LogLevel.Debug or LogLevel.Trace)
392409
_systemWrapper.LogLine(
393410
$"Skipping sampling rate configuration because of invalid value. Sampling rate: {samplingRate.Value}");
394411
config.SamplingRate = null;

0 commit comments

Comments
 (0)