diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 5e5e73817f1..7f4bf8ce32d 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -546,7 +546,7 @@ jobs: export TAG='${{ needs.build.outputs.stable_version }}' services='editoast core front gateway' docker compose pull $services - docker compose up --no-build -d $services + docker compose up --no-build -d $services jaeger env: DOCKER_BUILDKIT: 1 COMPOSE_DOCKER_CLI_BUILD: 1 diff --git a/docker-compose.yml b/docker-compose.yml index 8e7afd11f90..4193a670170 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -111,6 +111,14 @@ services: restart: unless-stopped ports: ["4000:4000"] + jaeger: + image: jaegertracing/all-in-one:latest + container_name: osrd-jaeger + restart: unless-stopped + ports: + - "4317:4317" + - "16686:16686" + wait-healthy: depends_on: editoast: {condition: service_healthy} diff --git a/docker/docker-compose.host.yml b/docker/docker-compose.host.yml index cd64233ebb8..7dd0a984143 100644 --- a/docker/docker-compose.host.yml +++ b/docker/docker-compose.host.yml @@ -35,6 +35,10 @@ services: volumes: - "./docker/gateway.dev.host.toml:/gateway.toml" + jaeger: + ports: [] + network_mode: host + wait-healthy: depends_on: editoast: {condition: service_healthy} diff --git a/docker/gateway.dev.host.toml b/docker/gateway.dev.host.toml index 0564f05c085..7c15f9143fa 100644 --- a/docker/gateway.dev.host.toml +++ b/docker/gateway.dev.host.toml @@ -6,12 +6,18 @@ secret_key = "NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECR listen_addr = "0.0.0.0" +[telemetry.tracing] +type = "OpenTelemetry" +endpoint = "http://localhost:4317" + [[targets]] +tracing_name = "editoast" prefix = "/api" upstream = "http://localhost:8090" require_auth = true [[targets]] +tracing_name = "front" upstream = "http://localhost:3000" require_auth = false diff --git a/docker/gateway.dev.simple.toml b/docker/gateway.dev.simple.toml index d4a2a3c10db..ac719d3723f 100644 --- a/docker/gateway.dev.simple.toml +++ b/docker/gateway.dev.simple.toml @@ -6,12 +6,18 @@ secret_key = "NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECRET++NOT+A+SECR listen_addr = "0.0.0.0" +[telemetry.tracing] +type = "OpenTelemetry" +endpoint = "http://osrd-jaeger:4317" + [[targets]] +tracing_name = "editoast" prefix = "/api" upstream = "http://osrd-editoast:80" require_auth = true [[targets]] +tracing_name = "front" upstream = "http://osrd-front:3000" require_auth = false diff --git a/gateway/Cargo.lock b/gateway/Cargo.lock index 18300b957fa..e308e90fe80 100644 --- a/gateway/Cargo.lock +++ b/gateway/Cargo.lock @@ -301,6 +301,21 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "actix-web-opentelemetry" +version = "0.16.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "d164a9fe425132ff4ae92ce04912f7e31529b48a6e7cdce769ab45d272de285c" +dependencies = [ + "actix-http", + "actix-web", + "awc", + "futures-util", + "opentelemetry", + "opentelemetry-semantic-conventions", + "serde", +] + [[package]] name = "actix_auth" version = "0.1.0" @@ -336,6 +351,7 @@ dependencies = [ "actix", "actix-web", "actix-web-actors", + "actix-web-opentelemetry", "awc", "bytestring", "dyn-clone", @@ -343,6 +359,8 @@ dependencies = [ "futures-util", "ipnet", "log", + "opentelemetry", + "opentelemetry_sdk", "percent-encoding", "phf", "smallvec", @@ -522,6 +540,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "axum" +version = "0.6.20" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "3b829e4e32b91e643de6eafe82b1d90675f5874230191a4ffbc1b336dec4d6bf" +dependencies = [ + "async-trait", + "axum-core", + "bitflags 1.3.2", + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "sync_wrapper", + "tower", + "tower-layer", + "tower-service", +] + +[[package]] +name = "axum-core" +version = "0.3.4" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "759fa577a247914fd3f7f76d62972792636412fbfd634cd452f6a385a74d2d2c" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -1086,6 +1149,17 @@ version = "0.3.29" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "eb1d22c66e66d9d72e1758f0bd7d4fd0bee04cad842ee34587d68c07e45d088c" +[[package]] +name = "futures-executor" +version = "0.3.29" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "0f4fb8693db0cf099eadcca0efe2a5a22e4550f98ed16aba6c48700da29597bc" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.29" @@ -1172,6 +1246,12 @@ version = "0.28.0" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "glob" +version = "0.3.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" + [[package]] name = "group" version = "0.13.0" @@ -1338,6 +1418,18 @@ dependencies = [ "tokio-rustls 0.24.1", ] +[[package]] +name = "hyper-timeout" +version = "0.4.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "bbb958482e8c7be4bc3cf272a766a2b0bf1a6755e7a6ae777f017a31d11b13b1" +dependencies = [ + "hyper", + "pin-project-lite", + "tokio", + "tokio-io-timeout", +] + [[package]] name = "iana-time-zone" version = "0.1.58" @@ -1446,6 +1538,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.11.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "b1c173a5686ce8bfa551b3563d0c2170bf24ca44da99c7ca4bfdab5418c3fe57" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.9" @@ -1536,6 +1637,12 @@ version = "0.4.20" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "memchr" version = "2.6.4" @@ -1690,7 +1797,7 @@ dependencies = [ "ed25519-dalek", "hmac", "http", - "itertools", + "itertools 0.10.5", "log", "oauth2", "p256", @@ -1716,6 +1823,116 @@ version = "0.1.5" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" +[[package]] +name = "opentelemetry" +version = "0.21.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "1e32339a5dc40459130b3bd269e9892439f55b33e772d2a9d402a789baaf4e8a" +dependencies = [ + "futures-core", + "futures-sink", + "indexmap 2.1.0", + "js-sys", + "once_cell", + "pin-project-lite", + "thiserror", + "urlencoding", +] + +[[package]] +name = "opentelemetry-datadog" +version = "0.9.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "3e09667367cb509f10d7cf5960a83f9c4d96e93715f750b164b4b98d46c3cbf4" +dependencies = [ + "futures-core", + "http", + "indexmap 2.1.0", + "itertools 0.11.0", + "once_cell", + "opentelemetry", + "opentelemetry-http", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "rmp", + "thiserror", + "url", +] + +[[package]] +name = "opentelemetry-http" +version = "0.10.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "7f51189ce8be654f9b5f7e70e49967ed894e84a06fc35c6c042e64ac1fc5399e" +dependencies = [ + "async-trait", + "bytes", + "http", + "opentelemetry", +] + +[[package]] +name = "opentelemetry-otlp" +version = "0.14.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" +dependencies = [ + "async-trait", + "futures-core", + "http", + "opentelemetry", + "opentelemetry-proto", + "opentelemetry-semantic-conventions", + "opentelemetry_sdk", + "prost", + "thiserror", + "tokio", + "tonic", +] + +[[package]] +name = "opentelemetry-proto" +version = "0.4.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "a2e155ce5cc812ea3d1dffbd1539aed653de4bf4882d60e6e04dcf0901d674e1" +dependencies = [ + "opentelemetry", + "opentelemetry_sdk", + "prost", + "tonic", +] + +[[package]] +name = "opentelemetry-semantic-conventions" +version = "0.13.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "f5774f1ef1f982ef2a447f6ee04ec383981a3ab99c8e77a1a7b30182e65bbc84" +dependencies = [ + "opentelemetry", +] + +[[package]] +name = "opentelemetry_sdk" +version = "0.21.1" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "968ba3f2ca03e90e5187f5e4f46c791ef7f2c163ae87789c8ce5f5ca3b7b7de5" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float 4.2.0", + "percent-encoding", + "rand", + "thiserror", + "tokio", + "tokio-stream", +] + [[package]] name = "ordered-float" version = "2.10.1" @@ -1725,6 +1942,15 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ordered-float" +version = "4.2.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "a76df7075c7d4d01fdcb46c912dd17fba5b60c78ea480b475f2b6ab6f666584e" +dependencies = [ + "num-traits", +] + [[package]] name = "osrd_gateway" version = "0.1.0" @@ -1732,6 +1958,7 @@ dependencies = [ "actix-files", "actix-session", "actix-web", + "actix-web-opentelemetry", "actix_auth", "actix_proxy", "base64ct", @@ -1741,6 +1968,10 @@ dependencies = [ "humantime", "humantime-serde", "log", + "opentelemetry", + "opentelemetry-datadog", + "opentelemetry-otlp", + "opentelemetry_sdk", "serde", ] @@ -1877,6 +2108,26 @@ dependencies = [ "siphasher", ] +[[package]] +name = "pin-project" +version = "1.1.3" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "fda4ed1c6c173e3fc7a83629421152e01d7b1f9b7f65fb301e490e8cfc656422" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.3" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "4359fd9c9171ec6e8c62926d6faaf553a8dc3f64e1507e76da7911b4f6a04405" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "pin-project-lite" version = "0.2.13" @@ -1977,6 +2228,29 @@ dependencies = [ "yansi", ] +[[package]] +name = "prost" +version = "0.11.9" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "0b82eaa1d779e9a4bc1c3217db8ffbeabaae1dca241bf70183242128d48681cd" +dependencies = [ + "bytes", + "prost-derive", +] + +[[package]] +name = "prost-derive" +version = "0.11.9" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "e5d2d8d10f3c6ded6da8b05b5fb3b8a5082514344d56c9f871412d29b4e075b4" +dependencies = [ + "anyhow", + "itertools 0.10.5", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.33" @@ -2133,6 +2407,17 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "rmp" +version = "0.8.12" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "7f9860a6cc38ed1da53456442089b4dfa35e7cedaa326df63017af88385e6b20" +dependencies = [ + "byteorder", + "num-traits", + "paste", +] + [[package]] name = "rsa" version = "0.9.3" @@ -2236,6 +2521,12 @@ dependencies = [ "untrusted 0.9.0", ] +[[package]] +name = "rustversion" +version = "1.0.14" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "7ffc183a10b4478d04cbbbfc96d0873219d962dd5accaff2ffbd4ceb7df837f4" + [[package]] name = "ryu" version = "1.0.15" @@ -2325,7 +2616,7 @@ version = "0.7.0" source = "registry+/~https://github.com/rust-lang/crates.io-index" checksum = "f3a1a3341211875ef120e117ea7fd5228530ae7e7036a779fdc9117be6b3282c" dependencies = [ - "ordered-float", + "ordered-float 2.10.1", "serde", ] @@ -2558,6 +2849,12 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "system-configuration" version = "0.5.1" @@ -2667,9 +2964,31 @@ dependencies = [ "pin-project-lite", "signal-hook-registry", "socket2 0.5.5", + "tokio-macros", "windows-sys", ] +[[package]] +name = "tokio-io-timeout" +version = "1.2.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "30b74022ada614a1b4834de765f9bb43877f910cc8ce4be40e89042c9223a8bf" +dependencies = [ + "pin-project-lite", + "tokio", +] + +[[package]] +name = "tokio-macros" +version = "2.2.0" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "tokio-rustls" version = "0.23.4" @@ -2691,6 +3010,17 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-stream" +version = "0.1.14" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +dependencies = [ + "futures-core", + "pin-project-lite", + "tokio", +] + [[package]] name = "tokio-util" version = "0.7.10" @@ -2739,6 +3069,60 @@ dependencies = [ "winnow", ] +[[package]] +name = "tonic" +version = "0.9.2" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "3082666a3a6433f7f511c7192923fa1fe07c69332d3c6a2e6bb040b569199d5a" +dependencies = [ + "async-trait", + "axum", + "base64 0.21.5", + "bytes", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "hyper", + "hyper-timeout", + "percent-encoding", + "pin-project", + "prost", + "tokio", + "tokio-stream", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "indexmap 1.9.3", + "pin-project", + "pin-project-lite", + "rand", + "slab", + "tokio", + "tokio-util", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -2753,9 +3137,21 @@ checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" dependencies = [ "log", "pin-project-lite", + "tracing-attributes", "tracing-core", ] +[[package]] +name = "tracing-attributes" +version = "0.1.27" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + [[package]] name = "tracing-core" version = "0.1.32" @@ -2850,6 +3246,12 @@ dependencies = [ "serde", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+/~https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "version_check" version = "0.9.4" diff --git a/gateway/Cargo.toml b/gateway/Cargo.toml index d3ce3e0a65c..c87082a5367 100644 --- a/gateway/Cargo.toml +++ b/gateway/Cargo.toml @@ -31,6 +31,11 @@ actix-web = "4.4.0" actix-session = "0.8" actix-files = "0.6.2" base64ct = "1.6.0" +opentelemetry = "0.21.0" +opentelemetry_sdk = { version = "0.21.1", features = ["rt-tokio-current-thread", "metrics"] } +opentelemetry-datadog = "0.9.0" +opentelemetry-otlp = { version = "0.14.0" } +actix-web-opentelemetry = { version = "0.16.0", features = ["awc", "metrics"] } # reverse proxy dependencies ipnet = "2.9" @@ -66,3 +71,10 @@ actix-files.workspace = true actix-session = { workspace = true, features = ["cookie-session"] } actix_auth = { path = "./actix_auth" } actix_proxy = { path = "./actix_proxy" } + +# tracing / metrics +opentelemetry.workspace = true +opentelemetry_sdk.workspace = true +opentelemetry-datadog.workspace = true +opentelemetry-otlp.workspace = true +actix-web-opentelemetry.workspace = true diff --git a/gateway/actix_proxy/Cargo.toml b/gateway/actix_proxy/Cargo.toml index 19788e0359f..3ac4c1e3f72 100644 --- a/gateway/actix_proxy/Cargo.toml +++ b/gateway/actix_proxy/Cargo.toml @@ -29,3 +29,7 @@ actix.workspace = true actix-web-actors.workspace = true bytestring.workspace = true +# tracing / metrics +opentelemetry.workspace = true +actix-web-opentelemetry.workspace = true +opentelemetry_sdk.workspace = true \ No newline at end of file diff --git a/gateway/actix_proxy/src/lib.rs b/gateway/actix_proxy/src/lib.rs index 91b802c4ad2..5bd4438f6dd 100644 --- a/gateway/actix_proxy/src/lib.rs +++ b/gateway/actix_proxy/src/lib.rs @@ -13,10 +13,20 @@ use actix_web::{ web, FromRequest, HttpRequest, HttpResponse, ResponseError, }; use actix_web_actors::ws; +use actix_web_opentelemetry::ClientExt; use awc::Client; use either::Either; use futures_util::future::LocalBoxFuture; use log::{debug, warn}; +use opentelemetry::{ + global::{self, BoxedTracer}, + trace::Tracer, + Context, +}; +use opentelemetry::{ + trace::{TraceContextExt, TracerProvider}, + KeyValue, +}; use percent_encoding::{utf8_percent_encode, AsciiSet}; use awc::error::{ConnectError, SendRequestError as AwcSendRequestError}; @@ -67,6 +77,7 @@ pub struct Proxy { upstream_authority: Authority, upstream_path_prefix: String, timeout: Option, + tracing_name: Option, } /// The set of characters that have to be percent encoded in the path. @@ -93,6 +104,15 @@ const REQUIRES_PATH_ENCODING: &AsciiSet = &percent_encoding::CONTROLS .add(b'>') .add(b'?'); +fn get_tracer() -> BoxedTracer { + global::tracer_provider().versioned_tracer( + "actix_proxy", + Some(env!("CARGO_PKG_VERSION")), + Some("https://opentelemetry.io/schemas/1.17.0"), + None, + ) +} + impl Proxy { pub fn new( mount_path: Option, @@ -101,6 +121,7 @@ impl Proxy { forwarded_headers: Option>, request_modifier: Option>, timeout: Option, + tracing_name: Option, ) -> Self { let upstream_scheme = upstream.scheme_str().unwrap().to_owned(); let upstream_authority = upstream @@ -113,6 +134,7 @@ impl Proxy { } Self { + tracing_name, mount_path: match mount_path { Some(prefix) => prefix.trim_end_matches('/').to_owned(), None => "/".to_owned(), @@ -177,13 +199,11 @@ impl ServiceFactory for Proxy { client = client.timeout(timeout); } - ready(Ok(ProxyService { - inner: Rc::new(InnerProxyService { - client: client.finish(), - proxy: self.clone(), - request_modifier: self.request_modifier.clone(), - }), - })) + ready(Ok(ProxyService::new( + client.finish(), + self.clone(), + self.request_modifier.clone(), + ))) } } @@ -236,13 +256,17 @@ impl InnerProxyService { stream: web::Payload, unprocessed_path: &str, query: &str, + context: Context, ) -> Result { match req.headers().get(&header::UPGRADE) { Some(header) if header.as_bytes() == b"websocket" => { - self.handle_websocket(req, stream, unprocessed_path, query) + self.handle_websocket(req, stream, unprocessed_path, query, context) + .await + } + _ => { + self.handle_http(req, stream, unprocessed_path, query, context) .await } - _ => self.handle_http(req, stream, unprocessed_path, query).await, } } @@ -290,8 +314,15 @@ impl InnerProxyService { stream: web::Payload, unprocessed_path: &str, query: &str, + context: Context, ) -> Result { let back_uri = self.proxy.rebase_uri(Some("ws"), unprocessed_path, query); + context + .span() + .set_attribute(KeyValue::new("proxy.upstream_uri", back_uri.to_string())); + context + .span() + .set_attribute(KeyValue::new("proxy.type", "ws")); debug!("proxy: websocket - received request forwarded to {back_uri}"); // open a websocket connection to the backend @@ -354,8 +385,15 @@ impl InnerProxyService { stream: web::Payload, unprocessed_path: &str, query: &str, + context: Context, ) -> Result { let back_uri = self.proxy.rebase_uri(None, unprocessed_path, query); + context + .span() + .set_attribute(KeyValue::new("proxy.upstream_uri", back_uri.to_string())); + context + .span() + .set_attribute(KeyValue::new("proxy.type", "http")); debug!("proxy: http - received request forwarded to {back_uri}"); let mut back_request = self.client.request(req.method().clone(), back_uri); @@ -394,9 +432,11 @@ impl InnerProxyService { } let back_response = back_request + .trace_request() .send_stream(stream) .await .map_err(SendRequestError)?; + let mut response = HttpResponse::build(back_response.status()); { @@ -408,10 +448,27 @@ impl InnerProxyService { } } } + Ok(response.streaming(back_response)) } } +impl ProxyService { + pub fn new( + client: Client, + proxy: Proxy, + request_modifier: Option>, + ) -> Self { + Self { + inner: Rc::new(InnerProxyService { + client, + proxy, + request_modifier, + }), + } + } +} + impl Service for ProxyService { type Response = ServiceResponse; type Error = actix_web::Error; @@ -421,22 +478,41 @@ impl Service for ProxyService { fn call(&self, mut req: ServiceRequest) -> Self::Future { let proxy_service = self.inner.clone(); - Box::pin(async move { - let (http_request, payload) = req.parts_mut(); - let stream = web::Payload::from_request(http_request, payload).await?; - let unprocessed_path = http_request.match_info().unprocessed(); - let query = http_request.query_string(); - match proxy_service - .handle(http_request, stream, unprocessed_path, query) + Box::pin(async move { + get_tracer() + .in_span("Proxying request", |cx| async move { + // set tracing attributes + cx.span().set_attribute(KeyValue::new( + "proxy.name", + proxy_service + .proxy + .tracing_name + .clone() + .unwrap_or("unnamed".to_owned()), + )); + cx.span().set_attribute(KeyValue::new( + "proxy.mount_path", + proxy_service.proxy.mount_path.clone(), + )); + + // forward request + let (http_request, payload) = req.parts_mut(); + let unprocessed_path = http_request.match_info().unprocessed(); + let query = http_request.query_string(); + let stream = web::Payload::from_request(http_request, payload).await?; + match proxy_service + .handle(http_request, stream, unprocessed_path, query, cx) + .await + { + Ok(resp) => Ok(req.into_response(resp)), + Err(e) => { + warn!("proxy: error forwarding request: {}", e); + Err(e) + } + } + }) .await - { - Ok(resp) => Ok(req.into_response(resp)), - Err(e) => { - warn!("proxy: error forwarding request: {}", e); - Err(e) - } - } }) } } diff --git a/gateway/src/config.rs b/gateway/src/config.rs index b2f1bab9fa8..88bfcf5f53c 100644 --- a/gateway/src/config.rs +++ b/gateway/src/config.rs @@ -4,8 +4,85 @@ use figment::{ providers::{Env, Format, Serialized, Toml}, Figment, }; +use log::info; +use opentelemetry::global; +use opentelemetry_otlp::WithExportConfig; +use opentelemetry_sdk::{runtime::TokioCurrentThread, trace::TracerProvider}; use serde::{Deserialize, Serialize}; +#[derive(Deserialize, Serialize, Clone)] +pub struct Telemetry { + tracing: TracingTelemetry, +} + +impl Telemetry { + pub fn enable(self) { + self.tracing.enable_providers(); + } +} + +#[derive(Deserialize, Serialize, Clone)] +#[serde(tag = "type")] +pub enum TracingTelemetry { + NoTelemetry, + OpenTelemetry { endpoint: Option }, + Datadog { agent_url: Option }, +} + +impl TracingTelemetry { + pub fn enable_providers(self) { + let provider: Option = match self { + TracingTelemetry::OpenTelemetry { endpoint } => { + let exporter = opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(endpoint.expect("Missing agent endpoint for opentelemetry")) + // Set service name + .build_span_exporter() + .expect("Failed to initialize OpenTelemetry exporter"); + + info!("Tracing enabled with OpenTelemetry"); + + Some( + TracerProvider::builder() + .with_config(opentelemetry_sdk::trace::Config::default().with_resource( + opentelemetry_sdk::Resource::new(vec![opentelemetry::KeyValue::new( + "service.name", + "gateway", + )]), + )) + .with_batch_exporter(exporter, TokioCurrentThread) + .build(), + ) + } + + TracingTelemetry::Datadog { agent_url } => { + global::set_text_map_propagator(opentelemetry_datadog::DatadogPropagator::new()); + let exporter = opentelemetry_datadog::new_pipeline() + .with_agent_endpoint(agent_url.expect("Missing agent endpoint for datadog")) + .with_service_name("gateway") + .build_exporter() + .expect("Failed to initialize Datadog exporter"); + + info!("Tracing enabled with Datadog"); + Some( + TracerProvider::builder() + .with_batch_exporter(exporter, TokioCurrentThread) + .build(), + ) + } + + _ => { + info!("Tracing disabled"); + None + } + }; + + if let Some(provider) = provider { + global::set_tracer_provider(provider); + } + } +} + /// A proxy route #[derive(Deserialize, Serialize, Clone)] pub struct ProxyTarget { @@ -23,6 +100,8 @@ pub struct ProxyTarget { /// A connection, send and read timeout #[serde(default, with = "humantime_serde")] pub timeout: Option, + /// The tracing name for this target + pub tracing_name: Option, } #[derive(Deserialize, Serialize, Clone)] @@ -85,6 +164,8 @@ pub struct ProxyConfig { pub trusted_proxies: Vec, /// Authentication configuration pub auth: AuthConfig, + /// Telemetry configuration + pub telemetry: Option, } #[derive(Deserialize, Serialize, Clone)] @@ -108,6 +189,7 @@ impl Default for ProxyConfig { secure_cookies: true, providers: vec![], }, + telemetry: None, } } } diff --git a/gateway/src/config_parser.rs b/gateway/src/config_parser.rs index 52096ee83c0..5e3586e221c 100644 --- a/gateway/src/config_parser.rs +++ b/gateway/src/config_parser.rs @@ -68,6 +68,7 @@ pub fn parse_targets( None }, target.timeout, + target.tracing_name.clone(), ); match &target.prefix { diff --git a/gateway/src/main.rs b/gateway/src/main.rs index 372b3f09809..598b776e194 100644 --- a/gateway/src/main.rs +++ b/gateway/src/main.rs @@ -11,6 +11,7 @@ use actix_web::{ App, HttpServer, }; use actix_web::{web, HttpResponse}; +use actix_web_opentelemetry::RequestTracing; use config_parser::{ parse_auth_config, parse_files_config, parse_secret_key, parse_targets, Files, }; @@ -57,6 +58,11 @@ async fn main() -> std::io::Result<()> { (None, None) => None, }; + // Enable telemetry + if let Some(telemetry) = config.telemetry { + telemetry.enable(); + } + // Start server HttpServer::new(move || { let session_middleware = @@ -68,6 +74,7 @@ async fn main() -> std::io::Result<()> { .build(); let mut app = App::new() + .wrap(RequestTracing::new()) .wrap(Compress::default()) // enable compress .route("/health", web::get().to(|| async { "OK" })) .wrap(AuthMiddleware::new(Rc::new(auth_context.clone())))