// 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; /// /// Configures and sets the server logger. /// Mirrors Go Server.ConfigureLogger(). /// 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); } /// /// Sets the server logger. /// Mirrors Go Server.SetLogger(). /// public void SetLogger(INatsLogger? logger, bool debugFlag, bool traceFlag) => SetLoggerV2(logger, debugFlag, traceFlag, false); /// /// Sets the server logger and trace flags. /// Mirrors Go Server.SetLoggerV2(). /// 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); } } } /// /// Re-opens file logger when file logging is enabled. /// Mirrors Go Server.ReOpenLogFile(). /// 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"); } /// /// Executes a logging callback if a logger is present. /// Mirrors Go Server.executeLogCall(). /// internal void ExecuteLogCall(Action action) { INatsLogger? logger; lock (_loggingLock) { logger = _natsLogger; } if (logger == null) { return; } action(logger); } /// /// Logs an error with a scope. /// Mirrors Go Server.Errors(). /// public void Errors(object scope, Exception e) { ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, ErrorContextHelper.UnpackIfErrorCtx(e))); } /// /// Logs an error with context. /// Mirrors Go Server.Errorc(). /// public void Errorc(string ctx, Exception e) { ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, ErrorContextHelper.UnpackIfErrorCtx(e))); } /// /// Logs an error with scope and context. /// Mirrors Go Server.Errorsc(). /// public void Errorsc(object scope, string ctx, Exception e) { ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, ErrorContextHelper.UnpackIfErrorCtx(e))); } /// /// Rate-limited warning based on the raw format string. /// Mirrors Go Server.rateLimitFormatWarnf(). /// internal void RateLimitFormatWarnf(string format, params object[] args) { if (!_rateLimitLogging.TryAdd(format, DateTime.UtcNow)) { return; } var statement = string.Format(format, args); ExecuteLogCall(l => l.Warnf("{0}", statement)); } /// /// Rate-limited warning based on rendered statement. /// Mirrors Go Server.RateLimitWarnf(). /// public void RateLimitWarnf(string format, params object[] args) { var statement = string.Format(format, args); if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow)) { return; } ExecuteLogCall(l => l.Warnf("{0}", statement)); } /// /// Rate-limited debug logging based on rendered statement. /// Mirrors Go Server.RateLimitDebugf(). /// public void RateLimitDebugf(string format, params object[] args) { var statement = string.Format(format, args); if (!_rateLimitLogging.TryAdd(statement, DateTime.UtcNow)) { return; } if (Interlocked.CompareExchange(ref _debugEnabled, 0, 0) == 0) { return; } ExecuteLogCall(l => l.Debugf("{0}", statement)); } 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 state) where TState : notnull => NoopDisposable.Instance; public bool IsEnabled(LogLevel logLevel) => logLevel != LogLevel.None; public void Log( LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func 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() { } } }