diff --git a/CHANGELOG.md b/CHANGELOG.md index ebd0e0a29d..216189178d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ Increment the: ## [Unreleased] +* [EXPORTER] Prometheus Exporter ([#1031](https://github.com/open-telemetry/opentelemetry-cpp/pull/1031)) * [EXPORTER] Add OTLP/HTTP Log Exporter ([#1030](https://github.com/open-telemetry/opentelemetry-cpp/pull/1030)) ## [1.0.1] 2021-10-21 diff --git a/CMakeLists.txt b/CMakeLists.txt index b6e2585631..8844a66624 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -299,6 +299,24 @@ if((NOT WITH_API_ONLY) endif() endif() +if(WITH_PROMETHEUS) + find_package(prometheus-cpp CONFIG QUIET) + if(NOT prometheus-cpp_FOUND) + message("Trying to use local prometheus-cpp from submodule") + if(EXISTS ${PROJECT_SOURCE_DIR}/third_party/prometheus-cpp/.git) + add_subdirectory(third_party/prometheus-cpp) + else() + message( + FATAL_ERROR + "\nprometheus-cpp package was not found. Please either provide it manually or clone with submodules. " + "To initialize, fetch and checkout any nested submodules, you can use the following command:\n" + "git submodule update --init --recursive") + endif() + else() + message("Using external prometheus-cpp") + endif() +endif() + if(WITH_OTLP) set(protobuf_MODULE_COMPATIBLE ON) find_package(Protobuf) diff --git a/ci/do_ci.sh b/ci/do_ci.sh index 7dfb2fe983..b70d565b0d 100755 --- a/ci/do_ci.sh +++ b/ci/do_ci.sh @@ -35,7 +35,6 @@ BAZEL_STARTUP_OPTIONS="--output_user_root=$HOME/.cache/bazel" export CTEST_OUTPUT_ON_FAILURE=1 if [[ "$1" == "cmake.test" ]]; then - install_prometheus_cpp_client cd "${BUILD_DIR}" rm -rf * cmake -DCMAKE_BUILD_TYPE=Debug \ diff --git a/exporters/prometheus/BUILD b/exporters/prometheus/BUILD index 01a2fe14f3..f5ac897a3c 100644 --- a/exporters/prometheus/BUILD +++ b/exporters/prometheus/BUILD @@ -15,25 +15,26 @@ package(default_visibility = ["//visibility:public"]) cc_library( - name = "prometheus_collector", + name = "prometheus_exporter", srcs = [ - "src/prometheus_collector.cc", + "src/prometheus_exporter.cc", ], hdrs = [ - "include/opentelemetry/exporters/prometheus/prometheus_collector.h", - "include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h", + "include/opentelemetry/exporters/prometheus/prometheus_exporter.h", ], strip_include_prefix = "include", deps = [ - ":prometheus_utils", + ":prometheus_collector", + ":prometheus_exporter_utils", "//api", "//sdk:headers", "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", ], ) cc_library( - name = "prometheus_utils", + name = "prometheus_exporter_utils", srcs = [ "src/prometheus_exporter_utils.cc", ], @@ -45,27 +46,35 @@ cc_library( "//api", "//sdk:headers", "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", ], ) -cc_test( - name = "prometheus_collector_test", +cc_library( + name = "prometheus_collector", srcs = [ - "test/prometheus_collector_test.cc", + "src/prometheus_collector.cc", ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/prometheus_collector.h", + ], + strip_include_prefix = "include", deps = [ - ":prometheus_collector", - "@com_google_googletest//:gtest_main", + ":prometheus_exporter_utils", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", ], ) cc_test( - name = "prometheus_exporter_utils_test", + name = "prometheus_exporter_test", srcs = [ - "test/prometheus_exporter_utils_test.cc", + "test/prometheus_exporter_test.cc", ], deps = [ - ":prometheus_utils", + ":prometheus_exporter", "@com_google_googletest//:gtest_main", ], ) diff --git a/exporters/prometheus/CMakeLists.txt b/exporters/prometheus/CMakeLists.txt old mode 100644 new mode 100755 index 6d3289243a..3ef8e035ae --- a/exporters/prometheus/CMakeLists.txt +++ b/exporters/prometheus/CMakeLists.txt @@ -13,22 +13,26 @@ # limitations under the License. include_directories(include) -find_package(prometheus-cpp CONFIG REQUIRED) - -add_library(prometheus_exporter_deprecated src/prometheus_collector.cc - src/prometheus_exporter_utils.cc) +if(NOT TARGET prometheus-cpp::core) + find_package(prometheus-cpp CONFIG REQUIRED) +endif() +add_library( + prometheus_exporter_deprecated + src/prometheus_exporter.cc src/prometheus_collector.cc + src/prometheus_exporter_utils.cc) target_include_directories( prometheus_exporter_deprecated PUBLIC "$" "$") +set(PROMETHEUS_EXPORTER_TARGETS prometheus_exporter_deprecated pull core) target_link_libraries( - prometheus_exporter_deprecated PUBLIC opentelemetry_metrics_deprecated - prometheus-cpp::core) - + prometheus_exporter_deprecated + PUBLIC opentelemetry_metrics_deprecated prometheus-cpp::pull + prometheus-cpp::core) install( - TARGETS prometheus_exporter_deprecated + TARGETS ${PROMETHEUS_EXPORTER_TARGETS} EXPORT "${PROJECT_NAME}-target" RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} diff --git a/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h b/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h index 69d7754ad6..aed12e0fc6 100644 --- a/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h +++ b/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h @@ -8,10 +8,10 @@ # include # include +# include "opentelemetry/exporters/prometheus/prometheus_exporter_utils.h" # include "opentelemetry/sdk/_metrics/record.h" # include "prometheus/collectable.h" # include "prometheus/metric_family.h" -# include "prometheus_exporter_utils.h" namespace prometheus_client = ::prometheus; namespace metric_sdk = opentelemetry::sdk::metrics; 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..7c1f99a757 --- /dev/null +++ b/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h @@ -0,0 +1,98 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifdef ENABLE_METRICS_PREVIEW +# include +# include +# include + +# include "opentelemetry/exporters/prometheus/prometheus_collector.h" +# include "opentelemetry/sdk/_metrics/exporter.h" +# include "opentelemetry/sdk/_metrics/record.h" +# include "opentelemetry/version.h" +# include "prometheus/exposer.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::common::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 +#endif // ENABLE_METRICS_PREVIEW diff --git a/exporters/prometheus/src/prometheus_exporter.cc b/exporters/prometheus/src/prometheus_exporter.cc new file mode 100644 index 0000000000..b64af1e904 --- /dev/null +++ b/exporters/prometheus/src/prometheus_exporter.cc @@ -0,0 +1,110 @@ +/* + * 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. + */ + +#ifdef ENABLE_METRICS_PREVIEW +# 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::common::ExportResult PrometheusExporter::Export( + const std::vector &records) noexcept +{ + if (is_shutdown_) + { + return sdk::common::ExportResult::kFailure; + } + else if (records.empty()) + { + return sdk::common::ExportResult::kFailureInvalidArgument; + } + else if (collector_->GetCollection().size() + records.size() > + (size_t)collector_->GetMaxCollectionSize()) + { + return sdk::common::ExportResult::kFailureFull; + } + else + { + collector_->AddMetricData(records); + return sdk::common::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 +#endif // ENABLE_METRICS_PREVIEW diff --git a/exporters/prometheus/test/CMakeLists.txt b/exporters/prometheus/test/CMakeLists.txt index 11f0d2999d..6c45e9299c 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..564880c26d --- /dev/null +++ b/exporters/prometheus/test/prometheus_exporter_test.cc @@ -0,0 +1,218 @@ +/* + * 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. + */ + +#ifdef ENABLE_METRICS_PREVIEW + +# 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::common::ExportResult; +using opentelemetry::sdk::metrics::CounterAggregator; +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); +} + +#endif // ENABLE_METRICS_PREVIEW