diff --git a/README.md b/README.md index b1f8633..5bb691a 100644 --- a/README.md +++ b/README.md @@ -9,12 +9,13 @@ Dedykowana usługa Windows zastępująca proces Node na stanowiskach z enova365. - Operacje Enovy są wykonywane pojedynczo i w osobnych sesjach. Zapytania SQL korzystają z puli połączeń i mogą działać równolegle. - ZO jest zapisywane transakcyjnie, domyślnie do bufora. Powtarzający się numer obcy nie jest blokowany. - Pierwsza wersja obsługuje PLN, `price_netto` i rabat procentowy `0-100`. +- Sentry raportuje błędy krytyczne procesu oraz nieoczekiwane błędy procedur Enovy i SQL, oznaczone portalem i nazwą procedury WAMP. ## Konfiguracja -Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora wraz z hasłem, connection string SQL, definicję dokumentu, magazyn domyślny i tryb zapisu. Repozytorium oraz paczka zawierają tylko `config/enova.json.sample`. Skrypt instalacyjny kopiuje wzorzec do `config/enova.json` wyłącznie wtedy, gdy lokalna konfiguracja jeszcze nie istnieje. +Plik `config/enova.json` zawiera portal WAMP, ścieżkę instalacji, alias bazy, operatora wraz z hasłem, connection string SQL, definicję dokumentu, magazyn domyślny, tryb zapisu oraz DSN Sentry. Repozytorium oraz paczka zawierają tylko `config/enova.json.sample`. Skrypt instalacyjny kopiuje wzorzec do `config/enova.json` wyłącznie wtedy, gdy lokalna konfiguracja jeszcze nie istnieje. -Uzupełnij pola `enova.operator`, `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Jeżeli operator lub hasło Enovy nie są podane, usługa nie loguje się do bazy Enovy i nie rejestruje endpointu `eu.smartb2b.place_order`; endpoint SQL oraz diagnostyka pozostają dostępne. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą. +Uzupełnij pola `enova.operator`, `enova.password` i `sql.connectionString` bezpośrednio w lokalnym pliku `config/enova.json`. Parametr `sentry.dsn` wskazuje projekt Sentry; pusty lub pominięty DSN wyłącza wysyłanie błędów. Jeżeli operator lub hasło Enovy nie są podane, usługa nie loguje się do bazy Enovy i nie rejestruje endpointu `eu.smartb2b.place_order`; endpoint SQL oraz diagnostyka pozostają dostępne. Po zmianie konfiguracji trzeba zrestartować usługę. Connection string powinien używać konta SQL o możliwie najmniejszych uprawnieniach; `sql_raw` celowo dopuszcza również polecenia modyfikujące dane dla zgodności z obecną usługą. ## Budowanie i testy diff --git a/daemon/smartb2bsync-enova.xml b/daemon/smartb2bsync-enova.xml index 2a10562..1232ad7 100644 --- a/daemon/smartb2bsync-enova.xml +++ b/daemon/smartb2bsync-enova.xml @@ -32,6 +32,7 @@ + diff --git a/release/Sentry.dll b/release/Sentry.dll new file mode 100644 index 0000000..1671d8b Binary files /dev/null and b/release/Sentry.dll differ diff --git a/release/SmartB2B.Enova.Contracts.dll b/release/SmartB2B.Enova.Contracts.dll index 47b969b..0e99549 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 1a3ce34..c4ca4b6 100644 Binary files a/release/SmartB2B.Enova.Contracts.pdb and b/release/SmartB2B.Enova.Contracts.pdb differ diff --git a/release/SmartB2B.Enova.Service.deps.json b/release/SmartB2B.Enova.Service.deps.json index db31af5..8e6b7c9 100644 --- a/release/SmartB2B.Enova.Service.deps.json +++ b/release/SmartB2B.Enova.Service.deps.json @@ -9,6 +9,7 @@ "SmartB2B.Enova.Service/1.0.0": { "dependencies": { "Newtonsoft.Json": "13.0.3", + "Sentry": "6.9.0", "SmartB2B.Enova.Contracts": "1.0.0", "WampSharp.Default.Client": "23.8.1" }, @@ -57,6 +58,14 @@ } } }, + "Sentry/6.9.0": { + "runtime": { + "lib/net8.0/Sentry.dll": { + "assemblyVersion": "6.9.0.0", + "fileVersion": "6.9.0.0" + } + } + }, "System.Collections/4.3.0": { "dependencies": { "Microsoft.NETCore.Platforms": "1.1.0", @@ -396,6 +405,13 @@ "path": "newtonsoftmessagepack/0.1.11", "hashPath": "newtonsoftmessagepack.0.1.11.nupkg.sha512" }, + "Sentry/6.9.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-qQIvEwuvjAB6fDLVLLcDj/5f8n5jOyPyHjj3a/GQ1ogTLLQqsSxgQj1fEEquNT9HQuj4ZTyCg3c1DCBMUIvJGQ==", + "path": "sentry/6.9.0", + "hashPath": "sentry.6.9.0.nupkg.sha512" + }, "System.Collections/4.3.0": { "type": "package", "serviceable": true, diff --git a/release/SmartB2B.Enova.Service.dll b/release/SmartB2B.Enova.Service.dll index 132d6d3..6f7e9c5 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 ceb7c37..38494e5 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 c5b9055..a308ada 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 8eaf0d9..36d62cc 100644 Binary files a/release/adapters/SmartB2B.Enova.Adapter.dll and b/release/adapters/SmartB2B.Enova.Adapter.dll differ diff --git a/release/config/enova.json.sample b/release/config/enova.json.sample index 0754aa2..4368348 100644 --- a/release/config/enova.json.sample +++ b/release/config/enova.json.sample @@ -17,6 +17,9 @@ "connectionString": "Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=True", "commandTimeoutSeconds": 30 }, + "sentry": { + "dsn": "https://9ae7cbe5e29f4a85b2a748a78e55a868@glitchtip.glebocka.synology.me/10" + }, "diagnostics": { "logDirectory": "daemon", "logPrefix": "smartb2bsync-enova" diff --git a/release/daemon/smartb2bsync-enova.xml b/release/daemon/smartb2bsync-enova.xml index 2a10562..1232ad7 100644 --- a/release/daemon/smartb2bsync-enova.xml +++ b/release/daemon/smartb2bsync-enova.xml @@ -32,6 +32,7 @@ + diff --git a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs index 9334774..28471ed 100644 --- a/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs +++ b/src/SmartB2B.Enova.Service/Configuration/ServiceSettings.cs @@ -14,6 +14,8 @@ public sealed class ServiceSettings public SqlSettings Sql { get; init; } = new(); + public SentrySettings Sentry { get; init; } = new(); + public DiagnosticsSettings Diagnostics { get; init; } = new(); public static ServiceSettings Load(string path) @@ -137,6 +139,11 @@ public sealed class SqlSettings public int CommandTimeoutSeconds { get; init; } = 30; } +public sealed class SentrySettings +{ + public string? Dsn { get; init; } +} + public sealed class DiagnosticsSettings { public string LogDirectory { get; init; } = "daemon"; diff --git a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs index 1acb221..3cfe779 100644 --- a/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs +++ b/src/SmartB2B.Enova.Service/Orders/PlaceOrderHandler.cs @@ -1,4 +1,5 @@ using Newtonsoft.Json; +using Sentry; using SmartB2B.Enova.Contracts; using SmartB2B.Enova.Service.Rpc; @@ -73,6 +74,11 @@ public sealed class PlaceOrderHandler } catch (EnovaOperationException exception) { + SentrySdk.CaptureException(exception, scope => + { + scope.SetTag("rpc.procedure", "eu.smartb2b.place_order"); + scope.SetTag("wamp.error_uri", exception.ErrorUri); + }); throw WampErrorFactory.FromEnova(exception); } diff --git a/src/SmartB2B.Enova.Service/Program.cs b/src/SmartB2B.Enova.Service/Program.cs index 1f97762..307e11d 100644 --- a/src/SmartB2B.Enova.Service/Program.cs +++ b/src/SmartB2B.Enova.Service/Program.cs @@ -5,6 +5,7 @@ using SmartB2B.Enova.Service.Orders; using SmartB2B.Enova.Service.Rpc; using SmartB2B.Enova.Service.Runtime; using SmartB2B.Enova.Service.Sql; +using Sentry; using WampSharp.V2.Rpc; namespace SmartB2B.Enova.Service; @@ -13,6 +14,7 @@ internal static class Program { public static async Task Main(string[] args) { + IDisposable? sentry = null; var cancellation = new CancellationTokenSource(); Console.CancelKeyPress += (_, eventArgs) => { @@ -25,6 +27,7 @@ internal static class Program { var commandLine = CommandLineOptions.Parse(args); var settings = ServiceSettings.Load(commandLine.ConfigurationPath); + sentry = InitializeSentry(settings); var runtime = settings.ResolveRuntimeSettings(); using var adapterLoader = new EnovaAdapterLoader(runtime.EnovaInstallationPath); @@ -98,8 +101,34 @@ 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); return 1; } + finally + { + sentry?.Dispose(); + } + } + + private static IDisposable? InitializeSentry(ServiceSettings settings) + { + if (string.IsNullOrWhiteSpace(settings.Sentry.Dsn)) + { + Console.WriteLine("Sentry wyłączone (brak parametru sentry.dsn)."); + return null; + } + + var sentry = SentrySdk.Init(options => + { + options.Dsn = settings.Sentry.Dsn; + options.IsGlobalModeEnabled = true; + options.Release = typeof(Program).Assembly.GetName().Version?.ToString(); + }); + + SentrySdk.ConfigureScope(scope => scope.SetTag("portal", settings.Portal)); + Console.WriteLine("Raportowanie błędów do Sentry włączone."); + return sentry; } } diff --git a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs index 6de62e3..d980304 100644 --- a/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs +++ b/src/SmartB2B.Enova.Service/Rpc/DelegateRpcOperation.cs @@ -1,6 +1,7 @@ using WampSharp.Core.Serialization; using WampSharp.V2.Core.Contracts; using WampSharp.V2.Rpc; +using Sentry; namespace SmartB2B.Enova.Service.Rpc; @@ -99,6 +100,9 @@ public sealed class DelegateRpcOperation : IWampRpcOperation catch (Exception exception) { Console.Error.WriteLine($"Nieobsłużony błąd procedury {Procedure}: {exception}"); + SentrySdk.CaptureException( + exception, + scope => scope.SetTag("rpc.procedure", Procedure)); var details = formatter.Serialize(new Dictionary()); var keywordArguments = formatter.Serialize(new Dictionary { diff --git a/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj b/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj index 22fbe0e..deb9ea8 100644 --- a/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj +++ b/src/SmartB2B.Enova.Service/SmartB2B.Enova.Service.csproj @@ -10,6 +10,7 @@ + diff --git a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs index 020e2e1..18ac047 100644 --- a/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs +++ b/src/SmartB2B.Enova.Service/Sql/SqlRawHandler.cs @@ -1,5 +1,6 @@ using System.Data.Common; using Newtonsoft.Json; +using Sentry; using SmartB2B.Enova.Service.Rpc; namespace SmartB2B.Enova.Service.Sql; @@ -98,6 +99,11 @@ 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); + }); throw WampErrorFactory.Create( "eu.smartb2b.sql_error", exception.Message, diff --git a/src/SmartB2B.Enova.Service/config/enova.json.sample b/src/SmartB2B.Enova.Service/config/enova.json.sample index 0754aa2..4368348 100644 --- a/src/SmartB2B.Enova.Service/config/enova.json.sample +++ b/src/SmartB2B.Enova.Service/config/enova.json.sample @@ -17,6 +17,9 @@ "connectionString": "Server=...;Database=...;User ID=...;Password=...;TrustServerCertificate=True", "commandTimeoutSeconds": 30 }, + "sentry": { + "dsn": "https://9ae7cbe5e29f4a85b2a748a78e55a868@glitchtip.glebocka.synology.me/10" + }, "diagnostics": { "logDirectory": "daemon", "logPrefix": "smartb2bsync-enova" diff --git a/tests/SmartB2B.Enova.Tests/Program.cs b/tests/SmartB2B.Enova.Tests/Program.cs index 91e303a..86387da 100644 --- a/tests/SmartB2B.Enova.Tests/Program.cs +++ b/tests/SmartB2B.Enova.Tests/Program.cs @@ -207,6 +207,9 @@ static Task SecretsComeFromConfigurationFile() "connectionString": "Server=db;Database=enova;User ID=user;Password=sql-z-pliku", "commandTimeoutSeconds": 30 }, + "sentry": { + "dsn": "https://public@example.com/1" + }, "diagnostics": { "logDirectory": "C:/logs", "logPrefix": "test" @@ -214,8 +217,10 @@ static Task SecretsComeFromConfigurationFile() } """); - var runtime = ServiceSettings.Load(path).ResolveRuntimeSettings(); + var settings = ServiceSettings.Load(path); + var runtime = settings.ResolveRuntimeSettings(); + AssertEqual("https://public@example.com/1", settings.Sentry.Dsn, "DSN Sentry"); AssertEqual( "haslo-z-pliku", runtime.EnovaConfiguration?.Connection.Password,