diff --git a/.gitignore b/.gitignore index 6857461..48cd76d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ /*build* /deps /.idea +/.vscode .pkg.mutex +.vscode diff --git a/include/net/get_otel_tracer.h b/include/net/get_otel_tracer.h new file mode 100644 index 0000000..f89803d --- /dev/null +++ b/include/net/get_otel_tracer.h @@ -0,0 +1,15 @@ +#pragma once + +#include "opentelemetry/trace/provider.h" +#include "opentelemetry/trace/scope.h" +#include "opentelemetry/trace/span.h" +#include "opentelemetry/trace/tracer.h" + +namespace net { + +inline opentelemetry::nostd::shared_ptr +get_otel_tracer() { + return opentelemetry::trace::Provider::GetTracerProvider()->GetTracer("net"); +} + +} // namespace net diff --git a/include/net/otel_text_map_carrier.h b/include/net/otel_text_map_carrier.h new file mode 100644 index 0000000..079544d --- /dev/null +++ b/include/net/otel_text_map_carrier.h @@ -0,0 +1,29 @@ +#pragma once + +#include + +#include "opentelemetry/context/propagation/text_map_propagator.h" + +namespace net { + +template +struct http_text_map_carrier + : public opentelemetry::context::propagation::TextMapCarrier { + explicit http_text_map_carrier(T& header) : header_{header} {} + + virtual opentelemetry::nostd::string_view Get( + opentelemetry::nostd::string_view key) const noexcept override { + return header_[key]; + } + + virtual void Set(opentelemetry::nostd::string_view key, + opentelemetry::nostd::string_view value) noexcept override { + if constexpr (!std::is_const_v) { + header_.set(key, value); + } + } + + T& header_; +}; + +} // namespace net \ No newline at end of file diff --git a/include/net/web_server/query_router.h b/include/net/web_server/query_router.h index b593d18..eb27784 100644 --- a/include/net/web_server/query_router.h +++ b/include/net/web_server/query_router.h @@ -6,6 +6,7 @@ #include #include #include +#include #include #include "utl/helpers/algorithm.h" @@ -17,11 +18,19 @@ #include "boost/json.hpp" #include "boost/url.hpp" +#include "opentelemetry/context/propagation/global_propagator.h" +#include "opentelemetry/context/runtime_context.h" +#include "opentelemetry/sdk/resource/semantic_conventions.h" +#include "opentelemetry/trace/context.h" +#include "opentelemetry/trace/span_metadata.h" + #include "openapi/bad_request_exception.h" #include "net/bad_request_exception.h" #include "net/base64.h" +#include "net/get_otel_tracer.h" #include "net/not_found_exception.h" +#include "net/otel_text_map_carrier.h" #include "net/too_many_exception.h" #include "net/web_server/content_encoding.h" #include "net/web_server/enable_cors.h" @@ -287,10 +296,59 @@ struct query_router { void operator()(web_server::http_req_t req, web_server::http_res_cb_t cb, bool is_ssl) { - try { - auto const url = boost::urls::url_view{req.target()}; - auto const path = url.path(); + namespace semconv = opentelemetry::sdk::resource::SemanticConventions; + + http_text_map_carrier const carrier{ + req.base()}; + auto otel_propagator = opentelemetry::context::propagation:: + GlobalTextMapPropagator::GetGlobalPropagator(); + auto current_ctx = opentelemetry::context::RuntimeContext::GetCurrent(); + auto new_ctx = otel_propagator->Extract(carrier, current_ctx); + + auto const url = boost::urls::url_view{req.target()}; + auto const path = url.path(); + + auto span = get_otel_tracer()->StartSpan( + fmt::format("{} {}", req.method_string(), req.target()), + { + {semconv::kHttpRequestMethod, req.method_string()}, + {semconv::kUrlPath, path}, + {semconv::kUrlQuery, url.query()}, + {semconv::kUrlScheme, url.scheme()}, + {semconv::kServerAddress, url.host_address()}, + {semconv::kServerPort, url.port()}, + }, + opentelemetry::trace::StartSpanOptions{ + .parent = opentelemetry::trace::GetSpan(new_ctx)->GetContext(), + .kind = opentelemetry::trace::SpanKind::kServer}); + + if (auto const user_agent = req[boost::beast::http::field::user_agent]; + !user_agent.empty()) { + span->SetAttribute(semconv::kUserAgentOriginal, user_agent); + } + + // for (auto it = req.cbegin(); it != req.cend(); it++) { + // span->SetAttribute(fmt::format("http.request.header.{}", *it), + // req[*it]); + // } + for (auto const& field : req) { + span->SetAttribute( + fmt::format("http.request.header.{}", field.name_string()), + field.value()); + } + + auto const set_otlp_status = [span](reply const& rep, bool is_error) { + auto const field = + is_error ? semconv::kErrorType : semconv::kHttpResponseStatusCode; + std::visit( + [&span, &field](auto& rep) { + span->SetAttribute(field, rep.result_int()); + }, + rep); + }; + + try { auto route = utl::find_if(routes_, [&](handler const& h) { return (h.method_ == "*" || h.method_ == req.method_string()) && path.starts_with(h.prefix_); @@ -309,12 +367,29 @@ struct query_router { set_credentials(route_req); decode_content(route_req); + span->UpdateName(fmt::format("{} {}", route->method_, route->prefix_)); + span->SetAttribute(semconv::kHttpRoute, route->prefix_); + if (route->method_ == "POST") { + span->SetAttribute(semconv::kHttpRequestBodySize, req.body().size()); + span->SetAttribute("query_router.http.request.body", req.body()); + } + return exec_.exec( - [this, route, is_ssl, r = std::move(route_req)]() { + [this, route, is_ssl, span, set_otlp_status, + r = std::move(route_req)]() { reply rep; using namespace boost::json; try { + span->AddEvent("Processing Request"); rep = route->request_handler_(r, is_ssl); + set_otlp_status(rep, false); + std::visit( + [&span](auto& rep) { + span->SetAttribute(semconv::kHttpResponseBodySize, + rep.payload_size().value_or(0)); + }, + rep); + } catch (openapi::bad_request_exception const& e) { rep = bad_request_response(r, serialize(value{{"error", e.what()}})); @@ -330,6 +405,8 @@ struct query_router { } catch (std::exception const& e) { rep = server_error_response( r, serialize(value{{"error", e.what()}})); + span->SetStatus(opentelemetry::trace::StatusCode::kError); + set_otlp_status(rep, true); } catch (...) { rep = server_error_response( r, serialize(value{{"error", "Unknown error"}})); @@ -354,14 +431,18 @@ struct query_router { }, std::move(cb)); } catch (...) { + constexpr auto const what = "malformed URI or request"; auto rep = reply{bad_request_response( - req, serialize( - boost::json::value{{"error", "malformed URI or request"}}))}; + req, serialize(boost::json::value{{"error", what}}))}; + span->SetStatus(opentelemetry::trace::StatusCode::kError, what); + set_otlp_status(rep, true); if (reply_hook_) { reply_hook_(rep); } + span->End(); return cb(std::move(rep)); } + span->End(); } void reply_hook(std::function reply_hook) {