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;