diff --git a/CHANGELOG.md b/CHANGELOG.md index f3008f7..618c9c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added -- +- New exporter, `otel_push_exporter` is now available in addition to the existing + `prometheus_exporter`. It can be used to push metrics in the OTEL format via + HTTP or gRPC to a OTEL-collector-(compatible) server. ### Changed diff --git a/autometrics/Cargo.toml b/autometrics/Cargo.toml index d81a17e..408b40a 100644 --- a/autometrics/Cargo.toml +++ b/autometrics/Cargo.toml @@ -36,6 +36,38 @@ prometheus-exporter = [ "dep:prometheus-client", ] +otel-push-exporter = [ + "opentelemetry_sdk", + "opentelemetry_api", + "opentelemetry-otlp", + "opentelemetry-otlp/tls-roots", +] + +otel-push-exporter-http = [ + "otel-push-exporter", + "opentelemetry-otlp/http-proto" +] + +otel-push-exporter-grpc = [ + "otel-push-exporter", + "opentelemetry-otlp/grpc-tonic" +] + +otel-push-exporter-tokio = [ + "otel-push-exporter", + "opentelemetry_sdk/rt-tokio" +] + +otel-push-exporter-tokio-current-thread = [ + "otel-push-exporter", + "opentelemetry_sdk/rt-tokio-current-thread" +] + +otel-push-exporter-async-std = [ + "otel-push-exporter", + "opentelemetry_sdk/rt-async-std" +] + # Exemplars exemplars-tracing = ["tracing", "tracing-subscriber"] exemplars-tracing-opentelemetry-0_20 = [ @@ -68,6 +100,7 @@ opentelemetry-prometheus = { version = "0.13.0", optional = true } opentelemetry_sdk = { version = "0.20", default-features = false, features = [ "metrics", ], optional = true } +opentelemetry-otlp = { version = "0.13.0", default-features = false, optional = true } prometheus = { version = "0.13", default-features = false, optional = true } # Used for prometheus-client feature diff --git a/autometrics/src/README.md b/autometrics/src/README.md index ffcaa5c..d163d0c 100644 --- a/autometrics/src/README.md +++ b/autometrics/src/README.md @@ -125,6 +125,25 @@ pub fn main() { - `prometheus-exporter` - exports a Prometheus metrics collector and exporter. This is compatible with any of the [Metrics backends](#metrics-backends) and uses `prometheus-client` by default if none are explicitly selected +### Pushing metrics + +Easily push collected metrics to a OpenTelemetry collector and compatible software. +Combine one of the transport feature flags together with your runtime feature flag: + +**Transport feature flags**: + +- `otel-push-exporter-http` - metrics sent over HTTP(s) using `hyper` +- `otel-push-exporter-grpc` - metrics sent over gRPC using `tonic` + +**Runtime feature flags**: + +- `otel-push-exporter-tokio` - tokio +- `otel-push-exporter-tokio-current-thread` - tokio with `flavor = "current_thread"` +- `otel-push-exporter-async-std` - async-std + +If you require more customization than these offered feature flags, enable just +`otel-push-exporter` and follow the [example](https://github.com/autometrics-dev/autometrics-rs/tree/main/examples/opentelemetry-push-custom). + ### Metrics backends > If you are exporting metrics yourself rather than using the `prometheus-exporter`, you must ensure that you are using the exact same version of the metrics library as `autometrics` (and it must come from `crates.io` rather than git or another source). If not, the autometrics metrics will not appear in your exported metrics. diff --git a/autometrics/src/lib.rs b/autometrics/src/lib.rs index 1033b68..46261c2 100644 --- a/autometrics/src/lib.rs +++ b/autometrics/src/lib.rs @@ -14,6 +14,8 @@ mod constants; pub mod exemplars; mod labels; pub mod objectives; +#[cfg(feature = "otel-push-exporter")] +pub mod otel_push_exporter; #[cfg(feature = "prometheus-exporter")] pub mod prometheus_exporter; pub mod settings; diff --git a/autometrics/src/otel_push_exporter.rs b/autometrics/src/otel_push_exporter.rs new file mode 100644 index 0000000..a475e7e --- /dev/null +++ b/autometrics/src/otel_push_exporter.rs @@ -0,0 +1,160 @@ +use opentelemetry_api::metrics::MetricsError; +use opentelemetry_otlp::{ExportConfig, Protocol, WithExportConfig}; +use opentelemetry_otlp::{OtlpMetricPipeline, OTEL_EXPORTER_OTLP_TIMEOUT_DEFAULT}; +use opentelemetry_sdk::metrics::MeterProvider; +use opentelemetry_sdk::runtime; +use std::ops::Deref; +use std::time::Duration; + +/// Newtype struct holding a [`MeterProvider`] with a custom `Drop` implementation to automatically clean up itself +#[repr(transparent)] +#[must_use = "Assign this to a unused variable instead: `let _meter = ...` (NOT `let _ = ...`), as else it will be dropped immediately - which will cause it to be shut down"] +pub struct OtelMeterProvider(MeterProvider); + +impl Deref for OtelMeterProvider { + type Target = MeterProvider; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Drop for OtelMeterProvider { + fn drop(&mut self) { + // this will only error if `.shutdown` gets called multiple times + let _ = self.0.shutdown(); + } +} + +/// Initialize the OpenTelemetry push exporter using HTTP transport. +/// +/// # Interval and timeout +/// This function uses the environment variables `OTEL_METRIC_EXPORT_TIMEOUT` and `OTEL_METRIC_EXPORT_INTERVAL` +/// to configure the timeout and interval respectively. If you want to customize those +/// from within code, consider using [`init_http_with_timeout_period`]. +#[cfg(feature = "otel-push-exporter-http")] +pub fn init_http(url: impl Into) -> Result { + runtime() + .with_exporter( + opentelemetry_otlp::new_exporter() + .http() + .with_export_config(ExportConfig { + endpoint: url.into(), + protocol: Protocol::HttpBinary, + ..Default::default() + }), + ) + .build() + .map(OtelMeterProvider) +} + +/// Initialize the OpenTelemetry push exporter using HTTP transport with customized `timeout` and `period`. +#[cfg(feature = "otel-push-exporter-http")] +pub fn init_http_with_timeout_period( + url: impl Into, + timeout: Duration, + period: Duration, +) -> Result { + runtime() + .with_exporter( + opentelemetry_otlp::new_exporter() + .http() + .with_export_config(ExportConfig { + endpoint: url.into(), + protocol: Protocol::HttpBinary, + timeout, + ..Default::default() + }), + ) + .with_period(period) + .build() + .map(OtelMeterProvider) +} + +/// Initialize the OpenTelemetry push exporter using gRPC transport. +/// +/// # Interval and timeout +/// This function uses the environment variables `OTEL_METRIC_EXPORT_TIMEOUT` and `OTEL_METRIC_EXPORT_INTERVAL` +/// to configure the timeout and interval respectively. If you want to customize those +/// from within code, consider using [`init_grpc_with_timeout_period`]. +#[cfg(feature = "otel-push-exporter-grpc")] +pub fn init_grpc(url: impl Into) -> Result { + runtime() + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_export_config(ExportConfig { + endpoint: url.into(), + protocol: Protocol::Grpc, + ..Default::default() + }), + ) + .build() + .map(OtelMeterProvider) +} + +/// Initialize the OpenTelemetry push exporter using gRPC transport with customized `timeout` and `period`. +#[cfg(feature = "otel-push-exporter-grpc")] +pub fn init_grpc_with_timeout_period( + url: impl Into, + timeout: Duration, + period: Duration, +) -> Result { + runtime() + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_export_config(ExportConfig { + endpoint: url.into(), + protocol: Protocol::Grpc, + timeout, + ..Default::default() + }), + ) + .with_period(period) + .build() + .map(OtelMeterProvider) +} + +#[cfg(all( + feature = "otel-push-exporter-tokio", + not(any( + feature = "otel-push-exporter-tokio-current-thread", + feature = "otel-push-exporter-async-std" + )) +))] +fn runtime() -> OtlpMetricPipeline { + return opentelemetry_otlp::new_pipeline().metrics(opentelemetry_sdk::runtime::Tokio); +} + +#[cfg(all( + feature = "otel-push-exporter-tokio-current-thread", + not(any( + feature = "otel-push-exporter-tokio", + feature = "otel-push-exporter-async-std" + )) +))] +fn runtime() -> OtlpMetricPipeline { + return opentelemetry_otlp::new_pipeline() + .metrics(opentelemetry_sdk::runtime::TokioCurrentThread); +} + +#[cfg(all( + feature = "otel-push-exporter-async-std", + not(any( + feature = "otel-push-exporter-tokio", + feature = "otel-push-exporter-tokio-current-thread" + )) +))] +fn runtime() -> OtlpMetricPipeline { + return opentelemetry_otlp::new_pipeline().metrics(opentelemetry_sdk::runtime::AsyncStd); +} + +#[cfg(not(any( + feature = "otel-push-exporter-tokio", + feature = "otel-push-exporter-tokio-current-thread", + feature = "otel-push-exporter-async-std" +)))] +fn runtime() -> ! { + compile_error!("select your runtime (`otel-push-exporter-tokio`, `otel-push-exporter-tokio-current-thread` or `otel-push-exporter-async-std`) for the autometrics push exporter or use the custom push exporter if none fit") +} diff --git a/autometrics/tests/compile_test.rs b/autometrics/tests/compile_test.rs index 7edb0cb..ec54f77 100644 --- a/autometrics/tests/compile_test.rs +++ b/autometrics/tests/compile_test.rs @@ -1,5 +1,5 @@ -use std::io; use autometrics::autometrics; +use std::io; // general purpose `Result`, part of the std prelude. // notice both `Ok` and `Err` generic type arguments are explicitly provided diff --git a/examples/README.md b/examples/README.md index 123fb9b..7dec046 100644 --- a/examples/README.md +++ b/examples/README.md @@ -13,7 +13,8 @@ cargo run --package example-{name of example} - [axum](./axum) - Use autometrics to instrument HTTP handlers using the `axum` framework - [custom-metrics](./custom-metrics/) - Define your own custom metrics alongside the ones generated by autometrics (using any of the metrics collection crates) - [exemplars-tracing](./exemplars-tracing/) - Use fields from `tracing::Span`s as Prometheus exemplars -- [opentelemetry-push](./opentelemetry-push/) - Push metrics to an OpenTelemetry Collector via the OTLP gRPC protocol +- [opentelemetry-push](./opentelemetry-push/) - Push metrics to an OpenTelemetry Collector via the OTLP HTTP or gRPC protocol using the Autometrics provided interface +- [opentelemetry-push-custom](./opentelemetry-push-custom/) - Push metrics to an OpenTelemetry Collector via the OTLP gRPC protocol using custom options ## Full Example diff --git a/examples/opentelemetry-push-custom/Cargo.toml b/examples/opentelemetry-push-custom/Cargo.toml new file mode 100644 index 0000000..fca59cc --- /dev/null +++ b/examples/opentelemetry-push-custom/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "example-opentelemetry-push-custom" +version = "0.0.0" +publish = false +edition = "2021" + +[dependencies] +autometrics = { path = "../../autometrics", features = ["opentelemetry-0_20"] } +autometrics-example-util = { path = "../util" } +# Note that the version of the opentelemetry crate MUST match +# the version used by autometrics +opentelemetry = { version = "0.20", features = ["metrics", "rt-tokio"] } +opentelemetry-otlp = { version = "0.13", features = ["tonic", "metrics"] } +opentelemetry-semantic-conventions = { version = "0.12.0" } +tokio = { version = "1", features = ["full"] } diff --git a/examples/opentelemetry-push-custom/README.md b/examples/opentelemetry-push-custom/README.md new file mode 100644 index 0000000..8ff8f73 --- /dev/null +++ b/examples/opentelemetry-push-custom/README.md @@ -0,0 +1,106 @@ +# Autometrics + OTLP push controller (custom) + +This example demonstrates how you can push autometrics via OTLP gRPC protocol to the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or another OTel-compatible solution +without using the Autometrics provided interface. + +## Running the example + +### Start a basic OTEL-Collector + +You can use the [`otel-collector-config.yml`](./otel-collector-config.yml) file to start an otel-collector container that listens on 0.0.0.0:4317 for incoming otlp-gRPC traffic, and exports the metrics it receives to stdout. + +Run the following command in a second terminal to start a container in interactive mode: + +```bash +docker run -it --name otel-col \ + -p 4317:4317 -p 13133:13133 \ + -v $PWD/otel-collector-config.yml:/etc/otelcol/config.yaml \ + otel/opentelemetry-collector:latest +``` + +You should see the collector initialization output, that should end with something like: + +```text +... +2023-06-07T15:56:42.617Z info otlpreceiver@v0.75.0/otlp.go:94 Starting GRPC server {"kind": "receiver", "name": "otlp", "data_type": "metrics", "endpoint": "0.0.0.0:4317"} +2023-06-07T15:56:42.618Z info service/service.go:146 Everything is ready. Begin running and processing data. +``` + +### Execute example code + +Then come back on your primary shell and run this example: + +```shell +cargo run -p example-opentelemetry-push-custom +``` + +### Check the output + +On the stdout of the terminal where you started the opentelemetry-collector container, you should see the metrics generated by autometrics macro being pushed every 10 seconds since example exit, like: + +```text +... +Metric #0 +Descriptor: + -> Name: function.calls + -> Description: Autometrics counter for tracking function calls + -> Unit: + -> DataType: Sum + -> IsMonotonic: true + -> AggregationTemporality: Cumulative +NumberDataPoints #0 +Data point attributes: + -> caller: Str() + -> function: Str(do_stuff) + -> module: Str(example_opentelemetry_push) +StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC +Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC +Value: 10 +Metric #1 +Descriptor: + -> Name: build_info + -> Description: Autometrics info metric for tracking software version and build details + -> Unit: + -> DataType: Sum + -> IsMonotonic: false + -> AggregationTemporality: Cumulative +NumberDataPoints #0 +Data point attributes: + -> branch: Str() + -> commit: Str() + -> version: Str(0.0.0) +StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC +Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC +Value: 1.000000 +Metric #2 +Descriptor: + -> Name: function.calls.duration + -> Description: Autometrics histogram for tracking function call duration + -> Unit: + -> DataType: Sum + -> IsMonotonic: false + -> AggregationTemporality: Cumulative +NumberDataPoints #0 +Data point attributes: + -> function: Str(do_stuff) + -> module: Str(example_opentelemetry_push) +StartTimestamp: 2023-06-07 16:01:08.549300623 +0000 UTC +Timestamp: 2023-06-07 16:01:48.551531429 +0000 UTC +Value: 0.000122 + {"kind": "exporter", "data_type": "metrics", "name": "logging"} +... +``` + +### Cleanup + +In the end, to stop the opentelemetry collector container just hit `^C`. + +Then delete the container with + +```bash +docker rm otel-col +``` + +## OpenTelemetry Metrics Push Controller + +The metric push controller is implemented as from this [example](https://github.com/open-telemetry/opentelemetry-rust/blob/f20c9b40547ee20b6ec99414bb21abdd3a54d99b/examples/basic-otlp/src/main.rs#L35-L52) from `opentelemetry-rust` crate. diff --git a/examples/opentelemetry-push-custom/otel-collector-config.yml b/examples/opentelemetry-push-custom/otel-collector-config.yml new file mode 100644 index 0000000..731c5d0 --- /dev/null +++ b/examples/opentelemetry-push-custom/otel-collector-config.yml @@ -0,0 +1,19 @@ +receivers: + otlp: + protocols: + grpc: + endpoint: 0.0.0.0:4317 + +exporters: + logging: + loglevel: debug + +processors: + batch: + +service: + pipelines: + metrics: + receivers: [otlp] + processors: [] + exporters: [logging] diff --git a/examples/opentelemetry-push-custom/src/main.rs b/examples/opentelemetry-push-custom/src/main.rs new file mode 100644 index 0000000..57ba0b2 --- /dev/null +++ b/examples/opentelemetry-push-custom/src/main.rs @@ -0,0 +1,49 @@ +use autometrics::autometrics; +use autometrics_example_util::sleep_random_duration; +use opentelemetry::metrics::MetricsError; +use opentelemetry::sdk::metrics::MeterProvider; +use opentelemetry::{runtime, Context}; +use opentelemetry_otlp::{ExportConfig, WithExportConfig}; +use std::error::Error; +use std::time::Duration; +use tokio::time::sleep; + +fn init_metrics() -> Result { + let export_config = ExportConfig { + endpoint: "http://localhost:4317".to_string(), + ..ExportConfig::default() + }; + let push_interval = Duration::from_secs(1); + opentelemetry_otlp::new_pipeline() + .metrics(runtime::Tokio) + .with_exporter( + opentelemetry_otlp::new_exporter() + .tonic() + .with_export_config(export_config), + ) + .with_period(push_interval) + .build() +} + +#[autometrics] +async fn do_stuff() { + println!("Doing stuff..."); + sleep_random_duration().await; +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let meter_provider = init_metrics()?; + let cx = Context::current(); + + for _ in 0..100 { + do_stuff().await; + } + + println!("Waiting so that we could see metrics going down..."); + sleep(Duration::from_secs(10)).await; + meter_provider.force_flush(&cx)?; + + meter_provider.shutdown()?; + Ok(()) +} diff --git a/examples/opentelemetry-push/Cargo.toml b/examples/opentelemetry-push/Cargo.toml index dd0262f..b33a36e 100644 --- a/examples/opentelemetry-push/Cargo.toml +++ b/examples/opentelemetry-push/Cargo.toml @@ -5,11 +5,6 @@ publish = false edition = "2021" [dependencies] -autometrics = { path = "../../autometrics", features = ["opentelemetry-0_20"] } +autometrics = { path = "../../autometrics", features = ["opentelemetry-0_20", "otel-push-exporter-http", "otel-push-exporter-tokio"] } autometrics-example-util = { path = "../util" } -# Note that the version of the opentelemetry crate MUST match -# the version used by autometrics -opentelemetry = { version = "0.20", features = ["metrics", "rt-tokio"] } -opentelemetry-otlp = { version = "0.13", features = ["tonic", "metrics"] } -opentelemetry-semantic-conventions = { version = "0.12.0" } tokio = { version = "1", features = ["full"] } diff --git a/examples/opentelemetry-push/README.md b/examples/opentelemetry-push/README.md index 020afd8..7822135 100644 --- a/examples/opentelemetry-push/README.md +++ b/examples/opentelemetry-push/README.md @@ -1,12 +1,14 @@ # Autometrics + OTLP push controller -This example demonstrates how you can push autometrics via OTLP gRPC protocol to the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or another OTel-compatible solution. +This example demonstrates how you can push autometrics via OTLP HTTP and GRPC protocol +to the [OpenTelemetry Collector](https://opentelemetry.io/docs/collector/) or another OTel-compatible solution. It +uses the Autometrics provided wrapper to achieve this. ## Running the example ### Start a basic OTEL-Collector -You can use the [`otel-collector-config.yml`](./otel-collector-config.yml) file to start an otel-collector container that listens on 0.0.0.0:4317 for incoming otlp-gRPC traffic, and exports the metrics it receives to stdout. +You can use the [`otel-collector-config.yml`](./otel-collector-config.yml) file to start an otel-collector container that listens on 0.0.0.0:4317 for incoming otlp-gRPC traffic as well as on 0.0.0.0:4318 for incoming otlp-http traffic, and exports the metrics it receives to stdout. Run the following command in a second terminal to start a container in interactive mode: @@ -14,7 +16,7 @@ Run the following command in a second terminal to start a container in interacti docker run -it --name otel-col \ -p 4317:4317 -p 13133:13133 \ -v $PWD/otel-collector-config.yml:/etc/otelcol/config.yaml \ - otel/opentelemetry-collector:latest + otel/opentelemetry-collector-contrib:latest ``` You should see the collector initialization output, that should end with something like: @@ -99,7 +101,3 @@ Then delete the container with ```bash docker rm otel-col ``` - -## OpenTelemetry Metrics Push Controller - -The metric push controller is implemented as from this [example](https://github.com/open-telemetry/opentelemetry-rust/blob/f20c9b40547ee20b6ec99414bb21abdd3a54d99b/examples/basic-otlp/src/main.rs#L35-L52) from `opentelemetry-rust` crate. diff --git a/examples/opentelemetry-push/otel-collector-config.yml b/examples/opentelemetry-push/otel-collector-config.yml index 731c5d0..1303b66 100644 --- a/examples/opentelemetry-push/otel-collector-config.yml +++ b/examples/opentelemetry-push/otel-collector-config.yml @@ -1,6 +1,8 @@ receivers: otlp: protocols: + http: + endpoint: 0.0.0.0:4318 grpc: endpoint: 0.0.0.0:4317 diff --git a/examples/opentelemetry-push/src/main.rs b/examples/opentelemetry-push/src/main.rs index cee72c6..9616484 100644 --- a/examples/opentelemetry-push/src/main.rs +++ b/examples/opentelemetry-push/src/main.rs @@ -1,30 +1,9 @@ -use autometrics::autometrics; +use autometrics::{autometrics, otel_push_exporter}; use autometrics_example_util::sleep_random_duration; -use opentelemetry::metrics::MetricsError; -use opentelemetry::sdk::metrics::MeterProvider; -use opentelemetry::{runtime, Context}; -use opentelemetry_otlp::{ExportConfig, WithExportConfig}; use std::error::Error; use std::time::Duration; use tokio::time::sleep; -fn init_metrics() -> Result { - let export_config = ExportConfig { - endpoint: "http://localhost:4317".to_string(), - ..ExportConfig::default() - }; - let push_interval = Duration::from_secs(1); - opentelemetry_otlp::new_pipeline() - .metrics(runtime::Tokio) - .with_exporter( - opentelemetry_otlp::new_exporter() - .tonic() - .with_export_config(export_config), - ) - .with_period(push_interval) - .build() -} - #[autometrics] async fn do_stuff() { println!("Doing stuff..."); @@ -33,8 +12,10 @@ async fn do_stuff() { #[tokio::main] async fn main() -> Result<(), Box> { - let meter_provider = init_metrics()?; - let cx = Context::current(); + // NOTICE: the variable gets assigned to `_meter_provider` instead of just `_`, as the later case + // would cause it to be dropped immediately and thus shut down. + let _meter_provider = otel_push_exporter::init_http("http://0.0.0.0:4318")?; + // or: otel_push_exporter::init_grpc("http://0.0.0.0:4317"); for _ in 0..100 { do_stuff().await; @@ -42,7 +23,7 @@ async fn main() -> Result<(), Box> { println!("Waiting so that we could see metrics going down..."); sleep(Duration::from_secs(10)).await; - meter_provider.force_flush(&cx)?; + // no need to call `.shutdown` as the returned `OtelMeterProvider` has a `Drop` implementation Ok(()) }