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
|
.\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
|
## Publikacja i instalacja
|
||||||
|
|
||||||
```powershell
|
```powershell
|
||||||
|
|||||||
@@ -4,6 +4,8 @@
|
|||||||
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||||
<workingdirectory>%BASE%\..</workingdirectory>
|
<workingdirectory>%BASE%\..</workingdirectory>
|
||||||
|
<!-- Odkomentuj, aby włączyć logowanie etapów Enovy i diagnostykę Sentry. -->
|
||||||
|
<!-- <arguments>/verbose</arguments> -->
|
||||||
<stoptimeout>30sec</stoptimeout>
|
<stoptimeout>30sec</stoptimeout>
|
||||||
<log mode="roll-by-time">
|
<log mode="roll-by-time">
|
||||||
<pattern>yyyyMMdd</pattern>
|
<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>
|
<description>Synchronizuje enova365 z portalem SmartB2B.</description>
|
||||||
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
<executable>%BASE%\..\SmartB2B.Enova.Service.exe</executable>
|
||||||
<workingdirectory>%BASE%\..</workingdirectory>
|
<workingdirectory>%BASE%\..</workingdirectory>
|
||||||
|
<!-- Odkomentuj, aby włączyć logowanie etapów Enovy i diagnostykę Sentry. -->
|
||||||
|
<!-- <arguments>/verbose</arguments> -->
|
||||||
<stoptimeout>30sec</stoptimeout>
|
<stoptimeout>30sec</stoptimeout>
|
||||||
<log mode="roll-by-time">
|
<log mode="roll-by-time">
|
||||||
<pattern>yyyyMMdd</pattern>
|
<pattern>yyyyMMdd</pattern>
|
||||||
|
|||||||
@@ -53,9 +53,14 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
Verbose(configuration.VerboseLogging, "Enova: przygotowanie stanu sesji.");
|
||||||
var sessionState = SessionState.Create();
|
var sessionState = SessionState.Create();
|
||||||
using var attachedSessionState = sessionState.Attach();
|
using var attachedSessionState = sessionState.Attach();
|
||||||
|
|
||||||
|
Verbose(
|
||||||
|
configuration.VerboseLogging,
|
||||||
|
$"Enova: logowanie operatora '{configuration.Connection.OperatorName}' do bazy " +
|
||||||
|
$"'{configuration.Connection.DatabaseName}'.");
|
||||||
using var login = database.Login(
|
using var login = database.Login(
|
||||||
winAuth: false,
|
winAuth: false,
|
||||||
user: configuration.Connection.OperatorName,
|
user: configuration.Connection.OperatorName,
|
||||||
@@ -64,8 +69,9 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
readOnly: false,
|
readOnly: false,
|
||||||
config: false,
|
config: false,
|
||||||
name: "SmartB2B Enova Sync");
|
name: "SmartB2B Enova Sync");
|
||||||
|
Verbose(configuration.VerboseLogging, "Enova: utworzono sesję zapisu.");
|
||||||
|
|
||||||
return CreateOrderInSession(session, request);
|
return CreateOrderInSession(session, request, configuration.VerboseLogging);
|
||||||
}
|
}
|
||||||
catch (EnovaOperationException)
|
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 handel = HandelModule.GetInstance(session);
|
||||||
var crm = CRMModule.GetInstance(session);
|
var crm = CRMModule.GetInstance(session);
|
||||||
var towary = TowaryModule.GetInstance(session);
|
var towary = TowaryModule.GetInstance(session);
|
||||||
@@ -93,11 +103,13 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
"eu.smartb2b.erp.document_definition_not_found",
|
"eu.smartb2b.erp.document_definition_not_found",
|
||||||
$"Nie znaleziono definicji dokumentu '{request.DocumentDefinition}'.",
|
$"Nie znaleziono definicji dokumentu '{request.DocumentDefinition}'.",
|
||||||
new Dictionary<string, object?> { ["documentDefinition"] = request.DocumentDefinition });
|
new Dictionary<string, object?> { ["documentDefinition"] = request.DocumentDefinition });
|
||||||
|
Verbose(verboseLogging, $"Enova: znaleziono definicję dokumentu '{definition.Symbol}'.");
|
||||||
var customer = crm.Kontrahenci.WgKodu[request.CustomerCode]
|
var customer = crm.Kontrahenci.WgKodu[request.CustomerCode]
|
||||||
?? throw new EnovaOperationException(
|
?? throw new EnovaOperationException(
|
||||||
"eu.smartb2b.company_not_found",
|
"eu.smartb2b.company_not_found",
|
||||||
$"Nie znaleziono kontrahenta o kodzie '{request.CustomerCode}'.",
|
$"Nie znaleziono kontrahenta o kodzie '{request.CustomerCode}'.",
|
||||||
new Dictionary<string, object?> { ["companyErpId"] = request.CustomerCode });
|
new Dictionary<string, object?> { ["companyErpId"] = request.CustomerCode });
|
||||||
|
Verbose(verboseLogging, $"Enova: znaleziono kontrahenta '{customer.Kod}'.");
|
||||||
var warehouse = string.IsNullOrWhiteSpace(request.WarehouseCode)
|
var warehouse = string.IsNullOrWhiteSpace(request.WarehouseCode)
|
||||||
? warehouses.StandardowyMagazyn
|
? warehouses.StandardowyMagazyn
|
||||||
: warehouses.GetGrantedView()
|
: warehouses.GetGrantedView()
|
||||||
@@ -116,15 +128,18 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
: $"Nie znaleziono magazynu o symbolu '{request.WarehouseCode.Trim()}'.",
|
: $"Nie znaleziono magazynu o symbolu '{request.WarehouseCode.Trim()}'.",
|
||||||
new Dictionary<string, object?> { ["warehouseErpId"] = request.WarehouseCode });
|
new Dictionary<string, object?> { ["warehouseErpId"] = request.WarehouseCode });
|
||||||
}
|
}
|
||||||
|
Verbose(verboseLogging, $"Enova: wybrano magazyn '{warehouse.Symbol}'.");
|
||||||
|
|
||||||
DokumentHandlowy document;
|
DokumentHandlowy document;
|
||||||
|
|
||||||
|
Verbose(verboseLogging, "Enova: rozpoczęcie transakcji edycyjnej.");
|
||||||
using (var transaction = session.Logout(editMode: true))
|
using (var transaction = session.Logout(editMode: true))
|
||||||
{
|
{
|
||||||
document = session.AddRow(new DokumentHandlowy());
|
document = session.AddRow(new DokumentHandlowy());
|
||||||
document.Definicja = definition;
|
document.Definicja = definition;
|
||||||
document.Kontrahent = customer;
|
document.Kontrahent = customer;
|
||||||
document.Magazyn = warehouse;
|
document.Magazyn = warehouse;
|
||||||
|
Verbose(verboseLogging, "Enova: dodano nagłówek dokumentu.");
|
||||||
|
|
||||||
if (!string.IsNullOrWhiteSpace(request.CustomerReference))
|
if (!string.IsNullOrWhiteSpace(request.CustomerReference))
|
||||||
{
|
{
|
||||||
@@ -137,8 +152,12 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
document.Opis.Add(request.Notes.Trim());
|
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]
|
var product = towary.Towary.WgKodu[line.ProductCode]
|
||||||
?? throw new EnovaOperationException(
|
?? throw new EnovaOperationException(
|
||||||
"eu.smartb2b.product_not_found",
|
"eu.smartb2b.product_not_found",
|
||||||
@@ -150,6 +169,7 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
position.Ilosc = new Quantity((double)line.Quantity);
|
position.Ilosc = new Quantity((double)line.Quantity);
|
||||||
position.Cena = new DoubleCy(line.UnitPrice, CurrencySymbol);
|
position.Cena = new DoubleCy(line.UnitPrice, CurrencySymbol);
|
||||||
position.UstawRabat(new Percent(line.Discount / 100m), ręcznie: true);
|
position.UstawRabat(new Percent(line.Discount / 100m), ręcznie: true);
|
||||||
|
Verbose(verboseLogging, $"Enova: dodano pozycję {index + 1}.");
|
||||||
}
|
}
|
||||||
|
|
||||||
document.Stan = request.SaveMode switch
|
document.Stan = request.SaveMode switch
|
||||||
@@ -160,11 +180,17 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
"eu.smartb2b.erp.invalid_order",
|
"eu.smartb2b.erp.invalid_order",
|
||||||
$"Nieobsługiwany tryb zapisu: {request.SaveMode}.")
|
$"Nieobsługiwany tryb zapisu: {request.SaveMode}.")
|
||||||
};
|
};
|
||||||
|
Verbose(verboseLogging, $"Enova: ustawiono stan dokumentu na {request.SaveMode}.");
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
|
Verbose(verboseLogging, "Enova: zatwierdzono transakcję edycyjną.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Verbose(verboseLogging, "Enova: wykonywanie session.Save().");
|
||||||
session.Save();
|
session.Save();
|
||||||
|
Verbose(
|
||||||
|
verboseLogging,
|
||||||
|
$"Enova: session.Save() zakończone; numer={document.Numer.Pelny}, ID={document.ID}.");
|
||||||
|
|
||||||
return new OrderResult(
|
return new OrderResult(
|
||||||
document.ID,
|
document.ID,
|
||||||
@@ -271,4 +297,12 @@ public sealed class EnovaOrderAdapter : IEnovaOrderAdapter
|
|||||||
|
|
||||||
private static string GetAssemblyVersion(Assembly assembly) =>
|
private static string GetAssemblyVersion(Assembly assembly) =>
|
||||||
assembly.GetName().Version?.ToString() ?? "unknown";
|
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,
|
EnovaConnectionOptions Connection,
|
||||||
string DocumentDefinition,
|
string DocumentDefinition,
|
||||||
string? DefaultWarehouseCode,
|
string? DefaultWarehouseCode,
|
||||||
OrderSaveMode SaveMode);
|
OrderSaveMode SaveMode,
|
||||||
|
bool VerboseLogging = false);
|
||||||
|
|
||||||
public sealed class OrderRequest
|
public sealed class OrderRequest
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -48,7 +48,7 @@ public sealed class ServiceSettings
|
|||||||
return settings;
|
return settings;
|
||||||
}
|
}
|
||||||
|
|
||||||
public RuntimeSettings ResolveRuntimeSettings()
|
public RuntimeSettings ResolveRuntimeSettings(bool verboseLogging = false)
|
||||||
{
|
{
|
||||||
var installationPath = Path.GetFullPath(
|
var installationPath = Path.GetFullPath(
|
||||||
Environment.ExpandEnvironmentVariables(Enova.InstallationPath));
|
Environment.ExpandEnvironmentVariables(Enova.InstallationPath));
|
||||||
@@ -64,7 +64,8 @@ public sealed class ServiceSettings
|
|||||||
Sql.ConnectionString),
|
Sql.ConnectionString),
|
||||||
Enova.DocumentDefinition,
|
Enova.DocumentDefinition,
|
||||||
Enova.DefaultWarehouseCode,
|
Enova.DefaultWarehouseCode,
|
||||||
Enova.SaveMode);
|
Enova.SaveMode,
|
||||||
|
verboseLogging);
|
||||||
}
|
}
|
||||||
|
|
||||||
return new RuntimeSettings(
|
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 Newtonsoft.Json;
|
||||||
using Sentry;
|
|
||||||
using SmartB2B.Enova.Contracts;
|
using SmartB2B.Enova.Contracts;
|
||||||
|
using SmartB2B.Enova.Service.Diagnostics;
|
||||||
using SmartB2B.Enova.Service.Rpc;
|
using SmartB2B.Enova.Service.Rpc;
|
||||||
|
|
||||||
namespace SmartB2B.Enova.Service.Orders;
|
namespace SmartB2B.Enova.Service.Orders;
|
||||||
@@ -60,6 +60,10 @@ public sealed class PlaceOrderHandler
|
|||||||
new Dictionary<string, object?> { ["errors"] = errors });
|
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);
|
await _orderLock.WaitAsync(cancellationToken).ConfigureAwait(false);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -69,20 +73,28 @@ public sealed class PlaceOrderHandler
|
|||||||
OrderResult result;
|
OrderResult result;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
|
ConsoleLogger.Verbose("ZO: przekazanie żądania do adaptera Enovy.");
|
||||||
result = await Task.Run(() => _adapter.CreateOrder(request), cancellationToken)
|
result = await Task.Run(() => _adapter.CreateOrder(request), cancellationToken)
|
||||||
.ConfigureAwait(false);
|
.ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (EnovaOperationException exception)
|
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("rpc.procedure", "eu.smartb2b.place_order");
|
||||||
scope.SetTag("wamp.error_uri", exception.ErrorUri);
|
scope.SetTag("wamp.error_uri", exception.ErrorUri);
|
||||||
});
|
})
|
||||||
|
.ConfigureAwait(false);
|
||||||
throw WampErrorFactory.FromEnova(exception);
|
throw WampErrorFactory.FromEnova(exception);
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.WriteLine($"Utworzono ZO {result.Number} (ID: {result.Id}).");
|
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?>
|
return new KeywordResult(new Dictionary<string, object?>
|
||||||
{
|
{
|
||||||
["order_erp_id"] = result.Id,
|
["order_erp_id"] = result.Id,
|
||||||
@@ -95,6 +107,7 @@ public sealed class PlaceOrderHandler
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
_orderLock.Release();
|
_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.Runtime;
|
||||||
using SmartB2B.Enova.Service.Sql;
|
using SmartB2B.Enova.Service.Sql;
|
||||||
using Sentry;
|
using Sentry;
|
||||||
|
using Sentry.Infrastructure;
|
||||||
using WampSharp.V2.Rpc;
|
using WampSharp.V2.Rpc;
|
||||||
|
|
||||||
namespace SmartB2B.Enova.Service;
|
namespace SmartB2B.Enova.Service;
|
||||||
@@ -26,9 +27,13 @@ internal static class Program
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var commandLine = CommandLineOptions.Parse(args);
|
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);
|
var settings = ServiceSettings.Load(commandLine.ConfigurationPath);
|
||||||
sentry = InitializeSentry(settings);
|
ConsoleLogger.Verbose("Wczytano i zweryfikowano konfigurację usługi.");
|
||||||
var runtime = settings.ResolveRuntimeSettings();
|
sentry = InitializeSentry(settings, commandLine.ConfigurationPath, commandLine.VerboseLogging);
|
||||||
|
var runtime = settings.ResolveRuntimeSettings(commandLine.VerboseLogging);
|
||||||
|
|
||||||
using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath);
|
using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath);
|
||||||
var adapter = adapterLoader.Load();
|
var adapter = adapterLoader.Load();
|
||||||
@@ -101,8 +106,7 @@ internal static class Program
|
|||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Błąd krytyczny: {exception}");
|
Console.Error.WriteLine($"Błąd krytyczny: {exception}");
|
||||||
SentrySdk.CaptureException(exception);
|
await SentryReporter.CaptureAsync(exception).ConfigureAwait(false);
|
||||||
await SentrySdk.FlushAsync(TimeSpan.FromSeconds(2)).ConfigureAwait(false);
|
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
finally
|
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))
|
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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -124,20 +132,41 @@ internal static class Program
|
|||||||
options.Dsn = settings.Sentry.Dsn;
|
options.Dsn = settings.Sentry.Dsn;
|
||||||
options.IsGlobalModeEnabled = true;
|
options.IsGlobalModeEnabled = true;
|
||||||
options.Release = typeof(Program).Assembly.GetName().Version?.ToString();
|
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));
|
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;
|
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)
|
public static CommandLineOptions Parse(string[] args)
|
||||||
{
|
{
|
||||||
var path = Path.Combine(AppContext.BaseDirectory, "config", "enova.json");
|
var path = Path.Combine(AppContext.BaseDirectory, "config", "enova.json");
|
||||||
var checkOnly = false;
|
var checkOnly = false;
|
||||||
|
var verbose = false;
|
||||||
|
|
||||||
for (var index = 0; index < args.Length; index++)
|
for (var index = 0; index < args.Length; index++)
|
||||||
{
|
{
|
||||||
@@ -154,11 +183,23 @@ internal sealed record CommandLineOptions(string ConfigurationPath, bool CheckCo
|
|||||||
case "--check-config":
|
case "--check-config":
|
||||||
checkOnly = true;
|
checkOnly = true;
|
||||||
break;
|
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:
|
default:
|
||||||
throw new ConfigurationException($"Nieznany argument: {args[index]}");
|
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.Core.Serialization;
|
||||||
using WampSharp.V2.Core.Contracts;
|
using WampSharp.V2.Core.Contracts;
|
||||||
using WampSharp.V2.Rpc;
|
using WampSharp.V2.Rpc;
|
||||||
using Sentry;
|
using SmartB2B.Enova.Service.Diagnostics;
|
||||||
|
|
||||||
namespace SmartB2B.Enova.Service.Rpc;
|
namespace SmartB2B.Enova.Service.Rpc;
|
||||||
|
|
||||||
@@ -87,6 +87,12 @@ public sealed class DelegateRpcOperation : IWampRpcOperation
|
|||||||
}
|
}
|
||||||
catch (WampException exception)
|
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 details = formatter.Serialize(exception.Details);
|
||||||
var errorArguments = exception.Arguments.Select(formatter.Serialize).ToArray();
|
var errorArguments = exception.Arguments.Select(formatter.Serialize).ToArray();
|
||||||
var keywordArguments = formatter.Serialize(exception.ArgumentsKeywords);
|
var keywordArguments = formatter.Serialize(exception.ArgumentsKeywords);
|
||||||
@@ -94,15 +100,19 @@ public sealed class DelegateRpcOperation : IWampRpcOperation
|
|||||||
}
|
}
|
||||||
catch (OperationCanceledException)
|
catch (OperationCanceledException)
|
||||||
{
|
{
|
||||||
|
ConsoleLogger.Error($"Anulowano wykonanie procedury {Procedure}.");
|
||||||
var details = formatter.Serialize(new Dictionary<string, object>());
|
var details = formatter.Serialize(new Dictionary<string, object>());
|
||||||
caller.Error(formatter, details, "wamp.error.canceled");
|
caller.Error(formatter, details, "wamp.error.canceled");
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}");
|
ConsoleLogger.Error(
|
||||||
SentrySdk.CaptureException(
|
$"Nieobsłużony błąd procedury {Procedure}: {exception.Message}",
|
||||||
|
exception);
|
||||||
|
await SentryReporter.CaptureAsync(
|
||||||
exception,
|
exception,
|
||||||
scope => scope.SetTag("rpc.procedure", Procedure));
|
scope => scope.SetTag("rpc.procedure", Procedure))
|
||||||
|
.ConfigureAwait(false);
|
||||||
var details = formatter.Serialize(new Dictionary<string, object>());
|
var details = formatter.Serialize(new Dictionary<string, object>());
|
||||||
var keywordArguments = 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.Client;
|
||||||
using WampSharp.V2.Core.Contracts;
|
using WampSharp.V2.Core.Contracts;
|
||||||
using WampSharp.V2.Rpc;
|
using WampSharp.V2.Rpc;
|
||||||
|
using SmartB2B.Enova.Service.Diagnostics;
|
||||||
|
|
||||||
namespace SmartB2B.Enova.Service.Rpc;
|
namespace SmartB2B.Enova.Service.Rpc;
|
||||||
|
|
||||||
@@ -45,6 +46,7 @@ public sealed class WampServiceClient
|
|||||||
|
|
||||||
foreach (var operation in _operations)
|
foreach (var operation in _operations)
|
||||||
{
|
{
|
||||||
|
ConsoleLogger.Verbose($"WAMP: rejestracja procedury {operation.Procedure}.");
|
||||||
var registration = await channel.RealmProxy.RpcCatalog.Register(
|
var registration = await channel.RealmProxy.RpcCatalog.Register(
|
||||||
operation,
|
operation,
|
||||||
new RegisterOptions { Invoke = "last" })
|
new RegisterOptions { Invoke = "last" })
|
||||||
@@ -56,7 +58,7 @@ public sealed class WampServiceClient
|
|||||||
Console.WriteLine(
|
Console.WriteLine(
|
||||||
$"Połączono z WAMP; zarejestrowano {_operations.Count} procedur.");
|
$"Połączono z WAMP; zarejestrowano {_operations.Count} procedur.");
|
||||||
await disconnected.Task.WaitAsync(cancellationToken).ConfigureAwait(false);
|
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)
|
catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
|
||||||
{
|
{
|
||||||
@@ -64,7 +66,7 @@ public sealed class WampServiceClient
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"Błąd połączenia WAMP: {exception.Message}");
|
ConsoleLogger.Error($"Błąd połączenia WAMP: {exception.Message}", exception);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using Microsoft.Data.SqlClient;
|
using Microsoft.Data.SqlClient;
|
||||||
using Newtonsoft.Json;
|
using Newtonsoft.Json;
|
||||||
using Sentry;
|
using SmartB2B.Enova.Service.Diagnostics;
|
||||||
using SmartB2B.Enova.Service.Rpc;
|
using SmartB2B.Enova.Service.Rpc;
|
||||||
|
|
||||||
namespace SmartB2B.Enova.Service.Sql;
|
namespace SmartB2B.Enova.Service.Sql;
|
||||||
@@ -97,12 +97,13 @@ public sealed class SqlRawHandler
|
|||||||
{
|
{
|
||||||
var code = exception.GetType().GetProperty("Number")?.GetValue(exception)
|
var code = exception.GetType().GetProperty("Number")?.GetValue(exception)
|
||||||
?? exception.ErrorCode;
|
?? exception.ErrorCode;
|
||||||
Console.Error.WriteLine($"Błąd SQL {code}: {exception.Message}");
|
ConsoleLogger.Error($"Błąd SQL {code}: {exception.Message}", exception);
|
||||||
SentrySdk.CaptureException(exception, scope =>
|
await SentryReporter.CaptureAsync(exception, scope =>
|
||||||
{
|
{
|
||||||
scope.SetTag("rpc.procedure", "eu.smartb2b.sql_raw");
|
scope.SetTag("rpc.procedure", "eu.smartb2b.sql_raw");
|
||||||
scope.SetExtra("sql.error_code", code);
|
scope.SetExtra("sql.error_code", code);
|
||||||
});
|
})
|
||||||
|
.ConfigureAwait(false);
|
||||||
throw WampErrorFactory.Create(
|
throw WampErrorFactory.Create(
|
||||||
"eu.smartb2b.sql_error",
|
"eu.smartb2b.sql_error",
|
||||||
exception.Message,
|
exception.Message,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
using System.Data.Common;
|
using System.Data.Common;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using SmartB2B.Enova.Contracts;
|
using SmartB2B.Enova.Contracts;
|
||||||
|
using SmartB2B.Enova.Service;
|
||||||
using SmartB2B.Enova.Service.Configuration;
|
using SmartB2B.Enova.Service.Configuration;
|
||||||
using SmartB2B.Enova.Service.Orders;
|
using SmartB2B.Enova.Service.Orders;
|
||||||
using SmartB2B.Enova.Service.Rpc;
|
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),
|
("Warstwa WAMP wysyła wynik jako kwargs", WampKeywordWireResult),
|
||||||
("Brak waluty zwraca stabilny błąd", MissingCurrency),
|
("Brak waluty zwraca stabilny błąd", MissingCurrency),
|
||||||
("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency),
|
("Waluta inna niż PLN jest odrzucana", UnsupportedCurrency),
|
||||||
|
("Błąd adaptera jest wypisywany na stderr", AdapterErrorIsWrittenToStderr),
|
||||||
("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory),
|
("Brak katalogu Enovy jest wykrywany", MissingEnovaDirectory),
|
||||||
("Sekrety są odczytywane z pliku konfiguracji", SecretsComeFromConfigurationFile),
|
("Sekrety są odczytywane z pliku konfiguracji", SecretsComeFromConfigurationFile),
|
||||||
("Brak danych operatora wyłącza place_order", MissingCredentialsDisablePlaceOrder),
|
("Brak danych operatora wyłącza place_order", MissingCredentialsDisablePlaceOrder),
|
||||||
("Częściowe dane operatora wyłączają place_order", PartialCredentialsDisablePlaceOrder),
|
("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)
|
("Provider SQL działa na bieżącej platformie", SqlProviderIsSupported)
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -195,6 +200,29 @@ static async Task UnsupportedCurrency()
|
|||||||
"eu.smartb2b.erp.currency_not_supported");
|
"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()
|
static Task MissingEnovaDirectory()
|
||||||
{
|
{
|
||||||
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
var path = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"));
|
||||||
@@ -281,7 +309,30 @@ static Task PartialCredentialsDisablePlaceOrder()
|
|||||||
return Task.CompletedTask;
|
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");
|
var path = Path.Combine(Path.GetTempPath(), $"enova-settings-{Guid.NewGuid():N}.json");
|
||||||
try
|
try
|
||||||
@@ -300,7 +351,7 @@ static RuntimeSettings LoadRuntimeSettings(string credentialsJson)
|
|||||||
}
|
}
|
||||||
""");
|
""");
|
||||||
|
|
||||||
return ServiceSettings.Load(path).ResolveRuntimeSettings();
|
return ServiceSettings.Load(path).ResolveRuntimeSettings(verboseLogging);
|
||||||
}
|
}
|
||||||
finally
|
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
|
file sealed class FakeRouterCallback : IWampRawRpcOperationRouterCallback
|
||||||
{
|
{
|
||||||
public long RequestId => 1;
|
public long RequestId => 1;
|
||||||
|
|||||||
Reference in New Issue
Block a user