file optional

alphons <alphons@heijden.com> 3 Aug 2026, 16:14
98d22b74bd5c08ca22e2bccc50ce4747675c3e34
5 files changed
  • SeriLoog/Extensions/SerilogBuilderExtensions.cs
  • SeriLoog/LogStoreOptions.cs
  • SeriLoog/Program.cs
  • SeriLoog/Services/LogQueryService.cs
  • SeriLoog/appsettings.json
diff --git a/SeriLoog/Extensions/SerilogBuilderExtensions.cs b/SeriLoog/Extensions/SerilogBuilderExtensions.cs
index 928a871..3d03223 100644
--- a/SeriLoog/Extensions/SerilogBuilderExtensions.cs
+++ b/SeriLoog/Extensions/SerilogBuilderExtensions.cs
@@ -13,10 +13,10 @@ public static class SerilogBuilderExtensions
{
var configuration = builder.Configuration;
- var mongoConnectionString = configuration.GetConnectionString("Mongo") ?? "mongodb://localhost:27017";
- var mongoDatabase = configuration["SeriLoog:MongoDatabase"] ?? "SeriLoog";
- var mongoCollection = configuration["SeriLoog:MongoCollection"] ?? "logs";
- var logFilePath = configuration["SeriLoog:LogFilePath"] ?? "Logs/log-.txt";
+ var mongoConnectionString = configuration.GetConnectionString("MongoDb") ?? "mongodb://localhost:27017";
+ var mongoDatabase = configuration["MongoDb:DatabaseName"] ?? "SeriLoog";
+ var mongoCollection = configuration["MongoDb:Collections:Logs"] ?? "logs";
+ var logFilePath = configuration["MongoDb:LogFilePath"];
builder.Services.AddHttpContextAccessor();
@@ -29,15 +29,19 @@ public static class SerilogBuilderExtensions
.Enrich.WithProperty("Application", "SeriLoog")
.Enrich.With(new HttpContextEnricher(services.GetRequiredService<IHttpContextAccessor>()))
.WriteTo.Console()
- .WriteTo.File(
- formatter: new CompactJsonFormatter(),
- path: logFilePath,
- rollingInterval: Serilog.RollingInterval.Day,
- retainedFileCountLimit: 14,
- shared: true)
.WriteTo.MongoDB(
$"{mongoConnectionString}/{mongoDatabase}",
collectionName: mongoCollection);
+
+ if (!string.IsNullOrWhiteSpace(logFilePath))
+ {
+ loggerConfiguration.WriteTo.File(
+ formatter: new CompactJsonFormatter(),
+ path: logFilePath,
+ rollingInterval: RollingInterval.Day,
+ retainedFileCountLimit: 14,
+ shared: true);
+ }
});
builder.Services.AddSingleton(new LogStoreOptions
diff --git a/SeriLoog/LogStoreOptions.cs b/SeriLoog/LogStoreOptions.cs
index 68fee6f..abded49 100644
--- a/SeriLoog/LogStoreOptions.cs
+++ b/SeriLoog/LogStoreOptions.cs
@@ -5,5 +5,5 @@ public sealed class LogStoreOptions
public required string MongoConnectionString { get; init; }
public required string MongoDatabase { get; init; }
public required string MongoCollection { get; init; }
- public required string LogFilePath { get; init; }
+ public string? LogFilePath { get; init; }
}
diff --git a/SeriLoog/Program.cs b/SeriLoog/Program.cs
index a9f2458..4b4dd07 100644
--- a/SeriLoog/Program.cs
+++ b/SeriLoog/Program.cs
@@ -1,10 +1,13 @@
using Serilog;
using SeriLoog.Extensions;
+using SeriLoog.Services;
var builder = WebApplication.CreateBuilder(args);
builder.AddSerilogRequestLogging();
+builder.Services.AddSingleton<LogQueryService>();
+
builder.Services.AddControllers();
var app = builder.Build();
diff --git a/SeriLoog/Services/LogQueryService.cs b/SeriLoog/Services/LogQueryService.cs
index fc4ab95..db8d928 100644
--- a/SeriLoog/Services/LogQueryService.cs
+++ b/SeriLoog/Services/LogQueryService.cs
@@ -7,336 +7,340 @@ namespace SeriLoog.Services;
public sealed class LogQuery
{
- public int Page { get; set; } = 1;
- public int PageSize { get; set; } = 25;
- public string[]? Levels { get; set; }
- public string? Search { get; set; }
- public DateTimeOffset? From { get; set; }
- public DateTimeOffset? To { get; set; }
- public string Source { get; set; } = "mongo";
+ public int Page { get; set; } = 1;
+ public int PageSize { get; set; } = 25;
+ public string[]? Levels { get; set; }
+ public string? Search { get; set; }
+ public DateTimeOffset? From { get; set; }
+ public DateTimeOffset? To { get; set; }
+ public string Source { get; set; } = "mongo";
}
-public sealed class LogQueryService
+public sealed class LogQueryService(LogStoreOptions options, ILogger<LogQueryService> logger)
{
- private readonly LogStoreOptions _options;
- private readonly ILogger<LogQueryService> _logger;
-
- public LogQueryService(LogStoreOptions options, ILogger<LogQueryService> logger)
- {
- _options = options;
- _logger = logger;
- }
-
- public async Task<LogQueryResult> QueryAsync(LogQuery query, CancellationToken ct)
- {
- if (string.Equals(query.Source, "file", StringComparison.OrdinalIgnoreCase))
- {
- return QueryFile(query);
- }
-
- try
- {
- return await QueryMongoAsync(query, ct);
- }
- catch (Exception ex)
- {
- _logger.LogWarning(ex, "MongoDB unavailable, falling back to file log source");
- var result = QueryFile(query);
- result.Warning = $"MongoDB unavailable ({ex.Message}). Showing file log instead.";
- return result;
- }
- }
-
- private async Task<LogQueryResult> QueryMongoAsync(LogQuery query, CancellationToken ct)
- {
- var client = new MongoClient(_options.MongoConnectionString);
- var db = client.GetDatabase(_options.MongoDatabase);
- var collection = db.GetCollection<BsonDocument>(_options.MongoCollection);
-
- var filters = new List<FilterDefinition<BsonDocument>>();
- var builder = Builders<BsonDocument>.Filter;
-
- if (query.Levels is { Length: > 0 })
- {
- var levelFilters = query.Levels.Select(l =>
- builder.Regex("Level", new BsonRegularExpression($"^{System.Text.RegularExpressions.Regex.Escape(l)}", "i")));
- filters.Add(builder.Or(levelFilters));
- }
-
- if (!string.IsNullOrWhiteSpace(query.Search))
- {
- var pattern = new BsonRegularExpression(System.Text.RegularExpressions.Regex.Escape(query.Search), "i");
- filters.Add(builder.Or(
- builder.Regex("RenderedMessage", pattern),
- builder.Regex("MessageTemplate", pattern),
- builder.Regex("Exception", pattern)));
- }
-
- if (query.From is not null)
- {
- filters.Add(builder.Gte("Timestamp", query.From.Value.UtcDateTime));
- }
-
- if (query.To is not null)
- {
- filters.Add(builder.Lte("Timestamp", query.To.Value.UtcDateTime));
- }
-
- var filter = filters.Count > 0 ? builder.And(filters) : builder.Empty;
-
- var total = await collection.CountDocumentsAsync(filter, cancellationToken: ct);
-
- var page = Math.Max(1, query.Page);
- var pageSize = Math.Clamp(query.PageSize, 1, 500);
-
- var docs = await collection.Find(filter)
- .SortByDescending(d => d["Timestamp"])
- .Skip((page - 1) * pageSize)
- .Limit(pageSize)
- .ToListAsync(ct);
-
- var items = docs.Select(MapMongoDocument).ToList();
-
- return new LogQueryResult
- {
- Items = items,
- Total = total,
- Page = page,
- PageSize = pageSize,
- Source = "mongo"
- };
- }
-
- private static LogEntryDto MapMongoDocument(BsonDocument doc)
- {
- string GetString(params string[] names)
- {
- foreach (var name in names)
- {
- if (doc.TryGetValue(name, out var v) && !v.IsBsonNull)
- return v.IsString ? v.AsString : v.ToString() ?? "";
- }
- return "";
- }
-
- DateTimeOffset GetTimestamp()
- {
- foreach (var name in new[] { "Timestamp", "UtcTimeStamp", "@t" })
- {
- if (doc.TryGetValue(name, out var v) && !v.IsBsonNull)
- {
- return v.BsonType == BsonType.DateTime
- ? new DateTimeOffset(v.ToUniversalTime(), TimeSpan.Zero)
- : DateTimeOffset.Parse(v.ToString()!);
- }
- }
- return DateTimeOffset.UtcNow;
- }
-
- var properties = new Dictionary<string, object?>();
- if (doc.TryGetValue("Properties", out var propsVal) && propsVal.IsBsonDocument)
- {
- foreach (var el in propsVal.AsBsonDocument)
- {
- properties[el.Name] = BsonTypeMapper.MapToDotNetValue(el.Value);
- }
- }
-
- var id = doc.TryGetValue("_id", out var idVal) ? idVal.ToString() ?? "" : "";
- var json = doc.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true });
-
- return new LogEntryDto
- {
- Id = id ?? "",
- Timestamp = GetTimestamp(),
- Level = NormalizeLevel(GetString("Level")),
- Message = GetString("RenderedMessage", "MessageTemplate"),
- MessageTemplate = GetString("MessageTemplate"),
- Exception = doc.TryGetValue("Exception", out var exVal) && !exVal.IsBsonNull ? exVal.ToString() : null,
- Properties = properties,
- Source = "mongo",
- RawJson = json
- };
- }
-
- private LogQueryResult QueryFile(LogQuery query)
- {
- var dir = Path.GetDirectoryName(Path.GetFullPath(_options.LogFilePath)) ?? ".";
- var baseName = Path.GetFileNameWithoutExtension(_options.LogFilePath).TrimEnd('-');
- var ext = Path.GetExtension(_options.LogFilePath);
-
- var allLines = new List<string>();
- if (Directory.Exists(dir))
- {
- var files = Directory.GetFiles(dir, $"{baseName}*{ext}").OrderByDescending(f => f);
- foreach (var f in files)
- {
- try
- {
- using var stream = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
- using var reader = new StreamReader(stream);
- while (reader.ReadLine() is { } line)
- {
- if (!string.IsNullOrWhiteSpace(line)) allLines.Add(line);
- }
- }
- catch (IOException)
- {
- // file locked/rolling, skip
- }
- }
- }
-
- var entries = allLines.Select(ParseCompactJsonLine).Where(e => e is not null).Select(e => e!).ToList();
- entries.Reverse(); // newest first (file lines are chronological)
-
- IEnumerable<LogEntryDto> filtered = entries;
-
- if (query.Levels is { Length: > 0 })
- {
- var set = new HashSet<string>(query.Levels, StringComparer.OrdinalIgnoreCase);
- filtered = filtered.Where(e => set.Contains(e.Level));
- }
-
- if (!string.IsNullOrWhiteSpace(query.Search))
- {
- var s = query.Search;
- filtered = filtered.Where(e =>
- e.Message.Contains(s, StringComparison.OrdinalIgnoreCase) ||
- (e.Exception?.Contains(s, StringComparison.OrdinalIgnoreCase) ?? false));
- }
-
- if (query.From is not null)
- {
- var from = query.From.Value;
- filtered = filtered.Where(e => e.Timestamp >= from);
- }
-
- if (query.To is not null)
- {
- var to = query.To.Value;
- filtered = filtered.Where(e => e.Timestamp <= to);
- }
-
- var list = filtered.ToList();
- var total = list.Count;
- var page = Math.Max(1, query.Page);
- var pageSize = Math.Clamp(query.PageSize, 1, 500);
-
- var pageItems = list.Skip((page - 1) * pageSize).Take(pageSize).ToList();
-
- return new LogQueryResult
- {
- Items = pageItems,
- Total = total,
- Page = page,
- PageSize = pageSize,
- Source = "file"
- };
- }
-
- private static LogEntryDto? ParseCompactJsonLine(string line)
- {
- try
- {
- using var doc = JsonDocument.Parse(line);
- var root = doc.RootElement;
-
- var properties = new Dictionary<string, object?>();
- string? messageTemplate = null;
- string? exception = null;
- var timestamp = DateTimeOffset.UtcNow;
- var level = "Information";
-
- foreach (var prop in root.EnumerateObject())
- {
- switch (prop.Name)
- {
- case "@t":
- timestamp = DateTimeOffset.Parse(prop.Value.GetString()!, null, System.Globalization.DateTimeStyles.AssumeUniversal);
- break;
- case "@mt":
- messageTemplate = prop.Value.GetString();
- break;
- case "@l":
- level = prop.Value.GetString() ?? "Information";
- break;
- case "@x":
- exception = prop.Value.GetString();
- break;
- case "@m":
- case "@i":
- case "@r":
- break;
- default:
- properties[prop.Name] = JsonElementToObject(prop.Value);
- break;
- }
- }
-
- var message = RenderMessageTemplate(messageTemplate ?? "", properties);
-
- return new LogEntryDto
- {
- Id = Guid.NewGuid().ToString("N"),
- Timestamp = timestamp,
- Level = NormalizeLevel(level),
- Message = message,
- MessageTemplate = messageTemplate,
- Exception = exception,
- Properties = properties,
- Source = "file",
- RawJson = System.Text.Json.JsonSerializer.Serialize(JsonDocument.Parse(line).RootElement, new JsonSerializerOptions { WriteIndented = true })
- };
- }
- catch
- {
- return null;
- }
- }
-
- private static readonly System.Text.RegularExpressions.Regex TokenRegex =
- new(@"\{(@|\$)?(?<name>[a-zA-Z_][a-zA-Z0-9_]*)(?<format>:[^}]+)?\}", System.Text.RegularExpressions.RegexOptions.Compiled);
-
- private static string RenderMessageTemplate(string template, Dictionary<string, object?> properties)
- {
- return TokenRegex.Replace(template, match =>
- {
- var name = match.Groups["name"].Value;
- if (!properties.TryGetValue(name, out var value))
- return match.Value;
-
- var format = match.Groups["format"].Success ? match.Groups["format"].Value[1..] : null;
-
- if (format is not null && value is IFormattable formattable)
- {
- try { return formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); }
- catch { /* fall through to plain rendering */ }
- }
-
- return value?.ToString() ?? "";
- });
- }
-
- private static object? JsonElementToObject(JsonElement el) => el.ValueKind switch
- {
- JsonValueKind.String => el.GetString(),
- JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),
- JsonValueKind.True => true,
- JsonValueKind.False => false,
- JsonValueKind.Null => null,
- _ => el.GetRawText()
- };
-
- private static string NormalizeLevel(string level) => level switch
- {
- "Verbose" or "VRB" or "Trace" => "Verbose",
- "Debug" or "DBG" => "Debug",
- "Information" or "INF" or "Info" => "Information",
- "Warning" or "WRN" or "Warn" => "Warning",
- "Error" or "ERR" => "Error",
- "Fatal" or "FTL" or "Critical" => "Fatal",
- "" => "Information",
- _ => level
- };
+ public async Task<LogQueryResult> QueryAsync(LogQuery query, CancellationToken ct)
+ {
+ if (string.Equals(query.Source, "file", StringComparison.OrdinalIgnoreCase))
+ {
+ return QueryFile(query);
+ }
+
+ try
+ {
+ return await QueryMongoAsync(query, ct);
+ }
+ catch (Exception ex)
+ {
+ logger.LogWarning(ex, "MongoDB unavailable, falling back to file log source");
+ var result = QueryFile(query);
+ result.Warning = $"MongoDB unavailable ({ex.Message}). Showing file log instead.";
+ return result;
+ }
+ }
+
+ private async Task<LogQueryResult> QueryMongoAsync(LogQuery query, CancellationToken ct)
+ {
+ var client = new MongoClient(options.MongoConnectionString);
+ var db = client.GetDatabase(options.MongoDatabase);
+ var collection = db.GetCollection<BsonDocument>(options.MongoCollection);
+
+ var filters = new List<FilterDefinition<BsonDocument>>();
+ var builder = Builders<BsonDocument>.Filter;
+
+ if (query.Levels is { Length: > 0 })
+ {
+ var levelFilters = query.Levels.Select(l =>
+ builder.Regex("Level", new BsonRegularExpression($"^{System.Text.RegularExpressions.Regex.Escape(l)}", "i")));
+ filters.Add(builder.Or(levelFilters));
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.Search))
+ {
+ var pattern = new BsonRegularExpression(System.Text.RegularExpressions.Regex.Escape(query.Search), "i");
+ filters.Add(builder.Or(
+ builder.Regex("RenderedMessage", pattern),
+ builder.Regex("MessageTemplate", pattern),
+ builder.Regex("Exception", pattern)));
+ }
+
+ if (query.From is not null)
+ {
+ filters.Add(builder.Gte("Timestamp", query.From.Value.UtcDateTime));
+ }
+
+ if (query.To is not null)
+ {
+ filters.Add(builder.Lte("Timestamp", query.To.Value.UtcDateTime));
+ }
+
+ var filter = filters.Count > 0 ? builder.And(filters) : builder.Empty;
+
+ var total = await collection.CountDocumentsAsync(filter, cancellationToken: ct);
+
+ var page = Math.Max(1, query.Page);
+ var pageSize = Math.Clamp(query.PageSize, 1, 500);
+
+ var docs = await collection.Find(filter)
+ .SortByDescending(d => d["Timestamp"])
+ .Skip((page - 1) * pageSize)
+ .Limit(pageSize)
+ .ToListAsync(ct);
+
+ var items = docs.Select(MapMongoDocument).ToList();
+
+ return new LogQueryResult
+ {
+ Items = items,
+ Total = total,
+ Page = page,
+ PageSize = pageSize,
+ Source = "mongo"
+ };
+ }
+
+ private static LogEntryDto MapMongoDocument(BsonDocument doc)
+ {
+ string GetString(params string[] names)
+ {
+ foreach (var name in names)
+ {
+ if (doc.TryGetValue(name, out var v) && !v.IsBsonNull)
+ return v.IsString ? v.AsString : v.ToString() ?? "";
+ }
+ return "";
+ }
+
+ DateTimeOffset GetTimestamp()
+ {
+ foreach (var name in new[] { "Timestamp", "UtcTimeStamp", "@t" })
+ {
+ if (doc.TryGetValue(name, out var v) && !v.IsBsonNull)
+ {
+ return v.BsonType == BsonType.DateTime
+ ? new DateTimeOffset(v.ToUniversalTime(), TimeSpan.Zero)
+ : DateTimeOffset.Parse(v.ToString()!);
+ }
+ }
+ return DateTimeOffset.UtcNow;
+ }
+
+ var properties = new Dictionary<string, object?>();
+ if (doc.TryGetValue("Properties", out var propsVal) && propsVal.IsBsonDocument)
+ {
+ foreach (var el in propsVal.AsBsonDocument)
+ {
+ properties[el.Name] = BsonTypeMapper.MapToDotNetValue(el.Value);
+ }
+ }
+
+ var id = doc.TryGetValue("_id", out var idVal) ? idVal.ToString() ?? "" : "";
+ var json = doc.ToJson(new MongoDB.Bson.IO.JsonWriterSettings { Indent = true });
+
+ return new LogEntryDto
+ {
+ Id = id ?? "",
+ Timestamp = GetTimestamp(),
+ Level = NormalizeLevel(GetString("Level")),
+ Message = GetString("RenderedMessage", "MessageTemplate"),
+ MessageTemplate = GetString("MessageTemplate"),
+ Exception = doc.TryGetValue("Exception", out var exVal) && !exVal.IsBsonNull ? exVal.ToString() : null,
+ Properties = properties,
+ Source = "mongo",
+ RawJson = json
+ };
+ }
+
+ private LogQueryResult QueryFile(LogQuery query)
+ {
+ if (string.IsNullOrWhiteSpace(options.LogFilePath))
+ {
+ return new LogQueryResult
+ {
+ Items = [],
+ Total = 0,
+ Page = Math.Max(1, query.Page),
+ PageSize = Math.Clamp(query.PageSize, 1, 500),
+ Source = "file",
+ Warning = "File logging is not configured (MongoDb:LogFilePath is empty)."
+ };
+ }
+
+ var dir = Path.GetDirectoryName(Path.GetFullPath(options.LogFilePath)) ?? ".";
+ var baseName = Path.GetFileNameWithoutExtension(options.LogFilePath).TrimEnd('-');
+ var ext = Path.GetExtension(options.LogFilePath);
+
+ var allLines = new List<string>();
+ if (Directory.Exists(dir))
+ {
+ var files = Directory.GetFiles(dir, $"{baseName}*{ext}").OrderByDescending(f => f);
+ foreach (var f in files)
+ {
+ try
+ {
+ using var stream = new FileStream(f, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
+ using var reader = new StreamReader(stream);
+ while (reader.ReadLine() is { } line)
+ {
+ if (!string.IsNullOrWhiteSpace(line)) allLines.Add(line);
+ }
+ }
+ catch (IOException)
+ {
+ // file locked/rolling, skip
+ }
+ }
+ }
+
+ var entries = allLines.Select(ParseCompactJsonLine).Where(e => e is not null).Select(e => e!).ToList();
+ entries.Reverse(); // newest first (file lines are chronological)
+
+ IEnumerable<LogEntryDto> filtered = entries;
+
+ if (query.Levels is { Length: > 0 })
+ {
+ var set = new HashSet<string>(query.Levels, StringComparer.OrdinalIgnoreCase);
+ filtered = filtered.Where(e => set.Contains(e.Level));
+ }
+
+ if (!string.IsNullOrWhiteSpace(query.Search))
+ {
+ var s = query.Search;
+ filtered = filtered.Where(e =>
+ e.Message.Contains(s, StringComparison.OrdinalIgnoreCase) ||
+ (e.Exception?.Contains(s, StringComparison.OrdinalIgnoreCase) ?? false));
+ }
+
+ if (query.From is not null)
+ {
+ var from = query.From.Value;
+ filtered = filtered.Where(e => e.Timestamp >= from);
+ }
+
+ if (query.To is not null)
+ {
+ var to = query.To.Value;
+ filtered = filtered.Where(e => e.Timestamp <= to);
+ }
+
+ var list = filtered.ToList();
+ var total = list.Count;
+ var page = Math.Max(1, query.Page);
+ var pageSize = Math.Clamp(query.PageSize, 1, 500);
+
+ var pageItems = list.Skip((page - 1) * pageSize).Take(pageSize).ToList();
+
+ return new LogQueryResult
+ {
+ Items = pageItems,
+ Total = total,
+ Page = page,
+ PageSize = pageSize,
+ Source = "file"
+ };
+ }
+
+ private static LogEntryDto? ParseCompactJsonLine(string line)
+ {
+ try
+ {
+ using var doc = JsonDocument.Parse(line);
+ var root = doc.RootElement;
+
+ var properties = new Dictionary<string, object?>();
+ string? messageTemplate = null;
+ string? exception = null;
+ var timestamp = DateTimeOffset.UtcNow;
+ var level = "Information";
+
+ foreach (var prop in root.EnumerateObject())
+ {
+ switch (prop.Name)
+ {
+ case "@t":
+ timestamp = DateTimeOffset.Parse(prop.Value.GetString()!, null, System.Globalization.DateTimeStyles.AssumeUniversal);
+ break;
+ case "@mt":
+ messageTemplate = prop.Value.GetString();
+ break;
+ case "@l":
+ level = prop.Value.GetString() ?? "Information";
+ break;
+ case "@x":
+ exception = prop.Value.GetString();
+ break;
+ case "@m":
+ case "@i":
+ case "@r":
+ break;
+ default:
+ properties[prop.Name] = JsonElementToObject(prop.Value);
+ break;
+ }
+ }
+
+ var message = RenderMessageTemplate(messageTemplate ?? "", properties);
+
+ return new LogEntryDto
+ {
+ Id = Guid.NewGuid().ToString("N"),
+ Timestamp = timestamp,
+ Level = NormalizeLevel(level),
+ Message = message,
+ MessageTemplate = messageTemplate,
+ Exception = exception,
+ Properties = properties,
+ Source = "file",
+ RawJson = System.Text.Json.JsonSerializer.Serialize(JsonDocument.Parse(line).RootElement, new JsonSerializerOptions { WriteIndented = true })
+ };
+ }
+ catch
+ {
+ return null;
+ }
+ }
+
+ private static readonly System.Text.RegularExpressions.Regex TokenRegex =
+ new(@"\{(@|\$)?(?<name>[a-zA-Z_][a-zA-Z0-9_]*)(?<format>:[^}]+)?\}", System.Text.RegularExpressions.RegexOptions.Compiled);
+
+ private static string RenderMessageTemplate(string template, Dictionary<string, object?> properties)
+ {
+ return TokenRegex.Replace(template, match =>
+ {
+ var name = match.Groups["name"].Value;
+ if (!properties.TryGetValue(name, out var value))
+ return match.Value;
+
+ var format = match.Groups["format"].Success ? match.Groups["format"].Value[1..] : null;
+
+ if (format is not null && value is IFormattable formattable)
+ {
+ try { return formattable.ToString(format, System.Globalization.CultureInfo.InvariantCulture); }
+ catch { /* fall through to plain rendering */ }
+ }
+
+ return value?.ToString() ?? "";
+ });
+ }
+
+ private static object? JsonElementToObject(JsonElement el) => el.ValueKind switch
+ {
+ JsonValueKind.String => el.GetString(),
+ JsonValueKind.Number => el.TryGetInt64(out var l) ? l : el.GetDouble(),
+ JsonValueKind.True => true,
+ JsonValueKind.False => false,
+ JsonValueKind.Null => null,
+ _ => el.GetRawText()
+ };
+
+ private static string NormalizeLevel(string level) => level switch
+ {
+ "Verbose" or "VRB" or "Trace" => "Verbose",
+ "Debug" or "DBG" => "Debug",
+ "Information" or "INF" or "Info" => "Information",
+ "Warning" or "WRN" or "Warn" => "Warning",
+ "Error" or "ERR" => "Error",
+ "Fatal" or "FTL" or "Critical" => "Fatal",
+ "" => "Information",
+ _ => level
+ };
}
diff --git a/SeriLoog/appsettings.json b/SeriLoog/appsettings.json
index 6ad229b..58b5d7c 100644
--- a/SeriLoog/appsettings.json
+++ b/SeriLoog/appsettings.json
@@ -1,17 +1,15 @@
{
- "Logging": {
- "LogLevel": {
- "Default": "Information",
- "Microsoft.AspNetCore": "Warning"
- }
- },
"AllowedHosts": "*",
"ConnectionStrings": {
- "Mongo": "mongodb://localhost:27017"
+ "MongoDb": "mongodb://localhost:27017"
},
- "SeriLoog": {
- "MongoDatabase": "SeriLoog",
- "MongoCollection": "logs",
+ "MongoDb": {
+ "DatabaseName": "serilog_moneywise_nl",
+ "IdleTimeoutInMinutes": 20,
+ "Collections": {
+ "Logs": "Logs"
+ },
"LogFilePath": "Logs/log-.txt"
}
+
}