diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:54:28 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-21 11:54:28 +0000 |
commit | e6918187568dbd01842d8d1d2c808ce16a894239 (patch) | |
tree | 64f88b554b444a49f656b6c656111a145cbbaa28 /src/jaegertracing/opentelemetry-cpp/exporters/prometheus | |
parent | Initial commit. (diff) | |
download | ceph-e6918187568dbd01842d8d1d2c808ce16a894239.tar.xz ceph-e6918187568dbd01842d8d1d2c808ce16a894239.zip |
Adding upstream version 18.2.2.upstream/18.2.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/jaegertracing/opentelemetry-cpp/exporters/prometheus')
18 files changed, 3591 insertions, 0 deletions
diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/BUILD b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/BUILD new file mode 100644 index 000000000..47c9bbab1 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/BUILD @@ -0,0 +1,144 @@ +# Copyright 2020, 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. + +package(default_visibility = ["//visibility:public"]) + +cc_library( + name = "prometheus_exporter_deprecated", + srcs = [ + "src/prometheus_exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/prometheus_exporter.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + ":prometheus_collector_deprecated", + ":prometheus_exporter_utils_deprecated", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + +cc_library( + name = "prometheus_exporter_utils_deprecated", + srcs = [ + "src/prometheus_exporter_utils.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + +cc_library( + name = "prometheus_collector_deprecated", + srcs = [ + "src/prometheus_collector.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/prometheus_collector.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + ":prometheus_exporter_utils_deprecated", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + +cc_test( + name = "prometheus_exporter_test_deprecated", + srcs = [ + "test/prometheus_exporter_test.cc", + ], + tags = [ + "prometheus", + "test", + ], + deps = [ + ":prometheus_exporter_deprecated", + "@com_google_googletest//:gtest_main", + ], +) + +cc_library( + name = "prometheus_exporter", + srcs = [ + "src/exporter.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/exporter.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + ":prometheus_collector", + ":prometheus_exporter_utils", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + +cc_library( + name = "prometheus_exporter_utils", + srcs = [ + "src/exporter_utils.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/exporter_utils.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) + +cc_library( + name = "prometheus_collector", + srcs = [ + "src/collector.cc", + ], + hdrs = [ + "include/opentelemetry/exporters/prometheus/collector.h", + ], + strip_include_prefix = "include", + tags = ["prometheus"], + deps = [ + ":prometheus_exporter_utils", + "//api", + "//sdk:headers", + "@com_github_jupp0r_prometheus_cpp//core", + "@com_github_jupp0r_prometheus_cpp//pull", + ], +) diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/CMakeLists.txt b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/CMakeLists.txt new file mode 100755 index 000000000..56523ec84 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/CMakeLists.txt @@ -0,0 +1,90 @@ +# Copyright 2020, 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) +if(NOT TARGET prometheus-cpp::core) + find_package(prometheus-cpp CONFIG REQUIRED) +endif() +if(WITH_METRICS_PREVIEW) + 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 "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>" + "$<INSTALL_INTERFACE:include>") + + set(PROMETHEUS_EXPORTER_TARGETS_DEPRECATED prometheus_exporter_deprecated) + if(TARGET pull) + list(APPEND PROMETHEUS_EXPORTER_TARGETS_DEPRECATED pull) + endif() + if(TARGET core) + list(APPEND PROMETHEUS_EXPORTER_TARGETS_DEPRECATED core) + endif() + target_link_libraries( + prometheus_exporter_deprecated + PUBLIC opentelemetry_metrics_deprecated prometheus-cpp::pull + prometheus-cpp::core) + install( + TARGETS ${PROMETHEUS_EXPORTER_TARGETS_DEPRECATED} + EXPORT "${PROJECT_NAME}-target" + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + + install( + DIRECTORY include/opentelemetry/exporters/prometheus + DESTINATION include/opentelemetry/exporters/ + FILES_MATCHING + PATTERN "*.h") + if(BUILD_TESTING) + add_subdirectory(test) + endif() +else() + + add_library(prometheus_exporter src/exporter.cc src/collector.cc + src/exporter_utils.cc) + target_include_directories( + prometheus_exporter + PUBLIC "$<BUILD_INTERFACE:${CMAKE_CURRENT_LIST_DIR}/include>" + "$<INSTALL_INTERFACE:include>") + + set(PROMETHEUS_EXPORTER_TARGETS prometheus_exporter) + if(TARGET pull) + list(APPEND PROMETHEUS_EXPORTER_TARGETS pull) + endif() + if(TARGET core) + list(APPEND PROMETHEUS_EXPORTER_TARGETS core) + endif() + target_link_libraries( + prometheus_exporter PUBLIC opentelemetry_metrics prometheus-cpp::pull + prometheus-cpp::core) + install( + TARGETS ${PROMETHEUS_EXPORTER_TARGETS} + EXPORT "${PROJECT_NAME}-target" + RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR} + LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR} + ARCHIVE DESTINATION ${CMAKE_INSTALL_LIBDIR}) + + install( + DIRECTORY include/opentelemetry/exporters/prometheus + DESTINATION include/opentelemetry/exporters/ + FILES_MATCHING + PATTERN "*.h") + + if(BUILD_TESTING) + add_subdirectory(test) + endif() +endif() diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/collector.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/collector.h new file mode 100644 index 000000000..68f50d29f --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/collector.h @@ -0,0 +1,87 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifndef ENABLE_METRICS_PREVIEW + +# include <memory> +# include <mutex> +# include <vector> + +# include <prometheus/collectable.h> +# include <prometheus/metric_family.h> +# include "opentelemetry/exporters/prometheus/exporter_utils.h" + +namespace prometheus_client = ::prometheus; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace metrics +{ +/** + * The Prometheus Collector maintains the intermediate collection in Prometheus Exporter + */ +class PrometheusCollector : public prometheus_client::Collectable +{ +public: + /** + * Default Constructor. + * + * This constructor initializes the collection for metrics to export + * in this class with default capacity + */ + explicit PrometheusCollector(size_t max_collection_size = 2048); + + /** + * Collects all metrics data from metricsToCollect collection. + * + * @return all metrics in the metricsToCollect snapshot + */ + std::vector<prometheus_client::MetricFamily> Collect() const override; + + /** + * This function is called by export() function and add the collection of + * records to the metricsToCollect collection + * + * @param records a collection of records to add to the metricsToCollect collection + */ + void AddMetricData(const sdk::metrics::ResourceMetrics &data); + + /** + * Get the current collection in the collector. + * + * @return the current metricsToCollect collection + */ + std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> &GetCollection(); + + /** + * Gets the maximum size of the collection. + * + * @return max collection size + */ + int GetMaxCollectionSize() const; + +private: + /** + * Collection of metrics data from the export() function, and to be export + * to user when they send a pull request. This collection is a pointer + * to a collection so Collect() is able to clear the collection, even + * though it is a const function. + */ + mutable std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> metrics_to_collect_; + + /** + * Maximum size of the metricsToCollect collection. + */ + size_t max_collection_size_; + + /* + * Lock when operating the metricsToCollect collection + */ + mutable std::mutex collection_lock_; +}; +} // namespace metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter.h new file mode 100644 index 000000000..59ef1a11a --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter.h @@ -0,0 +1,126 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifndef ENABLE_METRICS_PREVIEW +# include <memory> +# include <string> +# include <vector> + +# include <prometheus/exposer.h> +# include "opentelemetry/common/spin_lock_mutex.h" +# include "opentelemetry/exporters/prometheus/collector.h" +# include "opentelemetry/nostd/span.h" +# include "opentelemetry/sdk/common/env_variables.h" +# include "opentelemetry/sdk/metrics/metric_exporter.h" +# include "opentelemetry/version.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 metrics +{ + +inline const std::string GetPrometheusDefaultHttpEndpoint() +{ + constexpr char kPrometheusEndpointEnv[] = "PROMETHEUS_EXPORTER_ENDPOINT"; + constexpr char kPrometheusEndpointDefault[] = "localhost:9464"; + + auto endpoint = opentelemetry::sdk::common::GetEnvironmentVariable(kPrometheusEndpointEnv); + return endpoint.size() ? endpoint : kPrometheusEndpointDefault; +} + +/** + * Struct to hold Prometheus exporter options. + */ +struct PrometheusExporterOptions +{ + // The endpoint the Prometheus backend can collect metrics from + std::string url = GetPrometheusDefaultHttpEndpoint(); +}; + +class PrometheusExporter : public sdk::metrics::MetricExporter +{ +public: + /** + * Constructor - binds an exposer and collector to the exporter + * @param options: options for an exposer that exposes + * an HTTP endpoint for the exporter to connect to + */ + PrometheusExporter(const PrometheusExporterOptions &options); + + /** + * 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 sdk::metrics::ResourceMetrics &data) noexcept override; + + /** + * Force flush the exporter. + */ + bool ForceFlush( + std::chrono::microseconds timeout = (std::chrono::microseconds::max)()) 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. + */ + bool Shutdown(std::chrono::microseconds timeout = std::chrono::microseconds(0)) noexcept override; + + /** + * @return: returns a shared_ptr to + * the PrometheusCollector instance + */ + std::shared_ptr<PrometheusCollector> &GetCollector(); + + /** + * @return: Gets the shutdown status of the exporter + */ + bool IsShutdown() const; + +private: + // The configuration options associated with this exporter. + const PrometheusExporterOptions options_; + /** + * exporter shutdown status + */ + bool is_shutdown_; + + /** + * Pointer to a + * PrometheusCollector instance + */ + std::shared_ptr<PrometheusCollector> 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 metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif // ENABLE_METRICS_PREVIEW diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h new file mode 100644 index 000000000..c8df4f6cf --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/exporter_utils.h @@ -0,0 +1,115 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once + +#ifndef ENABLE_METRICS_PREVIEW + +# include <prometheus/metric_family.h> +# include <string> +# include <vector> +# include "opentelemetry/metrics/provider.h" +# include "opentelemetry/sdk/metrics/meter.h" +# include "opentelemetry/version.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace metrics +{ +/** + * The Prometheus Utils contains utility functions for Prometheus Exporter + */ +class PrometheusExporterUtils +{ +public: + /** + * Helper function to convert OpenTelemetry metrics data collection + * to Prometheus metrics data collection + * + * @param records a collection of metrics in OpenTelemetry + * @return a collection of translated metrics that is acceptable by Prometheus + */ + static std::vector<::prometheus::MetricFamily> TranslateToPrometheus( + const std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> &data); + +private: + /** + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + */ + static std::string SanitizeNames(std::string name); + + static opentelemetry::sdk::metrics::AggregationType getAggregationType( + const opentelemetry::sdk::metrics::PointType &point_type); + + /** + * Translate the OTel metric type to Prometheus metric type + */ + static ::prometheus::MetricType TranslateType(opentelemetry::sdk::metrics::AggregationType kind); + + /** + * Set metric data for: + * Counter => Prometheus Counter + */ + template <typename T> + static void SetData(std::vector<T> values, + const opentelemetry::sdk::metrics::PointAttributes &labels, + ::prometheus::MetricType type, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family); + + /** + * Set metric data for: + * Histogram => Prometheus Histogram + */ + template <typename T> + static void SetData(std::vector<T> values, + const opentelemetry::sdk::metrics::ListType &boundaries, + const std::vector<uint64_t> &counts, + const opentelemetry::sdk::metrics::PointAttributes &labels, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family); + + /** + * Set time and labels to metric data + */ + static void SetMetricBasic(::prometheus::ClientMetric &metric, + std::chrono::nanoseconds time, + const opentelemetry::sdk::metrics::PointAttributes &labels); + + /** + * Convert attribute value to string + */ + static std::string AttributeValueToString( + const opentelemetry::sdk::common::OwnedAttributeValue &value); + + /** + * Handle Counter and Gauge. + */ + template <typename T> + static void SetValue(std::vector<T> values, + ::prometheus::MetricType type, + ::prometheus::ClientMetric *metric); + + /** + * Handle Gauge from MinMaxSumCount + */ + static void SetValue(double value, ::prometheus::ClientMetric *metric); + + /** + * Handle Histogram + */ + template <typename T, typename U> + static void SetValue(std::vector<T> values, + const std::list<U> &boundaries, + const std::vector<uint64_t> &counts, + ::prometheus::ClientMetric *metric); +}; +} // namespace metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h new file mode 100644 index 000000000..4be748385 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_collector.h @@ -0,0 +1,88 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifdef ENABLE_METRICS_PREVIEW + +# include <memory> +# include <mutex> +# include <vector> + +# include "opentelemetry/exporters/prometheus/prometheus_exporter_utils.h" +# include "opentelemetry/sdk/_metrics/record.h" +# include "prometheus/collectable.h" +# include "prometheus/metric_family.h" + +namespace prometheus_client = ::prometheus; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +/** + * The Prometheus Collector maintains the intermediate collection in Prometheus Exporter + */ +class PrometheusCollector : public prometheus_client::Collectable +{ +public: + /** + * Default Constructor. + * + * This constructor initializes the collection for metrics to export + * in this class with default capacity + */ + explicit PrometheusCollector(size_t max_collection_size = 2048); + + /** + * Collects all metrics data from metricsToCollect collection. + * + * @return all metrics in the metricsToCollect snapshot + */ + std::vector<prometheus_client::MetricFamily> Collect() const override; + + /** + * This function is called by export() function and add the collection of + * records to the metricsToCollect collection + * + * @param records a collection of records to add to the metricsToCollect collection + */ + void AddMetricData(const std::vector<opentelemetry::sdk::metrics::Record> &records); + + /** + * Get the current collection in the collector. + * + * @return the current metricsToCollect collection + */ + std::vector<opentelemetry::sdk::metrics::Record> GetCollection(); + + /** + * Gets the maximum size of the collection. + * + * @return max collection size + */ + int GetMaxCollectionSize() const; + +private: + /** + * Collection of metrics data from the export() function, and to be export + * to user when they send a pull request. This collection is a pointer + * to a collection so Collect() is able to clear the collection, even + * though it is a const function. + */ + std::unique_ptr<std::vector<opentelemetry::sdk::metrics::Record>> metrics_to_collect_; + + /** + * Maximum size of the metricsToCollect collection. + */ + size_t max_collection_size_; + + /* + * Lock when operating the metricsToCollect collection + */ + mutable std::mutex collection_lock_; +}; +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter.h new file mode 100644 index 000000000..7c1f99a75 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/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 <memory> +# include <string> +# include <vector> + +# 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<sdk::metrics::Record> &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<PrometheusCollector> &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<PrometheusCollector> 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/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h new file mode 100644 index 000000000..7efea9d39 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/include/opentelemetry/exporters/prometheus/prometheus_exporter_utils.h @@ -0,0 +1,171 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#pragma once +#ifdef ENABLE_METRICS_PREVIEW + +# include <string> +# include <vector> + +# include "opentelemetry/sdk/_metrics/record.h" +# include "prometheus/metric_family.h" + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +/** + * The Prometheus Utils contains utility functions for Prometheus Exporter + */ +class PrometheusExporterUtils +{ +public: + /** + * Helper function to convert OpenTelemetry metrics data collection + * to Prometheus metrics data collection + * + * @param records a collection of metrics in OpenTelemetry + * @return a collection of translated metrics that is acceptable by Prometheus + */ + static std::vector<::prometheus::MetricFamily> TranslateToPrometheus( + const std::vector<opentelemetry::sdk::metrics::Record> &records); + +private: + /** + * Set value to metric family according to record + */ + static void SetMetricFamily(opentelemetry::sdk::metrics::Record &record, + ::prometheus::MetricFamily *metric_family); + + /** + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + */ + static std::string SanitizeNames(std::string name); + + /** + * Set value to metric family for different aggregator + */ + template <typename T> + static void SetMetricFamilyByAggregator( + std::shared_ptr<opentelemetry::sdk::metrics::Aggregator<T>> aggregator, + std::string labels_str, + ::prometheus::MetricFamily *metric_family); + + /** + * Translate the OTel metric type to Prometheus metric type + */ + static ::prometheus::MetricType TranslateType(opentelemetry::sdk::metrics::AggregatorKind kind); + + /** + * Set metric data for: + * Counter => Prometheus Counter + * Gauge => Prometheus Gauge + */ + template <typename T> + static void SetData(std::vector<T> values, + const std::string &labels, + ::prometheus::MetricType type, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family); + + /** + * Set metric data for: + * Histogram => Prometheus Histogram + */ + template <typename T> + static void SetData(std::vector<T> values, + const std::vector<double> &boundaries, + const std::vector<int> &counts, + const std::string &labels, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family); + + /** + * Set metric data for: + * MinMaxSumCount => Prometheus Gauge + * Use Average (sum / count) as the gauge metric + */ + static void SetData(double value, + const std::string &labels, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family); + + /** + * Set metric data for: + * Exact => Prometheus Summary + * Sketch => Prometheus Summary + */ + template <typename T> + static void SetData(std::vector<T> values, + opentelemetry::sdk::metrics::AggregatorKind kind, + const std::vector<T> &quantiles, + const std::string &labels, + std::chrono::nanoseconds time, + ::prometheus::MetricFamily *metric_family, + bool do_quantile, + std::vector<double> quantile_points); + + /** + * Set time and labels to metric data + */ + static void SetMetricBasic(::prometheus::ClientMetric &metric, + std::chrono::nanoseconds time, + const std::string &labels); + + /** + * Parse a string of labels (key:value) into a vector of pairs + * {,} + * {l1:v1,l2:v2,...,} + */ + static std::vector<std::pair<std::string, std::string>> ParseLabel(std::string labels); + + /** + * Build a quantiles vector from aggregator + */ + template <typename T> + static std::vector<T> GetQuantilesVector( + std::shared_ptr<opentelemetry::sdk::metrics::Aggregator<T>> aggregator, + const std::vector<double> &quantile_points); + + /** + * Handle Counter and Gauge. + */ + template <typename T> + static void SetValue(std::vector<T> values, + ::prometheus::MetricType type, + ::prometheus::ClientMetric *metric); + + /** + * Handle Gauge from MinMaxSumCount + */ + static void SetValue(double value, ::prometheus::ClientMetric *metric); + + /** + * Handle Histogram + */ + template <typename T> + static void SetValue(std::vector<T> values, + std::vector<double> boundaries, + std::vector<int> counts, + ::prometheus::ClientMetric *metric); + + /** + * Handle Exact and Sketch + */ + template <typename T> + static void SetValue(std::vector<T> values, + opentelemetry::sdk::metrics::AggregatorKind kind, + std::vector<T> quantiles, + ::prometheus::ClientMetric *metric, + bool do_quantile, + const std::vector<double> &quantile_points); +}; +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/collector.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/collector.cc new file mode 100644 index 000000000..03793a8ee --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/collector.cc @@ -0,0 +1,89 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef ENABLE_METRICS_PREVIEW + +# include "opentelemetry/exporters/prometheus/collector.h" + +namespace metric_sdk = opentelemetry::sdk::metrics; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace metrics +{ +/** + * Default Constructor. + * + * This constructor initializes the collection for metrics to export + * in this class with default capacity + */ +PrometheusCollector::PrometheusCollector(size_t max_collection_size) + : max_collection_size_(max_collection_size) +{} + +/** + * Collects all metrics data from metricsToCollect collection. + * + * @return all metrics in the metricsToCollect snapshot + */ +std::vector<prometheus_client::MetricFamily> PrometheusCollector::Collect() const +{ + this->collection_lock_.lock(); + if (metrics_to_collect_.empty()) + { + this->collection_lock_.unlock(); + return {}; + } + + std::vector<prometheus_client::MetricFamily> result; + + // copy the intermediate collection, and then clear it + std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> copied_data; + copied_data.swap(metrics_to_collect_); + this->collection_lock_.unlock(); + + result = PrometheusExporterUtils::TranslateToPrometheus(copied_data); + return result; +} + +/** + * This function is called by export() function and add the collection of + * records to the metricsToCollect collection + * + * @param records a collection of records to add to the metricsToCollect collection + */ +void PrometheusCollector::AddMetricData(const sdk::metrics::ResourceMetrics &data) +{ + collection_lock_.lock(); + if (metrics_to_collect_.size() + 1 <= max_collection_size_) + { + metrics_to_collect_.emplace_back(new sdk::metrics::ResourceMetrics{data}); + } + collection_lock_.unlock(); +} + +/** + * Get the current collection in the collector. + * + * @return the current metrics_to_collect collection + */ +std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> &PrometheusCollector::GetCollection() +{ + return metrics_to_collect_; +} + +/** + * Gets the maximum size of the collection. + * + * @return max collection size + */ +int PrometheusCollector::GetMaxCollectionSize() const +{ + return max_collection_size_; +} + +} // namespace metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter.cc new file mode 100644 index 000000000..a0bd9e27a --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter.cc @@ -0,0 +1,100 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef ENABLE_METRICS_PREVIEW +# include "opentelemetry/exporters/prometheus/exporter.h" + +OPENTELEMETRY_BEGIN_NAMESPACE + +namespace exporter +{ +namespace metrics +{ +/** + * 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(const PrometheusExporterOptions &options) + : options_(options), is_shutdown_(false) +{ + exposer_ = std::unique_ptr<::prometheus::Exposer>(new ::prometheus::Exposer{options_.url}); + collector_ = std::shared_ptr<PrometheusCollector>(new PrometheusCollector); + + exposer_->RegisterCollectable(collector_); +} + +/** + * PrometheusExporter constructor with no parameters + * Used for testing only + */ +PrometheusExporter::PrometheusExporter() : is_shutdown_(false) +{ + collector_ = std::unique_ptr<PrometheusCollector>(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 sdk::metrics::ResourceMetrics &data) noexcept +{ + if (is_shutdown_) + { + return sdk::common::ExportResult::kFailure; + } + else if (collector_->GetCollection().size() + 1 > (size_t)collector_->GetMaxCollectionSize()) + { + return sdk::common::ExportResult::kFailureFull; + } + else + { + collector_->AddMetricData(data); + return sdk::common::ExportResult::kSuccess; + } + return sdk::common::ExportResult::kSuccess; +} + +bool PrometheusExporter::ForceFlush(std::chrono::microseconds timeout) noexcept +{ + return true; +} + +/** + * 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. + */ +bool PrometheusExporter::Shutdown(std::chrono::microseconds timeout) noexcept +{ + is_shutdown_ = true; + return true; + + collector_->GetCollection().clear(); +} + +/** + * @return: returns a shared_ptr to + * the PrometheusCollector instance + */ +std::shared_ptr<PrometheusCollector> &PrometheusExporter::GetCollector() +{ + return collector_; +} + +/** + * @return: Gets the shutdown status of the exporter + */ +bool PrometheusExporter::IsShutdown() const +{ + return is_shutdown_; +} + +} // namespace metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif // ENABLE_METRICS_PREVIEW diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter_utils.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter_utils.cc new file mode 100644 index 000000000..383925f98 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/exporter_utils.cc @@ -0,0 +1,314 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifndef ENABLE_METRICS_PREVIEW +# include <sstream> +# include <utility> +# include <vector> + +# include <prometheus/metric_type.h> +# include "opentelemetry/exporters/prometheus/exporter_utils.h" +# include "opentelemetry/sdk/metrics/export/metric_producer.h" + +# include "opentelemetry/sdk/common/global_log_handler.h" + +namespace prometheus_client = ::prometheus; +namespace metric_sdk = opentelemetry::sdk::metrics; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace metrics +{ +/** + * Helper function to convert OpenTelemetry metrics data collection + * to Prometheus metrics data collection + * + * @param records a collection of metrics in OpenTelemetry + * @return a collection of translated metrics that is acceptable by Prometheus + */ +std::vector<prometheus_client::MetricFamily> PrometheusExporterUtils::TranslateToPrometheus( + const std::vector<std::unique_ptr<sdk::metrics::ResourceMetrics>> &data) +{ + if (data.empty()) + { + return {}; + } + + // initialize output vector + std::vector<prometheus_client::MetricFamily> output; + + // iterate through the vector and set result data into it + for (const auto &r : data) + { + for (const auto &instrumentation_info : r->instrumentation_info_metric_data_) + { + for (const auto &metric_data : instrumentation_info.metric_data_) + { + auto origin_name = metric_data.instrument_descriptor.name_; + auto sanitized = SanitizeNames(origin_name); + prometheus_client::MetricFamily metric_family; + metric_family.name = sanitized; + metric_family.help = metric_data.instrument_descriptor.description_; + auto time = metric_data.start_ts.time_since_epoch(); + for (const auto &point_data_attr : metric_data.point_data_attr_) + { + auto kind = getAggregationType(point_data_attr.point_data); + const prometheus_client::MetricType type = TranslateType(kind); + metric_family.type = type; + if (type == prometheus_client::MetricType::Histogram) // Histogram + { + auto histogram_point_data = + nostd::get<sdk::metrics::HistogramPointData>(point_data_attr.point_data); + auto boundaries = histogram_point_data.boundaries_; + auto counts = histogram_point_data.counts_; + SetData(std::vector<double>{nostd::get<double>(histogram_point_data.sum_), + (double)histogram_point_data.count_}, + boundaries, counts, point_data_attr.attributes, time, &metric_family); + } + else // Counter, Untyped + { + auto sum_point_data = + nostd::get<sdk::metrics::SumPointData>(point_data_attr.point_data); + std::vector<metric_sdk::ValueType> values{sum_point_data.value_}; + SetData(values, point_data_attr.attributes, type, time, &metric_family); + } + } + output.emplace_back(metric_family); + } + } + } + return output; +} + +/** + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + */ +std::string PrometheusExporterUtils::SanitizeNames(std::string name) +{ + // replace all '.' and '-' with '_' + std::replace(name.begin(), name.end(), '.', '_'); + std::replace(name.begin(), name.end(), '-', '_'); + + return name; +} + +metric_sdk::AggregationType PrometheusExporterUtils::getAggregationType( + const metric_sdk::PointType &point_type) +{ + + if (nostd::holds_alternative<sdk::metrics::SumPointData>(point_type)) + { + return metric_sdk::AggregationType::kSum; + } + else if (nostd::holds_alternative<sdk::metrics::DropPointData>(point_type)) + { + return metric_sdk::AggregationType::kDrop; + } + else if (nostd::holds_alternative<sdk::metrics::HistogramPointData>(point_type)) + { + return metric_sdk::AggregationType::kHistogram; + } + else if (nostd::holds_alternative<sdk::metrics::LastValuePointData>(point_type)) + { + return metric_sdk::AggregationType::kLastValue; + } + return metric_sdk::AggregationType::kDefault; +} + +/** + * Translate the OTel metric type to Prometheus metric type + */ +prometheus_client::MetricType PrometheusExporterUtils::TranslateType( + metric_sdk::AggregationType kind) +{ + switch (kind) + { + case metric_sdk::AggregationType::kSum: + return prometheus_client::MetricType::Counter; + case metric_sdk::AggregationType::kHistogram: + return prometheus_client::MetricType::Histogram; + default: + return prometheus_client::MetricType::Untyped; + } +} + +/** + * Set metric data for: + * sum => Prometheus Counter + */ +template <typename T> +void PrometheusExporterUtils::SetData(std::vector<T> values, + const metric_sdk::PointAttributes &labels, + prometheus_client::MetricType type, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + SetValue(values, type, &metric); +} + +/** + * Set metric data for: + * Histogram => Prometheus Histogram + */ +template <typename T> +void PrometheusExporterUtils::SetData(std::vector<T> values, + const opentelemetry::sdk::metrics::ListType &boundaries, + const std::vector<uint64_t> &counts, + const metric_sdk::PointAttributes &labels, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + if (nostd::holds_alternative<std::list<long>>(boundaries)) + { + SetValue(values, nostd::get<std::list<long>>(boundaries), counts, &metric); + } + else + { + SetValue(values, nostd::get<std::list<double>>(boundaries), counts, &metric); + } +} + +/** + * Set time and labels to metric data + */ +void PrometheusExporterUtils::SetMetricBasic(prometheus_client::ClientMetric &metric, + std::chrono::nanoseconds time, + const metric_sdk::PointAttributes &labels) +{ + metric.timestamp_ms = time.count() / 1000000; + + // auto label_pairs = ParseLabel(labels); + if (!labels.empty()) + { + metric.label.resize(labels.size()); + size_t i = 0; + for (auto const &label : labels) + { + auto sanitized = SanitizeNames(label.first); + metric.label[i].name = sanitized; + metric.label[i++].value = AttributeValueToString(label.second); + } + } +}; + +std::string PrometheusExporterUtils::AttributeValueToString( + const opentelemetry::sdk::common::OwnedAttributeValue &value) +{ + std::string result; + if (nostd::holds_alternative<bool>(value)) + { + result = nostd::get<bool>(value) ? "true" : "false"; + } + else if (nostd::holds_alternative<int>(value)) + { + result = std::to_string(nostd::get<int>(value)); + } + else if (nostd::holds_alternative<int64_t>(value)) + { + result = std::to_string(nostd::get<int64_t>(value)); + } + else if (nostd::holds_alternative<unsigned int>(value)) + { + result = std::to_string(nostd::get<unsigned int>(value)); + } + else if (nostd::holds_alternative<uint64_t>(value)) + { + result = std::to_string(nostd::get<uint64_t>(value)); + } + else if (nostd::holds_alternative<double>(value)) + { + result = std::to_string(nostd::get<double>(value)); + } + else if (nostd::holds_alternative<std::string>(value)) + { + result = nostd::get<std::string>(value); + } + else + { + OTEL_INTERNAL_LOG_WARN( + "[Prometheus Exporter] AttributeValueToString - " + " Nested attributes not supported - ignored"); + } + return result; +} + +/** + * Handle Counter. + */ +template <typename T> +void PrometheusExporterUtils::SetValue(std::vector<T> values, + prometheus_client::MetricType type, + prometheus_client::ClientMetric *metric) +{ + double value = 0.0; + const auto &value_var = values[0]; + if (nostd::holds_alternative<long>(value_var)) + { + value = nostd::get<long>(value_var); + } + else + { + value = nostd::get<double>(value_var); + } + + switch (type) + { + case prometheus_client::MetricType::Counter: { + metric->counter.value = value; + break; + } + case prometheus_client::MetricType::Untyped: { + metric->untyped.value = value; + break; + } + default: + return; + } +} + +/** + * Handle Histogram + */ +template <typename T, typename U> +void PrometheusExporterUtils::SetValue(std::vector<T> values, + const std::list<U> &boundaries, + const std::vector<uint64_t> &counts, + prometheus_client::ClientMetric *metric) +{ + metric->histogram.sample_sum = values[0]; + metric->histogram.sample_count = values[1]; + int cumulative = 0; + std::vector<prometheus_client::ClientMetric::Bucket> buckets; + uint32_t idx = 0; + for (const auto &boundary : boundaries) + { + prometheus_client::ClientMetric::Bucket bucket; + cumulative += counts[idx]; + bucket.cumulative_count = cumulative; + bucket.upper_bound = boundary; + buckets.emplace_back(bucket); + ++idx; + } + prometheus_client::ClientMetric::Bucket bucket; + cumulative += counts[idx]; + bucket.cumulative_count = cumulative; + bucket.upper_bound = std::numeric_limits<double>::infinity(); + buckets.emplace_back(bucket); + metric->histogram.bucket = buckets; +} + +} // namespace metrics +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_collector.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_collector.cc new file mode 100644 index 000000000..53b7913de --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_collector.cc @@ -0,0 +1,166 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ENABLE_METRICS_PREVIEW +# include <iostream> + +# include "opentelemetry/exporters/prometheus/prometheus_collector.h" + +namespace metric_sdk = opentelemetry::sdk::metrics; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +/** + * Default Constructor. + * + * This constructor initializes the collection for metrics to export + * in this class with default capacity + */ +PrometheusCollector::PrometheusCollector(size_t max_collection_size) + : max_collection_size_(max_collection_size) +{ + metrics_to_collect_ = + std::unique_ptr<std::vector<metric_sdk::Record>>(new std::vector<metric_sdk::Record>); +} + +/** + * Collects all metrics data from metricsToCollect collection. + * + * @return all metrics in the metricsToCollect snapshot + */ +std::vector<prometheus_client::MetricFamily> PrometheusCollector::Collect() const +{ + this->collection_lock_.lock(); + if (metrics_to_collect_->empty()) + { + this->collection_lock_.unlock(); + return {}; + } + this->collection_lock_.unlock(); + + std::vector<prometheus_client::MetricFamily> result; + + // copy the intermediate collection, and then clear it + std::vector<metric_sdk::Record> copied_data; + + this->collection_lock_.lock(); + copied_data = std::vector<metric_sdk::Record>(*metrics_to_collect_); + metrics_to_collect_->clear(); + this->collection_lock_.unlock(); + + result = PrometheusExporterUtils::TranslateToPrometheus(copied_data); + return result; +} + +/** + * This function is called by export() function and add the collection of + * records to the metricsToCollect collection + * + * @param records a collection of records to add to the metricsToCollect collection + */ +void PrometheusCollector::AddMetricData(const std::vector<sdk::metrics::Record> &records) +{ + if (records.empty()) + { + return; + } + + collection_lock_.lock(); + if (metrics_to_collect_->size() + records.size() <= max_collection_size_) + { + /** + * ValidAggregator is a lambda that checks a Record to see if its + * Aggregator is a valid nostd::shared_ptr and not a nullptr. + */ + auto ValidAggregator = [](sdk::metrics::Record record) { + auto aggregator_variant = record.GetAggregator(); + if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<int>>>( + aggregator_variant)) + { + auto aggregator = + nostd::get<std::shared_ptr<metric_sdk::Aggregator<int>>>(aggregator_variant); + if (!aggregator) + { + return false; + } + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<short>>>( + aggregator_variant)) + { + auto aggregator = + nostd::get<std::shared_ptr<metric_sdk::Aggregator<short>>>(aggregator_variant); + if (!aggregator) + { + return false; + } + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<float>>>( + aggregator_variant)) + { + auto aggregator = + nostd::get<std::shared_ptr<metric_sdk::Aggregator<float>>>(aggregator_variant); + if (!aggregator) + { + return false; + } + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<double>>>( + aggregator_variant)) + { + auto aggregator = + nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(aggregator_variant); + if (!aggregator) + { + return false; + } + } + + return true; + }; + + for (auto &r : records) + { + if (ValidAggregator(r)) + { + metrics_to_collect_->emplace_back(r); + } + // Drop the record and write to std::cout + else + { + // Cannot call non const functions on const Record r + sdk::metrics::Record c = r; + std::cout << "Dropped Record containing invalid aggregator with name: " + c.GetName() + << std::endl; + } + } + } + collection_lock_.unlock(); +} + +/** + * Get the current collection in the collector. + * + * @return the current metrics_to_collect collection + */ +std::vector<sdk::metrics::Record> PrometheusCollector::GetCollection() +{ + return *metrics_to_collect_; +} + +/** + * Gets the maximum size of the collection. + * + * @return max collection size + */ +int PrometheusCollector::GetMaxCollectionSize() const +{ + return max_collection_size_; +} + +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_exporter.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_exporter.cc new file mode 100644 index 000000000..b64af1e90 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/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<PrometheusCollector>(new PrometheusCollector); + + exposer_->RegisterCollectable(collector_); +} + +/** + * PrometheusExporter constructor with no parameters + * Used for testing only + */ +PrometheusExporter::PrometheusExporter() : is_shutdown_(false) +{ + collector_ = std::unique_ptr<PrometheusCollector>(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<sdk::metrics::Record> &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<PrometheusCollector> &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/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_exporter_utils.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_exporter_utils.cc new file mode 100644 index 000000000..5eec9e3e8 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/src/prometheus_exporter_utils.cc @@ -0,0 +1,446 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ENABLE_METRICS_PREVIEW +# include <iostream> +# include <sstream> +# include <utility> +# include <vector> + +# include "opentelemetry/exporters/prometheus/prometheus_exporter_utils.h" +# include "opentelemetry/sdk/_metrics/aggregator/aggregator.h" +# include "prometheus/metric_type.h" + +namespace prometheus_client = ::prometheus; +namespace metric_sdk = opentelemetry::sdk::metrics; + +OPENTELEMETRY_BEGIN_NAMESPACE +namespace exporter +{ +namespace prometheus +{ +/** + * Helper function to convert OpenTelemetry metrics data collection + * to Prometheus metrics data collection + * + * @param records a collection of metrics in OpenTelemetry + * @return a collection of translated metrics that is acceptable by Prometheus + */ +std::vector<prometheus_client::MetricFamily> PrometheusExporterUtils::TranslateToPrometheus( + const std::vector<metric_sdk::Record> &records) +{ + if (records.empty()) + { + return {}; + } + + // initialize output vector + std::vector<prometheus_client::MetricFamily> output(records.size()); + + // iterate through the vector and set result data into it + int i = 0; + for (auto r : records) + { + SetMetricFamily(r, &output[i]); + i++; + } + + return output; +} + +// ======================= private helper functions ========================= +/** + * Set value to metric family according to record + */ +void PrometheusExporterUtils::SetMetricFamily(metric_sdk::Record &record, + prometheus_client::MetricFamily *metric_family) +{ + + auto origin_name = record.GetName(); + auto sanitized = SanitizeNames(origin_name); + metric_family->name = sanitized; + metric_family->help = record.GetDescription(); + + // unpack the variant and set the metric data to metric family struct + auto agg_var = record.GetAggregator(); + auto labels_str = record.GetLabels(); + if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<int>>>(agg_var)) + { + auto aggregator = nostd::get<std::shared_ptr<metric_sdk::Aggregator<int>>>(agg_var); + SetMetricFamilyByAggregator(aggregator, labels_str, metric_family); + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<short>>>(agg_var)) + { + auto aggregator = nostd::get<std::shared_ptr<metric_sdk::Aggregator<short>>>(agg_var); + SetMetricFamilyByAggregator(aggregator, labels_str, metric_family); + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<float>>>( + record.GetAggregator())) + { + auto aggregator = nostd::get<std::shared_ptr<metric_sdk::Aggregator<float>>>(agg_var); + SetMetricFamilyByAggregator(aggregator, labels_str, metric_family); + } + else if (nostd::holds_alternative<std::shared_ptr<metric_sdk::Aggregator<double>>>( + record.GetAggregator())) + { + auto aggregator = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(agg_var); + SetMetricFamilyByAggregator(aggregator, labels_str, metric_family); + } +} + +/** + * Sanitize the given metric name or label according to Prometheus rule. + * + * This function is needed because names in OpenTelemetry can contain + * alphanumeric characters, '_', '.', and '-', whereas in Prometheus the + * name should only contain alphanumeric characters and '_'. + */ +std::string PrometheusExporterUtils::SanitizeNames(std::string name) +{ + // replace all '.' and '-' with '_' + std::replace(name.begin(), name.end(), '.', '_'); + std::replace(name.begin(), name.end(), '-', '_'); + + return name; +} + +/** + * Set value to metric family for different aggregator + */ +template <typename T> +void PrometheusExporterUtils::SetMetricFamilyByAggregator( + std::shared_ptr<metric_sdk::Aggregator<T>> aggregator, + std::string labels_str, + prometheus_client::MetricFamily *metric_family) +{ + // get aggregator kind and translate to Prometheus metric type + auto kind = aggregator->get_aggregator_kind(); + const prometheus_client::MetricType type = TranslateType(kind); + metric_family->type = type; + // get check-pointed values, label string and check-pointed time + auto checkpointed_values = aggregator->get_checkpoint(); + auto time = aggregator->get_checkpoint_timestamp().time_since_epoch(); + + if (type == prometheus_client::MetricType::Histogram) // Histogram + { + auto boundaries = aggregator->get_boundaries(); + auto counts = aggregator->get_counts(); + SetData(checkpointed_values, boundaries, counts, labels_str, time, metric_family); + } + else if (type == prometheus_client::MetricType::Summary) // Sketch, Exact + { + std::vector<double> quantile_points = {0, 0.5, 0.9, 0.95, 0.99, 1}; + if (kind == metric_sdk::AggregatorKind::Exact) + { + std::vector<T> quantiles; + bool do_quantile = aggregator->get_quant_estimation(); + if (do_quantile) + { + quantiles = GetQuantilesVector(aggregator, quantile_points); + } + SetData(checkpointed_values, kind, quantiles, labels_str, time, metric_family, do_quantile, + quantile_points); + } + else if (kind == metric_sdk::AggregatorKind::Sketch) + { + auto quantiles = GetQuantilesVector(aggregator, quantile_points); + SetData(checkpointed_values, kind, quantiles, labels_str, time, metric_family, true, + quantile_points); + } + } + else // Counter, Gauge, MinMaxSumCount, Untyped + { + // Handle MinMaxSumCount: https://github.com/open-telemetry/opentelemetry-cpp/issues/228 + // Use sum/count is ok. + if (kind == metric_sdk::AggregatorKind::MinMaxSumCount) + { + double avg = (double)checkpointed_values[2] / checkpointed_values[3]; + SetData(avg, labels_str, time, metric_family); + } + else + { + SetData(checkpointed_values, labels_str, type, time, metric_family); + } + } +} + +/** + * Translate the OTel metric type to Prometheus metric type + */ +prometheus_client::MetricType PrometheusExporterUtils::TranslateType( + metric_sdk::AggregatorKind kind) +{ + switch (kind) + { + case metric_sdk::AggregatorKind::Counter: + return prometheus_client::MetricType::Counter; + case metric_sdk::AggregatorKind::Gauge: + case metric_sdk::AggregatorKind::MinMaxSumCount: + return prometheus_client::MetricType::Gauge; + case metric_sdk::AggregatorKind::Histogram: + return prometheus_client::MetricType::Histogram; + case metric_sdk::AggregatorKind::Sketch: + case metric_sdk::AggregatorKind::Exact: + return prometheus_client::MetricType::Summary; + default: + return prometheus_client::MetricType::Untyped; + } +} + +/** + * Set metric data for: + * Counter => Prometheus Counter + * Gauge => Prometheus Gauge + */ +template <typename T> +void PrometheusExporterUtils::SetData(std::vector<T> values, + const std::string &labels, + prometheus_client::MetricType type, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + SetValue(values, type, &metric); +} + +/** + * Set metric data for: + * Histogram => Prometheus Histogram + */ +template <typename T> +void PrometheusExporterUtils::SetData(std::vector<T> values, + const std::vector<double> &boundaries, + const std::vector<int> &counts, + const std::string &labels, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + SetValue(values, boundaries, counts, &metric); +} + +/** + * Set metric data for: + * MinMaxSumCount => Prometheus Gauge + * Use Average (sum / count) as the gauge metric + */ +void PrometheusExporterUtils::SetData(double value, + const std::string &labels, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + SetValue(value, &metric); +} + +/** + * Set metric data for: + * Exact => Prometheus Summary + * Sketch => Prometheus Summary + */ +template <typename T> +void PrometheusExporterUtils::SetData(std::vector<T> values, + metric_sdk::AggregatorKind kind, + const std::vector<T> &quantiles, + const std::string &labels, + std::chrono::nanoseconds time, + prometheus_client::MetricFamily *metric_family, + bool do_quantile, + std::vector<double> quantile_points) +{ + metric_family->metric.emplace_back(); + prometheus_client::ClientMetric &metric = metric_family->metric.back(); + SetMetricBasic(metric, time, labels); + SetValue(values, kind, quantiles, &metric, do_quantile, quantile_points); +} + +/** + * Set time and labels to metric data + */ +void PrometheusExporterUtils::SetMetricBasic(prometheus_client::ClientMetric &metric, + std::chrono::nanoseconds time, + const std::string &labels) +{ + metric.timestamp_ms = time.count() / 1000000; + + auto label_pairs = ParseLabel(labels); + if (!label_pairs.empty()) + { + metric.label.resize(label_pairs.size()); + for (size_t i = 0; i < label_pairs.size(); ++i) + { + auto origin_name = label_pairs[i].first; + auto sanitized = SanitizeNames(origin_name); + metric.label[i].name = sanitized; + metric.label[i].value = label_pairs[i].second; + } + } +}; + +/** + * Parse a string of labels (key:value) into a vector of pairs + * {,} + * {l1:v1,l2:v2,...,} + */ +std::vector<std::pair<std::string, std::string>> PrometheusExporterUtils::ParseLabel( + std::string labels) +{ + labels = labels.substr(1, labels.size() - 2); + + std::vector<std::string> paired_labels; + std::stringstream s_stream(labels); + while (s_stream.good()) + { + std::string substr; + getline(s_stream, substr, ','); // get first string delimited by comma + if (!substr.empty()) + { + paired_labels.push_back(substr); + } + } + + std::vector<std::pair<std::string, std::string>> result; + for (auto &paired : paired_labels) + { + std::size_t split_index = paired.find(':'); + std::string label = paired.substr(0, split_index); + std::string value = paired.substr(split_index + 1); + result.emplace_back(std::pair<std::string, std::string>(label, value)); + } + + return result; +} + +/** + * Build a quantiles vector from aggregator + */ +template <typename T> +std::vector<T> PrometheusExporterUtils::GetQuantilesVector( + std::shared_ptr<metric_sdk::Aggregator<T>> aggregator, + const std::vector<double> &quantile_points) +{ + std::vector<T> quantiles; + for (double q : quantile_points) + { + T quantile = aggregator->get_quantiles(q); + quantiles.emplace_back(quantile); + } + return quantiles; +} + +/** + * Handle Counter and Gauge. + */ +template <typename T> +void PrometheusExporterUtils::SetValue(std::vector<T> values, + prometheus_client::MetricType type, + prometheus_client::ClientMetric *metric) +{ + switch (type) + { + case prometheus_client::MetricType::Counter: { + metric->counter.value = values[0]; + break; + } + case prometheus_client::MetricType::Gauge: { + metric->gauge.value = values[0]; + break; + } + case prometheus_client::MetricType::Untyped: { + metric->untyped.value = values[0]; + break; + } + default: + return; + } +} + +/** + * Handle Gauge from MinMaxSumCount + */ +void PrometheusExporterUtils::SetValue(double value, prometheus_client::ClientMetric *metric) +{ + metric->gauge.value = value; +} + +/** + * Handle Histogram + */ +template <typename T> +void PrometheusExporterUtils::SetValue(std::vector<T> values, + std::vector<double> boundaries, + std::vector<int> counts, + prometheus_client::ClientMetric *metric) +{ + metric->histogram.sample_sum = values[0]; + metric->histogram.sample_count = values[1]; + int cumulative = 0; + std::vector<prometheus_client::ClientMetric::Bucket> buckets; + for (size_t i = 0; i < boundaries.size() + 1; i++) + { + prometheus_client::ClientMetric::Bucket bucket; + cumulative += counts[i]; + bucket.cumulative_count = cumulative; + if (i != boundaries.size()) + { + bucket.upper_bound = boundaries[i]; + } + else + { + bucket.upper_bound = std::numeric_limits<double>::infinity(); + } + buckets.emplace_back(bucket); + } + metric->histogram.bucket = buckets; +} + +/** + * Handle Exact and Sketch + */ +template <typename T> +void PrometheusExporterUtils::SetValue(std::vector<T> values, + metric_sdk::AggregatorKind kind, + std::vector<T> quantiles, + prometheus_client::ClientMetric *metric, + bool do_quantile, + const std::vector<double> &quantile_points) +{ + if (kind == metric_sdk::AggregatorKind::Exact) + { + metric->summary.sample_count = values.size(); + auto sum = 0; + for (auto val : values) + { + sum += val; + } + metric->summary.sample_sum = sum; + } + else if (kind == metric_sdk::AggregatorKind::Sketch) + { + metric->summary.sample_sum = values[0]; + metric->summary.sample_count = values[1]; + } + + if (do_quantile) + { + std::vector<prometheus_client::ClientMetric::Quantile> prometheus_quantiles; + for (size_t i = 0; i < quantiles.size(); i++) + { + prometheus_client::ClientMetric::Quantile quantile; + quantile.quantile = quantile_points[i]; + quantile.value = quantiles[i]; + prometheus_quantiles.emplace_back(quantile); + } + metric->summary.quantile = prometheus_quantiles; + } +} +} // namespace prometheus +} // namespace exporter +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/CMakeLists.txt b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/CMakeLists.txt new file mode 100644 index 000000000..1a2246979 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/CMakeLists.txt @@ -0,0 +1,13 @@ +if(WITH_METRICS_PREVIEW) + 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} + prometheus_exporter_deprecated prometheus-cpp::pull) + gtest_add_tests( + TARGET ${testname} + TEST_PREFIX exporter. + TEST_LIST ${testname}) + endforeach() +endif() diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_collector_test.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_collector_test.cc new file mode 100644 index 000000000..d1aff1b0f --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_collector_test.cc @@ -0,0 +1,756 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ENABLE_METRICS_PREVIEW +# include <gtest/gtest.h> +# include <future> +# include <map> +# include <thread> + +# include "opentelemetry/_metrics/instrument.h" +# include "opentelemetry/exporters/prometheus/prometheus_collector.h" +# include "opentelemetry/sdk/_metrics/aggregator/aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/counter_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/exact_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/gauge_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/histogram_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/min_max_sum_count_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/sketch_aggregator.h" +# include "opentelemetry/sdk/_metrics/record.h" +# include "opentelemetry/version.h" + +using opentelemetry::exporter::prometheus::PrometheusCollector; +namespace metric_api = opentelemetry::metrics; +namespace metric_sdk = opentelemetry::sdk::metrics; + +OPENTELEMETRY_BEGIN_NAMESPACE + +/** + * CreateAgg() is a helper function that returns a + * nostd::shared_ptr given an AggregatorKind + */ +template <typename T> +std::shared_ptr<metric_sdk::Aggregator<T>> CreateAgg(metric_sdk::AggregatorKind kind, + bool exactMode = true) +{ + std::shared_ptr<metric_sdk::Aggregator<T>> aggregator; + switch (kind) + { + case metric_sdk::AggregatorKind::Counter: { + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::CounterAggregator<T>(metric_api::InstrumentKind::Counter)); + break; + } + case metric_sdk::AggregatorKind::MinMaxSumCount: { + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::MinMaxSumCountAggregator<T>(metric_api::InstrumentKind::Counter)); + break; + } + case metric_sdk::AggregatorKind::Gauge: { + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::GaugeAggregator<T>(metric_api::InstrumentKind::Counter)); + break; + } + case metric_sdk::AggregatorKind::Sketch: { + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::SketchAggregator<T>(metric_api::InstrumentKind::Counter, 0.000005)); + break; + } + case metric_sdk::AggregatorKind::Histogram: { + std::vector<double> boundaries{10, 20}; + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::HistogramAggregator<T>(metric_api::InstrumentKind::Counter, boundaries)); + break; + } + case metric_sdk::AggregatorKind::Exact: { + aggregator = std::shared_ptr<metric_sdk::Aggregator<T>>( + new metric_sdk::ExactAggregator<T>(metric_api::InstrumentKind::Counter, exactMode)); + break; + } + default: + aggregator = nullptr; + } + return aggregator; +} + +/** + * Populate() updates the aggregator with values and checkpoints it based + * on what its AggregatorKind is + */ +template <typename T> +void Populate(std::shared_ptr<metric_sdk::Aggregator<T>> &aggregator) +{ + if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::Counter) + { + aggregator->update(10.0); + aggregator->update(5.0); + aggregator->checkpoint(); + } + else if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::MinMaxSumCount) + { + aggregator->update(10); + aggregator->update(2); + aggregator->update(5); + aggregator->checkpoint(); + } + else if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::Gauge) + { + aggregator->update(10); + aggregator->update(5); + aggregator->checkpoint(); + } + else if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::Sketch) + { + for (double i = 0; i < 10.0; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + } + else if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::Histogram) + { + for (float i = 0; i < 30.0; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + } + else if (aggregator->get_aggregator_kind() == metric_sdk::AggregatorKind::Exact) + { + for (double i = 0; i < 10.0; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + } +} + +/** + * Helper function to create a collection of records taken from + * a aggregator of specified AggregatorKind + */ +template <typename T> +std::vector<metric_sdk::Record> CreateRecords(int num, + metric_sdk::AggregatorKind kind, + bool exactMode = true) +{ + std::vector<metric_sdk::Record> records; + + for (int i = 0; i < num; i++) + { + std::string name = "record-" + std::to_string(i); + std::string description = "record " + std::to_string(i) + " for test purpose"; + std::string labels = "{label1:v1,label2:v2,}"; + std::shared_ptr<metric_sdk::Aggregator<T>> aggregator = CreateAgg<T>(kind, exactMode); + Populate(aggregator); + + metric_sdk::Record r{name, description, labels, aggregator}; + records.push_back(r); + } + return records; +} + +// ==================== Test for addMetricsData() function ====================== + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with Counter Aggregators. It checks that the cumulative + * sum of updates to the aggregator of a record before and after AddMetricData() + * is called are equal. + */ +TEST(PrometheusCollector, AddMetricDataWithCounterRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 2; + + // construct a collection of records with CounterAggregators and double + std::vector<metric_sdk::Record> records = + CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Counter); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size()); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(after_agg_var); + + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + } +} + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with MinMaxSumCount Aggregators. It checks that the min, max, + * sum, and count of updates to the aggregator of a record before and after AddMetricData() + * is called are equal. + */ +TEST(PrometheusCollector, AddMetricDataWithMinMaxSumCountRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 2; + + // construct a collection of records with MinMaxSumCountAggregators and short + std::vector<metric_sdk::Record> records = + CreateRecords<short>(num_records, metric_sdk::AggregatorKind::MinMaxSumCount); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size()); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<short>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<short>>>(after_agg_var); + + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + } +} + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with Gauge Aggregators. It checks that the last update + * to the aggregator of a record before and after AddMetricData() + * is called are equal. + */ +TEST(PrometheusCollector, AddMetricDataWithGaugeRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 2; + + // construct a collection of records with GaugeAggregators and int + std::vector<metric_sdk::Record> records = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Gauge); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size()); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<int>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<int>>>(after_agg_var); + + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + } +} + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with Sketch Aggregators. It checks that the sum of updates + * and count of values added for a record before and after being added are + * equal using get_checkpoint(). It also checks the same for buckets, in + * get_boundaries(), and counts for buckets, in get_counts(). + */ +TEST(PrometheusCollector, AddMetricDataWithSketchRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 2; + + // construct a collection of records with SketchAggregators and double + std::vector<metric_sdk::Record> records = + CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Sketch); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size()); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(after_agg_var); + + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + for (size_t i = 0; i < before_agg->get_boundaries().size(); i++) + { + ASSERT_EQ(before_agg->get_boundaries()[i], after_agg->get_boundaries()[i]); + } + for (size_t i = 0; i < before_agg->get_counts().size(); i++) + { + ASSERT_EQ(before_agg->get_counts()[i], after_agg->get_counts()[i]); + } + } +} + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with Histogram Aggregators. It checks that the sum of + * updates, number of updates, boundaries, and counts for each bucket + * for the aggregator of a record before and after AddMetricData() + * is called are equal. + */ +TEST(PrometheusCollector, AddMetricDataWithHistogramRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 2; + + // construct a collection of records with HistogramAggregators and float + std::vector<metric_sdk::Record> records = + CreateRecords<float>(num_records, metric_sdk::AggregatorKind::Histogram); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size()); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<float>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<float>>>(after_agg_var); + + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + for (size_t i = 0; i < before_agg->get_boundaries().size(); i++) + { + ASSERT_EQ(before_agg->get_boundaries()[i], after_agg->get_boundaries()[i]); + } + for (size_t i = 0; i < before_agg->get_counts().size(); i++) + { + ASSERT_EQ(before_agg->get_counts()[i], after_agg->get_counts()[i]); + } + } +} + +/** + * AddMetricData() should be able to successfully add a collection + * of Records with Exact Aggregators. If the Exact Aggregator is in + * quantile mode, it will check quantiles at selected values of 0, 0.25, + * 0.5, 0.75, and 1. If not, it will check the vector of checkpointed + * values in get_checkpoint(). + */ +TEST(PrometheusCollector, AddMetricDataWithExactRecordsSuccessfully) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 1; + + // construct a collection of a single record with a quantile + // estimation ExactAggregator and double + std::vector<metric_sdk::Record> records = + CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Exact, true); + + // add records to collection + collector.AddMetricData(records); + + // construct a collection of a single record with an in-order + // ExactAggregator and double + records = CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Exact, false); + + // add records to collection + collector.AddMetricData(records); + + // Collection size should be the same as the size + // of the records collection passed to addMetricData() + ASSERT_EQ(collector.GetCollection().size(), records.size() * 2); + + // check values of records created vs records from metricsToCollect, + // accessed by getCollection() + + for (int i = 0; i < num_records; i++) + { + metric_sdk::Record before = records[i]; + metric_sdk::Record after = collector.GetCollection()[i]; + + ASSERT_EQ(before.GetName(), after.GetName()); + + ASSERT_EQ(before.GetDescription(), after.GetDescription()); + + ASSERT_EQ(before.GetLabels(), after.GetLabels()); + + auto before_agg_var = before.GetAggregator(); + auto before_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(before_agg_var); + + auto after_agg_var = after.GetAggregator(); + auto after_agg = nostd::get<std::shared_ptr<metric_sdk::Aggregator<double>>>(after_agg_var); + + if (before_agg->get_quant_estimation() && after_agg->get_quant_estimation()) + { + for (double i = 0; i <= 1;) + { + ASSERT_EQ(before_agg->get_quantiles(i), after_agg->get_quantiles(i)); + i += 0.25; + } + } + else + { + ASSERT_EQ(before_agg->get_checkpoint().size(), after_agg->get_checkpoint().size()); + for (size_t i = 0; i < before_agg->get_checkpoint().size(); i++) + { + ASSERT_EQ(before_agg->get_checkpoint()[i], after_agg->get_checkpoint()[i]); + } + } + } +} + +TEST(PrometheusCollector, AddMetricDataDoesNotAddWithInsufficentSpace) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = collector.GetMaxCollectionSize() - 5; + + // construct a collection close to max capacity + std::vector<metric_sdk::Record> records = + CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Counter); + + collector.AddMetricData(records); + + // Check if all the records have been added + ASSERT_EQ(collector.GetCollection().size(), num_records); + + // Try adding the same collection of records again to + // metricsToCollect. + collector.AddMetricData(records); + + // Check that the number of records in metricsToCollect + // has not changed. + ASSERT_EQ(collector.GetCollection().size(), num_records); +} + +TEST(PrometheusCollector, AddMetricDataDoesNotAddBadIndividualRecords) +{ + PrometheusCollector collector; + + // number of records to create + int num_records = 5; + + // construct a collection with the specified number of records + std::vector<metric_sdk::Record> records = + CreateRecords<double>(num_records, metric_sdk::AggregatorKind::Counter); + + // add records to collection + collector.AddMetricData(records); + + // Check if all the records have been added + ASSERT_EQ(collector.GetCollection().size(), num_records); + + // Creates a bad record, with a nullptr aggregator and adds + // it to the colelction of records + std::string name = "bad_record"; + std::string description = "nullptr_agg"; + std::string labels = "{label1:v1}"; + std::shared_ptr<metric_sdk::Aggregator<int>> aggregator; + metric_sdk::Record bad_record{name, description, labels, aggregator}; + + records.push_back(bad_record); + + // add records to collection + collector.AddMetricData(records); + + // Check if all the records except the bad + // record have been added; the number of records added + // should be twice the original number of records + // epecified to be created + ASSERT_EQ(collector.GetCollection().size(), num_records * 2); +} + +// ==================== Test for Constructor ====================== +TEST(PrometheusCollector, ConstructorInitializesCollector) +{ + PrometheusCollector collector; + + // current size should be 0, capacity should be set to default + ASSERT_EQ(collector.GetCollection().size(), 0); +} + +// ==================== Tests for collect() function ====================== + +/** + * When collector is initialized, the collection inside is should also be initialized + */ +TEST(PrometheusCollector, CollectInitializesMetricFamilyCollection) +{ + PrometheusCollector collector; + auto c1 = collector.Collect(); + ASSERT_EQ(c1.size(), 0); +} + +/** + * Collect function should collect all data and clear the intermediate collection + */ +TEST(PrometheusCollector, CollectClearsTheCollection) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 2; + auto records = CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + collector.AddMetricData(records); + + // the collection should not be empty now + ASSERT_EQ(collector.GetCollection().size(), num_records); + + // don't care the collected result in this test + collector.Collect(); + + // after the collect() call, the collection should be empty + ASSERT_EQ(collector.GetCollection().size(), 0); +} + +/** + * Collected data should be already be parsed to Prometheus Metric format + */ +TEST(PrometheusCollector, CollectParsesDataToMetricFamily) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 1; + auto records = CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + collector.AddMetricData(records); + + // the collection should not be empty now + ASSERT_EQ(collector.GetCollection().size(), num_records); + auto collected = collector.Collect(); + + ASSERT_EQ(collected.size(), num_records); + + auto metric_family = collected[0]; + + // Collect function really collects a vector of MetricFamily + ASSERT_EQ(metric_family.name, "record_0"); + ASSERT_EQ(metric_family.help, "record 0 for test purpose"); + ASSERT_EQ(metric_family.type, prometheus_client::MetricType::Counter); + ASSERT_EQ(metric_family.metric.size(), 1); + ASSERT_DOUBLE_EQ(metric_family.metric[0].counter.value, 15); +} + +/** + * Concurrency Test 1: After adding data concurrently, the intermediate collection should + * contain all data from all threads. + */ +TEST(PrometheusCollector, ConcurrencyAddingRecords) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 2; + std::vector<metric_sdk::Record> records1 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + + std::vector<metric_sdk::Record> records2 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Gauge); + + std::thread first(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records1)); + std::thread second(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records2)); + + first.join(); + second.join(); + + ASSERT_EQ(collector.GetCollection().size(), 4); +} + +/** + * Concurrency Test 2: After adding data concurrently and collecting, the intermediate collection + * should be empty, and all data are collected in the result vector. + */ +TEST(PrometheusCollector, ConcurrentlyAddingAndThenCollecting) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 2; + std::vector<metric_sdk::Record> records1 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + + std::vector<metric_sdk::Record> records2 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Gauge); + + std::thread first(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records1)); + std::thread second(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records2)); + first.join(); + second.join(); + + auto collect_future = std::async(&PrometheusCollector::Collect, std::ref(collector)); + auto res = collect_future.get(); + + ASSERT_EQ(collector.GetCollection().size(), 0); + ASSERT_EQ(res.size(), 4); +} + +/** + * Concurrency Test 3: Concurrently adding and collecting. We don't know when the collect function + * is called, but all data entries are either collected or left in the collection. + */ +TEST(PrometheusCollector, ConcurrentlyAddingAndCollecting) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 2; + std::vector<metric_sdk::Record> records1 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + + std::vector<metric_sdk::Record> records2 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Gauge); + + std::thread first(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records1)); + std::thread second(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records2)); + auto collect_future = std::async(&PrometheusCollector::Collect, std::ref(collector)); + + first.join(); + second.join(); + + auto res = collect_future.get(); + + // the size of collection can be 0, 2, 4, because we don't know when the collect() + // is really called. However, we claim that if the data in the collection is collected, + // they must be in the res. So res.size() + collection.size() must be the total number + // of data records we generated. + ASSERT_EQ(res.size() + collector.GetCollection().size(), 4); +} + +/** + * Concurrency Test 4: Concurrently adding then concurrently collecting. We don't know which + * collecting thread fetches all data, but either one should succeed. + */ +TEST(PrometheusCollector, ConcurrentlyAddingAndConcurrentlyCollecting) +{ + PrometheusCollector collector; + + // construct a collection to add metric records + int num_records = 2; + std::vector<metric_sdk::Record> records1 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Counter); + + std::vector<metric_sdk::Record> records2 = + CreateRecords<int>(num_records, metric_sdk::AggregatorKind::Gauge); + + // concurrently adding + std::thread first(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records1)); + std::thread second(&PrometheusCollector::AddMetricData, std::ref(collector), std::ref(records2)); + first.join(); + second.join(); + + // after adding, then concurrently consuming + auto collect_future1 = std::async(&PrometheusCollector::Collect, std::ref(collector)); + auto collect_future2 = std::async(&PrometheusCollector::Collect, std::ref(collector)); + auto res1 = collect_future1.get(); + auto res2 = collect_future2.get(); + + // all added data must be collected in either res1 or res2 + ASSERT_EQ(res1.size() + res2.size(), 4); +} + +OPENTELEMETRY_END_NAMESPACE +#endif diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_exporter_test.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_exporter_test.cc new file mode 100644 index 000000000..564880c26 --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/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 <gtest/gtest.h> +# include <typeinfo> + +# 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<Record> CreateRecords(int num) +{ + + std::vector<Record> 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<opentelemetry::sdk::metrics::Aggregator<int>>( + new opentelemetry::sdk::metrics::CounterAggregator<int>( + 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<Record> 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<Record> 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<Record> full_records = CreateRecords(max_collection_size - 1); + std::vector<Record> 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<Record> 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 diff --git a/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_exporter_utils_test.cc b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_exporter_utils_test.cc new file mode 100644 index 000000000..22ce3f5fd --- /dev/null +++ b/src/jaegertracing/opentelemetry-cpp/exporters/prometheus/test/prometheus_exporter_utils_test.cc @@ -0,0 +1,460 @@ +// Copyright The OpenTelemetry Authors +// SPDX-License-Identifier: Apache-2.0 + +#ifdef ENABLE_METRICS_PREVIEW +# include <gtest/gtest.h> +# include <map> +# include <numeric> +# include <string> +# include <typeinfo> + +# include <opentelemetry/version.h> +# include "opentelemetry/exporters/prometheus/prometheus_exporter_utils.h" +# include "opentelemetry/sdk/_metrics/aggregator/counter_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/exact_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/gauge_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/histogram_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/min_max_sum_count_aggregator.h" +# include "opentelemetry/sdk/_metrics/aggregator/sketch_aggregator.h" + +using opentelemetry::exporter::prometheus::PrometheusExporterUtils; +namespace metric_sdk = opentelemetry::sdk::metrics; +namespace metric_api = opentelemetry::metrics; +namespace prometheus_client = ::prometheus; + +OPENTELEMETRY_BEGIN_NAMESPACE +template <typename T> +void assert_basic(prometheus_client::MetricFamily &metric, + const std::string &sanitized_name, + const std::string &description, + prometheus_client::MetricType type, + int label_num, + std::vector<T> vals) +{ + ASSERT_EQ(metric.name, sanitized_name); // name sanitized + ASSERT_EQ(metric.help, description); // description not changed + ASSERT_EQ(metric.type, type); // type translated + + auto metric_data = metric.metric[0]; + ASSERT_EQ(metric_data.label.size(), label_num); + + switch (type) + { + case prometheus_client::MetricType::Counter: { + ASSERT_DOUBLE_EQ(metric_data.counter.value, vals[0]); + break; + } + case prometheus_client::MetricType::Gauge: { + ASSERT_EQ(metric_data.gauge.value, vals[0]); + break; + } + case prometheus_client::MetricType::Histogram: { + ASSERT_DOUBLE_EQ(metric_data.histogram.sample_count, vals[0]); + ASSERT_DOUBLE_EQ(metric_data.histogram.sample_sum, vals[1]); + auto buckets = metric_data.histogram.bucket; + ASSERT_EQ(buckets.size(), vals[2]); + break; + } + case prometheus_client::MetricType::Summary: { + ASSERT_DOUBLE_EQ(metric_data.summary.sample_count, vals[0]); + ASSERT_DOUBLE_EQ(metric_data.summary.sample_sum, vals[1]); + break; + } + case prometheus::MetricType::Untyped: + break; + } +} + +void assert_histogram(prometheus_client::MetricFamily &metric, + std::vector<double> boundaries, + std::vector<int> correct) +{ + int cumulative_count = 0; + auto buckets = metric.metric[0].histogram.bucket; + for (size_t i = 0; i < buckets.size(); i++) + { + auto bucket = buckets[i]; + if (i != buckets.size() - 1) + { + ASSERT_DOUBLE_EQ(boundaries[i], bucket.upper_bound); + } + else + { + ASSERT_DOUBLE_EQ(std::numeric_limits<double>::infinity(), bucket.upper_bound); + } + cumulative_count += correct[i]; + ASSERT_EQ(cumulative_count, bucket.cumulative_count); + } +} + +template <typename T> +metric_sdk::Record get_record(const std::string &type, + int version, + const std::string &label, + std::shared_ptr<metric_sdk::Aggregator<T>> aggregator) +{ + std::string name = "test-" + type + "-metric-record-v_" + std::to_string(version) + ".0"; + std::string desc = "this is a test " + type + " metric record"; + metric_sdk::Record record(name, desc, label, aggregator); + return record; +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusEmptyInputReturnsEmptyCollection) +{ + std::vector<metric_sdk::Record> collection; + auto translated2 = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated2.size(), 0); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusIntegerCounter) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::CounterAggregator<int>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + + auto record1 = get_record("int-counter", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + aggregator->update(10); + aggregator->checkpoint(); + collection.emplace_back(record1); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<int> vals = {10}; + assert_basic(metric1, "test_int_counter_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Counter, 3, vals); + + auto record2 = get_record("int-counter", 2, "{,}", aggregator); + aggregator->update(20); + aggregator->update(30); + aggregator->checkpoint(); + collection.emplace_back(record2); + + translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric2 = translated[1]; + vals = {50}; + assert_basic(metric2, "test_int_counter_metric_record_v_2_0", record2.GetDescription(), + prometheus_client::MetricType::Counter, 0, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusDoubleCounter) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<double>>( + new metric_sdk::CounterAggregator<double>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + aggregator->update(10.5); + aggregator->checkpoint(); + auto record1 = get_record("double-counter", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + aggregator->update(22.4); + aggregator->update(31.2); + aggregator->checkpoint(); + auto record2 = get_record("double-counter", 2, "{,}", aggregator); + collection.emplace_back(record1); + collection.emplace_back(record2); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<double> vals = {53.6}; + assert_basic(metric1, "test_double_counter_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Counter, 3, vals); + auto metric2 = translated[1]; + assert_basic(metric2, "test_double_counter_metric_record_v_2_0", record2.GetDescription(), + prometheus_client::MetricType::Counter, 0, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusShortCounter) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<short>>( + new metric_sdk::CounterAggregator<short>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + aggregator->update(10); + aggregator->checkpoint(); + auto record1 = get_record("short-counter", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + aggregator->update(20); + aggregator->update(30); + aggregator->checkpoint(); + auto record2 = get_record("short-counter", 2, "{,}", aggregator); + collection.emplace_back(record1); + collection.emplace_back(record2); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<short> vals = {50}; + assert_basic(metric1, "test_short_counter_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Counter, 3, vals); + auto metric2 = translated[1]; + assert_basic(metric2, "test_short_counter_metric_record_v_2_0", record2.GetDescription(), + prometheus_client::MetricType::Counter, 0, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusFloatCounter) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<float>>( + new metric_sdk::CounterAggregator<float>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + aggregator->update(10.5f); + aggregator->checkpoint(); + auto record1 = get_record("float-counter", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + aggregator->update(22.4f); + aggregator->update(31.2f); + aggregator->checkpoint(); + auto record2 = get_record("float-counter", 2, "{,}", aggregator); + collection.emplace_back(record1); + collection.emplace_back(record2); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<float> vals = {53.6f}; + assert_basic(metric1, "test_float_counter_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Counter, 3, vals); + auto metric2 = translated[1]; + assert_basic(metric2, "test_float_counter_metric_record_v_2_0", record2.GetDescription(), + prometheus_client::MetricType::Counter, 0, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusGauge) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::GaugeAggregator<int>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + aggregator->update(10); + aggregator->checkpoint(); + auto record1 = get_record("gauge", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + aggregator->update(20); + aggregator->update(30); + aggregator->checkpoint(); + auto record2 = get_record("gauge", 2, "{,}", aggregator); + collection.emplace_back(record1); + collection.emplace_back(record2); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<int> vals = {30}; + assert_basic(metric1, "test_gauge_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Gauge, 3, vals); + auto metric2 = translated[1]; + assert_basic(metric2, "test_gauge_metric_record_v_2_0", record2.GetDescription(), + prometheus_client::MetricType::Gauge, 0, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusHistogramUniform) +{ + std::vector<double> boundaries{10, 20, 30, 40, 50}; + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::HistogramAggregator<int>(metric_api::InstrumentKind::Counter, boundaries)); + + std::vector<metric_sdk::Record> collection; + auto record = get_record("histogram-uniform", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + int count_num = 60; + for (int i = 0; i < count_num; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + std::vector<int> vals = {aggregator->get_checkpoint()[1], aggregator->get_checkpoint()[0], + (int)boundaries.size() + 1}; + assert_basic(metric, "test_histogram_uniform_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Histogram, 3, vals); + std::vector<int> correct = aggregator->get_counts(); + assert_histogram(metric, boundaries, correct); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusHistogramNormal) +{ + std::vector<double> boundaries{2, 4, 6, 8, 10, 12}; + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::HistogramAggregator<int>(metric_api::InstrumentKind::Counter, boundaries)); + + std::vector<metric_sdk::Record> collection; + auto record = get_record("histogram-normal", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + std::vector<int> values{1, 3, 3, 5, 5, 5, 7, 7, 7, 7, 9, 9, 9, 11, 11, 13}; + for (int i : values) + { + aggregator->update(i); + } + aggregator->checkpoint(); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + std::vector<int> vals = {aggregator->get_checkpoint()[1], aggregator->get_checkpoint()[0], + (int)boundaries.size() + 1}; + assert_basic(metric, "test_histogram_normal_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Histogram, 3, vals); + std::vector<int> correct = aggregator->get_counts(); + assert_histogram(metric, boundaries, correct); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusExact) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::ExactAggregator<int>(metric_api::InstrumentKind::Counter, true)); + + std::vector<metric_sdk::Record> collection; + int count_num = 100; + for (int i = 0; i <= count_num; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + auto record = get_record("exact", 1, "{label-1:v1,label_2:v2,label3:v3,}", aggregator); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + std::vector<int> vals = {101, 5050}; + assert_basic(metric, "test_exact_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Summary, 3, vals); + auto quantile = metric.metric[0].summary.quantile; + ASSERT_EQ(quantile.size(), 6); + ASSERT_DOUBLE_EQ(quantile[0].value, 0); + ASSERT_DOUBLE_EQ(quantile[1].value, 50); + ASSERT_DOUBLE_EQ(quantile[2].value, 90); + ASSERT_DOUBLE_EQ(quantile[3].value, 95); + ASSERT_DOUBLE_EQ(quantile[4].value, 99); + ASSERT_DOUBLE_EQ(quantile[5].value, 100); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusExactNoQuantile) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::ExactAggregator<int>(metric_api::InstrumentKind::Counter, false)); + + std::vector<metric_sdk::Record> collection; + int count_num = 10; + for (int i = 0; i < count_num; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + auto record = get_record("exact-no-quantile", 1, "{label1:v1,label2:v2,}", aggregator); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + std::vector<int> vals = {count_num, 45}; + assert_basic(metric, "test_exact_no_quantile_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Summary, 2, vals); + auto quantile = metric.metric[0].summary.quantile; + ASSERT_EQ(quantile.size(), 0); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusMinMaxSumCount) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::MinMaxSumCountAggregator<int>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + // min: 1, max: 10, sum: 55, count: 10 + for (int i = 1; i <= 10; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + auto record = get_record("mmsc", 1, "{label1:v1,label2:v2,label3:v3,}", aggregator); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + // in this version of implementation, we use the sum/count as a gauge + std::vector<double> vals = {5.5}; + assert_basic(metric, "test_mmsc_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Gauge, 3, vals); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusSketch) +{ + auto aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::SketchAggregator<int>(metric_api::InstrumentKind::Counter, 0.0005)); + + std::vector<metric_sdk::Record> collection; + for (int i = 0; i <= 100; i++) + { + aggregator->update(i); + } + aggregator->checkpoint(); + auto record = get_record("sketch", 1, "{label1:v1,label2:v2,}", aggregator); + collection.emplace_back(record); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric = translated[0]; + std::vector<int> vals = {aggregator->get_checkpoint()[1], aggregator->get_checkpoint()[0]}; + assert_basic(metric, "test_sketch_metric_record_v_1_0", record.GetDescription(), + prometheus_client::MetricType::Summary, 2, vals); + + auto quantile = metric.metric[0].summary.quantile; + ASSERT_EQ(quantile.size(), 6); + ASSERT_DOUBLE_EQ(quantile[0].value, 0); + ASSERT_DOUBLE_EQ(quantile[1].value, 49); + ASSERT_DOUBLE_EQ(quantile[2].value, 89); + ASSERT_DOUBLE_EQ(quantile[3].value, 94); + ASSERT_DOUBLE_EQ(quantile[4].value, 98); + ASSERT_DOUBLE_EQ(quantile[5].value, 99); +} + +TEST(PrometheusExporterUtils, TranslateToPrometheusMultipleAggregators) +{ + auto counter_aggregator = std::shared_ptr<metric_sdk::Aggregator<double>>( + new metric_sdk::CounterAggregator<double>(metric_api::InstrumentKind::Counter)); + auto gauge_aggregator = std::shared_ptr<metric_sdk::Aggregator<int>>( + new metric_sdk::GaugeAggregator<int>(metric_api::InstrumentKind::Counter)); + + std::vector<metric_sdk::Record> collection; + counter_aggregator->update(10); + counter_aggregator->update(20); + counter_aggregator->checkpoint(); + auto record1 = get_record("counter", 1, "{label1:v1,label2:v2,label3:v3,}", counter_aggregator); + gauge_aggregator->update(10); + gauge_aggregator->update(30); + gauge_aggregator->update(20); + gauge_aggregator->checkpoint(); + auto record2 = get_record("gauge", 1, "{label1:v1,}", gauge_aggregator); + collection.emplace_back(record1); + collection.emplace_back(record2); + + auto translated = PrometheusExporterUtils::TranslateToPrometheus(collection); + ASSERT_EQ(translated.size(), collection.size()); + + auto metric1 = translated[0]; + std::vector<int> vals = {30}; + assert_basic(metric1, "test_counter_metric_record_v_1_0", record1.GetDescription(), + prometheus_client::MetricType::Counter, 3, vals); + auto metric2 = translated[1]; + vals = {20}; + assert_basic(metric2, "test_gauge_metric_record_v_1_0", record2.GetDescription(), + prometheus_client::MetricType::Gauge, 1, vals); +} +OPENTELEMETRY_END_NAMESPACE +#endif |