Libraries/src/Amazon.Lambda.RuntimeSupport/Helpers/Logging/MessageProperty.cs (120 lines of code) (raw):
#if NET6_0_OR_GREATER
using System;
using System.Collections;
using System.Globalization;
using System.Text;
namespace Amazon.Lambda.RuntimeSupport.Helpers.Logging
{
/// <summary>
/// Represents a message property in a message template. For example the message
/// template "User bought {count} of {product}" has count and product as message properties.
///
/// </summary>
public class MessageProperty
{
private static readonly char[] PARAM_FORMAT_DELIMITERS = { ':' };
/// <summary>
/// Parse the string representation of the message property without the brackets
/// to construct the MessageProperty.
/// </summary>
/// <param name="messageToken"></param>
public MessageProperty(ReadOnlySpan<char> messageToken)
{
// messageToken format is:
// <optional-directive><name>:<optional-format-argument>
this.MessageToken = "{" + messageToken.ToString() + "}";
this.FormatDirective = Directive.Default;
if (messageToken[0] == '@')
{
this.FormatDirective = Directive.JsonSerialization;
messageToken = messageToken.Slice(1);
}
var idxOfDelimeter = messageToken.IndexOfAny(PARAM_FORMAT_DELIMITERS);
if (idxOfDelimeter < 0)
{
this.Name = messageToken.ToString().Trim();
this.FormatArgument = null;
}
else
{
this.Name = messageToken.Slice(0, idxOfDelimeter).ToString().Trim();
this.FormatArgument = messageToken.Slice(idxOfDelimeter + 1).ToString().Trim();
if(this.FormatArgument == string.Empty)
{
this.FormatArgument = null;
}
}
}
/// <summary>
/// The original text of the message property.
/// </summary>
public string MessageToken { get; private set; }
/// <summary>
/// Enum for controlling the formatting of complex logging arguments.
/// </summary>
public enum Directive {
/// <summary>
/// Perform a string formatting for the logging argument.
/// </summary>
Default,
/// <summary>
/// Perform a JSON serialization on the logging argument.
/// </summary>
JsonSerialization
};
/// <summary>
/// The Name of the message property.
/// </summary>
public string Name { get; private set; }
/// <summary>
/// Optional format argument. If used the value will be formatted using string.Format passing in this format argument.
/// </summary>
public string FormatArgument { get; private set; }
/// <summary>
/// Optional format directive. Gives users the ability
/// to indicate when types should be serialized to JSON when using structured logging.
/// </summary>
public Directive FormatDirective { get; private set; }
/// <summary>
/// Formats the value as a string that can be used to replace the message property token inside a message template.
/// </summary>
/// <param name="value"></param>
/// <returns></returns>
public string FormatForMessage(object value)
{
if(value is byte[] bytes)
{
return FormatByteArray(bytes);
}
if (value is ReadOnlyMemory<byte> roBytes)
{
return FormatByteArray(roBytes.Span);
}
if (value is Memory<byte> meBytes)
{
return FormatByteArray(meBytes.Span);
}
if (value == null || value is IList || value is IDictionary)
{
return this.MessageToken;
}
if(!string.IsNullOrEmpty(this.FormatArgument))
{
return ApplyFormatArgument(value, this.FormatArgument);
}
if(value is DateTime dt)
{
return dt.ToString(AbstractLogMessageFormatter.DateFormat, CultureInfo.InvariantCulture);
}
if (value is DateTimeOffset dto)
{
return dto.ToString(AbstractLogMessageFormatter.DateFormat, CultureInfo.InvariantCulture);
}
if (value is DateOnly dateOnly)
{
return dateOnly.ToString(AbstractLogMessageFormatter.DateOnlyFormat, CultureInfo.InvariantCulture);
}
if (value is TimeOnly timeOnly)
{
return timeOnly.ToString(AbstractLogMessageFormatter.TimeOnlyFormat, CultureInfo.InvariantCulture);
}
return value.ToString();
}
/// <summary>
/// If format argument is provided formats the value using string.Format otherwise returns
/// the ToString value.
/// </summary>
/// <param name="value"></param>
/// <param name="formatArgument"></param>
/// <returns></returns>
public static string ApplyFormatArgument(object value, string formatArgument)
{
if(string.IsNullOrEmpty(formatArgument))
{
return value.ToString();
}
try
{
var formattedValue = string.Format("{0:" + formatArgument + "}", value);
return formattedValue;
}
catch(FormatException ex)
{
InternalLogger.GetDefaultLogger().LogError(ex, "Failed to apply logging format argument: " + formatArgument);
return value.ToString();
}
}
/// <summary>
/// Formats byte span, including byte arrays, as a hex string. If the byte span is long the hex string
/// will be truncated with a suffix added for the count of the byte span.
/// </summary>
/// <param name="bytes"></param>
/// <returns></returns>
public static string FormatByteArray(ReadOnlySpan<byte> bytes)
{
// Follow Serilog's example of outputting at 16 bytes before stopping and adding a count suffix
const int MAX_LENGTH = 16;
var sb = new StringBuilder();
for(int i = 0; i < bytes.Length; i++)
{
if(i == MAX_LENGTH)
{
sb.Append("... (");
sb.Append(bytes.Length);
sb.Append(" bytes)");
break;
}
sb.Append(bytes[i].ToString("X2"));
}
return sb.ToString();
}
}
}
#endif