Add Scoped Plugin Log Service (#1341)

Adds a new `IPluginLog` service to Dalamud, which provides scoped logging on a per-plugin basis. This improves log performance for plugins, and paves the way for per-plugin log levels.

* Plugins must opt in to enable verbose logging by setting `IPluginLog.MinimumLogLevel` to `LogEventLevel.Verbose`. This option is automatically enabled for dev plugins and is currently not persisted.
    * All release plugins will default to `Debug` as their lowest allowed log level.
    * This setting does not override the global log level set in Dalamud.
This commit is contained in:
KazWolfe 2023-09-07 10:58:41 -07:00 committed by GitHub
parent 1dbf93e428
commit 8c51bbf0f8
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
4 changed files with 263 additions and 67 deletions

View file

@ -1,9 +1,6 @@
using System;
using System.Reflection;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Types;
using Serilog;
using Serilog.Events;
@ -12,29 +9,9 @@ namespace Dalamud.Logging;
/// <summary>
/// Class offering various static methods to allow for logging in plugins.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
public class PluginLog : IServiceType, IDisposable
public static class PluginLog
{
private readonly LocalPlugin plugin;
/// <summary>
/// Initializes a new instance of the <see cref="PluginLog"/> class.
/// Do not use this ctor, inject PluginLog instead.
/// </summary>
/// <param name="plugin">The plugin this service is scoped for.</param>
internal PluginLog(LocalPlugin plugin)
{
this.plugin = plugin;
}
/// <summary>
/// Gets or sets a prefix appended to log messages.
/// </summary>
public string? LogPrefix { get; set; } = null;
#region Legacy static "Log" prefixed Serilog style methods
#region "Log" prefixed Serilog style methods
/// <summary>
/// Log a templated message to the in-game debug log.
@ -157,7 +134,7 @@ public class PluginLog : IServiceType, IDisposable
#endregion
#region Legacy static Serilog style methods
#region Serilog style methods
/// <summary>
/// Log a templated verbose message to the in-game debug log.
@ -277,25 +254,6 @@ public class PluginLog : IServiceType, IDisposable
public static void LogRaw(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values)
=> WriteLog(Assembly.GetCallingAssembly().GetName().Name, level, messageTemplate, exception, values);
/// <inheritdoc/>
void IDisposable.Dispose()
{
// ignored
}
#region New instanced methods
/// <summary>
/// Log some information.
/// </summary>
/// <param name="message">The message.</param>
internal void Information(string message)
{
Serilog.Log.Information($"[{this.plugin.InternalName}] {this.LogPrefix} {message}");
}
#endregion
private static ILogger GetPluginLogger(string? pluginName)
{
return Serilog.Log.ForContext("SourceContext", pluginName ?? string.Empty);
@ -314,24 +272,3 @@ public class PluginLog : IServiceType, IDisposable
values);
}
}
/// <summary>
/// Class offering logging services, for a specific type.
/// </summary>
/// <typeparam name="T">The type to log for.</typeparam>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
public class PluginLog<T> : PluginLog
{
/// <summary>
/// Initializes a new instance of the <see cref="PluginLog{T}"/> class.
/// Do not use this ctor, inject PluginLog instead.
/// </summary>
/// <param name="plugin">The plugin this service is scoped for.</param>
internal PluginLog(LocalPlugin plugin)
: base(plugin)
{
this.LogPrefix = typeof(T).Name;
}
}

View file

@ -0,0 +1,130 @@
using System;
using Dalamud.IoC;
using Dalamud.IoC.Internal;
using Dalamud.Plugin.Internal.Types;
using Dalamud.Plugin.Services;
using Serilog;
using Serilog.Core;
using Serilog.Events;
namespace Dalamud.Logging;
/// <summary>
/// Implementation of <see cref="IPluginLog"/>.
/// </summary>
[PluginInterface]
[InterfaceVersion("1.0")]
[ServiceManager.ScopedService]
#pragma warning disable SA1015
[ResolveVia<IPluginLog>]
#pragma warning restore SA1015
public class ScopedPluginLogService : IServiceType, IPluginLog, IDisposable
{
private readonly LocalPlugin localPlugin;
private readonly LoggingLevelSwitch levelSwitch;
/// <summary>
/// Initializes a new instance of the <see cref="ScopedPluginLogService"/> class.
/// </summary>
/// <param name="localPlugin">The plugin that owns this service.</param>
internal ScopedPluginLogService(LocalPlugin localPlugin)
{
this.localPlugin = localPlugin;
this.levelSwitch = new LoggingLevelSwitch(this.GetDefaultLevel());
var loggerConfiguration = new LoggerConfiguration()
.Enrich.WithProperty("Dalamud.PluginName", localPlugin.InternalName)
.MinimumLevel.ControlledBy(this.levelSwitch)
.WriteTo.Logger(Log.Logger);
this.Logger = loggerConfiguration.CreateLogger();
}
/// <inheritdoc />
public LogEventLevel MinimumLogLevel
{
get => this.levelSwitch.MinimumLevel;
set => this.levelSwitch.MinimumLevel = value;
}
/// <inheritdoc />
public ILogger Logger { get; }
/// <inheritdoc />
public void Dispose()
{
GC.SuppressFinalize(this);
}
/// <inheritdoc />
public void Fatal(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Fatal, null, messageTemplate, values);
/// <inheritdoc />
public void Fatal(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Fatal, exception, messageTemplate, values);
/// <inheritdoc />
public void Error(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Error, null, messageTemplate, values);
/// <inheritdoc />
public void Error(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Error, exception, messageTemplate, values);
/// <inheritdoc />
public void Warning(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Warning, null, messageTemplate, values);
/// <inheritdoc />
public void Warning(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Warning, exception, messageTemplate, values);
/// <inheritdoc />
public void Information(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Information, null, messageTemplate, values);
/// <inheritdoc />
public void Information(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Information, exception, messageTemplate, values);
/// <inheritdoc />
public void Debug(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Debug, null, messageTemplate, values);
/// <inheritdoc />
public void Debug(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Debug, exception, messageTemplate, values);
/// <inheritdoc />
public void Verbose(string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Verbose, null, messageTemplate, values);
/// <inheritdoc />
public void Verbose(Exception? exception, string messageTemplate, params object[] values) =>
this.Write(LogEventLevel.Verbose, exception, messageTemplate, values);
/// <inheritdoc />
public void Write(LogEventLevel level, Exception? exception, string messageTemplate, params object[] values)
{
this.Logger.Write(
level,
exception: exception,
messageTemplate: $"[{this.localPlugin.InternalName}] {messageTemplate}",
values);
}
/// <summary>
/// Gets the default log level for this plugin.
/// </summary>
/// <returns>A log level.</returns>
private LogEventLevel GetDefaultLevel()
{
// TODO: Add some way to save log levels to a config. Or let plugins handle it?
return this.localPlugin.IsDev ? LogEventLevel.Verbose : LogEventLevel.Debug;
}
}