diff options
Diffstat (limited to 'src/jaegertracing/opentelemetry-cpp/exporters/zipkin')
9 files changed, 1250 insertions, 0 deletions
diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/BUILD b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/BUILD new file mode 100644 index 000000000..6cd52b2d0 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/BUILD @@ -0,0 +1,64 @@ +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "zipkin_recordable", + srcs = [ + "src/recordable.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/zipkin/recordable.h", + ], + strip_include_prefix = "include", + tags = ["zipkin"], + deps = [ + "//sdk/src/resource", + "//sdk/src/trace", + "@github_nlohmann_json//:json", + ], +) + +cc_library( + name = "zipkin_exporter", + srcs = [ + "src/zipkin_exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/zipkin/zipkin_exporter.h", + ], + copts = [ + "-DCURL_STATICLIB", + ], + strip_include_prefix = "include", + tags = ["zipkin"], + deps = [ + ":zipkin_recordable", + "//ext/src/http/client/curl:http_client_curl", + ], +) + +cc_test( + name = "zipkin_recordable_test", + srcs = ["test/zipkin_recordable_test.cc"], + tags = [ + "test", + "zipkin", + ], + deps = [ + ":zipkin_recordable", + "@com_google_googletest//:gtest_main", + ], +) + +cc_test( + name = "zipkin_exporter_test", + srcs = ["test/zipkin_exporter_test.cc"], + tags = [ + "test", + "zipkin", + ], + deps = [ + ":zipkin_exporter", + ":zipkin_recordable", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/CMakeLists.txt b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/CMakeLists.txt new file mode 100644 index 000000000..559e8d500 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/CMakeLists.txt @@ -0,0 +1,75 @@ +# 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 REQUIRED) +add_definitions(-DWITH_CURL) +add_library(opentelemetry_exporter_zipkin_trace src/zipkin_exporter.cc + src/recordable.cc) + +target_link_libraries( + opentelemetry_exporter_zipkin_trace + PUBLIC opentelemetry_trace opentelemetry_http_client_curl + nlohmann_json::nlohmann_json) + +install( + TARGETS opentelemetry_exporter_zipkin_trace + EXPORT "${PROJECT_NAME}-target" + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + +install( + DIRECTORY include/opentelemetry/exporters/zipkin + DESTINATION include/opentelemetry/exporters + FILES_MATCHING + PATTERN "*.h" + PATTERN "recordable.h" EXCLUDE) + +if(BUILD_TESTING) + add_definitions(-DGTEST_LINKED_AS_SHARED_LIBRARY=1) + + add_executable(zipkin_recordable_test test/zipkin_recordable_test.cc) + + target_link_libraries( + zipkin_recordable_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_exporter_zipkin_trace opentelemetry_resources) + + gtest_add_tests( + TARGET zipkin_recordable_test + TEST_PREFIX exporter. + TEST_LIST zipkin_recordable_test) + + if(MSVC) + if(GMOCK_LIB) + unset(GMOCK_LIB CACHE) + endif() + endif() + if(MSVC AND CMAKE_BUILD_TYPE STREQUAL "Debug") + find_library(GMOCK_LIB gmockd PATH_SUFFIXES lib) + else() + find_library(GMOCK_LIB gmock PATH_SUFFIXES lib) + endif() + + add_executable(zipkin_exporter_test test/zipkin_exporter_test.cc) + + target_link_libraries( + zipkin_exporter_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + ${GMOCK_LIB} opentelemetry_exporter_zipkin_trace opentelemetry_resources) + + gtest_add_tests( + TARGET zipkin_exporter_test + TEST_PREFIX exporter. + TEST_LIST zipkin_exporter_test) +endif() # BUILD_TESTING diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/README.md b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/README.md new file mode 100644 index 000000000..40bb1f061 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/README.md @@ -0,0 +1,56 @@ +# Zipkin Exporter for OpenTelemetry C++ + +## Prerequisite + +* [Get Zipkin](https://zipkin.io/pages/quickstart.html) + +## Installation + +### CMake Install Instructions + +Refer to install instructions [INSTALL.md](../../INSTALL.md#building-as-standalone-cmake-project). +Modify step 2 to create `cmake` build configuration for compiling Zipkin as below: + +```console + $ cmake -DWITH_ZIPKIN=ON .. + -- The C compiler identification is GNU 9.3.0 + -- The CXX compiler identification is GNU 9.3.0 + ... + -- Configuring done + -- Generating done + -- Build files have been written to: /home/<user>/source/opentelemetry-cpp/build + $ +``` + +### Bazel Install Instructions + +TODO + +## Usage + +Install the exporter on your application and pass the options. `service_name` +is an optional string. If omitted, the exporter will first try to get the +service name from the Resource. If no service name can be detected on the +Resource, a fallback name of "unknown_service" will be used. + +```cpp + +opentelemetry::exporter::zipkin::ZipkinExporterOptions options; +options.endpoint = "http://localhost:9411/api/v2/spans"; +options.service_name = "my_service"; + +auto exporter = std::unique_ptr<opentelemetry::sdk::trace::SpanExporter>( + new opentelemetry::exporter::zipkin::ZipkinExporter(options)); +auto processor = std::shared_ptr<sdktrace::SpanProcessor>( + new sdktrace::SimpleSpanProcessor(std::move(exporter))); +auto provider = nostd::shared_ptr<opentelemetry::trace::TracerProvider>( + new sdktrace::TracerProvider(processor, opentelemetry::sdk::resource::Resource::Create({}), + std::make_shared<opentelemetry::sdk::trace::AlwaysOnSampler>())); + +// Set the global trace provider +opentelemetry::trace::Provider::SetTracerProvider(provider); +``` + +## Viewing your traces + +Please visit the Zipkin UI endpoint <http://localhost:9411> diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h new file mode 100644 index 000000000..51f83211f --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/recordable.h @@ -0,0 +1,60 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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; + +class Recordable final : public sdk::trace::Recordable +{ +public: + const ZipkinSpan &span() const noexcept { return span_; } + + const std::string &GetServiceName() const noexcept { return service_name_; } + + void SetIdentity(const opentelemetry::trace::SpanContext &span_context, + opentelemetry::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, + common::SystemTimestamp timestamp, + const common::KeyValueIterable &attributes) noexcept override; + + void AddLink(const opentelemetry::trace::SpanContext &span_context, + const common::KeyValueIterable &attributes) noexcept override; + + void SetStatus(opentelemetry::trace::StatusCode code, + nostd::string_view description) noexcept override; + + void SetName(nostd::string_view name) noexcept override; + + void SetStartTime(opentelemetry::common::SystemTimestamp start_time) noexcept override; + + void SetSpanKind(opentelemetry::trace::SpanKind span_kind) noexcept override; + + void SetResource(const opentelemetry::sdk::resource::Resource &resource) noexcept override; + + void SetDuration(std::chrono::nanoseconds duration) noexcept override; + + void SetInstrumentationLibrary( + const opentelemetry::sdk::instrumentationlibrary::InstrumentationLibrary + &instrumentation_library) noexcept override; + +private: + ZipkinSpan span_; + std::string service_name_; +}; +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h new file mode 100644 index 000000000..ae0e8173f --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/include/opentelemetry/exporters/zipkin/zipkin_exporter.h @@ -0,0 +1,113 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#include "opentelemetry/common/spin_lock_mutex.h" +#include "opentelemetry/ext/http/client/http_client_factory.h" +#include "opentelemetry/ext/http/common/url_parser.h" +#include "opentelemetry/sdk/common/env_variables.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 +{ + +inline const std::string GetDefaultZipkinEndpoint() +{ + const char *otel_exporter_zipkin_endpoint_env = "OTEL_EXPORTER_ZIPKIN_ENDPOINT"; + const char *kZipkinEndpointDefault = "http://localhost:9411/api/v2/spans"; + + auto endpoint = + opentelemetry::sdk::common::GetEnvironmentVariable(otel_exporter_zipkin_endpoint_env); + return endpoint.size() ? endpoint : kZipkinEndpointDefault; +} + +enum class TransportFormat +{ + kJson, + kProtobuf +}; + +/** + * Struct to hold Zipkin exporter options. + */ +struct ZipkinExporterOptions +{ + // The endpoint to export to. By default the OpenTelemetry Collector's default endpoint. + std::string endpoint = GetDefaultZipkinEndpoint(); + TransportFormat format = TransportFormat::kJson; + std::string service_name = "default-service"; + std::string ipv4; + std::string ipv6; + ext::http::client::Headers headers = {{"content-type", "application/json"}}; +}; + +/** + * The Zipkin exporter exports span data in JSON format as expected by Zipkin + */ +class ZipkinExporter final : public opentelemetry::sdk::trace::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<opentelemetry::sdk::trace::Recordable> MakeRecordable() noexcept override; + + /** + * Export a batch of span recordables in JSON format. + * @param spans a span of unique pointers to span recordables + */ + sdk::common::ExportResult Export( + const nostd::span<std::unique_ptr<opentelemetry::sdk::trace::Recordable>> &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; + +private: + void InitializeLocalEndpoint(); + +private: + // The configuration options associated with this exporter. + bool is_shutdown_ = false; + ZipkinExporterOptions options_; + std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync> http_client_; + opentelemetry::ext::http::common::UrlParser url_parser_; + nlohmann::json local_end_point_; + + // For testing + friend class ZipkinExporterTestPeer; + /** + * Create an ZipkinExporter using the specified thrift sender. + * Only tests can call this constructor directly. + * @param http_client the http client to be used for exporting + */ + ZipkinExporter(std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync> http_client); + + mutable opentelemetry::common::SpinLockMutex lock_; + bool isShutdown() const noexcept; +}; +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/recordable.cc b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/recordable.cc new file mode 100644 index 000000000..700d6a964 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/recordable.cc @@ -0,0 +1,254 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#include "opentelemetry/exporters/zipkin/recordable.h" +#include "opentelemetry/sdk/resource/experimental_semantic_conventions.h" + +#include <map> +#include <string> + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ + +using namespace opentelemetry::sdk::resource; +namespace trace_api = opentelemetry::trace; +namespace common = opentelemetry::common; +namespace sdk = opentelemetry::sdk; + +// constexpr needs keys to be constexpr, const is next best to use. +static const std::map<trace_api::SpanKind, std::string> kSpanKindMap = { + {trace_api::SpanKind::kClient, "CLIENT"}, + {trace_api::SpanKind::kServer, "SERVER"}, + {trace_api::SpanKind::kConsumer, "CONSUMER"}, + {trace_api::SpanKind::kProducer, "PRODUCER"}, +}; + +// +// See `attribute_value.h` for details. +// +const int kAttributeValueSize = 16; + +void Recordable::SetIdentity(const trace_api::SpanContext &span_context, + trace_api::SpanId parent_span_id) noexcept +{ + char trace_id_lower_base16[trace::TraceId::kSize * 2] = {0}; + span_context.trace_id().ToLowerBase16(trace_id_lower_base16); + char span_id_lower_base16[trace::SpanId::kSize * 2] = {0}; + span_context.span_id().ToLowerBase16(span_id_lower_base16); + if (parent_span_id.IsValid()) + { + char parent_span_id_lower_base16[trace::SpanId::kSize * 2] = {0}; + parent_span_id.ToLowerBase16(parent_span_id_lower_base16); + span_["parentId"] = std::string(parent_span_id_lower_base16, 16); + } + + span_["id"] = std::string(span_id_lower_base16, 16); + span_["traceId"] = std::string(trace_id_lower_base16, 32); +} + +void PopulateAttribute(nlohmann::json &attribute, + nostd::string_view key, + const common::AttributeValue &value) +{ + // Assert size of variant to ensure that this method gets updated if the variant + // definition changes + static_assert(nostd::variant_size<common::AttributeValue>::value == kAttributeValueSize, + "AttributeValue contains unknown type"); + + if (nostd::holds_alternative<bool>(value)) + { + attribute[key.data()] = nostd::get<bool>(value); + } + else if (nostd::holds_alternative<int>(value)) + { + attribute[key.data()] = nostd::get<int>(value); + } + else if (nostd::holds_alternative<int64_t>(value)) + { + attribute[key.data()] = nostd::get<int64_t>(value); + } + else if (nostd::holds_alternative<unsigned int>(value)) + { + attribute[key.data()] = nostd::get<unsigned int>(value); + } + else if (nostd::holds_alternative<uint64_t>(value)) + { + attribute[key.data()] = nostd::get<uint64_t>(value); + } + else if (nostd::holds_alternative<double>(value)) + { + attribute[key.data()] = nostd::get<double>(value); + } + else if (nostd::holds_alternative<const char *>(value)) + { + attribute[key.data()] = nostd::get<const char *>(value); + } + else if (nostd::holds_alternative<nostd::string_view>(value)) + { + attribute[key.data()] = nostd::string_view(nostd::get<nostd::string_view>(value).data(), + nostd::get<nostd::string_view>(value).size()); + } + else if (nostd::holds_alternative<nostd::span<const uint8_t>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const uint8_t>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const bool>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const bool>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const int>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const int>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const int64_t>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const int64_t>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const unsigned int>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const unsigned int>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const uint64_t>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const uint64_t>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const double>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const double>>(value)) + { + attribute[key.data()].push_back(val); + } + } + else if (nostd::holds_alternative<nostd::span<const nostd::string_view>>(value)) + { + attribute[key.data()] = {}; + for (const auto &val : nostd::get<nostd::span<const nostd::string_view>>(value)) + { + attribute[key.data()].push_back(std::string(val.data(), val.size())); + } + } +} + +void Recordable::SetAttribute(nostd::string_view key, const 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, + common::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<std::chrono::microseconds>( + timestamp.time_since_epoch()) + .count()}}; + + if (!span_.contains("annotations")) + { + span_["annotations"] = nlohmann::json::array(); + } + span_["annotations"].push_back(annotation); +} + +void Recordable::AddLink(const trace_api::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 +{ + if (code != trace::StatusCode::kUnset) + { + span_["tags"]["otel.status_code"] = code; + if (code == trace::StatusCode::kError) + { + span_["tags"]["error"] = description; + } + } +} + +void Recordable::SetName(nostd::string_view name) noexcept +{ + span_["name"] = name.data(); +} + +void Recordable::SetResource(const sdk::resource::Resource &resource) noexcept +{ + // only service.name attribute is supported by specs as of now. + auto attributes = resource.GetAttributes(); + if (attributes.find(OTEL_GET_RESOURCE_ATTR(AttrServiceName)) != attributes.end()) + { + service_name_ = nostd::get<std::string>(attributes[OTEL_GET_RESOURCE_ATTR(AttrServiceName)]); + } +} + +void Recordable::SetStartTime(common::SystemTimestamp start_time) noexcept +{ + span_["timestamp"] = + std::chrono::duration_cast<std::chrono::microseconds>(start_time.time_since_epoch()).count(); +} + +void Recordable::SetDuration(std::chrono::nanoseconds duration) noexcept +{ + span_["duration"] = std::chrono::duration_cast<std::chrono::microseconds>(duration).count(); +} + +void Recordable::SetSpanKind(trace_api::SpanKind span_kind) noexcept +{ + auto span_iter = kSpanKindMap.find(span_kind); + if (span_iter != kSpanKindMap.end()) + { + span_["kind"] = span_iter->second; + } +} + +void Recordable::SetInstrumentationLibrary( + const sdk::instrumentationlibrary::InstrumentationLibrary &instrumentation_library) noexcept +{ + span_["tags"]["otel.library.name"] = instrumentation_library.GetName(); + span_["tags"]["otel.library.version"] = instrumentation_library.GetVersion(); +} + +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/zipkin_exporter.cc b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/zipkin_exporter.cc new file mode 100644 index 000000000..240144599 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/src/zipkin_exporter.cc @@ -0,0 +1,128 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#define _WINSOCKAPI_ // stops including winsock.h +#include "opentelemetry/exporters/zipkin/zipkin_exporter.h" +#include <mutex> +#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_config.h" + +namespace http_client = opentelemetry::ext::http::client; + +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(); +} + +ZipkinExporter::ZipkinExporter( + std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync> http_client) + : options_(ZipkinExporterOptions()), url_parser_(options_.endpoint) +{ + http_client_ = http_client; + InitializeLocalEndpoint(); +} + +// ----------------------------- Exporter methods ------------------------------ + +std::unique_ptr<sdk::trace::Recordable> ZipkinExporter::MakeRecordable() noexcept +{ + return std::unique_ptr<sdk::trace::Recordable>(new Recordable); +} + +sdk::common::ExportResult ZipkinExporter::Export( + const nostd::span<std::unique_ptr<sdk::trace::Recordable>> &spans) noexcept +{ + if (isShutdown()) + { + OTEL_INTERNAL_LOG_ERROR("[Zipkin Trace Exporter] Exporting " + << spans.size() << " span(s) failed, exporter is shutdown"); + return sdk::common::ExportResult::kFailure; + } + exporter::zipkin::ZipkinSpan json_spans = {}; + for (auto &recordable : spans) + { + auto rec = std::unique_ptr<Recordable>(static_cast<Recordable *>(recordable.release())); + if (rec != nullptr) + { + auto json_span = rec->span(); + // add localEndPoint + json_span["localEndpoint"] = local_end_point_; + // check service.name + auto service_name = rec->GetServiceName(); + if (service_name.size()) + { + json_span["localEndpoint"]["serviceName"] = service_name; + } + 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, options_.headers); + if (result && + (result.GetResponse().GetStatusCode() == 200 || result.GetResponse().GetStatusCode() == 202)) + { + return sdk::common::ExportResult::kSuccess; + } + else + { + if (result.GetSessionState() == http_client::SessionState::ConnectFailed) + { + OTEL_INTERNAL_LOG_ERROR("ZIPKIN EXPORTER] Zipkin Exporter: Connection failed"); + } + return sdk::common::ExportResult::kFailure; + } + return sdk::common::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_; +} + +bool ZipkinExporter::Shutdown(std::chrono::microseconds timeout) noexcept +{ + const std::lock_guard<opentelemetry::common::SpinLockMutex> locked(lock_); + is_shutdown_ = true; + return true; +} + +bool ZipkinExporter::isShutdown() const noexcept +{ + const std::lock_guard<opentelemetry::common::SpinLockMutex> locked(lock_); + return is_shutdown_; +} + +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_exporter_test.cc b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_exporter_test.cc new file mode 100644 index 000000000..eec71f43d --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_exporter_test.cc @@ -0,0 +1,215 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef HAVE_CPP_STDLIB + +# include "opentelemetry/exporters/zipkin/zipkin_exporter.h" +# include <string> +# include "opentelemetry/ext/http/client/curl/http_client_curl.h" +# include "opentelemetry/ext/http/server/http_server.h" +# include "opentelemetry/sdk/trace/batch_span_processor.h" +# include "opentelemetry/sdk/trace/tracer_provider.h" +# include "opentelemetry/trace/provider.h" + +# include <gtest/gtest.h> +# include "gmock/gmock.h" + +# include "nlohmann/json.hpp" + +# if defined(_MSC_VER) +# include "opentelemetry/sdk/common/env_variables.h" +using opentelemetry::sdk::common::setenv; +using opentelemetry::sdk::common::unsetenv; +# endif +namespace sdk_common = opentelemetry::sdk::common; +using namespace testing; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace zipkin +{ + +namespace trace_api = opentelemetry::trace; +namespace resource = opentelemetry::sdk::resource; + +template <class T, size_t N> +static nostd::span<T, N> MakeSpan(T (&array)[N]) +{ + return nostd::span<T, N>(array); +} + +class ZipkinExporterTestPeer : public ::testing::Test +{ +public: + std::unique_ptr<sdk::trace::SpanExporter> GetExporter( + std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync> http_client) + { + return std::unique_ptr<sdk::trace::SpanExporter>(new ZipkinExporter(http_client)); + } + + // Get the options associated with the given exporter. + const ZipkinExporterOptions &GetOptions(std::unique_ptr<ZipkinExporter> &exporter) + { + return exporter->options_; + } +}; + +class MockHttpClient : public opentelemetry::ext::http::client::HttpClientSync +{ +public: + MOCK_METHOD(ext::http::client::Result, + Post, + (const nostd::string_view &, + const ext::http::client::Body &, + const ext::http::client::Headers &), + (noexcept, override)); + MOCK_METHOD(ext::http::client::Result, + Get, + (const nostd::string_view &, const ext::http::client::Headers &), + (noexcept, override)); +}; + +class IsValidMessageMatcher +{ +public: + IsValidMessageMatcher(const std::string &trace_id) : trace_id_(trace_id) {} + template <typename T> + bool MatchAndExplain(const T &p, MatchResultListener * /* listener */) const + { + auto body = std::string(p.begin(), p.end()); + nlohmann::json check_json = nlohmann::json::parse(body); + auto trace_id_kv = check_json.at(0).find("traceId"); + auto received_trace_id = trace_id_kv.value().get<std::string>(); + return trace_id_ == received_trace_id; + } + + void DescribeTo(std::ostream *os) const { *os << "received trace_id matches"; } + + void DescribeNegationTo(std::ostream *os) const { *os << "received trace_id does not matche"; } + +private: + std::string trace_id_; +}; + +PolymorphicMatcher<IsValidMessageMatcher> IsValidMessage(const std::string &trace_id) +{ + return MakePolymorphicMatcher(IsValidMessageMatcher(trace_id)); +} + +// Create spans, let processor call Export() +TEST_F(ZipkinExporterTestPeer, ExportJsonIntegrationTest) +{ + auto mock_http_client = new MockHttpClient; + // Leave a comment line here or different version of clang-format has a different result here + auto exporter = GetExporter( + std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync>{mock_http_client}); + + resource::ResourceAttributes resource_attributes = {{"service.name", "unit_test_service"}, + {"tenant.id", "test_user"}}; + resource_attributes["bool_value"] = true; + resource_attributes["int32_value"] = static_cast<int32_t>(1); + resource_attributes["uint32_value"] = static_cast<uint32_t>(2); + resource_attributes["int64_value"] = static_cast<int64_t>(0x1100000000LL); + resource_attributes["uint64_value"] = static_cast<uint64_t>(0x1200000000ULL); + resource_attributes["double_value"] = static_cast<double>(3.1); + resource_attributes["vec_bool_value"] = std::vector<bool>{true, false, true}; + resource_attributes["vec_int32_value"] = std::vector<int32_t>{1, 2}; + resource_attributes["vec_uint32_value"] = std::vector<uint32_t>{3, 4}; + resource_attributes["vec_int64_value"] = std::vector<int64_t>{5, 6}; + resource_attributes["vec_uint64_value"] = std::vector<uint64_t>{7, 8}; + resource_attributes["vec_double_value"] = std::vector<double>{3.2, 3.3}; + resource_attributes["vec_string_value"] = std::vector<std::string>{"vector", "string"}; + auto resource = resource::Resource::Create(resource_attributes); + + auto processor_opts = sdk::trace::BatchSpanProcessorOptions(); + processor_opts.max_export_batch_size = 5; + processor_opts.max_queue_size = 5; + processor_opts.schedule_delay_millis = std::chrono::milliseconds(256); + auto processor = std::unique_ptr<sdk::trace::SpanProcessor>( + new sdk::trace::BatchSpanProcessor(std::move(exporter), processor_opts)); + auto provider = nostd::shared_ptr<trace::TracerProvider>( + new sdk::trace::TracerProvider(std::move(processor), resource)); + + std::string report_trace_id; + char trace_id_hex[2 * trace_api::TraceId::kSize] = {0}; + auto tracer = provider->GetTracer("test"); + auto parent_span = tracer->StartSpan("Test parent span"); + + trace_api::StartSpanOptions child_span_opts = {}; + child_span_opts.parent = parent_span->GetContext(); + auto child_span = tracer->StartSpan("Test child span", child_span_opts); + + nostd::get<trace_api::SpanContext>(child_span_opts.parent) + .trace_id() + .ToLowerBase16(MakeSpan(trace_id_hex)); + report_trace_id.assign(trace_id_hex, sizeof(trace_id_hex)); + + auto expected_url = nostd::string_view{"http://localhost:9411/api/v2/spans"}; + EXPECT_CALL(*mock_http_client, Post(expected_url, IsValidMessage(report_trace_id), _)) + .Times(Exactly(1)) + .WillOnce(Return(ByMove(std::move(ext::http::client::Result{ + std::unique_ptr<ext::http::client::Response>{new ext::http::client::curl::Response()}, + ext::http::client::SessionState::Response})))); + + child_span->End(); + parent_span->End(); +} + +// Create spans, let processor call Export() +TEST_F(ZipkinExporterTestPeer, ShutdownTest) +{ + auto mock_http_client = new MockHttpClient; + // Leave a comment line here or different version of clang-format has a different result here + auto exporter = GetExporter( + std::shared_ptr<opentelemetry::ext::http::client::HttpClientSync>{mock_http_client}); + auto recordable_1 = exporter->MakeRecordable(); + recordable_1->SetName("Test span 1"); + auto recordable_2 = exporter->MakeRecordable(); + recordable_2->SetName("Test span 2"); + + // exporter shuold not be shutdown by default + nostd::span<std::unique_ptr<sdk::trace::Recordable>> batch_1(&recordable_1, 1); + EXPECT_CALL(*mock_http_client, Post(_, _, _)) + .Times(Exactly(1)) + .WillOnce(Return(ByMove(std::move(ext::http::client::Result{ + std::unique_ptr<ext::http::client::Response>{new ext::http::client::curl::Response()}, + ext::http::client::SessionState::Response})))); + auto result = exporter->Export(batch_1); + EXPECT_EQ(sdk_common::ExportResult::kSuccess, result); + + exporter->Shutdown(); + + nostd::span<std::unique_ptr<sdk::trace::Recordable>> batch_2(&recordable_2, 1); + result = exporter->Export(batch_2); + EXPECT_EQ(sdk_common::ExportResult::kFailure, result); +} + +// Test exporter configuration options +TEST_F(ZipkinExporterTestPeer, ConfigTest) +{ + ZipkinExporterOptions opts; + opts.endpoint = "http://localhost:45455/v1/traces"; + std::unique_ptr<ZipkinExporter> exporter(new ZipkinExporter(opts)); + EXPECT_EQ(GetOptions(exporter).endpoint, "http://localhost:45455/v1/traces"); +} + +# ifndef NO_GETENV +// Test exporter configuration options from env +TEST_F(ZipkinExporterTestPeer, ConfigFromEnv) +{ + const std::string endpoint = "http://localhost:9999/v1/traces"; + setenv("OTEL_EXPORTER_ZIPKIN_ENDPOINT", endpoint.c_str(), 1); + + std::unique_ptr<ZipkinExporter> exporter(new ZipkinExporter()); + EXPECT_EQ(GetOptions(exporter).endpoint, endpoint); + + unsetenv("OTEL_EXPORTER_ZIPKIN_ENDPOINT"); +} + +# endif // NO_GETENV + +} // namespace zipkin +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif // HAVE_CPP_STDLIB diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_recordable_test.cc b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_recordable_test.cc new file mode 100644 index 000000000..967aebaab --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/zipkin/test/zipkin_recordable_test.cc @@ -0,0 +1,285 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#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/common/timestamp.h" +#include "opentelemetry/exporters/zipkin/recordable.h" + +#include <gtest/gtest.h> + +namespace trace = opentelemetry::trace; +namespace nostd = opentelemetry::nostd; +namespace sdktrace = opentelemetry::sdk::trace; +namespace common = opentelemetry::common; +namespace zipkin = opentelemetry::exporter::zipkin; +using json = nlohmann::json; + +// Testing Shutdown functionality of OStreamSpanExporter, should expect no data to be sent to Stream +TEST(ZipkinSpanRecordable, SetIdentity) +{ + json j_span = {{"id", "0000000000000002"}, + {"parentId", "0000000000000003"}, + {"traceId", "00000000000000000000000000000001"}}; + zipkin::Recordable rec; + const trace::TraceId trace_id(std::array<const uint8_t, trace::TraceId::kSize>( + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})); + + const trace::SpanId span_id( + std::array<const uint8_t, trace::SpanId::kSize>({0, 0, 0, 0, 0, 0, 0, 2})); + + const trace::SpanId parent_span_id( + std::array<const uint8_t, trace::SpanId::kSize>({0, 0, 0, 0, 0, 0, 0, 3})); + + const trace::SpanContext span_context{trace_id, span_id, + trace::TraceFlags{trace::TraceFlags::kIsSampled}, true}; + + rec.SetIdentity(span_context, parent_span_id); + EXPECT_EQ(rec.span(), j_span); +} + +// according to https://zipkin.io/zipkin-api/#/ in case root span is created +// the parentId filed should be absent. +TEST(ZipkinSpanRecordable, SetIdentityEmptyParent) +{ + json j_span = {{"id", "0000000000000002"}, {"traceId", "00000000000000000000000000000001"}}; + zipkin::Recordable rec; + const trace::TraceId trace_id(std::array<const uint8_t, trace::TraceId::kSize>( + {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1})); + + const trace::SpanId span_id( + std::array<const uint8_t, trace::SpanId::kSize>({0, 0, 0, 0, 0, 0, 0, 2})); + + const trace::SpanId parent_span_id( + std::array<const uint8_t, trace::SpanId::kSize>({0, 0, 0, 0, 0, 0, 0, 0})); + + const trace::SpanContext span_context{trace_id, span_id, + trace::TraceFlags{trace::TraceFlags::kIsSampled}, true}; + + rec.SetIdentity(span_context, parent_span_id); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetName) +{ + nostd::string_view name = "Test Span"; + json j_span = {{"name", name}}; + zipkin::Recordable rec; + rec.SetName(name); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetStartTime) +{ + zipkin::Recordable rec; + std::chrono::system_clock::time_point start_time = std::chrono::system_clock::now(); + common::SystemTimestamp start_timestamp(start_time); + + uint64_t unix_start = + std::chrono::duration_cast<std::chrono::microseconds>(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) +{ + std::chrono::nanoseconds durationNS(1000000000); // in ns + std::chrono::microseconds durationMS = + std::chrono::duration_cast<std::chrono::microseconds>(durationNS); // in ms + json j_span = {{"duration", durationMS.count()}, {"timestamp", 0}}; + zipkin::Recordable rec; + // Start time is 0 + common::SystemTimestamp start_timestamp; + + rec.SetStartTime(start_timestamp); + rec.SetDuration(durationNS); + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetInstrumentationLibrary) +{ + using InstrumentationLibrary = opentelemetry::sdk::instrumentationlibrary::InstrumentationLibrary; + + const char *library_name = "otel-cpp"; + const char *library_version = "0.5.0"; + json j_span = { + {"tags", {{"otel.library.name", library_name}, {"otel.library.version", library_version}}}}; + zipkin::Recordable rec; + + rec.SetInstrumentationLibrary(*InstrumentationLibrary::Create(library_name, library_version)); + + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetStatus) +{ + std::string description = "Error description"; + std::vector<trace::StatusCode> status_codes = {trace::StatusCode::kError, trace::StatusCode::kOk}; + for (auto &status_code : status_codes) + { + zipkin::Recordable rec; + trace::StatusCode code(status_code); + json j_span; + if (status_code == trace::StatusCode::kError) + j_span = {{"tags", {{"otel.status_code", status_code}, {"error", description}}}}; + else + j_span = {{"tags", {{"otel.status_code", status_code}}}}; + + rec.SetStatus(code, description); + EXPECT_EQ(rec.span(), j_span); + } +} + +TEST(ZipkinSpanRecordable, SetSpanKind) +{ + json j_json_client = {{"kind", "CLIENT"}}; + zipkin::Recordable rec; + rec.SetSpanKind(trace::SpanKind::kClient); + EXPECT_EQ(rec.span(), j_json_client); +} + +TEST(ZipkinSpanRecordable, AddEventDefault) +{ + zipkin::Recordable rec; + nostd::string_view name = "Test Event"; + + std::chrono::system_clock::time_point event_time = std::chrono::system_clock::now(); + common::SystemTimestamp event_timestamp(event_time); + + rec.sdktrace::Recordable::AddEvent(name, event_timestamp); + + uint64_t unix_event_time = + std::chrono::duration_cast<std::chrono::microseconds>(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) +{ + zipkin::Recordable rec; + + std::chrono::system_clock::time_point event_time = std::chrono::system_clock::now(); + common::SystemTimestamp event_timestamp(event_time); + uint64_t unix_event_time = + std::chrono::duration_cast<std::chrono::microseconds>(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<std::string, int> attributes = { + {keys[0], values[0]}, {keys[1], values[1]}, {keys[2], values[2]}}; + + rec.AddEvent("Test Event", event_timestamp, + common::KeyValueIterableView<std::map<std::string, int>>(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) +{ + zipkin::Recordable rec; + nostd::string_view bool_key = "bool_attr"; + common::AttributeValue bool_val(true); + rec.SetAttribute(bool_key, bool_val); + + nostd::string_view double_key = "double_attr"; + common::AttributeValue double_val(3.3); + rec.SetAttribute(double_key, double_val); + + nostd::string_view str_key = "str_attr"; + 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) +{ + 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<const bool> bool_span(bool_arr); + rec.SetAttribute("bool_arr_attr", bool_span); + + double double_arr[kArraySize] = {22.3, 33.4, 44.5}; + nostd::span<const double> double_span(double_arr); + rec.SetAttribute("double_arr_attr", double_span); + + nostd::string_view str_arr[kArraySize] = {"Hello", "World", "Test"}; + nostd::span<const nostd::string_view> str_span(str_arr); + rec.SetAttribute("str_arr_attr", str_span); + + EXPECT_EQ(rec.span(), j_span); +} + +TEST(ZipkinSpanRecordable, SetResource) +{ + zipkin::Recordable rec; + std::string service_name = "test"; + auto resource = opentelemetry::sdk::resource::Resource::Create({{"service.name", service_name}}); + rec.SetResource(resource); + EXPECT_EQ(rec.GetServiceName(), service_name); +} + +/** + * 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 <typename T> +struct ZipkinIntAttributeTest : public testing::Test +{ + using IntParamType = T; +}; + +using IntTypes = testing::Types<int, int64_t, unsigned int, uint64_t>; +TYPED_TEST_SUITE(ZipkinIntAttributeTest, IntTypes); + +TYPED_TEST(ZipkinIntAttributeTest, SetIntSingleAttribute) +{ + using IntType = typename TestFixture::IntParamType; + IntType i = 2; + common::AttributeValue int_val(i); + + 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<const IntType> int_span(int_arr); + + 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); +} |