elfisa-pharmacy/src/ElectronicPharmacy/Classes/AppDebugLog.cs
Magomed c447048b66 Add invoice request from orders and fix overlapping context menus.
Enable Получить накладную for sent orders, build local invoices without refusals, and stop the tab close menu from covering dedicated grid menus. Also fix price-list Backspace search and refresh the Release dist binaries.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-16 14:00:50 +03:00

203 lines
6.4 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System;
using System.Diagnostics;
using System.IO;
using System.Text;
using System.Text.RegularExpressions;
namespace Электроннаяармация.Classes
{
/// <summary>
/// File logger for API/auth/network debugging.
/// Logs are written to Logs/api-debug-YYYY-MM-DD.log next to the executable.
/// </summary>
public static class AppDebugLog
{
private static readonly object Sync = new object();
private const int MaxBodyLength = 4000;
private const string DisableEnvVar = "ELF_API_DEBUG";
private const string DisableMarkerFile = "api-debug.off";
public static bool IsEnabled
{
get
{
var env = Environment.GetEnvironmentVariable(DisableEnvVar);
if (string.Equals(env, "0", StringComparison.OrdinalIgnoreCase) ||
string.Equals(env, "false", StringComparison.OrdinalIgnoreCase))
{
return false;
}
var markerPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory, DisableMarkerFile);
return !File.Exists(markerPath);
}
}
public static string LogDirectory =>
Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "Logs");
public static string CurrentLogFilePath =>
Path.Combine(LogDirectory, $"api-debug-{DateTime.Now:yyyy-MM-dd}.log");
public static void Info(string category, string message)
{
Write("INFO", category, message, null);
}
public static void Warning(string category, string message)
{
Write("WARN", category, message, null);
}
public static void Error(string category, string message, Exception exception = null)
{
Write("ERROR", category, message, exception);
}
public static void ApiRequest(string method, string url, string requestBody = null, string extra = null)
{
var message = new StringBuilder();
message.Append(method).Append(" ").Append(url);
if (!string.IsNullOrWhiteSpace(extra))
{
message.Append(" | ").Append(extra);
}
if (!string.IsNullOrWhiteSpace(requestBody))
{
message.Append(" | body=").Append(Truncate(SanitizeJson(requestBody)));
}
Write("REQ", "Api", message.ToString(), null);
}
public static void ApiResponse(string method, string url, int statusCode, long elapsedMs, string responseBody = null)
{
var message = new StringBuilder();
message.Append(method).Append(" ").Append(url);
message.Append(" -> HTTP ").Append(statusCode);
message.Append(" (").Append(elapsedMs).Append(" ms)");
if (!string.IsNullOrWhiteSpace(responseBody))
{
message.Append(" | body=").Append(Truncate(SanitizeJson(responseBody)));
}
Write(statusCode >= 400 ? "RESP-ERR" : "RESP", "Api", message.ToString(), null);
}
public static void ApiFailure(string method, string url, long elapsedMs, Exception exception)
{
var message = $"{method} {url} failed after {elapsedMs} ms: {exception.Message}";
Write("FAIL", "Api", message, exception);
}
public static string MaskToken(string token)
{
if (string.IsNullOrWhiteSpace(token))
{
return "(empty)";
}
if (token.Length <= 8)
{
return "***";
}
return token.Substring(0, 4) + "..." + token.Substring(token.Length - 4);
}
public static string SanitizeJson(string json)
{
if (string.IsNullOrWhiteSpace(json))
{
return string.Empty;
}
var sanitized = Regex.Replace(
json,
@"""password""\s*:\s*""[^""]*""",
@"""password"":""***""",
RegexOptions.IgnoreCase);
sanitized = Regex.Replace(
sanitized,
@"""token""\s*:\s*""([^""]*)""",
m => @"""token"":""" + MaskToken(m.Groups[1].Value) + @"""",
RegexOptions.IgnoreCase);
return sanitized;
}
public static string FormatException(Exception exception)
{
if (exception == null)
{
return string.Empty;
}
var sb = new StringBuilder();
var current = exception;
var depth = 0;
while (current != null && depth < 8)
{
if (depth > 0)
{
sb.AppendLine();
sb.Append(" -> ");
}
sb.Append(current.GetType().Name).Append(": ").Append(current.Message);
current = current.InnerException;
depth++;
}
return sb.ToString();
}
private static void Write(string level, string category, string message, Exception exception)
{
if (!IsEnabled)
{
return;
}
try
{
var line = new StringBuilder();
line.Append(DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.fff"));
line.Append(" [").Append(level).Append("] ");
line.Append("[").Append(category).Append("] ");
line.Append(message);
if (exception != null)
{
line.Append(" | ").Append(FormatException(exception));
}
lock (Sync)
{
Directory.CreateDirectory(LogDirectory);
File.AppendAllText(CurrentLogFilePath, line + Environment.NewLine, Encoding.UTF8);
}
Debug.WriteLine(line.ToString());
}
catch
{
// Logging must never break the app.
}
}
private static string Truncate(string text)
{
if (string.IsNullOrEmpty(text) || text.Length <= MaxBodyLength)
{
return text ?? string.Empty;
}
return text.Substring(0, MaxBodyLength) + "...[truncated]";
}
}
}