< Summary

Information
Class: OzonOrdersWeb.Observability.InfrastructureNoiseLogFilter
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Observability/InfrastructureNoiseLogFilter.cs
Line coverage
95%
Covered lines: 378
Uncovered lines: 18
Coverable lines: 396
Total lines: 818
Line coverage: 95.4%
Branch coverage
86%
Covered branches: 320
Total branches: 368
Branch coverage: 86.9%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
.cctor()100%11100%
IsTransientOutboundNetworkNoise(...)100%22100%
IsTransientImapNoise(...)100%22100%
IsTransientTelegramNetworkNoise(...)100%88100%
IsTransientElasticsearchNoise(...)100%22100%
IsTransientGoogleSheetsNoise(...)100%22100%
IsCorruptTempDataCookieNoise(...)100%66100%
IsTransactionGridCancellationNoise(...)100%44100%
ShouldExcludeFromErrorLog(...)100%22100%
IsTransientInfrastructureNoise(...)100%5656100%
IsAuthRateLimitNoise(...)81.25%1616100%
IsFailedLoginAttemptNoise(...)40.9%242284.61%
IsClientDiagnosticsRateLimitNoise(...)100%1010100%
IsArmtekDailyQuotaNoise(...)87.5%1616100%
IsErrorPageNoise(...)100%88100%
IsTransientOzonMethodNotAllowed(...)80.76%352676.47%
IsSuccessfulPerformanceTelemetry(...)100%1212100%
HasSuccessStatusCode(...)50%9871.42%
HasStatusCode(...)100%66100%
GetStringProperty(...)100%66100%
IsHostingNoise(...)100%22100%
IsDataProtectionNoEncryptorNoise(...)87.5%88100%
IsHangfireStorageContention(...)100%22100%
IsHangfireDistributedLockWait(...)90%1010100%
IsHangfireDistributedLockContention(...)100%44100%
IsHangfireWorkerStopRequested(...)100%66100%
IsRetriedEfConnectionError(...)100%22100%
IsRetriedEfQueryIteration(...)100%66100%
IsUncorrelatedEfTransactionError(...)100%44100%
IsDegradedHealthCheckNoise(...)100%22100%
IsBitrixConnectionNoise(...)100%44100%
IsOzonWebhookAvailabilityDiagnostic(...)100%22100%
IsOzonWebhookRejectionNoise(...)100%1414100%
IsSystemNotificationRejectionNoise(...)61.53%342676.92%
MatchesSource(...)90%101087.5%
IsExpectedBusinessOperationNoise(...)87.5%8887.5%
ContainsAny(...)100%1010100%
IsTransientSupplierServiceNoise(...)79.41%353490.47%

File(s)

/home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Observability/InfrastructureNoiseLogFilter.cs

#LineLine coverage
 1using System.Globalization;
 2using Microsoft.AspNetCore.Http;
 3using Serilog.Events;
 4using Services.ApiServices.ArmtekApi;
 5
 6namespace OzonOrdersWeb.Observability;
 7
 8/// <summary>
 9/// Решает, что не должно попадать в файл <c>errors-*.log</c>.
 10///
 11/// Этот файл — журнал отказов самого Studio2: наблюдатель
 12/// <c>Scripts/log-watcher/studio2_log_watcher.py</c> заводит по нему GitHub issue на каждую
 13/// уникальную запись уровня Warning и выше. Отсеиваются два класса записей:
 14///
 15/// <list type="number">
 16/// <item>транзиентный сбой чужой инфраструктуры, который приложение уже обрабатывает собственным
 17/// ретраем: отказом Studio2 он не является, исправлять в коде нечего;</item>
 18/// <item>телеметрия производительности об успешно завершённой операции («медленно, но успешно»):
 19/// это метрика, а не отказ, и её место в Seq, а не в очереди инцидентов.</item>
 20/// </list>
 21///
 22/// Отсев действует только на файловый sink — в Seq и консоль такие записи уходят как были,
 23/// с прежним уровнем и стеком, так что всплеск (например, длительная деградация БД) остаётся виден.
 24/// Правила намеренно узкие: сверяется и источник записи, и текст конкретного состояния.
 25/// </summary>
 26public static class InfrastructureNoiseLogFilter
 27{
 28    private const string RequestObservabilitySource = "OzonOrdersWeb.Observability.RequestObservabilityMiddleware";
 29    private const string ClientDiagnosticsPath = "/api/diagnostics/client-event";
 30
 31    /// <summary>
 32    /// Служебные процессы Hangfire, работающие со своими же таблицами. Прикладные джобы сюда
 33    /// не входят: их падения логируют <c>Hangfire.Server.Worker</c> и
 34    /// <c>Hangfire.AutomaticRetryAttribute</c>, и они обязаны оставаться в errors-*.log.
 35    /// </summary>
 136    private static readonly string[] HangfireStorageSources =
 137    [
 138        "Hangfire.MySql.",
 139        "Hangfire.Processing.BackgroundExecution",
 140        "Hangfire.Server.RecurringJobScheduler",
 141        "Hangfire.Server.DelayedJobScheduler",
 142        "Hangfire.Server.ServerWatchdog",
 143        "Hangfire.Server.ServerHeartbeatProcess"
 144    ];
 45
 46    private const string HangfireWorkerSource = "Hangfire.Server.Worker";
 47
 48    /// <summary>
 49    /// Состояния конкуренции MySQL/MariaDB, которые Hangfire переживает собственным ретраем
 50    /// («execution will be retried in no more than 00:00:04») и повторяет операцию сам.
 51    /// </summary>
 152    private static readonly string[] TransientStorageContention =
 153    [
 154        "Lock wait timeout exceeded",
 155        "Deadlock found when trying to get lock",
 156        "The Command Timeout expired",
 157        "Query execution was interrupted",
 158        "cannot acquire lock"
 159    ];
 60
 61    /// <summary>
 62    /// Текст события <c>RelationalEventId.ConnectionError</c> EF Core. На всех контекстах включён
 63    /// <c>EnableRetryOnFailure</c>, поэтому одиночный обрыв соединения гасится ретраем; если же
 64    /// ретраи не помогли, отказ прилетает вызывающему коду и логируется там с бизнес-контекстом.
 65    /// </summary>
 66    private const string EfConnectionErrorTemplate = "An error occurred using the connection to database";
 67
 68    // EF Core writes these diagnostics before MySqlRetryingExecutionStrategy retries an operation.
 69    // The final unhandled failure is logged by the caller with application context.
 70    private const string EfQueryIterationFailedTemplate = "An exception occurred while iterating over the results of a q
 71    private const string EfTransactionErrorTemplate = "An error occurred using a transaction.";
 72
 173    private static readonly string[] RetriableMySqlTimeoutMessages =
 174    [
 175        "The Command Timeout expired",
 176        "Query execution was interrupted",
 177        "Operation canceled"
 178    ];
 79
 80    /// <summary>
 81    /// Предупреждения инфраструктуры ASP.NET Core о переопределении портов и адресов при запуске хоста
 82    /// и редиректе HTTPS при SSL-терминации на обратном прокси (Nginx).
 83    /// </summary>
 184    private static readonly string[] HostingNoiseSources =
 185    [
 186        "Microsoft.AspNetCore.Hosting.Diagnostics",
 187        "Microsoft.AspNetCore.Server.Kestrel",
 188        "Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware"
 189    ];
 90
 191    private static readonly string[] HostingNoiseMessages =
 192    [
 193        "Overriding HTTP_PORTS",
 194        "Overriding address(es)",
 195        "Failed to determine the https port for redirect"
 196    ];
 97
 98    /// <summary>
 99    /// Источники и сообщения HealthCheck с результатом Degraded.
 100    /// Предупреждения о временной недоступности необязательных внешних БД (OzonApi, Bitrix) или
 101    /// пороге диска <5 ГБ не являются отказом Studio2: основная БД доступна, приложение работает штатно.
 102    /// Место таких предупреждений — Seq и дашборд готовности, а не журнал отказов.
 103    /// </summary>
 1104    private static readonly string[] HealthCheckSources =
 1105    [
 1106        "Microsoft.Extensions.Diagnostics.HealthChecks"
 1107    ];
 108
 109    private const string HealthCheckDegradedMessage = "status \"Degraded\"";
 110
 111    /// <summary>
 112    /// Источники и сообщения проверки доступности внешней базы данных Bitrix.
 113    /// Внешняя БД Bitrix является необязательной интеграцией: при её недоступности сервис
 114    /// BitrixConnectionService возвращает false, кэширует статус и приложение продолжает
 115    /// работу без падений. Такие предупреждения отслеживаются в Seq, но исключаются из errors-*.log (#2290).
 116    /// </summary>
 1117    private static readonly string[] BitrixConnectionNoiseSources =
 1118    [
 1119        "OzonRepositories.Services.BitrixConnectionService"
 1120    ];
 121
 1122    private static readonly string[] BitrixConnectionNoiseMessages =
 1123    [
 1124        "Bitrix connection check failed",
 1125        "Access denied for user",
 1126        "to database 'cd48454_4'"
 1127    ];
 128
 129    /// <summary>
 130    /// Источники логирования исходящих HTTP-запросов.
 131    /// </summary>
 1132    private static readonly string[] OutboundHttpSources =
 1133    [
 1134        "OzonOrdersWeb.Observability.OutboundRequestLoggingHandler",
 1135        "TecDocApi.API.Observability.TecDocOutboundRequestLoggingHandler"
 1136    ];
 137
 138    /// <summary>
 139    /// Сообщения о сбоях сетевого уровня и TLS-рукопожатия при обращениях к внешним сервисам.
 140    /// Сброс TCP-соединения сервером или сбой TLS — транзиентный отказ чужой инфраструктуры (#1994).
 141    /// </summary>
 1142    private static readonly string[] OutboundNetworkNoiseMessages =
 1143    [
 1144        "The SSL connection could not be established",
 1145        "Connection reset by peer",
 1146        "The remote computer refused the network connection"
 1147    ];
 148
 149    private static bool IsTransientOutboundNetworkNoise(LogEvent logEvent)
 149150        => MatchesSource(logEvent, OutboundHttpSources) && ContainsAny(logEvent, OutboundNetworkNoiseMessages);
 151
 152    /// Транзиентные сбои связи со сторонним почтовым сервером IMAP (MailKit).
 153    /// Временные разрывы сокета, протокольные сбои или таймауты SSL-рукопожатия обрабатываются
 154    /// ретраями и не являются отказом Studio2.
 155    /// </summary>
 1156    private static readonly string[] ImapNoiseSources =
 1157    [
 1158        "Services.EmailRoutingServices",
 1159        "Services.HangfireService.EmailAttachmentRoutingJob",
 1160        "MailKit"
 1161    ];
 162
 1163    private static readonly string[] ImapNoiseMessages =
 1164    [
 1165        "problems with connection",
 1166        "The server unexpected disconnected",
 1167        "An error occurred while attempting to establish an SSL or TLS connection",
 1168        "Network is unreachable",
 1169        "Connection reset by peer",
 1170        "ImapProtocolException",
 1171        "SslHandshakeException"
 1172    ];
 173
 174    private static bool IsTransientImapNoise(LogEvent logEvent)
 145175        => MatchesSource(logEvent, ImapNoiseSources) && ContainsAny(logEvent, ImapNoiseMessages);
 176
 177    /// <summary>
 178    /// Telegram-клиент возвращает вызывающему коду <c>Failure</c> при кратковременной сетевой
 179    /// недоступности, сохраняя Warning в Seq и консоли. Одиночный отказ внешнего Telegram не
 180    /// означает сбой Studio2, поэтому не должен создавать отдельный GitHub issue из errors-лога.
 181    /// Правило ограничено точным источником, шаблоном клиента и сетевой причиной (#2449).
 182    /// </summary>
 183    private static bool IsTransientTelegramNetworkNoise(LogEvent logEvent)
 142184        => logEvent.Level == LogEventLevel.Warning
 142185           && MatchesSource(logEvent, ["Services.ApiServices.TelegramBotApi.TelegramBotApiClient"])
 142186           && logEvent.MessageTemplate.Text.StartsWith("Telegram ", StringComparison.Ordinal)
 142187           && logEvent.MessageTemplate.Text.Contains("сбой запроса", StringComparison.Ordinal)
 142188           && ContainsAny(logEvent, ["Network is unreachable"]);
 189
 190    /// <summary>
 191    /// Источники логирования подсистемы поиска через Elasticsearch.
 192    /// </summary>
 1193    private static readonly string[] ElasticsearchNoiseSources =
 1194    [
 1195        "Services.SearchServices.OrderSearch",
 1196        "OzonOrdersWeb.HostedServices.OrderSearchStartupHostedService",
 1197        "Services.FiltersServices.DataFilterManagers.OrderDataFilterManager",
 1198        "Elasticsearch.Net",
 1199        "Nest"
 1200    ];
 201
 202    /// <summary>
 203    /// Сообщения о транзиентной недоступности узла Elasticsearch, сбоях подключения и восстановлении шардов.
 204    /// В Studio2 Elasticsearch — опциональный индекс ускорения: при его недоступности срабатывает штатный
 205    /// fallback на поиск по MariaDB. Транзиентные сбои во время старта или рестарта ноды уходят в Seq и консоль,
 206    /// но не засоряют errors-*.log ложными инцидентами.
 207    /// </summary>
 1208    private static readonly string[] ElasticsearchNoiseMessages =
 1209    [
 1210        "all shards failed",
 1211        "The client is unable to verify that the server is Elasticsearch",
 1212        "Connection refused",
 1213        "Elasticsearch-поиск заказов недоступен — fallback на LIKE-фильтрацию",
 1214        "не удалось создать индекс — проверьте доступность Elasticsearch",
 1215        "OrderSearch startup bootstrap failed",
 1216        "node_not_connected_exception"
 1217    ];
 218
 219    public static bool IsTransientElasticsearchNoise(LogEvent logEvent)
 123220        => MatchesSource(logEvent, ElasticsearchNoiseSources) && ContainsAny(logEvent, ElasticsearchNoiseMessages);
 221
 222    /// <summary>
 223    /// Транзиентные сбои связи со сторонним API Google Sheets (sheets.googleapis.com, oauth2.googleapis.com).
 224    /// Временная сетевая недоступность или ошибки авторизации Google не являются отказом Studio2
 225    /// и обрабатываются локальными повторами (#2304, #2305, #2306).
 226    /// </summary>
 1227    private static readonly string[] GoogleSheetsNoiseSources =
 1228    [
 1229        "Services.StockSyncServices.WarehouseStockSyncService",
 1230        "Services.StockSyncServices.WarehouseStockSyncJob",
 1231        "Google.Apis"
 1232    ];
 233
 1234    private static readonly string[] GoogleSheetsNoiseMessages =
 1235    [
 1236        "sheets.googleapis.com",
 1237        "oauth2.googleapis.com",
 1238        "Network is unreachable",
 1239        "GoogleApiException",
 1240        "TokenResponseException"
 1241    ];
 242
 243    private static bool IsTransientGoogleSheetsNoise(LogEvent logEvent)
 140244        => MatchesSource(logEvent, GoogleSheetsNoiseSources) && ContainsAny(logEvent, GoogleSheetsNoiseMessages);
 245
 246    /// <summary>
 247    /// CookieTempDataProvider штатно отбрасывает повреждённую клиентскую TempData-cookie и
 248    /// продолжает запрос с пустым TempData. Такие cookie появляются после ручного изменения,
 249    /// устаревшей версии приложения или обрыва записи и не означают отказа страницы входа.
 250    /// Отсеиваем только известный формат Base64 от встроенного провайдера (#2319).
 251    /// </summary>
 252    private static bool IsCorruptTempDataCookieNoise(LogEvent logEvent)
 137253        => logEvent.Level == LogEventLevel.Warning
 137254           && MatchesSource(logEvent, ["Microsoft.AspNetCore.Mvc.ViewFeatures.CookieTempDataProvider"])
 137255           && logEvent.MessageTemplate.Text.StartsWith("The temp data cookie", StringComparison.Ordinal)
 137256           && ContainsAny(logEvent, ["The input is not a valid Base-64 string"]);
 257
 258    /// <summary>
 259    /// Штатная отмена HTTP-запроса браузером (OperationCanceledException) при закрытии вкладки или переходе
 260    /// во время загрузки TransactionsGrid / остатков C0 (#2324).
 261    /// </summary>
 262    private static bool IsTransactionGridCancellationNoise(LogEvent logEvent)
 136263        => MatchesSource(logEvent, ["OzonOrdersWeb.WebServices.Transactions"])
 136264           && (logEvent.Exception is OperationCanceledException
 136265               || ContainsAny(logEvent, ["OperationCanceledException", "The operation was canceled"]));
 266
 267    /// <summary>
 268    /// Единственная точка решения для файлового sink <c>errors-*.log</c>: запись либо транзиентный
 269    /// шум чужой инфраструктуры, либо метрика производительности успешной операции.
 270    /// </summary>
 271    public static bool ShouldExcludeFromErrorLog(LogEvent logEvent)
 77272        => IsTransientInfrastructureNoise(logEvent)
 77273           || IsSuccessfulPerformanceTelemetry(logEvent);
 274
 275    /// <summary>Запись — транзиентный шум инфраструктуры и в errors-*.log не нужна.</summary>
 276    public static bool IsTransientInfrastructureNoise(LogEvent logEvent)
 177277        => IsHangfireStorageContention(logEvent)
 177278           || IsHangfireDistributedLockWait(logEvent)
 177279           || IsHangfireDistributedLockContention(logEvent)
 177280           || IsHangfireWorkerStopRequested(logEvent)
 177281           || IsRetriedEfConnectionError(logEvent)
 177282           || IsRetriedEfQueryIteration(logEvent)
 177283           || IsUncorrelatedEfTransactionError(logEvent)
 177284           || IsHostingNoise(logEvent)
 177285           || IsDataProtectionNoEncryptorNoise(logEvent)
 177286           || IsDegradedHealthCheckNoise(logEvent)
 177287           || IsBitrixConnectionNoise(logEvent)
 177288           || IsTransientOutboundNetworkNoise(logEvent)
 177289           || IsTransientImapNoise(logEvent)
 177290           || IsTransientTelegramNetworkNoise(logEvent)
 177291           || IsTransientGoogleSheetsNoise(logEvent)
 177292           || IsCorruptTempDataCookieNoise(logEvent)
 177293           || IsTransactionGridCancellationNoise(logEvent)
 177294           || IsClientDiagnosticsRateLimitNoise(logEvent)
 177295           || IsErrorPageNoise(logEvent)
 177296           || IsTransientOzonMethodNotAllowed(logEvent)
 177297           || IsTransientElasticsearchNoise(logEvent)
 177298           || IsOzonWebhookAvailabilityDiagnostic(logEvent)
 177299           || IsOzonWebhookRejectionNoise(logEvent)
 177300           || IsAuthRateLimitNoise(logEvent)
 177301           || IsFailedLoginAttemptNoise(logEvent)
 177302           || IsTransientSupplierServiceNoise(logEvent)
 177303           || IsArmtekDailyQuotaNoise(logEvent)
 177304           || IsExpectedBusinessOperationNoise(logEvent)
 177305           || IsSystemNotificationRejectionNoise(logEvent);
 306
 307    /// <summary>
 308    /// Ограничение частоты запросов к эндпоинтам входа (/Account/Login, /Identity/Account/Login)
 309    /// политикой AuthRateLimiterPolicy для защиты от брутфорса (#2402).
 310    /// Срабатывание лимита возвращает 429 Too Many Requests — штатная защитная функция приложения, не отказ.
 311    /// </summary>
 312    public static bool IsAuthRateLimitNoise(LogEvent logEvent)
 313    {
 95314        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 34315            return false;
 316
 61317        if (HasStatusCode(logEvent, StatusCodes.Status429TooManyRequests))
 318        {
 2319            var path = GetStringProperty(logEvent, "RequestPath");
 2320            if (path is not null && (path.StartsWith("/Identity/Account/Login", StringComparison.OrdinalIgnoreCase)
 2321                                     || path.StartsWith("/Account/Login", StringComparison.OrdinalIgnoreCase)))
 322            {
 2323                return true;
 324            }
 325        }
 326
 59327        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 59328        return message.Contains("429", StringComparison.Ordinal)
 59329               && (message.Contains("/Identity/Account/Login", StringComparison.OrdinalIgnoreCase)
 59330                   || message.Contains("/Account/Login", StringComparison.OrdinalIgnoreCase));
 331    }
 332
 333    /// <summary>
 334    /// Неуспешные попытки входа в систему (неверный пароль, несуществующий логин, невалидная форма)
 335    /// являются аудитом безопасности и действиями внешних клиентов/ботов, а не отказом приложения (#2320, #2393, #2400,
 336    /// Записи остаются в Seq и консоли со всем скоупом (IP, username, EventKind), но исключаются из errors-*.log.
 337    /// </summary>
 338    public static bool IsFailedLoginAttemptNoise(LogEvent logEvent)
 339    {
 93340        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 34341            return false;
 342
 59343        if (MatchesSource(logEvent, ["OzonOrdersWeb.Areas.Identity.Pages.Account.LoginModel"]))
 5344            return true;
 345
 54346        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 54347        if (message.StartsWith("Неуспешная попытка входа для ", StringComparison.Ordinal))
 0348            return true;
 349
 54350        var category = GetStringProperty(logEvent, "EventCategory");
 54351        var eventKind = GetStringProperty(logEvent, "EventKind");
 54352        if (string.Equals(category, "auth", StringComparison.OrdinalIgnoreCase)
 54353            && eventKind is "LoginUserNotFound" or "LoginInvalidPassword" or "LoginFormValidationFailed" or "LoginEmailN
 354        {
 0355            return true;
 356        }
 357
 54358        return false;
 359    }
 360
 361    /// <summary>
 362    /// Лимит клиентской телеметрии защищает endpoint от лавины одинаковых событий. Ответ 429
 363    /// в этом случае ожидаем: сами события уже не нужны, а запись остаётся доступна в Seq.
 364    /// Другие 429 и любые исключения остаются инцидентами.
 365    /// </summary>
 366    private static bool IsClientDiagnosticsRateLimitNoise(LogEvent logEvent)
 130367        => logEvent.Exception is null
 130368           && logEvent.Level <= LogEventLevel.Warning
 130369           && MatchesSource(logEvent, [RequestObservabilitySource])
 130370           && string.Equals(GetStringProperty(logEvent, "EventKind"), "HttpRequestFailed", StringComparison.Ordinal)
 130371           && HasStatusCode(logEvent, StatusCodes.Status429TooManyRequests)
 130372           && string.Equals(GetStringProperty(logEvent, "RequestPath"), ClientDiagnosticsPath, StringComparison.OrdinalI
 373
 374    /// <summary>
 375    /// Исчерпание суточной квоты внешнего API Armtek (ws.armtek.ru).
 376    /// Это лимит тарифа поставщика, а не отказ Studio2 (#2350).
 377    /// </summary>
 378    public static bool IsArmtekDailyQuotaNoise(LogEvent logEvent)
 379    {
 89380        if (logEvent.Level > LogEventLevel.Warning)
 29381            return false;
 382
 60383        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 60384        var exception = logEvent.Exception?.ToString();
 385
 60386        var isQuotaMessage = ArmtekDailyQuotaGuard.IsExceeded(message)
 60387                             || message.Contains("исчерпал суточную квоту", StringComparison.OrdinalIgnoreCase)
 60388                             || (exception != null && (ArmtekDailyQuotaGuard.IsExceeded(exception) || exception.Contains
 389
 60390        return isQuotaMessage
 60391               && (MatchesSource(logEvent, ["Services.ApiServices.ArmtekApi", "OzonOrdersWeb"]) || message.Contains("Arm
 392    }
 393
 394    /// <summary>
 395    /// Ответ страницы ошибки (/Home/Error): отдача клиенту страницы со статусом 4xx/5xx не
 396    /// является самостоятельным отказом приложения. Настоящие сбои в логике рендеринга страницы ошибки
 397    /// несут исключение и остаются в журнале.
 398    /// </summary>
 399    public static bool IsErrorPageNoise(LogEvent logEvent)
 400    {
 129401        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 36402            return false;
 403
 93404        var path = GetStringProperty(logEvent, "RequestPath");
 93405        if (path?.StartsWith("/Home/Error", StringComparison.OrdinalIgnoreCase) == true)
 3406            return true;
 407
 90408        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 90409        return message.Contains(" /Home/Error ", StringComparison.OrdinalIgnoreCase);
 410    }
 411
 412    /// <summary>
 413    /// Ответы 405 Method Not Allowed от Ozon API (api-seller.ozon.ru, ozon.ru) при отказе метода шлюзом/прокси.
 414    /// </summary>
 415    public static bool IsTransientOzonMethodNotAllowed(LogEvent logEvent)
 416    {
 126417        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 36418            return false;
 419
 90420        if (!MatchesSource(logEvent, OutboundHttpSources))
 79421            return false;
 422
 11423        if (!logEvent.Properties.TryGetValue("StatusCode", out var value) || value is not ScalarValue { Value: 405 })
 424        {
 7425            var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 7426            if (!message.Contains(" 405", StringComparison.Ordinal))
 7427                return false;
 428        }
 429
 4430        var uri = GetStringProperty(logEvent, "RequestUri");
 4431        if (string.IsNullOrWhiteSpace(uri))
 432        {
 0433            var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 0434            return message.Contains("api-seller.ozon.ru", StringComparison.OrdinalIgnoreCase)
 0435                   || message.Contains(".ozon.ru", StringComparison.OrdinalIgnoreCase)
 0436                   || message.Contains(".ozone.ru", StringComparison.OrdinalIgnoreCase);
 437        }
 438
 4439        return uri.Contains("api-seller.ozon.ru", StringComparison.OrdinalIgnoreCase)
 4440               || uri.Contains(".ozon.ru", StringComparison.OrdinalIgnoreCase)
 4441               || uri.Contains(".ozone.ru", StringComparison.OrdinalIgnoreCase);
 442    }
 443
 444    /// <summary>
 445    /// Запись сообщает, что операция ЗАВЕРШИЛАСЬ УСПЕШНО, но дольше порога наблюдаемости
 446    /// (<see cref="ObservabilityOptions.SlowRequestMilliseconds"/> и соседние).
 447    ///
 448    /// Пороги специально низкие — 1 с на внешний вызов, 3 с на запрос страницы, — потому что они
 449    /// задуманы как метрика для Seq: там такая запись отбирается по <c>EventKind</c> и показывает
 450    /// распределение по маршрутам. В errors-*.log она попадает только из-за уровня Warning, и
 451    /// наблюдатель заводит по ней отдельный GitHub issue на каждый URL, маршрут, SQL и джоб.
 452    /// Ответ поставщика за 1,1 с и страница за 3,2 с инцидентами не являются: чинить в коде нечего,
 453    /// сигнал самим фактом попадания в очередь инцидентов ничего не добавляет, а настоящие отказы
 454    /// тонут в нём. Уровень и содержимое записи не меняются: в Seq и консоли она остаётся Warning.
 455    ///
 456    /// Отсев узкий: отбрасывается только успешный исход. Отказ той же операции идёт другим
 457    /// <c>EventKind</c> (<c>HttpRequestFailed</c>, <c>DatabaseCommandFailed</c>) либо несёт код
 458    /// ответа 4xx/5xx или исключение — и остаётся в errors-*.log.
 459    /// </summary>
 460    public static bool IsSuccessfulPerformanceTelemetry(LogEvent logEvent)
 461    {
 462        // Error и Fatal ставит только ветка отказа: такую запись не прячем ни при каком EventKind.
 32463        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 13464            return false;
 465
 19466        return GetStringProperty(logEvent, "EventKind") switch
 19467        {
 19468            // Медленный, но не сбойный входящий запрос: статус >= 400 даёт EventKind HttpRequestFailed.
 3469            "SlowHttpRequest" => HasSuccessStatusCode(logEvent),
 19470            // Интерцептор пишет этот EventKind только для выполненной команды; ошибка идёт
 19471            // DatabaseCommandFailed уровня Error.
 1472            "SlowDatabaseCommand" => true,
 19473            // У джоба один EventKind на все исходы, поэтому сверяем шаблон конкретной ветки.
 1474            "HangfireJob" => logEvent.MessageTemplate.Text.StartsWith(SlowHangfireJobTemplate, StringComparison.Ordinal)
 19475            // Один EventKind и на медленный успех, и на ошибочный код ответа: различает статус.
 9476            "OutboundHttp" => HasSuccessStatusCode(logEvent),
 5477            _ => false
 19478        };
 479    }
 480
 481    /// <summary>Начало шаблона ветки «успешно, но дольше порога» в <see cref="HangfireObservabilityFilter"/>.</summary>
 482    private const string SlowHangfireJobTemplate = "Медленный Hangfire job";
 483
 484    /// <summary>
 485    /// Код ответа записан и лежит вне диапазона ошибок. Отсутствие кода трактуется как отказ:
 486    /// у исходящего вызова его нет только тогда, когда ответа не было вовсе.
 487    /// </summary>
 488    private static bool HasSuccessStatusCode(LogEvent logEvent)
 489    {
 12490        if (!logEvent.Properties.TryGetValue("StatusCode", out var value) || value is not ScalarValue scalar)
 0491            return false;
 492
 12493        return scalar.Value switch
 12494        {
 12495            int statusCode => statusCode is >= 100 and < 400,
 0496            _ => false
 12497        };
 498    }
 499
 500    private static bool HasStatusCode(LogEvent logEvent, int expectedStatusCode)
 83501        => logEvent.Properties.TryGetValue("StatusCode", out var value)
 83502           && value is ScalarValue { Value: int statusCode }
 83503           && statusCode == expectedStatusCode;
 504
 505    private static string? GetStringProperty(LogEvent logEvent, string name)
 487506        => logEvent.Properties.TryGetValue(name, out var value) && value is ScalarValue { Value: string text }
 487507            ? text
 487508            : null;
 509
 510    private static bool IsHostingNoise(LogEvent logEvent)
 160511        => MatchesSource(logEvent, HostingNoiseSources) && ContainsAny(logEvent, HostingNoiseMessages);
 512
 513    /// <summary>
 514    /// Штатное предупреждение ASP.NET Core Data Protection (XmlKeyManager) при сохранении ключей шифрования
 515    /// в файловую систему Linux/Docker без внешнего XML-шифратора (DPAPI/KeyVault/сертификата):
 516    /// "No XML encryptor configured. Key {guid} may be persisted to storage in unencrypted form."
 517    /// Ключи защищены правами доступа файловой системы и тома контейнера (#2325).
 518    /// </summary>
 519    private const string DataProtectionXmlKeyManagerSource = "Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyMa
 520    private const string DataProtectionNoEncryptorTemplate = "No XML encryptor configured";
 521
 522    public static bool IsDataProtectionNoEncryptorNoise(LogEvent logEvent)
 157523        => logEvent.Exception is null
 157524           && logEvent.Level <= LogEventLevel.Warning
 157525           && MatchesSource(logEvent, [DataProtectionXmlKeyManagerSource])
 157526           && (logEvent.MessageTemplate.Text.StartsWith(DataProtectionNoEncryptorTemplate, StringComparison.Ordinal)
 157527               || logEvent.RenderMessage(CultureInfo.InvariantCulture).StartsWith(DataProtectionNoEncryptorTemplate, Str
 528
 529    private static bool IsHangfireStorageContention(LogEvent logEvent)
 177530        => MatchesSource(logEvent, HangfireStorageSources) && ContainsAny(logEvent, TransientStorageContention);
 531
 532    /// <summary>
 533    /// Штатное ожидание распределённой блокировки <c>DisableConcurrentExecution</c> завершилось
 534    /// успешно. Hangfire пишет его как slow log от Worker, но это метрика очереди, не отказ job.
 535    /// Настоящие ошибки Worker (с исключением или другим шаблоном) остаются в errors-*.log.
 536    /// </summary>
 537    private static bool IsHangfireDistributedLockWait(LogEvent logEvent)
 538    {
 171539        if (logEvent.Exception is not null || logEvent.Level != LogEventLevel.Warning)
 57540            return false;
 541
 114542        if (!MatchesSource(logEvent, [HangfireWorkerSource]))
 111543            return false;
 544
 3545        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 3546        return message.Contains("DisableConcurrentExecutionAttribute", StringComparison.OrdinalIgnoreCase)
 3547               && (message.Contains("Slow log:", StringComparison.OrdinalIgnoreCase)
 3548                   || message.Contains("OnPerforming", StringComparison.OrdinalIgnoreCase));
 549    }
 550
 551    /// <summary>
 552    /// Конкуренция за распределённую блокировку между параллельными воркерами (cannot acquire lock).
 553    /// </summary>
 554    private static bool IsHangfireDistributedLockContention(LogEvent logEvent)
 555    {
 169556        var matchesSource = MatchesSource(logEvent, [HangfireWorkerSource, "Hangfire.AutomaticRetryAttribute", typeof(Ha
 169557                            || string.Equals(GetStringProperty(logEvent, "EventKind"), "HangfireJob", StringComparison.O
 558
 169559        return matchesSource && ContainsAny(logEvent, ["cannot acquire lock", "MySqlDistributedLockException"]);
 560    }
 561
 562    /// <summary>
 563    /// Штатная остановка воркера Hangfire при рестарте контейнера или деплое: фоновая задача
 564    /// возвращается в очередь и выполняется заново. Это не отказ Studio2 (#1084). Настоящее падение
 565    /// задачи несёт исключение и сюда не попадает.
 566    /// </summary>
 567    private const string HangfireWorkerStopTemplate = "Worker stop requested while processing background job";
 568
 569    private static bool IsHangfireWorkerStopRequested(LogEvent logEvent)
 165570        => logEvent.Exception is null
 165571           && logEvent.Level <= LogEventLevel.Warning
 165572           && MatchesSource(logEvent, [HangfireWorkerSource])
 165573           && logEvent.MessageTemplate.Text.StartsWith(HangfireWorkerStopTemplate, StringComparison.Ordinal);
 574
 575    private static bool IsRetriedEfConnectionError(LogEvent logEvent)
 164576        => MatchesSource(logEvent, ["Microsoft.EntityFrameworkCore"])
 164577           && logEvent.MessageTemplate.Text.StartsWith(EfConnectionErrorTemplate, StringComparison.Ordinal);
 578
 579    /// <summary>
 580    /// EF Core emits <c>QueryIterationFailed</c> for every failed attempt before the configured
 581    /// MySQL execution strategy retries it. Keep non-MySQL and non-timeout failures in the error log.
 582    /// </summary>
 583    private static bool IsRetriedEfQueryIteration(LogEvent logEvent)
 162584        => MatchesSource(logEvent, ["Microsoft.EntityFrameworkCore.Query"])
 162585           && logEvent.MessageTemplate.Text.StartsWith(EfQueryIterationFailedTemplate, StringComparison.Ordinal)
 162586           && ContainsAny(logEvent, ["MySqlConnector.MySqlException"])
 162587           && ContainsAny(logEvent, RetriableMySqlTimeoutMessages);
 588
 589    /// <summary>
 590    /// A bare EF <c>TransactionError</c> has no exception or operation context and is a duplicate
 591    /// of the preceding database diagnostic. Events carrying an exception remain visible.
 592    /// </summary>
 593    private static bool IsUncorrelatedEfTransactionError(LogEvent logEvent)
 161594        => logEvent.Exception is null
 161595           && MatchesSource(logEvent, ["Microsoft.EntityFrameworkCore.Database.Transaction"])
 161596           && string.Equals(logEvent.MessageTemplate.Text, EfTransactionErrorTemplate, StringComparison.Ordinal);
 597
 598    private static bool IsDegradedHealthCheckNoise(LogEvent logEvent)
 152599        => MatchesSource(logEvent, HealthCheckSources) && ContainsAny(logEvent, [HealthCheckDegradedMessage]);
 600
 601    public static bool IsBitrixConnectionNoise(LogEvent logEvent)
 151602        => logEvent.Level <= LogEventLevel.Warning
 151603           && MatchesSource(logEvent, BitrixConnectionNoiseSources)
 151604           && ContainsAny(logEvent, BitrixConnectionNoiseMessages);
 605
 606    /// <summary>
 607    /// Диагностика доступности пуш-адресов в кабинетах Ozon.
 608    /// Контролируется джобом OzonWebhookAvailabilityJob и выводится в UI через баннер OzonSystemAlertSource
 609    /// и раздел /Studio2/JobLogs?tab=webhook. Внешняя недоступность или отключение адреса Ozon не является
 610    /// отказом Studio2: приложение продолжает работать штатно (заказы подтягиваются регулярным опросом API).
 611    /// Запись остаётся в Seq и консоли, но исключается из errors-*.log, чтобы не плодить ложные инциденты (#2363).
 612    /// </summary>
 613    public static bool IsOzonWebhookAvailabilityDiagnostic(LogEvent logEvent)
 116614        => MatchesSource(logEvent, ["Services.OzonWebhookServices.Monitoring.OzonWebhookAvailabilityJob"])
 116615           && logEvent.MessageTemplate.Text.StartsWith("Пуш-уведомления Ozon недоступны", StringComparison.Ordinal);
 616
 617    /// <summary>
 618    /// Штатное отклонение некорректных входящих запросов на публичный эндпоинт приёма вебхуков Ozon
 619    /// (/Studio2/OzonWebhook/Receive): отсутствие/невалидность токена, IP, пустой payload, не-JSON payload (#2357, #235
 620    /// </summary>
 621    public static bool IsOzonWebhookRejectionNoise(LogEvent logEvent)
 622    {
 117623        if (logEvent.Level > LogEventLevel.Warning)
 29624            return false;
 625
 88626        if (MatchesSource(logEvent, [RequestObservabilitySource]))
 627        {
 24628            var path = GetStringProperty(logEvent, "RequestPath");
 24629            if (path?.StartsWith("/Studio2/OzonWebhook/Receive", StringComparison.OrdinalIgnoreCase) == true)
 630            {
 6631                if (HasStatusCode(logEvent, StatusCodes.Status400BadRequest) || HasStatusCode(logEvent, StatusCodes.Stat
 6632                    return true;
 633            }
 634        }
 635
 82636        if (MatchesSource(logEvent, [
 82637            "OzonOrdersWeb.Middleware.OzonIpWhitelistFilter",
 82638            "Services.OzonWebhookServices.OzonWebhookService",
 82639            "Services.OzonWebhookServices.OzonWebhookNotificationFactory",
 82640            "OzonOrdersWeb.Areas.Studio2.Controllers.OzonWebhookController"]))
 641        {
 15642            return ContainsAny(logEvent, [
 15643                "Ozon webhook: запрос отклонён",
 15644                "не входит в список разрешённых диапазонов",
 15645                "неверный или отсутствующий token",
 15646                "SharedToken не задан",
 15647                "Получен пустой payload",
 15648                "Не удалось разобрать JSON для определения типа",
 15649                "не удалось разобрать JSON payload"
 15650            ]);
 651        }
 652
 67653        return false;
 654    }
 655
 656    /// <summary>
 657    /// Штатное отклонение некорректных/неавторизованных входящих запросов на эндпоинты системных уведомлений деплоя
 658    /// (/Studio2/SystemNotification/DeployEvent): отсутствие/неверный ключ X-Api-Key, ошибки валидации формы деплоя (#2
 659    /// </summary>
 660    public static bool IsSystemNotificationRejectionNoise(LogEvent logEvent)
 661    {
 77662        if (logEvent.Level > LogEventLevel.Warning)
 29663            return false;
 664
 48665        if (MatchesSource(logEvent, [RequestObservabilitySource]))
 666        {
 18667            var path = GetStringProperty(logEvent, "RequestPath");
 18668            if (path?.StartsWith("/Studio2/SystemNotification", StringComparison.OrdinalIgnoreCase) == true)
 669            {
 6670                if (HasStatusCode(logEvent, StatusCodes.Status401Unauthorized) || HasStatusCode(logEvent, StatusCodes.St
 6671                    return true;
 672            }
 673        }
 674
 42675        if (MatchesSource(logEvent, [
 42676            "OzonOrdersWeb.Areas.Studio2.Controllers.SystemNotificationController",
 42677            "OzonOrdersWeb.Observability.InvalidModelStateLoggingFilter"]))
 678        {
 12679            return ContainsAny(logEvent, [
 12680                "Событие деплоя с неверным ключом",
 12681                "Событие деплоя отклонено: неверный или отсутствующий ключ безопасности",
 12682                "Попытка отправки системного уведомления отклонена: неверный ключ безопасности",
 12683                "Форма POST /Studio2/SystemNotification/DeployEvent не прошла серверную валидацию",
 12684                "Форма POST /Studio2/SystemNotification"
 12685            ]);
 686        }
 687
 30688        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 30689        if (message.Contains("/Studio2/SystemNotification", StringComparison.OrdinalIgnoreCase))
 690        {
 0691            if (message.Contains("401", StringComparison.Ordinal) || message.Contains("403", StringComparison.Ordinal))
 0692                return true;
 0693            if (message.Contains("Событие деплоя", StringComparison.OrdinalIgnoreCase)
 0694                || message.Contains("системного уведомления", StringComparison.OrdinalIgnoreCase)
 0695                || message.Contains("серверную валидацию", StringComparison.OrdinalIgnoreCase))
 0696                return true;
 697        }
 698
 30699        return false;
 700    }
 701
 702    private static bool MatchesSource(LogEvent logEvent, string[] prefixes)
 703    {
 3210704        if (!logEvent.Properties.TryGetValue("SourceContext", out var value) || value is not ScalarValue scalar)
 15705            return false;
 3195706        if (scalar.Value is not string sourceContext)
 0707            return false;
 708
 20372709        foreach (var prefix in prefixes)
 710        {
 7116711            if (sourceContext.StartsWith(prefix, StringComparison.Ordinal))
 250712                return true;
 713        }
 714
 2945715        return false;
 716    }
 717
 718    /// <summary>
 719    /// Штатные бизнес-отказы операций, возвращающих HTTP 200 (например, валидация экземпляров Ozon,
 720    /// печать этикеток или проведение), когда отказ является штатным ответом пользователю (#1418, #1813, #2134, #2383).
 721    /// </summary>
 722    public static bool IsExpectedBusinessOperationNoise(LogEvent logEvent)
 723    {
 83724        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 34725            return false;
 726
 49727        if (!MatchesSource(logEvent, ["OzonOrdersWeb.Observability.BusinessFailureLoggingFilter"]))
 35728            return false;
 729
 14730        var path = GetStringProperty(logEvent, "RequestPath");
 14731        if (string.IsNullOrEmpty(path))
 0732            return false;
 733
 14734        return BusinessFailureLoggingFilter.IsExpectedFailurePath(path);
 735    }
 736
 737    /// <summary>
 738    /// Hangfire кладёт причину то в текст записи, то в приложенное исключение, поэтому смотрим оба.
 739    /// </summary>
 740    private static bool ContainsAny(LogEvent logEvent, string[] needles)
 741    {
 106742        var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 106743        var exception = logEvent.Exception?.ToString();
 744
 705745        foreach (var needle in needles)
 746        {
 282747            if (message.Contains(needle, StringComparison.Ordinal))
 65748                return true;
 217749            if (exception is not null && exception.Contains(needle, StringComparison.Ordinal))
 6750                return true;
 751        }
 752
 35753        return false;
 754    }
 755
 1756    private static readonly string[] SupplierHosts =
 1757    [
 1758        "armtek.ru",
 1759        "armtek.by",
 1760        "forum-auto.ru",
 1761        "ml-auto.ru",
 1762        "ml-auto.by",
 1763        "motexc.by",
 1764        "profit-liga.ru",
 1765        "favorit-parts.ru",
 1766        "moskvorechie.ru",
 1767        "berg.ru",
 1768        "shate-m.by",
 1769        "japarts.ru",
 1770        "nikei.ru",
 1771        "avdmotors.ru",
 1772        "mikado-parts.ru",
 1773        "zzap.ru",
 1774        "zzap.pro"
 1775    ];
 776
 777    /// <summary>
 778    /// Временные сбои сервисов внешних поставщиков (HTTP 500, 502, 503, 504 при временной недоступности стороннего серв
 779    /// В OutboundRequestLoggingHandler такие ответы логируются на уровне Information (#2098, #2173, #2307).
 780    /// Фильтр страхует от попадания в errors-*.log предупреждений внешних сервисов при сетевых сбоях третьих сторон.
 781    /// </summary>
 782    public static bool IsTransientSupplierServiceNoise(LogEvent logEvent)
 783    {
 92784        if (logEvent.Exception is not null || logEvent.Level > LogEventLevel.Warning)
 35785            return false;
 786
 57787        if (!MatchesSource(logEvent, OutboundHttpSources))
 46788            return false;
 789
 11790        var is5xx = false;
 11791        if (logEvent.Properties.TryGetValue("StatusCode", out var value) && value is ScalarValue { Value: int code and (
 792        {
 8793            is5xx = true;
 794        }
 795        else
 796        {
 3797            var message = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 3798            if (message.Contains("вернул 500", StringComparison.Ordinal) ||
 3799                message.Contains("вернул 502", StringComparison.Ordinal) ||
 3800                message.Contains("вернул 503", StringComparison.Ordinal) ||
 3801                message.Contains("вернул 504", StringComparison.Ordinal))
 802            {
 0803                is5xx = true;
 804            }
 805        }
 806
 11807        if (!is5xx)
 3808            return false;
 809
 8810        var uri = GetStringProperty(logEvent, "RequestUri");
 8811        var host = GetStringProperty(logEvent, "DependencyHost");
 8812        var fullText = (uri ?? string.Empty) + " " + (host ?? string.Empty);
 8813        if (string.IsNullOrWhiteSpace(fullText.Trim()))
 0814            fullText = logEvent.RenderMessage(CultureInfo.InvariantCulture);
 815
 123816        return SupplierHosts.Any(h => fullText.Contains(h, StringComparison.OrdinalIgnoreCase));
 817    }
 818}

Methods/Properties

.cctor()
IsTransientOutboundNetworkNoise(Serilog.Events.LogEvent)
IsTransientImapNoise(Serilog.Events.LogEvent)
IsTransientTelegramNetworkNoise(Serilog.Events.LogEvent)
IsTransientElasticsearchNoise(Serilog.Events.LogEvent)
IsTransientGoogleSheetsNoise(Serilog.Events.LogEvent)
IsCorruptTempDataCookieNoise(Serilog.Events.LogEvent)
IsTransactionGridCancellationNoise(Serilog.Events.LogEvent)
ShouldExcludeFromErrorLog(Serilog.Events.LogEvent)
IsTransientInfrastructureNoise(Serilog.Events.LogEvent)
IsAuthRateLimitNoise(Serilog.Events.LogEvent)
IsFailedLoginAttemptNoise(Serilog.Events.LogEvent)
IsClientDiagnosticsRateLimitNoise(Serilog.Events.LogEvent)
IsArmtekDailyQuotaNoise(Serilog.Events.LogEvent)
IsErrorPageNoise(Serilog.Events.LogEvent)
IsTransientOzonMethodNotAllowed(Serilog.Events.LogEvent)
IsSuccessfulPerformanceTelemetry(Serilog.Events.LogEvent)
HasSuccessStatusCode(Serilog.Events.LogEvent)
HasStatusCode(Serilog.Events.LogEvent,System.Int32)
GetStringProperty(Serilog.Events.LogEvent,System.String)
IsHostingNoise(Serilog.Events.LogEvent)
IsDataProtectionNoEncryptorNoise(Serilog.Events.LogEvent)
IsHangfireStorageContention(Serilog.Events.LogEvent)
IsHangfireDistributedLockWait(Serilog.Events.LogEvent)
IsHangfireDistributedLockContention(Serilog.Events.LogEvent)
IsHangfireWorkerStopRequested(Serilog.Events.LogEvent)
IsRetriedEfConnectionError(Serilog.Events.LogEvent)
IsRetriedEfQueryIteration(Serilog.Events.LogEvent)
IsUncorrelatedEfTransactionError(Serilog.Events.LogEvent)
IsDegradedHealthCheckNoise(Serilog.Events.LogEvent)
IsBitrixConnectionNoise(Serilog.Events.LogEvent)
IsOzonWebhookAvailabilityDiagnostic(Serilog.Events.LogEvent)
IsOzonWebhookRejectionNoise(Serilog.Events.LogEvent)
IsSystemNotificationRejectionNoise(Serilog.Events.LogEvent)
MatchesSource(Serilog.Events.LogEvent,System.String[])
IsExpectedBusinessOperationNoise(Serilog.Events.LogEvent)
ContainsAny(Serilog.Events.LogEvent,System.String[])
IsTransientSupplierServiceNoise(Serilog.Events.LogEvent)