diff --git a/src/PSRule.Types/Runtime/EventId.cs b/src/PSRule.Types/Runtime/EventId.cs new file mode 100644 index 0000000000..f24cf518bc --- /dev/null +++ b/src/PSRule.Types/Runtime/EventId.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// Identifies a logging event. +/// The primary identifier is the "Id" property, with the "Name" property providing a short description of this type of event. +/// +public readonly struct EventId : IEquatable +{ + /// + /// Implicitly creates an EventId from the given . + /// + /// The to convert to an EventId. + public static implicit operator EventId(int i) + { + return new EventId(i); + } + + /// + /// Checks if two specified instances have the same value. They are equal if they have the same Id. + /// + /// The first . + /// The second . + /// if the objects are equal. + public static bool operator ==(EventId left, EventId right) + { + return left.Equals(right); + } + + /// + /// Checks if two specified instances have different values. + /// + /// The first . + /// The second . + /// if the objects are not equal. + public static bool operator !=(EventId left, EventId right) + { + return !left.Equals(right); + } + + /// + /// Initializes an instance of the struct. + /// + /// The numeric identifier for this event. + /// The name of this event. + public EventId(int id, string? name = null) + { + Id = id; + Name = name; + } + + /// + /// Gets the numeric identifier for this event. + /// + public int Id { get; } + + /// + /// Gets the name of this event. + /// + public string? Name { get; } + + /// + public override string ToString() + { + return Name ?? Id.ToString(); + } + + /// + /// Indicates whether the current object is equal to another object of the same type. Two events are equal if they have the same id. + /// + /// An object to compare with this object. + /// if the current object is equal to the other parameter; otherwise, . + public bool Equals(EventId other) + { + return Id == other.Id; + } + + /// + public override bool Equals(object? obj) + { + if (obj is null) return false; + + return obj is EventId eventId && Equals(eventId); + } + + /// + public override int GetHashCode() + { + return Id; + } +} diff --git a/src/PSRule.Types/Runtime/FormattedLogValues.cs b/src/PSRule.Types/Runtime/FormattedLogValues.cs new file mode 100644 index 0000000000..6b34b3d155 --- /dev/null +++ b/src/PSRule.Types/Runtime/FormattedLogValues.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Collections; + +namespace PSRule.Runtime; + +/// +/// Enable formatted log values in diagnostic messages. +/// +internal readonly struct FormattedLogValues : IReadOnlyList> +{ + private const string NullFormat = "[null]"; + + private readonly object?[]? _Values; + private readonly string _OriginalMessage; + + public FormattedLogValues(string? format, params object?[]? values) + { + _OriginalMessage = format ?? NullFormat; + _Values = values; + } + + public KeyValuePair this[int index] + { + get + { + if (index < 0 || index >= Count) throw new IndexOutOfRangeException(nameof(index)); + + if (index == Count - 1) + { + return new KeyValuePair("{OriginalFormat}", _OriginalMessage); + } + + return new KeyValuePair($"{index}", _Values?[index]); + } + } + + public int Count + { + get + { + return _Values == null ? 1 : _Values.Length + 1; + } + } + + public IEnumerator> GetEnumerator() + { + for (int i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + + public override string ToString() + { + return string.Format(Thread.CurrentThread.CurrentCulture, _OriginalMessage, _Values); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } +} diff --git a/src/PSRule.Types/Runtime/ILogger.cs b/src/PSRule.Types/Runtime/ILogger.cs new file mode 100644 index 0000000000..f8dbc8f934 --- /dev/null +++ b/src/PSRule.Types/Runtime/ILogger.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// Log diagnostic messages at runtime. +/// +public interface ILogger +{ + /// + /// Determine if the specified type of diagnostic message should be logged. + /// + /// The type of the diagnostic message. + /// Returns true if the should be logged. + public bool IsEnabled(LogLevel logLevel); + + /// + /// Log a diagnostic message. + /// + /// Additional information that describes the diagnostic state to log. + /// The type of the diagnostic message. + /// An event identifier for the diagnostic message. + /// Additional information that describes the diagnostic state to log. + /// An optional exception which the diagnostic message is related to. + /// A function to format the diagnostic message for the outpuyt stream. + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter); +} diff --git a/src/PSRule.Types/Runtime/LogLevel.cs b/src/PSRule.Types/Runtime/LogLevel.cs new file mode 100644 index 0000000000..606d928018 --- /dev/null +++ b/src/PSRule.Types/Runtime/LogLevel.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// A set of log levels which indicate different types of diagnostic messages. +/// +public enum LogLevel +{ + /// + /// + /// + Trace = 0, + + /// + /// + /// + Debug = 1, + + /// + /// + /// + Information = 2, + + /// + /// + /// + Warning = 3, + + /// + /// + /// + Error = 4, + + /// + /// + /// + Critical = 5, + + /// + /// + /// + None = 6 +} diff --git a/src/PSRule.Types/Runtime/LoggerExtensions.cs b/src/PSRule.Types/Runtime/LoggerExtensions.cs new file mode 100644 index 0000000000..d35ad00e02 --- /dev/null +++ b/src/PSRule.Types/Runtime/LoggerExtensions.cs @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Runtime; + +/// +/// Extension for to log diagnostic messages. +/// +public static class LoggerExtensions +{ + private static readonly Func _messageFormatter = MessageFormatter; + + /// + /// Log a warning message. + /// + /// A valid instance. + /// An event identifier for the warning. + /// The format message text. + /// Additional arguments to use within the format message. + public static void LogWarning(this ILogger logger, EventId eventId, string? message, params object?[] args) + { + logger.Log(LogLevel.Warning, eventId, default, message, args); + } + + /// + /// Log an error message. + /// + /// A valid instance. + /// An event identifier for the error. + /// An optional exception which the error message is related to. + /// The format message text. + /// Additional arguments to use within the format message. + public static void LogError(this ILogger logger, EventId eventId, Exception? exception, string? message, params object?[] args) + { + logger.Log(LogLevel.Error, eventId, exception, message, args); + } + + /// + /// Log a diagnostic message. + /// + /// A valid instance. + /// The type of diagnostic message. + /// An event identifier for the diagnostic message. + /// An optional exception which the diagnostic message is related to. + /// The format message text. + /// Additional arguments to use within the format message. + public static void Log(this ILogger logger, LogLevel logLevel, EventId eventId, Exception? exception, string? message, params object?[] args) + { + if (logger == null || !logger.IsEnabled(logLevel)) + return; + + logger.Log(logLevel, eventId, new FormattedLogValues(message, args), exception, _messageFormatter); + } + + /// + /// Format log messages with values. + /// + private static string MessageFormatter(FormattedLogValues state, Exception? error) + { + return state.ToString(); + } +} diff --git a/src/PSRule/Common/LoggerExtensions.cs b/src/PSRule/Common/LoggerExtensions.cs index fbc7c0c138..35f7390ad0 100644 --- a/src/PSRule/Common/LoggerExtensions.cs +++ b/src/PSRule/Common/LoggerExtensions.cs @@ -8,26 +8,51 @@ namespace PSRule; +/// +/// Extension for to log common messages. +/// internal static class LoggerExtensions { + private static readonly EventId PSR0004 = new(4, "PSR0004"); + private static readonly EventId PSR0005 = new(5, "PSR0005"); + + /// + /// PSR0005: The {0} '{1}' is obsolete. + /// internal static void WarnResourceObsolete(this ILogger logger, ResourceKind kind, string id) { - if (logger == null || !logger.ShouldLog(LogLevel.Warning)) + if (logger == null || !logger.IsEnabled(LogLevel.Warning)) return; - logger.Warning(PSRuleResources.ResourceObsolete, Enum.GetName(typeof(ResourceKind), kind), id); + logger.LogWarning + ( + PSR0005, + PSRuleResources.PSR0005, + Enum.GetName(typeof(ResourceKind), kind), + id + ); } + /// + /// PSR0004: The specified {0} resource '{1}' is not known. + /// internal static void ErrorResourceUnresolved(this ILogger logger, ResourceKind kind, string id) { - if (logger == null || !logger.ShouldLog(LogLevel.Error)) + if (logger == null || !logger.IsEnabled(LogLevel.Error)) return; - logger.Error(new PipelineBuilderException(string.Format( - Thread.CurrentThread.CurrentCulture, + logger.LogError + ( + PSR0004, + new PipelineBuilderException(string.Format( + Thread.CurrentThread.CurrentCulture, + PSRuleResources.PSR0004, + Enum.GetName(typeof(ResourceKind), + kind), id + )), PSRuleResources.PSR0004, - Enum.GetName(typeof(ResourceKind), - kind), id - )), "PSR0004"); + Enum.GetName(typeof(ResourceKind), kind), + id + ); } } diff --git a/src/PSRule/Resources/PSRuleResources.Designer.cs b/src/PSRule/Resources/PSRuleResources.Designer.cs index b5027f9a6c..46329418c3 100644 --- a/src/PSRule/Resources/PSRuleResources.Designer.cs +++ b/src/PSRule/Resources/PSRuleResources.Designer.cs @@ -591,6 +591,15 @@ internal static string PSR0004 { } } + /// + /// Looks up a localized string similar to PSR0005: The {0} '{1}' is obsolete.. + /// + internal static string PSR0005 { + get { + return ResourceManager.GetString("PSR0005", resourceCulture); + } + } + /// /// Looks up a localized string similar to Failed to deserialize the file '{0}': {1}. /// @@ -636,15 +645,6 @@ internal static string RequiredVersionMismatch { } } - /// - /// Looks up a localized string similar to The {0} '{1}' is obsolete. Consider switching to an alternative {0}.. - /// - internal static string ResourceObsolete { - get { - return ResourceManager.GetString("ResourceObsolete", resourceCulture); - } - } - /// /// Looks up a localized string similar to {0} rule/s were suppressed for '{1}'.. /// diff --git a/src/PSRule/Resources/PSRuleResources.resx b/src/PSRule/Resources/PSRuleResources.resx index 1aa7b83796..6bef017a69 100644 --- a/src/PSRule/Resources/PSRuleResources.resx +++ b/src/PSRule/Resources/PSRuleResources.resx @@ -117,8 +117,8 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 - - The {0} '{1}' is obsolete. Consider switching to an alternative {0}. + + PSR0005: The {0} '{1}' is obsolete. Occurs when a resource is used that has been flagged as obsolete. diff --git a/src/PSRule/Runtime/ILogger.cs b/src/PSRule/Runtime/ILogger.cs deleted file mode 100644 index e798490126..0000000000 --- a/src/PSRule/Runtime/ILogger.cs +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace PSRule.Runtime; - -/// -/// A generic interface for diagnostic logging within PSRule. -/// -internal interface ILogger -{ - /// - /// Determines if a specific log level should be written. - /// - /// The level to query. - /// Returns true when the log level should be written or false otherwise. - bool ShouldLog(LogLevel level); - - /// - /// Write a warning. - /// - /// The warning message write. - /// Any arguments to format the string with. - void Warning(string message, params object[] args); - - /// - /// Write an error from an exception. - /// - /// The exception to write. - /// A string identifier for the error. - void Error(Exception exception, string errorId = null); -} diff --git a/src/PSRule/Runtime/LogLevel.cs b/src/PSRule/Runtime/LogLevel.cs deleted file mode 100644 index 2dcb10ba5b..0000000000 --- a/src/PSRule/Runtime/LogLevel.cs +++ /dev/null @@ -1,23 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -namespace PSRule.Runtime; - -/// -/// A set of log levels which indicate different types of diagnostic messages. -/// -[Flags] -internal enum LogLevel -{ - None = 0, - - Error = 1, - - Warning = 2, - - Info = 4, - - Verbose = 8, - - Debug = 16, -} diff --git a/src/PSRule/Runtime/RunspaceContext.cs b/src/PSRule/Runtime/RunspaceContext.cs index bcb84cf033..44e3203e76 100644 --- a/src/PSRule/Runtime/RunspaceContext.cs +++ b/src/PSRule/Runtime/RunspaceContext.cs @@ -852,34 +852,53 @@ internal bool TryGetConfigurationValue(string name, out object value) #region ILogger /// - public bool ShouldLog(LogLevel level) + bool ILogger.IsEnabled(LogLevel logLevel) { return Writer != null && ( - (level == LogLevel.Warning && Writer.ShouldWriteWarning()) || - (level == LogLevel.Error && Writer.ShouldWriteError()) || - (level == LogLevel.Info && Writer.ShouldWriteInformation()) || - (level == LogLevel.Verbose && Writer.ShouldWriteVerbose()) || - (level == LogLevel.Debug && Writer.ShouldWriteDebug()) + (logLevel == LogLevel.Warning && Writer.ShouldWriteWarning()) || + ((logLevel == LogLevel.Error || logLevel == LogLevel.Critical) && Writer.ShouldWriteError()) || + (logLevel == LogLevel.Information && Writer.ShouldWriteInformation()) || + (logLevel == LogLevel.Debug && Writer.ShouldWriteVerbose()) || + (logLevel == LogLevel.Trace && Writer.ShouldWriteDebug()) ); } +#nullable enable + /// - public void Warning(string message, params object[] args) + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (Writer == null || string.IsNullOrEmpty(message)) - return; + if (Writer == null) return; - Writer.WriteWarning(message, args); + if (logLevel == LogLevel.Error || logLevel == LogLevel.Critical) + { + Writer.WriteError(new ErrorRecord(exception, eventId.Id.ToString(), ErrorCategory.InvalidOperation, null)); + } + else if (logLevel == LogLevel.Warning) + { + Writer.WriteWarning(formatter(state, exception)); + } + else if (logLevel == LogLevel.Information) + { + Writer.WriteInformation(new InformationRecord(formatter(state, exception), null)); + } + else if (logLevel == LogLevel.Debug) + { + Writer.WriteDebug(formatter(state, exception)); + } + else if (logLevel == LogLevel.Trace) + { + Writer.WriteVerbose(formatter(state, exception)); + } } - /// - public void Error(Exception exception, string errorId = null) - { - if (Writer == null || exception == null) - return; + ///// + //IDisposable? ILogger.BeginScope(TState state) //where TState : notnull + //{ + // throw new NotImplementedException(); + //} - Writer.WriteError(new ErrorRecord(exception, errorId, ErrorCategory.InvalidOperation, null)); - } +#nullable restore #endregion ILogger diff --git a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 index fa30160645..83a4d7d342 100644 --- a/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Baseline.Tests.ps1 @@ -833,7 +833,7 @@ Describe 'Baseline' -Tag 'Baseline' { $Null = @($testObject | Invoke-PSRule -Path $ruleFilePath,$baselineFilePath -Baseline 'TestBaseline5' -WarningVariable outWarn -WarningAction SilentlyContinue); $warnings = @($outWarn); $warnings.Length | Should -Be 1; - $warnings[0] | Should -BeExactly "The Baseline '.\TestBaseline5' is obsolete. Consider switching to an alternative Baseline."; + $warnings[0] | Should -BeExactly "PSR0005: The Baseline '.\TestBaseline5' is obsolete."; } It 'With scoped configuration' {