mirror of https://github.com/abpframework/abp.git
Browse Source
Added IMcpLogger interface and McpLogger implementation to provide structured logging for MCP operations, supporting log levels and file rotation. Replaced direct Console.Error logging with IMcpLogger in MCP-related services and commands. Log level is now configurable via the ABP_MCP_LOG_LEVEL environment variable, and logs are written to both file and stderr as appropriate.pull/24677/head
9 changed files with 316 additions and 56 deletions
@ -0,0 +1,36 @@ |
|||
using System; |
|||
|
|||
namespace Volo.Abp.Cli.Commands.Services; |
|||
|
|||
/// <summary>
|
|||
/// Logger interface for MCP operations.
|
|||
/// Writes detailed logs to file and critical messages (Warning/Error) to stderr.
|
|||
/// Log level is controlled via ABP_MCP_LOG_LEVEL environment variable.
|
|||
/// </summary>
|
|||
public interface IMcpLogger |
|||
{ |
|||
/// <summary>
|
|||
/// Logs a debug message. Only written to file when log level is Debug.
|
|||
/// </summary>
|
|||
void Debug(string source, string message); |
|||
|
|||
/// <summary>
|
|||
/// Logs an informational message. Written to file when log level is Debug or Info.
|
|||
/// </summary>
|
|||
void Info(string source, string message); |
|||
|
|||
/// <summary>
|
|||
/// Logs a warning message. Written to file and stderr.
|
|||
/// </summary>
|
|||
void Warning(string source, string message); |
|||
|
|||
/// <summary>
|
|||
/// Logs an error message. Written to file and stderr.
|
|||
/// </summary>
|
|||
void Error(string source, string message); |
|||
|
|||
/// <summary>
|
|||
/// Logs an error message with exception details. Written to file and stderr.
|
|||
/// </summary>
|
|||
void Error(string source, string message, Exception exception); |
|||
} |
|||
@ -0,0 +1,210 @@ |
|||
using System; |
|||
using System.IO; |
|||
using System.Text; |
|||
using Volo.Abp.DependencyInjection; |
|||
|
|||
namespace Volo.Abp.Cli.Commands.Services; |
|||
|
|||
/// <summary>
|
|||
/// MCP logger implementation that writes to both file and stderr.
|
|||
/// - All logs at or above the configured level are written to file
|
|||
/// - Warning and Error logs are also written to stderr
|
|||
/// - Log level is controlled via ABP_MCP_LOG_LEVEL environment variable
|
|||
/// </summary>
|
|||
public class McpLogger : IMcpLogger, ISingletonDependency |
|||
{ |
|||
private const long MaxLogFileSizeBytes = 5 * 1024 * 1024; // 5MB
|
|||
private const string LogPrefix = "[MCP]"; |
|||
|
|||
private readonly object _fileLock = new(); |
|||
private readonly McpLogLevel _configuredLogLevel; |
|||
|
|||
public McpLogger() |
|||
{ |
|||
_configuredLogLevel = GetConfiguredLogLevel(); |
|||
} |
|||
|
|||
public void Debug(string source, string message) |
|||
{ |
|||
Log(McpLogLevel.Debug, source, message); |
|||
} |
|||
|
|||
public void Info(string source, string message) |
|||
{ |
|||
Log(McpLogLevel.Info, source, message); |
|||
} |
|||
|
|||
public void Warning(string source, string message) |
|||
{ |
|||
Log(McpLogLevel.Warning, source, message); |
|||
} |
|||
|
|||
public void Error(string source, string message) |
|||
{ |
|||
Log(McpLogLevel.Error, source, message); |
|||
} |
|||
|
|||
public void Error(string source, string message, Exception exception) |
|||
{ |
|||
#if DEBUG
|
|||
var fullMessage = $"{message} | Exception: {exception.GetType().Name}: {exception.Message}"; |
|||
#else
|
|||
var fullMessage = $"{message} | Exception: {exception.GetType().Name}"; |
|||
#endif
|
|||
Log(McpLogLevel.Error, source, fullMessage); |
|||
} |
|||
|
|||
private void Log(McpLogLevel level, string source, string message) |
|||
{ |
|||
if (_configuredLogLevel == McpLogLevel.None) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
if (level < _configuredLogLevel) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var timestamp = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss"); |
|||
var levelStr = level.ToString().ToUpperInvariant(); |
|||
var formattedMessage = $"[{timestamp}][{levelStr}][{source}] {message}"; |
|||
|
|||
// Write to file (all levels at or above configured level)
|
|||
WriteToFile(formattedMessage); |
|||
|
|||
// Write to stderr for Warning and Error levels
|
|||
if (level >= McpLogLevel.Warning) |
|||
{ |
|||
WriteToStderr(levelStr, message); |
|||
} |
|||
} |
|||
|
|||
private void WriteToFile(string formattedMessage) |
|||
{ |
|||
try |
|||
{ |
|||
lock (_fileLock) |
|||
{ |
|||
EnsureLogDirectoryExists(); |
|||
RotateLogFileIfNeeded(); |
|||
|
|||
File.AppendAllText( |
|||
CliPaths.McpLog, |
|||
formattedMessage + Environment.NewLine, |
|||
Encoding.UTF8); |
|||
} |
|||
} |
|||
catch |
|||
{ |
|||
// Silently ignore file write errors to not disrupt MCP operations
|
|||
} |
|||
} |
|||
|
|||
private void WriteToStderr(string level, string message) |
|||
{ |
|||
try |
|||
{ |
|||
// Use synchronous write to avoid async issues in MCP context
|
|||
Console.Error.WriteLine($"{LogPrefix}[{level}] {message}"); |
|||
} |
|||
catch |
|||
{ |
|||
// Silently ignore stderr write errors
|
|||
} |
|||
} |
|||
|
|||
private void EnsureLogDirectoryExists() |
|||
{ |
|||
var directory = Path.GetDirectoryName(CliPaths.McpLog); |
|||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory)) |
|||
{ |
|||
Directory.CreateDirectory(directory); |
|||
} |
|||
} |
|||
|
|||
private void RotateLogFileIfNeeded() |
|||
{ |
|||
try |
|||
{ |
|||
if (!File.Exists(CliPaths.McpLog)) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var fileInfo = new FileInfo(CliPaths.McpLog); |
|||
if (fileInfo.Length < MaxLogFileSizeBytes) |
|||
{ |
|||
return; |
|||
} |
|||
|
|||
var backupPath = CliPaths.McpLog + ".1"; |
|||
|
|||
// Delete old backup if exists
|
|||
if (File.Exists(backupPath)) |
|||
{ |
|||
File.Delete(backupPath); |
|||
} |
|||
|
|||
// Rename current log to backup
|
|||
File.Move(CliPaths.McpLog, backupPath); |
|||
} |
|||
catch |
|||
{ |
|||
// Silently ignore rotation errors
|
|||
} |
|||
} |
|||
|
|||
private static McpLogLevel GetConfiguredLogLevel() |
|||
{ |
|||
#if DEBUG
|
|||
// In development builds, allow full control via environment variable
|
|||
var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); |
|||
|
|||
if (string.IsNullOrWhiteSpace(envValue)) |
|||
{ |
|||
return McpLogLevel.Info; // Default level
|
|||
} |
|||
|
|||
return envValue.ToLowerInvariant() switch |
|||
{ |
|||
"debug" => McpLogLevel.Debug, |
|||
"info" => McpLogLevel.Info, |
|||
"warning" => McpLogLevel.Warning, |
|||
"error" => McpLogLevel.Error, |
|||
"none" => McpLogLevel.None, |
|||
_ => McpLogLevel.Info |
|||
}; |
|||
#else
|
|||
// In release builds, restrict to Warning or higher (ignore env variable for Debug/Info)
|
|||
var envValue = Environment.GetEnvironmentVariable(CliConsts.McpLogLevelEnvironmentVariable); |
|||
|
|||
if (string.IsNullOrWhiteSpace(envValue)) |
|||
{ |
|||
return McpLogLevel.Info; // Default level
|
|||
} |
|||
|
|||
return envValue.ToLowerInvariant() switch |
|||
{ |
|||
"debug" => McpLogLevel.Info, // Cap Debug to Info
|
|||
"info" => McpLogLevel.Info, |
|||
"warning" => McpLogLevel.Warning, |
|||
"error" => McpLogLevel.Error, |
|||
"none" => McpLogLevel.None, |
|||
_ => McpLogLevel.Info |
|||
}; |
|||
#endif
|
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Log levels for MCP logging.
|
|||
/// </summary>
|
|||
public enum McpLogLevel |
|||
{ |
|||
Debug = 0, |
|||
Info = 1, |
|||
Warning = 2, |
|||
Error = 3, |
|||
None = 4 |
|||
} |
|||
Loading…
Reference in new issue