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' {