using Microsoft.Extensions.Configuration; using System; using System.Collections.Generic; using System.Linq; namespace Microsoft.Extensions.Logging { /// /// Options that can be used to configure Lambda logging. /// public class LambdaLoggerOptions { // Default configuration section. // Customer configuration will be fetched from this section, unless otherwise specified. internal const string DEFAULT_SECTION_NAME = "Lambda.Logging"; private const string INCLUDE_LOG_LEVEL_KEY = "IncludeLogLevel"; private const string INCLUDE_CATEGORY_KEY = "IncludeCategory"; private const string INCLUDE_NEWLINE_KEY = "IncludeNewline"; private const string INCLUDE_EXCEPTION_KEY = "IncludeException"; private const string INCLUDE_EVENT_ID_KEY = "IncludeEventId"; private const string INCLUDE_SCOPES_KEY = "IncludeScopes"; private const string LOG_LEVEL_KEY = "LogLevel"; private const string DEFAULT_CATEGORY = "Default"; /// /// Flag to indicate if LogLevel should be part of logged message. /// Default is true. /// public bool IncludeLogLevel { get; set; } /// /// Flag to indicate if Category should be part of logged message. /// Default is true. /// public bool IncludeCategory { get; set; } /// /// Flag to indicate if logged messages should have a newline appended /// to them, if one isn't already there. /// Default is true. /// public bool IncludeNewline { get; set; } /// /// Flag to indicate if Exception should be part of logged message. /// Default is false. /// public bool IncludeException { get; set; } /// /// Flag to indicate if EventId should be part of logged message. /// Default is false. /// public bool IncludeEventId { get; set; } /// /// Gets or sets a value indicating whether scopes should be included in the message. /// Defaults to false. /// public bool IncludeScopes { get; set; } /// /// Function used to filter events based on the log level. /// Default value is null and will instruct logger to log everything. /// [CLSCompliant(false)] // https://github.com/aspnet/Logging/issues/500 public Func Filter { get; set; } /// /// Constructs instance of LambdaLoggerOptions with default values. /// public LambdaLoggerOptions() { IncludeCategory = true; IncludeLogLevel = true; IncludeNewline = true; IncludeException = false; IncludeEventId = false; IncludeScopes = false; Filter = null; } /// /// Constructs instance of LambdaLoggerOptions with values from "Lambda.Logging" /// subsection of the specified configuration. /// The following configuration keys are supported: /// IncludeLogLevel - boolean flag indicates if LogLevel should be part of logged message. /// IncludeCategory - boolean flag indicates if Category should be part of logged message. /// LogLevels - category-to-LogLevel mapping which indicates minimum LogLevel for a category. /// /// [CLSCompliant(false)] // https://github.com/aspnet/Logging/issues/500 public LambdaLoggerOptions(IConfiguration configuration) : this(configuration, DEFAULT_SECTION_NAME) { } /// /// Constructs instance of LambdaLoggerOptions with values from specified /// subsection of the configuration. /// The following configuration keys are supported: /// IncludeLogLevel - boolean flag indicates if LogLevel should be part of logged message. /// IncludeCategory - boolean flag indicates if Category should be part of logged message. /// LogLevels - category-to-LogLevel mapping which indicates minimum LogLevel for a category. /// /// /// [CLSCompliant(false)] // https://github.com/aspnet/Logging/issues/500 public LambdaLoggerOptions(IConfiguration configuration, string loggingSectionName) : this() { // Input validation if (configuration == null) { throw new ArgumentNullException(nameof(configuration)); } if (string.IsNullOrEmpty(loggingSectionName)) { throw new ArgumentNullException(nameof(loggingSectionName)); } var loggerConfiguration = configuration.GetSection(loggingSectionName); if (loggerConfiguration == null) { throw new ArgumentOutOfRangeException(nameof(loggingSectionName), $"Unable to find section '{loggingSectionName}' in current configuration."); } // Parse settings if (TryGetString(loggerConfiguration, INCLUDE_CATEGORY_KEY, out string includeCategoryString)) { IncludeCategory = bool.Parse(includeCategoryString); } if (TryGetString(loggerConfiguration, INCLUDE_LOG_LEVEL_KEY, out string includeLogLevelString)) { IncludeLogLevel = bool.Parse(includeLogLevelString); } if (TryGetString(loggerConfiguration, INCLUDE_EXCEPTION_KEY, out string includeExceptionString)) { IncludeException = bool.Parse(includeExceptionString); } if (TryGetString(loggerConfiguration, INCLUDE_EVENT_ID_KEY, out string includeEventIdString)) { IncludeEventId = bool.Parse(includeEventIdString); } if (TryGetString(loggerConfiguration, INCLUDE_NEWLINE_KEY, out string includeNewlineString)) { IncludeNewline = bool.Parse(includeNewlineString); } if (TryGetSection(loggerConfiguration, LOG_LEVEL_KEY, out IConfiguration logLevelsSection)) { Filter = CreateFilter(logLevelsSection); } if (TryGetString(loggerConfiguration, INCLUDE_SCOPES_KEY, out string includeScopesString)) { IncludeScopes = bool.Parse(includeScopesString); } } // Retrieves configuration value for key, if one exists private static bool TryGetString(IConfiguration configuration, string key, out string value) { value = configuration[key]; return (value != null); } // Retrieves configuration section for key, if one exists private static bool TryGetSection(IConfiguration configuration, string key, out IConfiguration value) { value = configuration.GetSection(key); return (value != null); } // Creates filter for log levels section private static Func CreateFilter(IConfiguration logLevelsSection) { // Empty section means log everything var logLevels = logLevelsSection.GetChildren().ToList(); if (logLevels.Count == 0) { return null; } // Populate mapping of category to LogLevel var logLevelsMapping = new Dictionary(StringComparer.Ordinal); var defaultLogLevel = LogLevel.Information; foreach (var logLevel in logLevels) { var category = logLevel.Key; var minLevelValue = logLevel.Value; LogLevel minLevel; if (!Enum.TryParse(minLevelValue, out minLevel)) { throw new InvalidCastException($"Unable to convert level '{minLevelValue}' for category '{category}' to LogLevel."); } if (category.Contains("*")) { // Only 1 wildcard is supported var wildcardCount = category.Count(x => x == '*'); if (wildcardCount > 1) { throw new ArgumentOutOfRangeException($"Category '{category}' is invalid - only 1 wildcard is supported in a category."); } // Wildcards are only supported at the end of a Category name! var wildcardLocation = category.IndexOf('*'); if (wildcardLocation != category.Length - 1) { throw new ArgumentException($"Category '{category}' is invalid - wilcards are only supported at the end of a category."); } var trimmedCategory = category.TrimEnd('*'); logLevelsMapping[trimmedCategory] = minLevel; } else if (category.Equals(DEFAULT_CATEGORY, StringComparison.OrdinalIgnoreCase)) { defaultLogLevel = minLevel; } else { logLevelsMapping[category] = minLevel; } } // Extract a reverse sorted list of categories. This allows us to quickly search for most specific match to least specific. var orderedCategories = logLevelsMapping.Keys .OrderByDescending(categoryKey => categoryKey) .ToList(); // Filter lambda that examines mapping return (string category, LogLevel logLevel) => { LogLevel minLevel; // Exact match takes priority if (logLevelsMapping.TryGetValue(category, out minLevel)) { return logLevel >= minLevel; } // Find the most specific wildcard or namespace prefix category that matches the logger category var matchedCategory = orderedCategories.FirstOrDefault(category.StartsWith); if (matchedCategory != null) { // If no log filters then default to logging the log message. minLevel = logLevelsMapping[matchedCategory]; return logLevel >= minLevel; } else { // If no log filters then default to logging the log message. return (logLevel >= defaultLogLevel); } }; } } }