From a02ca90ad7eafdbe75ecbe49dadf126029e1fad8 Mon Sep 17 00:00:00 2001 From: phillip-haydon Date: Mon, 9 Dec 2024 07:55:34 +1300 Subject: [PATCH] Implementation of mslogger and sample projects --- .editorconfig | 13 +- .../ApplicationBuilderExtensions.cs | 6 +- .../RaygunAspNetCoreResponseMessageBuilder.cs | 11 +- .../Builders/RequestDataBuilder.cs | 27 +++ .../RaygunClient.cs | 7 +- ...scape.Raygun4Net.Extensions.Logging.csproj | 4 +- .../README.md | 97 +++++++++-- .../RaygunLogger.cs | 163 +++++++++++++++--- .../RaygunLoggerExtensions.cs | 61 ++++++- .../RaygunLoggerProvider.cs | 48 +++++- .../RaygunLoggerSettings.cs | 25 +++ .../IMessageBuilder.cs | 16 ++ .../RaygunClientBase.cs | 20 ++- .../Model/FakeRaygunClient.cs | 6 +- .../ApplicationBuilderExtensions.cs | 4 +- Mindscape.Raygun4Net.NetCore/RaygunClient.cs | 9 +- Raygun.CrashReporting.sln | 1 + .../Controllers/HomeController.cs | 39 ++++- .../Program.cs | 10 +- Raygun4Net.MSLogger.Service.Tests/Program.cs | 4 +- 20 files changed, 484 insertions(+), 87 deletions(-) create mode 100644 Mindscape.Raygun4Net.AspNetCore/Builders/RequestDataBuilder.cs create mode 100644 Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerSettings.cs create mode 100644 Mindscape.Raygun4Net.NetCore.Common/IMessageBuilder.cs diff --git a/.editorconfig b/.editorconfig index bbf3e21f..90512efc 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,14 @@ [*] indent_style = space -indent_size = 2 \ No newline at end of file +indent_size = 2 + +[*.cs] +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/style-rules/ide0290 +csharp_style_prefer_primary_constructors = false +dotnet_diagnostic.IDE0290.severity = none + +# https://www.jetbrains.com/help/rider/ConvertToPrimaryConstructor.html +resharper_convert_to_primary_constructor_highlighting = none + +# Ensure if statements always use braces +csharp_prefer_braces = true:error diff --git a/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs b/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs index b7cda147..1a921013 100644 --- a/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs +++ b/Mindscape.Raygun4Net.AspNetCore/ApplicationBuilderExtensions.cs @@ -7,6 +7,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.Logging; +using Mindscape.Raygun4Net.AspNetCore.Builders; namespace Mindscape.Raygun4Net.AspNetCore; @@ -51,7 +52,7 @@ public static IServiceCollection AddRaygun(this IServiceCollection services, ICo options?.Invoke(settings); services.TryAddSingleton(settings); - services.TryAddSingleton(s => new RaygunClient(s.GetService()!, s.GetService()!)); + services.TryAddSingleton(s => new RaygunClient(s.GetRequiredService(), s.GetRequiredService(), s.GetServices())); services.TryAddSingleton(provider => provider.GetRequiredService()); services.AddHttpContextAccessor(); @@ -69,8 +70,9 @@ public static IServiceCollection AddRaygun(this IServiceCollection services, Act // Override settings with user-provided settings options?.Invoke(settings); + services.TryAddSingleton(); services.TryAddSingleton(settings); - services.TryAddSingleton(s => new RaygunClient(s.GetService()!, s.GetService()!)); + services.TryAddSingleton(s => new RaygunClient(s.GetRequiredService(), s.GetRequiredService(), s.GetServices())); services.TryAddSingleton(provider => provider.GetRequiredService()); services.AddHttpContextAccessor(); diff --git a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs index f1587822..e35ac0dd 100644 --- a/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs +++ b/Mindscape.Raygun4Net.AspNetCore/Builders/RaygunAspNetCoreResponseMessageBuilder.cs @@ -1,5 +1,6 @@ #nullable enable +using System.Threading.Tasks; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; @@ -8,19 +9,19 @@ namespace Mindscape.Raygun4Net.AspNetCore.Builders // ReSharper disable once ClassNeverInstantiated.Global public class RaygunAspNetCoreResponseMessageBuilder { - public static RaygunResponseMessage Build(HttpContext? context) + public static Task Build(HttpContext? context, RaygunSettings _) { if (context == null) { - return new RaygunResponseMessage(); + return Task.FromResult(new RaygunResponseMessage()); } - + var httpResponseFeature = context.Features.Get(); - return new RaygunResponseMessage + return Task.FromResult(new RaygunResponseMessage { StatusCode = context.Response.StatusCode, StatusDescription = httpResponseFeature?.ReasonPhrase - }; + }); } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/Builders/RequestDataBuilder.cs b/Mindscape.Raygun4Net.AspNetCore/Builders/RequestDataBuilder.cs new file mode 100644 index 00000000..d4342d2d --- /dev/null +++ b/Mindscape.Raygun4Net.AspNetCore/Builders/RequestDataBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; + +namespace Mindscape.Raygun4Net.AspNetCore.Builders; + +public class RequestDataBuilder : IMessageBuilder +{ + private readonly IHttpContextAccessor _httpContextAccessor; + private readonly RaygunSettings _settings; + + public RequestDataBuilder(IHttpContextAccessor httpContextAccessor, RaygunSettings settings) + { + _httpContextAccessor = httpContextAccessor; + _settings = settings; + } + + public async Task Apply(RaygunMessage message, Exception exception) + { + var ctx = _httpContextAccessor.HttpContext; + + message.Details.Request = await RaygunAspNetCoreRequestMessageBuilder.Build(ctx, _settings); + message.Details.Response = await RaygunAspNetCoreResponseMessageBuilder.Build(ctx, _settings); + + return message; + } +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs index 36c12ad3..d5427ffc 100644 --- a/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.AspNetCore/RaygunClient.cs @@ -29,11 +29,11 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(setti { } - public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider) + public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider, IEnumerable messageBuilders) : base(settings, userProvider, messageBuilders) { } - public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider, []) { } // ReSharper restore MemberCanBeProtected.Global @@ -58,6 +58,7 @@ protected override bool CanSend(RaygunMessage? message) return !settings.ExcludedStatusCodes.Contains(message.Details.Response.StatusCode); } + /// /// Asynchronously transmits an exception to Raygun with optional Http Request data. /// @@ -72,7 +73,7 @@ public async Task SendInBackground(Exception exception, IList tags, Http // otherwise it will be disposed while we are using it on the other thread. // BuildRequestMessage relies on ReadFormAsync, so we need to await it to ensure it's processed before continuing. var currentRequestMessage = await RaygunAspNetCoreRequestMessageBuilder.Build(context, Settings.Value); - var currentResponseMessage = RaygunAspNetCoreResponseMessageBuilder.Build(context); + var currentResponseMessage = await RaygunAspNetCoreResponseMessageBuilder.Build(context, Settings.Value); var exceptions = StripWrapperExceptions(exception); diff --git a/Mindscape.Raygun4Net.Extensions.Logging/Mindscape.Raygun4Net.Extensions.Logging.csproj b/Mindscape.Raygun4Net.Extensions.Logging/Mindscape.Raygun4Net.Extensions.Logging.csproj index 8e09276f..c165e4f1 100644 --- a/Mindscape.Raygun4Net.Extensions.Logging/Mindscape.Raygun4Net.Extensions.Logging.csproj +++ b/Mindscape.Raygun4Net.Extensions.Logging/Mindscape.Raygun4Net.Extensions.Logging.csproj @@ -1,7 +1,7 @@ - netstandard2.0;netstandard2.1;net6.0;net7.0;net8.0 + netstandard2.0;net6.0;net7.0;net8.0 enable enable @@ -25,10 +25,8 @@ - - diff --git a/Mindscape.Raygun4Net.Extensions.Logging/README.md b/Mindscape.Raygun4Net.Extensions.Logging/README.md index cb658c52..ac12900b 100644 --- a/Mindscape.Raygun4Net.Extensions.Logging/README.md +++ b/Mindscape.Raygun4Net.Extensions.Logging/README.md @@ -7,29 +7,98 @@ where they can be viewed and managed in the Raygun dashboard. Install the **Mindscape.Raygun4Net.Extensions.Logging** NuGet package into your project. You can either use the below dotnet CLI command, or the NuGet management GUI in the IDE you use. -``` +```csharp dotnet add package Mindscape.Raygun4Net.Extensions.Logging ``` +You will need to install the **Mindscape.Raygun4Net** package as well, if you haven't already. + +```csharp +// If you're using it in an ASP.NET Core application: +dotnet add package Mindscape.Raygun4Net.AspNetCore + +// If you're using it in a .NET Core service application: +dotnet add package Mindscape.Raygun4Net.NetCore +``` + ## Usage Add the Raygun provider to the logging configuration in your `Program.cs` or `Startup.cs` file. +### ASP.NET Core Application + ```csharp -using Microsoft.Extensions.Logging; +using Mindscape.Raygun4Net.AspNetCore; using Mindscape.Raygun4Net.Extensions.Logging; -public class Program +var builder = WebApplication.CreateBuilder(args); + +// Registers the Raygun Client for AspNetCore +builder.Services.AddRaygun(settings => +{ + settings.ApiKey = "*your api key*"; +}); + +// (Optional) Registers the Raygun User Provider +builder.Services.AddRaygunUserProvider(); + +// Registers the Raygun Logger for use in MS Logger +builder.Logging.AddRaygunLogger(); + +var app = builder.Build(); +``` + +### .NET Core Service Application + +```csharp +using Mindscape.Raygun4Net.Extensions.Logging; +using Mindscape.Raygun4Net.NetCore; + +var builder = Host.CreateApplicationBuilder(args); + +// Registers the Raygun Client for NetCore +builder.Services.AddRaygun(options => { - public static void Main(string[] args) - { - var loggerFactory = LoggerFactory.Create(builder => - { - builder.AddRaygun("paste_your_api_key_here"); - }); - - var logger = loggerFactory.CreateLogger(); - logger.LogInformation("Hello, Raygun!"); - } + options.ApiKey = "*your api key*"; +}); + +// Registers the Raygun Logger for use in MS Logger +builder.Logging.AddRaygunLogger(); + +var host = builder.Build(); +``` + +## Configuration + +When registering the Raygun provider, you can configure it with the following options: + +* MinimumLogLevel: The minimum log level for messages to be sent to Raygun. Defaults to `LogLevel.Error`. +* OnlyLogExceptions: If false, logs without exceptions will be sent to Raygun. Defaults to `true`. + +These can be set in code: + +```csharp +builder.Logging.AddRaygunLogger(options: options => +{ + options.MinimumLogLevel = LogLevel.Information; + options.OnlyLogExceptions = false; +}); +``` + +Or in the `appsettings.json` file: + +```json +{ + "RaygunSettings": { + "MinimumLogLevel": "Information", + "OnlyLogExceptions": false + } } -``` \ No newline at end of file +``` + +## Notes + +The category/contextSource set as a tag in Raygun. + +When logs are sent without an exception, a Custom Data property is added to the Raygun message with the +key `NullException` with the value `Logged without exception` to identify Raygun Logs that have no exception. diff --git a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLogger.cs b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLogger.cs index df48f19f..9e64faa8 100644 --- a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLogger.cs +++ b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLogger.cs @@ -2,56 +2,173 @@ namespace Mindscape.Raygun4Net.Extensions.Logging; +/// +/// Implementation of ILogger that sends logs to Raygun. Supports structured logging, +/// async/sync sending based on log level, and logging scopes. +/// public class RaygunLogger : ILogger { private readonly string _category; private readonly RaygunClientBase _client; private readonly RaygunLoggerSettings _settings; + private readonly AsyncLocal> _scopeData = new(); + /// + /// Initializes a new instance of the RaygunLogger. + /// + /// The category name for the logger. + /// The Raygun client used to send logs. + /// Configuration settings for the logger. + /// Thrown when category, client, or settings is null. public RaygunLogger(string category, RaygunClientBase client, RaygunLoggerSettings settings) { - _category = category; - _client = client; - _settings = settings; + _category = category ?? throw new ArgumentNullException(nameof(category)); + _client = client ?? throw new ArgumentNullException(nameof(client)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); } + /// + /// Writes a log entry to Raygun. + /// + /// The type of the object to be written. + /// Entry will be written on this level. + /// Id of the event. + /// The entry to be written. Can be also an object. + /// The exception related to this entry. + /// Function to create a string message of the state and exception. + /// + /// If the log level is LogLevel.Critical, the log will be sent synchronously.
+ /// If the log level is LogLevel.Error, LogLevel.Warning, LogLevel.Information, or LogLevel.Debug, the log will be sent asynchronously.
+ /// If the log level is below the LogLevel setting, or the OnlyLogExceptions is true, the log will be ignored. + ///
public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) { - if (exception == null || !IsEnabled(logLevel)) + if (!IsEnabled(logLevel) || (exception == null && _settings.OnlyLogExceptions)) { return; } + + var message = formatter?.Invoke(state, exception); + var tags = new List { _category }; + var customData = new Dictionary + { + ["Message"] = message ?? string.Empty, + ["EventId"] = eventId.ToString(), + ["LogLevel"] = logLevel.ToString() + }; + + if (exception == null) + { + customData["NullException"] = "Logged without exception"; + } - if (logLevel <= LogLevel.Error) + // Add scope data if available + if (_scopeData.Value?.Count > 0) { - _ = _client.SendInBackground(exception, new List + foreach (var item in _scopeData.Value) { - _category - }, new Dictionary - { - ["Message"] = formatter.Invoke(state, exception) - }); - } else if (logLevel == LogLevel.Critical) + customData[$"Scope{item.Key}"] = item.Value.ToString() ?? string.Empty; + } + } + + var ex = exception ?? new Exception(message); + + if (logLevel == LogLevel.Critical) { - _client.SendInBackground(exception, new List - { - _category - }, new Dictionary - { - ["Message"] = formatter.Invoke(state, exception) - // Force blocking call for critical exceptions to ensure they are logged as the application has potentially crashed. - }).ConfigureAwait(false).GetAwaiter().GetResult(); + // For critical errors, send synchronously + SendLogSync(ex, tags, customData); + } + else + { + // For other levels, send asynchronously + _ = SendLogAsync(ex, tags, customData); + } + } + + private async Task SendLogAsync(Exception? exception, List tags, Dictionary customData) + { + try + { + await _client.SendInBackground(exception, tags, customData); + } + catch (Exception ex) + { + // Log the failure to send to Raygun - you might want to use a fallback logger here + System.Diagnostics.Debug.WriteLine($"Failed to send log to Raygun: {ex}"); + } + } + + private void SendLogSync(Exception? exception, List tags, Dictionary customData) + { + try + { + _client.SendInBackground(exception, tags, customData) + .ConfigureAwait(false) + .GetAwaiter() + .GetResult(); + } + catch (Exception ex) + { + // Log the failure to send to Raygun - you might want to use a fallback logger here + System.Diagnostics.Debug.WriteLine($"Failed to send log to Raygun: {ex}"); } } public bool IsEnabled(LogLevel logLevel) { - return logLevel >= _settings.LogLevel; + return logLevel >= _settings.MinimumLogLevel; } + /// + /// Begins a new logging scope. Scopes can be nested and are stored per-async-context. + /// + /// The type of the state to begin scope for. + /// The state to begin scope for. + /// An IDisposable that ends the scope when disposed. + /// Thrown when state is null. public IDisposable BeginScope(TState state) { - // huh... - return null; + if (state == null) + { + throw new ArgumentNullException(nameof(state)); + } + + var scopeData = _scopeData.Value; + if (scopeData == null) + { + scopeData = new Dictionary(); + _scopeData.Value = scopeData; + } + + // Handle different types of state + switch (state) + { + case IEnumerable> properties: + foreach (var prop in properties) + { + scopeData[$"[{scopeData.Count}].{prop.Key}"] = prop.Value; + } + break; + default: + scopeData[$"[{scopeData.Count}].Unnamed"] = state; + break; + } + + return new RaygunLoggerScope(_scopeData); + } + + private class RaygunLoggerScope : IDisposable + { + private readonly AsyncLocal> _scopeData; + + public RaygunLoggerScope(AsyncLocal> scopeData) + { + _scopeData = scopeData; + } + + public void Dispose() + { + _scopeData.Value?.Clear(); + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerExtensions.cs b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerExtensions.cs index 48f1d47f..5af9c511 100644 --- a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerExtensions.cs +++ b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerExtensions.cs @@ -7,7 +7,29 @@ namespace Mindscape.Raygun4Net.Extensions.Logging; public static class RaygunLoggerExtensions { - public static ILoggingBuilder AddRaygunLogger(this ILoggingBuilder builder, IConfiguration? configuration = null, Action? options = null) + /// + /// Adds Raygun logging capabilities to the logging builder with support for configuration and custom settings. + /// + /// The ILoggingBuilder instance to add Raygun logging to. + /// The configuration instance containing Raygun settings under the "RaygunSettings" section. + /// Optional delegate to configure additional Raygun logger settings programmatically. + /// The ILoggingBuilder instance for method chaining. + /// + /// This method configures Raygun logging with the following precedence:
+ /// 1. Default settings
+ /// 2. Configuration from the "RaygunSettings" section (if provided)
+ /// 3. Programmatic options (if provided)
+ ///
+ /// Example usage:
+ /// + /// builder.AddRaygunLogger(configuration, options => + /// { + /// options.MinimumLogLevel = LogLevel.Information; + /// options.OnlyLogExceptions = false; + /// }); + /// + ///
+ public static ILoggingBuilder AddRaygunLogger(this ILoggingBuilder builder, IConfiguration? configuration, Action? options = null) { // Since we are not using IConfiguration, we need to create a new instance of RaygunSettings var settings = new RaygunLoggerSettings(); @@ -19,15 +41,40 @@ public static ILoggingBuilder AddRaygunLogger(this ILoggingBuilder builder, ICon options?.Invoke(settings); builder.Services.TryAddSingleton(settings); - - //builder.Services.TryAddSingleton(s => new RaygunClientBase(s.GetService()!, s.GetService()!)); builder.Services.AddSingleton(); return builder; } -} + + /// + /// Adds Raygun logging capabilities to the logging builder with programmatic configuration only. + /// + /// The ILoggingBuilder instance to add Raygun logging to. + /// Optional delegate to configure Raygun logger settings programmatically. + /// The ILoggingBuilder instance for method chaining. + /// + /// This overload is useful when you want to configure Raygun logging entirely through code without using an IConfiguration instance.
+ ///
+ /// Example usage:
+ /// + /// builder.AddRaygunLogger(options => + /// { + /// options.MinimumLogLevel = LogLevel.Information; + /// options.OnlyLogExceptions = false; + /// }); + /// + ///
+ public static ILoggingBuilder AddRaygunLogger(this ILoggingBuilder builder, Action? options = null) + { + // Since we are not using IConfiguration, we need to create a new instance of RaygunSettings + var settings = new RaygunLoggerSettings(); -public class RaygunLoggerSettings -{ - public LogLevel LogLevel { get; set; } = LogLevel.Error; + // Override settings with user-provided settings + options?.Invoke(settings); + + builder.Services.TryAddSingleton(settings); + builder.Services.AddSingleton(); + + return builder; + } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerProvider.cs b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerProvider.cs index e3c6e040..83465ae2 100644 --- a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerProvider.cs +++ b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerProvider.cs @@ -1,25 +1,59 @@ -using Microsoft.Extensions.Logging; +using System.Collections.Concurrent; +using Microsoft.Extensions.Logging; namespace Mindscape.Raygun4Net.Extensions.Logging; -public class RaygunLoggerProvider : ILoggerProvider +/// +/// Provides Raygun logging capabilities by implementing ILoggerProvider. +/// Creates and manages RaygunLogger instances for different categories. +/// +public sealed class RaygunLoggerProvider : ILoggerProvider { - //private readonly RaygunLoggerOptions _config; private readonly RaygunClientBase _client; private readonly RaygunLoggerSettings _settings; + private readonly ConcurrentDictionary _loggers; + private bool _disposed; + /// + /// Initializes a new instance of the RaygunLoggerProvider. + /// + /// The Raygun client used to send logs. + /// Configuration settings for the logger. + /// Thrown when client or settings is null. public RaygunLoggerProvider(RaygunClientBase client, RaygunLoggerSettings settings) { - _client = client; - _settings = settings; + _client = client ?? throw new ArgumentNullException(nameof(client)); + _settings = settings ?? throw new ArgumentNullException(nameof(settings)); + _loggers = new ConcurrentDictionary(); } + /// + /// Creates or retrieves a RaygunLogger instance for the specified category. + /// + /// The category name for the logger. + /// An ILogger instance configured for the specified category. + /// Thrown when the provider has been disposed. public ILogger CreateLogger(string categoryName) { - return new RaygunLogger(categoryName, _client, _settings); - } + if (_disposed) + { + throw new ObjectDisposedException(nameof(RaygunLoggerProvider)); + } + return _loggers.GetOrAdd(categoryName, CreateLoggerInternal); + + RaygunLogger CreateLoggerInternal(string name) + { + return new RaygunLogger(name, _client, _settings); + } + } + public void Dispose() { + if (!_disposed) + { + _disposed = true; + _loggers.Clear(); + } } } \ No newline at end of file diff --git a/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerSettings.cs b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerSettings.cs new file mode 100644 index 00000000..6eb874d4 --- /dev/null +++ b/Mindscape.Raygun4Net.Extensions.Logging/RaygunLoggerSettings.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Logging; + +namespace Mindscape.Raygun4Net.Extensions.Logging; + +public class RaygunLoggerSettings +{ + /// + /// Specifies the minimum log level that will be processed by the Raygun logger. + /// + /// + /// LogLevel determines the severity of logs that are allowed to be emitted by the logger. + /// Messages with a severity level below this setting will be ignored. + /// The default value is LogLevel.Error. + /// + public LogLevel MinimumLogLevel { get; set; } = LogLevel.Error; + + /// + /// Determines whether only exceptions should be logged to Raygun. + /// + /// + /// When set to true, only exceptions are sent to Raygun. Other log messages are ignored by the logger. + /// Default value is true. + /// + public bool OnlyLogExceptions { get; set; } = true; +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/IMessageBuilder.cs b/Mindscape.Raygun4Net.NetCore.Common/IMessageBuilder.cs new file mode 100644 index 00000000..4e74ba6e --- /dev/null +++ b/Mindscape.Raygun4Net.NetCore.Common/IMessageBuilder.cs @@ -0,0 +1,16 @@ +#nullable enable + +using System; +using System.Threading.Tasks; + +namespace Mindscape.Raygun4Net; + +/// +/// Interface that can be implemented to modify the RaygunMessage before it is sent to Raygun. +/// These should be registered as a Singleton in the DI container as the RaygunClient will only +/// use a single instance of each. +/// +public interface IMessageBuilder +{ + Task Apply(RaygunMessage message, Exception exception); +} \ No newline at end of file diff --git a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs index 20dae188..de00e374 100644 --- a/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs +++ b/Mindscape.Raygun4Net.NetCore.Common/RaygunClientBase.cs @@ -23,6 +23,8 @@ public abstract class RaygunClientBase // The default timeout is 100 seconds for the HttpClient, Timeout = TimeSpan.FromSeconds(30) }; + + private IEnumerable _messageBuilders = []; /// /// This is the HttpClient that will be used to send messages to the Raygun endpoint. @@ -103,23 +105,25 @@ private void OnApplicationUnhandledException(Exception exception, bool isTermina } protected RaygunClientBase(RaygunSettingsBase settings) - : this(settings, DefaultClient, null) + : this(settings, DefaultClient, null, []) { } // ReSharper disable once IntroduceOptionalParameters.Global protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client) - : this(settings, client, null) + : this(settings, client, null, []) { } - protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider userProvider) - : this(settings, DefaultClient, userProvider) + protected RaygunClientBase(RaygunSettingsBase settings, IRaygunUserProvider userProvider, IEnumerable messageBuilders) + : this(settings, DefaultClient, userProvider, messageBuilders) { } - protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider) + protected RaygunClientBase(RaygunSettingsBase settings, HttpClient client, IRaygunUserProvider userProvider, IEnumerable messageBuilders) { + _messageBuilders = messageBuilders ?? []; + _client = client ?? DefaultClient; _settings = settings; _backgroundMessageProcessor = new ThrottledBackgroundMessageProcessor(settings.BackgroundMessageQueueMax, _settings.BackgroundMessageWorkerCount, _settings.BackgroundMessageWorkerBreakpoint, Send); @@ -438,6 +442,12 @@ protected async Task BuildMessage(Exception exception, var customGroupingKey = await OnCustomGroupingKey(exception, message).ConfigureAwait(false); + // TODO - Move old message builders to IMessageBuilder + foreach (var builder in _messageBuilders) + { + await builder.Apply(message, exception); + } + if (string.IsNullOrEmpty(customGroupingKey) == false) { message.Details.GroupingKey = customGroupingKey; diff --git a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs index cd2002ae..282f3ae1 100644 --- a/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore.Tests/Model/FakeRaygunClient.cs @@ -7,15 +7,15 @@ namespace Mindscape.Raygun4Net.NetCore.Tests { public class FakeRaygunClient : RaygunClient { - public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty }, null, null) + public FakeRaygunClient() : base(new RaygunSettings { ApiKey = string.Empty }, null, []) { } - public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey }, null, null) + public FakeRaygunClient(string apiKey) : base(new RaygunSettings { ApiKey = apiKey }, null, []) { } - public FakeRaygunClient(RaygunSettings settings) : base(settings, null, null) + public FakeRaygunClient(RaygunSettings settings) : base(settings, null, []) { } diff --git a/Mindscape.Raygun4Net.NetCore/ApplicationBuilderExtensions.cs b/Mindscape.Raygun4Net.NetCore/ApplicationBuilderExtensions.cs index e06244d8..8b7a0ceb 100644 --- a/Mindscape.Raygun4Net.NetCore/ApplicationBuilderExtensions.cs +++ b/Mindscape.Raygun4Net.NetCore/ApplicationBuilderExtensions.cs @@ -23,7 +23,7 @@ public static IServiceCollection AddRaygun(this IServiceCollection services, ICo options?.Invoke(settings); services.TryAddSingleton(settings); - services.TryAddSingleton(s => new RaygunClient(s.GetService()!, s.GetService()!)); + services.TryAddSingleton(s => new RaygunClient(s.GetRequiredService(), s.GetRequiredService(), s.GetServices())); return services; } @@ -40,7 +40,7 @@ public static IServiceCollection AddRaygun(this IServiceCollection services, Act options?.Invoke(settings); services.TryAddSingleton(settings); - services.TryAddSingleton(s => new RaygunClient(s.GetService()!, s.GetService()!)); + services.TryAddSingleton(s => new RaygunClient(s.GetRequiredService(), s.GetRequiredService(), s.GetServices())); return services; } diff --git a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs index 8005318a..73877219 100644 --- a/Mindscape.Raygun4Net.NetCore/RaygunClient.cs +++ b/Mindscape.Raygun4Net.NetCore/RaygunClient.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Net.Http; namespace Mindscape.Raygun4Net; @@ -26,11 +27,15 @@ public RaygunClient(RaygunSettings settings, HttpClient httpClient) : base(setti { } - public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider) + public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider) : base(settings, userProvider, []) { } - public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider) + public RaygunClient(RaygunSettings settings, IRaygunUserProvider userProvider, IEnumerable messageBuilders) : base(settings, userProvider, messageBuilders) + { + } + + public RaygunClient(RaygunSettings settings, HttpClient httpClient, IRaygunUserProvider userProvider) : base(settings, httpClient, userProvider, []) { } diff --git a/Raygun.CrashReporting.sln b/Raygun.CrashReporting.sln index f05583ae..b56fc949 100644 --- a/Raygun.CrashReporting.sln +++ b/Raygun.CrashReporting.sln @@ -51,6 +51,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "..Items", "..Items", "{1F1C pack-all.sh = pack-all.sh pack-all.bat = pack-all.bat pack-all.ps1 = pack-all.ps1 + .editorconfig = .editorconfig EndProjectSection EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Raygun4Net.AspNetCore.Tests", "Raygun4Net.AspNetCore.Tests\Raygun4Net.AspNetCore.Tests.csproj", "{D932B9F5-9F08-495B-AAC7-16CE44C74CA4}" diff --git a/Raygun4Net.MSLogger.AspNetCore.Tests/Controllers/HomeController.cs b/Raygun4Net.MSLogger.AspNetCore.Tests/Controllers/HomeController.cs index 63289c17..bfb63892 100644 --- a/Raygun4Net.MSLogger.AspNetCore.Tests/Controllers/HomeController.cs +++ b/Raygun4Net.MSLogger.AspNetCore.Tests/Controllers/HomeController.cs @@ -15,17 +15,46 @@ public HomeController(ILogger logger) public IActionResult Index() { - return View(); + throw new Exception("Banana was not yellow."); } - public IActionResult Privacy() + [HttpGet("test-2")] + public IActionResult ScopeNotCaptured() { - return View(); + // Because we throw and the capture happens outside the scope, the scope data is not captured. + using (_logger.BeginScope("Banana")) + using (_logger.BeginScope("User {Thing}", "Fred")) + using (_logger.BeginScope("Age {Age}", 30)) + { + throw new Exception("A scoped exception"); + } } - [ResponseCache(Duration = 0, Location = ResponseCacheLocation.None, NoStore = true)] + [HttpGet("test-3")] + public IActionResult ScopeCaptured() + { + // Because we catch the exception and the capture happens inside the scope, the scope data is captured. + using (_logger.BeginScope("Banana")) + using (_logger.BeginScope("User {Thing}", "Fred")) + using (_logger.BeginScope("Age {Age}", 30)) + { + try + { + throw new Exception("A scoped exception"); + } + catch (Exception e) + { + _logger.LogError(e, "An error occurred"); + return Problem(detail: "An error occurred", statusCode: 500); + } + } + } + + [HttpGet("test-4")] public IActionResult Error() { - return View(new ErrorViewModel { RequestId = Activity.Current?.Id ?? HttpContext.TraceIdentifier }); + _logger.Log(LogLevel.Information, "An error occurred, don't know what tho..."); + + return Content("Hello world", "text/plain"); } } \ No newline at end of file diff --git a/Raygun4Net.MSLogger.AspNetCore.Tests/Program.cs b/Raygun4Net.MSLogger.AspNetCore.Tests/Program.cs index 58949d4b..f1e37662 100644 --- a/Raygun4Net.MSLogger.AspNetCore.Tests/Program.cs +++ b/Raygun4Net.MSLogger.AspNetCore.Tests/Program.cs @@ -9,14 +9,18 @@ // Registers the Raygun Client for AspNetCore builder.Services.AddRaygun(settings => { - settings.ApiKey = "zqpKCLNE8SXj7aBfjZv98w"; + settings.ApiKey = "*your_api_key*"; }); // (Optional) Registers the Raygun User Provider builder.Services.AddRaygunUserProvider(); -// Registers the Raygun Logger for MS Logger -builder.Logging.AddRaygunLogger(); +// Registers the Raygun Logger for use in MS Logger +builder.Logging.AddRaygunLogger(x => +{ + x.OnlyLogExceptions = false; + x.MinimumLogLevel = LogLevel.Information; +}); var app = builder.Build(); diff --git a/Raygun4Net.MSLogger.Service.Tests/Program.cs b/Raygun4Net.MSLogger.Service.Tests/Program.cs index 8d5d1eca..ace2c6ac 100644 --- a/Raygun4Net.MSLogger.Service.Tests/Program.cs +++ b/Raygun4Net.MSLogger.Service.Tests/Program.cs @@ -9,13 +9,13 @@ // Registers the Raygun Client for NetCore builder.Services.AddRaygun(options => { - options.ApiKey = "zqpKCLNE8SXj7aBfjZv98w"; + options.ApiKey = "*your_api_key*"; }); // (Optional) Add Raygun User Provider - no default implementation provided in non ASP.NET projects //builder.Services.AddRaygunUserProvider<...>() -// Registers the Raygun Logger for MS Logger +// Registers the Raygun Logger for use in MS Logger builder.Logging.AddRaygunLogger(); var host = builder.Build();