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>
203 lines
6.4 KiB
C#
203 lines
6.4 KiB
C#
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]";
|
||
}
|
||
}
|
||
}
|