feat(batch4-task2): implement core logger wiring features

This commit is contained in:
Joseph Doherty
2026-02-28 07:56:32 -05:00
parent 52cdc4c08a
commit b79e7aafe9
4 changed files with 497 additions and 0 deletions

View File

@@ -0,0 +1,367 @@
// Copyright 2012-2026 The NATS Authors
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
//
// Adapted from server/log.go in the NATS server Go source.
using System.Text;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using ZB.MOM.NatsNet.Server.Internal;
namespace ZB.MOM.NatsNet.Server;
public sealed partial class NatsServer
{
private readonly object _loggingLock = new();
private INatsLogger? _natsLogger;
/// <summary>
/// Configures and sets the server logger.
/// Mirrors Go <c>Server.ConfigureLogger()</c>.
/// </summary>
public void ConfigureLogger()
{
var opts = GetOpts();
if (opts.NoLog)
{
return;
}
var syslog = opts.Syslog;
if (ServiceManager.IsWindowsService() && string.IsNullOrEmpty(opts.LogFile))
{
syslog = true;
}
if (!string.IsNullOrEmpty(opts.LogFile))
{
var fileLogger = new FileNatsLogger(opts.LogFile, opts.Logtime, opts.LogtimeUtc);
if (opts.LogSizeLimit > 0)
{
fileLogger.SetSizeLimit(opts.LogSizeLimit);
}
if (opts.LogMaxFiles > 0)
{
var maxFiles = opts.LogMaxFiles > int.MaxValue ? 0 : (int)opts.LogMaxFiles;
fileLogger.SetMaxNumFiles(maxFiles);
}
SetLoggerV2(fileLogger, opts.Debug, opts.Trace, opts.TraceVerbose);
return;
}
// Syslog/remote-syslog-specific sinks are not yet ported.
// Keep parity with Go option precedence and wire to the current ILogger backend.
if (!string.IsNullOrEmpty(opts.RemoteSyslog) || syslog)
{
SetLoggerV2(new MicrosoftLoggerAdapter(_logger), opts.Debug, opts.Trace, opts.TraceVerbose);
return;
}
SetLoggerV2(new MicrosoftLoggerAdapter(_logger), opts.Debug, opts.Trace, opts.TraceVerbose);
}
/// <summary>
/// Sets the server logger.
/// Mirrors Go <c>Server.SetLogger()</c>.
/// </summary>
public void SetLogger(INatsLogger? logger, bool debugFlag, bool traceFlag)
=> SetLoggerV2(logger, debugFlag, traceFlag, false);
/// <summary>
/// Sets the server logger and trace flags.
/// Mirrors Go <c>Server.SetLoggerV2()</c>.
/// </summary>
public void SetLoggerV2(INatsLogger? logger, bool debugFlag, bool traceFlag, bool sysTrace)
{
Interlocked.Exchange(ref _debugEnabled, debugFlag ? 1 : 0);
Interlocked.Exchange(ref _traceEnabled, traceFlag ? 1 : 0);
Interlocked.Exchange(ref _traceSysAcc, sysTrace ? 1 : 0);
INatsLogger? previous;
lock (_loggingLock)
{
previous = _natsLogger;
_natsLogger = logger;
_logger = ToMicrosoftLogger(logger);
}
if (previous is IDisposable disposable)
{
try
{
disposable.Dispose();
}
catch (Exception ex)
{
_logger.LogError("Error closing logger: {Error}", ex.Message);
}
}
}
/// <summary>
/// Re-opens file logger when file logging is enabled.
/// Mirrors Go <c>Server.ReOpenLogFile()</c>.
/// </summary>
public void ReOpenLogFile()
{
INatsLogger? activeLogger;
lock (_loggingLock)
{
activeLogger = _natsLogger;
}
if (activeLogger == null)
{
Noticef("File log re-open ignored, no logger");
return;
}
var opts = GetOpts();
if (string.IsNullOrEmpty(opts.LogFile))
{
Noticef("File log re-open ignored, not a file logger");
return;
}
var fileLogger = new FileNatsLogger(opts.LogFile, opts.Logtime, opts.LogtimeUtc);
if (opts.LogSizeLimit > 0)
{
fileLogger.SetSizeLimit(opts.LogSizeLimit);
}
if (opts.LogMaxFiles > 0)
{
var maxFiles = opts.LogMaxFiles > int.MaxValue ? 0 : (int)opts.LogMaxFiles;
fileLogger.SetMaxNumFiles(maxFiles);
}
SetLogger(fileLogger, opts.Debug, opts.Trace);
Noticef("File log re-opened");
}
/// <summary>
/// Executes a logging callback if a logger is present.
/// Mirrors Go <c>Server.executeLogCall()</c>.
/// </summary>
internal void ExecuteLogCall(Action<INatsLogger> action)
{
INatsLogger? logger;
lock (_loggingLock)
{
logger = _natsLogger;
}
if (logger == null)
{
return;
}
action(logger);
}
private static ILogger ToMicrosoftLogger(INatsLogger? logger)
{
return logger switch
{
null => NullLogger.Instance,
MicrosoftLoggerAdapter adapter => adapter.UnderlyingLogger,
_ => new NatsLoggerBridge(logger)
};
}
private sealed class NatsLoggerBridge(INatsLogger natsLogger) : ILogger
{
public IDisposable BeginScope<TState>(TState state) where TState : notnull
=> NoopDisposable.Instance;
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 message = formatter(state, exception);
if (exception != null)
{
message = $"{message}: {exception.Message}";
}
switch (logLevel)
{
case LogLevel.Trace:
natsLogger.Tracef("{0}", message);
break;
case LogLevel.Debug:
natsLogger.Debugf("{0}", message);
break;
case LogLevel.Information:
natsLogger.Noticef("{0}", message);
break;
case LogLevel.Warning:
natsLogger.Warnf("{0}", message);
break;
case LogLevel.Error:
natsLogger.Errorf("{0}", message);
break;
case LogLevel.Critical:
natsLogger.Fatalf("{0}", message);
break;
}
}
}
private sealed class FileNatsLogger(string filePath, bool includeTimestamp, bool useUtc) : INatsLogger, IDisposable
{
private readonly object _sync = new();
private readonly string _filePath = filePath;
private readonly bool _includeTimestamp = includeTimestamp;
private readonly bool _useUtc = useUtc;
private long _sizeLimit;
private int _maxNumFiles;
private bool _disposed;
public void SetSizeLimit(long sizeLimit)
{
_sizeLimit = sizeLimit;
}
public void SetMaxNumFiles(int maxNumFiles)
{
_maxNumFiles = maxNumFiles < 0 ? 0 : maxNumFiles;
}
public void Noticef(string format, params object[] args) => Write("INF", format, args);
public void Warnf(string format, params object[] args) => Write("WRN", format, args);
public void Fatalf(string format, params object[] args) => Write("FTL", format, args);
public void Errorf(string format, params object[] args) => Write("ERR", format, args);
public void Debugf(string format, params object[] args) => Write("DBG", format, args);
public void Tracef(string format, params object[] args) => Write("TRC", format, args);
public void Dispose()
{
_disposed = true;
}
private void Write(string level, string format, object[] args)
{
if (_disposed)
{
return;
}
var directory = Path.GetDirectoryName(_filePath);
if (!string.IsNullOrEmpty(directory))
{
Directory.CreateDirectory(directory);
}
var rendered = string.Format(format, args);
var line = BuildLine(level, rendered);
lock (_sync)
{
RotateIfNeeded(line);
File.AppendAllText(_filePath, line, Encoding.UTF8);
}
}
private string BuildLine(string level, string rendered)
{
if (!_includeTimestamp)
{
return $"[{level}] {rendered}{Environment.NewLine}";
}
var now = _useUtc ? DateTime.UtcNow : DateTime.Now;
return $"[{now:yyyy-MM-dd HH:mm:ss.fff}] [{level}] {rendered}{Environment.NewLine}";
}
private void RotateIfNeeded(string nextLine)
{
if (_sizeLimit <= 0)
{
return;
}
var currentSize = File.Exists(_filePath) ? new FileInfo(_filePath).Length : 0L;
var nextSize = Encoding.UTF8.GetByteCount(nextLine);
if (currentSize + nextSize <= _sizeLimit)
{
return;
}
RotateFiles();
File.AppendAllText(_filePath, "Rotated log, backup saved" + Environment.NewLine, Encoding.UTF8);
}
private void RotateFiles()
{
if (!File.Exists(_filePath))
{
return;
}
if (_maxNumFiles <= 1)
{
var backup = _filePath + ".bak";
if (File.Exists(backup))
{
File.Delete(backup);
}
File.Move(_filePath, backup);
return;
}
for (var i = _maxNumFiles - 1; i >= 1; i--)
{
var src = $"{_filePath}.{i}";
var dst = $"{_filePath}.{i + 1}";
if (!File.Exists(src))
{
continue;
}
if (File.Exists(dst))
{
File.Delete(dst);
}
File.Move(src, dst);
}
var firstBackup = $"{_filePath}.1";
if (File.Exists(firstBackup))
{
File.Delete(firstBackup);
}
File.Move(_filePath, firstBackup);
}
}
private sealed class NoopDisposable : IDisposable
{
public static NoopDisposable Instance { get; } = new();
public void Dispose()
{
}
}
}