From 384f99b63109d87cf5201ff1d7e93060b4cffa6f Mon Sep 17 00:00:00 2001
From: Mark Ingram <mark@lincs.dev>
Date: Tue, 4 Jun 2024 08:49:58 +0100
Subject: [PATCH] [opentelemetry-otlp] adds an example HTTP exporter backed by
 a Hyper 0.14 Client

Resolves #1659
---
 opentelemetry-otlp/CHANGELOG.md               |   1 +
 .../examples/basic-otlp-http-hyper/Cargo.toml |  20 +++
 .../examples/basic-otlp-http-hyper/README.md  | 116 ++++++++++++++++++
 .../basic-otlp-http-hyper/docker-compose.yaml |  15 +++
 .../otel-collector-config.yaml                |  21 ++++
 .../basic-otlp-http-hyper/src/main.rs         | 107 ++++++++++++++++
 6 files changed, 280 insertions(+)
 create mode 100644 opentelemetry-otlp/examples/basic-otlp-http-hyper/Cargo.toml
 create mode 100644 opentelemetry-otlp/examples/basic-otlp-http-hyper/README.md
 create mode 100644 opentelemetry-otlp/examples/basic-otlp-http-hyper/docker-compose.yaml
 create mode 100644 opentelemetry-otlp/examples/basic-otlp-http-hyper/otel-collector-config.yaml
 create mode 100644 opentelemetry-otlp/examples/basic-otlp-http-hyper/src/main.rs

diff --git a/opentelemetry-otlp/CHANGELOG.md b/opentelemetry-otlp/CHANGELOG.md
index 5ec7699d2a3..0d7976b49ce 100644
--- a/opentelemetry-otlp/CHANGELOG.md
+++ b/opentelemetry-otlp/CHANGELOG.md
@@ -16,6 +16,7 @@ now use `.with_resource(RESOURCE::default())` to configure Resource when using
   These methods would also no longer set the global tracer provider. It would now be the responsibility of users to set it by calling `global::set_tracer_provider(tracer_provider.clone());`. Refer to the [basic-otlp](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp/src/main.rs) and [basic-otlp-http](https://github.com/open-telemetry/opentelemetry-rust/blob/main/opentelemetry-otlp/examples/basic-otlp-http/src/main.rs) examples on how to initialize OTLP Trace Exporter.
 - **Breaking** Correct the misspelling of "webkpi" to "webpki" in features [#1842](https://github.com/open-telemetry/opentelemetry-rust/pull/1842)
 - Bump MSRV to 1.70 [#1840](https://github.com/open-telemetry/opentelemetry-rust/pull/1840)
+- Adds `basic-otlp-http-hyper` example showing how to export with a custom Hyper Client
 
 ## v0.16.0
 
diff --git a/opentelemetry-otlp/examples/basic-otlp-http-hyper/Cargo.toml b/opentelemetry-otlp/examples/basic-otlp-http-hyper/Cargo.toml
new file mode 100644
index 00000000000..044e1f3fa43
--- /dev/null
+++ b/opentelemetry-otlp/examples/basic-otlp-http-hyper/Cargo.toml
@@ -0,0 +1,20 @@
+[package]
+name = "basic-otlp-http-hyper"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+publish = false
+
+[dependencies]
+async-trait = { workspace = true }
+bytes = { workspace = true }
+once_cell = { workspace = true }
+opentelemetry = { path = "../../../opentelemetry" }
+opentelemetry_sdk = { path = "../../../opentelemetry-sdk", features = ["rt-tokio"] }
+opentelemetry-http = { path = "../../../opentelemetry-http" }
+opentelemetry-otlp = { path = "../..", features = ["http-proto"] }
+opentelemetry-semantic-conventions = { path = "../../../opentelemetry-semantic-conventions" }
+
+http = { workspace = true }
+hyper = { workspace = true, features = ["client"] }
+tokio = { workspace = true, features = ["full"] }
diff --git a/opentelemetry-otlp/examples/basic-otlp-http-hyper/README.md b/opentelemetry-otlp/examples/basic-otlp-http-hyper/README.md
new file mode 100644
index 00000000000..a23bd92dad2
--- /dev/null
+++ b/opentelemetry-otlp/examples/basic-otlp-http-hyper/README.md
@@ -0,0 +1,116 @@
+# Basic OTLP exporter Example - Hyper 0.14
+
+This example shows how to setup OpenTelemetry OTLP exporter for traces to exports them to the [OpenTelemetry
+Collector](https://github.com/open-telemetry/opentelemetry-collector) via OTLP
+over HTTP/protobuf. The Collector then sends the data to the appropriate
+backend, in this case, the logging Exporter, which displays data to console.
+
+This example uses a simple implementation of the `HttpClient` backed by a Hyper Client.
+
+## Usage
+
+### `docker-compose`
+
+By default runs against the `otel/opentelemetry-collector:latest` image, and uses `reqwest-client`
+as the http client, using http as the transport.
+
+```shell
+docker-compose up
+```
+
+In another terminal run the application `cargo run`
+
+The docker-compose terminal will display traces.
+
+Press Ctrl+C to stop the collector, and then tear it down:
+
+```shell
+docker-compose down
+```
+
+### Manual
+
+If you don't want to use `docker-compose`, you can manually run the `otel/opentelemetry-collector` container
+and inspect the logs to see traces being transferred.
+
+On Unix based systems use:
+
+```shell
+# From the current directory, run `opentelemetry-collector`
+docker run --rm -it -p 4318:4318 -v $(pwd):/cfg otel/opentelemetry-collector:latest --config=/cfg/otel-collector-config.yaml
+```
+
+On Windows use:
+
+```shell
+# From the current directory, run `opentelemetry-collector`
+docker run --rm -it -p 4318:4318 -v "%cd%":/cfg otel/opentelemetry-collector:latest --config=/cfg/otel-collector-config.yaml
+```
+
+Run the app which exports traces via OTLP to the collector
+
+```shell
+cargo run
+```
+
+## View results
+
+You should be able to see something similar below with different time and ID in the same console that docker runs.
+
+### Span
+
+```text
+...
+2024-06-04T07:39:05.722Z     info    ResourceSpans #0
+Resource SchemaURL: 
+Resource attributes:
+     -> service.name: Str(basic-otlp-http-hyper)
+ScopeSpans #0
+ScopeSpans SchemaURL: 
+InstrumentationScope basic 
+Span #0
+    Trace ID       : 0e833514074391284d809e71afd34931
+    Parent ID      : b5ce8f2bdedd0698
+    ID             : c2555b67b2072134
+    Name           : Sub operation...
+    Kind           : Internal
+    Start time     : 2024-06-04 07:39:05.698201 +0000 UTC
+    End time       : 2024-06-04 07:39:05.698213 +0000 UTC
+    Status code    : Unset
+    Status message : 
+Attributes:
+     -> another.key: Str(yes)
+Events:
+SpanEvent #0
+     -> Name: Sub span event
+     -> Timestamp: 2024-06-04 07:39:05.698206 +0000 UTC
+     -> DroppedAttributesCount: 0
+ResourceSpans #1
+Resource SchemaURL: 
+Resource attributes:
+     -> service.name: Str(basic-otlp-http-hyper)
+ScopeSpans #0
+ScopeSpans SchemaURL: 
+InstrumentationScope basic 
+Span #0
+    Trace ID       : 0e833514074391284d809e71afd34931
+    Parent ID      : 
+    ID             : b5ce8f2bdedd0698
+    Name           : Main operation
+    Kind           : Internal
+    Start time     : 2024-06-04 07:39:05.698168 +0000 UTC
+    End time       : 2024-06-04 07:39:05.698223 +0000 UTC
+    Status code    : Unset
+    Status message : 
+Attributes:
+     -> another.key: Str(yes)
+Events:
+SpanEvent #0
+     -> Name: Nice operation!
+     -> Timestamp: 2024-06-04 07:39:05.698186 +0000 UTC
+     -> DroppedAttributesCount: 0
+     -> Attributes::
+          -> bogons: Int(100)
+     {"kind": "exporter", "data_type": "traces", "name": "logging"}
+...
+```
diff --git a/opentelemetry-otlp/examples/basic-otlp-http-hyper/docker-compose.yaml b/opentelemetry-otlp/examples/basic-otlp-http-hyper/docker-compose.yaml
new file mode 100644
index 00000000000..dc9d1e7a5d4
--- /dev/null
+++ b/opentelemetry-otlp/examples/basic-otlp-http-hyper/docker-compose.yaml
@@ -0,0 +1,15 @@
+version: "2"
+services:
+
+  # Collector
+  otel-collector:
+    image: otel/opentelemetry-collector:latest
+    command: ["--config=/etc/otel-collector-config.yaml", "${OTELCOL_ARGS}"]
+    volumes:
+      - ./otel-collector-config.yaml:/etc/otel-collector-config.yaml
+    ports:
+      - "4318:4318"   # OTLP HTTP receiver
+
+
+
+
diff --git a/opentelemetry-otlp/examples/basic-otlp-http-hyper/otel-collector-config.yaml b/opentelemetry-otlp/examples/basic-otlp-http-hyper/otel-collector-config.yaml
new file mode 100644
index 00000000000..dd7d7ad40f2
--- /dev/null
+++ b/opentelemetry-otlp/examples/basic-otlp-http-hyper/otel-collector-config.yaml
@@ -0,0 +1,21 @@
+# This is a configuration file for the OpenTelemetry Collector intended to be
+# used in conjunction with the opentelemetry-otlp example.
+#
+# For more information about the OpenTelemetry Collector see:
+#   https://github.com/open-telemetry/opentelemetry-collector
+#
+receivers:
+  otlp:
+    protocols:
+      grpc:
+      http:
+
+exporters:
+  logging:
+    loglevel: debug
+
+service:
+  pipelines:
+    traces:
+      receivers: [otlp]
+      exporters: [logging]
diff --git a/opentelemetry-otlp/examples/basic-otlp-http-hyper/src/main.rs b/opentelemetry-otlp/examples/basic-otlp-http-hyper/src/main.rs
new file mode 100644
index 00000000000..fc64ac97c80
--- /dev/null
+++ b/opentelemetry-otlp/examples/basic-otlp-http-hyper/src/main.rs
@@ -0,0 +1,107 @@
+use async_trait::async_trait;
+use bytes::Bytes;
+use http::{Request, Response};
+use hyper::client::{connect::Connect, HttpConnector};
+use hyper::{Body, Client};
+use once_cell::sync::Lazy;
+use opentelemetry::{
+    global,
+    trace::{TraceContextExt, TraceError, Tracer, TracerProvider as _},
+    Key, KeyValue,
+};
+use opentelemetry_http::{HttpClient, HttpError, ResponseExt};
+use opentelemetry_otlp::WithExportConfig;
+use opentelemetry_sdk::trace::{self as sdktrace, Config};
+use opentelemetry_sdk::Resource;
+
+use std::error::Error;
+
+static RESOURCE: Lazy<Resource> = Lazy::new(|| {
+    Resource::new(vec![KeyValue::new(
+        opentelemetry_semantic_conventions::resource::SERVICE_NAME,
+        "basic-otlp-http-hyper",
+    )])
+});
+
+struct HyperClient<C> {
+    inner: hyper::Client<C>,
+}
+
+impl Default for HyperClient<HttpConnector> {
+    fn default() -> Self {
+        Self {
+            inner: Client::new(),
+        }
+    }
+}
+
+impl<C> std::fmt::Debug for HyperClient<C> {
+    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+        f.debug_struct("HyperClient")
+            .field("inner", &self.inner)
+            .finish()
+    }
+}
+
+#[async_trait]
+impl<C: Connect + Clone + Send + Sync + 'static> HttpClient for HyperClient<C> {
+    async fn send(&self, request: Request<Vec<u8>>) -> Result<Response<Bytes>, HttpError> {
+        let request = request.map(Body::from);
+
+        let (parts, body) = self
+            .inner
+            .request(request)
+            .await?
+            .error_for_status()?
+            .into_parts();
+        let body = hyper::body::to_bytes(body).await?;
+
+        Ok(Response::from_parts(parts, body))
+    }
+}
+
+fn init_tracer_provider() -> Result<sdktrace::TracerProvider, TraceError> {
+    opentelemetry_otlp::new_pipeline()
+        .tracing()
+        .with_exporter(
+            opentelemetry_otlp::new_exporter()
+                .http()
+                .with_http_client(HyperClient::default())
+                .with_endpoint("http://localhost:4318/v1/traces"),
+        )
+        .with_trace_config(Config::default().with_resource(RESOURCE.clone()))
+        .install_batch(opentelemetry_sdk::runtime::Tokio)
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
+    let result = init_tracer_provider();
+    assert!(
+        result.is_ok(),
+        "Init tracer failed with error: {:?}",
+        result.err()
+    );
+
+    let tracer_provider = result.unwrap();
+    global::set_tracer_provider(tracer_provider.clone());
+
+    let tracer = global::tracer_provider().tracer_builder("basic").build();
+
+    tracer.in_span("Main operation", |cx| {
+        let span = cx.span();
+        span.add_event(
+            "Nice operation!".to_string(),
+            vec![Key::new("bogons").i64(100)],
+        );
+        span.set_attribute(KeyValue::new("another.key", "yes"));
+
+        tracer.in_span("Sub operation...", |cx| {
+            let span = cx.span();
+            span.set_attribute(KeyValue::new("another.key", "yes"));
+            span.add_event("Sub span event", vec![]);
+        });
+    });
+
+    global::shutdown_tracer_provider();
+    Ok(())
+}