Fix live log reads and timestamp service output

This commit is contained in:
janusz committed 2026-09-18 14:40:44 +00:00
1 parent bb1f2d530d
commit 75ef1ed69c
17 files changed
+198 -6

No files matched your search

+3 -1
View File
@@ -62,7 +62,9 @@ operatora:
Szczegółowe logowanie włącza parametr `/debug` albo `/verbose` (obsługiwane są też Szczegółowe logowanie włącza parametr `/debug` albo `/verbose` (obsługiwane są też
warianty `--debug` i `--verbose`). Wyłącza je brak parametru lub jawny parametr warianty `--debug` i `--verbose`). Wyłącza je brak parametru lub jawny parametr
`/nodebug` albo `/noverbose`. Błędy są zapisywane do `stderr` niezależnie od tego `/nodebug` albo `/noverbose`. Błędy są zapisywane do `stderr` niezależnie od tego
ustawienia. W usłudze Windows parametr można włączyć przez odkomentowanie elementu ustawienia. Każdy wiersz `stdout` i `stderr` zaczyna się znacznikiem czasu ISO 8601
w UTC, np. `2026-09-18T12:34:56.7890000+00:00`. W usłudze Windows parametr można
włączyć przez odkomentowanie elementu
`<arguments>/verbose</arguments>` w `daemon-enova/smartb2bsync-enova.xml` i restart usługi. `<arguments>/verbose</arguments>` w `daemon-enova/smartb2bsync-enova.xml` i restart usługi.
W trybie szczegółowym zapis ZO raportuje kolejne etapy: walidację żądania, logowanie W trybie szczegółowym zapis ZO raportuje kolejne etapy: walidację żądania, logowanie
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -52,8 +52,8 @@
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.VisualBasic.dll" to="%BASE%\..\Microsoft.VisualBasic.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.VisualBasic.dll" to="%BASE%\..\Microsoft.VisualBasic.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.Win32.Primitives.dll" to="%BASE%\..\Microsoft.Win32.Primitives.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.Win32.Primitives.dll" to="%BASE%\..\Microsoft.Win32.Primitives.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.Win32.Registry.dll" to="%BASE%\..\Microsoft.Win32.Registry.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/Microsoft.Win32.Registry.dll" to="%BASE%\..\Microsoft.Win32.Registry.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordaccore.dll" to="%BASE%\..\mscordaccore.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordaccore_x86_x86_8.0.3126.42015.dll" to="%BASE%\..\mscordaccore_x86_x86_8.0.3126.42015.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordaccore_x86_x86_8.0.3126.42015.dll" to="%BASE%\..\mscordaccore_x86_x86_8.0.3126.42015.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordaccore.dll" to="%BASE%\..\mscordaccore.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordbi.dll" to="%BASE%\..\mscordbi.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscordbi.dll" to="%BASE%\..\mscordbi.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscorlib.dll" to="%BASE%\..\mscorlib.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscorlib.dll" to="%BASE%\..\mscorlib.dll" failOnError="false" />
<download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscorrc.dll" to="%BASE%\..\mscorrc.dll" failOnError="false" /> <download from="https://demo.smartb2b.eu/files/smartb2b-sync-optima/mscorrc.dll" to="%BASE%\..\mscorrc.dll" failOnError="false" />
+1
View File
@@ -11,6 +11,7 @@ internal static class Program
{ {
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
ConsoleLogger.InstallTimestampedOutput();
IDisposable? sentry = null; IDisposable? sentry = null;
IErpOrderAdapter? adapter = null; IErpOrderAdapter? adapter = null;
var cancellation = new CancellationTokenSource(); var cancellation = new CancellationTokenSource();
+1
View File
@@ -11,6 +11,7 @@ internal static class Program
{ {
public static async Task<int> Main(string[] args) public static async Task<int> Main(string[] args)
{ {
ConsoleLogger.InstallTimestampedOutput();
IDisposable? sentry = null; IDisposable? sentry = null;
IErpOrderAdapter? adapter = null; IErpOrderAdapter? adapter = null;
var cancellation = new CancellationTokenSource(); var cancellation = new CancellationTokenSource();
@@ -4,6 +4,12 @@ public static class ConsoleLogger
{ {
public static bool VerboseEnabled { get; private set; } public static bool VerboseEnabled { get; private set; }
public static void InstallTimestampedOutput()
{
Console.SetOut(new TimestampedTextWriter(Console.Out));
Console.SetError(new TimestampedTextWriter(Console.Error));
}
public static void Configure(bool verboseEnabled) => VerboseEnabled = verboseEnabled; public static void Configure(bool verboseEnabled) => VerboseEnabled = verboseEnabled;
public static void Verbose(string message) public static void Verbose(string message)
@@ -88,9 +88,16 @@ public sealed partial class DiagnosticsHandler
var compactDate = date.Replace("-", string.Empty, StringComparison.Ordinal); var compactDate = date.Replace("-", string.Empty, StringComparison.Ordinal);
var path = Path.Combine(_logDirectory, $"{_logPrefix}_{compactDate}.{stream}.log"); var path = Path.Combine(_logDirectory, $"{_logPrefix}_{compactDate}.{stream}.log");
return File.Exists(path) if (!File.Exists(path))
? await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false) {
: null; return null;
}
await using var log = new FileStream(
path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete,
bufferSize: 4096, useAsync: true);
using var reader = new StreamReader(log);
return await reader.ReadToEndAsync(cancellationToken).ConfigureAwait(false);
} }
[GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.CultureInvariant)] [GeneratedRegex("^\\d{4}-\\d{2}-\\d{2}$", RegexOptions.CultureInvariant)]
@@ -0,0 +1,93 @@
using System.Globalization;
using System.Text;
namespace SmartB2B.Sync.Service.Diagnostics;
internal sealed class TimestampedTextWriter : TextWriter
{
private readonly TextWriter _inner;
private readonly object _sync = new();
private bool _atLineStart = true;
public TimestampedTextWriter(TextWriter inner) => _inner = inner;
public override Encoding Encoding => _inner.Encoding;
public override void Write(char value)
{
lock (_sync)
{
WriteCore(value.ToString().AsSpan());
}
}
public override void Write(string? value)
{
if (value is null) return;
lock (_sync)
{
WriteCore(value.AsSpan());
}
}
public override void Write(char[] buffer, int index, int count)
{
ArgumentNullException.ThrowIfNull(buffer);
lock (_sync)
{
WriteCore(buffer.AsSpan(index, count));
}
}
public override void Write(ReadOnlySpan<char> buffer)
{
lock (_sync)
{
WriteCore(buffer);
}
}
public override void WriteLine(string? value)
{
lock (_sync)
{
if (value is not null) WriteCore(value.AsSpan());
WriteCore(NewLine.AsSpan());
}
}
public override void WriteLine()
{
lock (_sync)
{
WriteCore(NewLine.AsSpan());
}
}
public override void Flush()
{
lock (_sync)
{
_inner.Flush();
}
}
private void WriteCore(ReadOnlySpan<char> text)
{
while (!text.IsEmpty)
{
if (_atLineStart)
{
_inner.Write(DateTimeOffset.UtcNow.ToString("O", CultureInfo.InvariantCulture));
_inner.Write(' ');
_atLineStart = false;
}
var newline = text.IndexOf('\n');
var length = newline >= 0 ? newline + 1 : text.Length;
_inner.Write(text[..length]);
_atLineStart = newline >= 0;
text = text[length..];
}
}
}
+83 -1
View File
@@ -1,7 +1,9 @@
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Globalization;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using SmartB2B.Optima.Adapter; using SmartB2B.Optima.Adapter;
using SmartB2B.Sync.Contracts; using SmartB2B.Sync.Contracts;
using SmartB2B.Sync.Service.Diagnostics;
using SmartB2B.Sync.Service.Orders; using SmartB2B.Sync.Service.Orders;
using SmartB2B.Sync.Service.Rpc; using SmartB2B.Sync.Service.Rpc;
using WampSharp.V2.Core.Contracts; using WampSharp.V2.Core.Contracts;
@@ -26,7 +28,9 @@ var tests = new (string Name, Func<Task> Run)[]
("Wynik Optimy zachowuje kwargs WAMP", HandlerReturnsKeywordResult), ("Wynik Optimy zachowuje kwargs WAMP", HandlerReturnsKeywordResult),
("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized), ("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized),
("Fasada COM działa na jednym wątku STA", FacadeRunsOnSingleStaThread), ("Fasada COM działa na jednym wątku STA", FacadeRunsOnSingleStaThread),
("Adapter serializuje bezpośrednie wywołania COM", AdapterSerializesComCalls) ("Adapter serializuje bezpośrednie wywołania COM", AdapterSerializesComCalls),
("Diagnostyka czyta log podczas zapisu", DiagnosticsReadsOpenLogs),
("Każdy wiersz logu ma czas ISO", ConsoleOutputHasIsoTimestamps)
}; };
var failures = 0; var failures = 0;
@@ -111,6 +115,84 @@ static async Task HandlerMapsOptimaContract()
AssertEqual(123m, request.Items[0].UnitPriceGross, "brutto"); AssertEqual(123m, request.Items[0].UnitPriceGross, "brutto");
} }
static async Task DiagnosticsReadsOpenLogs()
{
var directory = Path.Combine(Path.GetTempPath(), $"smartb2b-log-test-{Guid.NewGuid():N}");
Directory.CreateDirectory(directory);
try
{
var diagnostics = new DiagnosticsHandler("test", "1", ".NET", "test", null, directory, "service");
var invocation = new RpcInvocation([], new Dictionary<string, object?> { ["date"] = "2026-09-18" });
foreach (var stream in new[] { "out", "err" })
{
var path = Path.Combine(directory, $"service_20260918.{stream}.log");
await using var writer = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.ReadWrite);
var content = $"Log {stream} zażółć";
var bytes = System.Text.Encoding.UTF8.GetBytes(content);
await writer.WriteAsync(bytes);
await writer.FlushAsync();
var result = stream == "out"
? await diagnostics.GetLogAsync(invocation, CancellationToken.None)
: await diagnostics.GetErrorLogAsync(invocation, CancellationToken.None);
AssertEqual(content, result, $"odczyt otwartego pliku {stream}");
}
}
finally
{
Directory.Delete(directory, recursive: true);
}
}
static Task ConsoleOutputHasIsoTimestamps()
{
var originalOut = Console.Out;
var originalError = Console.Error;
using var output = new StringWriter();
using var error = new StringWriter();
try
{
Console.SetOut(output);
Console.SetError(error);
ConsoleLogger.InstallTimestampedOutput();
Console.Write("pierw");
Console.WriteLine("szy\ndrugi");
ConsoleLogger.Error("błąd\nszczegóły");
AssertTimestampedLines(output.ToString(), ["pierwszy", "drugi"]);
AssertTimestampedLines(error.ToString(), ["błąd", "szczegóły"]);
}
finally
{
Console.SetOut(originalOut);
Console.SetError(originalError);
}
return Task.CompletedTask;
}
static void AssertTimestampedLines(string output, string[] expectedMessages)
{
var lines = output.Split('\n', StringSplitOptions.RemoveEmptyEntries);
AssertEqual(expectedMessages.Length, lines.Length, "liczba wierszy logu");
for (var index = 0; index < lines.Length; index++)
{
var line = lines[index].TrimEnd('\r');
var separator = line.IndexOf(' ');
if (separator < 0 ||
!DateTimeOffset.TryParseExact(line[..separator], "O", CultureInfo.InvariantCulture,
DateTimeStyles.None, out var timestamp) ||
timestamp.Offset != TimeSpan.Zero)
{
throw new InvalidOperationException($"Brak czasu ISO UTC w wierszu: {line}");
}
AssertEqual(expectedMessages[index], line[(separator + 1)..], "treść wiersza logu");
}
}
static async Task HandlerMapsOptionalExtraFields() static async Task HandlerMapsOptionalExtraFields()
{ {
using var adapter = new CapturingAdapter(); using var adapter = new CapturingAdapter();