445 lines
13 KiB
C#
445 lines
13 KiB
C#
// 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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs an error with a scope.
|
|
/// Mirrors Go <c>Server.Errors()</c>.
|
|
/// </summary>
|
|
public void Errors(object scope, Exception e)
|
|
{
|
|
ExecuteLogCall(l => l.Errorf("{0} - {1}", scope, ErrorContextHelper.UnpackIfErrorCtx(e)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs an error with context.
|
|
/// Mirrors Go <c>Server.Errorc()</c>.
|
|
/// </summary>
|
|
public void Errorc(string ctx, Exception e)
|
|
{
|
|
ExecuteLogCall(l => l.Errorf("{0}: {1}", ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Logs an error with scope and context.
|
|
/// Mirrors Go <c>Server.Errorsc()</c>.
|
|
/// </summary>
|
|
public void Errorsc(object scope, string ctx, Exception e)
|
|
{
|
|
ExecuteLogCall(l => l.Errorf("{0} - {1}: {2}", scope, ctx, ErrorContextHelper.UnpackIfErrorCtx(e)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rate-limited warning based on the raw format string.
|
|
/// Mirrors Go <c>Server.rateLimitFormatWarnf()</c>.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rate-limited warning based on rendered statement.
|
|
/// Mirrors Go <c>Server.RateLimitWarnf()</c>.
|
|
/// </summary>
|
|
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));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Rate-limited debug logging based on rendered statement.
|
|
/// Mirrors Go <c>Server.RateLimitDebugf()</c>.
|
|
/// </summary>
|
|
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>(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()
|
|
{
|
|
}
|
|
}
|
|
}
|