diff --git a/CHANGELOG.md b/CHANGELOG.md index ba42e5b013..54b6d0df48 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,8 @@ Increment the: ## [Unreleased] +* [EXPORTER] Added Zipkin Exporter. ([#471](https://github.com/open-telemetry/opentelemetry-cpp/pull/471)) + ## [0.2.0] 2021-03-02 * [SDK] Added `ForceFlush` to `TracerProvider`. ([#588](https://github.com/open-telemetry/opentelemetry-cpp/pull/588)). diff --git a/CMakeLists.txt b/CMakeLists.txt index 833a177f29..6b6592511e 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -74,6 +74,7 @@ if(WITH_STL) endif() option(WITH_OTLP "Whether to include the OpenTelemetry Protocol in the SDK" OFF) +option(WITH_ZIPKIN "Whether to include the Zipkin exporter in the SDK" OFF) option(WITH_PROMETHEUS "Whether to include the Prometheus Client in the SDK" OFF) diff --git a/exporters/CMakeLists.txt b/exporters/CMakeLists.txt index 1f18637712..69cff59e89 100644 --- a/exporters/CMakeLists.txt +++ b/exporters/CMakeLists.txt @@ -1,3 +1,17 @@ +# Copyright 2021, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + if(WITH_OTLP) add_subdirectory(otlp) endif() @@ -9,6 +23,10 @@ if(WITH_PROMETHEUS) add_subdirectory(prometheus) endif() +if(WITH_ZIPKIN) + add_subdirectory(zipkin) +endif() + if(WITH_ELASTICSEARCH) add_subdirectory(elasticsearch) endif() diff --git a/exporters/zipkin/CMakeLists.txt b/exporters/zipkin/CMakeLists.txt new file mode 100644 index 0000000000..b9e8610d1d --- /dev/null +++ b/exporters/zipkin/CMakeLists.txt @@ -0,0 +1,31 @@ +# Copyright 2021, OpenTelemetry Authors +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +include_directories(include) +find_package(CURL) +if(CURL_FOUND) + add_definitions(-DWITH_CURL) +endif() +add_library(zipkin_trace_exporter src/zipkin_exporter.cc src/recordable.cc) +if(BUILD_TESTING) + add_executable(zipkin_recordable_test test/zipkin_recordable_test.cc) + + target_link_libraries(zipkin_recordable_test ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT} zipkin_trace_exporter) + + gtest_add_tests( + TARGET zipkin_recordable_test + TEST_PREFIX exporter. + TEST_LIST zipkin_recordable_test) +endif() # BUILD_TESTING diff --git a/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h b/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h new file mode 100644 index 0000000000..3672911f70 --- /dev/null +++ b/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h @@ -0,0 +1,70 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "nlohmann/json.hpp" +#include "opentelemetry/sdk/trace/recordable.h" +#include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ +using ZipkinSpan = nlohmann::json; + +enum class TransportFormat +{ + kJson, + kProtobuf +}; + +class Recordable final : public sdk::trace::Recordable +{ +public: + const ZipkinSpan &span() const noexcept { return span_; } + + void SetIds(trace::TraceId trace_id, + trace::SpanId span_id, + trace::SpanId parent_span_id) noexcept override; + + void SetAttribute(nostd::string_view key, + const opentelemetry::common::AttributeValue &value) noexcept override; + + void AddEvent(nostd::string_view name, + core::SystemTimestamp timestamp, + const common::KeyValueIterable &attributes) noexcept override; + + void AddLink(const opentelemetry::trace::SpanContext &span_context, + const common::KeyValueIterable &attributes) noexcept override; + + void SetStatus(trace::StatusCode code, nostd::string_view description) noexcept override; + + void SetName(nostd::string_view name) noexcept override; + + void SetStartTime(opentelemetry::core::SystemTimestamp start_time) noexcept override; + + virtual void SetSpanKind(opentelemetry::trace::SpanKind span_kind) noexcept override; + + void SetDuration(std::chrono::nanoseconds duration) noexcept override; + +private: + ZipkinSpan span_; +}; +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h b/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h new file mode 100644 index 0000000000..31f6244bdf --- /dev/null +++ b/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h @@ -0,0 +1,103 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#pragma once + +#include "opentelemetry/exporters/zipkin/recordable.h" +#include "opentelemetry/ext/http/client/http_client_factory.h" +#include "opentelemetry/ext/http/common/url_parser.h" +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/sdk/trace/span_data.h" + +#include "nlohmann/json.hpp" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ + +const std::string kZipkinEndpointDefault = "http://localhost:9411/api/v2/spans"; + +/** + * Struct to hold Zipkin exporter options. + */ +struct ZipkinExporterOptions +{ + // The endpoint to export to. By default the OpenTelemetry Collector's default endpoint. + std::string endpoint = kZipkinEndpointDefault; + TransportFormat format = TransportFormat::kJson; + std::string service_name = "default-service"; + std::string ipv4; + std::string ipv6; +}; + +namespace trace_sdk = opentelemetry::sdk::trace; +namespace http_client = opentelemetry::ext::http::client; + +/** + * The Zipkin exporter exports span data in JSON format as expected by Zipkin + */ +class ZipkinExporter final : public trace_sdk::SpanExporter +{ +public: + /** + * Create a ZipkinExporter using all default options. + */ + ZipkinExporter(); + + /** + * Create a ZipkinExporter using the given options. + */ + explicit ZipkinExporter(const ZipkinExporterOptions &options); + + /** + * Create a span recordable. + * @return a newly initialized Recordable object + */ + std::unique_ptr MakeRecordable() noexcept override; + + /** + * Export a batch of span recordables in JSON format. + * @param spans a span of unique pointers to span recordables + */ + trace_sdk::ExportResult Export( + const nostd::span> &spans) noexcept override; + + /** + * Shut down the exporter. + * @param timeout an optional timeout, default to max. + */ + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override + { + return true; + } + +private: + void InitializeLocalEndpoint(); + +private: + // The configuration options associated with this exporter. + bool isShutdown_ = false; + ZipkinExporterOptions options_; + std::shared_ptr http_client_; + opentelemetry::ext::http::common::UrlParser url_parser_; + nlohmann::json local_end_point_; +}; +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/zipkin/src/recordable.cc b/exporters/zipkin/src/recordable.cc new file mode 100644 index 0000000000..eda23dcf89 --- /dev/null +++ b/exporters/zipkin/src/recordable.cc @@ -0,0 +1,205 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "opentelemetry/exporters/zipkin/recordable.h" + +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ + +const int kAttributeValueSize = 14; + +void Recordable::SetIds(trace::TraceId trace_id, + trace::SpanId span_id, + trace::SpanId parent_span_id) noexcept +{ + char trace_id_lower_base16[trace::TraceId::kSize * 2] = {0}; + trace_id.ToLowerBase16(trace_id_lower_base16); + char span_id_lower_base16[trace::SpanId::kSize * 2] = {0}; + span_id.ToLowerBase16(span_id_lower_base16); + char parent_span_id_lower_base16[trace::SpanId::kSize * 2] = {0}; + parent_span_id.ToLowerBase16(parent_span_id_lower_base16); + span_["id"] = std::string(span_id_lower_base16, 16); + span_["parentId"] = std::string(parent_span_id_lower_base16, 16); + span_["traceId"] = std::string(trace_id_lower_base16, 32); +} + +void PopulateAttribute(nlohmann::json &attribute, + nostd::string_view key, + const opentelemetry::common::AttributeValue &value) +{ + // Assert size of variant to ensure that this method gets updated if the variant + // definition changes + static_assert( + nostd::variant_size::value == kAttributeValueSize, + "AttributeValue contains unknown type"); + + if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::get(value); + } + else if (nostd::holds_alternative(value)) + { + attribute[key.data()] = nostd::string_view(nostd::get(value).data(), + nostd::get(value).size()); + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get>(value)) + { + attribute[key.data()].push_back(std::string(val.data(), val.size())); + } + } +} + +void Recordable::SetAttribute(nostd::string_view key, + const opentelemetry::common::AttributeValue &value) noexcept +{ + if (!span_.contains("tags")) + { + span_["tags"] = nlohmann::json::object(); + } + PopulateAttribute(span_["tags"], key, value); +} + +void Recordable::AddEvent(nostd::string_view name, + core::SystemTimestamp timestamp, + const common::KeyValueIterable &attributes) noexcept +{ + nlohmann::json attrs = nlohmann::json::object(); // empty object + attributes.ForEachKeyValue([&](nostd::string_view key, common::AttributeValue value) noexcept { + PopulateAttribute(attrs, key, value); + return true; + }); + + nlohmann::json annotation = {{"value", nlohmann::json::object({{name.data(), attrs}}).dump()}, + {"timestamp", std::chrono::duration_cast( + timestamp.time_since_epoch()) + .count()}}; + + if (!span_.contains("annotations")) + { + span_["annotations"] = nlohmann::json::array(); + } + span_["annotations"].push_back(annotation); +} + +void Recordable::AddLink(const opentelemetry::trace::SpanContext &span_context, + const common::KeyValueIterable &attributes) noexcept +{ + // TODO: Currently not supported by specs: + // https://github.com/open-telemetry/opentelemetry-specification/blob/master/specification/trace/sdk_exporters/zipkin.md +} + +void Recordable::SetStatus(trace::StatusCode code, nostd::string_view description) noexcept +{ + span_["tags"]["otel.status_code"] = code; + if (description.size()) + span_["tags"]["otel.status_description"] = description; +} + +void Recordable::SetName(nostd::string_view name) noexcept +{ + span_["name"] = name.data(); +} + +void Recordable::SetStartTime(opentelemetry::core::SystemTimestamp start_time) noexcept +{ + span_["timestamp"] = start_time.time_since_epoch().count(); +} + +void Recordable::SetDuration(std::chrono::nanoseconds duration) noexcept +{ + span_["duration"] = duration.count(); +} + +void Recordable::SetSpanKind(opentelemetry::trace::SpanKind span_kind) noexcept {} +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/zipkin/src/zipkin_exporter.cc b/exporters/zipkin/src/zipkin_exporter.cc new file mode 100644 index 0000000000..cae2cd2246 --- /dev/null +++ b/exporters/zipkin/src/zipkin_exporter.cc @@ -0,0 +1,106 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "opentelemetry/exporters/zipkin/zipkin_exporter.h" +#include "opentelemetry/exporters/zipkin/recordable.h" +#include "opentelemetry/ext/http/client/http_client_factory.h" +#include "opentelemetry/ext/http/common/url_parser.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ + +// -------------------------------- Constructors -------------------------------- + +ZipkinExporter::ZipkinExporter(const ZipkinExporterOptions &options) + : options_(options), url_parser_(options_.endpoint) +{ + http_client_ = ext::http::client::HttpClientFactory::CreateSync(); + InitializeLocalEndpoint(); +} + +ZipkinExporter::ZipkinExporter() : options_(ZipkinExporterOptions()), url_parser_(options_.endpoint) +{ + http_client_ = ext::http::client::HttpClientFactory::CreateSync(); + InitializeLocalEndpoint(); +} + +// ----------------------------- Exporter methods ------------------------------ + +std::unique_ptr ZipkinExporter::MakeRecordable() noexcept +{ + return std::unique_ptr(new Recordable); +} + +sdk::trace::ExportResult ZipkinExporter::Export( + const nostd::span> &spans) noexcept +{ + if (isShutdown_) + { + return sdk::trace::ExportResult::kFailure; + } + exporter::zipkin::ZipkinSpan json_spans = {}; + for (auto &recordable : spans) + { + auto rec = std::unique_ptr(static_cast(recordable.release())); + if (rec != nullptr) + { + auto json_span = rec->span(); + // add localEndPoint + json_span["localEndpoint"] = local_end_point_; + json_spans.push_back(json_span); + } + } + auto body_s = json_spans.dump(); + http_client::Body body_v(body_s.begin(), body_s.end()); + auto result = http_client_->Post(url_parser_.url_, body_v); + if (result && result.GetResponse().GetStatusCode() == 200 || + result.GetResponse().GetStatusCode() == 202) + { + return sdk::trace::ExportResult::kSuccess; + } + else + { + if (result.GetSessionState() == http_client::SessionState::ConnectFailed) + { + // TODO -> Handle error / retries + } + return sdk::trace::ExportResult::kFailure; + } + return sdk::trace::ExportResult::kSuccess; +} + +void ZipkinExporter::InitializeLocalEndpoint() +{ + if (options_.service_name.length()) + { + local_end_point_["serviceName"] = options_.service_name; + } + if (options_.ipv4.length()) + { + local_end_point_["ipv4"] = options_.ipv4; + } + if (options_.ipv6.length()) + { + local_end_point_["ipv6"] = options_.ipv6; + } + local_end_point_["port"] = url_parser_.port_; +} + +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/zipkin/test/zipkin_recordable_test.cc b/exporters/zipkin/test/zipkin_recordable_test.cc new file mode 100644 index 0000000000..d3c482df5c --- /dev/null +++ b/exporters/zipkin/test/zipkin_recordable_test.cc @@ -0,0 +1,232 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +#include "opentelemetry/sdk/trace/recordable.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/span_data.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/provider.h" + +#include "opentelemetry/sdk/trace/exporter.h" + +#include "opentelemetry/core/timestamp.h" +#include "opentelemetry/exporters/zipkin/recordable.h" + +#include + +#include + +namespace trace = opentelemetry::trace; +namespace nostd = opentelemetry::nostd; +namespace sdktrace = opentelemetry::sdk::trace; +using json = nlohmann::json; + +// Testing Shutdown functionality of OStreamSpanExporter, should expect no data to be sent to Stream +TEST(ZipkinSpanRecordable, SetIds) +{ + json j_span = {{"id", "0000000000000002"}, + {"parentId", "0000000000000003"}, + {"traceId", "00000000000000000000000000000001"}}; + opentelemetry::exporter::zipkin::Recordable rec; + const trace::TraceId trace_id(std::array( + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})); + + const trace::SpanId span_id( + std::array({0, 0, 0, 0, 0, 0, 0, 2})); + + const trace::SpanId parent_span_id( + std::array({0, 0, 0, 0, 0, 0, 0, 3})); + + rec.SetIds(trace_id, span_id, parent_span_id); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetName) +{ + nostd::string_view name = "Test Span"; + json j_span = {{"name", name}}; + opentelemetry::exporter::zipkin::Recordable rec; + rec.SetName(name); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetStartTime) +{ + opentelemetry::exporter::zipkin::Recordable rec; + std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now(); + opentelemetry::core::SystemTimestamp start_timestamp(start_time); + + uint64_t unix_start = + std::chrono::duration_cast(start_time.time_since_epoch()).count(); + json j_span = {{"timestamp", unix_start}}; + rec.SetStartTime(start_timestamp); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetDuration) +{ + json j_span = {{"duration", 10}, {"timestamp", 0}}; + opentelemetry::exporter::zipkin::Recordable rec; + // Start time is 0 + opentelemetry::core::SystemTimestamp start_timestamp; + + std::chrono::nanoseconds duration(10); + uint64_t unix_end = duration.count(); + + rec.SetStartTime(start_timestamp); + rec.SetDuration(duration); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetStatus) +{ + opentelemetry::exporter::zipkin::Recordable rec; + trace::StatusCode code(trace::StatusCode::kOk); + nostd::string_view description = "For test"; + json j_span = {{"tags", {{"otel.status_code", code}, {"otel.status_description", description}}}}; + rec.SetStatus(code, description); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, AddEventDefault) +{ + opentelemetry::exporter::zipkin::Recordable rec; + nostd::string_view name = "Test Event"; + + std::chrono::system_clock::time_point event_time = std::chrono::system_clock::now(); + opentelemetry::core::SystemTimestamp event_timestamp(event_time); + + rec.opentelemetry::sdk::trace::Recordable::AddEvent(name, event_timestamp); + + uint64_t unix_event_time = + std::chrono::duration_cast(event_time.time_since_epoch()).count(); + + json j_span = { + {"annotations", + {{{"value", json({{name, json::object()}}).dump()}, {"timestamp", unix_event_time}}}}}; + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, AddEventWithAttributes) +{ + opentelemetry::exporter::zipkin::Recordable rec; + nostd::string_view name = "Test Event"; + + std::chrono::system_clock::time_point event_time = std::chrono::system_clock::now(); + opentelemetry::core::SystemTimestamp event_timestamp(event_time); + uint64_t unix_event_time = + std::chrono::duration_cast(event_time.time_since_epoch()).count(); + + const int kNumAttributes = 3; + std::string keys[kNumAttributes] = {"attr1", "attr2", "attr3"}; + int values[kNumAttributes] = {4, 7, 23}; + std::map attributes = { + {keys[0], values[0]}, {keys[1], values[1]}, {keys[2], values[2]}}; + + rec.AddEvent("Test Event", event_timestamp, + opentelemetry::common::KeyValueIterableView>(attributes)); + + nlohmann::json j_span = { + {"annotations", + {{{"value", json({{"Test Event", {{"attr1", 4}, {"attr2", 7}, {"attr3", 23}}}}).dump()}, + {"timestamp", unix_event_time}}}}}; + EXPECT_EQ(rec.span(), j_span); +} + +// Test non-int single types. Int single types are tested using templates (see IntAttributeTest) +TEST(ZipkinSpanRecordable, SetSingleAtrribute) +{ + opentelemetry::exporter::zipkin::Recordable rec; + nostd::string_view bool_key = "bool_attr"; + opentelemetry::common::AttributeValue bool_val(true); + rec.SetAttribute(bool_key, bool_val); + + nostd::string_view double_key = "double_attr"; + opentelemetry::common::AttributeValue double_val(3.3); + rec.SetAttribute(double_key, double_val); + + nostd::string_view str_key = "str_attr"; + opentelemetry::common::AttributeValue str_val(nostd::string_view("Test")); + rec.SetAttribute(str_key, str_val); + nlohmann::json j_span = { + {"tags", {{"bool_attr", true}, {"double_attr", 3.3}, {"str_attr", "Test"}}}}; + + EXPECT_EQ(rec.span(), j_span); +} + +// Test non-int array types. Int array types are tested using templates (see IntAttributeTest) +TEST(ZipkinSpanRecordable, SetArrayAtrribute) +{ + opentelemetry::exporter::zipkin::Recordable rec; + nlohmann::json j_span = {{"tags", + {{"bool_arr_attr", {true, false, true}}, + {"double_arr_attr", {22.3, 33.4, 44.5}}, + {"str_arr_attr", {"Hello", "World", "Test"}}}}}; + const int kArraySize = 3; + + bool bool_arr[kArraySize] = {true, false, true}; + nostd::span bool_span(bool_arr); + rec.SetAttribute("bool_arr_attr", bool_span); + + double double_arr[kArraySize] = {22.3, 33.4, 44.5}; + nostd::span double_span(double_arr); + rec.SetAttribute("double_arr_attr", double_span); + + nostd::string_view str_arr[kArraySize] = {"Hello", "World", "Test"}; + nostd::span str_span(str_arr); + rec.SetAttribute("str_arr_attr", str_span); + + EXPECT_EQ(rec.span(), j_span); +} + +/** + * AttributeValue can contain different int types, such as int, int64_t, + * unsigned int, and uint64_t. To avoid writing test cases for each, we can + * use a template approach to test all int types. + */ +template +struct ZipkinIntAttributeTest : public testing::Test +{ + using IntParamType = T; +}; + +using IntTypes = testing::Types; +TYPED_TEST_CASE(ZipkinIntAttributeTest, IntTypes); + +TYPED_TEST(ZipkinIntAttributeTest, SetIntSingleAttribute) +{ + using IntType = typename TestFixture::IntParamType; + IntType i = 2; + opentelemetry::common::AttributeValue int_val(i); + + opentelemetry::exporter::zipkin::Recordable rec; + rec.SetAttribute("int_attr", int_val); + nlohmann::json j_span = {{"tags", {{"int_attr", 2}}}}; + EXPECT_EQ(rec.span(), j_span); +} + +TYPED_TEST(ZipkinIntAttributeTest, SetIntArrayAttribute) +{ + using IntType = typename TestFixture::IntParamType; + + const int kArraySize = 3; + IntType int_arr[kArraySize] = {4, 5, 6}; + nostd::span int_span(int_arr); + + opentelemetry::exporter::zipkin::Recordable rec; + rec.SetAttribute("int_arr_attr", int_span); + nlohmann::json j_span = {{"tags", {{"int_arr_attr", {4, 5, 6}}}}}; + EXPECT_EQ(rec.span(), j_span); +} diff --git a/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h b/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h index 522ade705f..ad27ce4dea 100644 --- a/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h +++ b/ext/include/opentelemetry/ext/http/client/curl/http_client_curl.h @@ -138,7 +138,7 @@ class Session : public http_client::Session std::string url = host_ + std::string(http_request_->uri_); auto callback_ptr = &callback; curl_operation_.reset(new HttpOperation( - http_request_->method_, url, callback_ptr, RequestMode::Sync, http_request_->headers_, + http_request_->method_, url, callback_ptr, RequestMode::Async, http_request_->headers_, http_request_->body_, false, http_request_->timeout_ms_)); curl_operation_->SendAsync([this, callback_ptr](HttpOperation &operation) { if (operation.WasAborted()) @@ -213,11 +213,11 @@ class HttpClientSync : public http_client::HttpClientSync } http_client::Result Post(const nostd::string_view &url, - const Data &data, + const Body &body, const http_client::Headers &headers) noexcept override { HttpOperation curl_operation(http_client::Method::Post, url.data(), nullptr, RequestMode::Sync, - headers); + headers, body); curl_operation.SendSync(); auto session_state = curl_operation.GetSessionState(); if (curl_operation.WasAborted()) diff --git a/ext/include/opentelemetry/ext/http/client/http_client.h b/ext/include/opentelemetry/ext/http/client/http_client.h index e1588ec9d0..682030e532 100644 --- a/ext/include/opentelemetry/ext/http/client/http_client.h +++ b/ext/include/opentelemetry/ext/http/client/http_client.h @@ -94,7 +94,6 @@ enum class SessionState using Byte = uint8_t; using StatusCode = uint16_t; using Body = std::vector; -using Data = std::map; using SSLCertificate = std::vector; struct cmp_ic @@ -239,7 +238,7 @@ class HttpClientSync virtual Result Get(const nostd::string_view &url, const Headers & = {{}}) noexcept = 0; virtual Result Post(const nostd::string_view &url, - const Data &data, + const Body &body, const Headers & = {{"content-type", "application/json"}}) noexcept = 0; virtual ~HttpClientSync() = default; diff --git a/ext/include/opentelemetry/ext/http/client/http_client_factory.h b/ext/include/opentelemetry/ext/http/client/http_client_factory.h index d09c957051..9ae5b0dd1b 100644 --- a/ext/include/opentelemetry/ext/http/client/http_client_factory.h +++ b/ext/include/opentelemetry/ext/http/client/http_client_factory.h @@ -18,4 +18,4 @@ class HttpClientFactory } // namespace client } // namespace http } // namespace ext -OPENTELEMETRY_END_NAMESPACE +OPENTELEMETRY_END_NAMESPACE \ No newline at end of file diff --git a/ext/include/opentelemetry/ext/http/common/url_parser.h b/ext/include/opentelemetry/ext/http/common/url_parser.h new file mode 100644 index 0000000000..bf05f35b9b --- /dev/null +++ b/ext/include/opentelemetry/ext/http/common/url_parser.h @@ -0,0 +1,127 @@ +#pragma once + +#include +#include +#include "opentelemetry/nostd/string_view.h" +#include "opentelemetry/version.h" +OPENTELEMETRY_BEGIN_NAMESPACE +namespace ext +{ +namespace http +{ +namespace common +{ +// http://user:password@host:port/path1/path2?key1=val2&key2=val2 +// http://host:port/path1/path2?key1=val1&key2=val2 +// host:port/path1 +// host:port ( path defaults to "/") +// host:port? + +class UrlParser +{ +public: + std::string url_; + std::string host_; + std::string scheme_; + std::string path_; + size_t port_; + std::string query_; + bool success_; + + UrlParser(std::string url) : url_(url), success_(true) + { + if (url_.length() == 0) + { + return; + } + size_t cpos = 0; + // scheme + size_t pos = url_.find("://", cpos); + if (pos == std::string::npos) + { + // scheme missing, use default as http + scheme_ = "http"; + } + else + { + scheme_ = std::string(url_.begin() + cpos, url_.begin() + pos); + cpos = pos + 3; + } + + // credentials + pos = url_.find_first_of("@", cpos); + if (pos != std::string::npos) + { + // TODO - handle credentials + cpos = pos + 1; + } + pos = url_.find_first_of(":", cpos); + bool is_port = false; + if (pos == std::string::npos) + { + // port missing. Used default 80 / 443 + if (scheme_ == "http") + port_ = 80; + if (scheme_ == "https") + port_ = 443; + } + else + { + // port present + is_port = true; + host_ = std::string(url_.begin() + cpos, url_.begin() + pos); + cpos = pos + 1; + } + pos = url_.find_first_of("/?", cpos); + if (pos == std::string::npos) + { + path_ = "/"; // use default path + if (is_port) + { + port_ = std::stoi(std::string(url_.begin() + cpos, url_.begin() + url_.length())); + } + else + { + host_ = std::string(url_.begin() + cpos, url_.begin() + url_.length()); + } + return; + } + if (is_port) + { + port_ = std::stoi(std::string(url_.begin() + cpos, url_.begin() + pos)); + } + else + { + host_ = std::string(url_.begin() + cpos, url_.begin() + pos); + } + cpos = pos; + + if (url_[cpos] == '/') + { + pos = url_.find('?', cpos); + if (pos == std::string::npos) + { + path_ = std::string(url_.begin() + cpos, url_.begin() + url_.length()); + query_ = ""; + } + else + { + path_ = std::string(url_.begin() + cpos, url_.begin() + pos); + cpos = pos + 1; + query_ = std::string(url_.begin() + cpos, url_.begin() + url_.length()); + } + return; + } + path_ = "/"; + if (url_[cpos] == '?') + { + query_ = std::string(url_.begin() + cpos, url_.begin() + url_.length()); + } + } +}; + +} // namespace common + +} // namespace http +} // namespace ext +OPENTELEMETRY_END_NAMESPACE \ No newline at end of file diff --git a/ext/test/http/CMakeLists.txt b/ext/test/http/CMakeLists.txt index 221f901b1d..13b7c8206e 100644 --- a/ext/test/http/CMakeLists.txt +++ b/ext/test/http/CMakeLists.txt @@ -17,3 +17,11 @@ if(CURL_FOUND) TEST_PREFIX ext.http.curl. TEST_LIST ${FILENAME}) endif() +set(URL_PARSER_FILENAME url_parser_test) +add_executable(${URL_PARSER_FILENAME} ${URL_PARSER_FILENAME}.cc) +target_link_libraries(${URL_PARSER_FILENAME} ${GTEST_BOTH_LIBRARIES} + ${CMAKE_THREAD_LIBS_INIT}) +gtest_add_tests( + TARGET ${URL_PARSER_FILENAME} + TEST_PREFIX ext.http.urlparser. + TEST_LIST ${URL_PARSER_FILENAME}) diff --git a/ext/test/http/curl_http_test.cc b/ext/test/http/curl_http_test.cc index 6338fc6a91..88be5fe535 100644 --- a/ext/test/http/curl_http_test.cc +++ b/ext/test/http/curl_http_test.cc @@ -273,7 +273,7 @@ TEST_F(BasicCurlHttpTests, SendGetRequestSync) curl::HttpClientSync http_client; http_client::Headers m1 = {}; - auto result = http_client.Get("http://127.0.0.1:19000/get/", m1); // (session_state); + auto result = http_client.Get("http://127.0.0.1:19000/get/", m1); EXPECT_EQ(result, true); EXPECT_EQ(result.GetSessionState(), http_client::SessionState::Response); } @@ -284,13 +284,24 @@ TEST_F(BasicCurlHttpTests, SendGetRequestSyncTimeout) curl::HttpClientSync http_client; http_client::Headers m1 = {}; - auto result = - http_client.Get("http://222.222.222.200:19000/get/", m1); // (session_state); + auto result = http_client.Get("http://222.222.222.200:19000/get/", m1); EXPECT_EQ(result, false); EXPECT_EQ(result.GetSessionState(), http_client::SessionState::ConnectFailed); } +TEST_F(BasicCurlHttpTests, SendPostRequestSync) +{ + received_requests_.clear(); + curl::HttpClientSync http_client; + + http_client::Headers m1 = {}; + http_client::Body body = {}; + auto result = http_client.Post("http://127.0.0.1:19000/post/", body, m1); + EXPECT_EQ(result, true); + EXPECT_EQ(result.GetSessionState(), http_client::SessionState::Response); +} + TEST_F(BasicCurlHttpTests, GetBaseUri) { curl::HttpClient session_manager; @@ -305,4 +316,4 @@ TEST_F(BasicCurlHttpTests, GetBaseUri) session = session_manager.CreateSession("http://127.0.0.1", 31339); ASSERT_EQ(std::static_pointer_cast(session)->GetBaseUri(), "http://127.0.0.1:31339/"); -} \ No newline at end of file +} diff --git a/ext/test/http/url_parser_test.cc b/ext/test/http/url_parser_test.cc new file mode 100644 index 0000000000..dfc9c3f961 --- /dev/null +++ b/ext/test/http/url_parser_test.cc @@ -0,0 +1,126 @@ +#include "opentelemetry/ext/http/common/url_parser.h" + +#include + +namespace http_common = opentelemetry::ext::http::common; + +inline const char *const BoolToString(bool b) +{ + return b ? "true" : "false"; +} + +TEST(UrlParserTests, BasicTests) +{ + std::map> urls_map{ + {"www.abc.com", + {{"host", "www.abc.com"}, + {"port", "80"}, + {"scheme", "http"}, + {"path", "/"}, + {"query", ""}, + {"success", "true"}}}, + {"http://www.abc.com", + {{"host", "www.abc.com"}, + {"port", "80"}, + {"scheme", "http"}, + {"path", "/"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com", + {{"host", "www.abc.com"}, + {"port", "443"}, + {"scheme", "https"}, + {"path", "/"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com:4431", + {{"host", "www.abc.com"}, + {"port", "4431"}, + {"scheme", "https"}, + {"path", "/"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com:4431", + {{"host", "www.abc.com"}, + {"port", "4431"}, + {"scheme", "https"}, + {"path", "/"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com:4431/path1", + {{"host", "www.abc.com"}, + {"port", "4431"}, + {"scheme", "https"}, + {"path", "/path1"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com:4431/path1/path2", + {{"host", "www.abc.com"}, + {"port", "4431"}, + {"scheme", "https"}, + {"path", "/path1/path2"}, + {"query", ""}, + {"success", "true"}}}, + {"https://www.abc.com/path1/path2", + {{"host", "www.abc.com"}, + {"port", "443"}, + {"scheme", "https"}, + {"path", "/path1/path2"}, + {"query", ""}, + {"success", "true"}}}, + {"http://www.abc.com/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "80"}, + {"scheme", "http"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + {"http://www.abc.com:8080/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "8080"}, + {"scheme", "http"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + {"www.abc.com:8080/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "8080"}, + {"scheme", "http"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + {"http://user:password@www.abc.com:8080/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "8080"}, + {"scheme", "http"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + {"user:password@www.abc.com:8080/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "8080"}, + {"scheme", "http"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + {"https://user@www.abc.com/path1/path2?q1=a1&q2=a2", + {{"host", "www.abc.com"}, + {"port", "443"}, + {"scheme", "https"}, + {"path", "/path1/path2"}, + {"query", "q1=a1&q2=a2"}, + {"success", "true"}}}, + + }; + for (auto &url_map : urls_map) + { + http_common::UrlParser url(url_map.first); + auto url_properties = url_map.second; + ASSERT_EQ(BoolToString(url.success_), url_properties["success"]); + ASSERT_EQ(url.host_, url_properties["host"]); + ASSERT_EQ(std::to_string(url.port_), url_properties["port"]); + ASSERT_EQ(url.scheme_, url_properties["scheme"]); + ASSERT_EQ(url.path_, url_properties["path"]); + ASSERT_EQ(url.query_, url_properties["query"]); + } +}