diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 00ccc05f09..f3fe946a9f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,42 +12,41 @@ jobs: build_and_test: name: Build and test runs-on: ${{ matrix.os }} + continue-on-error: ${{ matrix.experimental }} strategy: matrix: - os: [ubuntu-latest, windows-latest, macOS-latest] + os: [ubuntu-latest, macOS-latest] rust: [nightly, stable] + experimental: [false] include: - os: ubuntu-latest sccache-path: /home/runner/.cache/sccache + release-arch: linux - os: macos-latest sccache-path: /Users/runner/Library/Caches/Mozilla.sccache + release-arch: darwin - os: windows-latest + rust: stable sccache-path: "%LOCALAPPDATA%\\sccache" + release-arch: windows + experimental: true env: RUST_BACKTRACE: full RUSTC_WRAPPER: sccache RUSTV: ${{ matrix.rust }} SCCACHE_CACHE_SIZE: 2G - # SCCACHE_RECACHE: 1 # Uncomment this to clear cache, then comment it back out steps: - uses: actions/checkout@master + - name: Set sccache env path if: matrix.os != 'windows-latest' run: | echo "SCCACHE_DIR=${{ matrix.sccache-path }}" >> $GITHUB_ENV - - name: Set windows arch - if: matrix.os == 'windows-latest' - run: | - echo "RELEASE_ARCH=windows" >> $GITHUB_ENV - - name: Set linux arch - if: matrix.os == 'ubuntu-latest' + - name: Set build arch run: | - echo "RELEASE_ARCH=linux" >> $GITHUB_ENV - - name: Set macOS arch - if: matrix.os == 'macos-latest' - run: | - echo "RELEASE_ARCH=darwin" >> $GITHUB_ENV + echo "RELEASE_ARCH=${{ matrix.release-arch }}" >> $GITHUB_ENV + - name: Install sccache (ubuntu-latest) if: matrix.os == 'ubuntu-latest' env: @@ -59,7 +58,8 @@ jobs: curl -L "$LINK/$SCCACHE_VERSION/$SCCACHE_FILE.tar.gz" | tar xz mv -f $SCCACHE_FILE/sccache $HOME/.local/bin/sccache chmod 755 $HOME/.local/bin/sccache - echo "$HOME/.local/bin" >> $GITHUB_PATH + echo "$HOME/.local/bin" >> $GITHUB_PATH + - name: Install scoop (windows-latest) if: matrix.os == 'windows-latest' shell: powershell @@ -68,15 +68,18 @@ jobs: iwr -useb get.scoop.sh -outfile 'install.ps1' .\\install.ps1 -RunAsAdmin Join-Path (Resolve-Path ~).Path "scoop\shims" >> $Env:GITHUB_PATH + - name: Install sccache (windows-latest) if: matrix.os == 'windows-latest' shell: powershell run: scoop install sccache + - name: Install sccache (macos-latest) if: matrix.os == 'macos-latest' run: | brew update brew install sccache + - name: Install ${{ matrix.rust }} uses: actions-rs/toolchain@v1 with: @@ -111,6 +114,22 @@ jobs: - name: Start sccache server run: sccache --start-server + - name: Install Protoc windows + if: matrix.os == 'windows-latest' + uses: arduino/setup-protoc@v1 + + - name: Install Protoc linux + if: matrix.os == 'ubuntu-latest' + run: | + sudo apt install -y protobuf-compiler + echo "PROTOC=/usr/bin/protoc" >> $GITHUB_ENV + echo "PROTOC_INCLUDE=/usr/include" >> $GITHUB_ENV + + - name: Install Protoc macOS + if: matrix.os == 'macOS-latest' + run: | + brew install protobuf + - name: check uses: actions-rs/cargo@v1 with: diff --git a/iroh-gateway/Cargo.toml b/iroh-gateway/Cargo.toml index 6af9a25fca..cdd2f9346c 100644 --- a/iroh-gateway/Cargo.toml +++ b/iroh-gateway/Cargo.toml @@ -14,6 +14,7 @@ axum = "0.5.1" clap = { version = "3.1.8", features = ["derive"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0.78" +serde_qs = "0.9.2" tower = { version = "0.4", features = ["util", "timeout", "load-shed", "limit"] } mime_guess = "2.0.4" iroh-metrics = { path = "../iroh-metrics" } @@ -24,6 +25,8 @@ git-version = "0.3.5" rand = "0.8.5" tracing-opentelemetry = "0.17.2" opentelemetry = { version = "0.17.0", features = ["rt-tokio"] } +time = "0.3.9" +headers = "0.3.7" [dev-dependencies] axum-macros = "0.2.0" # use #[axum_macros::debug_handler] for better error messages on handlers diff --git a/iroh-gateway/src/client.rs b/iroh-gateway/src/client.rs index b6b585c87c..e0f39406f1 100644 --- a/iroh-gateway/src/client.rs +++ b/iroh-gateway/src/client.rs @@ -3,6 +3,7 @@ use crate::metrics::*; use crate::response::ResponseFormat; use axum::body::Body; +use cid::Cid; use metrics::{counter, gauge, histogram, increment_counter}; use rand::{prelude::StdRng, Rng, SeedableRng}; use std::{fs::File, io::Read, path::Path, time::Duration}; @@ -86,7 +87,7 @@ impl Client { #[derive(Debug, Clone)] pub struct Request { pub format: ResponseFormat, - pub cid: String, + pub cid: Cid, pub full_content_path: String, pub query_file_name: String, pub content_path: String, diff --git a/iroh-gateway/src/config.rs b/iroh-gateway/src/config.rs index fc42d1414d..869a81fab3 100644 --- a/iroh-gateway/src/config.rs +++ b/iroh-gateway/src/config.rs @@ -1,7 +1,8 @@ -use std::collections::HashMap; - -use axum::http::header::*; - +use crate::constants::*; +use axum::http::{header::*, Method}; +use headers::{ + AccessControlAllowHeaders, AccessControlAllowMethods, AccessControlAllowOrigin, HeaderMapExt, +}; pub const DEFAULT_PORT: u16 = 9050; #[derive(Debug, Clone)] @@ -13,7 +14,7 @@ pub struct Config { /// flag to toggle whether the gateway enables/utilizes caching pub cache: bool, /// set of user provided headers to attach to all responses - pub headers: HashMap, //todo(arqu): convert to use axum::http::header + pub headers: HeaderMap, /// default port to listen on pub port: u16, } @@ -24,21 +25,46 @@ impl Config { writeable, fetch, cache, - headers: HashMap::new(), + headers: HeaderMap::new(), port, } } pub fn set_default_headers(&mut self) { - let mut headers = HashMap::new(); - headers.insert(ACCESS_CONTROL_ALLOW_ORIGIN.to_string(), "*".to_string()); - headers.insert(ACCESS_CONTROL_ALLOW_HEADERS.to_string(), "*".to_string()); - headers.insert(ACCESS_CONTROL_ALLOW_METHODS.to_string(), "*".to_string()); - headers.insert( - CACHE_CONTROL.to_string(), - "no-cache, no-transform".to_string(), + let mut headers = HeaderMap::new(); + headers.typed_insert(AccessControlAllowOrigin::ANY); + headers.typed_insert( + [ + Method::GET, + Method::PUT, + Method::POST, + Method::DELETE, + Method::HEAD, + Method::OPTIONS, + ] + .into_iter() + .collect::(), ); - headers.insert(ACCEPT_RANGES.to_string(), "none".to_string()); + headers.typed_insert( + [ + CONTENT_TYPE, + CONTENT_DISPOSITION, + LAST_MODIFIED, + CACHE_CONTROL, + ACCEPT_RANGES, + ETAG, + HEADER_SERVICE_WORKER.clone(), + HEADER_X_IPFS_GATEWAY_PREFIX.clone(), + HEADER_X_TRACE_ID.clone(), + HEADER_X_CONTENT_TYPE_OPTIONS.clone(), + HEADER_X_IPFS_PATH.clone(), + ] + .into_iter() + .collect::(), + ); + // todo(arqu): remove these once propperly implmented + headers.insert(CACHE_CONTROL, VALUE_NO_CACHE_NO_TRANSFORM.clone()); + headers.insert(ACCEPT_RANGES, VALUE_NONE.clone()); self.headers = headers; } } @@ -49,7 +75,7 @@ impl Default for Config { writeable: false, fetch: false, cache: false, - headers: HashMap::new(), + headers: HeaderMap::new(), port: DEFAULT_PORT, }; t.set_default_headers(); @@ -66,10 +92,8 @@ mod tests { let mut config = Config::new(false, false, false, 9050); config.set_default_headers(); assert_eq!(config.headers.len(), 5); - assert_eq!( - config.headers.get(&ACCESS_CONTROL_ALLOW_ORIGIN.to_string()), - Some(&"*".to_string()) - ); + let h = config.headers.get(&ACCESS_CONTROL_ALLOW_ORIGIN).unwrap(); + assert_eq!(h, "*"); } #[test] diff --git a/iroh-gateway/src/constants.rs b/iroh-gateway/src/constants.rs index 1096fc1e4c..9a0cff76e4 100644 --- a/iroh-gateway/src/constants.rs +++ b/iroh-gateway/src/constants.rs @@ -1,17 +1,30 @@ +use axum::http::{header::HeaderName, HeaderValue}; + // Headers -pub const HEADER_X_IPFS_PATH: &str = "X-Ipfs-Path"; -pub const HEADER_X_CONTENT_TYPE_OPTIONS: &str = "X-Content-Type-Options"; -pub const HEADER_X_TRACE_ID: &str = "X-Trace-Id"; +pub static HEADER_X_IPFS_PATH: HeaderName = HeaderName::from_static("x-ipfs-path"); +pub static HEADER_X_CONTENT_TYPE_OPTIONS: HeaderName = + HeaderName::from_static("x-content-type-options"); +pub static HEADER_X_TRACE_ID: HeaderName = HeaderName::from_static("x-trace-id"); +pub static HEADER_X_IPFS_GATEWAY_PREFIX: HeaderName = + HeaderName::from_static("x-ipfs-gateway-prefix"); +pub static HEADER_SERVICE_WORKER: HeaderName = HeaderName::from_static("service-worker"); // Common Header Values -pub const VALUE_XCTO_NOSNIFF: &str = "nosniff"; +pub static VALUE_XCTO_NOSNIFF: HeaderValue = HeaderValue::from_static("nosniff"); +pub static VALUE_NONE: HeaderValue = HeaderValue::from_static("none"); +pub static VALUE_NO_CACHE_NO_TRANSFORM: HeaderValue = + HeaderValue::from_static("no-cache, no-transform"); +pub static VAL_IMMUTABLE_MAX_AGE: HeaderValue = + HeaderValue::from_static("public, max-age=31536000, immutable"); // Dispositions -pub const DISPOSITION_ATTACHMENT: &str = "attachment"; -pub const DISPOSITION_INLINE: &str = "inline"; +pub static DISPOSITION_ATTACHMENT: &str = "attachment"; +pub static DISPOSITION_INLINE: &str = "inline"; // Content Types -pub const CONTENT_TYPE_IPLD_RAW: &str = "application/vnd.ipld.raw"; -pub const CONTENT_TYPE_IPLD_CAR: &str = "application/vnd.ipld.car; version=1"; -pub const CONTENT_TYPE_OCTET_STREAM: &str = "application/octet-stream"; -pub const CONTENT_TYPE_HTML: &str = "text/html"; +pub static CONTENT_TYPE_IPLD_RAW: HeaderValue = + HeaderValue::from_static("application/vnd.ipld.raw"); +pub static CONTENT_TYPE_IPLD_CAR: HeaderValue = + HeaderValue::from_static("application/vnd.ipld.car; version=1"); +pub static CONTENT_TYPE_OCTET_STREAM: HeaderValue = + HeaderValue::from_static("application/octet-stream"); diff --git a/iroh-gateway/src/core.rs b/iroh-gateway/src/core.rs index d29dc53c57..b01be2accb 100644 --- a/iroh-gateway/src/core.rs +++ b/iroh-gateway/src/core.rs @@ -3,14 +3,20 @@ use axum::{ error_handling::HandleErrorLayer, extract::{Extension, Path, Query}, http::{header::*, StatusCode}, - response::IntoResponse, + response::{IntoResponse, Redirect}, routing::get, BoxError, Router, }; use cid::Cid; use metrics::increment_counter; -use serde::Deserialize; -use std::{borrow::Cow, collections::HashMap, sync::Arc, time::Duration}; +use serde::{Deserialize, Serialize}; +use serde_qs; +use std::{ + borrow::Cow, + collections::HashMap, + sync::Arc, + time::{self, Duration}, +}; use tower::ServiceBuilder; use crate::{ @@ -18,8 +24,9 @@ use crate::{ config::Config, constants::*, error::GatewayError, + headers::*, metrics::{get_current_trace_id, METRICS_CNT_REQUESTS_TOTAL}, - response::{GatewayResponse, ResponseFormat}, + response::{get_response_format, GatewayResponse, ResponseFormat}, }; #[derive(Debug)] @@ -28,7 +35,7 @@ pub struct Core { client: Arc, } -#[derive(Debug, Deserialize)] +#[derive(Debug, Deserialize, Serialize)] pub struct GetParams { // todo(arqu): swap this for ResponseFormat /// specifies the expected format of the response @@ -39,6 +46,17 @@ pub struct GetParams { download: Option, } +impl GetParams { + pub fn to_query_string(&self) -> String { + let q = serde_qs::to_string(self).unwrap(); + if q.is_empty() { + q + } else { + format!("?{}", q) + } + } +} + impl Core { pub fn new(config: Config) -> Self { Self { @@ -48,9 +66,12 @@ impl Core { } pub async fn serve(self) { + // todo(arqu): ?uri=... https://github.com/ipfs/go-ipfs/pull/7802 let app = Router::new() .route("/ipfs/:cid", get(get_ipfs)) .route("/ipfs/:cid/*cpath", get(get_ipfs)) + .route("/ipfs/ipfs/:cid", get(redundant_ipfs)) + .route("/ipfs/ipfs/:cid/*cpath", get(redundant_ipfs)) .layer(Extension(Arc::clone(&self.config))) .layer(Extension(Arc::clone(&self.client))) .layer( @@ -73,18 +94,49 @@ impl Core { } } +#[tracing::instrument()] +async fn redundant_ipfs( + Path(params): Path>, + Query(query_params): Query, +) -> impl IntoResponse { + let cid = params.get("cid").unwrap(); + let cpath = "".to_string(); + let cpath = params.get("cpath").unwrap_or(&cpath); + let redirect_path: String = if cpath.is_empty() { + format!("/ipfs/{}", cid) + } else { + format!("/ipfs/{}/{}", cid, cpath) + }; + let redirect_path = format!("{}{}", redirect_path, query_params.to_query_string()); + Redirect::to(&redirect_path).into_response() +} + #[tracing::instrument()] async fn get_ipfs( Extension(config): Extension>, Extension(client): Extension>, Path(params): Path>, Query(query_params): Query, + request_headers: HeaderMap, ) -> Result { increment_counter!(METRICS_CNT_REQUESTS_TOTAL); - let start_time = std::time::Instant::now(); + let start_time = time::Instant::now(); // parse path params let cid_param = params.get("cid").unwrap(); let cid = Cid::try_from(cid_param.clone()); + let cpath = "".to_string(); + let cpath = params.get("cpath").unwrap_or(&cpath); + + if request_headers.contains_key(&HEADER_SERVICE_WORKER) { + let sw = request_headers.get(&HEADER_SERVICE_WORKER).unwrap(); + if sw.to_str().unwrap() == "script" && cpath.is_empty() { + return error(StatusCode::BAD_REQUEST, "Service Worker not supported"); + } + } + if request_headers.contains_key(&HEADER_X_IPFS_GATEWAY_PREFIX) { + return error(StatusCode::BAD_REQUEST, "Unsupported HTTP header"); + } + let cid = match cid { Ok(cid) => cid, Err(_) => { @@ -92,34 +144,50 @@ async fn get_ipfs( return error(StatusCode::BAD_REQUEST, "invalid cid"); } }; - - let cpath = "".to_string(); - let cpath = params.get("cpath").unwrap_or(&cpath); let full_content_path = format!("/ipfs/{}{}", cid, cpath); + + // todo(arqu): actually plug in a resolver + let resolved_cid = resolve_cid(&cid).await.unwrap(); + // parse query params - let format = if let Some(format) = query_params.format { - match ResponseFormat::try_from(format.as_str()) { - Ok(format) => format, - Err(err) => { - return error(StatusCode::BAD_REQUEST, &err); - } + let format = match get_response_format(&request_headers, query_params.format) { + Ok(format) => format, + Err(err) => { + return error(StatusCode::BAD_REQUEST, &err); } - } else { - ResponseFormat::Fs }; + let query_file_name = query_params.filename.unwrap_or_default(); let download = query_params.download.unwrap_or_default(); + let mut headers = HeaderMap::new(); + + if request_headers.contains_key("If-None-Match") { + // todo(arqu): handle dir etags + let cid_etag = get_etag(&resolved_cid, Some(format.clone())); + let inm = request_headers + .get("If-None-Match") + .unwrap() + .to_str() + .unwrap(); + if etag_matches(inm, &cid_etag) { + return response(StatusCode::NOT_MODIFIED, body::BoxBody::default(), headers); + } + } + // init headers - let mut headers = HashMap::new(); format.write_headers(&mut headers); - headers.insert(HEADER_X_IPFS_PATH.to_string(), full_content_path.clone()); add_user_headers(&mut headers, config.headers.clone()); + headers.insert( + &HEADER_X_IPFS_PATH, + HeaderValue::from_str(&full_content_path).unwrap(), + ); + // todo(arqu): add X-Ipfs-Roots // handle request and fetch data let req = Request { format, - cid: cid.to_string(), + cid, full_content_path, query_file_name, content_path: cpath.to_string(), @@ -128,16 +196,21 @@ async fn get_ipfs( match req.format { ResponseFormat::Raw => serve_raw(&req, *client, headers, start_time).await, ResponseFormat::Car => serve_car(&req, *client, headers, start_time).await, - ResponseFormat::Html => serve_html(&req, *client, headers, start_time).await, - ResponseFormat::Fs => serve_fs(&req, *client, headers, start_time).await, + ResponseFormat::Fs(_) => serve_fs(&req, *client, headers, start_time).await, } } +// todo(arqu): flesh out resolving +#[tracing::instrument()] +async fn resolve_cid(cid: &Cid) -> Result { + Ok(*cid) +} + #[tracing::instrument()] async fn serve_raw( req: &Request, client: Client, - mut headers: HashMap, + mut headers: HeaderMap, start_time: std::time::Instant, ) -> Result { let body = client @@ -154,6 +227,8 @@ async fn serve_raw( format!("{}.bin", req.cid).as_str(), DISPOSITION_ATTACHMENT, ); + set_etag_headers(&mut headers, get_etag(&req.cid, Some(req.format.clone()))); + add_cache_control_headers(&mut headers, req.full_content_path.to_string()); response(StatusCode::OK, body::boxed(body), headers.clone()) } @@ -161,7 +236,7 @@ async fn serve_raw( async fn serve_car( req: &Request, client: Client, - mut headers: HashMap, + mut headers: HeaderMap, start_time: std::time::Instant, ) -> Result { let body = client @@ -178,25 +253,10 @@ async fn serve_car( format!("{}.car", req.cid).as_str(), DISPOSITION_ATTACHMENT, ); - response(StatusCode::OK, body::boxed(body), headers.clone()) -} - -#[tracing::instrument()] -async fn serve_html( - req: &Request, - client: Client, - headers: HashMap, - start_time: std::time::Instant, -) -> Result { - let body = client - .get_file_simulated(req.full_content_path.as_str(), start_time) - .await; - let body = match body { - Ok(b) => b, - Err(e) => { - return error(StatusCode::INTERNAL_SERVER_ERROR, &e); - } - }; + // todo(arqu): this should be root cid + let etag = format!("W/{}", get_etag(&req.cid, Some(req.format.clone()))); + set_etag_headers(&mut headers, etag); + // todo(arqu): check if etag matches for root cid response(StatusCode::OK, body::boxed(body), headers.clone()) } @@ -204,7 +264,7 @@ async fn serve_html( async fn serve_fs( req: &Request, client: Client, - mut headers: HashMap, + mut headers: HeaderMap, start_time: std::time::Instant, ) -> Result { let body = client @@ -222,71 +282,17 @@ async fn serve_fs( &req.content_path, req.download, ); + set_etag_headers(&mut headers, get_etag(&req.cid, Some(req.format.clone()))); + add_cache_control_headers(&mut headers, req.full_content_path.to_string()); add_content_type_headers(&mut headers, &name); response(StatusCode::OK, body::boxed(body), headers.clone()) } -#[tracing::instrument()] -fn add_user_headers(headers: &mut HashMap, user_headers: HashMap) { - headers.extend(user_headers.into_iter()); -} - -#[tracing::instrument()] -fn add_content_type_headers(headers: &mut HashMap, name: &str) { - let guess = mime_guess::from_path(name); - let content_type = guess.first_or_octet_stream().to_string(); - headers.insert(CONTENT_TYPE.to_string(), content_type); -} - -#[tracing::instrument()] -fn add_content_disposition_headers( - headers: &mut HashMap, - filename: &str, - content_path: &str, - should_download: bool, -) -> String { - let mut name = get_filename(content_path); - if !filename.is_empty() { - name = filename.to_string(); - } - if !name.is_empty() { - let disposition = if should_download { - DISPOSITION_ATTACHMENT - } else { - DISPOSITION_INLINE - }; - set_content_disposition_headers(headers, &name, disposition); - } - name -} - -#[tracing::instrument()] -fn set_content_disposition_headers( - headers: &mut HashMap, - filename: &str, - disposition: &str, -) { - headers.insert( - CONTENT_DISPOSITION.to_string(), - format!("{}; filename={}", disposition, filename), - ); -} - -#[tracing::instrument()] -fn get_filename(content_path: &str) -> String { - content_path - .split('/') - .filter(|s| !s.is_empty()) - .map(|s| s.to_string()) - .last() - .unwrap_or_default() -} - #[tracing::instrument()] fn response( status_code: StatusCode, body: BoxBody, - headers: HashMap, + headers: HeaderMap, ) -> Result { Ok(GatewayResponse { status_code, @@ -322,120 +328,3 @@ async fn middleware_error_handler(error: BoxError) -> impl IntoResponse { Cow::from(format!("unhandled internal error: {}", error)), ) } - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn add_user_headers_test() { - let mut headers = HashMap::new(); - let user_headers = HashMap::from_iter(vec![ - (HEADER_X_IPFS_PATH.to_string(), "QmHeaderPath1".to_string()), - (HEADER_X_IPFS_PATH.to_string(), "QmHeaderPath2".to_string()), - ]); - add_user_headers(&mut headers, user_headers); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&HEADER_X_IPFS_PATH.to_string()).unwrap(), - &"QmHeaderPath2".to_string() - ); - } - - #[test] - fn add_content_type_headers_test() { - let mut headers = HashMap::new(); - let name = "test.txt"; - add_content_type_headers(&mut headers, name); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &"text/plain".to_string() - ); - - let mut headers = HashMap::new(); - let name = "test.RAND_EXT"; - add_content_type_headers(&mut headers, name); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &CONTENT_TYPE_OCTET_STREAM.to_string() - ); - } - - #[test] - fn add_content_disposition_headers_test() { - // inline - let mut headers = HashMap::new(); - let filename = "test.txt"; - let content_path = "QmSomeCid"; - let download = false; - let name = add_content_disposition_headers(&mut headers, filename, content_path, download); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_DISPOSITION.to_string()).unwrap(), - &"inline; filename=test.txt".to_string() - ); - assert_eq!(name, "test.txt"); - - // attachment - let mut headers = HashMap::new(); - let filename = "test.txt"; - let content_path = "QmSomeCid"; - let download = true; - let name = add_content_disposition_headers(&mut headers, filename, content_path, download); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_DISPOSITION.to_string()).unwrap(), - &"attachment; filename=test.txt".to_string() - ); - assert_eq!(name, "test.txt"); - - // no filename & no content path filename - let mut headers = HashMap::new(); - let filename = ""; - let content_path = "QmSomeCid"; - let download = true; - let name = add_content_disposition_headers(&mut headers, filename, content_path, download); - assert_eq!(headers.len(), 1); - assert_eq!(name, "QmSomeCid"); - - // no filename & with content path filename - let mut headers = HashMap::new(); - let filename = ""; - let content_path = "QmSomeCid/folder/test.txt"; - let download = true; - let name = add_content_disposition_headers(&mut headers, filename, content_path, download); - assert_eq!(headers.len(), 1); - assert_eq!(name, "test.txt"); - } - - #[test] - fn set_content_disposition_headers_test() { - let mut headers = HashMap::new(); - let filename = "test_inline.txt"; - set_content_disposition_headers(&mut headers, filename, DISPOSITION_INLINE); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_DISPOSITION.to_string()).unwrap(), - &"inline; filename=test_inline.txt".to_string() - ); - - let mut headers = HashMap::new(); - let filename = "test_attachment.txt"; - set_content_disposition_headers(&mut headers, filename, DISPOSITION_ATTACHMENT); - assert_eq!(headers.len(), 1); - assert_eq!( - headers.get(&CONTENT_DISPOSITION.to_string()).unwrap(), - &"attachment; filename=test_attachment.txt".to_string() - ); - } - - #[test] - fn get_filename_test() { - assert_eq!(get_filename("QmSomeCid/folder/test.txt"), "test.txt"); - assert_eq!(get_filename("QmSomeCid/folder"), "folder"); - assert_eq!(get_filename("QmSomeCid"), "QmSomeCid"); - assert_eq!(get_filename(""), ""); - } -} diff --git a/iroh-gateway/src/error.rs b/iroh-gateway/src/error.rs index b320a5f407..e277333b2c 100644 --- a/iroh-gateway/src/error.rs +++ b/iroh-gateway/src/error.rs @@ -23,7 +23,6 @@ impl IntoResponse for GatewayError { "message": self.message, "trace_id": self.trace_id, })); - // todo(arqu): add headers (self.status_code, body).into_response() } } diff --git a/iroh-gateway/src/headers.rs b/iroh-gateway/src/headers.rs new file mode 100644 index 0000000000..5938835b69 --- /dev/null +++ b/iroh-gateway/src/headers.rs @@ -0,0 +1,288 @@ +use crate::{constants::*, response::ResponseFormat}; +use ::time::OffsetDateTime; +use axum::http::header::*; +use cid::Cid; +use std::time; + +#[tracing::instrument()] +pub fn add_user_headers(headers: &mut HeaderMap, user_headers: HeaderMap) { + headers.extend(user_headers.into_iter()); +} + +#[tracing::instrument()] +pub fn add_content_type_headers(headers: &mut HeaderMap, name: &str) { + let guess = mime_guess::from_path(name); + let content_type = guess.first_or_octet_stream().to_string(); + headers.insert(CONTENT_TYPE, HeaderValue::from_str(&content_type).unwrap()); +} + +#[tracing::instrument()] +pub fn add_content_disposition_headers( + headers: &mut HeaderMap, + filename: &str, + content_path: &str, + should_download: bool, +) -> String { + let mut name = get_filename(content_path); + if !filename.is_empty() { + name = filename.to_string(); + } + if !name.is_empty() { + let disposition = if should_download { + DISPOSITION_ATTACHMENT + } else { + DISPOSITION_INLINE + }; + set_content_disposition_headers(headers, &name, disposition); + } + name +} + +#[tracing::instrument()] +pub fn set_content_disposition_headers(headers: &mut HeaderMap, filename: &str, disposition: &str) { + headers.insert( + CONTENT_DISPOSITION, + HeaderValue::from_str(&format!("{}; filename={}", disposition, filename)).unwrap(), + ); +} + +#[tracing::instrument()] +pub fn add_cache_control_headers(headers: &mut HeaderMap, content_path: String) { + if true { + // todo(arqu): work out if cpath is mutable + // now we just treat everything as mutable + // should also utilize the cache flag on config + let lmdt: OffsetDateTime = time::SystemTime::now().into(); + headers.insert( + LAST_MODIFIED, + HeaderValue::from_str(&lmdt.to_string()).unwrap(), + ); + } else { + headers.insert(LAST_MODIFIED, HeaderValue::from_str("0").unwrap()); + headers.insert(CACHE_CONTROL, VAL_IMMUTABLE_MAX_AGE.clone()); + } +} + +#[tracing::instrument()] +pub fn set_etag_headers(headers: &mut HeaderMap, etag: String) { + headers.insert(ETAG, HeaderValue::from_str(&etag).unwrap()); +} + +#[tracing::instrument()] +pub fn get_etag(cid: &Cid, response_format: Option) -> String { + let mut suffix = "".to_string(); + if let Some(fmt) = response_format { + let ext = fmt.get_extenstion(); + if !ext.is_empty() { + suffix = format!(".{}", ext); + } + } + format!("\"{}{}\"", cid, suffix) +} + +#[tracing::instrument()] +pub fn etag_matches(inm: &str, cid_etag: &str) -> bool { + let mut buf = inm.trim(); + loop { + if buf.is_empty() { + break; + } + if buf.starts_with(',') { + buf = &buf[1..]; + continue; + } + if buf.starts_with('*') { + return true; + } + let (etag, remain) = scan_etag(buf); + if etag.is_empty() { + break; + } + if etag_weak_match(etag, cid_etag) { + return true; + } + buf = remain; + } + false +} + +#[tracing::instrument()] +pub fn scan_etag(buf: &str) -> (&str, &str) { + let s = buf.trim(); + let mut start = 0; + if s.starts_with("W/") { + start = 2; + } + if s.len() - start < 2 || s.chars().nth(start) != Some('"') { + return ("", ""); + } + for i in start + 1..s.len() { + let c = s.as_bytes().get(i).unwrap(); + if *c == 0x21 || (0x23..0x7E).contains(c) || *c >= 0x80 { + continue; + } + if *c == b'"' { + return (&s[..i + 1], &s[i + 1..]); + } + return ("", ""); + } + ("", "") +} + +#[tracing::instrument()] +pub fn etag_weak_match(etag: &str, cid_etag: &str) -> bool { + etag.trim_start_matches("W/") == cid_etag.trim_start_matches("W/") +} + +#[tracing::instrument()] +fn get_filename(content_path: &str) -> String { + content_path + .split('/') + .filter(|s| !s.is_empty()) + .map(|s| s.to_string()) + .last() + .unwrap_or_default() +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn add_user_headers_test() { + let mut headers = HeaderMap::new(); + let mut user_headers = HeaderMap::new(); + user_headers.insert( + &HEADER_X_IPFS_PATH, + HeaderValue::from_str("QmHeaderPath1").unwrap(), + ); + user_headers.insert( + &HEADER_X_IPFS_PATH, + HeaderValue::from_str("QmHeaderPath2").unwrap(), + ); + add_user_headers(&mut headers, user_headers); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&HEADER_X_IPFS_PATH).unwrap(), + &"QmHeaderPath2".to_string() + ); + } + + #[test] + fn add_content_type_headers_test() { + let mut headers = HeaderMap::new(); + let name = "test.txt"; + add_content_type_headers(&mut headers, name); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_TYPE).unwrap(), + &"text/plain".to_string() + ); + + let mut headers = HeaderMap::new(); + let name = "test.RAND_EXT"; + add_content_type_headers(&mut headers, name); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_TYPE).unwrap(), + &CONTENT_TYPE_OCTET_STREAM + ); + } + + #[test] + fn add_content_disposition_headers_test() { + // inline + let mut headers = HeaderMap::new(); + let filename = "test.txt"; + let content_path = "QmSomeCid"; + let download = false; + let name = add_content_disposition_headers(&mut headers, filename, content_path, download); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_DISPOSITION).unwrap(), + &"inline; filename=test.txt".to_string() + ); + assert_eq!(name, "test.txt"); + + // attachment + let mut headers = HeaderMap::new(); + let filename = "test.txt"; + let content_path = "QmSomeCid"; + let download = true; + let name = add_content_disposition_headers(&mut headers, filename, content_path, download); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_DISPOSITION).unwrap(), + &"attachment; filename=test.txt".to_string() + ); + assert_eq!(name, "test.txt"); + + // no filename & no content path filename + let mut headers = HeaderMap::new(); + let filename = ""; + let content_path = "QmSomeCid"; + let download = true; + let name = add_content_disposition_headers(&mut headers, filename, content_path, download); + assert_eq!(headers.len(), 1); + assert_eq!(name, "QmSomeCid"); + + // no filename & with content path filename + let mut headers = HeaderMap::new(); + let filename = ""; + let content_path = "QmSomeCid/folder/test.txt"; + let download = true; + let name = add_content_disposition_headers(&mut headers, filename, content_path, download); + assert_eq!(headers.len(), 1); + assert_eq!(name, "test.txt"); + } + + #[test] + fn set_content_disposition_headers_test() { + let mut headers = HeaderMap::new(); + let filename = "test_inline.txt"; + set_content_disposition_headers(&mut headers, filename, DISPOSITION_INLINE); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_DISPOSITION).unwrap(), + &"inline; filename=test_inline.txt".to_string() + ); + + let mut headers = HeaderMap::new(); + let filename = "test_attachment.txt"; + set_content_disposition_headers(&mut headers, filename, DISPOSITION_ATTACHMENT); + assert_eq!(headers.len(), 1); + assert_eq!( + headers.get(&CONTENT_DISPOSITION).unwrap(), + &"attachment; filename=test_attachment.txt".to_string() + ); + } + + #[test] + fn get_filename_test() { + assert_eq!(get_filename("QmSomeCid/folder/test.txt"), "test.txt"); + assert_eq!(get_filename("QmSomeCid/folder"), "folder"); + assert_eq!(get_filename("QmSomeCid"), "QmSomeCid"); + assert_eq!(get_filename(""), ""); + } + + #[test] + fn etag_test() { + let any_etag = "*"; + let etag = get_etag( + &Cid::try_from("bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4s52zy").unwrap(), + Some(ResponseFormat::Raw), + ); + let wetag = format!("W/{}", etag); + let other_etag = get_etag( + &Cid::try_from("bafkreigh2akiscaildcqabsyg3dfr6chu3fgpregiymsck7e7aqa4aaaaa").unwrap(), + Some(ResponseFormat::Raw), + ); + let other_wetag = format!("W/{}", other_etag); + let long_etag = format!("{},{}", other_etag, wetag); + + assert!(etag_matches(any_etag, &etag)); + assert!(etag_matches(&etag, &wetag)); + assert!(etag_matches(&long_etag, &etag)); + assert!(!etag_matches(&etag, &other_wetag)); + } +} diff --git a/iroh-gateway/src/lib.rs b/iroh-gateway/src/lib.rs index d819cbc8d0..a3e617d817 100644 --- a/iroh-gateway/src/lib.rs +++ b/iroh-gateway/src/lib.rs @@ -3,5 +3,6 @@ pub mod config; mod constants; pub mod core; mod error; +mod headers; pub mod metrics; mod response; diff --git a/iroh-gateway/src/main.rs b/iroh-gateway/src/main.rs index f5aa99498a..cbb8e44c7e 100644 --- a/iroh-gateway/src/main.rs +++ b/iroh-gateway/src/main.rs @@ -12,6 +12,8 @@ struct Args { fetch: bool, #[clap(short, long)] cache: bool, + #[clap(long = "no-metrics")] + no_metrics: bool, } #[tokio::main] @@ -22,7 +24,8 @@ async fn main() { config.set_default_headers(); println!("{:#?}", config); - iroh_metrics::init(metrics::metrics_config()).expect("failed to initialize metrics"); + iroh_metrics::init(metrics::metrics_config(args.no_metrics)) + .expect("failed to initialize metrics"); metrics::register_counters(); let handler = Core::new(config); diff --git a/iroh-gateway/src/metrics.rs b/iroh-gateway/src/metrics.rs index 9028f462cc..cf4d71030b 100644 --- a/iroh-gateway/src/metrics.rs +++ b/iroh-gateway/src/metrics.rs @@ -4,7 +4,7 @@ use metrics::{describe_counter, describe_gauge, describe_histogram, Unit}; use opentelemetry::trace::{TraceContextExt, TraceId}; use tracing_opentelemetry::OpenTelemetrySpanExt; -pub fn metrics_config() -> iroh_metrics::Config { +pub fn metrics_config(logger_only: bool) -> iroh_metrics::Config { // compile time configuration let service_name = env!("CARGO_PKG_NAME").to_string(); let build = git_version!().to_string(); @@ -14,7 +14,14 @@ pub fn metrics_config() -> iroh_metrics::Config { let instance_id = std::env::var("IROH_INSTANCE_ID") .unwrap_or_else(|_| names::Generator::default().next().unwrap()); let service_env = std::env::var("IROH_ENV").unwrap_or_else(|_| "dev".to_string()); - iroh_metrics::Config::new(service_name, instance_id, build, version, service_env) + iroh_metrics::Config::new( + service_name, + instance_id, + build, + version, + service_env, + logger_only, + ) } pub const METRICS_CNT_REQUESTS_TOTAL: &str = "gw_requests_total"; diff --git a/iroh-gateway/src/response.rs b/iroh-gateway/src/response.rs index d3b7591447..5a2d37944d 100644 --- a/iroh-gateway/src/response.rs +++ b/iroh-gateway/src/response.rs @@ -1,5 +1,3 @@ -use std::{collections::HashMap, str::FromStr}; - use axum::{ body::BoxBody, http::{header::*, HeaderValue, StatusCode}, @@ -12,10 +10,9 @@ pub const ERR_UNSUPPORTED_FORMAT: &str = "unsuported format"; #[derive(Debug, Clone, PartialEq)] pub enum ResponseFormat { - Html, Raw, Car, - Fs, + Fs(String), } impl std::convert::TryFrom<&str> for ResponseFormat { @@ -23,65 +20,131 @@ impl std::convert::TryFrom<&str> for ResponseFormat { fn try_from(s: &str) -> Result { match s.to_lowercase().as_str() { - "html" => Ok(ResponseFormat::Html), - "raw" => Ok(ResponseFormat::Raw), - "car" => Ok(ResponseFormat::Car), - "fs" | "" => Ok(ResponseFormat::Fs), - _ => Err(format!("{}: {}", ERR_UNSUPPORTED_FORMAT, s)), + "application/vnd.ipld.raw" | "raw" => Ok(ResponseFormat::Raw), + "application/vnd.ipld.car" | "car" => Ok(ResponseFormat::Car), + "fs" | "" => Ok(ResponseFormat::Fs(String::new())), + rf => { + if rf.starts_with("application/vnd.ipld.") { + Ok(ResponseFormat::Fs(rf.to_string())) + } else { + Err(format!("{}: {}", ERR_UNSUPPORTED_FORMAT, rf)) + } + } } } } impl ResponseFormat { - pub fn write_headers(&self, headers: &mut HashMap) { + pub fn write_headers(&self, headers: &mut HeaderMap) { match self { ResponseFormat::Raw => { - headers.insert(CONTENT_TYPE.to_string(), CONTENT_TYPE_IPLD_RAW.to_string()); - headers.insert( - HEADER_X_CONTENT_TYPE_OPTIONS.to_string(), - VALUE_XCTO_NOSNIFF.to_string(), - ); + headers.insert(CONTENT_TYPE, CONTENT_TYPE_IPLD_RAW.clone()); + headers.insert(&HEADER_X_CONTENT_TYPE_OPTIONS, VALUE_XCTO_NOSNIFF.clone()); } ResponseFormat::Car => { - headers.insert(CONTENT_TYPE.to_string(), CONTENT_TYPE_IPLD_CAR.to_string()); - headers.insert( - HEADER_X_CONTENT_TYPE_OPTIONS.to_string(), - VALUE_XCTO_NOSNIFF.to_string(), - ); + headers.insert(CONTENT_TYPE, CONTENT_TYPE_IPLD_CAR.clone()); + headers.insert(&HEADER_X_CONTENT_TYPE_OPTIONS, VALUE_XCTO_NOSNIFF.clone()); + headers.insert(ACCEPT_RANGES, VALUE_NONE.clone()); + headers.insert(CACHE_CONTROL, VALUE_NO_CACHE_NO_TRANSFORM.clone()); } - ResponseFormat::Html => { - headers.insert(CONTENT_TYPE.to_string(), CONTENT_TYPE_HTML.to_string()); + ResponseFormat::Fs(_) => { + headers.insert(CONTENT_TYPE, CONTENT_TYPE_OCTET_STREAM.clone()); + } + } + } + + pub fn get_extenstion(&self) -> String { + match self { + ResponseFormat::Raw => "bin".to_string(), + ResponseFormat::Car => "car".to_string(), + ResponseFormat::Fs(s) => { + if s.is_empty() { + String::new() + } else { + s.split('.').last().unwrap().to_string() + } } - ResponseFormat::Fs => { - headers.insert( - CONTENT_TYPE.to_string(), - CONTENT_TYPE_OCTET_STREAM.to_string(), - ); + } + } + + pub fn try_from_headers(headers: &HeaderMap) -> Result { + if headers.contains_key("Accept") { + if let Some(h_values) = headers.get("Accept") { + let h_values = h_values.to_str().unwrap().split(','); + for h_value in h_values { + let h_value = h_value.trim(); + if h_value.starts_with("application/vnd.ipld.") { + // if valid media type use it, otherwise return error + // todo(arqu): add support for better media type detection + if h_value != "application/vnd.ipld.raw" + && h_value != "application/vnd.ipld.car" + { + return Err(format!("{}: {}", ERR_UNSUPPORTED_FORMAT, h_value)); + } + return ResponseFormat::try_from(h_value); + } + } } } + Ok(ResponseFormat::Fs(String::new())) } } +#[tracing::instrument()] +pub fn get_response_format( + request_headers: &HeaderMap, + query_format: Option, +) -> Result { + let format = if let Some(format) = query_format { + if format.is_empty() { + match ResponseFormat::try_from_headers(request_headers) { + Ok(format) => format, + Err(_) => { + return Err("invalid format".to_string()); + } + } + } else { + match ResponseFormat::try_from(format.as_str()) { + Ok(format) => format, + Err(_) => { + match ResponseFormat::try_from_headers(request_headers) { + Ok(format) => format, + Err(_) => { + return Err("invalid format".to_string()); + } + }; + return Err("invalid format".to_string()); + } + } + } + } else { + match ResponseFormat::try_from_headers(request_headers) { + Ok(format) => format, + Err(_) => { + return Err("invalid format".to_string()); + } + } + }; + Ok(format) +} + #[derive(Debug)] pub struct GatewayResponse { pub status_code: StatusCode, pub body: BoxBody, - pub headers: HashMap, + pub headers: HeaderMap, pub trace_id: String, } impl IntoResponse for GatewayResponse { - fn into_response(self) -> Response { + fn into_response(mut self) -> Response { let mut rb = Response::builder().status(self.status_code); - let headers = rb.headers_mut().unwrap(); - for (key, value) in &self.headers { - let header_name = HeaderName::from_str(key).unwrap(); - headers.insert(header_name, HeaderValue::from_str(value).unwrap()); - } - headers.insert( - HEADER_X_TRACE_ID, + self.headers.insert( + &HEADER_X_TRACE_ID, HeaderValue::from_str(&self.trace_id).unwrap(), ); + let rh = rb.headers_mut().unwrap(); + rh.extend(self.headers); rb.body(self.body).unwrap() } } @@ -89,7 +152,6 @@ impl IntoResponse for GatewayResponse { #[cfg(test)] mod tests { use super::*; - use std::collections::HashMap; #[test] fn response_format_try_from() { @@ -97,12 +159,10 @@ mod tests { assert_eq!(rf, Ok(ResponseFormat::Raw)); let rf = ResponseFormat::try_from("car"); assert_eq!(rf, Ok(ResponseFormat::Car)); - let rf = ResponseFormat::try_from("html"); - assert_eq!(rf, Ok(ResponseFormat::Html)); let rf = ResponseFormat::try_from("fs"); - assert_eq!(rf, Ok(ResponseFormat::Fs)); + assert_eq!(rf, Ok(ResponseFormat::Fs(String::new()))); let rf = ResponseFormat::try_from(""); - assert_eq!(rf, Ok(ResponseFormat::Fs)); + assert_eq!(rf, Ok(ResponseFormat::Fs(String::new()))); let rf = ResponseFormat::try_from("RaW"); assert_eq!(rf, Ok(ResponseFormat::Raw)); @@ -114,51 +174,32 @@ mod tests { #[test] fn response_format_write_headers() { let rf = ResponseFormat::try_from("raw").unwrap(); - let mut headers = HashMap::new(); + let mut headers = HeaderMap::new(); rf.write_headers(&mut headers); assert_eq!(headers.len(), 2); + assert_eq!(headers.get(&CONTENT_TYPE).unwrap(), &CONTENT_TYPE_IPLD_RAW); assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &CONTENT_TYPE_IPLD_RAW.to_string() - ); - assert_eq!( - headers - .get(&HEADER_X_CONTENT_TYPE_OPTIONS.to_string()) - .unwrap(), - &VALUE_XCTO_NOSNIFF.to_string() + headers.get(&HEADER_X_CONTENT_TYPE_OPTIONS).unwrap(), + &VALUE_XCTO_NOSNIFF ); let rf = ResponseFormat::try_from("car").unwrap(); - let mut headers = HashMap::new(); - rf.write_headers(&mut headers); - assert_eq!(headers.len(), 2); - assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &CONTENT_TYPE_IPLD_CAR.to_string() - ); - assert_eq!( - headers - .get(&HEADER_X_CONTENT_TYPE_OPTIONS.to_string()) - .unwrap(), - &VALUE_XCTO_NOSNIFF.to_string() - ); - - let rf = ResponseFormat::try_from("html").unwrap(); - let mut headers = HashMap::new(); + let mut headers = HeaderMap::new(); rf.write_headers(&mut headers); - assert_eq!(headers.len(), 1); + assert_eq!(headers.len(), 4); + assert_eq!(headers.get(&CONTENT_TYPE).unwrap(), &CONTENT_TYPE_IPLD_CAR); assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &CONTENT_TYPE_HTML.to_string() + headers.get(&HEADER_X_CONTENT_TYPE_OPTIONS).unwrap(), + &VALUE_XCTO_NOSNIFF ); let rf = ResponseFormat::try_from("fs").unwrap(); - let mut headers = HashMap::new(); + let mut headers = HeaderMap::new(); rf.write_headers(&mut headers); assert_eq!(headers.len(), 1); assert_eq!( - headers.get(&CONTENT_TYPE.to_string()).unwrap(), - &CONTENT_TYPE_OCTET_STREAM.to_string() + headers.get(&CONTENT_TYPE).unwrap(), + &CONTENT_TYPE_OCTET_STREAM ); } } diff --git a/iroh-metrics/Cargo.toml b/iroh-metrics/Cargo.toml index d1fb527029..b19a7fe0bf 100644 --- a/iroh-metrics/Cargo.toml +++ b/iroh-metrics/Cargo.toml @@ -10,7 +10,7 @@ repository = "https://github.com/n0-computer/iroh" [dependencies] tracing = "0.1.33" tracing-futures = "0.2.5" -tracing-subscriber = "0.3.9" +tracing-subscriber = { version = "0.3.11", features = ["env-filter"] } tracing-opentelemetry = "0.17.2" opentelemetry = { version = "0.17.0", features = ["rt-tokio"] } opentelemetry-otlp = { version = "0.10.0", features = ["grpc-sys"] } diff --git a/iroh-metrics/src/lib.rs b/iroh-metrics/src/lib.rs index 837bd101e4..9d181a0c76 100644 --- a/iroh-metrics/src/lib.rs +++ b/iroh-metrics/src/lib.rs @@ -3,7 +3,7 @@ use opentelemetry::sdk::{trace, Resource}; use opentelemetry_otlp::WithExportConfig; use std::env::consts::{ARCH, OS}; use std::time::Duration; -use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt}; +use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, Layer}; /// Initialize the tracing and metrics subsystems. pub fn init(cfg: Config) -> Result<(), Box> { @@ -27,14 +27,11 @@ pub fn init_metrics(cfg: Config) -> Result<(), Box> { /// Initialize the tracing subsystem. pub fn init_tracer(cfg: Config) -> Result<(), Box> { + let log_subscriber = fmt::layer() + .pretty() + .with_filter(EnvFilter::from_default_env()); if cfg.debug { - tracing_subscriber::fmt() - .pretty() - .with_thread_names(true) - // enable everything - .with_max_level(tracing::Level::TRACE) - // sets this to be the default, global collector for this application. - .init(); + tracing_subscriber::registry().with(log_subscriber).init(); } else { let tracer = opentelemetry_otlp::new_pipeline() .tracing() @@ -57,10 +54,10 @@ pub fn init_tracer(cfg: Config) -> Result<(), Box> { let opentelemetry = tracing_opentelemetry::layer().with_tracer(tracer); tracing_subscriber::registry() + .with(log_subscriber) .with(opentelemetry) .try_init()?; } - Ok(()) } @@ -96,9 +93,10 @@ impl Config { build: String, version: String, service_env: String, + debug: bool, ) -> Self { let debug = - std::env::var("IROH_METRICS_DEBUG").unwrap_or_else(|_| "false".to_string()) == "true"; + std::env::var("IROH_METRICS_DEBUG").unwrap_or_else(|_| debug.to_string()) == "true"; let collector_endpoint = std::env::var("IROH_METRICS_COLLECTOR_ENDPOINT") .unwrap_or_else(|_| "http://localhost:4317".to_string()); let prometheus_gateway_endpoint = std::env::var("IROH_METRICS_PROM_GATEWAY_ENDPOINT")