diff --git a/README.md b/README.md index 6b83847..c43bd45 100644 --- a/README.md +++ b/README.md @@ -35,6 +35,20 @@ Kontrola konfiguracji i dynamicznego ładowania, bez zapisu do bazy: .\src\SmartB2B.Enova.Service\bin\Debug\net8.0\SmartB2B.Enova.Service.exe --config .\src\SmartB2B.Enova.Service\bin\Debug\net8.0\config\enova.json.sample --check-config ``` +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 +`/verbose` w `daemon/smartb2bsync-enova.xml` i restart usługi. + +W trybie szczegółowym zapis ZO raportuje kolejne etapy: walidację żądania, logowanie +do Enovy, wyszukanie definicji, kontrahenta, magazynu i towarów, commit transakcji +oraz `session.Save()`. Sentry zawsze wypisuje błędy własnego transportu, a w trybie +szczegółowym również przebieg kolejkowania i wysyłania zdarzenia. Przy starcie usługa +podaje używany cel Sentry bez ujawniania klucza DSN. Jeżeli lokalny `config/enova.json` +nie zawiera `sentry.dsn`, usługa wypisuje dokładną ścieżkę używanego pliku. Aktualizacja +nie kopiuje tej wartości automatycznie z `enova.json.sample` do istniejącej konfiguracji. + ## Publikacja i instalacja ```powershell diff --git a/daemon/smartb2bsync-enova.xml b/daemon/smartb2bsync-enova.xml index b69c61b..da3589a 100644 --- a/daemon/smartb2bsync-enova.xml +++ b/daemon/smartb2bsync-enova.xml @@ -4,6 +4,8 @@ Synchronizuje enova365 z portalem SmartB2B. %BASE%\..\SmartB2B.Enova.Service.exe %BASE%\.. + + 30sec yyyyMMdd diff --git a/release/SmartB2B.Enova.Contracts.dll b/release/SmartB2B.Enova.Contracts.dll index 47d11f4..498b0ee 100644 Binary files a/release/SmartB2B.Enova.Contracts.dll and b/release/SmartB2B.Enova.Contracts.dll differ diff --git a/release/SmartB2B.Enova.Contracts.pdb b/release/SmartB2B.Enova.Contracts.pdb index 01b0636..612b5af 100644 Binary files a/release/SmartB2B.Enova.Contracts.pdb and b/release/SmartB2B.Enova.Contracts.pdb differ diff --git a/release/SmartB2B.Enova.Service.dll b/release/SmartB2B.Enova.Service.dll index df5e434..d9e1fa8 100644 Binary files a/release/SmartB2B.Enova.Service.dll and b/release/SmartB2B.Enova.Service.dll differ diff --git a/release/SmartB2B.Enova.Service.exe b/release/SmartB2B.Enova.Service.exe index 7414d8c..659f976 100644 Binary files a/release/SmartB2B.Enova.Service.exe and b/release/SmartB2B.Enova.Service.exe differ diff --git a/release/SmartB2B.Enova.Service.pdb b/release/SmartB2B.Enova.Service.pdb index df3ac21..ae32ff1 100644 Binary files a/release/SmartB2B.Enova.Service.pdb and b/release/SmartB2B.Enova.Service.pdb differ diff --git a/release/adapters/SmartB2B.Enova.Adapter.dll b/release/adapters/SmartB2B.Enova.Adapter.dll index e8d75d9..48ffe9f 100644 Binary files a/release/adapters/SmartB2B.Enova.Adapter.dll and b/release/adapters/SmartB2B.Enova.Adapter.dll differ diff --git a/release/daemon/smartb2bsync-enova.xml b/release/daemon/smartb2bsync-enova.xml index b69c61b..da3589a 100644 --- a/release/daemon/smartb2bsync-enova.xml +++ b/release/daemon/smartb2bsync-enova.xml @@ -4,6 +4,8 @@ Synchronizuje enova365 z portalem SmartB2B. %BASE%\..\SmartB2B.Enova.Service.exe %BASE%\.. + + 30sec yyyyMMdd diff --git a/src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs b/src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs index d6cd4d9..3245be9 100644 --- a/src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs +++ b/src/SmartB2B.Enova.Adapter/EnovaOrderAdapter.cs @@ -53,9 +53,14 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter try { + Verbose(configuration.VerboseLogging, "Enova: przygotowanie stanu sesji."); var sessionState = SessionState.Create(); using var attachedSessionState = sessionState.Attach(); + Verbose( + configuration.VerboseLogging, + $"Enova: logowanie operatora '{configuration.Connection.OperatorName}' do bazy " + + $"'{configuration.Connection.DatabaseName}'."); using var login = database.Login( winAuth: false, user: configuration.Connection.OperatorName, @@ -64,8 +69,9 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter readOnly: false, config: false, name: "SmartB2B Enova Sync"); + Verbose(configuration.VerboseLogging, "Enova: utworzono sesję zapisu."); - return CreateOrderInSession(session, request); + return CreateOrderInSession(session, request, configuration.VerboseLogging); } catch (EnovaOperationException) { @@ -81,8 +87,12 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter } } - private static OrderResult CreateOrderInSession(Session session, OrderRequest request) + private static OrderResult CreateOrderInSession( + Session session, + OrderRequest request, + bool verboseLogging) { + Verbose(verboseLogging, "Enova: pobieranie modułów Handel, CRM, Towary i Magazyny."); var handel = HandelModule.GetInstance(session); var crm = CRMModule.GetInstance(session); var towary = TowaryModule.GetInstance(session); @@ -93,11 +103,13 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter "eu.smartb2b.erp.document_definition_not_found", $"Nie znaleziono definicji dokumentu '{request.DocumentDefinition}'.", new Dictionary { ["documentDefinition"] = request.DocumentDefinition }); + Verbose(verboseLogging, $"Enova: znaleziono definicję dokumentu '{definition.Symbol}'."); var customer = crm.Kontrahenci.WgKodu[request.CustomerCode] ?? throw new EnovaOperationException( "eu.smartb2b.company_not_found", $"Nie znaleziono kontrahenta o kodzie '{request.CustomerCode}'.", new Dictionary { ["companyErpId"] = request.CustomerCode }); + Verbose(verboseLogging, $"Enova: znaleziono kontrahenta '{customer.Kod}'."); var warehouse = string.IsNullOrWhiteSpace(request.WarehouseCode) ? warehouses.StandardowyMagazyn : warehouses.GetGrantedView() @@ -116,15 +128,18 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter : $"Nie znaleziono magazynu o symbolu '{request.WarehouseCode.Trim()}'.", new Dictionary { ["warehouseErpId"] = request.WarehouseCode }); } + Verbose(verboseLogging, $"Enova: wybrano magazyn '{warehouse.Symbol}'."); DokumentHandlowy document; + Verbose(verboseLogging, "Enova: rozpoczęcie transakcji edycyjnej."); using (var transaction = session.Logout(editMode: true)) { document = session.AddRow(new DokumentHandlowy()); document.Definicja = definition; document.Kontrahent = customer; document.Magazyn = warehouse; + Verbose(verboseLogging, "Enova: dodano nagłówek dokumentu."); if (!string.IsNullOrWhiteSpace(request.CustomerReference)) { @@ -137,8 +152,12 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter document.Opis.Add(request.Notes.Trim()); } - foreach (var line in request.Items) + for (var index = 0; index < request.Items.Count; index++) { + var line = request.Items[index]; + Verbose( + verboseLogging, + $"Enova: pozycja {index + 1}/{request.Items.Count}, wyszukiwanie towaru '{line.ProductCode}'."); var product = towary.Towary.WgKodu[line.ProductCode] ?? throw new EnovaOperationException( "eu.smartb2b.product_not_found", @@ -150,6 +169,7 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter position.Ilosc = new Quantity((double)line.Quantity); position.Cena = new DoubleCy(line.UnitPrice, CurrencySymbol); position.UstawRabat(new Percent(line.Discount / 100m), ręcznie: true); + Verbose(verboseLogging, $"Enova: dodano pozycję {index + 1}."); } document.Stan = request.SaveMode switch @@ -160,11 +180,17 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter "eu.smartb2b.erp.invalid_order", $"Nieobsługiwany tryb zapisu: {request.SaveMode}.") }; + Verbose(verboseLogging, $"Enova: ustawiono stan dokumentu na {request.SaveMode}."); transaction.Commit(); + Verbose(verboseLogging, "Enova: zatwierdzono transakcję edycyjną."); } + Verbose(verboseLogging, "Enova: wykonywanie session.Save()."); session.Save(); + Verbose( + verboseLogging, + $"Enova: session.Save() zakończone; numer={document.Numer.Pelny}, ID={document.ID}."); return new OrderResult( document.ID, @@ -271,4 +297,12 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter private static string GetAssemblyVersion(Assembly assembly) => assembly.GetName().Version?.ToString() ?? "unknown"; + + private static void Verbose(bool enabled, string message) + { + if (enabled) + { + Console.WriteLine($"[verbose] {message}"); + } + } } diff --git a/src/SmartB2B.Enova.Contracts/EnovaContracts.cs b/src/SmartB2B.Enova.Contracts/EnovaContracts.cs index 39b8c1c..289a803 100644 --- a/src/SmartB2B.Enova.Contracts/EnovaContracts.cs +++ b/src/SmartB2B.Enova.Contracts/EnovaContracts.cs @@ -16,7 +16,8 @@ public sealed record EnovaAdapterConfiguration( EnovaConnectionOptions Connection, string DocumentDefinition, string? DefaultWarehouseCode, - OrderSaveMode SaveMode); + OrderSaveMode SaveMode, + bool VerboseLogging = false); public sealed class OrderRequest { diff --git a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs index af3940c..2fdbea2 100644 --- a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs +++ b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs @@ -48,7 +48,7 @@ public sealed class ServiceSettings return settings; } - public RuntimeSettings ResolveRuntimeSettings() + public RuntimeSettings ResolveRuntimeSettings(bool verboseLogging = false) { var installationPath = Path.GetFullPath( Environment.ExpandEnvironmentVariables(Enova.InstallationPath)); @@ -64,7 +64,8 @@ public sealed class ServiceSettings Sql.ConnectionString), Enova.DocumentDefinition, Enova.DefaultWarehouseCode, - Enova.SaveMode); + Enova.SaveMode, + verboseLogging); } return new RuntimeSettings( diff --git a/src/SmartB2B.Enova.Service/Diagnostics/ConsoleLogger.cs b/src/SmartB2B.Enova.Service/Diagnostics/ConsoleLogger.cs new file mode 100644 index 0000000..be192d6 --- /dev/null +++ b/src/SmartB2B.Enova.Service/Diagnostics/ConsoleLogger.cs @@ -0,0 +1,28 @@ +namespace SmartB2B.Enova.Service.Diagnostics; + +internal static class ConsoleLogger +{ + public static bool VerboseEnabled { get; private set; } + + public static void Configure(bool verboseEnabled) + { + VerboseEnabled = verboseEnabled; + } + + public static void Verbose(string message) + { + if (VerboseEnabled) + { + Console.WriteLine($"[verbose] {message}"); + } + } + + public static void Error(string message, Exception? exception = null) + { + Console.Error.WriteLine(message); + if (VerboseEnabled && exception is not null) + { + Console.Error.WriteLine(exception); + } + } +} diff --git a/src/SmartB2B.Enova.Service/Diagnostics/SentryReporter.cs b/src/SmartB2B.Enova.Service/Diagnostics/SentryReporter.cs new file mode 100644 index 0000000..1d92bbf --- /dev/null +++ b/src/SmartB2B.Enova.Service/Diagnostics/SentryReporter.cs @@ -0,0 +1,28 @@ +using Sentry; + +namespace SmartB2B.Enova.Service.Diagnostics; + +internal static class SentryReporter +{ + private static readonly TimeSpan FlushTimeout = TimeSpan.FromSeconds(5); + + public static async Task CaptureAsync(Exception exception, Action? configureScope = null) + { + try + { + var eventId = configureScope is null + ? SentrySdk.CaptureException(exception) + : SentrySdk.CaptureException(exception, configureScope); + + ConsoleLogger.Verbose($"Sentry: zakolejkowano zdarzenie {eventId}."); + await SentrySdk.FlushAsync(FlushTimeout).ConfigureAwait(false); + ConsoleLogger.Verbose("Sentry: zakończono oczekiwanie na opróżnienie kolejki."); + } + catch (Exception sentryException) + { + ConsoleLogger.Error( + $"Błąd raportowania do Sentry: {sentryException.Message}", + sentryException); + } + } +} diff --git a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs index 3cfe779..d63ca2b 100644 --- a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs +++ b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs @@ -1,6 +1,6 @@ using Newtonsoft.Json; -using Sentry; using SmartB2B.Enova.Contracts; +using SmartB2B.Enova.Service.Diagnostics; using SmartB2B.Enova.Service.Rpc; namespace SmartB2B.Enova.Service.Orders; @@ -60,6 +60,10 @@ public sealed class PlaceOrderHandler new Dictionary { ["errors"] = errors }); } + ConsoleLogger.Verbose( + $"ZO: żądanie zweryfikowane; definicja={request.DocumentDefinition}, " + + $"magazyn={request.WarehouseCode ?? "(standardowy)"}, tryb={request.SaveMode}."); + ConsoleLogger.Verbose("ZO: oczekiwanie na wyłączną sekcję operacji Enovy."); await _orderLock.WaitAsync(cancellationToken).ConfigureAwait(false); try { @@ -69,20 +73,28 @@ public sealed class PlaceOrderHandler OrderResult result; try { + ConsoleLogger.Verbose("ZO: przekazanie żądania do adaptera Enovy."); result = await Task.Run(() => _adapter.CreateOrder(request), cancellationToken) .ConfigureAwait(false); } catch (EnovaOperationException exception) { - SentrySdk.CaptureException(exception, scope => - { - scope.SetTag("rpc.procedure", "eu.smartb2b.place_order"); - scope.SetTag("wamp.error_uri", exception.ErrorUri); - }); + ConsoleLogger.Error( + $"Błąd tworzenia ZO [{exception.ErrorUri}]: {exception.Message}", + exception); + await SentryReporter.CaptureAsync(exception, scope => + { + scope.SetTag("rpc.procedure", "eu.smartb2b.place_order"); + scope.SetTag("wamp.error_uri", exception.ErrorUri); + }) + .ConfigureAwait(false); throw WampErrorFactory.FromEnova(exception); } Console.WriteLine($"Utworzono ZO {result.Number} (ID: {result.Id})."); + ConsoleLogger.Verbose( + $"ZO: zapis zakończony; stan={result.SaveMode}, magazyn={result.WarehouseCode}, " + + $"netto={result.Net}, VAT={result.Vat}, brutto={result.Gross}."); return new KeywordResult(new Dictionary { ["order_erp_id"] = result.Id, @@ -95,6 +107,7 @@ public sealed class PlaceOrderHandler finally { _orderLock.Release(); + ConsoleLogger.Verbose("ZO: zwolniono wyłączną sekcję operacji Enovy."); } } } diff --git a/src/SmartB2B.Enova.Service/Program.cs b/src/SmartB2B.Enova.Service/Program.cs index 307e11d..5084133 100644 --- a/src/SmartB2B.Enova.Service/Program.cs +++ b/src/SmartB2B.Enova.Service/Program.cs @@ -6,6 +6,7 @@ using SmartB2B.Enova.Service.Rpc; using SmartB2B.Enova.Service.Runtime; using SmartB2B.Enova.Service.Sql; using Sentry; +using Sentry.Infrastructure; using WampSharp.V2.Rpc; namespace SmartB2B.Enova.Service; @@ -26,9 +27,13 @@ internal static class Program try { var commandLine = CommandLineOptions.Parse(args); + ConsoleLogger.Configure(commandLine.VerboseLogging); + ConsoleLogger.Verbose( + $"Włączono szczegółowe logowanie; konfiguracja: '{commandLine.ConfigurationPath}'."); var settings = ServiceSettings.Load(commandLine.ConfigurationPath); - sentry = InitializeSentry(settings); - var runtime = settings.ResolveRuntimeSettings(); + ConsoleLogger.Verbose("Wczytano i zweryfikowano konfigurację usługi."); + sentry = InitializeSentry(settings, commandLine.ConfigurationPath, commandLine.VerboseLogging); + var runtime = settings.ResolveRuntimeSettings(commandLine.VerboseLogging); using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath); var adapter = adapterLoader.Load(); @@ -101,8 +106,7 @@ internal static class Program catch (Exception exception) { Console.Error.WriteLine($"Błąd krytyczny: {exception}"); - SentrySdk.CaptureException(exception); - await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false); + await SentryReporter.CaptureAsync(exception).ConfigureAwait(false); return 1; } finally @@ -111,11 +115,15 @@ internal static class Program } } - private static IDisposable? InitializeSentry(ServiceSettings settings) + private static IDisposable? InitializeSentry( + ServiceSettings settings, + string configurationPath, + bool verboseLogging) { if (string.IsNullOrWhiteSpace(settings.Sentry.Dsn)) { - Console.WriteLine("Sentry wyłączone (brak parametru sentry.dsn)."); + Console.WriteLine( + $"Sentry wyłączone: brak parametru sentry.dsn w używanym pliku '{configurationPath}'."); return null; } @@ -124,20 +132,41 @@ internal static class Program options.Dsn = settings.Sentry.Dsn; options.IsGlobalModeEnabled = true; options.Release = typeof(Program).Assembly.GetName().Version?.ToString(); + // Diagnostyka na poziomie Error pozostaje aktywna także bez /verbose, + // aby problemy transportu Sentry nie znikały bez śladu w usłudze Windows. + options.DiagnosticLevel = verboseLogging ? SentryLevel.Debug : SentryLevel.Error; + options.DiagnosticLogger = new ConsoleDiagnosticLogger(options.DiagnosticLevel); + options.ShutdownTimeout = TimeSpan.FromSeconds(5); }); SentrySdk.ConfigureScope(scope => scope.SetTag("portal", settings.Portal)); - Console.WriteLine("Raportowanie błędów do Sentry włączone."); + Console.WriteLine($"Raportowanie błędów do Sentry włączone; cel: {DescribeSentryTarget(settings.Sentry.Dsn)}."); return sentry; } + + private static string DescribeSentryTarget(string dsn) + { + if (!Uri.TryCreate(dsn, UriKind.Absolute, out var uri)) + { + return "niepoprawny DSN"; + } + + var port = uri.IsDefaultPort ? string.Empty : $":{uri.Port}"; + var project = uri.AbsolutePath.Trim('/').Split('/').LastOrDefault() ?? "?"; + return $"{uri.Scheme}://{uri.Host}{port}, projekt {project}"; + } } -internal sealed record CommandLineOptions(string ConfigurationPath, bool CheckConfigurationOnly) +public sealed record CommandLineOptions( + string ConfigurationPath, + bool CheckConfigurationOnly, + bool VerboseLogging) { public static CommandLineOptions Parse(string[] args) { var path = Path.Combine(AppContext.BaseDirectory, "config", "enova.json"); var checkOnly = false; + var verbose = false; for (var index = 0; index < args.Length; index++) { @@ -154,11 +183,23 @@ internal sealed record CommandLineOptions(string ConfigurationPath, bool CheckCo case "--check-config": checkOnly = true; break; + case "/debug": + case "/verbose": + case "--debug": + case "--verbose": + verbose = true; + break; + case "/nodebug": + case "/noverbose": + case "--no-debug": + case "--no-verbose": + verbose = false; + break; default: throw new ConfigurationException($"Nieznany argument: {args[index]}"); } } - return new CommandLineOptions(path, checkOnly); + return new CommandLineOptions(path, checkOnly, verbose); } } diff --git a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs index d980304..c19252f 100644 --- a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs +++ b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs @@ -1,7 +1,7 @@ using WampSharp.Core.Serialization; using WampSharp.V2.Core.Contracts; using WampSharp.V2.Rpc; -using Sentry; +using SmartB2B.Enova.Service.Diagnostics; namespace SmartB2B.Enova.Service.Rpc; @@ -87,6 +87,12 @@ public sealed class DelegateRpcOperation : IWampRpcOperation } catch (WampException exception) { + var message = exception.ArgumentsKeywords.TryGetValue("message", out var value) + ? Convert.ToString(value) + : exception.Message; + ConsoleLogger.Error( + $"Błąd procedury {Procedure} [{exception.ErrorUri}]: {message}", + exception); var details = formatter.Serialize(exception.Details); var errorArguments = exception.Arguments.Select(formatter.Serialize).ToArray(); var keywordArguments = formatter.Serialize(exception.ArgumentsKeywords); @@ -94,15 +100,19 @@ public sealed class DelegateRpcOperation : IWampRpcOperation } catch (OperationCanceledException) { + ConsoleLogger.Error($"Anulowano wykonanie procedury {Procedure}."); var details = formatter.Serialize(new Dictionary()); caller.Error(formatter, details, "wamp.error.canceled"); } catch (Exception exception) { - Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}"); - SentrySdk.CaptureException( - exception, - scope => scope.SetTag("rpc.procedure", Procedure)); + ConsoleLogger.Error( + $"Nieobsłużony błąd procedury {Procedure}: {exception.Message}", + exception); + await SentryReporter.CaptureAsync( + exception, + scope => scope.SetTag("rpc.procedure", Procedure)) + .ConfigureAwait(false); var details = formatter.Serialize(new Dictionary()); var keywordArguments = formatter.Serialize(new Dictionary { diff --git a/src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs b/src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs index 72f5597..a7b0819 100644 --- a/src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs +++ b/src/SmartB2B.Enova.Service/Rpc/WampServiceClient.cs @@ -2,6 +2,7 @@ using WampSharp.V2; using WampSharp.V2.Client; using WampSharp.V2.Core.Contracts; using WampSharp.V2.Rpc; +using SmartB2B.Enova.Service.Diagnostics; namespace SmartB2B.Enova.Service.Rpc; @@ -45,6 +46,7 @@ public sealed class WampServiceClient foreach (var operation in _operations) { + ConsoleLogger.Verbose($"WAMP: rejestracja procedury {operation.Procedure}."); var registration = await channel.RealmProxy.RpcCatalog.Register( operation, new RegisterOptions { Invoke = "last" }) @@ -56,7 +58,7 @@ public sealed class WampServiceClient Console.WriteLine( $"Połączono z WAMP; zarejestrowano {_operations.Count} procedur."); await disconnected.Task.WaitAsync(cancellationToken).ConfigureAwait(false); - Console.Error.WriteLine("Połączenie WAMP zostało przerwane."); + ConsoleLogger.Error("Połączenie WAMP zostało przerwane."); } catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) { @@ -64,7 +66,7 @@ public sealed class WampServiceClient } catch (Exception exception) { - Console.Error.WriteLine($"Błąd połączenia WAMP: {exception.Message}"); + ConsoleLogger.Error($"Błąd połączenia WAMP: {exception.Message}", exception); } finally { diff --git a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs index 6abb502..a808ec9 100644 --- a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs +++ b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs @@ -1,7 +1,7 @@ using System.Data.Common; using Microsoft.Data.SqlClient; using Newtonsoft.Json; -using Sentry; +using SmartB2B.Enova.Service.Diagnostics; using SmartB2B.Enova.Service.Rpc; namespace SmartB2B.Enova.Service.Sql; @@ -97,12 +97,13 @@ public sealed class SqlRawHandler { var code = exception.GetType().GetProperty("Number")?.GetValue(exception) ?? exception.ErrorCode; - Console.Error.WriteLine($"Błąd SQL {code}: {exception.Message}"); - SentrySdk.CaptureException(exception, scope => - { - scope.SetTag("rpc.procedure", "eu.smartb2b.sql_raw"); - scope.SetExtra("sql.error_code", code); - }); + ConsoleLogger.Error($"Błąd SQL {code}: {exception.Message}", exception); + await SentryReporter.CaptureAsync(exception, scope => + { + scope.SetTag("rpc.procedure", "eu.smartb2b.sql_raw"); + scope.SetExtra("sql.error_code", code); + }) + .ConfigureAwait(false); throw WampErrorFactory.Create( "eu.smartb2b.sql_error", exception.Message, diff --git a/tests/SmartB2B.Enova.Tests/Program.cs b/tests/SmartB2B.Enova.Tests/Program.cs index 82da1a3..886b447 100644 --- a/tests/SmartB2B.Enova.Tests/Program.cs +++ b/tests/SmartB2B.Enova.Tests/Program.cs @@ -1,6 +1,7 @@ using System.Data.Common; using System.Reflection; using SmartB2B.Enova.Contracts; +using SmartB2B.Enova.Service; using SmartB2B.Enova.Service.Configuration; using SmartB2B.Enova.Service.Orders; using SmartB2B.Enova.Service.Rpc; @@ -27,10 +28,14 @@ var tests = new (string Name, Func Run)[] ("Warstwa WAMP wysyła wynik jako kwargs", WampKeywordWireResult), ("Brak waluty zwraca stabilny błąd", MissingCurrency), ("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency), + ("Błąd adaptera jest wypisywany na stderr", AdapterErrorIsWrittenToStderr), ("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory), ("Sekrety są odczytywane z pliku konfiguracji", SecretsComeFromConfigurationFile), ("Brak danych operatora wyłącza place_order", MissingCredentialsDisablePlaceOrder), ("Częściowe dane operatora wyłączają place_order", PartialCredentialsDisablePlaceOrder), + ("Parametr /debug włącza szczegółowe logowanie", DebugSwitchEnablesVerboseLogging), + ("Parametr /verbose można wyłączyć", VerboseSwitchCanBeDisabled), + ("Tryb szczegółowy jest przekazywany do adaptera", VerboseModeReachesAdapter), ("Provider SQL działa na bieżącej platformie", SqlProviderIsSupported) }; @@ -195,6 +200,29 @@ static async Task UnsupportedCurrency() "eu.smartb2b.erp.currency_not_supported"); } +static async Task AdapterErrorIsWrittenToStderr() +{ + var previousError = Console.Error; + using var error = new StringWriter(); + try + { + Console.SetError(error); + var handler = new PlaceOrderHandler(new FailingAdapter(), CreateConfiguration()); + await AssertWampError( + handler.HandleAsync(CreateInvocation(), CancellationToken.None), + "eu.smartb2b.company_not_found"); + } + finally + { + Console.SetError(previousError); + } + + if (!error.ToString().Contains("Nie znaleziono kontrahenta", StringComparison.Ordinal)) + { + throw new InvalidOperationException($"Brak komunikatu błędu na stderr: {error}"); + } +} + static Task MissingEnovaDirectory() { var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); @@ -281,7 +309,30 @@ static Task PartialCredentialsDisablePlaceOrder() return Task.CompletedTask; } -static RuntimeSettings LoadRuntimeSettings(string credentialsJson) +static Task DebugSwitchEnablesVerboseLogging() +{ + var options = CommandLineOptions.Parse(["/debug"]); + AssertEqual(true, options.VerboseLogging, "tryb szczegółowy"); + return Task.CompletedTask; +} + +static Task VerboseSwitchCanBeDisabled() +{ + var options = CommandLineOptions.Parse(["/verbose", "/noverbose"]); + AssertEqual(false, options.VerboseLogging, "wyłączony tryb szczegółowy"); + return Task.CompletedTask; +} + +static Task VerboseModeReachesAdapter() +{ + var runtime = LoadRuntimeSettings( + "\"operator\": \"Administrator\", \"password\": \"haslo\",", + verboseLogging: true); + AssertEqual(true, runtime.EnovaConfiguration?.VerboseLogging, "tryb adaptera"); + return Task.CompletedTask; +} + +static RuntimeSettings LoadRuntimeSettings(string credentialsJson, bool verboseLogging = false) { var path = Path.Combine(Path.GetTempPath(), $"enova-settings-{Guid.NewGuid():N}.json"); try @@ -300,7 +351,7 @@ static RuntimeSettings LoadRuntimeSettings(string credentialsJson) } """); - return ServiceSettings.Load(path).ResolveRuntimeSettings(); + return ServiceSettings.Load(path).ResolveRuntimeSettings(verboseLogging); } finally { @@ -423,6 +474,17 @@ file sealed class FakeAdapter : IEnovaOrderAdapter } } +file sealed class FailingAdapter : IEnovaOrderAdapter +{ + public EnovaAdapterInfo Initialize(EnovaAdapterConfiguration configuration) => + new("1.0.0", "2604.4.4.0", "2604.4.4.0", configuration.Connection.DatabaseName); + + public OrderResult CreateOrder(OrderRequest request) => + throw new EnovaOperationException( + "eu.smartb2b.company_not_found", + $"Nie znaleziono kontrahenta o kodzie '{request.CustomerCode}'."); +} + file sealed class FakeRouterCallback : IWampRawRpcOperationRouterCallback { public long RequestId => 1;