diff --git a/exporters/ostream/BUILD b/exporters/ostream/BUILD index e2d7c206ea..d1b5d5bd98 100644 --- a/exporters/ostream/BUILD +++ b/exporters/ostream/BUILD @@ -1,5 +1,28 @@ package(default_visibility = ["//visibility:public"]) +cc_library( + name = "ostream_log_exporter", + srcs = [ + "src/log_exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/ostream/log_exporter.h", + ], + strip_include_prefix = "include", + deps = [ + "//sdk/src/logs", + ], +) + +cc_test( + name = "ostream_log_test", + srcs = ["test/ostream_log_test.cc"], + deps = [ + ":ostream_log_exporter", + "@com_google_googletest//:gtest_main", + ], +) + cc_library( name = "ostream_metrics_exporter", srcs = [ diff --git a/exporters/ostream/CMakeLists.txt b/exporters/ostream/CMakeLists.txt index b6faccf513..e870bb09cf 100644 --- a/exporters/ostream/CMakeLists.txt +++ b/exporters/ostream/CMakeLists.txt @@ -1,11 +1,13 @@ include_directories(include) +add_library(opentelemetry_exporter_ostream_logs src/log_exporter.cc) add_library(opentelemetry_exporter_ostream_metrics src/metrics_exporter.cc) add_library(opentelemetry_exporter_ostream_span src/span_exporter.cc) if(BUILD_TESTING) add_executable(ostream_metrics_test test/ostream_metrics_test.cc) add_executable(ostream_span_test test/ostream_span_test.cc) + add_executable(ostream_log_test test/ostream_log_test.cc) target_link_libraries( ostream_span_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} @@ -15,12 +17,23 @@ if(BUILD_TESTING) ostream_metrics_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} opentelemetry_exporter_ostream_metrics) + target_link_libraries( + ostream_log_test ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} + opentelemetry_exporter_ostream_logs opentelemetry_logs) + + gtest_add_tests( + TARGET ostream_log_test + TEST_PREFIX exporter. + TEST_LIST ostream_log_test) + gtest_add_tests( TARGET ostream_metrics_test TEST_PREFIX exporter. TEST_LIST ostream_metrics_test) + gtest_add_tests( TARGET ostream_span_test TEST_PREFIX exporter. TEST_LIST ostream_span_test) + endif() # BUILD_TESTING diff --git a/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h b/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h new file mode 100644 index 0000000000..47b58dd9ee --- /dev/null +++ b/exporters/ostream/include/opentelemetry/exporters/ostream/log_exporter.h @@ -0,0 +1,67 @@ +/* + * 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/nostd/type_traits.h" +#include "opentelemetry/sdk/logs/exporter.h" +#include "opentelemetry/sdk/logs/log_record.h" +#include "opentelemetry/version.h" + +#include +#include + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace logs +{ +/** + * The OStreamLogExporter exports logs through an ostream (default set to std::cout) + */ +class OStreamLogExporter final : public opentelemetry::sdk::logs::LogExporter +{ +public: + /** + * Create an OStreamLogExporter. This constructor takes in a reference to an ostream that the + * Export() method will send log data into. The default ostream is set to stdout. + */ + explicit OStreamLogExporter(std::ostream &sout = std::cout) noexcept; + + std::unique_ptr MakeRecordable() noexcept override; + + /** + * Exports a span of logs sent from the processor. + */ + opentelemetry::sdk::logs::ExportResult Export( + const opentelemetry::nostd::span> + &records) noexcept override; + + /** + * Marks the OStream Log Exporter as shut down. + */ + bool Shutdown( + std::chrono::microseconds timeout = std::chrono::microseconds::max()) noexcept override; + +private: + // The OStream to send the logs to + std::ostream &sout_; + // Whether this exporter has been shut down + bool is_shutdown_ = false; +}; +} // namespace logs +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/ostream/src/log_exporter.cc b/exporters/ostream/src/log_exporter.cc new file mode 100644 index 0000000000..37458df479 --- /dev/null +++ b/exporters/ostream/src/log_exporter.cc @@ -0,0 +1,189 @@ +/* + * 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/ostream/log_exporter.h" + +#include + +namespace nostd = opentelemetry::nostd; +namespace sdklogs = opentelemetry::sdk::logs; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace logs +{ +/*********************** Helper functions ************************/ + +/* + print_value is used to print out the value of an attribute within a vector. + These values are held in a variant which makes the process of printing them much more + complicated. +*/ + +template +void print_value(const T &item, std::ostream &sout) +{ + sout << item; +} + +template +void print_value(const std::vector &vec, std::ostream &sout) +{ + sout << '['; + size_t i = 1; + size_t sz = vec.size(); + for (auto v : vec) + { + sout << v; + if (i != sz) + sout << ',' << ' '; + i++; + }; + sout << ']'; +} + +// Prior to C++14, generic lambda is not available so fallback to functor. +#if __cplusplus < 201402L + +class OwnedAttributeValueVisitor +{ +public: + OwnedAttributeValueVisitor(std::ostream &sout) : sout_(sout) {} + + template + void operator()(T &&arg) + { + print_value(arg, sout_); + } + +private: + // The OStream to send the logs to + std::ostream &sout_; +}; + +#endif + +void print_value(sdk::common::OwnedAttributeValue &value, std::ostream &sout) +{ +#if __cplusplus < 201402L + nostd::visit(OwnedAttributeValueVisitor(sout), value); +#else + nostd::visit([&sout](auto &&arg) { print_value(arg, sout); }, value); +#endif +} + +void printMap(std::unordered_map map, + std::ostream &sout) +{ + sout << "{"; + size_t size = map.size(); + size_t i = 1; + for (auto kv : map) + { + sout << "{" << kv.first << ": "; + print_value(kv.second, sout); + sout << "}"; + + if (i != size) + sout << ", "; + i++; + } + sout << "}"; +} + +/*********************** Constructor ***********************/ + +OStreamLogExporter::OStreamLogExporter(std::ostream &sout) noexcept : sout_(sout) {} + +/*********************** Exporter methods ***********************/ + +std::unique_ptr OStreamLogExporter::MakeRecordable() noexcept +{ + return std::unique_ptr(new sdklogs::LogRecord()); +} + +sdklogs::ExportResult OStreamLogExporter::Export( + const nostd::span> &records) noexcept +{ + if (is_shutdown_) + { + return sdklogs::ExportResult::kFailure; + } + + for (auto &record : records) + { + // Convert recordable to a LogRecord so that the getters of the LogRecord can be used + auto log_record = + std::unique_ptr(static_cast(record.release())); + + if (log_record == nullptr) + { + // TODO: Log Internal SDK error "recordable data was lost" + continue; + } + + // Convert trace, spanid, traceflags into exportable representation + constexpr int trace_id_len = 32; + constexpr int span_id__len = 16; + constexpr int trace_flags_len = 2; + + char trace_id[trace_id_len] = {0}; + char span_id[span_id__len] = {0}; + char trace_flags[trace_flags_len] = {0}; + + log_record->GetTraceId().ToLowerBase16(trace_id); + log_record->GetSpanId().ToLowerBase16(span_id); + log_record->GetTraceFlags().ToLowerBase16(trace_flags); + + // Print out each field of the log record, noting that severity is separated + // into severity_num and severity_text + sout_ << "{\n" + << " timestamp : " << log_record->GetTimestamp().time_since_epoch().count() << "\n" + << " severity_num : " << static_cast(log_record->GetSeverity()) << "\n" + << " severity_text : " + << opentelemetry::logs::SeverityNumToText[static_cast(log_record->GetSeverity())] + << "\n" + << " name : " << log_record->GetName() << "\n" + << " body : " << log_record->GetBody() << "\n" + << " resource : "; + + printMap(log_record->GetResource(), sout_); + + sout_ << "\n" + << " attributes : "; + + printMap(log_record->GetAttributes(), sout_); + + sout_ << "\n" + << " trace_id : " << std::string(trace_id, trace_id_len) << "\n" + << " span_id : " << std::string(span_id, span_id__len) << "\n" + << " trace_flags : " << std::string(trace_flags, trace_flags_len) << "\n" + << "}\n"; + } + + return sdklogs::ExportResult::kSuccess; +} + +bool OStreamLogExporter::Shutdown(std::chrono::microseconds timeout) noexcept +{ + is_shutdown_ = true; + return true; +} + +} // namespace logs +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/ostream/test/ostream_log_test.cc b/exporters/ostream/test/ostream_log_test.cc new file mode 100644 index 0000000000..e91528b20a --- /dev/null +++ b/exporters/ostream/test/ostream_log_test.cc @@ -0,0 +1,284 @@ +#include "opentelemetry/exporters/ostream/log_exporter.h" +#include "opentelemetry/logs/provider.h" +#include "opentelemetry/sdk/logs/logger_provider.h" +#include "opentelemetry/sdk/logs/simple_log_processor.h" + +#include +#include + +namespace sdklogs = opentelemetry::sdk::logs; +namespace logs_api = opentelemetry::logs; +namespace nostd = opentelemetry::nostd; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace logs +{ + +// Test that when OStream Log exporter is shutdown, no logs should be sent to stream +TEST(OStreamLogExporter, Shutdown) +{ + auto exporter = + std::unique_ptr(new opentelemetry::exporter::logs::OStreamLogExporter); + + // Save cout's original buffer here + std::streambuf *original = std::cout.rdbuf(); + + // Redirect cout to our stringstream buffer + std::stringstream output; + std::cout.rdbuf(output.rdbuf()); + + EXPECT_TRUE(exporter->Shutdown()); + + // After processor/exporter is shutdown, no logs should be sent to stream + auto record = exporter->MakeRecordable(); + record->SetBody("Log record not empty"); + exporter->Export(nostd::span>(&record, 1)); + + // Restore original stringstream buffer + std::cout.rdbuf(original); + + ASSERT_EQ(output.str(), ""); +} + +// ---------------------------------- Print to cout ------------------------- + +// Testing what a default log record that has no values changed will print out +// This function tests MakeRecordable() as well as Export(). +TEST(OstreamLogExporter, DefaultLogRecordToCout) +{ + auto exporter = std::unique_ptr( + new opentelemetry::exporter::logs::OStreamLogExporter(std::cout)); + + // Save cout's original buffer here + std::streambuf *original = std::cout.rdbuf(); + + // Redirect cout to our stringstream buffer + std::stringstream output; + std::cout.rdbuf(output.rdbuf()); + + // Pass a default recordable created by the exporter to be exported + auto log_record = exporter->MakeRecordable(); + exporter->Export(nostd::span>(&log_record, 1)); + + // Restore cout's original stringstream + std::cout.rdbuf(original); + + std::string expectedOutput = + "{\n" + " timestamp : 0\n" + " severity_num : 0\n" + " severity_text : INVALID\n" + " name : \n" + " body : \n" + " resource : {}\n" + " attributes : {}\n" + " trace_id : 00000000000000000000000000000000\n" + " span_id : 0000000000000000\n" + " trace_flags : 00\n" + "}\n"; + + ASSERT_EQ(output.str(), expectedOutput); +} + +// Testing what a log record with only the "timestamp", "severity", "name" and "message" fields set, +// will print out +TEST(OStreamLogExporter, SimpleLogToCout) +{ + // Initialize an Ostream exporter to std::cout + auto exporter = std::unique_ptr( + new opentelemetry::exporter::logs::OStreamLogExporter(std::cout)); + + // Save original stream buffer, then redirect cout to our new stream buffer + std::streambuf *original = std::cout.rdbuf(); + std::stringstream output; + std::cout.rdbuf(output.rdbuf()); + + // Pass a default recordable created by the exporter to be exported + // Create a log record and manually timestamp, severity, name, message + opentelemetry::core::SystemTimestamp now(std::chrono::system_clock::now()); + + auto record = std::unique_ptr(new sdklogs::LogRecord()); + record->SetTimestamp(now); + record->SetSeverity(logs_api::Severity::kTrace); // kTrace has enum value of 1 + record->SetName("Name"); + record->SetBody("Message"); + + // Log a record to cout + exporter->Export(nostd::span>(&record, 1)); + + // Reset cout's original stringstream buffer + std::cout.rdbuf(original); + + std::string expectedOutput = + "{\n" + " timestamp : " + + std::to_string(now.time_since_epoch().count()) + + "\n" + " severity_num : 1\n" + " severity_text : TRACE\n" + " name : Name\n" + " body : Message\n" + " resource : {}\n" + " attributes : {}\n" + " trace_id : 00000000000000000000000000000000\n" + " span_id : 0000000000000000\n" + " trace_flags : 00\n" + "}\n"; + ASSERT_EQ(output.str(), expectedOutput); +} + +// ---------------------------------- Print to cerr -------------------------- + +// Testing what a log record with only the "resource" and "attributes" fields +// (i.e. KeyValueIterable types) set with primitive types, will print out +TEST(OStreamLogExporter, LogWithStringAttributesToCerr) +{ + // Initialize an Ostream exporter to cerr + auto exporter = std::unique_ptr( + new opentelemetry::exporter::logs::OStreamLogExporter(std::cerr)); + + // Save original stream buffer, then redirect cout to our new stream buffer + std::streambuf *original = std::cerr.rdbuf(); + std::stringstream stdcerrOutput; + std::cerr.rdbuf(stdcerrOutput.rdbuf()); + + // Pass a recordable created by the exporter to be exported + auto record = exporter->MakeRecordable(); + + // Set resources for this log record only of type + record->SetResource("key1", "val1"); + + // Set attributes to this log record of type + record->SetAttribute("a", true); + + // Log record to cerr + exporter->Export(nostd::span>(&record, 1)); + + // Reset cerr's original stringstream buffer + std::cerr.rdbuf(original); + + std::string expectedOutput = + "{\n" + " timestamp : 0\n" + " severity_num : 0\n" + " severity_text : INVALID\n" + " name : \n" + " body : \n" + " resource : {{key1: val1}}\n" + " attributes : {{a: 1}}\n" + " trace_id : 00000000000000000000000000000000\n" + " span_id : 0000000000000000\n" + " trace_flags : 00\n" + "}\n"; + ASSERT_EQ(stdcerrOutput.str(), expectedOutput); +} + +// ---------------------------------- Print to clog ------------------------- + +// Testing what a log record with only the "resource", and "attributes" fields +// (i.e. KeyValueIterable types), set with 2D arrays as values, will print out +TEST(OStreamLogExporter, LogWithVariantTypesToClog) +{ + + // Initialize an Ostream exporter to cerr + auto exporter = std::unique_ptr( + new opentelemetry::exporter::logs::OStreamLogExporter(std::clog)); + + // Save original stream buffer, then redirect cout to our new stream buffer + std::streambuf *original = std::clog.rdbuf(); + std::stringstream stdclogOutput; + std::clog.rdbuf(stdclogOutput.rdbuf()); + + // Pass a recordable created by the exporter to be exported + auto record = exporter->MakeRecordable(); + + // Set resources for this log record of only integer types as the value + std::array array1 = {1, 2, 3}; + opentelemetry::nostd::span data1{array1.data(), array1.size()}; + record->SetResource("res1", data1); + + // Set resources for this log record of bool types as the value + // e.g. key/value is a par of type + std::array array = {false, true, false}; + record->SetAttribute("attr1", opentelemetry::nostd::span{array.data(), array.size()}); + + // Log a record to clog + exporter->Export(nostd::span>(&record, 1)); + + // Reset clog's original stringstream buffer + std::clog.rdbuf(original); + + std::string expectedOutput = + "{\n" + " timestamp : 0\n" + " severity_num : 0\n" + " severity_text : INVALID\n" + " name : \n" + " body : \n" + " resource : {{res1: [1, 2, 3]}}\n" + " attributes : {{attr1: [0, 1, 0]}}\n" + " trace_id : 00000000000000000000000000000000\n" + " span_id : 0000000000000000\n" + " trace_flags : 00\n" + "}\n"; + ASSERT_EQ(stdclogOutput.str(), expectedOutput); +} + +// // ---------------------------------- Integration Tests ------------------------- + +// Test using the simple log processor and ostream exporter to cout +// and use the rest of the logging pipeline (Logger, LoggerProvider, Provider) as well +TEST(OStreamLogExporter, IntegrationTest) +{ + // Initialize a logger + auto exporter = + std::unique_ptr(new opentelemetry::exporter::logs::OStreamLogExporter); + auto processor = + std::shared_ptr(new sdklogs::SimpleLogProcessor(std::move(exporter))); + auto sdkProvider = std::shared_ptr(new sdklogs::LoggerProvider()); + sdkProvider->SetProcessor(processor); + auto apiProvider = nostd::shared_ptr(sdkProvider); + auto provider = nostd::shared_ptr(apiProvider); + logs_api::Provider::SetLoggerProvider(provider); + auto logger = logs_api::Provider::GetLoggerProvider()->GetLogger("Logger"); + + // Back up cout's streambuf + std::streambuf *original = std::cout.rdbuf(); + + // Redirect cout to our string stream + std::stringstream stdcoutOutput; + std::cout.rdbuf(stdcoutOutput.rdbuf()); + + // Write a log to ostream exporter + opentelemetry::core::SystemTimestamp now(std::chrono::system_clock::now()); + logger->Log(opentelemetry::logs::Severity::kDebug, "Hello", now); + + // Restore cout's original streambuf + std::cout.rdbuf(original); + + // Compare actual vs expected outputs + std::string expectedOutput = + "{\n" + " timestamp : " + + std::to_string(now.time_since_epoch().count()) + + "\n" + " severity_num : 5\n" + " severity_text : DEBUG\n" + " name : \n" + " body : Hello\n" + " resource : {}\n" + " attributes : {}\n" + " trace_id : 00000000000000000000000000000000\n" + " span_id : 0000000000000000\n" + " trace_flags : 00\n" + "}\n"; + "}\n"; + + ASSERT_EQ(stdcoutOutput.str(), expectedOutput); +} + +} // namespace logs +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE