diff --git a/CHANGELOG.md b/CHANGELOG.md index 19e3c1d41a..9c4c21a22e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,11 @@ Increment the: ## [Unreleased] +* [OTLP/HTTP] Honor `Retry-After` response header when retrying exports, + supporting both delay-seconds and HTTP-date formats per RFC 7231 ยง7.1.3. + [#4172](https://github.com/open-telemetry/opentelemetry-cpp/issues/4172) +* [CODE HEALTH] Move api/test and sdk/test classes into anonymous namespace + [#4217](https://github.com/open-telemetry/opentelemetry-cpp/pull/4217) * docs: update supported development platforms [#4260](https://github.com/open-telemetry/opentelemetry-cpp/pull/4260) diff --git a/api/include/opentelemetry/common/timestamp.h b/api/include/opentelemetry/common/timestamp.h index f7c79b8b57..22c26c1f52 100644 --- a/api/include/opentelemetry/common/timestamp.h +++ b/api/include/opentelemetry/common/timestamp.h @@ -3,9 +3,18 @@ #pragma once +#include #include #include +#include +#include +#include +#include +#include +#include +#include "opentelemetry/common/string_util.h" +#include "opentelemetry/nostd/string_view.h" #include "opentelemetry/version.h" OPENTELEMETRY_BEGIN_NAMESPACE @@ -202,5 +211,105 @@ class DurationUtil } }; +/** + * @brief Parsing helpers for RFC 7231 value formats (HTTP-date, section + * 7.1.1.1, and delay-seconds, section 7.1.3) that appear in headers such + * as Retry-After, Date, Last-Modified, and Expires. + */ +class HttpUtil +{ +public: + static bool ParseDelaySeconds(nostd::string_view value, std::chrono::seconds &delay) noexcept + { + value = StringUtil::Trim(value); + if (value.empty()) + { + return false; + } + + std::chrono::seconds::rep result = 0; + for (const char c : value) + { + if (!std::isdigit(static_cast(static_cast(c)))) + { + return false; + } + auto digit = c - '0'; + if (result > ((std::numeric_limits::max)() - digit) / 10) + { + return false; + } + result = result * 10 + digit; + } + + delay = std::chrono::seconds(result); + return true; + } + + static bool ParseHttpDate(nostd::string_view value, std::chrono::system_clock::time_point &date) + { + value = StringUtil::Trim(value); + std::string str(value.data(), value.size()); + std::tm tm = {}; + std::istringstream ss(str); + ss.imbue(std::locale::classic()); + + ss >> std::get_time(&tm, "%a, %d %b %Y %H:%M:%S"); + if (!ss.fail()) + { + date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm)); + return true; + } + + ss.clear(); + ss.str(str); + tm = {}; + ss >> std::get_time(&tm, "%A, %d-%b-%y %H:%M:%S"); + if (!ss.fail()) + { + std::time_t now_t = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + int current_year = 1970 + static_cast(now_t / 31556952); + int full_year = 1900 + tm.tm_year; + int centuries_ahead = (full_year - current_year + 50) / 100; + if (centuries_ahead > 0) + { + full_year -= 100 * centuries_ahead; + } + tm.tm_year = full_year - 1900; + date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm)); + return true; + } + + ss.clear(); + ss.str(str); + tm = {}; + ss >> std::get_time(&tm, "%a %b %d %H:%M:%S %Y"); + if (!ss.fail()) + { + date = std::chrono::system_clock::from_time_t(PortableTimegm(&tm)); + return true; + } + + return false; + } + +private: + static std::time_t PortableTimegm(std::tm *tm) noexcept + { + int year = tm->tm_year + 1900; + int month = tm->tm_mon + 1; + if (month <= 2) + { + year -= 1; + month += 12; + } + int day = tm->tm_mday; + int days = 365 * year + year / 4 - year / 100 + year / 400 + (153 * (month - 3) + 2) / 5 + day - + 719469; + return static_cast(days) * 86400 + tm->tm_hour * 3600 + tm->tm_min * 60 + + tm->tm_sec; + } +}; + } // namespace common OPENTELEMETRY_END_NAMESPACE diff --git a/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h b/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h index 541b74a85c..6ac23a3393 100644 --- a/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h +++ b/ext/include/opentelemetry/ext/http/client/curl/http_operation_curl.h @@ -348,6 +348,7 @@ class HttpOperation const RetryPolicy retry_policy_; decltype(RetryPolicy::max_attempts) retry_attempts_; std::chrono::system_clock::time_point last_attempt_time_; + std::chrono::system_clock::time_point retry_after_time_point_{}; // Processed response headers and body // See CURLINFO_RESPONSE_CODE, type is long diff --git a/ext/src/http/client/curl/http_operation_curl.cc b/ext/src/http/client/curl/http_operation_curl.cc index abd8b3ea77..f9f365e874 100644 --- a/ext/src/http/client/curl/http_operation_curl.cc +++ b/ext/src/http/client/curl/http_operation_curl.cc @@ -8,6 +8,7 @@ # include #endif // ENABLE_OTLP_RETRY_PREVIEW +#include #include #include #include @@ -23,6 +24,18 @@ #include #include +#ifdef _MSC_VER +# include +# define strncasecmp _strnicmp +#else +# include +#endif + +#ifdef OTEL_CURL_DEBUG +# include +#endif + +#include "opentelemetry/common/timestamp.h" #include "opentelemetry/ext/http/client/curl/http_client_curl.h" #include "opentelemetry/ext/http/client/curl/http_operation_curl.h" #include "opentelemetry/ext/http/client/http_client.h" @@ -35,6 +48,47 @@ # define CURL_VERSION_BITS(x, y, z) ((x) << 16 | (y) << 8 | (z)) #endif +namespace +{ + +bool FindRetryAfterValue(const std::vector &raw_headers, + opentelemetry::nostd::string_view &value) +{ + if (raw_headers.empty()) + { + return false; + } + + static const char kRetryAfterHeader[] = "retry-after:"; + static const size_t kRetryAfterLen = sizeof(kRetryAfterHeader) - 1; + + const char *data = reinterpret_cast(raw_headers.data()); + const char *end = data + raw_headers.size(); + const char *line = data; + + while (line < end) + { + const char *line_end = line; + while (line_end < end && *line_end != '\n') + { + ++line_end; + } + + size_t line_len = static_cast(line_end - line); + if (line_len > kRetryAfterLen && strncasecmp(line, kRetryAfterHeader, kRetryAfterLen) == 0) + { + value = opentelemetry::nostd::string_view(line + kRetryAfterLen, line_len - kRetryAfterLen); + return true; + } + + line = (line_end < end) ? line_end + 1 : end; + } + + return false; +} + +} // namespace + OPENTELEMETRY_BEGIN_NAMESPACE namespace ext { @@ -560,6 +614,11 @@ bool HttpOperation::IsRetryable() std::chrono::system_clock::time_point HttpOperation::NextRetryTime() { + if (retry_after_time_point_ != std::chrono::system_clock::time_point{}) + { + return retry_after_time_point_; + } + static std::random_device rd; static std::mt19937 gen(rd()); static std::uniform_real_distribution dis(0.8f, 1.2f); @@ -1463,8 +1522,9 @@ void HttpOperation::Abort() void HttpOperation::PerformCurlMessage(CURLcode code) { ++retry_attempts_; - last_attempt_time_ = std::chrono::system_clock::now(); - last_curl_result_ = code; + last_attempt_time_ = std::chrono::system_clock::now(); + last_curl_result_ = code; + retry_after_time_point_ = std::chrono::system_clock::time_point{}; if (code != CURLE_OK) { @@ -1513,6 +1573,27 @@ void HttpOperation::PerformCurlMessage(CURLcode code) if (IsRetryable()) { + + nostd::string_view retry_after; + if (FindRetryAfterValue(response_headers_, retry_after)) + { + std::chrono::seconds delay; + + if (opentelemetry::common::HttpUtil::ParseDelaySeconds(retry_after, delay)) + { + retry_after_time_point_ = std::chrono::system_clock::now() + delay; + } + else + { + std::chrono::system_clock::time_point date; + if (opentelemetry::common::HttpUtil::ParseHttpDate(retry_after, date)) + { + auto now = std::chrono::system_clock::now(); + retry_after_time_point_ = (date > now) ? date : now; + } + } + } + // Clear any response data received in previous attempt ReleaseResponse(); // Rewind request data so that read callback can re-transfer the payload