Skip to content

Commit 5ffd67f

Browse files
author
miglitskii
committed
feat userver: http client/server spans according to OTel semconv
commit_hash:1b0726dd0443095faa71fb0ae7c1e6a408a979f9
1 parent e5ca690 commit 5ffd67f

File tree

27 files changed

+280
-112
lines changed

27 files changed

+280
-112
lines changed

chaotic-openapi/chaotic_openapi/back/cpp_client/templates/client_impl.cpp.jinja

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace {{ namespace }} {
88

99
ClientImpl::ClientImpl(const USERVER_NAMESPACE::chaotic::openapi::client::Config& config,
10-
USERVER_NAMESPACE::clients::http::Client& http_client)
10+
USERVER_NAMESPACE::clients::http::Client& http_client)
1111
: config_(config), http_client_(http_client)
1212
{
1313
}
@@ -21,15 +21,16 @@ ClientImpl::ClientImpl(const USERVER_NAMESPACE::chaotic::openapi::client::Config
2121
const USERVER_NAMESPACE::chaotic::openapi::client::CommandControl& command_control
2222
) {
2323
auto r = http_client_.CreateRequest();
24+
r.SetUrlTemplate("{{ op.path }}");
2425
if (plugins_) {
2526
r.SetPluginsList(*plugins_);
2627
}
2728
ApplyConfig(r, command_control, config_);
28-
29+
2930
{% if not op.empty_request() -%}
3031
SerializeRequest(request, config_.base_url, r);
3132
{% endif %}
32-
33+
3334
middleware_manager_.ProcessRequest(r);
3435

3536
std::shared_ptr<USERVER_NAMESPACE::clients::http::Response> response;
@@ -39,7 +40,7 @@ ClientImpl::ClientImpl(const USERVER_NAMESPACE::chaotic::openapi::client::Config
3940
} catch (const USERVER_NAMESPACE::clients::http::TimeoutException& e) {
4041
throw {{ op.cpp_namespace() }}::TimeoutException();
4142
}
42-
43+
4344
return {{ op.cpp_namespace() }}::ParseResponse(*response);
4445
}
4546
{% endif %}

chaotic-openapi/integration_tests/src/requests_test.cpp

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ UTEST_F(RequestsQueryLogMode, HideOperation) {
115115

116116
EXPECT_EQ(response->status_code(), 200);
117117

118-
auto text = GetLogCapture().GetAll().back().GetTag("http.url");
118+
auto text = GetLogCapture().GetAll().back().GetTag("url.full");
119119
EXPECT_TRUE(utils::text::EndsWith(text, "test1/query-log-mode?password=***&secret=***"));
120120
}
121121

@@ -132,8 +132,8 @@ UTEST_F(RequestsQueryLogMode, HideParameter) {
132132

133133
EXPECT_EQ(response->status_code(), 200);
134134

135-
auto text = GetLogCapture().GetAll().back().GetTag("http.url");
136-
EXPECT_TRUE(utils::text::EndsWith(text, "test1/query-log-mode/parameter?password=***&secret=bar")) << text;
135+
auto text = GetLogCapture().GetAll().back().GetTag("url.full");
136+
EXPECT_TRUE(utils::text::EndsWith(text, "test1/query-log-mode/parameter?password=***&secret=bar"));
137137
}
138138

139139
} // namespace

core/functional_tests/basic_chaos/tests-deadline/test_client.py

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -96,11 +96,11 @@ async def test_timeout_expired(
9696
# Client metrics are counted per attempt.
9797
assert client_metrics.value_at('errors', {'http_error': 'timeout', **VERSION}) == attempts
9898

99-
logs = capture.select(stopwatch_name='external/localhost')
99+
logs = capture.select(stopwatch_name='GET localhost')
100100
assert len(logs) == 1
101101
assert logs[0]['error'] == '1'
102-
assert logs[0]['attempts'] == str(attempts)
103-
assert logs[0]['max_attempts'] == str(attempts)
102+
assert logs[0]['http.request.resend_count'] == str(attempts)
103+
assert logs[0]['http.request.max_resend_count'] == str(attempts)
104104
assert logs[0].get('cancelled_by_deadline', '0') == '0'
105105
assert logs[0]['error_msg'] == 'Timeout was reached'
106106
assert logs[0]['timeout_ms'] == str(timeout)
@@ -163,10 +163,10 @@ async def test_deadline_expired(
163163
# Client metrics are counted per attempt.
164164
assert timed_out == attempts
165165

166-
logs = capture.select(stopwatch_name='external/localhost')
166+
logs = capture.select(stopwatch_name='GET localhost')
167167
assert len(logs) == 1
168168
assert logs[0]['error'] == '1'
169-
assert logs[0]['max_attempts'] == str(attempts)
169+
assert logs[0]['http.request.max_resend_count'] == str(attempts)
170170
assert logs[0]['cancelled_by_deadline'] == '1'
171171
assert logs[0]['error_msg'] == 'Timeout was reached'
172172
assert logs[0]['timeout_ms'] == str(timeout)
@@ -222,7 +222,7 @@ async def test_fake_deadline_expired_with_exception(
222222
)
223223

224224
assert deadline_warning['body'] == 'Deadline expired'
225-
assert deadline_warning['meta_code'] == '504'
225+
assert deadline_warning['http.response.status_code'] == '504'
226226

227227

228228
async def test_fake_deadline_expired(
@@ -247,11 +247,11 @@ async def test_fake_deadline_expired(
247247
assert client_metrics.value_at('errors', {'http_error': 'ok', **VERSION}) == 0
248248
assert client_metrics.value_at('errors', {'http_error': 'timeout', **VERSION}) == 1
249249

250-
logs = capture.select(stopwatch_name='external/localhost')
250+
logs = capture.select(stopwatch_name='GET localhost')
251251
assert len(logs) == 1
252252
assert logs[0]['error'] == '1'
253-
assert logs[0]['attempts'] == '1'
254-
assert logs[0]['max_attempts'] == '3'
253+
assert logs[0]['http.request.resend_count'] == '1'
254+
assert logs[0]['http.request.max_resend_count'] == '3'
255255
assert logs[0]['cancelled_by_deadline'] == '1'
256256
assert logs[0]['error_msg'] == 'Timeout was reached'
257257
assert logs[0]['timeout_ms'] == '500'

core/functional_tests/tracing/tests/conftest.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -72,11 +72,11 @@ async def _check_the_files(check_trace_id: str):
7272
f'\ttrace_id={trace_id}',
7373
'service_name=http-tracing-test',
7474
'duration=',
75-
'operation_name=external',
75+
'operation_name=GET localhost',
7676
'tags=[{"',
7777
'test-service/echo-no-body"',
78-
'"key":"http.url"',
79-
'"key":"http.status_code"',
78+
'"key":"url.full"',
79+
'"key":"http.response.status_code"',
8080
'"value":"200"',
8181
'}]',
8282
}

core/include/userver/clients/http/request.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,6 +259,11 @@ class Request final {
259259
Request& SetLoggedUrl(std::string url) &;
260260
Request SetLoggedUrl(std::string url) &&;
261261

262+
/// Set URL template (low-cardinality) for tracing, i.e. `/v1/user/{user_id}`
263+
/// @see https://opentelemetry.io/docs/specs/semconv/registry/attributes/url/
264+
Request& SetUrlTemplate(std::string url_template) &;
265+
Request SetUrlTemplate(std::string url_template) &&;
266+
262267
/// Set destination name in metric "httpclient.destinations.<name>".
263268
/// If not set, defaults to HTTP path. Should be called for all requests
264269
/// with parameters in HTTP path.

core/include/userver/tracing/tags.hpp

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,25 @@ extern const std::string kSpanKindServer;
2121
extern const std::string kSpanKindClient;
2222
extern const std::string kSpanKindInternal;
2323
extern const std::string kHttpUrl;
24+
extern const std::string kUrlFull;
25+
extern const std::string kHttpUrlTemplate;
2426
extern const std::string kHttpMetaType;
2527
extern const std::string kHttpMethod;
28+
extern const std::string kHttpRequestMethod;
29+
extern const std::string kHttpRoute;
2630
extern const std::string kHttpStatusCode;
31+
extern const std::string kHttpResponseStatusCode;
2732
extern const std::string kAttempts;
2833
extern const std::string kMaxAttempts;
2934
extern const std::string kTimeoutMs;
3035
extern const std::string kErrorFlag;
3136
extern const std::string kErrorMessage;
3237

38+
extern const std::string kRpcSystem;
39+
extern const std::string kRpcService;
40+
extern const std::string kRpcMethod;
41+
extern const std::string kGrpcCode;
42+
3343
extern const std::string kDatabaseType;
3444
extern const std::string kDatabaseMongoType;
3545
extern const std::string kDatabasePostgresType;
@@ -42,6 +52,7 @@ extern const std::string kDatabaseStatementName;
4252
extern const std::string kDatabaseStatementDescription;
4353

4454
extern const std::string kPeerAddress;
55+
extern const std::string kServerAddress;
4556

4657
} // namespace tracing
4758

core/src/clients/http/request.cpp

Lines changed: 11 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,6 @@ namespace clients::http {
3434

3535
namespace {
3636

37-
constexpr std::string_view kHeaderExpect = "Expect";
38-
3937
static constexpr utils::TrivialBiMap kHttpMethodMap([](auto selector) {
4038
return selector()
4139
.Case(HttpMethod::kGet, "GET")
@@ -47,8 +45,6 @@ static constexpr utils::TrivialBiMap kHttpMethodMap([](auto selector) {
4745
.Case(HttpMethod::kOptions, "OPTIONS");
4846
});
4947

50-
std::string ToString(HttpMethod method) { return std::string{ToStringView(method)}; }
51-
5248
curl::easy::http_version_t ToNative(HttpVersion version) {
5349
switch (version) {
5450
case HttpVersion::kDefault:
@@ -340,8 +336,7 @@ Request& Request::connect_to(const ConnectTo& connect_to) & {
340336
Request Request::connect_to(const ConnectTo& connect_to) && { return std::move(this->connect_to(connect_to)); }
341337

342338
Request& Request::data(std::string data) & {
343-
if (!data.empty()) pimpl_->easy().add_header(kHeaderExpect, "", curl::easy::EmptyHeaderAction::kDoNotSend);
344-
pimpl_->easy().set_post_fields(std::move(data));
339+
pimpl_->data(data);
345340
return *this;
346341
}
347342
Request Request::data(std::string data) && { return std::move(this->data(std::move(data))); }
@@ -427,28 +422,7 @@ Request Request::cookies(const std::unordered_map<std::string, std::string>& coo
427422
}
428423

429424
Request& Request::method(HttpMethod method) & {
430-
switch (method) {
431-
case HttpMethod::kDelete:
432-
case HttpMethod::kOptions:
433-
pimpl_->easy().set_custom_request(ToString(method));
434-
break;
435-
case HttpMethod::kGet:
436-
pimpl_->easy().set_http_get(true);
437-
pimpl_->easy().set_custom_request(nullptr);
438-
break;
439-
case HttpMethod::kHead:
440-
pimpl_->easy().set_no_body(true);
441-
pimpl_->easy().set_custom_request(nullptr);
442-
break;
443-
// NOTE: set_post makes libcURL to read from stdin if no data is set
444-
case HttpMethod::kPost:
445-
case HttpMethod::kPut:
446-
case HttpMethod::kPatch:
447-
pimpl_->easy().set_custom_request(ToString(method));
448-
// ensure a body as we should send Content-Length for this method
449-
if (!pimpl_->easy().has_post_data()) data({});
450-
break;
451-
};
425+
pimpl_->SetMethod(method);
452426
return *this;
453427
}
454428

@@ -536,6 +510,15 @@ Request& Request::SetLoggedUrl(std::string url) & {
536510
}
537511
Request Request::SetLoggedUrl(std::string url) && { return std::move(this->SetLoggedUrl(std::move(url))); }
538512

513+
Request& Request::SetUrlTemplate(std::string url_template) & {
514+
pimpl_->SetUrlTemplate(std::move(url_template));
515+
return *this;
516+
}
517+
518+
Request Request::SetUrlTemplate(std::string url_template) && {
519+
return std::move(this->SetUrlTemplate(std::move(url_template)));
520+
}
521+
539522
Request& Request::SetDestinationMetricName(const std::string& destination) & {
540523
pimpl_->SetDestinationMetricName(destination);
541524
return *this;

core/src/clients/http/request_state.cpp

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
#include <userver/clients/http/connect_to.hpp>
2020
#include <userver/http/url.hpp>
2121
#include <userver/server/request/task_inherited_data.hpp>
22+
#include <userver/utils/algo.hpp>
2223
#include <userver/utils/assert.hpp>
2324
#include <userver/utils/async.hpp>
2425
#include <userver/utils/encoding/hex.hpp>
@@ -53,8 +54,6 @@ constexpr Status kLeastHttpCodeForDeadlineExpired{400};
5354

5455
constexpr Status kFakeHttpErrorCode{599};
5556

56-
const std::string kTracingClientName = "external/";
57-
5857
constexpr utils::TrivialBiMap kTestsuiteActions = [](auto selector) {
5958
return selector()
6059
.Case("timeout", curl::errc::EasyErrorCode::kOperationTimedout)
@@ -100,6 +99,8 @@ bool IsSetCookie(std::string_view key) {
10099
// Not a strict check, but OK for non-header line check
101100
bool IsHttpStatusLineStart(const char* ptr, size_t size) { return (size > 5 && memcmp(ptr, "HTTP/", 5) == 0); }
102101

102+
std::string ToString(HttpMethod method) { return std::string{ToStringView(method)}; }
103+
103104
char* RfindNotSpace(char* ptr, size_t size) {
104105
for (char* p = ptr + size - 1; p >= ptr; --p) {
105106
const char c = *p;
@@ -484,7 +485,7 @@ void RequestState::OnCompleted(std::shared_ptr<RequestState> holder, std::error_
484485

485486
span.AddTag(tracing::kErrorFlag, true);
486487
span.AddTag(tracing::kErrorMessage, err.message());
487-
span.AddTag(tracing::kHttpStatusCode, kFakeHttpErrorCode);
488+
span.AddTag(tracing::kHttpResponseStatusCode, kFakeHttpErrorCode);
488489

489490
if (holder->errorbuffer_.front()) {
490491
LOG_DEBUG() << "cURL error details: " << holder->errorbuffer_.data();
@@ -506,7 +507,7 @@ void RequestState::OnCompleted(std::shared_ptr<RequestState> holder, std::error_
506507
}};
507508
std::visit(visitor, holder->data_);
508509
} else {
509-
span.AddTag(tracing::kHttpStatusCode, status_code);
510+
span.AddTag(tracing::kHttpResponseStatusCode, status_code);
510511
holder->response()->SetStatusCode(status_code);
511512
holder->response()->SetStats(easy.get_local_stats());
512513

@@ -652,6 +653,39 @@ void RequestState::SetPluginsList(const std::vector<utils::NotNull<Plugin*>>& pl
652653

653654
void RequestState::SetLoggedUrl(std::string url) { log_url_ = std::move(url); }
654655

656+
void RequestState::SetUrlTemplate(std::string url_template) { url_template_ = std::move(url_template); }
657+
658+
void RequestState::SetMethod(HttpMethod method) {
659+
switch (method) {
660+
case HttpMethod::kDelete:
661+
case HttpMethod::kOptions:
662+
easy().set_custom_request(ToString(method));
663+
break;
664+
case HttpMethod::kGet:
665+
easy().set_http_get(true);
666+
easy().set_custom_request(nullptr);
667+
break;
668+
case HttpMethod::kHead:
669+
easy().set_no_body(true);
670+
easy().set_custom_request(nullptr);
671+
break;
672+
// NOTE: set_post makes libcURL to read from stdin if no data is set
673+
case HttpMethod::kPost:
674+
case HttpMethod::kPut:
675+
case HttpMethod::kPatch:
676+
easy().set_custom_request(ToString(method));
677+
// ensure a body as we should send Content-Length for this method
678+
if (!easy().has_post_data()) data({});
679+
break;
680+
};
681+
method_ = method;
682+
}
683+
684+
void RequestState::data(std::string data) {
685+
if (!data.empty()) easy().add_header(kHeaderExpect, "", curl::easy::EmptyHeaderAction::kDoNotSend);
686+
easy().set_post_fields(std::move(data));
687+
}
688+
655689
const std::string& RequestState::GetLoggedOriginalUrl() const noexcept {
656690
// We may want to use original_url if effective_url is not available yet.
657691
return log_url_ ? *log_url_ : easy().get_original_url();
@@ -992,16 +1026,30 @@ void RequestState::ApplyTestsuiteConfig() {
9921026
void RequestState::StartNewSpan(utils::impl::SourceLocation location) {
9931027
UINVARIANT(!span_storage_, "Attempt to reuse request while the previous one has not finished");
9941028

995-
span_storage_.emplace(
996-
kTracingClientName + std::string{USERVER_NAMESPACE::http::ExtractHostname(easy().get_original_url())}, location
997-
);
1029+
std::string span_name;
1030+
if (url_template_.has_value()) {
1031+
span_name = utils::StrCat(ToStringView(method_), " ", url_template_.value());
1032+
} else {
1033+
span_name = utils::StrCat(
1034+
ToStringView(method_), " ", USERVER_NAMESPACE::http::ExtractHostname(easy().get_original_url())
1035+
);
1036+
}
1037+
span_storage_.emplace(std::move(span_name), location);
1038+
9981039
auto& span = span_storage_->Get();
9991040

10001041
auto request_editable_instance = GetEditableRequestInstance();
10011042

10021043
tracing_manager_->FillRequestWithTracingContext(span, request_editable_instance);
10031044
plugin_pipeline_.HookCreateSpan(*this, span);
1004-
span.AddTag(tracing::kHttpUrl, GetLoggedOriginalUrl());
1045+
span.AddTag(tracing::kUrlFull, GetLoggedOriginalUrl());
1046+
span.AddTag(
1047+
tracing::kServerAddress, std::string{USERVER_NAMESPACE::http::ExtractHostname(easy().get_original_url())}
1048+
);
1049+
span.AddTag(tracing::kHttpRequestMethod, std::string{ToStringView(method_)});
1050+
if (url_template_.has_value()) {
1051+
span.AddTag(tracing::kHttpUrlTemplate, url_template_.value());
1052+
}
10051053
span.AddTag(tracing::kMaxAttempts, retry_.retries);
10061054

10071055
// Span is local to a Request, it is not related to current coroutine

core/src/clients/http/request_state.hpp

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
#include <userver/clients/http/error.hpp>
1313
#include <userver/clients/http/form.hpp>
1414
#include <userver/clients/http/plugin.hpp>
15+
#include <userver/clients/http/request.hpp>
1516
#include <userver/clients/http/response_future.hpp>
1617
#include <userver/concurrent/queue.hpp>
1718
#include <userver/crypto/certificate.hpp>
@@ -37,6 +38,8 @@ USERVER_NAMESPACE_BEGIN
3738

3839
namespace clients::http {
3940

41+
constexpr std::string_view kHeaderExpect = "Expect";
42+
4043
class StreamedResponse;
4144
class ConnectTo;
4245

@@ -136,6 +139,9 @@ class RequestState : public std::enable_shared_from_this<RequestState> {
136139

137140
void SetPluginsList(const std::vector<utils::NotNull<Plugin*>>& plugins);
138141
void SetLoggedUrl(std::string url);
142+
void SetUrlTemplate(std::string url_template);
143+
void SetMethod(clients::http::HttpMethod method);
144+
void data(std::string data);
139145
void SetEasyTimeout(std::chrono::milliseconds timeout);
140146

141147
void SetTracingManager(const tracing::TracingManagerBase&);
@@ -235,6 +241,9 @@ class RequestState : public std::enable_shared_from_this<RequestState> {
235241
std::optional<tracing::InPlaceSpan> span_storage_;
236242
std::optional<std::string> log_url_;
237243

244+
std::optional<std::string> url_template_;
245+
HttpMethod method_{HttpMethod::kGet};
246+
238247
std::atomic<bool> is_cancelled_{false};
239248
std::array<char, CURL_ERROR_SIZE> errorbuffer_{};
240249

0 commit comments

Comments
 (0)