Initial commit: ClawdDotNet
Import des bestehenden Projektstands in Git. - .NET 10 WinForms Anwendung (Multi-Agent / Tool-System) - .gitignore fuer Build-Artefakte, Secrets und Runtime-Daten ergaenzt Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Schreibt Log-Einträge asynchron in datei- und datumsgetrennte Logfiles.
|
||||
/// Struktur: {LogDirectory}/{Datum}/{Modul}.log
|
||||
/// Thread-safe durch ConcurrentQueue + dediziertem Writer-Task.
|
||||
/// </summary>
|
||||
public sealed class FileLogWriter : IAsyncDisposable
|
||||
{
|
||||
private readonly FileLoggerOptions _options;
|
||||
private readonly ConcurrentQueue<LogEntry> _queue = new();
|
||||
private readonly SemaphoreSlim _signal = new(0);
|
||||
private readonly CancellationTokenSource _cts = new();
|
||||
private readonly Task _writerTask;
|
||||
private readonly ConcurrentDictionary<string, Lock> _fileLocks = new();
|
||||
|
||||
public FileLogWriter(FileLoggerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
Directory.CreateDirectory(_options.LogDirectory);
|
||||
_writerTask = Task.Run(ProcessQueueAsync);
|
||||
}
|
||||
|
||||
public void Enqueue(LogEntry entry)
|
||||
{
|
||||
if (entry.Level < _options.MinimumLevel)
|
||||
return;
|
||||
|
||||
_queue.Enqueue(entry);
|
||||
_signal.Release();
|
||||
}
|
||||
|
||||
private async Task ProcessQueueAsync()
|
||||
{
|
||||
while (!_cts.Token.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
await _signal.WaitAsync(_cts.Token);
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
DrainQueue();
|
||||
}
|
||||
|
||||
DrainQueue();
|
||||
}
|
||||
|
||||
private void DrainQueue()
|
||||
{
|
||||
while (_queue.TryDequeue(out var entry))
|
||||
{
|
||||
try
|
||||
{
|
||||
WriteEntry(entry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// Logging darf die Anwendung niemals crashen
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void WriteEntry(LogEntry entry)
|
||||
{
|
||||
var dateDir = Path.Combine(
|
||||
_options.LogDirectory,
|
||||
entry.Timestamp.ToString(_options.DateFormat));
|
||||
|
||||
Directory.CreateDirectory(dateDir);
|
||||
|
||||
var safeModule = SanitizeModuleName(entry.Module);
|
||||
var filePath = Path.Combine(dateDir, $"{safeModule}.log");
|
||||
|
||||
var fileLock = _fileLocks.GetOrAdd(filePath, _ => new Lock());
|
||||
|
||||
lock (fileLock)
|
||||
{
|
||||
var line = entry.Format(_options.TimestampFormat) + Environment.NewLine;
|
||||
File.AppendAllText(filePath, line, Encoding.UTF8);
|
||||
}
|
||||
}
|
||||
|
||||
private static string SanitizeModuleName(string module)
|
||||
{
|
||||
var sanitized = module
|
||||
.Replace('.', '_')
|
||||
.Replace('/', '_')
|
||||
.Replace('\\', '_');
|
||||
|
||||
foreach (var c in Path.GetInvalidFileNameChars())
|
||||
sanitized = sanitized.Replace(c, '_');
|
||||
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "Unknown" : sanitized;
|
||||
}
|
||||
|
||||
public async ValueTask DisposeAsync()
|
||||
{
|
||||
await _cts.CancelAsync();
|
||||
|
||||
try
|
||||
{
|
||||
await _writerTask;
|
||||
}
|
||||
catch (OperationCanceledException)
|
||||
{
|
||||
}
|
||||
|
||||
_signal.Dispose();
|
||||
_cts.Dispose();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
public sealed class FileLoggerOptions
|
||||
{
|
||||
public string LogDirectory { get; set; } = "./Logs";
|
||||
public LogLevel MinimumLevel { get; set; } = LogLevel.Info;
|
||||
public int MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10 MB
|
||||
public string DateFormat { get; set; } = "yyyy-MM-dd";
|
||||
public string TimestampFormat { get; set; } = "HH:mm:ss.fff";
|
||||
}
|
||||
|
||||
public enum LogLevel
|
||||
{
|
||||
Debug = 0,
|
||||
Info = 1,
|
||||
Warn = 2,
|
||||
Error = 3
|
||||
}
|
||||
@@ -0,0 +1,66 @@
|
||||
using System.Collections.Concurrent;
|
||||
using MEL = Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// ILoggerProvider-Implementierung für das dateibasierte Logging.
|
||||
/// Erzeugt pro Kategorie (= Modul) einen eigenen ModuleLogger.
|
||||
///
|
||||
/// Kategorien werden auf Modulnamen gemappt:
|
||||
/// "ClawdDotNet.Core.Engine.AgentEngine" → "Core"
|
||||
/// "ClawdDotNet.Tools.Database.DatabaseTool" → "Tool_Database"
|
||||
/// "ClawdDotNet.Tools.FileRW.FileRWTool" → "Tool_FileRW"
|
||||
/// Alles andere → letzter Namespace-Teil oder "General"
|
||||
/// </summary>
|
||||
public sealed class FileLoggerProvider : MEL.ILoggerProvider
|
||||
{
|
||||
private readonly FileLogWriter _writer;
|
||||
private readonly FileLoggerOptions _options;
|
||||
private readonly ConcurrentDictionary<string, ModuleLogger> _loggers = new();
|
||||
|
||||
public FileLoggerProvider(FileLoggerOptions options)
|
||||
{
|
||||
_options = options;
|
||||
_writer = new FileLogWriter(options);
|
||||
}
|
||||
|
||||
public MEL.ILogger CreateLogger(string categoryName)
|
||||
{
|
||||
var module = MapCategoryToModule(categoryName);
|
||||
return _loggers.GetOrAdd(module, m => new ModuleLogger(m, _writer, _options));
|
||||
}
|
||||
|
||||
internal static string MapCategoryToModule(string categoryName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(categoryName))
|
||||
return "General";
|
||||
|
||||
// ClawdDotNet.Core.* → "Core"
|
||||
if (categoryName.StartsWith("ClawdDotNet.Core.", StringComparison.Ordinal))
|
||||
return "Core";
|
||||
|
||||
// ClawdDotNet.Host.* → "Host"
|
||||
if (categoryName.StartsWith("ClawdDotNet.Host.", StringComparison.Ordinal)
|
||||
|| categoryName == "ClawdDotNet.Host")
|
||||
return "Host";
|
||||
|
||||
// ClawdDotNet.Tools.{ToolName}.* → "Tool_{ToolName}"
|
||||
if (categoryName.StartsWith("ClawdDotNet.Tools.", StringComparison.Ordinal))
|
||||
{
|
||||
var afterTools = categoryName["ClawdDotNet.Tools.".Length..];
|
||||
var dotIndex = afterTools.IndexOf('.');
|
||||
var toolName = dotIndex > 0 ? afterTools[..dotIndex] : afterTools;
|
||||
return $"Tool_{toolName}";
|
||||
}
|
||||
|
||||
// Fallback: letzter Segment-Teil
|
||||
var lastDot = categoryName.LastIndexOf('.');
|
||||
return lastDot >= 0 ? categoryName[(lastDot + 1)..] : categoryName;
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_writer.DisposeAsync().AsTask().GetAwaiter().GetResult();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
public sealed record LogEntry(
|
||||
DateTime Timestamp,
|
||||
LogLevel Level,
|
||||
string Module,
|
||||
string Message,
|
||||
Exception? Exception = null
|
||||
)
|
||||
{
|
||||
public string Format(string timestampFormat)
|
||||
{
|
||||
var levelTag = Level switch
|
||||
{
|
||||
LogLevel.Debug => "DBG",
|
||||
LogLevel.Info => "INF",
|
||||
LogLevel.Warn => "WRN",
|
||||
LogLevel.Error => "ERR",
|
||||
_ => "???"
|
||||
};
|
||||
|
||||
var line = $"[{Timestamp.ToString(timestampFormat)}] [{levelTag}] {Message}";
|
||||
|
||||
if (Exception is not null)
|
||||
line += Environment.NewLine + Exception.ToString();
|
||||
|
||||
return line;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
using Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
public static class LoggingExtensions
|
||||
{
|
||||
public static ILoggerFactory AddClawdFileLogging(
|
||||
this ILoggerFactory factory,
|
||||
FileLoggerOptions? options = null)
|
||||
{
|
||||
factory.AddProvider(new FileLoggerProvider(options ?? new FileLoggerOptions()));
|
||||
return factory;
|
||||
}
|
||||
|
||||
public static ILoggerFactory CreateClawdLoggerFactory(
|
||||
string logDirectory = "./Logs",
|
||||
LogLevel minimumLevel = LogLevel.Info)
|
||||
{
|
||||
var options = new FileLoggerOptions
|
||||
{
|
||||
LogDirectory = logDirectory,
|
||||
MinimumLevel = minimumLevel
|
||||
};
|
||||
|
||||
var factory = LoggerFactory.Create(builder =>
|
||||
{
|
||||
builder.SetMinimumLevel(MapToMelLevel(minimumLevel));
|
||||
});
|
||||
|
||||
factory.AddClawdFileLogging(options);
|
||||
return factory;
|
||||
}
|
||||
|
||||
private static Microsoft.Extensions.Logging.LogLevel MapToMelLevel(LogLevel level) => level switch
|
||||
{
|
||||
LogLevel.Debug => Microsoft.Extensions.Logging.LogLevel.Debug,
|
||||
LogLevel.Info => Microsoft.Extensions.Logging.LogLevel.Information,
|
||||
LogLevel.Warn => Microsoft.Extensions.Logging.LogLevel.Warning,
|
||||
LogLevel.Error => Microsoft.Extensions.Logging.LogLevel.Error,
|
||||
_ => Microsoft.Extensions.Logging.LogLevel.Information
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
using MEL = Microsoft.Extensions.Logging;
|
||||
|
||||
namespace ClawdDotNet.Core.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Implementiert Microsoft.Extensions.Logging.ILogger und leitet alle
|
||||
/// Einträge an den zentralen FileLogWriter weiter.
|
||||
/// Jede Instanz ist einem Modul zugeordnet (z.B. "Core", "Tool_Database").
|
||||
/// </summary>
|
||||
public sealed class ModuleLogger : MEL.ILogger
|
||||
{
|
||||
private readonly string _module;
|
||||
private readonly FileLogWriter _writer;
|
||||
private readonly FileLoggerOptions _options;
|
||||
|
||||
public ModuleLogger(string module, FileLogWriter writer, FileLoggerOptions options)
|
||||
{
|
||||
_module = module;
|
||||
_writer = writer;
|
||||
_options = options;
|
||||
}
|
||||
|
||||
public IDisposable? BeginScope<TState>(TState state) where TState : notnull => null;
|
||||
|
||||
public bool IsEnabled(MEL.LogLevel logLevel)
|
||||
{
|
||||
var mapped = MapLevel(logLevel);
|
||||
return mapped >= _options.MinimumLevel;
|
||||
}
|
||||
|
||||
public void Log<TState>(
|
||||
MEL.LogLevel logLevel,
|
||||
MEL.EventId eventId,
|
||||
TState state,
|
||||
Exception? exception,
|
||||
Func<TState, Exception?, string> formatter)
|
||||
{
|
||||
if (!IsEnabled(logLevel))
|
||||
return;
|
||||
|
||||
var message = formatter(state, exception);
|
||||
var entry = new LogEntry(
|
||||
DateTime.Now,
|
||||
MapLevel(logLevel),
|
||||
_module,
|
||||
message,
|
||||
exception);
|
||||
|
||||
_writer.Enqueue(entry);
|
||||
}
|
||||
|
||||
private static LogLevel MapLevel(MEL.LogLevel level) => level switch
|
||||
{
|
||||
MEL.LogLevel.Trace => LogLevel.Debug,
|
||||
MEL.LogLevel.Debug => LogLevel.Debug,
|
||||
MEL.LogLevel.Information => LogLevel.Info,
|
||||
MEL.LogLevel.Warning => LogLevel.Warn,
|
||||
MEL.LogLevel.Error => LogLevel.Error,
|
||||
MEL.LogLevel.Critical => LogLevel.Error,
|
||||
_ => LogLevel.Info
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user