-
Notifications
You must be signed in to change notification settings - Fork 27
feat: Add bring your own log formatter to logger #375
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,29 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). | ||
* You may not use this file except in compliance with the License. | ||
* A copy of the License is located at | ||
* | ||
* http://aws.amazon.com/apache2.0 | ||
* | ||
* or in the "license" file accompanying this file. This file is distributed | ||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
* express or implied. See the License for the specific language governing | ||
* permissions and limitations under the License. | ||
*/ | ||
|
||
namespace AWS.Lambda.Powertools.Logging; | ||
|
||
/// <summary> | ||
/// Represents a type used to format Powertools log entries. | ||
/// </summary> | ||
public interface ILogFormatter | ||
{ | ||
/// <summary> | ||
/// Formats a log entry | ||
/// </summary> | ||
/// <param name="logEntry">The log entry.</param> | ||
/// <returns>Formatted log entry as object.</returns> | ||
object FormatLogEntry(LogEntry logEntry); | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -206,20 +206,43 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except | |
if (!IsEnabled(logLevel)) | ||
return; | ||
|
||
var message = new Dictionary<string, object>(StringComparer.Ordinal); | ||
var timestamp = DateTime.UtcNow; | ||
var message = CustomFormatter(state, exception, out var customMessage) && customMessage is not null | ||
? customMessage | ||
: formatter(state, exception); | ||
|
||
var logFormatter = Logger.GetFormatter(); | ||
var logEntry = logFormatter is null? | ||
GetLogEntry(logLevel, timestamp, message, exception) : | ||
GetFormattedLogEntry(logLevel, timestamp, message, exception, logFormatter); | ||
|
||
_systemWrapper.LogLine(JsonSerializer.Serialize(logEntry, JsonSerializerOptions)); | ||
} | ||
|
||
/// <summary> | ||
/// Gets a log entry. | ||
/// </summary> | ||
/// <param name="logLevel">Entry will be written on this level.</param> | ||
/// <param name="timestamp">Entry timestamp.</param> | ||
/// <param name="message">The message to be written. Can be also an object.</param> | ||
/// <param name="exception">The exception related to this entry.</param> | ||
private Dictionary<string, object> GetLogEntry(LogLevel logLevel, DateTime timestamp, object message, | ||
Exception exception) | ||
{ | ||
var logEntry = new Dictionary<string, object>(StringComparer.Ordinal); | ||
|
||
// Add Custom Keys | ||
foreach (var (key, value) in Logger.GetAllKeys()) | ||
message.TryAdd(key, value); | ||
logEntry.TryAdd(key, value); | ||
|
||
// Add Lambda Context Keys | ||
if (PowertoolsLambdaContext.Instance is not null) | ||
{ | ||
message.TryAdd(LoggingConstants.KeyFunctionName, PowertoolsLambdaContext.Instance.FunctionName); | ||
message.TryAdd(LoggingConstants.KeyFunctionVersion, PowertoolsLambdaContext.Instance.FunctionVersion); | ||
message.TryAdd(LoggingConstants.KeyFunctionMemorySize, PowertoolsLambdaContext.Instance.MemoryLimitInMB); | ||
message.TryAdd(LoggingConstants.KeyFunctionArn, PowertoolsLambdaContext.Instance.InvokedFunctionArn); | ||
message.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId); | ||
logEntry.TryAdd(LoggingConstants.KeyFunctionName, PowertoolsLambdaContext.Instance.FunctionName); | ||
logEntry.TryAdd(LoggingConstants.KeyFunctionVersion, PowertoolsLambdaContext.Instance.FunctionVersion); | ||
logEntry.TryAdd(LoggingConstants.KeyFunctionMemorySize, PowertoolsLambdaContext.Instance.MemoryLimitInMB); | ||
logEntry.TryAdd(LoggingConstants.KeyFunctionArn, PowertoolsLambdaContext.Instance.InvokedFunctionArn); | ||
logEntry.TryAdd(LoggingConstants.KeyFunctionRequestId, PowertoolsLambdaContext.Instance.AwsRequestId); | ||
} | ||
|
||
// Add Extra Fields | ||
|
@@ -228,24 +251,109 @@ public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Except | |
foreach (var (key, value) in CurrentScope.ExtraKeys) | ||
{ | ||
if (!string.IsNullOrWhiteSpace(key)) | ||
message.TryAdd(key, value); | ||
logEntry.TryAdd(key, value); | ||
} | ||
} | ||
|
||
message.TryAdd(LoggingConstants.KeyTimestamp, DateTime.UtcNow.ToString("o")); | ||
message.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString()); | ||
message.TryAdd(LoggingConstants.KeyService, Service); | ||
message.TryAdd(LoggingConstants.KeyLoggerName, _name); | ||
message.TryAdd(LoggingConstants.KeyMessage, | ||
CustomFormatter(state, exception, out var customMessage) && customMessage is not null | ||
? customMessage | ||
: formatter(state, exception)); | ||
logEntry.TryAdd(LoggingConstants.KeyTimestamp, timestamp.ToString("o")); | ||
logEntry.TryAdd(LoggingConstants.KeyLogLevel, logLevel.ToString()); | ||
logEntry.TryAdd(LoggingConstants.KeyService, Service); | ||
logEntry.TryAdd(LoggingConstants.KeyLoggerName, _name); | ||
logEntry.TryAdd(LoggingConstants.KeyMessage, message); | ||
|
||
if (CurrentConfig.SamplingRate.HasValue) | ||
message.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value); | ||
logEntry.TryAdd(LoggingConstants.KeySamplingRate, CurrentConfig.SamplingRate.Value); | ||
if (exception != null) | ||
message.TryAdd(LoggingConstants.KeyException, exception); | ||
logEntry.TryAdd(LoggingConstants.KeyException, exception); | ||
|
||
_systemWrapper.LogLine(JsonSerializer.Serialize(message, JsonSerializerOptions)); | ||
return logEntry; | ||
} | ||
|
||
/// <summary> | ||
/// Gets a formatted log entry. | ||
/// </summary> | ||
/// <param name="logLevel">Entry will be written on this level.</param> | ||
/// <param name="timestamp">Entry timestamp.</param> | ||
/// <param name="message">The message to be written. Can be also an object.</param> | ||
/// <param name="exception">The exception related to this entry.</param> | ||
/// <param name="logFormatter">The custom log entry formatter.</param> | ||
private object GetFormattedLogEntry(LogLevel logLevel, DateTime timestamp, object message, | ||
Exception exception, ILogFormatter logFormatter) | ||
{ | ||
if (logFormatter is null) | ||
return null; | ||
|
||
var logEntry = new LogEntry | ||
{ | ||
Timestamp = timestamp, | ||
Level = logLevel, | ||
Service = Service, | ||
Name = _name, | ||
Message = message, | ||
Exception = exception, | ||
SamplingRate = CurrentConfig.SamplingRate, | ||
}; | ||
|
||
var extraKeys = new Dictionary<string, object>(); | ||
|
||
// Add Custom Keys | ||
foreach (var (key, value) in Logger.GetAllKeys()) | ||
{ | ||
switch (key) | ||
{ | ||
case LoggingConstants.KeyColdStart: | ||
logEntry.ColdStart = (bool)value; | ||
break; | ||
case LoggingConstants.KeyXRayTraceId: | ||
logEntry.XRayTraceId = value as string; | ||
break; | ||
case LoggingConstants.KeyCorrelationId: | ||
logEntry.CorrelationId = value as string; | ||
break; | ||
default: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. In here we are changing the premise that what is logged should be exactly what is returned from the formatter. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We don't change the object returning from formatter and we log it as is. This is not for the object returned from the formatter, it's for creating an LogEntry to pass it to the formatter. |
||
extraKeys.TryAdd(key, value); | ||
break; | ||
} | ||
} | ||
|
||
// Add Extra Fields | ||
if (CurrentScope?.ExtraKeys is not null) | ||
{ | ||
foreach (var (key, value) in CurrentScope.ExtraKeys) | ||
{ | ||
if (!string.IsNullOrWhiteSpace(key)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. don't think is possible at runtime to have an empty or null key for dictionary, this could be simplified with There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It cannot, I can remove the null check but is's an append to the existing dictionary, so cannot be replaced. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I have replaced it with a new throw new LogFormatException (custom exception) |
||
extraKeys.TryAdd(key, value); | ||
} | ||
} | ||
|
||
if (extraKeys.Any()) | ||
logEntry.ExtraKeys = extraKeys; | ||
|
||
// Add Lambda Context Keys | ||
if (PowertoolsLambdaContext.Instance is not null) | ||
{ | ||
logEntry.LambdaContext = new LogEntryLambdaContext | ||
{ | ||
FunctionName = PowertoolsLambdaContext.Instance.FunctionName, | ||
FunctionVersion = PowertoolsLambdaContext.Instance.FunctionVersion, | ||
MemoryLimitInMB = PowertoolsLambdaContext.Instance.MemoryLimitInMB, | ||
InvokedFunctionArn = PowertoolsLambdaContext.Instance.InvokedFunctionArn, | ||
AwsRequestId = PowertoolsLambdaContext.Instance.AwsRequestId, | ||
}; | ||
} | ||
|
||
try | ||
{ | ||
var logObject = logFormatter.FormatLogEntry(logEntry); | ||
if (logObject is null) | ||
throw new LogFormatException($"{logFormatter.GetType().FullName} returned Null value."); | ||
return logObject; | ||
} | ||
catch (Exception e) | ||
{ | ||
throw new LogFormatException( | ||
$"{logFormatter.GetType().FullName} raised an exception: {e.Message}.", e); | ||
} | ||
} | ||
|
||
/// <summary> | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,92 @@ | ||
/* | ||
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"). | ||
* You may not use this file except in compliance with the License. | ||
* A copy of the License is located at | ||
* | ||
* http://aws.amazon.com/apache2.0 | ||
* | ||
* or in the "license" file accompanying this file. This file is distributed | ||
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either | ||
* express or implied. See the License for the specific language governing | ||
* permissions and limitations under the License. | ||
*/ | ||
|
||
using System; | ||
using System.Collections.Generic; | ||
using Microsoft.Extensions.Logging; | ||
|
||
namespace AWS.Lambda.Powertools.Logging; | ||
|
||
/// <summary> | ||
/// Powertools Log Entry | ||
/// </summary> | ||
public class LogEntry | ||
{ | ||
/// <summary> | ||
/// Indicates the cold start. | ||
/// </summary> | ||
/// <value>The cold start value.</value> | ||
public bool ColdStart { get; internal set; } | ||
|
||
/// <summary> | ||
/// Gets the X-Ray trace identifier. | ||
/// </summary> | ||
/// <value>The X-Ray trace identifier.</value> | ||
public string XRayTraceId { get; internal set; } | ||
|
||
/// <summary> | ||
/// Gets the correlation identifier. | ||
/// </summary> | ||
/// <value>The correlation identifier.</value> | ||
public string CorrelationId { get; internal set; } | ||
|
||
/// <summary> | ||
/// Log entry timestamp in UTC. | ||
/// </summary> | ||
public DateTime Timestamp { get; internal set; } | ||
|
||
/// <summary> | ||
/// Log entry Level is used for logging. | ||
/// </summary> | ||
public LogLevel Level { get; internal set; } | ||
|
||
/// <summary> | ||
/// Service name is used for logging. | ||
/// </summary> | ||
public string Service { get; internal set; } | ||
|
||
/// <summary> | ||
/// Logger name is used for logging. | ||
/// </summary> | ||
public string Name { get; internal set; } | ||
|
||
/// <summary> | ||
/// Log entry Level is used for logging. | ||
/// </summary> | ||
public object Message { get; internal set; } | ||
|
||
/// <summary> | ||
/// Dynamically set a percentage of logs to DEBUG level. | ||
/// This can be also set using the environment variable <c>POWERTOOLS_LOGGER_SAMPLE_RATE</c>. | ||
/// </summary> | ||
/// <value>The sampling rate.</value> | ||
public double? SamplingRate { get; internal set; } | ||
|
||
/// <summary> | ||
/// Gets the appended additional keys to a log entry. | ||
/// <value>The extra keys.</value> | ||
/// </summary> | ||
public Dictionary<string, object> ExtraKeys { get; internal set; } | ||
|
||
/// <summary> | ||
/// The exception related to this entry. | ||
/// </summary> | ||
public Exception Exception { get; internal set; } | ||
|
||
/// <summary> | ||
/// The Lambda Context related to this entry. | ||
/// </summary> | ||
public LogEntryLambdaContext LambdaContext { get; internal set; } | ||
} |
Uh oh!
There was an error while loading. Please reload this page.