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 src/Host/BravoApplicationBuilder.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.EventLog;
using Sqlbi.Bravo.Infrastructure.Configuration.Settings;
using Sqlbi.Bravo.Infrastructure.Diagnostics;
using Sqlbi.Bravo.Infrastructure.Services;

namespace Sqlbi.Bravo.Host;
Expand Down Expand Up @@ -81,6 +82,8 @@ private static void ConfigureLogging(WebApplicationBuilder builder)
builder.Logging.AddEventSourceLogger();
builder.Logging.AddEventLog();
builder.Logging.AddFilter<EventLogLoggerProvider>((level) => level >= LogLevel.Warning);
builder.Logging.AddDiagnosticMessages();
builder.Logging.AddFilter<DiagnosticMessageLoggerProvider>((level) => level >= LogLevel.Warning);
#if DEBUG
builder.Logging.AddConsole();
builder.Logging.AddDebug();
Expand Down
53 changes: 53 additions & 0 deletions src/Infrastructure/Diagnostics/DiagnosticMessageLogger.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
using System;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Logging;
using Sqlbi.Bravo.Models;

namespace Sqlbi.Bravo.Infrastructure.Diagnostics;

internal sealed class DiagnosticMessageLogger(string categoryName) : ILogger
{
private readonly string _categoryName = categoryName;

public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;

public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None;

public void Log<TState>(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func<TState, Exception?, string> formatter)
{
var content = formatter(state, exception);

if (exception is not null)
content = $"{content}{Environment.NewLine}{exception}";

try
{
AppEnvironment.AddDiagnostics(DiagnosticMessageType.Text, _categoryName, content, GetSeverity(logLevel));
}
catch (Exception)
{
// A sink failure does not propagate to the caller of ILogger.Log.
}
}

private static DiagnosticMessageSeverity GetSeverity(LogLevel logLevel)
{
return logLevel switch
{
LogLevel.Critical or LogLevel.Error => DiagnosticMessageSeverity.Error,
LogLevel.Warning => DiagnosticMessageSeverity.Warning,
_ => DiagnosticMessageSeverity.None,
};
}
}

internal static class DiagnosticMessageLoggerExtensions
{
public static ILoggingBuilder AddDiagnosticMessages(this ILoggingBuilder builder)
{
builder.Services.TryAddEnumerable(ServiceDescriptor.Singleton<ILoggerProvider, DiagnosticMessageLoggerProvider>());

return builder;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using Microsoft.Extensions.Logging;

namespace Sqlbi.Bravo.Infrastructure.Diagnostics;

/// <summary>
/// Writes log records to <see cref="AppEnvironment.Diagnostics"/>, the channel the user interface polls.
/// </summary>
[ProviderAlias("Diagnostics")]
internal sealed class DiagnosticMessageLoggerProvider : ILoggerProvider
{
public ILogger CreateLogger(string categoryName) => new DiagnosticMessageLogger(categoryName);

public void Dispose()
{
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
using System;
using System.Linq;
using Microsoft.Extensions.Logging;
using Sqlbi.Bravo.Infrastructure;
using Sqlbi.Bravo.Infrastructure.Diagnostics;
using Sqlbi.Bravo.Models;
using Xunit;

namespace Bravo.Tests.Infrastructure.Diagnostics;

public class DiagnosticMessageLoggerProviderTests
{
[Theory]
[InlineData(LogLevel.Critical, DiagnosticMessageSeverity.Error)]
[InlineData(LogLevel.Error, DiagnosticMessageSeverity.Error)]
[InlineData(LogLevel.Warning, DiagnosticMessageSeverity.Warning)]
[InlineData(LogLevel.Information, DiagnosticMessageSeverity.None)]
public void Log_MapsTheLevelToTheMessageSeverity(LogLevel logLevel, DiagnosticMessageSeverity expected)
{
var category = $"{nameof(Log_MapsTheLevelToTheMessageSeverity)}.{logLevel}";
using var provider = new DiagnosticMessageLoggerProvider();
var logger = provider.CreateLogger(category);

logger.Log(logLevel, default, "the message", null, (state, _) => state);

var message = FindMessage(category);
Assert.Equal(expected, message.Severity);
Assert.Equal(DiagnosticMessageType.Text, message.Type);
Assert.Equal("the message", message.Content);
}

[Fact]
public void Log_AppendsTheExceptionToTheContent()
{
var category = nameof(Log_AppendsTheExceptionToTheContent);
var exception = new InvalidOperationException("policies unavailable");
using var provider = new DiagnosticMessageLoggerProvider();
var logger = provider.CreateLogger(category);

logger.LogError(exception, "the message");

var message = FindMessage(category);
Assert.StartsWith("the message", message.Content);
Assert.Contains("policies unavailable", message.Content);
}

private static DiagnosticMessage FindMessage(string category)
{
var message = AppEnvironment.Diagnostics.Values.SingleOrDefault((m) => m.Name == $"[HOST] {category}");

Assert.NotNull(message);
Assert.NotNull(message.Content);

return message;
}
}
Loading