feat(batch4-task2): implement core logger wiring features
This commit is contained in:
367
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs
Normal file
367
dotnet/src/ZB.MOM.NatsNet.Server/NatsServer.Logging.cs
Normal 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()
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user