diff --git a/.bazelrc b/.bazelrc index 58140be3c9..e893a2e2fa 100644 --- a/.bazelrc +++ b/.bazelrc @@ -1,6 +1,9 @@ # bazel configurations for running tests under sanitizers. # Based on https://github.com/bazelment/trunk/blob/master/tools/bazel.rc +# Needed by gRPC to build on some platforms. +build --copt -DGRPC_BAZEL_BUILD + # --config=asan : Address Sanitizer. common:asan --copt -fsanitize=address common:asan --copt -DADDRESS_SANITIZER diff --git a/WORKSPACE b/WORKSPACE index 1dd2ea37fc..84ab876bc7 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -16,6 +16,30 @@ workspace(name = "io_opentelemetry_cpp") load("@bazel_tools//tools/build_defs/repo:http.bzl", "http_archive") +# Load gRPC dependency +# Note that this dependency needs to be loaded first due to +# https://github.com/bazelbuild/bazel/issues/6664 +http_archive( + name = "com_github_grpc_grpc", + strip_prefix = "grpc-1.28.0", + urls = [ + "https://github.com/grpc/grpc/archive/v1.28.0.tar.gz", + ], +) + +load("@com_github_grpc_grpc//bazel:grpc_deps.bzl", "grpc_deps") + +grpc_deps() + +# Load extra gRPC dependencies due to https://github.com/grpc/grpc/issues/20511 +load("@com_github_grpc_grpc//bazel:grpc_extra_deps.bzl", "grpc_extra_deps") + +grpc_extra_deps() + +load("@upb//bazel:repository_defs.bzl", "bazel_version_repository") + +bazel_version_repository(name = "upb_bazel_version") + # Uses older protobuf version because of # https://github.com/protocolbuffers/protobuf/issues/7179 http_archive( diff --git a/bazel/opentelemetry_proto.BUILD b/bazel/opentelemetry_proto.BUILD index ddb9915bf2..9c91edc2d0 100644 --- a/bazel/opentelemetry_proto.BUILD +++ b/bazel/opentelemetry_proto.BUILD @@ -15,6 +15,7 @@ package(default_visibility = ["//visibility:public"]) load("@rules_proto//proto:defs.bzl", "proto_library") +load("@com_github_grpc_grpc//bazel:cc_grpc_library.bzl", "cc_grpc_library") proto_library( name = "common_proto", @@ -58,3 +59,26 @@ cc_proto_library( name = "trace_proto_cc", deps = [":trace_proto"], ) + +proto_library( + name = "trace_service_proto", + srcs = [ + "opentelemetry/proto/collector/trace/v1/trace_service.proto", + ], + deps = [ + ":trace_proto", + ], +) + +cc_proto_library( + name = "trace_service_proto_cc", + deps = [":trace_service_proto"], +) + +cc_grpc_library( + name = "trace_service_grpc_cc", + srcs = [":trace_service_proto"], + grpc_only = True, + deps = [":trace_service_proto_cc"], + generate_mocks = True, +) diff --git a/exporters/otlp/BUILD b/exporters/otlp/BUILD index fd1f048889..e8c4a8ae7f 100644 --- a/exporters/otlp/BUILD +++ b/exporters/otlp/BUILD @@ -29,6 +29,24 @@ cc_library( ], ) +cc_library( + name = "otlp_exporter", + srcs = [ + 'otlp_exporter.cc', + ], + hdrs = [ + 'otlp_exporter.h', + ], + deps = [ + ":recordable", + "//sdk/src/trace", + + # For gRPC + "@com_github_opentelemetry_proto//:trace_service_grpc_cc", + "@com_github_grpc_grpc//:grpc++", + ], +) + cc_test( name = "recordable_test", srcs = ["recordable_test.cc"], @@ -37,3 +55,13 @@ cc_test( "@com_google_googletest//:gtest_main", ], ) + +cc_test( + name = "otlp_exporter_test", + srcs = ["otlp_exporter_test.cc"], + deps = [ + ":otlp_exporter", + "//api", + "@com_google_googletest//:gtest_main", + ], +) diff --git a/exporters/otlp/otlp_exporter.cc b/exporters/otlp/otlp_exporter.cc new file mode 100644 index 0000000000..951597044a --- /dev/null +++ b/exporters/otlp/otlp_exporter.cc @@ -0,0 +1,81 @@ +#include "otlp_exporter.h" +#include "recordable.h" + +#include +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +const std::string kCollectorAddress = "localhost:55678"; + +// ----------------------------- Helper functions ------------------------------ + +/** + * Add span protobufs contained in recordables to request. + * @param spans the spans to export + * @param request the current request + */ +void PopulateRequest(const nostd::span> &spans, + proto::collector::trace::v1::ExportTraceServiceRequest *request) +{ + auto resource_span = request->add_resource_spans(); + auto instrumentation_lib = resource_span->add_instrumentation_library_spans(); + + for (auto &recordable : spans) + { + auto rec = std::unique_ptr(static_cast(recordable.release())); + *instrumentation_lib->add_spans() = std::move(rec->span()); + } +} + +/** + * Create service stub to communicate with the OpenTelemetry Collector. + */ +std::unique_ptr MakeServiceStub() +{ + auto channel = grpc::CreateChannel(kCollectorAddress, grpc::InsecureChannelCredentials()); + return proto::collector::trace::v1::TraceService::NewStub(channel); +} + +// -------------------------------- Contructors -------------------------------- + +OtlpExporter::OtlpExporter() : OtlpExporter(MakeServiceStub()) {} + +OtlpExporter::OtlpExporter( + std::unique_ptr stub) + : trace_service_stub_(std::move(stub)) +{} + +// ----------------------------- Exporter methods ------------------------------ + +std::unique_ptr OtlpExporter::MakeRecordable() noexcept +{ + return std::unique_ptr(new Recordable); +} + +sdk::trace::ExportResult OtlpExporter::Export( + const nostd::span> &spans) noexcept +{ + proto::collector::trace::v1::ExportTraceServiceRequest request; + + PopulateRequest(spans, &request); + + grpc::ClientContext context; + proto::collector::trace::v1::ExportTraceServiceResponse response; + + grpc::Status status = trace_service_stub_->Export(&context, request, &response); + + if (!status.ok()) + { + std::cerr << "[OTLP Exporter] Export() failed: " << status.error_message() << "\n"; + return sdk::trace::ExportResult::kFailure; + } + return sdk::trace::ExportResult::kSuccess; +} +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/otlp_exporter.h b/exporters/otlp/otlp_exporter.h new file mode 100644 index 0000000000..4ac324f3ca --- /dev/null +++ b/exporters/otlp/otlp_exporter.h @@ -0,0 +1,59 @@ +#pragma once + +#include "opentelemetry/sdk/trace/exporter.h" +#include "opentelemetry/proto/collector/trace/v1/trace_service.grpc.pb.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ +/** + * The OTLP exporter exports span data in OpenTelemetry Protocol (OTLP) format. + */ +class OtlpExporter final : public opentelemetry::sdk::trace::SpanExporter +{ +public: + /** + * Create an OtlpExporter. This constructor initializes a service stub to be + * used for exporting. + */ + OtlpExporter(); + + /** + * Create a span recordable. + * @return a newly initialized Recordable object + */ + std::unique_ptr MakeRecordable() noexcept override; + + /** + * Export a batch of span recordables in OTLP format. + * @param spans a span of unique pointers to span recordables + */ + sdk::trace::ExportResult Export( + const nostd::span> &spans) noexcept override; + + /** + * Shut down the exporter. + * @param timeout an optional timeout, the default timeout of 0 means that no + * timeout is applied. + */ + void Shutdown(std::chrono::microseconds timeout = std::chrono::microseconds(0)) noexcept override {}; + +private: + // For testing + friend class OtlpExporterTestPeer; + + // Store service stub internally. Useful for testing. + std::unique_ptr trace_service_stub_; + + /** + * Create an OtlpExporter using the specified service stub. + * Only tests can call this constructor directly. + * @param stub the service stub to be used for exporting + */ + OtlpExporter(std::unique_ptr stub); +}; +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/otlp/otlp_exporter_test.cc b/exporters/otlp/otlp_exporter_test.cc new file mode 100644 index 0000000000..ea98357054 --- /dev/null +++ b/exporters/otlp/otlp_exporter_test.cc @@ -0,0 +1,81 @@ +#include "otlp_exporter.h" +#include "opentelemetry/proto/collector/trace/v1/trace_service_mock.grpc.pb.h" +#include "opentelemetry/sdk/trace/simple_processor.h" +#include "opentelemetry/sdk/trace/tracer_provider.h" +#include "opentelemetry/trace/provider.h" + +#include + +using namespace testing; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace otlp +{ + +class OtlpExporterTestPeer : public ::testing::Test +{ +public: + std::unique_ptr GetExporter( + std::unique_ptr &stub_interface) + { + return std::unique_ptr(new OtlpExporter(std::move(stub_interface))); + } +}; + +// Call Export() directly +TEST_F(OtlpExporterTestPeer, ExportUnitTest) +{ + auto mock_stub = new proto::collector::trace::v1::MockTraceServiceStub(); + std::unique_ptr stub_interface( + mock_stub); + auto exporter = GetExporter(stub_interface); + + auto recordable_1 = exporter->MakeRecordable(); + recordable_1->SetName("Test span 1"); + auto recordable_2 = exporter->MakeRecordable(); + recordable_2->SetName("Test span 2"); + + // Test successful RPC + nostd::span> batch_1(&recordable_1, 1); + EXPECT_CALL(*mock_stub, Export(_, _, _)).Times(Exactly(1)).WillOnce(Return(grpc::Status::OK)); + auto result = exporter->Export(batch_1); + EXPECT_EQ(sdk::trace::ExportResult::kSuccess, result); + + // Test failed RPC + nostd::span> batch_2(&recordable_2, 1); + EXPECT_CALL(*mock_stub, Export(_, _, _)) + .Times(Exactly(1)) + .WillOnce(Return(grpc::Status::CANCELLED)); + result = exporter->Export(batch_2); + EXPECT_EQ(sdk::trace::ExportResult::kFailure, result); +} + +// Create spans, let processor call Export() +TEST_F(OtlpExporterTestPeer, ExportIntegrationTest) +{ + auto mock_stub = new proto::collector::trace::v1::MockTraceServiceStub(); + std::unique_ptr stub_interface( + mock_stub); + + auto exporter = GetExporter(stub_interface); + + auto processor = std::shared_ptr( + new sdk::trace::SimpleSpanProcessor(std::move(exporter))); + auto provider = + nostd::shared_ptr(new sdk::trace::TracerProvider(processor)); + auto tracer = provider->GetTracer("test"); + + EXPECT_CALL(*mock_stub, Export(_, _, _)) + .Times(AtLeast(1)) + .WillRepeatedly(Return(grpc::Status::OK)); + + auto parent_span = tracer->StartSpan("Test parent span"); + auto child_span = tracer->StartSpan("Test child span"); + child_span->End(); + parent_span->End(); +} +} // namespace otlp +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE