diff --git a/ci/do_ci.sh b/ci/do_ci.sh index e97686b0b7..4d68b22125 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -63,6 +63,7 @@ elif [[ "$1" == "cmake.exporter.prometheus.test" ]]; then cmake -DCMAKE_BUILD_TYPE=Debug \ -DWITH_PROMETHEUS=ON \ -DCMAKE_CXX_FLAGS="-Werror" \ + -DCMAKE_EXE_LINKER_FLAGS="-lpthread" \ "${SRC_DIR}" make make test diff --git a/exporters/prometheus/BUILD b/exporters/prometheus/BUILD index 01a2fe14f3..238c62a6de 100644 --- a/exporters/prometheus/BUILD +++ b/exporters/prometheus/BUILD @@ -14,6 +14,26 @@ package(default_visibility = ["//visibility:public"]) +cc_library( + name = "prometheus_exporter", + srcs = [ + "src/prometheus_exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/prometheus_collector.h", + "include/opentelemetry/exporters/prometheus/prometheus_exporter.h", + "include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h", + ], + strip_include_prefix = "include", + deps = [ + ":prometheus_collector", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + cc_library( name = "prometheus_collector", srcs = [ @@ -48,6 +68,17 @@ cc_library( ], ) +cc_test( + name = "prometheus_exporter_test", + srcs = [ + "test/prometheus_exporter_test.cc", + ], + deps = [ + ":prometheus_exporter", + "@com_google_googletest//:gtest_main", + ], +) + cc_test( name = "prometheus_collector_test", srcs = [ diff --git a/exporters/prometheus/CMakeLists.txt b/exporters/prometheus/CMakeLists.txt index 1a58b175fb..337527de12 100644 --- a/exporters/prometheus/CMakeLists.txt +++ b/exporters/prometheus/CMakeLists.txt @@ -16,8 +16,9 @@ include_directories(include) find_package(prometheus-cpp CONFIG REQUIRED) -add_library(prometheus_exporter src/prometheus_collector.cc - src/prometheus_exporter_utils.cc) +add_library( + prometheus_exporter src/prometheus_exporter.cc src/prometheus_collector.cc + src/prometheus_exporter_utils.cc) if(BUILD_TESTING) add_subdirectory(test) diff --git a/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h b/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h new file mode 100644 index 0000000000..82e6be0d6a --- /dev/null +++ b/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h @@ -0,0 +1,109 @@ +/* + * 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 +#include +#include + +#include "opentelemetry/sdk/metrics/exporter.h" +#include "opentelemetry/sdk/metrics/record.h" +#include "opentelemetry/version.h" +#include "prometheus/exposer.h" +#include "prometheus_collector.h" + +/** + * This class is an implementation of the MetricsExporter interface and + * exports Prometheus metrics data. Functions in this class should be + * called by the Controller in our data pipeline. + */ + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +class PrometheusExporter : public sdk::metrics::MetricsExporter +{ +public: + /** + * Constructor - binds an exposer and collector to the exporter + * @param address: an address for an exposer that exposes + * an HTTP endpoint for the exporter to connect to + */ + PrometheusExporter(std::string &address); + + /** + * Exports a batch of Metric Records. + * @param records: a collection of records to export + * @return: returns a ReturnCode detailing a success, or type of failure + */ + sdk::metrics::ExportResult Export( + const std::vector &records) noexcept override; + + /** + * Shuts down the exporter and does cleanup. + * Since Prometheus is a pull based interface, + * we cannot serve data remaining in the intermediate + * collection to to client an HTTP request being sent, + * so we flush the data. + */ + void Shutdown() noexcept; + + /** + * @return: returns a shared_ptr to + * the PrometheusCollector instance + */ + std::shared_ptr &GetCollector(); + + /** + * @return: Gets the shutdown status of the exporter + */ + bool IsShutdown() const; + +private: + /** + * exporter shutdown status + */ + bool is_shutdown_; + + /** + * Pointer to a + * PrometheusCollector instance + */ + std::shared_ptr collector_; + + /** + * Pointer to an + * Exposer instance + */ + std::unique_ptr<::prometheus::Exposer> exposer_; + + /** + * friend class for testing + */ + friend class PrometheusExporterTest; + + /** + * PrometheusExporter constructor with no parameters + * Used for testing only + */ + PrometheusExporter(); +}; +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/prometheus/src/prometheus_exporter.cc b/exporters/prometheus/src/prometheus_exporter.cc new file mode 100644 index 0000000000..32014fb98d --- /dev/null +++ b/exporters/prometheus/src/prometheus_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/prometheus/prometheus_exporter.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +/** + * Constructor - binds an exposer and collector to the exporter + * @param address: an address for an exposer that exposes + * an HTTP endpoint for the exporter to connect to + */ +PrometheusExporter::PrometheusExporter(std::string &address) : is_shutdown_(false) +{ + exposer_ = std::unique_ptr<::prometheus::Exposer>(new ::prometheus::Exposer{address}); + collector_ = std::shared_ptr(new PrometheusCollector); + + exposer_->RegisterCollectable(collector_); +} + +/** + * PrometheusExporter constructor with no parameters + * Used for testing only + */ +PrometheusExporter::PrometheusExporter() : is_shutdown_(false) +{ + collector_ = std::unique_ptr(new PrometheusCollector); +} + +/** + * Exports a batch of Metric Records. + * @param records: a collection of records to export + * @return: returns a ReturnCode detailing a success, or type of failure + */ +sdk::metrics::ExportResult PrometheusExporter::Export( + const std::vector &records) noexcept +{ + if (is_shutdown_) + { + return sdk::metrics::ExportResult::kFailure; + } + else if (records.empty()) + { + return sdk::metrics::ExportResult::kFailureInvalidArgument; + } + else if (collector_->GetCollection().size() + records.size() > collector_->GetMaxCollectionSize()) + { + return sdk::metrics::ExportResult::kFailureFull; + } + else + { + collector_->AddMetricData(records); + return sdk::metrics::ExportResult::kSuccess; + } +} + +/** + * Shuts down the exporter and does cleanup. + * Since Prometheus is a pull based interface, + * we cannot serve data remaining in the intermediate + * collection to to client an HTTP request being sent, + * so we flush the data. + */ +void PrometheusExporter::Shutdown() noexcept +{ + is_shutdown_ = true; + + collector_->GetCollection().clear(); +} + +/** + * @return: returns a shared_ptr to + * the PrometheusCollector instance + */ +std::shared_ptr &PrometheusExporter::GetCollector() +{ + return collector_; +} + +/** + * @return: Gets the shutdown status of the exporter + */ +bool PrometheusExporter::IsShutdown() const +{ + return is_shutdown_; +} + +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE diff --git a/exporters/prometheus/test/CMakeLists.txt b/exporters/prometheus/test/CMakeLists.txt index c57f900347..a0280b4e15 100644 --- a/exporters/prometheus/test/CMakeLists.txt +++ b/exporters/prometheus/test/CMakeLists.txt @@ -1,4 +1,5 @@ -foreach(testname prometheus_collector_test prometheus_exporter_utils_test) +foreach(testname prometheus_exporter_test prometheus_collector_test + prometheus_exporter_utils_test) add_executable(${testname} "${testname}.cc") target_link_libraries( ${testname} ${GTEST_BOTH_LIBRARIES} ${CMAKE_THREAD_LIBS_INIT} diff --git a/exporters/prometheus/test/prometheus_exporter_test.cc b/exporters/prometheus/test/prometheus_exporter_test.cc new file mode 100644 index 0000000000..f2e1626603 --- /dev/null +++ b/exporters/prometheus/test/prometheus_exporter_test.cc @@ -0,0 +1,214 @@ +/* + * 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 +#include + +#include "opentelemetry/exporters/prometheus/prometheus_collector.h" +#include "opentelemetry/exporters/prometheus/prometheus_exporter.h" +#include "opentelemetry/sdk/metrics/aggregator/counter_aggregator.h" +#include "opentelemetry/version.h" + +/** + * PrometheusExporterTest is a friend class of PrometheusExporter. + * It has access to a private constructor that does not take in + * an exposer as an argument, and instead takes no arguments; this + * private constructor is only to be used here for testing + */ +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +class PrometheusExporterTest // : public ::testing::Test +{ +public: + PrometheusExporter GetExporter() { return PrometheusExporter(); } +}; +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE + +using opentelemetry::exporter::prometheus::PrometheusCollector; +using opentelemetry::exporter::prometheus::PrometheusExporter; +using opentelemetry::exporter::prometheus::PrometheusExporterTest; +using opentelemetry::sdk::metrics::CounterAggregator; +using opentelemetry::sdk::metrics::ExportResult; +using opentelemetry::sdk::metrics::Record; + +/** + * Helper function to create a collection of records taken from + * a counter aggregator + */ +std::vector CreateRecords(int num) +{ + + std::vector records; + + for (int i = 0; i < num; i++) + { + std::string name = "record-" + std::to_string(i); + std::string description = "record-" + std::to_string(i); + std::string labels = "record-" + std::to_string(i) + "-label-1.0"; + auto aggregator = std::shared_ptr>( + new opentelemetry::sdk::metrics::CounterAggregator( + opentelemetry::metrics::InstrumentKind::Counter)); + aggregator->update(10); + aggregator->checkpoint(); + + Record r{name, description, labels, aggregator}; + records.push_back(r); + } + return records; +} + +/** + * When a PrometheusExporter is initialized, + * isShutdown should be false. + */ +TEST(PrometheusExporter, InitializeConstructorIsNotShutdown) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + // Asserts that the exporter is not shutdown. + ASSERT_TRUE(!exporter.IsShutdown()); +} + +/** + * The shutdown() function should set the isShutdown field to true. + */ +TEST(PrometheusExporter, ShutdownSetsIsShutdownToTrue) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + // exporter shuold not be shutdown by default + ASSERT_TRUE(!exporter.IsShutdown()); + + exporter.Shutdown(); + + // the exporter shuold be shutdown + ASSERT_TRUE(exporter.IsShutdown()); + + // shutdown function should be idempotent + exporter.Shutdown(); + ASSERT_TRUE(exporter.IsShutdown()); +} + +/** + * The Export() function should return kSuccess = 0 + * when data is exported successfully. + */ +TEST(PrometheusExporter, ExportSuccessfully) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + int num_records = 2; + + std::vector records = CreateRecords(num_records); + + auto res = exporter.Export(records); + + // result should be kSuccess = 0 + ExportResult code = ExportResult::kSuccess; + ASSERT_EQ(res, code); +} + +/** + * If the exporter is shutdown, it cannot process + * any more export requests and returns kFailure = 1. + */ +TEST(PrometheusExporter, ExporterIsShutdown) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + int num_records = 1; + + std::vector records = CreateRecords(num_records); + + exporter.Shutdown(); + + // send export request after shutdown + auto res = exporter.Export(records); + + // result code should be kFailure = 1 + ExportResult code = ExportResult::kFailure; + ASSERT_EQ(res, code); +} + +/** + * The Export() function should return + * kFailureFull = 2 when the collection is full, + * or when the collection is not full but does not have enough + * space to hold the batch data. + */ +TEST(PrometheusExporter, CollectionNotEnoughSpace) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + int num_records = 2; + + // prepare two collections of records to export, + // one close to max size and another one that, when added + // to the first, will exceed the size of the collection + + int max_collection_size = exporter.GetCollector()->GetMaxCollectionSize(); + + std::vector full_records = CreateRecords(max_collection_size - 1); + std::vector records = CreateRecords(num_records); + + // send export request to fill the + // collection in the collector + auto res = exporter.Export(full_records); + + // the result code should be kSuccess = 0 + ExportResult code = ExportResult::kSuccess; + ASSERT_EQ(res, code); + + // send export request that does not complete + // due to not enough space in the collection + res = exporter.Export(records); + + // the result code should be kFailureFull = 2 + code = ExportResult::kFailureFull; + ASSERT_EQ(res, code); +} + +/** + * The Export() function should return + * kFailureInvalidArgument = 3 when an empty collection + * of records is passed to the Export() function. + */ +TEST(PrometheusExporter, InvalidArgumentWhenPassedEmptyRecordCollection) +{ + PrometheusExporterTest p; + PrometheusExporter exporter = p.GetExporter(); + + // Initializes an empty colelction of records + std::vector records; + + // send export request to fill the + // collection in the collector + auto res = exporter.Export(records); + + // the result code should be kFailureInvalidArgument = 3 + ExportResult code = ExportResult::kFailureInvalidArgument; + ASSERT_EQ(res, code); +}