Skip to content
33 changes: 33 additions & 0 deletions docs/core/logging.md
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,39 @@ You can remove any additional key from entry using `Logger.RemoveKeys()`.
}
```

## Extra Keys

Extra keys argument is available for all log levels' methods, as implemented in the standard logging library - e.g. Logger.Information, Logger.Warning.

It accepts any dictionary, and all keyword arguments will be added as part of the root structure of the logs for that log statement.

!!! info
Any keyword argument added using extra keys will not be persisted for subsequent messages.

=== "Function.cs"

```c# hl_lines="16"
/**
* Handler for requests to Lambda function.
*/
public class Function
{
[Logging(LogEvent = true)]
public async Task<APIGatewayProxyResponse> FunctionHandler
(APIGatewayProxyRequest apigProxyEvent, ILambdaContext context)
{
...
var extraKeys = new Dictionary<string, string>
{
{"extraKey1", "value1"}
};

Logger.LogInformation(extraKeys, "Collecting payment");
...
}
}
```

### Clearing all state

Logger is commonly initialized in the global scope. Due to [Lambda Execution Context reuse](https://docs.aws.amazon.com/lambda/latest/dg/runtimes-context.html), this means that custom keys can be persisted across invocations. If you want all custom keys to be deleted, you can use `ClearState=true` attribute on `[Logging]` attribute.
Expand Down
4 changes: 2 additions & 2 deletions examples/Logging/src/HelloWorld/Function.cs
Original file line number Diff line number Diff line change
Expand Up @@ -91,12 +91,12 @@ public async Task<APIGatewayProxyResponse> FunctionHandler(APIGatewayProxyReques

try
{
Logger.LogInformation("Calling Check IP API ");
Logger.LogInformation("Calling Check IP API");

var response = await _httpClient.GetStringAsync("https://checkip.amazonaws.com/").ConfigureAwait(false);
var ip = response.Replace("\n", "");

Logger.LogInformation("API response returned {}", ip);
Logger.LogInformation($"API response returned {ip}");

return ip;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ internal class LoggingAspectHandler : IMethodAspectHandler
/// The log level
/// </summary>
private readonly LogLevel? _logLevel;

/// <summary>
/// The logger output case
/// </summary>
private readonly LoggerOutputCase? _loggerOutputCase;

/// <summary>
/// The Powertools configurations
Expand Down Expand Up @@ -94,6 +99,7 @@ internal class LoggingAspectHandler : IMethodAspectHandler
/// </summary>
/// <param name="service">Service name</param>
/// <param name="logLevel">The log level.</param>
/// <param name="loggerOutputCase">The logger output case.</param>
/// <param name="samplingRate">The sampling rate.</param>
/// <param name="logEvent">if set to <c>true</c> [log event].</param>
/// <param name="correlationIdPath">The correlation identifier path.</param>
Expand All @@ -104,6 +110,7 @@ internal LoggingAspectHandler
(
string service,
LogLevel? logLevel,
LoggerOutputCase? loggerOutputCase,
double? samplingRate,
bool? logEvent,
string correlationIdPath,
Expand All @@ -114,6 +121,7 @@ ISystemWrapper systemWrapper
{
_service = service;
_logLevel = logLevel;
_loggerOutputCase = loggerOutputCase;
_samplingRate = samplingRate;
_logEvent = logEvent;
_clearState = clearState;
Expand All @@ -135,7 +143,8 @@ public void OnEntry(AspectEventArgs eventArgs)
{
Service = _service,
MinimumLevel = _logLevel,
SamplingRate = _samplingRate
SamplingRate = _samplingRate,
LoggerOutputCase = _loggerOutputCase
};

switch (Logger.LoggerProvider)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,11 @@ internal static class LoggingConstants
/// Constant for default log level
/// </summary>
internal const LogLevel DefaultLogLevel = LogLevel.Information;

/// <summary>
/// Constant for default log output case
/// </summary>
internal const LoggerOutputCase DefaultLoggerOutputCase = LoggerOutputCase.SnakeCase;

/// <summary>
/// Constant for key json formatter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,16 @@ internal static LogLevel GetLogLevel(this IPowertoolsConfigurations powertoolsCo

return LoggingConstants.DefaultLogLevel;
}

internal static LoggerOutputCase GetLoggerOutputCase(this IPowertoolsConfigurations powertoolsConfigurations,
LoggerOutputCase? loggerOutputCase = null)
{
if (loggerOutputCase.HasValue)
return loggerOutputCase.Value;

if (Enum.TryParse((powertoolsConfigurations.LoggerOutputCase ?? "").Trim(), true, out LoggerOutputCase result))
return result;

return LoggingConstants.DefaultLoggerOutputCase;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,8 @@ public PowertoolsLogger(
? CurrentConfig.Service
: _powertoolsConfigurations.Service;

internal PowertoolsLoggerScope CurrentScope { get; private set; }

/// <summary>
/// Begins the scope.
/// </summary>
Expand All @@ -102,7 +104,62 @@ public PowertoolsLogger(
/// <returns>System.IDisposable.</returns>
public IDisposable BeginScope<TState>(TState state)
{
return default!;
CurrentScope = new PowertoolsLoggerScope(this, GetScopeKeys(state));
return CurrentScope;
}

/// <summary>
/// Ends the scope.
/// </summary>
internal void EndScope()
{
CurrentScope = null;
}

/// <summary>
/// Extract provided scope keys
/// </summary>
/// <typeparam name="TState">The type of the t state.</typeparam>
/// <param name="state">The state.</param>
/// <returns>Key/Value pair of provided scope keys</returns>
private static Dictionary<string, object> GetScopeKeys<TState>(TState state)
{
var keys = new Dictionary<string, object>();

if (state is null)
return keys;

switch (state)
{
case IEnumerable<KeyValuePair<string, string>> pairs:
{
foreach (var (key, value) in pairs)
{
if (!string.IsNullOrWhiteSpace(key))
keys.TryAdd(key, value);
}
break;
}
case IEnumerable<KeyValuePair<string, object>> pairs:
{
foreach (var (key, value) in pairs)
{
if (!string.IsNullOrWhiteSpace(key))
keys.TryAdd(key, value);
}
break;
}
default:
{
foreach (var property in state.GetType().GetProperties())
{
keys.TryAdd(property.Name, property.GetValue(state));
}
break;
}
}

return keys;
}

/// <summary>
Expand Down Expand Up @@ -149,6 +206,16 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
message.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId);
}

// Add Extra Fields
if (CurrentScope?.ExtraKeys is not null)
{
foreach (var (key, value) in CurrentScope.ExtraKeys)
{
if (!string.IsNullOrWhiteSpace(key))
message.TryAdd(key, value);
}
}

message.TryAdd(LoggingConstants.KeyTimestamp, DateTime.UtcNow.ToString("o"));
message.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString());
message.TryAdd(LoggingConstants.KeyService, Service);
Expand All @@ -166,35 +233,28 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except
_systemWrapper.LogLine(JsonSerializer.Serialize(message, options));
}

internal JsonSerializerOptions BuildCaseSerializerOptions(){
object LogCase;
Enum.TryParse(typeof(LoggerOutputCase), _currentConfig.LogOutputCase, true, out LogCase);

if(LogCase != null){
switch (LogCase)
{
case LoggerOutputCase.CamelCase:
return new(JsonSerializerDefaults.Web){
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
};
case LoggerOutputCase.PascalCase:
return new() {
PropertyNamingPolicy = PascalCaseNamingPolicy.Instance,
DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance
};
default: // Snake case is the default
return new() {
PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
};
}
}
else {
return new() {
PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
};
private JsonSerializerOptions BuildCaseSerializerOptions()
{
switch (CurrentConfig.LoggerOutputCase)
{
case LoggerOutputCase.CamelCase:
return new(JsonSerializerDefaults.Web)
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
DictionaryKeyPolicy = JsonNamingPolicy.CamelCase
};
case LoggerOutputCase.PascalCase:
return new()
{
PropertyNamingPolicy = PascalCaseNamingPolicy.Instance,
DictionaryKeyPolicy = PascalCaseNamingPolicy.Instance
};
default: // Snake case is the default
return new()
{
PropertyNamingPolicy = SnakeCaseNamingPolicy.Instance,
DictionaryKeyPolicy = SnakeCaseNamingPolicy.Instance
};
}
}

Expand All @@ -215,14 +275,14 @@ private LoggerConfiguration GetCurrentConfig()
var currConfig = _getCurrentConfig();
var minimumLevel = _powertoolsConfigurations.GetLogLevel(currConfig?.MinimumLevel);
var samplingRate = currConfig?.SamplingRate ?? _powertoolsConfigurations.LoggerSampleRate;
var logOutputCase = _powertoolsConfigurations.LoggerOutputCase;
var loggerOutputCase = _powertoolsConfigurations.GetLoggerOutputCase(currConfig?.LoggerOutputCase);

var config = new LoggerConfiguration
{
Service = currConfig?.Service,
MinimumLevel = minimumLevel,
SamplingRate = samplingRate,
LogOutputCase = logOutputCase
LoggerOutputCase = loggerOutputCase
};

if (!samplingRate.HasValue)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;

namespace AWS.Lambda.Powertools.Logging.Internal;

/// <summary>
/// Class PowertoolsLoggerScope.
/// </summary>
internal class PowertoolsLoggerScope : IDisposable
{
/// <summary>
/// The associated logger
/// </summary>
private readonly PowertoolsLogger _logger;

/// <summary>
/// The provided extra keys
/// </summary>
internal Dictionary<string, object> ExtraKeys { get; }

/// <summary>
/// Creates a PowertoolsLoggerScope object
/// </summary>
internal PowertoolsLoggerScope(PowertoolsLogger logger, Dictionary<string, object> extraKeys)
{
_logger = logger;
ExtraKeys = extraKeys;
}

/// <summary>
/// Implements IDisposable interface
/// </summary>
public void Dispose()
{
_logger?.EndScope();
}
}
Loading