namespace Sonex.Client.Helpers;
using Npgsql;
using Sonex.Client.Dialogs;
using Sonex.Data.Records;
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
public static class Logger
{
private static readonly object Sync = new();
private static readonly UTF8Encoding Utf8NoBom = new(false);
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
};
private static string? _currentPage;
private static string? _currentPageTitle;
// Chroni przed zapętleniem, gdy wyjątek pojawi się podczas samego zapisu logu.
[ThreadStatic]
private static bool _isWriting;
///
/// Logger nie wymaga ciezkiej inicjalizacji przy starcie.
/// Plik JSON powstaje dopiero przy pierwszym faktycznie zapisanym bledzie.
///
public static void Initialize()
{
}
///
/// Zapisuje zwykly obsluzony wyjatek aplikacji i zwraca identyfikator wpisu.
///
public static string Error(
Exception exception,
string source,
string operation,
IReadOnlyDictionary? context = null,
string? message = null)
{
ArgumentNullException.ThrowIfNull(exception);
return Write("Error", exception, source, operation, message, context, isUnhandled: false);
}
///
/// Zapisuje prosty komunikat bledu bez obiektu wyjatku.
///
public static string Error(
string message,
string source,
string operation,
IReadOnlyDictionary? context = null)
{
return Write("Error", null, source, operation, message, context, isUnhandled: false);
}
///
/// Zapisuje blad krytyczny. Kazdy wpis trafia do osobnego pliku JSON.
///
public static string Fatal(
Exception exception,
string source,
string operation,
bool isUnhandled = true,
IReadOnlyDictionary? context = null,
string? message = null)
{
ArgumentNullException.ThrowIfNull(exception);
return Write("Fatal", exception, source, operation, message, context, isUnhandled);
}
///
/// Rozpoznaje oczekiwane bledy PostgreSQL, ktore w tej aplikacji sa traktowane jako komunikaty biznesowe.
///
public static bool IsBusinessDatabaseException(Exception exception)
{
ArgumentNullException.ThrowIfNull(exception);
if (exception is PostgresException)
return true;
return exception.InnerException != null && IsBusinessDatabaseException(exception.InnerException);
}
///
/// Buduje krotki komunikat dla uzytkownika z identyfikatorem bledu.
///
public static string BuildUserMessage(string errorId)
{
return $"Wystapil nieoczekiwany blad aplikacji. Identyfikator bledu: {errorId}.";
}
///
/// Zwraca wspolna lokalizacje folderu logow, przeniesiona o jeden poziom wyzej niz katalog binarny aplikacji.
///
public static string GetLogsDirectoryPath()
{
return Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "Logs"));
}
///
/// Zapamietuje aktualna strone nawigacji, aby byla automatycznie dopisywana do logow.
///
public static void SetCurrentPage(string? pageName, string? pageTitle = null)
{
lock (Sync)
{
_currentPage = Clean(pageName);
_currentPageTitle = Clean(pageTitle);
}
}
///
/// Pozostawione dla zgodnosci z istniejacym kodem.
/// Przy pojedynczych plikach JSON nie ma tu nic do oprozniania.
///
public static void FlushAndClose()
{
}
///
/// Globalny handler dla AppDomain.CurrentDomain.UnhandledException.
///
public static void OnException(object? sender, System.UnhandledExceptionEventArgs e)
{
var exception = e.ExceptionObject as Exception
?? new InvalidOperationException(e.ExceptionObject?.ToString() ?? "Unknown unhandled exception.");
Fatal(
exception,
source: sender?.GetType().Name ?? "AppDomain",
operation: "AppDomain.CurrentDomain.UnhandledException");
}
///
/// Globalny handler dla AppDomain.CurrentDomain.FirstChanceException.
/// Jesli bedzie wlaczony, kazdy taki wpis zapisze osobny plik JSON.
///
public static void OnException(object? sender, FirstChanceExceptionEventArgs e)
{
if (e.Exception == null)
return;
Write(
"Warning",
e.Exception,
source: sender?.GetType().Name ?? "AppDomain",
operation: "AppDomain.CurrentDomain.FirstChanceException",
message: null,
context: null,
isUnhandled: false);
}
///
/// Globalny handler dla wyjatkow z nieobserwowanych taskow.
///
public static void OnException(object? sender, UnobservedTaskExceptionEventArgs e)
{
Error(
e.Exception,
source: sender?.GetType().Name ?? nameof(TaskScheduler),
operation: "TaskScheduler.UnobservedTaskException");
e.SetObserved();
}
///
/// Globalny handler dla wyjatkow WinUI zglaszanych przez Application.UnhandledException.
///
public static void OnException(object sender, Microsoft.UI.Xaml.UnhandledExceptionEventArgs e)
{
if (e.Exception == null)
return;
Fatal(
e.Exception,
source: sender.GetType().Name,
operation: "Application.UnhandledException",
message: e.Message);
}
///
/// Wspolna sciezka zapisu wszystkich wpisow.
/// Kazdy wpis jest osobnym plikiem JSON w folderze Logs.
///
private static string Write(
string level,
Exception? exception,
string source,
string operation,
string? message,
IReadOnlyDictionary? context,
bool isUnhandled)
{
string errorId = CreateErrorId();
if (_isWriting)
return errorId;
_isWriting = true;
try
{
DateTime occurredAt = DateTime.Now;
AppLoggerRecord entry = BuildEntry(
errorId,
level,
occurredAt,
exception,
source,
operation,
message,
context,
isUnhandled);
string logDirectory = GetLogsDirectoryPath();
Directory.CreateDirectory(logDirectory);
string filePath = Path.Combine(
logDirectory,
$"{occurredAt:yyyyMMdd-HHmmssfff}-{level.ToLowerInvariant()}-{errorId}.json");
string json = JsonSerializer.Serialize(entry, JsonOptions);
File.WriteAllText(filePath, json, Utf8NoBom);
}
catch (Exception ex)
{
WriteFallback(ex);
}
finally
{
_isWriting = false;
}
return errorId;
}
///
/// Buduje obiekt, ktory potem zapisujemy bezposrednio do JSON.
///
private static AppLoggerRecord BuildEntry(
string errorId,
string level,
DateTime occurredAt,
Exception? exception,
string source,
string operation,
string? message,
IReadOnlyDictionary? context,
bool isUnhandled)
{
return new AppLoggerRecord
{
ErrorId = errorId,
OccurredAt = AppLoggerRecord.FormatOccurredAt(occurredAt),
Level = level,
ApplicationName = AppSessionInfo.ApplicationName,
AppVersion = AppInfo.InformationalVersion.Trim(),
InstanceId = AppSessionInfo.InstanceId,
UserName = GetUserName(),
MachineName = Environment.MachineName,
OsVersion = AppSessionInfo.OsVersion,
CurrentPageTitle = GetCurrentPageTitle(),
CurrentPage = GetCurrentPage(),
Source = Clean(source) ?? source,
Operation = Clean(operation) ?? operation,
IsUnhandled = isUnhandled,
Message = ResolveMessage(exception, message),
ReportedMessage = GetReportedMessage(exception, message),
ExceptionType = exception?.GetType().FullName,
Exception = exception?.ToString(),
Context = BuildContext(context),
ExceptionData = BuildExceptionData(exception)
};
}
private static string ResolveMessage(Exception? exception, string? message)
{
string? text = Clean(message);
if (!string.IsNullOrWhiteSpace(text))
return text;
text = Clean(exception?.Message);
if (!string.IsNullOrWhiteSpace(text))
return text;
return exception?.GetType().FullName ?? "Application error";
}
private static string? GetReportedMessage(Exception? exception, string? message)
{
string? cleanMessage = Clean(message);
string? cleanExceptionMessage = Clean(exception?.Message);
if (string.IsNullOrWhiteSpace(cleanMessage))
return null;
if (string.Equals(cleanMessage, cleanExceptionMessage, StringComparison.Ordinal))
return null;
return cleanMessage;
}
///
/// Zwraca dodatkowy kontekst przekazany z miejsca wywolania jako jeden plaski tekst.
///
private static string? BuildContext(IReadOnlyDictionary? context)
{
if (context == null || context.Count == 0)
return null;
var builder = new StringBuilder();
foreach (var pair in context)
{
string? cleanValue = Clean(pair.Value);
if (string.IsNullOrWhiteSpace(cleanValue))
continue;
AppendPair(builder, pair.Key, cleanValue);
}
return builder.Length == 0 ? null : builder.ToString();
}
///
/// Przepisuje exception.Data do tekstu JSON.
/// Dzięki temu glowny dokument pozostaje plaski, a dane dodatkowe dalej sa uporzadkowane.
///
private static string? BuildExceptionData(Exception? exception)
{
if (exception?.Data == null || exception.Data.Count == 0)
return null;
var result = new Dictionary(StringComparer.OrdinalIgnoreCase);
int index = 0;
foreach (DictionaryEntry item in exception.Data)
{
string key = Clean(item.Key?.ToString()) ?? $"Item{index}";
string? value = Clean(item.Value?.ToString());
if (string.IsNullOrWhiteSpace(value))
{
index++;
continue;
}
result[key] = value;
index++;
}
return result.Count == 0
? null
: JsonSerializer.Serialize(result, JsonOptions);
}
private static string? GetCurrentPage()
{
lock (Sync)
{
return _currentPage;
}
}
private static string? GetCurrentPageTitle()
{
lock (Sync)
{
return _currentPageTitle;
}
}
private static string GetUserName()
{
string configured = Config.GetStringValue("USER_NAME", string.Empty);
if (!string.IsNullOrWhiteSpace(configured))
return configured;
return Environment.UserName;
}
private static void AppendPair(StringBuilder builder, string key, string value)
{
if (builder.Length > 0)
builder.Append(" | ");
builder.Append(Clean(key) ?? key);
builder.Append('=');
builder.Append(value);
}
private static string? Clean(string? value)
{
if (string.IsNullOrWhiteSpace(value))
return null;
return value
.Replace("\r", " ")
.Replace("\n", " ")
.Trim();
}
private static string CreateErrorId()
{
return Guid.NewGuid().ToString("N")[..12].ToUpperInvariant();
}
///
/// Awaryjny zapis do prostego pliku JSON, gdy standardowy zapis logu sam rzuci wyjatkiem.
///
private static void WriteFallback(Exception exception)
{
try
{
DateTime occurredAt = DateTime.Now;
string logDirectory = GetLogsDirectoryPath();
Directory.CreateDirectory(logDirectory);
string filePath = Path.Combine(
logDirectory,
$"{occurredAt:yyyyMMdd-HHmmssfff}-logger-fallback-{CreateErrorId()}.json");
var fallback = new
{
OccurredAt = AppLoggerRecord.FormatOccurredAt(occurredAt),
Message = "Logger internal failure.",
Exception = exception.ToString()
};
string json = JsonSerializer.Serialize(fallback, JsonOptions);
File.WriteAllText(filePath, json, Utf8NoBom);
}
catch
{
}
}
}