Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add otel push exporter #137

Merged
merged 8 commits into from
Sep 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
33 changes: 33 additions & 0 deletions autometrics/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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 = [
Expand Down Expand Up @@ -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
Expand Down
19 changes: 19 additions & 0 deletions autometrics/src/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 2 additions & 0 deletions autometrics/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
160 changes: 160 additions & 0 deletions autometrics/src/otel_push_exporter.rs
Original file line number Diff line number Diff line change
@@ -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<String>) -> Result<OtelMeterProvider, MetricsError> {
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<String>,
timeout: Duration,
period: Duration,
) -> Result<OtelMeterProvider, MetricsError> {
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<String>) -> Result<OtelMeterProvider, MetricsError> {
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<String>,
timeout: Duration,
period: Duration,
) -> Result<OtelMeterProvider, MetricsError> {
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<opentelemetry_sdk::runtime::Tokio> {
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<opentelemetry_sdk::runtime::TokioCurrentThread> {
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<opentelemetry_sdk::runtime::AsyncStd> {
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")
}
2 changes: 1 addition & 1 deletion autometrics/tests/compile_test.rs
Original file line number Diff line number Diff line change
@@ -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
Expand Down
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
15 changes: 15 additions & 0 deletions examples/opentelemetry-push-custom/Cargo.toml
Original file line number Diff line number Diff line change
@@ -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"] }
106 changes: 106 additions & 0 deletions examples/opentelemetry-push-custom/README.md
Original file line number Diff line number Diff line change
@@ -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 [email protected]/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.
Loading