< Summary

Information
Class: OzonOrdersWeb.Observability.OutboundRequestLoggingHandler
Assembly: OzonOrdersWeb
File(s): /home/runner/work/Studio2Prod/Studio2Prod/OzonOrdersWeb/Observability/OutboundRequestLoggingHandler.cs
Line coverage
95%
Covered lines: 262
Uncovered lines: 12
Coverable lines: 274
Total lines: 459
Line coverage: 95.6%
Branch coverage
91%
Covered branches: 219
Total branches: 240
Branch coverage: 91.2%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Metrics

File(s)

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

#LineLine coverage
 1using System.Diagnostics;
 2using Microsoft.Extensions.Options;
 3using Polly.CircuitBreaker;
 4using Services.ApiServices.ArmtekApi;
 5
 6namespace OzonOrdersWeb.Observability;
 7
 728public sealed class OutboundRequestLoggingHandler(
 729    ILogger<OutboundRequestLoggingHandler> logger,
 7210    SensitiveDataRedactor redactor,
 7211    IOptions<ObservabilityOptions> options) : DelegatingHandler
 12{
 7213    private readonly ObservabilityOptions _options = options.Value;
 14
 15    protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellat
 16    {
 7217        var timer = Stopwatch.StartNew();
 18        try
 19        {
 7220            var response = await base.SendAsync(request, cancellationToken);
 5421            var elapsed = timer.ElapsedMilliseconds;
 5422            var isSlow = elapsed >= _options.SlowDependencyMilliseconds;
 5423            var isExpectedHeadNotFound = request.Method == HttpMethod.Head && response.StatusCode == System.Net.HttpStat
 5424            var isExpectedMotexSearchNotFound = IsExpectedMotexSearchNotFound(request, response);
 5425            var isExpectedMlAutoNotFound = IsExpectedMlAutoNotFound(request, response);
 5426            var isExpectedOzonProductAttributesNotFound = IsExpectedOzonProductAttributesNotFound(request, response);
 5427            var isExpectedOzonPostingNotFound = IsExpectedOzonPostingNotFound(request, response);
 5428            var isArmtekDailyQuotaExceeded = await IsArmtekDailyQuotaExceededAsync(request, response, cancellationToken)
 5429            var isTransientOzonS3ReportError = IsTransientOzonS3ReportError(request, response);
 5430            var isTransientOzonChatError = IsTransientOzonChatError(request, response);
 5431            var isTransientOzonWebhookRateLimit = IsTransientOzonWebhookRateLimit(request, response);
 5432            var isExpectedBusinessRejection = IsExpectedBusinessRejection(request, response);
 5433            var isExpectedOneCDocumentPostingRejection = IsExpectedOneCDocumentPostingRejection(request, response);
 5434            var isTransientSupplierServiceError = IsTransientSupplierServiceError(request, response);
 5435            var isExpectedOzonMethodNotAllowed = IsExpectedOzonMethodNotAllowed(request, response);
 5436            var isOpenRouterAuthError = IsOpenRouterAuthError(request, response);
 37
 5438            if ((!response.IsSuccessStatusCode && !isExpectedHeadNotFound) || isSlow)
 39            {
 5240                using var scope = BeginScope(request, elapsed, (int)response.StatusCode);
 5241                if (isExpectedMotexSearchNotFound)
 42                {
 143                    logger.LogInformation("МоТехС не нашёл предложение: HTTP {Method} {Uri} вернул 404 за {ElapsedMillis
 144                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 45                }
 5146                else if (isExpectedOneCDocumentPostingRejection)
 47                {
 348                    logger.LogInformation("1С OData отклонила проведение документа (ожидаемый бизнес-ответ, документ ост
 349                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 350                        (int)response.StatusCode, elapsed);
 51                }
 4852                else if (isExpectedMlAutoNotFound)
 53                {
 1054                    logger.LogInformation("ML-Auto не нашёл заказ или предложение: HTTP {Method} {Uri} вернул 404 за {El
 1055                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 56                }
 3857                else if (isExpectedOzonProductAttributesNotFound)
 58                {
 159                    logger.LogInformation("Товар Ozon не найден в кабинете: HTTP {Method} {Uri} вернул 404 за {ElapsedMi
 160                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 61                }
 3762                else if (isExpectedOzonPostingNotFound)
 63                {
 164                    logger.LogInformation("Отправление Ozon не найдено в кабинете: HTTP {Method} {Uri} вернул 404 за {El
 165                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 66                }
 3667                else if (isArmtekDailyQuotaExceeded)
 68                {
 169                    logger.LogInformation("Armtek исчерпал суточную квоту: HTTP {Method} {Uri} вернул {StatusCode} за {E
 170                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 171                        (int)response.StatusCode, elapsed);
 72                }
 3573                else if (isTransientOzonS3ReportError)
 74                {
 375                    logger.LogInformation("Временный ответ Ozon S3: HTTP {Method} {Uri} вернул {StatusCode} за {ElapsedM
 376                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 377                        (int)response.StatusCode, elapsed);
 78                }
 3279                else if (isTransientSupplierServiceError)
 80                {
 781                    logger.LogInformation("Временный сбой сервиса поставщика (HTTP {StatusCode}): {Method} {Uri} за {Ela
 782                        (int)response.StatusCode, request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPay
 783                        elapsed);
 84                }
 2585                else if (isTransientOzonChatError)
 86                {
 487                    logger.LogInformation("Временный сбой Ozon Chat API: HTTP {Method} {Uri} вернул {StatusCode} за {Ela
 488                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 489                        (int)response.StatusCode, elapsed);
 90                }
 2191                else if (isTransientOzonWebhookRateLimit)
 92                {
 93                    // OzonWebhookAvailabilityService повторяет этот безопасный запрос с паузами 1/2/4 с.
 94                    // Предыдущая неудачная попытка не является инцидентом; после исчерпания повторов
 95                    // сервис сам зафиксирует Warning с контекстом кабинета.
 196                    logger.LogInformation("Ozon ограничил частоту проверки webhook: HTTP {Method} {Uri} вернул 429 за {E
 197                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 98                }
 2099                else if (isExpectedBusinessRejection)
 100                {
 9101                    logger.LogInformation("Внешний сервис отклонил операцию (ожидаемый бизнес-ответ): HTTP {Method} {Uri
 9102                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 9103                        (int)response.StatusCode, elapsed);
 104                }
 11105                else if (isExpectedOzonMethodNotAllowed)
 106                {
 4107                    logger.LogInformation("Ozon API отклонил HTTP-метод (405 Method Not Allowed): {Method} {Uri} за {Ela
 4108                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 4109                        elapsed);
 110                }
 7111                else if (isOpenRouterAuthError)
 112                {
 0113                    logger.LogInformation("OpenRouter отклонил запрос (401 Unauthorized, не настроен или недействителен 
 0114                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 0115                        elapsed);
 116                }
 117                else
 118                {
 7119                    logger.LogWarning("Внешний HTTP {Method} {Uri} вернул {StatusCode} за {ElapsedMilliseconds} мс",
 7120                        request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 7121                        (int)response.StatusCode, elapsed);
 122                }
 123            }
 2124            else if (isExpectedHeadNotFound)
 125            {
 1126                using var scope = BeginScope(request, elapsed, (int)response.StatusCode);
 1127                logger.LogDebug("Внешний HTTP HEAD {Uri} вернул 404 (ресурс не найден) за {ElapsedMilliseconds} мс",
 1128                    redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), elapsed);
 129            }
 130
 54131            return response;
 132        }
 2133        catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested)
 134        {
 1135            using var scope = BeginScope(request, timer.ElapsedMilliseconds, 499);
 1136            logger.LogInformation("Внешний HTTP {Method} {Uri} отменен вызывающей стороной через {ElapsedMilliseconds} м
 1137                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 1138            throw;
 139        }
 140        // Внешний сервис не ответил за отведённое время. Это отказ третьей стороны, а не Studio2:
 141        // тот же поставщик, вернувший 504, логируется Warning'ом веткой выше. Исключение
 142        // пробрасывается дальше — если отказ важен для сценария, вызывающий код сообщит о нём
 143        // с бизнес-контекстом. Уровень Information: Warning и выше попадают в errors-*.log,
 144        // и наблюдатель заводит issue на каждый таймаут чужого API. В Seq запись остаётся,
 145        // код 504 в scope делает её отбираемой.
 1146        catch (OperationCanceledException) when (!cancellationToken.IsCancellationRequested)
 147        {
 1148            using var scope = BeginScope(request, timer.ElapsedMilliseconds, 504);
 1149            logger.LogInformation("Timeout внешнего HTTP {Method} {Uri} через {ElapsedMilliseconds} мс",
 1150                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 1151            throw;
 152        }
 153        // Сработал Circuit Breaker (Polly): контур разомкнут из-за серии сбоев внешнего сервиса.
 154        // Это защита от перегрузки третьей стороны, а не ошибка Studio2.
 155        // Исключение пробрасывается дальше, уровень Information и код 503 в scope
 156        // сохраняют запись в Seq без засорения errors-*.log (#2012, #2013).
 2157        catch (BrokenCircuitException)
 158        {
 2159            using var scope = BeginScope(request, timer.ElapsedMilliseconds, 503);
 2160            logger.LogInformation("Внешний HTTP {Method} {Uri} отклонён: Circuit Breaker разомкнут через {ElapsedMillise
 2161                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 2162            throw;
 163        }
 164        // Обрыв соединения внешним сервисом (TCP reset, сбой SSL/TLS рукопожатия, преждевременно закрытый ответ).
 165        // Это отказ сетевой инфраструктуры или третьей стороны, а не ошибка Studio2.
 166        // Исключение пробрасывается дальше, уровень Information и код 502 в scope
 167        // сохраняют запись в Seq без засорения errors-*.log (#1994).
 168        // DNS-ошибки и connection refused обрабатываются следующим блоком — только для сторонних хостов.
 13169        catch (HttpRequestException exception) when (OutboundTransportFailureClassifier.IsTransientTransportFailure(exce
 170        {
 4171            using var scope = BeginScope(request, timer.ElapsedMilliseconds, 502);
 4172            logger.LogInformation(exception, "Сетевой сбой внешнего HTTP {Method} {Uri} через {ElapsedMilliseconds} мс",
 4173                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 4174            throw;
 175        }
 176        // DNS, connection refused, сеть недостижима — у стороннего сервиса. Код 503 в scope
 177        // оставляет запись отбираемой в Seq; свои сервисы остаются Error (см. классификатор).
 9178        catch (HttpRequestException exception) when (OutboundTransportFailureClassifier.IsThirdPartyUnavailable(exceptio
 179        {
 3180            using var scope = BeginScope(request, timer.ElapsedMilliseconds, 503);
 3181            logger.LogInformation("Внешний сервис недоступен: HTTP {Method} {Uri} через {ElapsedMilliseconds} мс — {Reas
 3182                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 3183                exception.Message);
 3184            throw;
 185        }
 7186        catch (Exception exception)
 187        {
 7188            using var scope = BeginScope(request, timer.ElapsedMilliseconds, null);
 7189            logger.LogError(exception, "Ошибка внешнего HTTP {Method} {Uri} через {ElapsedMilliseconds} мс",
 7190                request.Method, redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters), timer.ElapsedMill
 7191            throw;
 192        }
 54193    }
 194
 195    private IDisposable? BeginScope(HttpRequestMessage request, long elapsed, int? statusCode) =>
 71196        logger.BeginScope(new Dictionary<string, object?>
 71197        {
 71198            ["EventKind"] = "OutboundHttp",
 71199            ["EventCategory"] = "external-api",
 71200            ["DependencyHost"] = request.RequestUri?.Host,
 71201            ["HttpMethod"] = request.Method.Method,
 71202            ["RequestUri"] = redactor.RedactUri(request.RequestUri, _options.MaxPayloadCharacters),
 71203            ["StatusCode"] = statusCode,
 71204            ["ElapsedMilliseconds"] = elapsed
 71205        });
 206
 207    private static bool IsExpectedMotexSearchNotFound(HttpRequestMessage request, HttpResponseMessage response) =>
 54208        request.Method == HttpMethod.Get &&
 54209        response.StatusCode == System.Net.HttpStatusCode.NotFound &&
 54210        string.Equals(request.RequestUri?.Host, "api.motexc.by", StringComparison.OrdinalIgnoreCase) &&
 54211        string.Equals(request.RequestUri?.AbsolutePath, "/api/v1/Articles/Search", StringComparison.OrdinalIgnoreCase);
 212
 213    private static bool IsExpectedMlAutoNotFound(HttpRequestMessage request, HttpResponseMessage response)
 214    {
 54215        if (request.Method != HttpMethod.Post || response.StatusCode != System.Net.HttpStatusCode.NotFound)
 40216            return false;
 217
 14218        var host = request.RequestUri?.Host;
 14219        if (host == null ||
 14220            (!string.Equals(host, "ml-auto.ru", StringComparison.OrdinalIgnoreCase) &&
 14221             !string.Equals(host, "www.ml-auto.ru", StringComparison.OrdinalIgnoreCase) &&
 14222             !string.Equals(host, "ml-auto.by", StringComparison.OrdinalIgnoreCase) &&
 14223             !string.Equals(host, "www.ml-auto.by", StringComparison.OrdinalIgnoreCase)))
 224        {
 3225            return false;
 226        }
 227
 11228        var path = request.RequestUri?.AbsolutePath;
 11229        if (path == null)
 0230            return false;
 231
 11232        var trimmed = path.TrimEnd('/');
 11233        return trimmed.EndsWith("/GetOrderByNumber", StringComparison.OrdinalIgnoreCase) ||
 11234               trimmed.EndsWith("/Search", StringComparison.OrdinalIgnoreCase);
 235    }
 236
 237    private static async Task<bool> IsArmtekDailyQuotaExceededAsync(HttpRequestMessage request, HttpResponseMessage resp
 238        CancellationToken ct)
 239    {
 54240        if (response.StatusCode != System.Net.HttpStatusCode.Unauthorized ||
 54241            !string.Equals(request.RequestUri?.Host, "ws.armtek.ru", StringComparison.OrdinalIgnoreCase))
 53242            return false;
 243
 1244        var body = await response.Content.ReadAsStringAsync(ct);
 1245        return ArmtekDailyQuotaGuard.IsExceeded(body);
 54246    }
 247
 248    private static bool IsTransientOzonS3ReportError(HttpRequestMessage request, HttpResponseMessage response)
 249    {
 54250        var uri = request.RequestUri;
 54251        if (uri == null)
 0252            return false;
 253
 54254        var host = uri.Host;
 54255        var isOzonHost = host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 54256                         host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 54257                         host.EndsWith(".ozonusercontent.com", StringComparison.OrdinalIgnoreCase) ||
 54258                         string.Equals(host, "ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 54259                         string.Equals(host, "ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 54260                         string.Equals(host, "ozonusercontent.com", StringComparison.OrdinalIgnoreCase);
 261
 54262        if (!isOzonHost || !uri.AbsolutePath.StartsWith("/s3/", StringComparison.OrdinalIgnoreCase))
 51263            return false;
 264
 3265        var code = (int)response.StatusCode;
 3266        return code == 403 || code == 404 || code >= 500;
 267    }
 268
 269    private static bool IsTransientSupplierServiceError(HttpRequestMessage request, HttpResponseMessage response)
 270    {
 54271        var code = (int)response.StatusCode;
 54272        if (code is not (500 or 502 or 503 or 504))
 38273            return false;
 274
 16275        var host = request.RequestUri?.Host;
 16276        if (string.IsNullOrWhiteSpace(host))
 0277            return false;
 278
 16279        return host.EndsWith("armtek.ru", StringComparison.OrdinalIgnoreCase) ||
 16280               host.EndsWith("armtek.by", StringComparison.OrdinalIgnoreCase) ||
 16281               host.EndsWith("forum-auto.ru", StringComparison.OrdinalIgnoreCase) ||
 16282               host.EndsWith("ml-auto.ru", StringComparison.OrdinalIgnoreCase) ||
 16283               host.EndsWith("ml-auto.by", StringComparison.OrdinalIgnoreCase) ||
 16284               host.EndsWith("motexc.by", StringComparison.OrdinalIgnoreCase) ||
 16285               host.EndsWith("profit-liga.ru", StringComparison.OrdinalIgnoreCase) ||
 16286               host.EndsWith("favorit-parts.ru", StringComparison.OrdinalIgnoreCase) ||
 16287               host.EndsWith("moskvorechie.ru", StringComparison.OrdinalIgnoreCase) ||
 16288               host.EndsWith("berg.ru", StringComparison.OrdinalIgnoreCase) ||
 16289               host.EndsWith("shate-m.by", StringComparison.OrdinalIgnoreCase) ||
 16290               host.EndsWith("japarts.ru", StringComparison.OrdinalIgnoreCase) ||
 16291               host.EndsWith("nikei.ru", StringComparison.OrdinalIgnoreCase) ||
 16292               host.EndsWith("avdmotors.ru", StringComparison.OrdinalIgnoreCase) ||
 16293               host.EndsWith("mikado-parts.ru", StringComparison.OrdinalIgnoreCase) ||
 16294               host.EndsWith("zzap.ru", StringComparison.OrdinalIgnoreCase) ||
 16295               host.EndsWith("zzap.pro", StringComparison.OrdinalIgnoreCase);
 296    }
 297
 298    private static bool IsOpenRouterAuthError(HttpRequestMessage request, HttpResponseMessage response)
 299    {
 54300        return response.StatusCode == System.Net.HttpStatusCode.Unauthorized &&
 54301               request.RequestUri?.Host.EndsWith("openrouter.ai", StringComparison.OrdinalIgnoreCase) == true;
 302    }
 303
 304    private static bool IsExpectedOzonProductAttributesNotFound(HttpRequestMessage request, HttpResponseMessage response
 305    {
 54306        if (response.StatusCode != System.Net.HttpStatusCode.NotFound || request.Method != HttpMethod.Post)
 40307            return false;
 308
 14309        var uri = request.RequestUri;
 14310        if (uri == null)
 0311            return false;
 312
 14313        return string.Equals(uri.Host, "api-seller.ozon.ru", StringComparison.OrdinalIgnoreCase) &&
 14314               string.Equals(uri.AbsolutePath, "/v4/product/info/attributes", StringComparison.OrdinalIgnoreCase);
 315    }
 316
 317    private static bool IsExpectedOzonPostingNotFound(HttpRequestMessage request, HttpResponseMessage response)
 318    {
 54319        if (response.StatusCode != System.Net.HttpStatusCode.NotFound || request.Method != HttpMethod.Post)
 40320            return false;
 321
 14322        var uri = request.RequestUri;
 14323        if (uri == null)
 0324            return false;
 325
 14326        var host = uri.Host;
 14327        var isOzonHost = host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 14328                         host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 14329                         string.Equals(host, "ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 14330                         string.Equals(host, "ozon.ru", StringComparison.OrdinalIgnoreCase);
 331
 14332        if (!isOzonHost)
 11333            return false;
 334
 3335        return string.Equals(uri.AbsolutePath, "/v3/posting/fbs/get", StringComparison.OrdinalIgnoreCase);
 336    }
 337
 338    private static bool IsTransientOzonChatError(HttpRequestMessage request, HttpResponseMessage response)
 339    {
 54340        var uri = request.RequestUri;
 54341        if (uri == null)
 0342            return false;
 343
 54344        var host = uri.Host;
 54345        var isOzonHost = host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 54346                         host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 54347                         string.Equals(host, "ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 54348                         string.Equals(host, "ozon.ru", StringComparison.OrdinalIgnoreCase);
 349
 54350        if (!isOzonHost || !uri.AbsolutePath.Contains("/chat/", StringComparison.OrdinalIgnoreCase))
 50351            return false;
 352
 4353        var code = (int)response.StatusCode;
 4354        return code is 500 or 502 or 503 or 504;
 355    }
 356
 357    private static bool IsTransientOzonWebhookRateLimit(HttpRequestMessage request, HttpResponseMessage response)
 358    {
 54359        if (request.Method != HttpMethod.Post || response.StatusCode != System.Net.HttpStatusCode.TooManyRequests)
 52360            return false;
 361
 2362        var uri = request.RequestUri;
 2363        return uri != null &&
 2364               string.Equals(uri.Host, "api-seller.ozon.ru", StringComparison.OrdinalIgnoreCase) &&
 2365               string.Equals(uri.AbsolutePath, "/v1/notification/list", StringComparison.OrdinalIgnoreCase);
 366    }
 367
 368    private static bool IsExpectedBusinessRejection(HttpRequestMessage request, HttpResponseMessage response)
 369    {
 54370        if (response.StatusCode != System.Net.HttpStatusCode.BadRequest)
 44371            return false;
 372
 10373        var uri = request.RequestUri;
 10374        if (uri == null)
 0375            return false;
 376
 10377        var host = uri.Host;
 10378        var isOzonHost = host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 10379                         host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 10380                         string.Equals(host, "ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 10381                         string.Equals(host, "ozon.ru", StringComparison.OrdinalIgnoreCase);
 382
 10383        if (isOzonHost)
 384        {
 7385            var path = uri.AbsolutePath;
 386            // Штатные отказы Ozon API при отмене, отгрузке или печати завершённых/отменённых отправлений (#2090, #1815,
 387            // create-or-get экземпляров (#1499) вызывается при проверке перед печатью; для отменённых/ушедших отправлен
 388            // Ozon отвечает 400, а OzonExemplarService.GetRequirementsAsync это штатно пропускает
 7389            if (path.Equals("/v6/fbs/posting/product/exemplar/create-or-get", StringComparison.OrdinalIgnoreCase) ||
 7390                path.Equals("/v2/posting/fbs/cancel", StringComparison.OrdinalIgnoreCase) ||
 7391                path.Equals("/v4/posting/fbs/ship", StringComparison.OrdinalIgnoreCase) ||
 7392                path.Equals("/v2/posting/fbs/package-label/create", StringComparison.OrdinalIgnoreCase) ||
 7393                path.Equals("/v3/posting/fbs/package-label/create", StringComparison.OrdinalIgnoreCase) ||
 7394                path.Equals("/v2/posting/fbs/package-label/get", StringComparison.OrdinalIgnoreCase) ||
 7395                path.Equals("/v2/posting/fbs/package-label", StringComparison.OrdinalIgnoreCase))
 396            {
 7397                return true;
 398            }
 399        }
 400
 3401        var isShateMHost = host.EndsWith("shate-m.by", StringComparison.OrdinalIgnoreCase) ||
 3402                           host.EndsWith("shate-m.ru", StringComparison.OrdinalIgnoreCase);
 403
 3404        if (isShateMHost)
 405        {
 2406            var path = uri.AbsolutePath;
 407            // Штатные отказы поставщика при заказе позиций (нет остатка, изменилась цена) (#2084)
 2408            if (path.StartsWith("/api/v1/orders/byPriceItems", StringComparison.OrdinalIgnoreCase) ||
 2409                path.StartsWith("/api/v1/orders/byCartItems", StringComparison.OrdinalIgnoreCase))
 410            {
 2411                return true;
 412            }
 413        }
 414
 1415        return false;
 416    }
 417
 418    private static bool IsExpectedOzonMethodNotAllowed(HttpRequestMessage request, HttpResponseMessage response)
 419    {
 54420        if (response.StatusCode != System.Net.HttpStatusCode.MethodNotAllowed)
 49421            return false;
 422
 5423        var uri = request.RequestUri;
 5424        if (uri == null)
 0425            return false;
 426
 5427        var host = uri.Host;
 5428        return host.EndsWith(".ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 5429               host.EndsWith(".ozon.ru", StringComparison.OrdinalIgnoreCase) ||
 5430               string.Equals(host, "ozone.ru", StringComparison.OrdinalIgnoreCase) ||
 5431               string.Equals(host, "ozon.ru", StringComparison.OrdinalIgnoreCase);
 432    }
 433
 434    private static bool IsExpectedOneCDocumentPostingRejection(HttpRequestMessage request, HttpResponseMessage response)
 435    {
 436        // Регрессия #654, #1995: 1С OData возвращает HTTP 500 (Internal Server Error) при штатном отказе проведения
 437        // документа (недостаточно остатка, не списано по партиям, блокировки и т.д.).
 438        // Это ожидаемый бизнес-исход: документ остаётся черновиком, а менеджеры 1С (OneCTransferManager,
 439        // OneCExpenseManager, OneCReceiptManager) штатно перехватывают OneCDocumentPostingException и логируют его как 
 54440        if (response.StatusCode != System.Net.HttpStatusCode.InternalServerError &&
 54441            response.StatusCode != System.Net.HttpStatusCode.BadRequest)
 442        {
 39443            return false;
 444        }
 445
 15446        if (request.Method != HttpMethod.Post)
 1447            return false;
 448
 14449        var uri = request.RequestUri;
 14450        if (uri == null)
 0451            return false;
 452
 14453        var path = uri.AbsolutePath;
 14454        var isOneCDocumentPost = path.Contains("/odata/standard.odata/Document_", StringComparison.OrdinalIgnoreCase)
 14455                                 && path.EndsWith("/Post", StringComparison.OrdinalIgnoreCase);
 456
 14457        return isOneCDocumentPost;
 458    }
 459}

Methods/Properties

.ctor(Microsoft.Extensions.Logging.ILogger`1<OzonOrdersWeb.Observability.OutboundRequestLoggingHandler>,OzonOrdersWeb.Observability.SensitiveDataRedactor,Microsoft.Extensions.Options.IOptions`1<OzonOrdersWeb.Observability.ObservabilityOptions>)
SendAsync()
BeginScope(System.Net.Http.HttpRequestMessage,System.Int64,System.Nullable`1<System.Int32>)
IsExpectedMotexSearchNotFound(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedMlAutoNotFound(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsArmtekDailyQuotaExceededAsync()
IsTransientOzonS3ReportError(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsTransientSupplierServiceError(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsOpenRouterAuthError(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedOzonProductAttributesNotFound(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedOzonPostingNotFound(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsTransientOzonChatError(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsTransientOzonWebhookRateLimit(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedBusinessRejection(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedOzonMethodNotAllowed(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)
IsExpectedOneCDocumentPostingRejection(System.Net.Http.HttpRequestMessage,System.Net.Http.HttpResponseMessage)