Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions docs/CHANGELOG-v3.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ What's changed since pre-release v3.0.0-B0203:
- With this update rules with the severity level `Error` that fail will break the pipeline by default.
- The `Execution.Break` option can be set to `Never`, `OnError`, `OnWarning`, or `OnInformation`.
- If a rule fails with a severity level equal or higher than the configured level the pipeline will break.
- General improvements:
- Added support for native logging within emitters by @BernieWhite.
[#1837](https://github.com/microsoft/PSRule/issues/1837)
- Engineering:
- Bump xunit to v2.9.0.
[#1869](https://github.com/microsoft/PSRule/pull/1869)
Expand Down
13 changes: 12 additions & 1 deletion docs/specs/emitter-spec.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,5 +25,16 @@ Goals with emitters:

The goal of emitters is to provide a high performance and extensible way to emit custom objects to the input stream.

Emitters define a C# `IEmitter` interface for emitting objects to the input stream.
Emitters define an `IEmitter` interface for emitting objects to the input stream.
The implementation of an emitter must be thread safe, as emitters can be run in parallel.

## Logging

An emitter may expose diagnostic logs by using the `PSRule.Runtime.ILogger<T>` interface.

## Dependency injection

PSRule uses dependency injection to create each emitter instance.
The following interfaces can optionally be specified in a emitter constructor to have references injected to the instance.

- `PSRule.Runtime.ILogger<T>`
17 changes: 17 additions & 0 deletions src/PSRule.Types/Runtime/ILoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// A factory that creates loggers.
/// </summary>
public interface ILoggerFactory
{
/// <summary>
/// A factory for creating loggers.
/// </summary>
/// <param name="categoryName">The category name for messages produced by the logger.</param>
/// <returns>Create an instance of an <see cref="ILogger"/> with the specified category name.</returns>
ILogger Create(string categoryName);
}
13 changes: 13 additions & 0 deletions src/PSRule.Types/Runtime/ILogger_TCategoryName.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// Log diagnostic messages at runtime.
/// </summary>
/// <typeparam name="TCategoryName">The type name to use for the logger category.</typeparam>
public interface ILogger<out TCategoryName> : ILogger
{

}
30 changes: 30 additions & 0 deletions src/PSRule.Types/Runtime/Logger_T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// Log diagnostic messages at runtime.
/// </summary>
/// <typeparam name="T">The type name to use for the logger category.</typeparam>
public sealed class Logger<T>(ILoggerFactory loggerFactory) : ILogger<T>
{
private readonly ILogger _Logger = loggerFactory.Create(typeof(T).FullName);

/// <summary>
/// The name of the category.
/// </summary>
public string CategoryName => typeof(T).Name;

/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel)
{
return _Logger.IsEnabled(logLevel);
}

/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
_Logger.Log(logLevel, eventId, state, exception, formatter);
}
}
28 changes: 28 additions & 0 deletions src/PSRule.Types/Runtime/NullLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.


namespace PSRule.Runtime;

/// <summary>
/// A logger that sinks all logs.
/// </summary>
public sealed class NullLogger : ILogger
{
/// <summary>
/// An default instance of the null logger.
/// </summary>
public static readonly NullLogger Instance = new();

/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel)
{
return false;
}

/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{

}
}
27 changes: 27 additions & 0 deletions src/PSRule.Types/Runtime/NullLogger_T.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// A logger that sinks all logs.
/// </summary>
public sealed class NullLogger<T> : ILogger<T>
{
/// <summary>
/// An default instance of the null logger.
/// </summary>
public static readonly NullLogger<T> Instance = new();

/// <inheritdoc/>
public bool IsEnabled(LogLevel logLevel)
{
return false;
}

/// <inheritdoc/>
public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{

}
}
2 changes: 1 addition & 1 deletion src/PSRule/Commands/ExportConventionCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ protected override void ProcessRecord()
WriteObject(block);
}

private LanguageScriptBlock? ConventionBlock(RunspaceContext context, ScriptBlock block, RunspaceScope scope)
private LanguageScriptBlock? ConventionBlock(RunspaceContext context, ScriptBlock? block, RunspaceScope scope)
{
if (block == null)
return null;
Expand Down
25 changes: 25 additions & 0 deletions src/PSRule/Common/LoggerExtensions.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Management.Automation;
using PSRule.Definitions;
using PSRule.Pipeline;
using PSRule.Resources;
Expand All @@ -15,6 +16,7 @@ internal static class LoggerExtensions
{
private static readonly EventId PSR0004 = new(4, "PSR0004");
private static readonly EventId PSR0005 = new(5, "PSR0005");
private static readonly EventId PSR0006 = new(6, "PSR0006");

/// <summary>
/// PSR0005: The {0} '{1}' is obsolete.
Expand Down Expand Up @@ -55,4 +57,27 @@ internal static void ErrorResourceUnresolved(this ILogger logger, ResourceKind k
id
);
}

/// <summary>
/// PSR0006: Failed to deserialize the file '{0}': {1}
/// </summary>
internal static void ErrorReadFileFailed(this ILogger logger, string path, Exception innerException)
{
if (logger == null || !logger.IsEnabled(LogLevel.Error))
return;

logger.LogError
(
PSR0006,
new PipelineSerializationException(string.Format(
Thread.CurrentThread.CurrentCulture,
PSRuleResources.PSR0006,
path,
innerException.Message), path, innerException
),
PSRuleResources.PSR0006,
path,
innerException.Message
);
}
}
20 changes: 19 additions & 1 deletion src/PSRule/Pipeline/Emitters/EmitterBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

using Microsoft.Extensions.DependencyInjection;
using PSRule.Emitters;
using PSRule.Runtime;

namespace PSRule.Pipeline.Emitters;

Expand All @@ -18,10 +19,15 @@ public EmitterBuilder()
{
_EmitterTypes = new List<Type>(4);
_Services = new ServiceCollection();
AddInternalServices();
AddInternalEmitters();
}

public void AddEmitter<T>() where T : IEmitter, new()
/// <summary>
/// Add an <see cref="IEmitter"/> implementation class.
/// </summary>
/// <typeparam name="T">An emitter type that implements <see cref="IEmitter"/>.</typeparam>
public void AddEmitter<T>() where T : class, IEmitter
{
_EmitterTypes.Add(typeof(T));
_Services.AddTransient(typeof(T));
Expand All @@ -48,6 +54,18 @@ public EmitterCollection Build(IEmitterContext context)
return new EmitterCollection(serviceProvider, [.. emitters], context);
}

/// <summary>
/// Add the default services automatically added to the DI container.
/// </summary>
private void AddInternalServices()
{
_Services.AddSingleton<ILoggerFactory, LoggerFactory>();
_Services.AddSingleton(typeof(ILogger<>), typeof(Logger<>));
}

/// <summary>
/// Add the built-in emitters to the list of emitters for processing items.
/// </summary>
private void AddInternalEmitters()
{
AddEmitter<YamlEmitter>();
Expand Down
8 changes: 5 additions & 3 deletions src/PSRule/Pipeline/Emitters/JsonEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@ internal sealed class JsonEmitter : FileEmitter
private const string EXTENSION_JSONC = ".jsonc";
private const string EXTENSION_SARIF = ".sarif";

private readonly JsonSerializer _Deserializer;
private readonly ILogger<JsonEmitter> _Logger;
private readonly JsonSerializerSettings _Settings;
private readonly JsonSerializer _Deserializer;
private readonly HashSet<string> _Extensions;

public JsonEmitter()
public JsonEmitter(ILogger<JsonEmitter> logger)
{
_Logger = logger;
_Settings = new JsonSerializerSettings
{

Expand Down Expand Up @@ -63,7 +65,7 @@ protected override bool VisitFile(IEmitterContext context, IFileStream stream)
{
if (stream.Info != null && !string.IsNullOrEmpty(stream.Info.Path))
{
RunspaceContext.CurrentThread.Writer.ErrorReadFileFailed(stream.Info.Path, ex);
_Logger.ErrorReadFileFailed(stream.Info.Path, ex);
}
throw;
}
Expand Down
6 changes: 4 additions & 2 deletions src/PSRule/Pipeline/Emitters/YamlEmitter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ internal sealed class YamlEmitter : FileEmitter
private const string EXTENSION_YAML = ".yaml";
private const string EXTENSION_YML = ".yml";

private readonly ILogger<YamlEmitter> _Logger;
private readonly PSObjectYamlTypeConverter _TypeConverter;
private readonly IDeserializer _Deserializer;

public YamlEmitter()
public YamlEmitter(ILogger<YamlEmitter> logger)
{
_Logger = logger;
_TypeConverter = new PSObjectYamlTypeConverter();
_Deserializer = GetDeserializer();
}
Expand Down Expand Up @@ -63,7 +65,7 @@ protected override bool VisitFile(IEmitterContext context, IFileStream stream)
{
if (stream.Info != null && !string.IsNullOrEmpty(stream.Info.Path))
{
RunspaceContext.CurrentThread.Writer.ErrorReadFileFailed(stream.Info.Path, ex);
_Logger.ErrorReadFileFailed(stream.Info.Path, ex);
}
throw;
}
Expand Down
9 changes: 9 additions & 0 deletions src/PSRule/Resources/PSRuleResources.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/PSRule/Resources/PSRuleResources.resx
Original file line number Diff line number Diff line change
Expand Up @@ -415,4 +415,7 @@
<data name="PSR0004" xml:space="preserve">
<value>PSR0004: The specified {0} resource '{1}' is not known.</value>
</data>
<data name="PSR0006" xml:space="preserve">
<value>PSR0006: Failed to deserialize the file '{0}': {1}</value>
</data>
</root>
15 changes: 15 additions & 0 deletions src/PSRule/Runtime/LoggerFactory.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace PSRule.Runtime;

/// <summary>
/// Implements creating a logger for a specific category.
/// </summary>
internal sealed class LoggerFactory : ILoggerFactory
{
public ILogger Create(string categoryName)
{
return RunspaceContext.CurrentThread == null ? RunspaceContext.CurrentThread : NullLogger.Instance;
}
}
14 changes: 7 additions & 7 deletions tests/PSRule.Tests/ConventionTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ public void WithConventions()
{
var testObject1 = new TestObject { Name = "TestObject1" };
var option = GetOption();
option.Rule.Include = new string[] { "ConventionTest" };
option.Convention.Include = new string[] { "Convention1" };
option.Rule.Include = ["ConventionTest"];
option.Convention.Include = ["Convention1"];
var builder = PipelineBuilder.Invoke(GetSource(), option, null);
var pipeline = builder.Build();

Expand All @@ -33,10 +33,10 @@ public void ConventionOrder()
{
var testObject1 = new TestObject { Name = "TestObject1" };
var option = GetOption();
option.Rule.Include = new string[] { "ConventionTest" };
option.Rule.Include = ["ConventionTest"];

// Order 1
option.Convention.Include = new string[] { "Convention1", "Convention2" };
option.Convention.Include = ["Convention1", "Convention2"];
var writer = new TestWriter(option);
var builder = PipelineBuilder.Invoke(GetSource(), option, null);
var pipeline = builder.Build(writer);
Expand All @@ -48,7 +48,7 @@ public void ConventionOrder()
Assert.Equal(110, actual2);

// Order 2
option.Convention.Include = new string[] { "Convention2", "Convention1" };
option.Convention.Include = ["Convention2", "Convention1"];
writer = new TestWriter(option);
builder = PipelineBuilder.Invoke(GetSource(), option, null);
pipeline = builder.Build(writer);
Expand All @@ -68,8 +68,8 @@ public void WithLocalizedData()
{
var testObject1 = new TestObject { Name = "TestObject1" };
var option = GetOption();
option.Rule.Include = new string[] { "WithLocalizedDataPrecondition" };
option.Convention.Include = new string[] { "Convention.WithLocalizedData" };
option.Rule.Include = ["WithLocalizedDataPrecondition"];
option.Convention.Include = ["Convention.WithLocalizedData"];
var writer = new TestWriter(option);
var builder = PipelineBuilder.Invoke(GetSource(), option, null);
var pipeline = builder.Build(writer);
Expand Down
Loading