Skip to content

Commit

Permalink
Add native logging for emitters #1837 (#1884)
Browse files Browse the repository at this point in the history
  • Loading branch information
BernieWhite authored Jul 29, 2024
1 parent 666416c commit 06662fc
Show file tree
Hide file tree
Showing 18 changed files with 229 additions and 24 deletions.
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

0 comments on commit 06662fc

Please sign in to comment.