Skip to content

Commit

Permalink
Made NullableLogger public.
Browse files Browse the repository at this point in the history
  • Loading branch information
CptMoore committed Dec 24, 2024
1 parent 86feaa8 commit 7da1676
Show file tree
Hide file tree
Showing 9 changed files with 63 additions and 93 deletions.
2 changes: 2 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ Since v2, ModTek adheres to [Semantic Versioning](http://semver.org/) for runtim
For modders:
- (Experimental!) Added ability to run injectors as part of a build task outside of BT. API will most likely change.
- Some libraries were renamed, as always don't just copy-paste, clean-copy-paste!
- Moved the NullableLogger to the `ModTek.Public` namespace. The NullableLogger allows readable code when skipping
trace and debug logging, but otherwise it is equivalent to the HBS logger. Do not use if you don't need it.

## 4.2 - CptMoore

Expand Down
6 changes: 3 additions & 3 deletions ModTek/Features/Logging/HarmonyXLoggerAdapter.cs
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
using HBS.Logging;
using NullableLogging;
using ModTek.Public;
using Logger = HarmonyLib.Tools.Logger;

namespace ModTek.Features.Logging;

internal static class HarmonyXLoggerAdapter
{
private static readonly HBS.Logging.Logger.LogImpl s_logImpl = Log.HarmonyX.Log;
private static readonly ILog s_log = Log.HarmonyX.Log;
internal static void Setup()
{
Logger.MessageReceived += (_, args) =>
{
var level = MapHarmonyLogChannelToHbsLogLevel(args.LogChannel);
s_logImpl.LogAtLevel(level, args.Message);
s_log.LogAtLevel(level, args.Message);
};
}

Expand Down
2 changes: 1 addition & 1 deletion ModTek/Features/Logging/LinePrefixToFilterTransformer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
using System.Linq;
using System.Text.RegularExpressions;
using HBS.Logging;
using NullableLogging;
using ModTek.Public;

namespace ModTek.Features.Logging;

Expand Down
3 changes: 2 additions & 1 deletion ModTek/Features/Logging/LogLevelExtension.cs
Original file line number Diff line number Diff line change
Expand Up @@ -56,10 +56,11 @@ private static ELogLevels Convert(LogLevel level)
return (ELogLevels)intLevel;
}

internal const int TraceLogLevel = 200;
// log levels
private enum ELogLevels
{
Trace = 200, // (extended) trace steps and variables, usually slows down the game considerably
Trace = TraceLogLevel, // (extended) trace steps and variables, usually slows down the game considerably
Debug = 210, // also used to trace steps and variables, but at a reduced rate to keep the logfiles readable and performance ok
Log = 220, // minimal logs required to know what the user is doing in general, should have no impact on performance; user clicked x
Warning = 230, // something wrong happened, but the mod will deal with it; a wrong config value, which has a safe fallback
Expand Down
15 changes: 0 additions & 15 deletions ModTek/Features/Logging/LoggingSettings.cs
Original file line number Diff line number Diff line change
Expand Up @@ -45,26 +45,11 @@ internal class LoggingSettings
[JsonProperty]
internal bool LogStackTracesOnExceptions = true;

[JsonProperty]
internal const string IgnoreLoggerLogLevel_Description = "Each logger has a log level, and when logging below that level it won't be logged. That behavior can be ignored to a certain extend. Set to true for FYLS behavior, not recommended though.";
[JsonProperty]
internal bool IgnoreLoggerLogLevel;

[JsonProperty]
internal const string DebugLogLevelSetters_Description = "Log who changed a log level changed.";
[JsonProperty]
internal bool DebugLogLevelSetters;

[JsonProperty]
internal const string SkipOriginalLoggers_Description = "If true, the original (HBS based) loggers and therefore their appenders and log files will be skipped.";
[JsonProperty]
internal bool SkipOriginalLoggers = true;

[JsonProperty]
internal const string IgnoreSkipForLoggers_Description = "Loggers defined here will never be skipped, meaning their log files will still be separately available.";
[JsonProperty(Required = Required.DisallowNull)]
internal string[] IgnoreSkipForLoggers = {};

[JsonProperty]
internal const string UnityConsoleAppenderEnabled_Description = "Append HBS log statements to the unity console. Disabled by default as it reduces performance.";
[JsonProperty]
Expand Down
31 changes: 5 additions & 26 deletions ModTek/Features/Logging/Patches/LogImpl_LogAtLevel_Patch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -19,27 +19,9 @@ internal static class LogImpl_LogAtLevel_Patch
{
public static bool Prepare()
{
if (!ModTek.Enabled)
{
return false;
}

if (Settings == null)
{
Settings = ModTek.Config.Logging;
if (Settings.SkipOriginalLoggers)
{
IgnoreSkipForLoggers = Settings.IgnoreSkipForLoggers.ToHashSet();
}
}

return true;
return ModTek.Enabled;
}

private static LoggingSettings Settings;
private static HashSet<string> IgnoreSkipForLoggers;
private static bool SkipOriginalLoggers => IgnoreSkipForLoggers != null;

public static IEnumerable<CodeInstruction> Transpiler(IEnumerable<CodeInstruction> instructions)
{
var m = AccessTools.Field(typeof(Logger), nameof(Logger.IsUnityApplication));
Expand All @@ -62,7 +44,9 @@ public static bool Prefix(Logger.LogImpl __instance, string ___name, LogLevel le
{
try
{
if (Settings.IgnoreLoggerLogLevel || __instance.IsEnabledFor(level))
// TODO __instance.IsEnabledFor is slow, speedup requires to keep track of all loggers and/or children
// NullableLogger can skip this check
if (__instance.IsEnabledFor(level))
{
LoggingFeature.LogAtLevel(
___name,
Expand All @@ -72,12 +56,7 @@ public static bool Prefix(Logger.LogImpl __instance, string ___name, LogLevel le
location
);
}

var skipOriginal = SkipOriginalLoggers && !IgnoreSkipForLoggers.Contains(___name);
if (skipOriginal)
{
return false;
}
return false;
}
catch (Exception e)
{
Expand Down
2 changes: 1 addition & 1 deletion ModTek/Log.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
using System.Diagnostics;
using NullableLogging;
using ModTek.Public;

namespace ModTek;

Expand Down
74 changes: 38 additions & 36 deletions ModTek/NullableLogger.cs → ModTek/Public/NullableLogger.cs
Original file line number Diff line number Diff line change
@@ -1,46 +1,49 @@
// get the latest version of the nullable logger from MechEngineer
#nullable enable
#nullable enable
using System;
using System.Collections.Generic;
using HBS.Logging;
using JetBrains.Annotations;
using ModTek.Features.Logging;

namespace NullableLogging;
namespace ModTek.Public;

[HarmonyPatch]
internal sealed class NullableLogger
[PublicAPI]
public sealed class NullableLogger
{
// instantiation

internal static NullableLogger GetLogger(string name, LogLevel? defaultLogLevel = null)
[PublicAPI]
public static NullableLogger GetLogger(string name, LogLevel? defaultLogLevel = null)
{
lock (_loggers)
lock (s_loggers)
{
if (!_loggers.TryGetValue(name, out var logger))
if (!s_loggers.TryGetValue(name, out var logger))
{
logger = new(name, defaultLogLevel);
_loggers[name] = logger;
s_loggers[name] = logger;
}
return logger;
}
}

// useful constants
// useful "constants"

internal const LogLevel TraceLogLevel = (LogLevel)200;
[PublicAPI]
public static LogLevel TraceLogLevel => (LogLevel)LogLevelExtension.TraceLogLevel;

// tracking

private static readonly SortedList<string, NullableLogger> _loggers = new();
private static readonly SortedList<string, NullableLogger> s_loggers = new();

[HarmonyPatch(typeof(Logger.LogImpl), nameof(Logger.LogImpl.Level), MethodType.Setter)]
[HarmonyPostfix]
[HarmonyWrapSafe]
private static void LogImpl_set_Level_Postfix()
{
lock (_loggers)
lock (s_loggers)
{
foreach (var modLogger in _loggers.Values)
foreach (var modLogger in s_loggers.Values)
{
modLogger.RefreshLogLevel();
}
Expand All @@ -49,24 +52,25 @@ private static void LogImpl_set_Level_Postfix()

// instantiation

internal Logger.LogImpl Log { get; }
private readonly Logger.LogImpl _logImpl;
private NullableLogger(string name, LogLevel? defaultLogLevel)
{
Log = (Logger.LogImpl)(
_logImpl = (Logger.LogImpl)(
defaultLogLevel == null
? Logger.GetLogger(name)
: Logger.GetLogger(name, defaultLogLevel.Value)
);
RefreshLogLevel();
}

// log levels
// logging

internal ILevel? Trace { get; private set; }
internal ILevel? Debug { get; private set; }
internal ILevel? Info { get; private set; }
internal ILevel? Warning { get; private set; }
internal ILevel? Error { get; private set; }
[PublicAPI] public ILog Log => _logImpl;
[PublicAPI] public ILevel? Trace { get; private set; }
[PublicAPI] public ILevel? Debug { get; private set; }
[PublicAPI] public ILevel? Info { get; private set; }
[PublicAPI] public ILevel? Warning { get; private set; }
[PublicAPI] public ILevel? Error { get; private set; }

private void RefreshLogLevel()
{
Expand All @@ -80,50 +84,48 @@ private void RefreshLogLevel()

private ILevel? SyncLevelLogger(ref bool lowerLevelEnabled, LogLevel logLevel, ILevel? logger)
{
if (lowerLevelEnabled || Log.IsEnabledFor(logLevel))
if (lowerLevelEnabled || _logImpl.IsEnabledFor(logLevel))
{
lowerLevelEnabled = true;
return logger ?? new LevelLogger(logLevel, Log);
return logger ?? new LevelLogger(logLevel, _logImpl.Name);
}
return null;
}

// logging

internal interface ILevel
[PublicAPI]
public interface ILevel
{
void Log(Exception e);
void Log(string message);
void Log(string message, Exception e);
[PublicAPI] void Log(Exception e);
[PublicAPI] void Log(string message);
[PublicAPI] void Log(string message, Exception e);
}

private sealed class LevelLogger : ILevel
{
private readonly LogLevel _level;
private readonly Logger.LogImpl _log;
private readonly string _loggerName;

internal LevelLogger(LogLevel level, Logger.LogImpl log)
internal LevelLogger(LogLevel level, string loggerName)
{
_level = level;
_log = log;
_loggerName = loggerName;
}

public void Log(Exception e)
{
// _log.LogAtLevel(_level, null, e);
LoggingFeature.LogAtLevel(_log.Name, _level, null, e, null);
LoggingFeature.LogAtLevel(_loggerName, _level, null, e, null);
}

public void Log(string message)
{
// _log.LogAtLevel(_level, message);
LoggingFeature.LogAtLevel(_log.Name, _level, message, null, null);
LoggingFeature.LogAtLevel(_loggerName, _level, message, null, null);
}

public void Log(string message, Exception e)
{
// _log.LogAtLevel(_level, message, e);
LoggingFeature.LogAtLevel(_log.Name, _level, message, e, null);
LoggingFeature.LogAtLevel(_loggerName, _level, message, e, null);
}
}
}
21 changes: 11 additions & 10 deletions doc/LOGGING.md
Original file line number Diff line number Diff line change
Expand Up @@ -86,14 +86,11 @@ If a mod author wants to have a separate copy of all logs a logger produces, one
### Nullable loggers

> **Note**
> Nullable loggers is an advanced feature only necesary if one takes the time to use it
> Nullable loggers are a convenience feature and mainly for advanced users who are faced with log spam and unreadable code.
Another way of enhancing performance is using nullable loggers. This is only for advanced users requiring lots of performance while still wanting readable code.
The best way of improving logging performance is not to log at all, or at least when requested.
The simplest way is to use the `Is*Enabled` flags to avoid expensive operations when logging is disabled for a log level.

In C# one can use Null-conditional operators to check for null and skip code from executing.
This is very useful in logging, as to avoid logging in debug and trace levels, one usually checked the log levels of a logger before actually doing expensive calculations.

Old style:
```csharp
// init
var logger = Logger.GetLogger("YourMod");
Expand All @@ -105,6 +102,9 @@ if (logger.IsDebugEnabled)
}
```

In C# one can use Null-conditional operators to check for null and skip code from executing.
When applied to logging, it makes for more readable code.

With nullable loggers:
```csharp
// init
Expand All @@ -124,7 +124,8 @@ if (nullableLogger != null)
}
```

There is a caveat, the game changes log levels after the mods have initialized, so in order to keep up there is a Harmony patch required and some boilerplate code.

For the boilerplate code, see [NullableLogger.cs](../ModTek/NullableLogger.cs) and [Log.cs](../ModTek/Log.cs) as used by ModTek itself.
ModTek comments out some parts in `NullableLogger.cs` and replaces them with internal calls, these would need to be reverted back.
ModTek provides a utility class called [NullableLogger](../ModTek/Public/NullableLogger.cs) that makes it easy to setup
nullable logging in your mod.
An example of how to use `NullableLogger` can be found in [Log.cs](../ModTek/Log.cs).
The `NullableLogger` keeps track of changes to log levels and adjusts itself according,
as repeated `Is*Enabled` checks are slower than simple null checks.

0 comments on commit 7da1676

Please sign in to comment.