diff --git a/Cargo.toml b/Cargo.toml index 4cd77211b5..3357c0d54a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,6 +50,7 @@ members = [ "examples/grpc", "examples/http", "examples/zipkin", + "examples/tracing-grpc" ] [[bench]] diff --git a/examples/tracing-grpc/Cargo.toml b/examples/tracing-grpc/Cargo.toml new file mode 100644 index 0000000000..3fde059402 --- /dev/null +++ b/examples/tracing-grpc/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "tracing-grpc" +version = "0.1.0" +edition = "2018" + +[[bin]] # Bin to run the gRPC server +name = "grpc-server" +path = "src/server.rs" + +[[bin]] # Bin to run the gRPC client +name = "grpc-client" +path = "src/client.rs" + +[dependencies] +http = "0.2" +tonic = "0.2" +prost = "0.6" +tokio = { version = "0.2", features = ["full"] } +opentelemetry = "0.5" +opentelemetry-jaeger ="0.4" +tracing = "0.1.14" +tracing-subscriber = "0.2.5" +tracing-opentelemetry = "0.4.0" +tracing-futures = "0.2.4" + +[build-dependencies] +tonic-build = "0.2" diff --git a/examples/tracing-grpc/README.md b/examples/tracing-grpc/README.md new file mode 100644 index 0000000000..33aba9982e --- /dev/null +++ b/examples/tracing-grpc/README.md @@ -0,0 +1,24 @@ +# GRPC example + +Example showing [Tonic] client and server interaction with OpenTelemetry context propagation. [tracing_opentelemetry](https://docs.rs/tracing-opentelemetry/0.4.0/tracing_opentelemetry/) is used to hook into the [tracing](https://github.com/tokio-rs/tracing) ecosystem, which enables drop-in replacements for [log](https://github.com/rust-lang/log) macros and an `#[instrument]` macro that will automatically add spans to your functions. + +[Tonic]: https://github.com/hyperium/tonic + +Examples +-------- + +```shell +# Run jaeger in background +$ docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest + +# Run the server +$ cargo run --bin grpc-server + +# Now run the client to make a request to the server +$ cargo run --bin grpc-client + +# View spans (see the image below) +$ firefox http://localhost:16686/ +``` + +![Jaeger UI](trace.png) \ No newline at end of file diff --git a/examples/tracing-grpc/build.rs b/examples/tracing-grpc/build.rs new file mode 100644 index 0000000000..7c661fe65b --- /dev/null +++ b/examples/tracing-grpc/build.rs @@ -0,0 +1,4 @@ +fn main() -> Result<(), Box> { + tonic_build::compile_protos("proto/helloworld.proto")?; + Ok(()) +} diff --git a/examples/tracing-grpc/proto/helloworld.proto b/examples/tracing-grpc/proto/helloworld.proto new file mode 100644 index 0000000000..b0e60043b1 --- /dev/null +++ b/examples/tracing-grpc/proto/helloworld.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; +package helloworld; + +service Greeter { + // Our SayHello rpc accepts HelloRequests and returns HelloReplies + rpc SayHello (HelloRequest) returns (HelloReply); +} + +message HelloRequest { + // Request message contains the name to be greeted + string name = 1; +} + +message HelloReply { + // Reply contains the greeting message + string message = 1; +} diff --git a/examples/tracing-grpc/src/client.rs b/examples/tracing-grpc/src/client.rs new file mode 100644 index 0000000000..7c69ca3969 --- /dev/null +++ b/examples/tracing-grpc/src/client.rs @@ -0,0 +1,88 @@ +use hello_world::greeter_client::GreeterClient; +use hello_world::HelloRequest; +use opentelemetry::api::{HttpTextFormat, KeyValue, Provider, TraceContextPropagator}; +use opentelemetry::sdk::Sampler; +use opentelemetry::{api, sdk}; +use tracing::*; +use tracing_futures::Instrument; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::prelude::*; + +pub mod hello_world { + tonic::include_proto!("helloworld"); +} + +fn tracing_init() -> Result<(), Box> { + let builder = opentelemetry_jaeger::Exporter::builder() + .with_agent_endpoint("127.0.0.1:6831".parse().unwrap()); + + let exporter = builder + .with_process(opentelemetry_jaeger::Process { + service_name: "grpc-client".to_string(), + tags: vec![KeyValue::new("version", "0.1.0")], + }) + .init()?; + + // For the demonstration, use `Sampler::Always` sampler to sample all traces. In a production + // application, use `Sampler::Parent` or `Sampler::Probability` with a desired probability. + let provider = sdk::Provider::builder() + .with_simple_exporter(exporter) + .with_config(sdk::Config { + default_sampler: Box::new(Sampler::Always), + ..Default::default() + }) + .build(); + let tracer = provider.get_tracer("grpc-client"); + + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init()?; + + Ok(()) +} + +struct TonicMetadataMapCarrier<'a>(&'a mut tonic::metadata::MetadataMap); +impl<'a> api::Carrier for TonicMetadataMapCarrier<'a> { + fn get(&self, key: &'static str) -> Option<&str> { + self.0.get(key).and_then(|metadata| metadata.to_str().ok()) + } + + fn set(&mut self, key: &'static str, value: String) { + if let Ok(key) = tonic::metadata::MetadataKey::from_bytes(key.to_lowercase().as_bytes()) { + self.0.insert( + key, + tonic::metadata::MetadataValue::from_str(&value).unwrap(), + ); + } + } +} + +#[instrument] +async fn greet() -> Result<(), Box> { + let mut client = GreeterClient::connect("http://[::1]:50051") + .instrument(info_span!("client connect")) + .await?; + let propagator = TraceContextPropagator::new(); + let cx = tracing::Span::current().context(); + + let mut request = tonic::Request::new(HelloRequest { + name: "Tonic".into(), + }); + propagator.inject_context(&cx, &mut TonicMetadataMapCarrier(request.metadata_mut())); + + let response = client + .say_hello(request) + .instrument(info_span!("say_hello")) + .await?; + info!("Response received: {:?}", response); + Ok(()) +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_init()?; + greet().await?; + + Ok(()) +} diff --git a/examples/tracing-grpc/src/server.rs b/examples/tracing-grpc/src/server.rs new file mode 100644 index 0000000000..262abcb739 --- /dev/null +++ b/examples/tracing-grpc/src/server.rs @@ -0,0 +1,103 @@ +use hello_world::greeter_server::{Greeter, GreeterServer}; +use hello_world::{HelloReply, HelloRequest}; +use opentelemetry::api::{self, HttpTextFormat, KeyValue, Provider}; +use opentelemetry::sdk::{self, Sampler}; +use tonic::{transport::Server, Request, Response, Status}; +use tracing::*; +use tracing_opentelemetry::OpenTelemetrySpanExt; +use tracing_subscriber::prelude::*; + +pub mod hello_world { + tonic::include_proto!("helloworld"); // The string specified here must match the proto package name +} + +#[instrument] +fn expensive_fn(to_print: String) { + for _ in 0..5 { + std::thread::sleep(std::time::Duration::from_secs(1)); + info!("{}", to_print); + } +} + +#[derive(Debug, Default)] +pub struct MyGreeter {} + +#[tonic::async_trait] +impl Greeter for MyGreeter { + #[instrument] + async fn say_hello( + &self, + request: Request, // Accept request of type HelloRequest + ) -> Result, Status> { + let propagator = api::TraceContextPropagator::new(); + let parent_cx = propagator.extract(&HttpHeaderMapCarrier(request.metadata())); + let span = tracing::Span::current(); + span.set_parent(&parent_cx); + let name = request.into_inner().name; + expensive_fn(format!("Got name: {:?}", name)); + + // Return an instance of type HelloReply + let reply = hello_world::HelloReply { + message: format!("Hello {}!", name), // We must use .into_inner() as the fields of gRPC requests and responses are private + }; + + Ok(Response::new(reply)) // Send back our formatted greeting + } +} + +fn tracing_init() -> Result<(), Box> { + let builder = opentelemetry_jaeger::Exporter::builder() + .with_agent_endpoint("127.0.0.1:6831".parse().unwrap()); + + let exporter = builder + .with_process(opentelemetry_jaeger::Process { + service_name: "grpc-server".to_string(), + tags: vec![KeyValue::new("version", "0.1.0")], + }) + .init()?; + + // For the demonstration, use `Sampler::Always` sampler to sample all traces. In a production + // application, use `Sampler::Parent` or `Sampler::Probability` with a desired probability. + let provider = sdk::Provider::builder() + .with_simple_exporter(exporter) + .with_config(sdk::Config { + default_sampler: Box::new(Sampler::Always), + ..Default::default() + }) + .build(); + let tracer = provider.get_tracer("grpc-server"); + + let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); + tracing_subscriber::registry() + .with(opentelemetry) + .try_init()?; + + Ok(()) +} + +struct HttpHeaderMapCarrier<'a>(&'a tonic::metadata::MetadataMap); +impl<'a> api::Carrier for HttpHeaderMapCarrier<'a> { + fn get(&self, key: &'static str) -> Option<&str> { + self.0 + .get(key.to_lowercase().as_str()) + .and_then(|value| value.to_str().ok()) + } + + fn set(&mut self, _key: &'static str, _value: String) { + unimplemented!() + } +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + tracing_init()?; + let addr = "[::1]:50051".parse()?; + let greeter = MyGreeter::default(); + + Server::builder() + .add_service(GreeterServer::new(greeter)) + .serve(addr) + .await?; + + Ok(()) +} diff --git a/examples/tracing-grpc/trace.png b/examples/tracing-grpc/trace.png new file mode 100644 index 0000000000..f587ac8049 Binary files /dev/null and b/examples/tracing-grpc/trace.png differ