diff --git a/README.md b/README.md index 39ef667..56ebd28 100644 --- a/README.md +++ b/README.md @@ -62,7 +62,9 @@ operatora: 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 `/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 `/verbose` w `daemon-enova/smartb2bsync-enova.xml` i restart usługi. W trybie szczegółowym zapis ZO raportuje kolejne etapy: walidację żądania, logowanie diff --git a/release-optima/SmartB2B.Optima.Adapter.dll b/release-optima/SmartB2B.Optima.Adapter.dll index f3d7e3e..3d9897e 100644 Binary files a/release-optima/SmartB2B.Optima.Adapter.dll and b/release-optima/SmartB2B.Optima.Adapter.dll differ diff --git a/release-optima/SmartB2B.Optima.Adapter.pdb b/release-optima/SmartB2B.Optima.Adapter.pdb index 75b0992..881437c 100644 Binary files a/release-optima/SmartB2B.Optima.Adapter.pdb and b/release-optima/SmartB2B.Optima.Adapter.pdb differ diff --git a/release-optima/SmartB2B.Optima.Service.dll b/release-optima/SmartB2B.Optima.Service.dll index 8e5b599..2be45da 100644 Binary files a/release-optima/SmartB2B.Optima.Service.dll and b/release-optima/SmartB2B.Optima.Service.dll differ diff --git a/release-optima/SmartB2B.Optima.Service.exe b/release-optima/SmartB2B.Optima.Service.exe index d69d8ca..3aa1081 100644 Binary files a/release-optima/SmartB2B.Optima.Service.exe and b/release-optima/SmartB2B.Optima.Service.exe differ diff --git a/release-optima/SmartB2B.Optima.Service.pdb b/release-optima/SmartB2B.Optima.Service.pdb index 7d4bc1a..e85dfe4 100644 Binary files a/release-optima/SmartB2B.Optima.Service.pdb and b/release-optima/SmartB2B.Optima.Service.pdb differ diff --git a/release-optima/SmartB2B.Sync.Contracts.dll b/release-optima/SmartB2B.Sync.Contracts.dll index 4746a50..4788d29 100644 Binary files a/release-optima/SmartB2B.Sync.Contracts.dll and b/release-optima/SmartB2B.Sync.Contracts.dll differ diff --git a/release-optima/SmartB2B.Sync.Contracts.pdb b/release-optima/SmartB2B.Sync.Contracts.pdb index 26f9100..d82a385 100644 Binary files a/release-optima/SmartB2B.Sync.Contracts.pdb and b/release-optima/SmartB2B.Sync.Contracts.pdb differ diff --git a/release-optima/SmartB2B.Sync.Service.dll b/release-optima/SmartB2B.Sync.Service.dll index 99652e8..e40ba0f 100644 Binary files a/release-optima/SmartB2B.Sync.Service.dll and b/release-optima/SmartB2B.Sync.Service.dll differ diff --git a/release-optima/SmartB2B.Sync.Service.pdb b/release-optima/SmartB2B.Sync.Service.pdb index 31edbbd..6a56e96 100644 Binary files a/release-optima/SmartB2B.Sync.Service.pdb and b/release-optima/SmartB2B.Sync.Service.pdb differ diff --git a/release-optima/daemon/smartb2bsync-optima.xml b/release-optima/daemon/smartb2bsync-optima.xml index 8402eca..9bde0f9 100644 --- a/release-optima/daemon/smartb2bsync-optima.xml +++ b/release-optima/daemon/smartb2bsync-optima.xml @@ -52,8 +52,8 @@ - + diff --git a/src/SmartB2B.Enova.Service/Program.cs b/src/SmartB2B.Enova.Service/Program.cs index 231300d..0a236e3 100644 --- a/src/SmartB2B.Enova.Service/Program.cs +++ b/src/SmartB2B.Enova.Service/Program.cs @@ -11,6 +11,7 @@ internal static class Program { public static async Task Main(string[] args) { + ConsoleLogger.InstallTimestampedOutput(); IDisposable? sentry = null; IErpOrderAdapter? adapter = null; var cancellation = new CancellationTokenSource(); diff --git a/src/SmartB2B.Optima.Service/Program.cs b/src/SmartB2B.Optima.Service/Program.cs index f04b670..92da474 100644 --- a/src/SmartB2B.Optima.Service/Program.cs +++ b/src/SmartB2B.Optima.Service/Program.cs @@ -11,6 +11,7 @@ internal static class Program { public static async Task Main(string[] args) { + ConsoleLogger.InstallTimestampedOutput(); IDisposable? sentry = null; IErpOrderAdapter? adapter = null; var cancellation = new CancellationTokenSource(); diff --git a/src/SmartB2B.Sync.Service/Diagnostics/ConsoleLogger.cs b/src/SmartB2B.Sync.Service/Diagnostics/ConsoleLogger.cs index ef5a16f..dd6ad4f 100644 --- a/src/SmartB2B.Sync.Service/Diagnostics/ConsoleLogger.cs +++ b/src/SmartB2B.Sync.Service/Diagnostics/ConsoleLogger.cs @@ -4,6 +4,12 @@ public static class ConsoleLogger { 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 Verbose(string message) diff --git a/src/SmartB2B.Sync.Service/Diagnostics/DiagnosticsHandler.cs b/src/SmartB2B.Sync.Service/Diagnostics/DiagnosticsHandler.cs index 6395920..053735a 100644 --- a/src/SmartB2B.Sync.Service/Diagnostics/DiagnosticsHandler.cs +++ b/src/SmartB2B.Sync.Service/Diagnostics/DiagnosticsHandler.cs @@ -88,9 +88,16 @@ public sealed partial class DiagnosticsHandler var compactDate = date.Replace("-", string.Empty, StringComparison.Ordinal); var path = Path.Combine(_logDirectory, $"{_logPrefix}_{compactDate}.{stream}.log"); - return File.Exists(path) - ? await File.ReadAllTextAsync(path, cancellationToken).ConfigureAwait(false) - : null; + if (!File.Exists(path)) + { + 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)] diff --git a/src/SmartB2B.Sync.Service/Diagnostics/TimestampedTextWriter.cs b/src/SmartB2B.Sync.Service/Diagnostics/TimestampedTextWriter.cs new file mode 100644 index 0000000..7b84407 --- /dev/null +++ b/src/SmartB2B.Sync.Service/Diagnostics/TimestampedTextWriter.cs @@ -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 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 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..]; + } + } +} diff --git a/tests/SmartB2B.Optima.Tests/Program.cs b/tests/SmartB2B.Optima.Tests/Program.cs index f7024a5..6734e5e 100644 --- a/tests/SmartB2B.Optima.Tests/Program.cs +++ b/tests/SmartB2B.Optima.Tests/Program.cs @@ -1,7 +1,9 @@ using System.Collections.Concurrent; +using System.Globalization; using Newtonsoft.Json.Linq; using SmartB2B.Optima.Adapter; using SmartB2B.Sync.Contracts; +using SmartB2B.Sync.Service.Diagnostics; using SmartB2B.Sync.Service.Orders; using SmartB2B.Sync.Service.Rpc; using WampSharp.V2.Core.Contracts; @@ -26,7 +28,9 @@ var tests = new (string Name, Func Run)[] ("Wynik Optimy zachowuje kwargs WAMP", HandlerReturnsKeywordResult), ("Operacje zamówień są serializowane", ConcurrentOrdersAreSerialized), ("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; @@ -111,6 +115,84 @@ static async Task HandlerMapsOptimaContract() 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 { ["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() { using var adapter = new CapturingAdapter();