Add verbose Enova diagnostics and reliable Sentry reporting
This commit is contained in:
14
README.md
14
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
|
||||
`<arguments>/verbose</arguments>` 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
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||
<workingdirectory>%BASE%\..</workingdirectory>
|
||||
<!-- Odkomentuj, aby włączyć logowanie etapów Enovy i diagnostykę Sentry. -->
|
||||
<!-- <arguments>/verbose</arguments> -->
|
||||
<stoptimeout>30sec</stoptimeout>
|
||||
<log mode="roll-by-time">
|
||||
<pattern>yyyyMMdd</pattern>
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -4,6 +4,8 @@
|
||||
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||
<workingdirectory>%BASE%\..</workingdirectory>
|
||||
<!-- Odkomentuj, aby włączyć logowanie etapów Enovy i diagnostykę Sentry. -->
|
||||
<!-- <arguments>/verbose</arguments> -->
|
||||
<stoptimeout>30sec</stoptimeout>
|
||||
<log mode="roll-by-time">
|
||||
<pattern>yyyyMMdd</pattern>
|
||||
|
||||
@@ -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<string, object?> { ["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<string, object?> { ["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<string, object?> { ["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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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(
|
||||
|
||||
28
src/SmartB2B.Enova.Service/Diagnostics/ConsoleLogger.cs
Normal file
28
src/SmartB2B.Enova.Service/Diagnostics/ConsoleLogger.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
28
src/SmartB2B.Enova.Service/Diagnostics/SentryReporter.cs
Normal file
28
src/SmartB2B.Enova.Service/Diagnostics/SentryReporter.cs
Normal file
@@ -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<Scope>? 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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<string, object?> { ["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 =>
|
||||
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<string, object?>
|
||||
{
|
||||
["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.");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<string, object>());
|
||||
caller.Error(formatter, details, "wamp.error.canceled");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}");
|
||||
SentrySdk.CaptureException(
|
||||
ConsoleLogger.Error(
|
||||
$"Nieobsłużony błąd procedury {Procedure}: {exception.Message}",
|
||||
exception);
|
||||
await SentryReporter.CaptureAsync(
|
||||
exception,
|
||||
scope => scope.SetTag("rpc.procedure", Procedure));
|
||||
scope => scope.SetTag("rpc.procedure", Procedure))
|
||||
.ConfigureAwait(false);
|
||||
var details = formatter.Serialize(new Dictionary<string, object>());
|
||||
var keywordArguments = formatter.Serialize(new Dictionary<string, object?>
|
||||
{
|
||||
|
||||
@@ -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
|
||||
{
|
||||
|
||||
@@ -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 =>
|
||||
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,
|
||||
|
||||
@@ -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<Task> 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;
|
||||
|
||||
Reference in New Issue
Block a user