From 0e3ea4b101d398835ea7c536bf6efc9297ba8635 Mon Sep 17 00:00:00 2001 From: Yassin Eldeeb Date: Mon, 8 Jan 2024 09:40:30 +0200 Subject: [PATCH 1/7] otel traces plugin fixes for endpoint filter ok fix stuff ok ok ok ok we have all info and bring back endpoint filter ok let's see now ok error handler now typo ok ok getting there with the config fix otlp for wasm ok wasm reporting is also accurate now ok it works improvements for datadog ok better error handling and reporting to otel of graphql errors ok ok rename plugin allow to customize service name allow to customize all batch configs ok getting there fix test ok integration with query planner ok ok replacing `tracing` with `minitrace` (#351) ok otel plugin docs fixes tiny fix fix ok docs are ready ok fix --- .github/workflows/ci.yaml | 6 +- .github/workflows/release.yaml | 4 +- Cargo.lock | 704 ++++- Cargo.toml | 9 +- README.md | 6 - benchmark/gw.yaml | 2 +- benchmark/k6.js | 4 +- bin/cloudflare_worker/Cargo.toml | 5 +- bin/cloudflare_worker/README.md | 24 +- bin/cloudflare_worker/src/lib.rs | 105 +- bin/conductor/Cargo.toml | 7 + bin/conductor/src/lib.rs | 84 +- bin/conductor/src/minitrace_actix.rs | 221 ++ libs/benches/Cargo.toml | 1 + libs/benches/bench.rs | 3 +- libs/benches/config.yaml | 3 +- libs/common/Cargo.toml | 2 + libs/common/src/graphql.rs | 35 +- libs/common/src/lib.rs | 1 + libs/common/src/plugin.rs | 8 +- libs/config/Cargo.toml | 5 +- libs/config/conductor.schema.json | 2596 +++++++++-------- libs/config/src/lib.rs | 89 +- libs/e2e_tests/Cargo.toml | 5 + libs/e2e_tests/suite.rs | 18 +- libs/e2e_tests/tests/mod.rs | 1 + libs/e2e_tests/tests/plugin_jwt.rs | 5 +- libs/e2e_tests/tests/plugin_telemetry.rs | 116 + libs/engine/Cargo.toml | 10 +- libs/engine/src/gateway.rs | 189 +- libs/engine/src/plugin_manager.rs | 73 +- libs/engine/src/source/federation_source.rs | 38 +- libs/engine/src/source/graphql_source.rs | 27 +- libs/engine/src/source/mock_source.rs | 9 +- libs/engine/src/source/runtime.rs | 20 +- libs/federation_query_planner/Cargo.toml | 4 + libs/federation_query_planner/src/executor.rs | 25 +- libs/federation_query_planner/src/lib.rs | 5 +- .../src/query_planner.rs | 7 +- libs/logger/Cargo.toml | 20 + libs/logger/src/config.rs | 72 + libs/logger/src/lib.rs | 2 + libs/logger/src/logger_layer.rs | 83 + libs/minitrace_reqwest/Cargo.toml | 15 + libs/minitrace_reqwest/src/lib.rs | 120 + libs/tracing/Cargo.toml | 26 + libs/tracing/src/lib.rs | 3 + libs/tracing/src/minitrace_mgr.rs | 183 ++ libs/tracing/src/otel_attrs.rs | 38 + libs/tracing/src/otel_utils.rs | 66 + libs/wasm_polyfills/Cargo.toml | 2 +- libs/wasm_polyfills/src/lib.rs | 5 + plugins/cors/src/plugin.rs | 2 +- plugins/disable_introspection/src/plugin.rs | 2 +- plugins/graphiql/src/plugin.rs | 2 +- plugins/http_get/src/plugin.rs | 2 +- plugins/jwt_auth/src/plugin.rs | 2 +- plugins/telemetry/Cargo.toml | 28 + plugins/telemetry/src/config.rs | 145 + plugins/telemetry/src/lib.rs | 6 + plugins/telemetry/src/plugin.rs | 120 + plugins/trusted_documents/src/plugin.rs | 2 +- plugins/vrl/src/plugin.rs | 2 +- test_config/config.yaml | 14 +- test_config/worker.yaml | 13 +- tests/graphql-over-http/config.yaml | 2 +- website/public/assets/telemetry.png | Bin 0 -> 278869 bytes website/src/components/index-page.tsx | 28 +- website/src/lib/json-schema-ui.tsx | 4 +- website/src/lib/json-schema.ts | 6 +- website/src/pages/_meta.ts | 2 +- website/src/pages/docs/plugins/_meta.ts | 1 + website/src/pages/docs/plugins/telemetry.mdx | 11 + website/theme.config.tsx | 2 +- 74 files changed, 4000 insertions(+), 1507 deletions(-) create mode 100644 bin/conductor/src/minitrace_actix.rs create mode 100644 libs/e2e_tests/tests/plugin_telemetry.rs create mode 100644 libs/logger/Cargo.toml create mode 100644 libs/logger/src/config.rs create mode 100644 libs/logger/src/lib.rs create mode 100644 libs/logger/src/logger_layer.rs create mode 100644 libs/minitrace_reqwest/Cargo.toml create mode 100644 libs/minitrace_reqwest/src/lib.rs create mode 100644 libs/tracing/Cargo.toml create mode 100644 libs/tracing/src/lib.rs create mode 100644 libs/tracing/src/minitrace_mgr.rs create mode 100644 libs/tracing/src/otel_attrs.rs create mode 100644 libs/tracing/src/otel_utils.rs create mode 100644 plugins/telemetry/Cargo.toml create mode 100644 plugins/telemetry/src/config.rs create mode 100644 plugins/telemetry/src/lib.rs create mode 100644 plugins/telemetry/src/plugin.rs create mode 100644 website/public/assets/telemetry.png create mode 100644 website/src/pages/docs/plugins/telemetry.mdx diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c55ac562..29707a16 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -10,7 +10,7 @@ on: jobs: build: - name: tests + name: build & test runs-on: ubuntu-22.04 steps: - name: checkout @@ -30,7 +30,9 @@ jobs: run: cargo install -q worker-build && worker-build - name: test - run: cargo test + run: cargo test -- --nocapture + env: + RUST_BACKTRACE: full - name: install node uses: actions/setup-node@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 46f23e1d..fcd8ae6c 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -33,7 +33,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: build docker images - timeout-minutes: 15 + timeout-minutes: 20 id: docker-bake uses: docker/bake-action@v4 env: @@ -176,4 +176,4 @@ jobs: file: target/${{ matrix.platform.target }}/release/conductor asset_name: conductor-${{ matrix.platform.target }} tag: ${{ github.ref }} - overwrite: true \ No newline at end of file + overwrite: true diff --git a/Cargo.lock b/Cargo.lock index dc6900d5..9ee573f6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -658,12 +658,68 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" +[[package]] +name = "atty" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8" +dependencies = [ + "hermit-abi 0.1.19", + "libc", + "winapi", +] + [[package]] name = "autocfg" version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +[[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 0.2.11", + "http-body", + "hyper", + "itoa", + "matchit 0.7.3", + "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 0.2.11", + "http-body", + "mime", + "rustversion", + "tower-layer", + "tower-service", +] + [[package]] name = "backtrace" version = "0.3.69" @@ -717,6 +773,7 @@ dependencies = [ "conductor_common", "conductor_config", "conductor_engine", + "conductor_tracing", "criterion", "futures", "hyper", @@ -896,6 +953,12 @@ dependencies = [ "bytes", ] +[[package]] +name = "cache-padded" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "981520c98f422fcc584dc1a95c334e6953900b9106bc47a9839b81790009eb21" + [[package]] name = "cast" version = "0.3.0" @@ -1093,6 +1156,18 @@ version = "0.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "702fc72eb24e5a1e48ce58027a675bc24edd52096d5397d4aea7c6dd9eca0bd1" +[[package]] +name = "coarsetime" +version = "0.1.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71367d3385c716342014ad17e3d19f7788ae514885a1f4c24f500260fb365e1a" +dependencies = [ + "libc", + "once_cell", + "wasi", + "wasm-bindgen", +] + [[package]] name = "codespan-reporting" version = "0.11.1" @@ -1144,12 +1219,18 @@ name = "conductor" version = "0.1.0" dependencies = [ "actix-web", + "anyhow", "conductor_common", "conductor_config", "conductor_engine", + "conductor_logger", + "conductor_tracing", + "futures-util", + "minitrace", "openssl", "tracing", "tracing-subscriber", + "ulid", ] [[package]] @@ -1159,6 +1240,7 @@ dependencies = [ "conductor_common", "conductor_config", "conductor_engine", + "conductor_logger", "console_error_panic_hook", "time", "tracing", @@ -1179,9 +1261,11 @@ dependencies = [ "http 0.2.11", "http-body", "mime", + "minitrace", "once_cell", "querystring", "reqwest", + "reqwest-middleware", "schemars", "serde", "serde_json", @@ -1196,6 +1280,8 @@ name = "conductor_config" version = "0.0.0" dependencies = [ "conductor_common", + "conductor_logger", + "conductor_tracing", "cors_plugin", "disable_introspection_plugin", "graphiql_plugin", @@ -1207,6 +1293,7 @@ dependencies = [ "serde", "serde_json", "serde_yaml 0.9.30", + "telemetry_plugin", "tracing", "trusted_documents_plugin", "vrl_plugin", @@ -1221,6 +1308,7 @@ dependencies = [ "base64 0.21.7", "conductor_common", "conductor_config", + "conductor_tracing", "cors_plugin", "disable_introspection_plugin", "federation_query_planner", @@ -1230,9 +1318,13 @@ dependencies = [ "humantime", "jwt_auth_plugin", "match_content_type_plugin", + "minitrace", + "minitrace_reqwest", "reqwest", + "reqwest-middleware", "serde", "serde_json", + "telemetry_plugin", "thiserror", "tokio", "tracing", @@ -1243,6 +1335,19 @@ dependencies = [ "wasm_polyfills", ] +[[package]] +name = "conductor_logger" +version = "0.0.0" +dependencies = [ + "atty", + "schemars", + "serde", + "serde_json", + "tracing", + "tracing-subscriber", + "tracing-web", +] + [[package]] name = "conductor_napi_lib" version = "0.0.0" @@ -1254,6 +1359,26 @@ dependencies = [ "napi-derive", ] +[[package]] +name = "conductor_tracing" +version = "0.0.0" +dependencies = [ + "conductor_common", + "minitrace", + "opentelemetry", + "opentelemetry_sdk", + "rand", + "reqwest", + "reqwest-middleware", + "schemars", + "serde", + "serde_json", + "task-local-extensions", + "tracing", + "tracing-opentelemetry", + "wasm_polyfills", +] + [[package]] name = "console" version = "0.15.8" @@ -1394,6 +1519,15 @@ dependencies = [ "itertools 0.10.5", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "176dc175b78f56c0f321911d9c8eb2b77a78a4860b9c19db83835fea1a46649b" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.5" @@ -1472,6 +1606,16 @@ dependencies = [ "memchr", ] +[[package]] +name = "ctor" +version = "0.1.26" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d2301688392eb071b0bf1a37be05c469d3cc4dbbd95df672fe28ab021e6a096" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "ctor" version = "0.2.6" @@ -1618,12 +1762,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "doc-comment" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea41bba32d969b513997752735605054bc0dfa92b4c56bf1189f2e174be7a10" - [[package]] name = "dyn-clone" version = "1.0.16" @@ -1637,6 +1775,7 @@ dependencies = [ "conductor_common", "conductor_config", "conductor_engine", + "conductor_tracing", "cors_plugin", "disable_introspection_plugin", "graphiql_plugin", @@ -1645,8 +1784,10 @@ dependencies = [ "jwt_auth_plugin", "lazy_static", "match_content_type_plugin", + "minitrace", "serde", "serde_json", + "telemetry_plugin", "tokio", "trusted_documents_plugin", "vrl_plugin", @@ -1767,17 +1908,21 @@ dependencies = [ "anyhow", "async-graphql", "async-trait", + "conductor_tracing", "criterion", "futures", "graphql-parser", "insta", "lazy_static", "linked-hash-map", + "minitrace", + "minitrace_reqwest", "reqwest", "serde", "serde_json", "tokio", "tracing", + "wasm_polyfills", ] [[package]] @@ -2088,6 +2233,15 @@ version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" +[[package]] +name = "hermit-abi" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33" +dependencies = [ + "libc", +] + [[package]] name = "hermit-abi" version = "0.3.4" @@ -2244,6 +2398,18 @@ dependencies = [ "want", ] +[[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 = "hyper-tls" version = "0.5.0" @@ -2304,6 +2470,7 @@ checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" dependencies = [ "autocfg", "hashbrown 0.12.3", + "serde", ] [[package]] @@ -2362,7 +2529,7 @@ version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eae7b9aee968036d54dce06cebaefd919e4472e753296daccd6d344e3e2df0c2" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.4", "libc", "windows-sys 0.48.0", ] @@ -2379,7 +2546,7 @@ version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bad00257d07be169d870ab665980b06cdb366d792ad690bf2e76876dc503455" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.4", "rustix 0.38.30", "windows-sys 0.52.0", ] @@ -2419,9 +2586,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.63" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f37a4a5928311ac501dee68b3c7613a1037d0edb30c8e5427bd832d55d1b790" +checksum = "9a1d36f1235bc969acba30b7f5990b864423a6068a10f7c90ae8f0112e3a59d1" dependencies = [ "wasm-bindgen", ] @@ -2642,6 +2809,12 @@ version = "0.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9376a4f0340565ad675d11fc1419227faf5f60cd7ac9cb2e7185a471f30af833" +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -2664,12 +2837,95 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "minimal-lexical" version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "minitrace" +version = "0.6.2" +source = "git+https://github.com/dotansimha/minitrace-rust.git?rev=2f3bad8297db0f9b980e3f69e73401ce957dccd1#2f3bad8297db0f9b980e3f69e73401ce957dccd1" +dependencies = [ + "futures", + "minitrace-macro", + "minstant", + "once_cell", + "parking_lot", + "pin-project", + "rand", + "rtrb", +] + +[[package]] +name = "minitrace-datadog" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8e529dacf89c312af24954807e682816ec0ef2891839f29aee88b56c5d23acf" +dependencies = [ + "minitrace", + "reqwest", + "rmp-serde", + "serde", +] + +[[package]] +name = "minitrace-jaeger" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ec139a7ece89fc1595003cafeaa9d5f466daa16e27c8ddbcdf2c4b33beb5972" +dependencies = [ + "log", + "minitrace", + "thrift_codec", +] + +[[package]] +name = "minitrace-macro" +version = "0.6.2" +source = "git+https://github.com/dotansimha/minitrace-rust.git?rev=2f3bad8297db0f9b980e3f69e73401ce957dccd1#2f3bad8297db0f9b980e3f69e73401ce957dccd1" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "minitrace-opentelemetry" +version = "0.6.2" +source = "git+https://github.com/dotansimha/minitrace-rust.git?rev=2f3bad8297db0f9b980e3f69e73401ce957dccd1#2f3bad8297db0f9b980e3f69e73401ce957dccd1" +dependencies = [ + "futures", + "log", + "minitrace", + "opentelemetry", + "opentelemetry_sdk", +] + +[[package]] +name = "minitrace_reqwest" +version = "0.0.0" +dependencies = [ + "async-trait", + "conductor_tracing", + "minitrace", + "reqwest", + "reqwest-middleware", + "task-local-extensions", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -2679,6 +2935,16 @@ dependencies = [ "adler", ] +[[package]] +name = "minstant" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaedbe34cd00417d78bd9cab37a8e2dffa8741c0c370ec1ac54485cd92d1c719" +dependencies = [ + "coarsetime", + "ctor 0.1.26", +] + [[package]] name = "mio" version = "0.8.10" @@ -2716,7 +2982,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2fc1cb00cde484640da9f01a124edbb013576a6ae9843b23857c940936b76d91" dependencies = [ "bitflags 2.4.2", - "ctor", + "ctor 0.2.6", "napi-derive", "napi-sys", "once_cell", @@ -2846,7 +3112,7 @@ version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4161fcb6d602d4d2081af7c3a45852d875a03dd337a6bfdd6e06407b61342a43" dependencies = [ - "hermit-abi", + "hermit-abi 0.3.4", "libc", ] @@ -2983,6 +3249,82 @@ dependencies = [ "vcpkg", ] +[[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-otlp" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f24cda83b20ed2433c68241f918d0f6fdec8b1d43b7a9590ab4420c5095ca930" +dependencies = [ + "async-trait", + "futures-core", + "http 0.2.11", + "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.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f16aec8a98a457a52664d69e0091bac3a0abd18ead9b641cb00202ba4e0efe4" +dependencies = [ + "async-trait", + "crossbeam-channel", + "futures-channel", + "futures-executor", + "futures-util", + "glob", + "once_cell", + "opentelemetry", + "ordered-float", + "percent-encoding", + "rand", + "thiserror", +] + [[package]] name = "ordered-float" version = "4.2.0" @@ -3336,6 +3678,7 @@ dependencies = [ "proc-macro-error-attr", "proc-macro2", "quote", + "syn 1.0.109", "version_check", ] @@ -3359,6 +3702,29 @@ dependencies = [ "unicode-ident", ] +[[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 = "ptr_meta" version = "0.1.4" @@ -3555,6 +3921,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -3573,6 +3940,21 @@ dependencies = [ "winreg", ] +[[package]] +name = "reqwest-middleware" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88a3e86aa6053e59030e7ce2d2a3b258dd08fc2d337d52f73f6cb480f5858690" +dependencies = [ + "anyhow", + "async-trait", + "http 0.2.11", + "reqwest", + "serde", + "task-local-extensions", + "thiserror", +] + [[package]] name = "ring" version = "0.17.7" @@ -3616,12 +3998,43 @@ dependencies = [ "syn 1.0.109", ] +[[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 = "rmp-serde" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bffea85eea980d8a74453e5d02a8d93028f3c34725de143085a844ebe953258a" +dependencies = [ + "byteorder", + "rmp", + "serde", +] + [[package]] name = "roxmltree" version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3cd14fd5e3b777a7422cca79358c57a8f6e3a703d9ac187448d0daf220c2407f" +[[package]] +name = "rtrb" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "99e704dd104faf2326a320140f70f0b736d607c1caa1b1748a6c568a79819109" +dependencies = [ + "cache-padded", +] + [[package]] name = "rust_decimal" version = "1.33.1" @@ -3748,6 +4161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "45a28f4c49489add4ce10783f7911893516f15afe45d015608d41faca6bc4d29" dependencies = [ "dyn-clone", + "indexmap 1.9.3", "schemars_derive", "serde", "serde_json", @@ -3845,6 +4259,17 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "serde-wasm-bindgen" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9b713f70513ae1f8d92665bbbbda5c295c2cf1da5542881ae5eefe20c9af132" +dependencies = [ + "js-sys", + "serde", + "wasm-bindgen", +] + [[package]] name = "serde_derive" version = "1.0.195" @@ -3873,7 +4298,6 @@ version = "1.0.111" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "176e46fa42316f18edd598015a5166857fc835ec732f5215eac6b7bdbf0a84f4" dependencies = [ - "indexmap 2.1.0", "itoa", "ryu", "serde", @@ -4034,24 +4458,23 @@ checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" [[package]] name = "snafu" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4de37ad025c587a29e8f3f5605c00f70b98715ef90b9061a815b9e59e9042d6" +checksum = "d342c51730e54029130d7dc9fd735d28c4cd360f1368c01981d4f03ff207f096" dependencies = [ - "doc-comment", "snafu-derive", ] [[package]] name = "snafu-derive" -version = "0.7.5" +version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "990079665f075b699031e9c08fd3ab99be5029b96f3b78dc0709e8f77e4efebf" +checksum = "080c44971436b1af15d6f61ddd8b543995cf63ab8e677d46b00cc06f4ef267a0" dependencies = [ "heck", "proc-macro2", "quote", - "syn 1.0.109", + "syn 2.0.48", ] [[package]] @@ -4182,6 +4605,12 @@ dependencies = [ "syn 2.0.48", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + [[package]] name = "syslog_loose" version = "0.21.0" @@ -4219,6 +4648,35 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "task-local-extensions" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" +dependencies = [ + "pin-utils", +] + +[[package]] +name = "telemetry_plugin" +version = "0.0.0" +dependencies = [ + "async-trait", + "conductor_common", + "conductor_tracing", + "humantime-serde", + "minitrace", + "minitrace-datadog", + "minitrace-jaeger", + "minitrace-opentelemetry", + "opentelemetry", + "opentelemetry-otlp", + "opentelemetry_sdk", + "schemars", + "serde", + "serde_json", +] + [[package]] name = "tempfile" version = "3.9.0" @@ -4282,6 +4740,16 @@ dependencies = [ "once_cell", ] +[[package]] +name = "thrift_codec" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce3200b189fd4733eb2bb22235755c8aa0361ba1c66b67db54893144d147279" +dependencies = [ + "byteorder", + "trackable", +] + [[package]] name = "time" version = "0.3.31" @@ -4365,6 +4833,16 @@ dependencies = [ "windows-sys 0.48.0", ] +[[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" @@ -4386,6 +4864,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" @@ -4428,6 +4917,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.7", + "bytes", + "futures-core", + "futures-util", + "h2", + "http 0.2.11", + "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" @@ -4478,6 +5021,24 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-opentelemetry" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c67ac25c5407e7b961fafc6f7e9aa5958fd297aada2d20fa2ae1737357e55596" +dependencies = [ + "js-sys", + "once_cell", + "opentelemetry", + "opentelemetry_sdk", + "smallvec", + "tracing", + "tracing-core", + "tracing-log", + "tracing-subscriber", + "web-time", +] + [[package]] name = "tracing-serde" version = "0.1.3" @@ -4523,6 +5084,25 @@ dependencies = [ "web-sys", ] +[[package]] +name = "trackable" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15bd114abb99ef8cee977e517c8f37aee63f184f2d08e3e6ceca092373369ae" +dependencies = [ + "trackable_derive", +] + +[[package]] +name = "trackable_derive" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebeb235c5847e2f82cfe0f07eb971d1e5f6804b18dac2ae16349cc604380f82f" +dependencies = [ + "quote", + "syn 1.0.109", +] + [[package]] name = "trusted_documents_plugin" version = "0.0.0" @@ -4569,6 +5149,24 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" +[[package]] +name = "ulid" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e37c4b6cbcc59a8dcd09a6429fbc7890286bcbb79215cea7b38a3c4c0921d93" +dependencies = [ + "rand", +] + +[[package]] +name = "unicase" +version = "2.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" +dependencies = [ + "version_check", +] + [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4666,6 +5264,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "urlencoding" +version = "2.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" + [[package]] name = "utf8-width" version = "0.1.7" @@ -4720,8 +5324,8 @@ checksum = "6a02e4885ed3bc0f2de90ea6dd45ebcbb66dacffe03547fadbb0eeae2770887d" [[package]] name = "vrl" -version = "0.9.0" -source = "git+https://github.com/vectordotdev/vrl.git?rev=afaca43e3ed266827e0921d1790a7f11e0abc0f0#afaca43e3ed266827e0921d1790a7f11e0abc0f0" +version = "0.9.1" +source = "git+https://github.com/vectordotdev/vrl.git?rev=2b39353b3236e0aac26314ad47153238c52aa2ff#2b39353b3236e0aac26314ad47153238c52aa2ff" dependencies = [ "aes", "base16", @@ -4857,9 +5461,9 @@ checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" [[package]] name = "wasm-bindgen" -version = "0.2.86" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5bba0e8cb82ba49ff4e229459ff22a191bbe9a1cb3a341610c9c33efc27ddf73" +checksum = "b1223296a201415c7fad14792dbefaace9bd52b62d33453ade1c5b5f07555406" dependencies = [ "cfg-if", "wasm-bindgen-macro", @@ -4867,9 +5471,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-backend" -version = "0.2.86" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19b04bc93f9d6bdee709f6bd2118f57dd6679cf1176a1af464fca3ab0d66d8fb" +checksum = "fcdc935b63408d58a32f8cc9738a0bffd8f05cc7c002086c6ef20b7312ad9dcd" dependencies = [ "bumpalo", "log", @@ -4882,9 +5486,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-futures" -version = "0.4.36" +version = "0.4.40" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2d1985d03709c53167ce907ff394f5316aa22cb4e12761295c5dc57dacb6297e" +checksum = "bde2032aeb86bdfaecc8b261eef3cba735cc426c1f3a3416d1e0791be95fc461" dependencies = [ "cfg-if", "js-sys", @@ -4894,9 +5498,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.86" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14d6b024f1a526bb0234f52840389927257beb670610081360e5a03c5df9c258" +checksum = "3e4c238561b2d428924c49815533a8b9121c664599558a5d9ec51f8a1740a999" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -4904,9 +5508,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.86" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e128beba882dd1eb6200e1dc92ae6c5dbaa4311aa7bb211ca035779e5efc39f8" +checksum = "bae1abb6806dc1ad9e560ed242107c0f6c84335f1749dd4e8ddb012ebd5e25a7" dependencies = [ "proc-macro2", "quote", @@ -4917,15 +5521,15 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.86" +version = "0.2.90" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed9d5b4305409d1fc9482fee2d7f9bcbf24b3972bf59817ef757e23982242a93" +checksum = "4d91413b1c31d7539ba5ef2451af3f0b833a005eb27a631cec32bc0635a8602b" [[package]] name = "wasm-streams" -version = "0.3.0" +version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4609d447824375f43e1ffbc051b50ad8f4b3ae8219680c94452ea05eb240ac7" +checksum = "b65dc4c90b63b118468cf747d8bf3566c1913ef60be765b5730ead9e0a3ba129" dependencies = [ "futures-util", "js-sys", @@ -4946,9 +5550,19 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.63" +version = "0.3.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3bdd9ef4e984da1187bf8110c5cf5b845fbc87a23602cdf912386a76fcd3a7c2" +checksum = "58cd2333b6e0be7a39605f0e255892fd7418a682d8da8fe042fe25128794d2ed" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "web-time" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa30049b1c872b72c89866d458eae9f20380ab280ffd1b1e18df2d3e2d98cfe0" dependencies = [ "js-sys", "wasm-bindgen", @@ -5164,8 +5778,7 @@ dependencies = [ [[package]] name = "worker" version = "0.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9cd7ad167392bdd707a963356f3478844019c74fc89f6af0dfc656914b30af24" +source = "git+https://github.com/dotansimha/workers-rs.git?rev=6de1efc2ef155236d8caf376cce2a0f9972e241f#6de1efc2ef155236d8caf376cce2a0f9972e241f" dependencies = [ "async-trait", "chrono", @@ -5174,11 +5787,12 @@ dependencies = [ "futures-util", "http 0.2.11", "js-sys", - "matchit", + "matchit 0.4.6", "pin-project", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.6.3", "serde_json", + "serde_urlencoded", "tokio", "url", "wasm-bindgen", @@ -5198,7 +5812,7 @@ checksum = "3d4b9fe1a87b7aef252fceb4f30bf6303036a5de329c81ccad9be9c35d1fdbc7" dependencies = [ "js-sys", "serde", - "serde-wasm-bindgen", + "serde-wasm-bindgen 0.5.0", "serde_json", "thiserror", "wasm-bindgen", @@ -5208,8 +5822,7 @@ dependencies = [ [[package]] name = "worker-macros" version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "306c6b6fc316ce129de9cc393dc614b244afb37d43d8ae7a4dccf45d6f8a5ff5" +source = "git+https://github.com/dotansimha/workers-rs.git?rev=6de1efc2ef155236d8caf376cce2a0f9972e241f#6de1efc2ef155236d8caf376cce2a0f9972e241f" dependencies = [ "async-trait", "proc-macro2", @@ -5224,8 +5837,7 @@ dependencies = [ [[package]] name = "worker-sys" version = "0.0.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f5db3bd0e45980dbcefe567c978b4930e4526e864cd9e70482252a60229ddd7" +source = "git+https://github.com/dotansimha/workers-rs.git?rev=6de1efc2ef155236d8caf376cce2a0f9972e241f#6de1efc2ef155236d8caf376cce2a0f9972e241f" dependencies = [ "cfg-if", "js-sys", diff --git a/Cargo.toml b/Cargo.toml index 818e917e..8dd5bf42 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,7 +7,7 @@ exclude = ["bin/npm", "tests/test-server", "tools/panic_free_analyzer"] tokio = "1.35.1" futures = "0.3.30" serde = { version = "1.0.195", features = ["derive"] } -serde_json = { version = "1.0.111", features = ["preserve_order"] } +serde_json = { version = "1.0.111" } tracing = "0.1.40" http = "0.2.11" http-body = "0.4.6" @@ -16,16 +16,21 @@ async-trait = "0.1.77" anyhow = "1.0.79" reqwest = "0.11.23" thiserror = "1.0.56" +reqwest-middleware = "0.2.4" tracing-subscriber = "0.3.18" base64 = "0.21.7" schemars = "0.8.16" -vrl = { git = "https://github.com/vectordotdev/vrl.git", rev = "afaca43e3ed266827e0921d1790a7f11e0abc0f0", default-features = false, features = [ +vrl = { git = "https://github.com/vectordotdev/vrl.git", rev = "2b39353b3236e0aac26314ad47153238c52aa2ff", default-features = false, features = [ "string_path", "compiler", "value", "stdlib", ] } +minitrace = { git = "https://github.com/dotansimha/minitrace-rust.git", rev = "2f3bad8297db0f9b980e3f69e73401ce957dccd1" } [profile.release.package.conductor-cf-worker] strip = true codegen-units = 1 + +[patch.crates-io] +minitrace = { git = "https://github.com/dotansimha/minitrace-rust.git", rev = "2f3bad8297db0f9b980e3f69e73401ce957dccd1" } diff --git a/README.md b/README.md index 2a070450..eba97cdf 100644 --- a/README.md +++ b/README.md @@ -52,12 +52,6 @@ Conductor's configuration can be defined in both YAML and JSON formats. The conf ### Configuration File Example ```yaml -server: - port: 9000 - -logger: - level: info - sources: - type: graphql id: my-source diff --git a/benchmark/gw.yaml b/benchmark/gw.yaml index 1275197c..a8a2ba9c 100644 --- a/benchmark/gw.yaml +++ b/benchmark/gw.yaml @@ -2,7 +2,7 @@ server: port: 9000 logger: - level: error + filter: error sources: - id: upstream diff --git a/benchmark/k6.js b/benchmark/k6.js index 43a6aa06..02a415d8 100644 --- a/benchmark/k6.js +++ b/benchmark/k6.js @@ -10,7 +10,7 @@ import { Rate } from "k6/metrics"; export const validGraphQLResponse = new Rate("valid_graphql_response"); export const validHttpCode = new Rate("valid_http_code"); -const RPS = 1000; +const RPS = 500; const TIME_SECONDS = 60; const SCENARIO_NAME = `rps_${RPS}`; const REQ_THRESHOLD = RPS * TIME_SECONDS - 1; @@ -29,7 +29,7 @@ export const options = { // The following two are here to make sure the runtime (CI, local) is capable of producing the desired RPS [`iterations{scenario:${SCENARIO_NAME}}`]: [`count>=${REQ_THRESHOLD}`], [`http_reqs{scenario:${SCENARIO_NAME}}`]: [`count>=${REQ_THRESHOLD}`], - [`http_req_duration{scenario:${SCENARIO_NAME}}`]: ["avg<=2", "p(99)<=3"], + [`http_req_duration{scenario:${SCENARIO_NAME}}`]: ["avg<=2"], [`http_req_failed{scenario:${SCENARIO_NAME}}`]: ["rate==0"], [`${validGraphQLResponse.name}{scenario:${SCENARIO_NAME}}`]: ["rate==1"], [`${validHttpCode.name}{scenario:${SCENARIO_NAME}}`]: ["rate==1"], diff --git a/bin/cloudflare_worker/Cargo.toml b/bin/cloudflare_worker/Cargo.toml index 21457b82..0d256144 100644 --- a/bin/cloudflare_worker/Cargo.toml +++ b/bin/cloudflare_worker/Cargo.toml @@ -14,12 +14,13 @@ wasm-opt = false crate-type = ["cdylib"] [dependencies] -worker = "0.0.18" +worker = { git = "https://github.com/dotansimha/workers-rs.git", rev = "6de1efc2ef155236d8caf376cce2a0f9972e241f" } conductor_config = { path = "../../libs/config" } conductor_engine = { path = "../../libs/engine" } conductor_common = { path = "../../libs/common" } +conductor_logger = { path = "../../libs/logger" } tracing = { workspace = true } tracing-web = "0.1.3" -tracing-subscriber = { version = "0.3.18", features = ['time', 'json'] } +tracing-subscriber = { workspace = true, features = ['time', 'json'] } time = { version = "0.3.31", features = ['wasm-bindgen'] } console_error_panic_hook = "0.1.7" diff --git a/bin/cloudflare_worker/README.md b/bin/cloudflare_worker/README.md index 1618d499..6e727b97 100644 --- a/bin/cloudflare_worker/README.md +++ b/bin/cloudflare_worker/README.md @@ -1,6 +1,16 @@ # Conductor (WASM) for CloudFlare Workers -## Note on building for macos users +## Running Locally + +1. Ensure you have NodeJS (>=18), `pnpm` (8). +2. Install NodeJS dependencies by using: `pnpm install` +3. Run `pnpm dev` + +> The default `dev` script is using the `test_config/worker.yaml` configuration from the root of this repository. + +## Build + +### Note on building for MacOS users If you ran `cargo install -q worker-build && worker-build --release`, and faced something similar to the following error on macos: @@ -14,11 +24,11 @@ Error: wasm-pack exited with status exit status: 1 Then, follow the below steps: +1. Install LLVM: `brew install llvm` +1. Configure your env to use the following vars: + ```sh - export LDFLAGS="-L/opt/homebrew/opt/llvm/lib" - export CPPFLAGS="-I/opt/homebrew/opt/llvm/include" - export PATH="/opt/homebrew/opt/llvm/bin:$PATH" -brew install llvm +export LDFLAGS="-L/opt/homebrew/opt/llvm/lib" +export CPPFLAGS="-I/opt/homebrew/opt/llvm/include" +export PATH="/opt/homebrew/opt/llvm/bin:$PATH" ``` - -And retry again, it should be resolved! diff --git a/bin/cloudflare_worker/src/lib.rs b/bin/cloudflare_worker/src/lib.rs index eba59c16..c4bf56fb 100644 --- a/bin/cloudflare_worker/src/lib.rs +++ b/bin/cloudflare_worker/src/lib.rs @@ -1,17 +1,17 @@ use std::str::FromStr; use conductor_common::http::{ - ConductorHttpRequest, ConductorHttpResponse, HeaderName, HeaderValue, HttpHeadersMap, Method, + header::USER_AGENT, ConductorHttpRequest, ConductorHttpResponse, HeaderName, HeaderValue, + HttpHeadersMap, Method, }; use conductor_config::parse_config_contents; -use conductor_engine::gateway::{ConductorGateway, GatewayError}; +use conductor_engine::gateway::{ConductorGateway, ConductorGatewayRouteData, GatewayError}; use std::panic; -use tracing_subscriber::fmt::time::UtcTime; +use tracing::{Instrument, Span}; use tracing_subscriber::prelude::*; -use tracing_web::MakeConsoleWriter; use worker::*; -#[tracing::instrument(level = "debug", skip(url, req))] +#[tracing::instrument(level = "debug", skip(url, req), name = "transform_http_request")] async fn transform_req(url: &Url, mut req: Request) -> Result { let mut headers_map = HttpHeadersMap::new(); @@ -35,6 +35,11 @@ async fn transform_req(url: &Url, mut req: Request) -> Result Result { let mut response_headers = Headers::new(); for (k, v) in conductor_response.headers.into_iter() { @@ -51,7 +56,43 @@ fn transform_res(conductor_response: ConductorHttpResponse) -> Result }) } -async fn run_flow(req: Request, env: Env, _ctx: Context) -> Result { +fn build_root_span(route_date: &ConductorGatewayRouteData, req: &Request) -> Span { + let method_str = req.method().to_string(); + let path_str = req.path(); + let name = format!("{} {}", method_str, path_str); + let http_protocol = req.cf().map(|v| v.http_protocol()); + let url = req.url().ok(); + let host = url.as_ref().and_then(|v| v.host().map(|v| v.to_string())); + let scheme = url.as_ref().map(|v| v.scheme().to_string()); + let user_agent = req.headers().get(USER_AGENT.as_str()).ok().and_then(|v| v); + // Based on https://developers.cloudflare.com/network/true-client-ip-header/ + let client_ip = req + .headers() + .get("true-client-ip") + .map_err(|_| req.headers().get("cf-connecting-ip")) + .ok() + .and_then(|v| v); + + tracing::info_span!( + "HTTP request", + "otel.name" = name, + "otel.kind" = "server", + endpoint = route_date.endpoint, + "http.method" = method_str, + "http.flavor" = http_protocol, + "http.host" = host, + "http.scheme" = scheme, + "http.path" = path_str, + "http.client_ip" = client_ip, + "http.user_agent" = user_agent, + "otel.status_code" = tracing::field::Empty, + "http.status_code" = tracing::field::Empty, + "trace_id" = tracing::field::Empty, + "request_id" = tracing::field::Empty, + ) +} + +async fn run_flow(req: Request, env: Env) -> Result { let conductor_config_str = env.var("CONDUCTOR_CONFIG").map(|v| v.to_string()); let get_env_value = |key: &str| env.var(key).map(|s| s.to_string()).ok(); @@ -63,16 +104,35 @@ async fn run_flow(req: Request, env: Env, _ctx: Context) -> Result { get_env_value, ); - match ConductorGateway::new(&conductor_config).await { + let logger_config = conductor_config.logger.clone().unwrap_or_default(); + let logger = conductor_logger::logger_layer::build_logger( + &logger_config.format, + &logger_config.filter, + logger_config.print_performance_info, + ) + .unwrap_or_else(|e| panic!("failed to build logger: {}", e)); + + match ConductorGateway::new(&conductor_config, &mut None).await { Ok(gw) => { + let _ = tracing_subscriber::registry().with(logger).try_init(); let url = req.url()?; match gw.match_route(&url) { Ok(route_data) => { - let conductor_req = transform_req(&url, req).await?; - let conductor_response = ConductorGateway::execute(conductor_req, route_data).await; + let root_span = build_root_span(route_data, &req); + + async move { + let conductor_req = transform_req(&url, req).await?; + let conductor_response = ConductorGateway::execute(conductor_req, route_data).await; - transform_res(conductor_response) + let status_code = conductor_response.status.as_u16(); + Span::current().record("otel.status_code", status_code); + Span::current().record("http.status_code", status_code); + + transform_res(conductor_response) + } + .instrument(root_span) + .await } Err(GatewayError::MissingEndpoint(_)) => { Response::error("failed to locate endpoint".to_string(), 404) @@ -91,20 +151,23 @@ async fn run_flow(req: Request, env: Env, _ctx: Context) -> Result { fn start() { // This will make sure to capture runtime events from the WASM and print it to the log panic::set_hook(Box::new(console_error_panic_hook::hook)); - - // This will make sure to capture the logs from the WASM and print it to the log - let fmt_layer = tracing_subscriber::fmt::layer() - .json() - .with_ansi(false) - .with_timer(UtcTime::rfc_3339()) // std::time is not available in wasm env - .with_writer(MakeConsoleWriter); - tracing_subscriber::registry().with(fmt_layer).init(); } #[event(fetch, respond_with_errors)] -async fn main(req: Request, env: Env, ctx: Context) -> Result { - match run_flow(req, env, ctx).await { - Ok(response) => Ok(response), +async fn main(req: Request, env: Env, _ctx: Context) -> Result { + let result = run_flow(req, env).await; + + match result { + Ok(response) => { + // todo: flush + // if let Some(tracing_manager) = tracing_manager { + // ctx.wait_until(async move { + // tracing_manager.shutdown().await; + // }); + // } + + Ok(response) + } Err(e) => Response::error(e.to_string(), 500), } } diff --git a/bin/conductor/Cargo.toml b/bin/conductor/Cargo.toml index 1d13d04f..8a0d9727 100644 --- a/bin/conductor/Cargo.toml +++ b/bin/conductor/Cargo.toml @@ -15,11 +15,18 @@ path = "src/lib.rs" conductor_config = { path = "../../libs/config" } conductor_engine = { path = "../../libs/engine" } conductor_common = { path = "../../libs/common" } +conductor_tracing = { path = "../../libs/tracing" } +conductor_logger = { path = "../../libs/logger" } +anyhow = { workspace = true } actix-web = "4.4.1" +futures-util = "0.3.30" +ulid = "1.1.0" tracing = { workspace = true } openssl = { version = "0.10", features = ["vendored"] } tracing-subscriber = { workspace = true, features = [ + "registry", "fmt", "env-filter", "time", ] } +minitrace = { workspace = true, features = ["enable"] } diff --git a/bin/conductor/src/lib.rs b/bin/conductor/src/lib.rs index 7a693822..8f3a8a97 100644 --- a/bin/conductor/src/lib.rs +++ b/bin/conductor/src/lib.rs @@ -1,7 +1,10 @@ +mod minitrace_actix; + use std::sync::Arc; use actix_web::{ dev::Response, + middleware::Compat, route, web::{self, Bytes}, App, HttpRequest, HttpResponse, HttpServer, Responder, Scope, @@ -9,51 +12,38 @@ use actix_web::{ use conductor_common::http::{ConductorHttpRequest, ConductorHttpResponse, HttpHeadersMap}; use conductor_config::load_config; use conductor_engine::gateway::{ConductorGateway, ConductorGatewayRouteData}; -use tracing::{debug, error, info}; -use tracing_subscriber::{ - fmt::{self, format::FmtSpan, time::UtcTime}, - layer::SubscriberExt, - registry, reload, EnvFilter, -}; +use conductor_tracing::minitrace_mgr::MinitraceManager; +use minitrace::{collector::Config, trace}; +use tracing::{debug, error}; +use tracing_subscriber::{layer::SubscriberExt, registry}; -pub async fn run_services(config_file_path: &String) -> std::io::Result<()> { - // Initialize logging with `info` before we read the `logger` config from file - let filter = EnvFilter::new("info"); - let (filter, reload_handle) = reload::Layer::new(filter); - let subscriber = registry::Registry::default().with(filter).with( - fmt::Layer::default() - .with_timer(UtcTime::rfc_3339()) - .with_span_events(FmtSpan::CLOSE), - ); - // Set the subscriber as the global default. - // @expected: we need to exit the process, if the logger can't be correctly set. - tracing::subscriber::set_global_default(subscriber).expect("failed to set up the logger"); - - info!("gateway process started"); - info!("loading configuration from {}", config_file_path); - let config_object = load_config(config_file_path, |key| std::env::var(key).ok()).await; - info!("configuration loaded and parsed"); - - // If there's a logger config, modify the logging level to match the config - if let Some(logger_config) = &config_object.logger { - let new_level = logger_config.level.into_level().to_string(); - reload_handle - .modify(|filter| { - *filter = EnvFilter::new(new_level); - }) - // @expected: we need to exit, if the provided log level in the configuration file is incompaitable. - .expect("Failed to modify the log level"); - } +use crate::minitrace_actix::MinitraceTransform; - debug!("building gateway from configuration..."); - match ConductorGateway::new(&config_object).await { +pub async fn run_services(config_file_path: &String) -> std::io::Result<()> { + let config = load_config(config_file_path, |key| std::env::var(key).ok()).await; + let logger_config = config.logger.clone().unwrap_or_default(); + let logger = conductor_logger::logger_layer::build_logger( + &logger_config.format, + &logger_config.filter, + logger_config.print_performance_info, + ) + .unwrap_or_else(|e| panic!("failed to build logger: {}", e)); + let mut tracing_manager = MinitraceManager::default(); + + match ConductorGateway::new(&config, &mut Some(&mut tracing_manager)).await { Ok(gw) => { + let subscriber = registry::Registry::default().with(logger); + // @expected: we need to exit the process, if the logger can't be correctly set. + tracing::subscriber::set_global_default(subscriber).expect("failed to set up tracing"); + minitrace::set_reporter(tracing_manager.build_reporter(), Config::default()); + let gateway = Arc::new(gw); let http_server = HttpServer::new(move || { let mut router = App::new(); for conductor_route in gateway.routes.iter() { let child_router = Scope::new(conductor_route.base_path.as_str()) + .wrap(Compat::new(MinitraceTransform::new())) .app_data(web::Data::new(conductor_route.route_data.clone())) .service(Scope::new("").default_service( web::route().to(handler), // handle all requests with this handler @@ -65,14 +55,18 @@ pub async fn run_services(config_file_path: &String) -> std::io::Result<()> { router.service(health_handler) }); - let server_config = config_object.server.clone().unwrap_or_default(); + let server_config = config.server.clone().unwrap_or_default(); let server_address = format!("{}:{}", server_config.host, server_config.port); debug!("server is trying to listen on {:?}", server_address); - http_server + let server_instance = http_server .bind((server_config.host, server_config.port))? .run() - .await + .await; + + minitrace::flush(); + + server_instance } Err(e) => { error!("failed to initialize gateway: {:?}", e); @@ -87,7 +81,7 @@ async fn health_handler() -> impl Responder { Response::ok() } -#[tracing::instrument(level = "debug", skip(req, body))] +#[trace(name = "transform_request")] fn transform_req(req: HttpRequest, body: Bytes) -> ConductorHttpRequest { let mut headers_map = HttpHeadersMap::new(); @@ -106,7 +100,7 @@ fn transform_req(req: HttpRequest, body: Bytes) -> ConductorHttpRequest { conductor_request } -#[tracing::instrument(level = "debug", skip(conductor_response))] +#[trace(name = "transform_response")] fn transform_res(conductor_response: ConductorHttpResponse) -> HttpResponse { let mut response = HttpResponse::build(conductor_response.status); @@ -117,18 +111,14 @@ fn transform_res(conductor_response: ConductorHttpResponse) -> HttpResponse { response.body(conductor_response.body) } -#[tracing::instrument( - level = "debug", - skip(req, body, route_data), - name = "conductor_bin::handler" -)] async fn handler( req: HttpRequest, body: Bytes, route_data: web::Data>, ) -> impl Responder { let conductor_request = transform_req(req, body); - let conductor_response = ConductorGateway::execute(conductor_request, &route_data).await; + let conductor_response: ConductorHttpResponse = + ConductorGateway::execute(conductor_request, &route_data).await; transform_res(conductor_response) } diff --git a/bin/conductor/src/minitrace_actix.rs b/bin/conductor/src/minitrace_actix.rs new file mode 100644 index 00000000..23409788 --- /dev/null +++ b/bin/conductor/src/minitrace_actix.rs @@ -0,0 +1,221 @@ +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + http::{header::USER_AGENT, StatusCode, Version}, + web, Error, ResponseError, +}; +use conductor_engine::gateway::ConductorGatewayRouteData; +use conductor_tracing::{minitrace_mgr::MinitraceManager, otel_attrs::*}; +use futures_util::future::LocalBoxFuture; +use minitrace::{ + collector::{SpanContext, SpanId}, + Span, +}; +use std::{ + future::{ready, Ready}, + sync::Arc, +}; +use ulid::Ulid; + +pub struct MinitraceTransform; + +impl MinitraceTransform { + pub fn new() -> Self { + MinitraceTransform + } +} + +impl Transform for MinitraceTransform +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type InitError = (); + type Transform = MinitraceMiddleware; + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ready(Ok(MinitraceMiddleware { service })) + } +} + +#[inline] +fn build_request_root_span(req: &ServiceRequest) -> Span { + let endpoint_data = req + .app_data::>>() + .expect("endpoint data not found, failed to setup tracing"); + + let span_name = format!("HTTP {} {}", req.method(), req.path()); + let mut properties: Vec<(&str, String)> = build_request_properties(req); + properties.push((CONDUCTOR_ENDPOINT, endpoint_data.endpoint.clone())); + + let span_context = SpanContext::new( + MinitraceManager::generate_trace_id(endpoint_data.tenant_id), + SpanId::default(), + ); + + Span::root(span_name, span_context).with_properties(|| properties) +} + +#[inline] +pub fn http_flavor(version: Version) -> String { + match version { + Version::HTTP_09 => "0.9".into(), + Version::HTTP_10 => "1.0".into(), + Version::HTTP_11 => "1.1".into(), + Version::HTTP_2 => "2.0".into(), + Version::HTTP_3 => "3.0".into(), + other => format!("{other:?}"), + } +} + +#[inline] +pub fn http_scheme(scheme: &str) -> String { + match scheme { + "http" => "http".into(), + "https" => "https".into(), + other => other.to_string(), + } +} + +fn gen_request_id() -> String { + Ulid::new().to_string() +} + +#[inline] +fn build_request_properties(req: &ServiceRequest) -> Vec<(&'static str, String)> { + let headers = req.headers(); + let user_agent = headers + .get(USER_AGENT) + .map(|h| h.to_str().unwrap_or("")) + .unwrap_or(""); + let http_route: std::borrow::Cow<'static, str> = req + .match_pattern() + .map(Into::into) + .unwrap_or_else(|| "default".into()); + let http_method = req.method().to_string(); + let connection_info = req.connection_info(); + let request_id = headers + .get("x-request-id") + .map(|v| v.to_str().unwrap().to_string()) + .unwrap_or_else(gen_request_id); + + vec![ + (HTTP_METHOD, http_method), + (HTTP_ROUTE, http_route.into_owned()), + (HTTP_FLAVOR, http_flavor(req.version())), + (HTTP_SCHEME, http_scheme(connection_info.scheme())), + (HTTP_HOST, connection_info.host().to_string()), + ( + HTTP_CLIENT_IP, + connection_info + .realip_remote_addr() + .unwrap_or("") + .to_string(), + ), + (HTTP_USER_AGENT, user_agent.to_string()), + ( + HTTP_TARGET, + req + .uri() + .path_and_query() + .map(|p| p.as_str()) + .unwrap_or("") + .to_string(), + ), + (OTEL_KIND, "server".to_string()), + (REQUEST_ID, request_id), + // Specific to DataDog + (SPAN_TYPE, "web".to_string()), + ] +} + +#[inline] +fn handle_error( + status_code: StatusCode, + response_error: &dyn ResponseError, +) -> Vec<(&'static str, String)> { + let mut properties: Vec<(&'static str, String)> = vec![]; + + // pre-formatting errors is a workaround for https://github.com/tokio-rs/tracing/issues/1565 + let display = format!("{response_error}"); + let debug = format!("{response_error:?}"); + properties.push((EXCEPTION_MESSAGE, display)); + properties.push((EXCEPTION_DETAILS, debug)); + properties.push((ERROR_INDICATOR, "true".to_string())); + + let code = status_code.as_u16().to_string(); + properties.push((HTTP_STATUS_CODE, code)); + + if status_code.is_client_error() { + properties.push((OTEL_STATUS_CODE, "OK".to_string())); + } else { + properties.push((OTEL_STATUS_CODE, "ERROR".to_string())); + } + + properties +} + +#[inline] +fn build_response_properties( + res: &Result, actix_web::Error>, +) -> Vec<(&'static str, String)> { + let mut properties: Vec<(&'static str, String)> = vec![]; + + match res { + Ok(response) => { + if let Some(error) = response.response().error() { + properties.append(&mut handle_error( + response.status(), + error.as_response_error(), + )); + } else { + let code = response.response().status().as_u16().to_string(); + properties.push((HTTP_STATUS_CODE, code)); + properties.push((OTEL_STATUS_CODE, "OK".to_string())); + } + } + Err(error) => { + let response_error = error.as_response_error(); + properties.append(&mut handle_error( + response_error.status_code(), + error.as_response_error(), + )); + } + }; + + properties +} + +pub struct MinitraceMiddleware { + service: S, +} + +impl Service for MinitraceMiddleware +where + S: Service, Error = Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let root_span = build_request_root_span(&req); + let fut = self.service.call(req); + + Box::pin(async move { + let _guard = root_span.set_local_parent(); + let res = fut.await; + let res_properties = build_response_properties(&res); + let _ = root_span.with_properties(|| res_properties); + + res + }) + } +} diff --git a/libs/benches/Cargo.toml b/libs/benches/Cargo.toml index 77063b68..034557ea 100644 --- a/libs/benches/Cargo.toml +++ b/libs/benches/Cargo.toml @@ -9,6 +9,7 @@ conductor = { path = "../../bin/conductor" } conductor_engine = { path = "../../libs/engine" } conductor_config = { path = "../../libs/config" } conductor_common = { path = "../../libs/common" } +conductor_tracing = { path = "../../libs/tracing" } futures = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } diff --git a/libs/benches/bench.rs b/libs/benches/bench.rs index cc13859c..fe49605d 100644 --- a/libs/benches/bench.rs +++ b/libs/benches/bench.rs @@ -92,7 +92,8 @@ fn criterion_benchmark(c: &mut Criterion) { plugins: None, }; - let gw_future = ConductorGateway::new(&config); + let t = &mut None; + let gw_future = ConductorGateway::new(&config, t); let rt = Runtime::new().unwrap(); let gw = rt.block_on(gw_future).unwrap(); let route_data = gw diff --git a/libs/benches/config.yaml b/libs/benches/config.yaml index c0d3557e..172d6a21 100644 --- a/libs/benches/config.yaml +++ b/libs/benches/config.yaml @@ -2,8 +2,7 @@ server: port: 9000 logger: - level: error - format: pretty + filter: error sources: - id: countries diff --git a/libs/common/Cargo.toml b/libs/common/Cargo.toml index d587a2e1..e34b26aa 100644 --- a/libs/common/Cargo.toml +++ b/libs/common/Cargo.toml @@ -23,8 +23,10 @@ anyhow = { workspace = true } thiserror = { workspace = true } async-trait = { workspace = true } reqwest = { workspace = true } +reqwest-middleware = { workspace = true } graphql-parser = "0.4.0" mime = "0.3.17" url = "2.5.0" querystring = "1.1.0" once_cell = "1.19.0" +minitrace = { workspace = true } diff --git a/libs/common/src/graphql.rs b/libs/common/src/graphql.rs index e9ca5610..c457e232 100644 --- a/libs/common/src/graphql.rs +++ b/libs/common/src/graphql.rs @@ -6,6 +6,7 @@ use graphql_parser::{ query::{Definition, Document, OperationDefinition, ParseError}, }; use mime::{Mime, APPLICATION_JSON}; +use minitrace::trace; use once_cell::sync::Lazy; use serde::{Deserialize, Serialize}; use serde_json::{Error as SerdeError, Map, Value}; @@ -192,6 +193,12 @@ pub struct GraphQLError { pub extensions: Option>, } +impl std::fmt::Display for GraphQLError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + impl GraphQLError { pub fn new(message: &str) -> Self { GraphQLError { @@ -210,10 +217,7 @@ pub struct ParsedGraphQLRequest { } impl ParsedGraphQLRequest { - #[tracing::instrument( - level = "debug", - name = "ParsedGraphQLRequest::parse_graphql_operation" - )] + #[trace(name = "graphql_parse")] pub fn create_and_parse(raw_request: GraphQLRequest) -> Result { parse_graphql_operation(&raw_request.operation).map(|parsed_operation| ParsedGraphQLRequest { request: raw_request, @@ -287,7 +291,11 @@ impl ParsedGraphQLRequest { false } - #[tracing::instrument(level = "trace", name = "ParsedGraphQLRequest::is_running_mutation")] + #[tracing::instrument( + level = "trace", + name = "ParsedGraphQLRequest::is_running_mutation", + skip_all + )] pub fn is_running_mutation(&self) -> bool { if let Some(operation_name) = &self.request.operation_name { for definition in &self.parsed_operation.definitions { @@ -319,6 +327,9 @@ pub struct GraphQLResponse { pub errors: Option>, #[serde(skip_serializing_if = "Option::is_none")] pub extensions: Option, + + #[serde(skip)] + downstream_http_code: Option, } impl GraphQLResponse { @@ -337,6 +348,16 @@ impl GraphQLResponse { data: None, errors: Some(vec![GraphQLError::new(error)]), extensions: None, + downstream_http_code: None, + } + } + + pub fn new_error_with_code(error: &str, status_code: StatusCode) -> Self { + GraphQLResponse { + data: None, + errors: Some(vec![GraphQLError::new(error)]), + extensions: None, + downstream_http_code: Some(status_code), } } @@ -363,9 +384,11 @@ impl From for Bytes { impl From for ConductorHttpResponse { fn from(response: GraphQLResponse) -> Self { + let status = response.downstream_http_code.unwrap_or(StatusCode::OK); + ConductorHttpResponse { body: response.into(), - status: StatusCode::OK, + status, headers: Default::default(), } } diff --git a/libs/common/src/lib.rs b/libs/common/src/lib.rs index 10e7653c..e52cac17 100644 --- a/libs/common/src/lib.rs +++ b/libs/common/src/lib.rs @@ -5,3 +5,4 @@ pub mod json; pub mod plugin; pub mod serde_utils; pub mod vrl_utils; +pub use graphql_parser::query::{Definition, Document, OperationDefinition, ParseError}; diff --git a/libs/common/src/plugin.rs b/libs/common/src/plugin.rs index 55c95551..d335e084 100644 --- a/libs/common/src/plugin.rs +++ b/libs/common/src/plugin.rs @@ -4,7 +4,7 @@ use crate::{ graphql::GraphQLRequest, http::{ConductorHttpRequest, ConductorHttpResponse}, }; -use reqwest::{Error, Response}; +use reqwest::Response; use crate::execute::RequestExecutionContext; @@ -12,13 +12,15 @@ use crate::execute::RequestExecutionContext; pub enum PluginError { #[error("Plugin init error: {source}")] InitError { source: anyhow::Error }, + #[error("Plugin \"{name}\" is not supported in the current runtime.")] + PluginNotSupportedInRuntime { name: String }, } #[async_trait::async_trait(?Send)] pub trait CreatablePlugin: Plugin { type Config; - async fn create(config: Self::Config) -> Result, PluginError>; + async fn create(config: Self::Config) -> Result, PluginError>; } #[async_trait::async_trait(?Send)] @@ -42,7 +44,7 @@ pub trait Plugin: Sync + Send + Debug { async fn on_upstream_http_response( &self, _ctx: &mut RequestExecutionContext, - _res: &Result, + _res: &Result, ) { } // Step 6: A final HTTP response send from Conductor to the client diff --git a/libs/config/Cargo.toml b/libs/config/Cargo.toml index 7c212957..7983a45a 100644 --- a/libs/config/Cargo.toml +++ b/libs/config/Cargo.toml @@ -11,13 +11,15 @@ name = "generate-config-schema" path = "src/generate-json-schema.rs" [dependencies] -schemars = { workspace = true } +schemars = { workspace = true, features = ["preserve_order"] } tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_yaml = "0.9.30" regex = "1.10.3" conductor_common = { path = "../common" } +conductor_tracing = { path = "../tracing" } +conductor_logger = { path = "../logger" } # Conductor plugins, referenced here because we are using it to compose the final config struct cors_plugin = { path = "../../plugins/cors" } vrl_plugin = { path = "../../plugins/vrl" } @@ -27,3 +29,4 @@ graphiql_plugin = { path = "../../plugins/graphiql" } http_get_plugin = { path = "../../plugins/http_get" } jwt_auth_plugin = { path = "../../plugins/jwt_auth" } humantime-serde = "1.1.1" +telemetry_plugin = { path = "../../plugins/telemetry" } diff --git a/libs/config/conductor.schema.json b/libs/config/conductor.schema.json index 860a776a..4667127f 100644 --- a/libs/config/conductor.schema.json +++ b/libs/config/conductor.schema.json @@ -8,39 +8,22 @@ "sources" ], "properties": { - "endpoints": { - "description": "List of GraphQL endpoints to be exposed by the gateway. Each endpoint is a GraphQL schema that is backed by one or more sources and can have a unique set of plugins applied to.\n\nFor additional information, please refer to the [Endpoints section](./endpoints).", - "type": "array", - "items": { - "$ref": "#/definitions/EndpointDefinition" - } - }, - "logger": { - "description": "Conductor logger configuration.", + "server": { + "description": "Configuration for the HTTP server.", "anyOf": [ { - "$ref": "#/definitions/LoggerConfig" + "$ref": "#/definitions/ServerConfig" }, { "type": "null" } ] }, - "plugins": { - "description": "List of global plugins to be applied to all endpoints. Global plugins are applied before endpoint-specific plugins.", - "type": [ - "array", - "null" - ], - "items": { - "$ref": "#/definitions/PluginDefinition" - } - }, - "server": { - "description": "Configuration for the HTTP server.", + "logger": { + "description": "Conductor logger configuration.", "anyOf": [ { - "$ref": "#/definitions/ServerConfig" + "$ref": "#/definitions/LoggerConfig" }, { "type": "null" @@ -53,166 +36,359 @@ "items": { "$ref": "#/definitions/SourceDefinition" } + }, + "endpoints": { + "description": "List of GraphQL endpoints to be exposed by the gateway. Each endpoint is a GraphQL schema that is backed by one or more sources and can have a unique set of plugins applied to.\n\nFor additional information, please refer to the [Endpoints section](./endpoints).", + "type": "array", + "items": { + "$ref": "#/definitions/EndpointDefinition" + } + }, + "plugins": { + "description": "List of global plugins to be applied to all endpoints. Global plugins are applied before endpoint-specific plugins.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/PluginDefinition" + } } }, "definitions": { - "CorsPluginConfig": { - "description": "The `cors` plugin enables [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) configuration for your GraphQL API.\n\nBy using this plugin, you can define rules for allowing cross-origin requests to your GraphQL server. This is essential for web applications that need to interact with your API from different domains.", - "examples": [ - { - "$metadata": { - "title": "Strict CORS", - "description": "This example demonstrates how to configure the CORS plugin with a strict list of methods, headers and origins." - }, - "type": "cors", - "enabled": true, - "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "GET, POST", - "allowed_origin": "https://example.com", - "allowed_headers": "Content-Type, Authorization", - "allow_private_network": false - } - }, - { - "$metadata": { - "title": "Permissive CORS", - "description": "This example demonstrates how to configure the CORS plugin with a permissive setup." - }, - "type": "cors", - "enabled": true, - "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "*", - "allowed_origin": "*", - "allowed_headers": "*", - "exposed_headers": "*", - "allow_private_network": true - } + "ServerConfig": { + "type": "object", + "properties": { + "port": { + "description": "The port to listen on, default to 9000", + "default": 9000, + "type": "integer", + "format": "uint16", + "minimum": 0.0 }, - { - "$metadata": { - "title": "Reflect Origin", - "description": "This example demonstrates how to configure the CORS plugin with a reflect Origin setup." - }, - "type": "cors", - "enabled": true, - "config": { - "max_age": 3600, - "allow_credentials": true, - "allowed_methods": "GET, POST", - "allowed_origin": "reflect", - "allowed_headers": "*", - "exposed_headers": "*", - "allow_private_network": false - } + "host": { + "description": "The host to listen on, default to 127.0.0.1", + "default": "127.0.0.1", + "type": "string" } - ], + } + }, + "LoggerConfig": { "type": "object", "properties": { - "allow_credentials": { - "description": "`Access-Control-Allow-Credentials`: Specifies whether to include credentials in the CORS headers. Credentials can include cookies, authorization headers, or TLS client certificates. Indicates whether the response to the request can be exposed when the credentials flag is true.", - "default": false, - "type": [ - "boolean", - "null" - ] + "filter": { + "description": "Environment filter configuration as a string. This allows extremely powerful control over Conductor's logging.\n\nThe `filter` can specify various directives to filter logs based on module paths, span names, and specific fields. These directives can also be combined using commas as a separator.\n\n**Basic Usage:**\n\n- `info` (logs all messages at info level and higher across all modules)\n\n- `error` (logs all messages at error level only, as it's the highest level of severity)\n\n**Module-Specific Logging:**\n\n- `conductor::gateway=debug` (logs all debug messages for the 'conductor::gateway' module)\n\n- `conductor::engine::source=trace` (logs all trace messages for the 'conductor::engine::source' module)\n\n**Combining Directives:**\n\n- `conductor::gateway=info,conductor::engine::source=trace` (sets info level for the gateway module and trace level for the engine's source module)\n\nThe syntax of directives is very flexible, allowing complex logging configurations.\n\nSee [tracing_subscriber::EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) for more information.", + "default": "info", + "type": "string" }, - "allow_private_network": { - "description": "`Access-Control-Allow-Private-Network`: Indicates whether requests from private networks are allowed when originating from public networks.", - "default": false, - "type": [ - "boolean", - "null" - ] + "format": { + "description": "Configured the logger format. See options below.\n\n- `pretty` format is human-readable, ideal for development and debugging.\n\n- `json` format is structured, suitable for production environments and log analysis tools.\n\nBy default, `pretty` is used in TTY environments, and `json` is used in non-TTY environments.", + "default": "pretty", + "$ref": "#/definitions/LoggerConfigFormat" }, - "allowed_headers": { - "description": "`Access-Control-Allow-Headers`: Lists the headers allowed in actual requests. This helps in specifying which headers can be used when making the actual request. Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. You can also specify a special value \"*\" to allow any headers to be used when making the actual request, and the `Access-Control-Request-Headers` will be used from the incoming request.", - "default": "*", - "type": [ - "string", - "null" + "print_performance_info": { + "description": "Emits performance information on in crucial areas of the gateway.\n\nLook for `close` and `idle` spans printed in the logs.\n\nNote: this option is not enabled on WASM runtime, and will be ignored if specified.", + "default": false, + "type": "boolean" + } + } + }, + "LoggerConfigFormat": { + "description": "A source definition for a GraphQL endpoint or a federated GraphQL implementation.", + "oneOf": [ + { + "title": "compact", + "description": "This logging format outputs minimal, compact logs. It focuses on the essential parts of the log message and its fields, making it suitable for production environments where performance and log size are crucial.\n\nPros:\n\n- Efficient in terms of space and performance.\n\n- Easy to read for brief messages and simple logs.\n\nCons:\n\n- May lack detailed context, making debugging a bit more challenging.", + "type": "string", + "enum": [ + "compact" ] }, - "allowed_methods": { - "description": "`Access-Control-Allow-Methods`: Defines the HTTP methods allowed when accessing the resource. This is used in response to a CORS preflight request. Specifies the method or methods allowed when accessing the resource in response to a preflight request. You can also specify a special value \"*\" to allow any HTTP method to access the resource.", - "default": "*", - "type": [ - "string", - "null" + { + "title": "pretty", + "description": "The pretty format is designed for enhanced readability, featuring more verbose output including well-formatted fields and context. Ideal for development and debugging purposes.\n\nPros:\n\n- Highly readable and provides detailed context.\n\n- Easier to understand complex log messages.\n\nCons:\n\n- More verbose, resulting in larger log sizes.\n\n- Potentially slower performance due to the additional formatting overhead.", + "type": "string", + "enum": [ + "pretty" ] }, - "allowed_origin": { - "description": "`Access-Control-Allow-Origin`: Determines which origins are allowed to access the resource. It can be a specific origin or a wildcard for allowing any origin. You can also specify a special value \"*\" to allow any origin to access the resource. You can also specify a special value \"reflect\" to allow the origin of the incoming request to access the resource.", - "default": "*", - "type": [ - "string", - "null" + { + "title": "json", + "description": "This format outputs logs in JSON. It is particularly useful when integrating with tools that consume or process JSON logs, such as log aggregators and analysis systems.\n\nPros:\n\n- Structured format makes it easy to parse and integrate with various tools.\n\n- Consistent and predictable output.\n\nCons:\n\n- Can be verbose and harder to read directly by developers.\n\n- Slightly more overhead compared to simpler formats like compact.", + "type": "string", + "enum": [ + "json" ] + } + ] + }, + "SourceDefinition": { + "description": "A source definition for a GraphQL endpoint or a federated GraphQL implementation.", + "oneOf": [ + { + "description": "A simple, single GraphQL endpoint", + "type": "object", + "required": [ + "config", + "id", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "graphql" + ] + }, + "id": { + "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", + "type": "string" + }, + "config": { + "description": "The configuration for the GraphQL source.", + "$ref": "#/definitions/GraphQLSourceConfig" + } + } }, - "exposed_headers": { - "description": "`Access-Control-Expose-Headers`: The \"Access-Control-Expose-Headers\" response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request. You can also specify a special value \"*\" to allow any headers to be exposed to scripts running in the browser.", - "default": "*", - "type": [ - "string", - "null" - ] + { + "description": "A simple, single GraphQL endpoint", + "type": "object", + "required": [ + "config", + "id", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "mocl" + ] + }, + "id": { + "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", + "type": "string" + }, + "config": { + "description": "The configuration for the GraphQL source.", + "$ref": "#/definitions/MockedSourceConfig" + } + } }, - "max_age": { - "description": "`Access-Control-Max-Age`: Indicates how long the results of a preflight request can be cached. This field represents the duration in seconds.", - "type": [ - "integer", - "null" + { + "description": "federation endpoint", + "type": "object", + "required": [ + "config", + "id", + "type" ], - "format": "uint64", - "minimum": 0.0 + "properties": { + "type": { + "type": "string", + "enum": [ + "federation" + ] + }, + "id": { + "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", + "type": "string" + }, + "config": { + "description": "The configuration for the GraphQL source.", + "$ref": "#/definitions/FederationSourceConfig" + } + } } - } + ] }, - "DisableIntrospectionPluginConfig": { - "description": "The `disable_introspection` plugin allows you to disable introspection for your GraphQL API.\n\nA [GraphQL introspection query](https://graphql.org/learn/introspection/) is a special GraphQL query that returns information about the GraphQL schema of your API.\n\nIt it [recommended to disable introspection for production environments](https://escape.tech/blog/should-i-disable-introspection-in-graphql/), unless you have a specific use-case for it.\n\nIt can either disable introspection for all requests, or only for requests that match a specific condition (using VRL scripting language).", + "GraphQLSourceConfig": { + "description": "An upstream based on a simple, single GraphQL endpoint.\n\nBy using this source, you can easily wrap an existing GraphQL upstream server, and enrich it with features and plugins.", "examples": [ { "$metadata": { - "title": "Disable Introspection", - "description": "This example disables introspection for all requests for the configured Endpoint." - }, - "type": "disable_introspection", - "enabled": true, - "config": {} - }, - { - "$metadata": { - "title": "Conditional", - "description": "This example disables introspection for all requests that doesn't have the \"bypass-introspection\" HTTP header." + "description": null, + "title": "Simple" }, - "type": "disable_introspection", - "enabled": true, "config": { - "condition": { - "from": "inline", - "content": "%downstream_http_req.headers.\"bypass-introspection\" != \"1\"" - } - } + "endpoint": "https://my-source.com/graphql" + }, + "id": "my-source", + "type": "graphql" } ], "type": "object", + "required": [ + "endpoint" + ], "properties": { - "condition": { - "description": "A VRL condition that determines whether to disable introspection for the request. This condition is evaluated only if the incoming GraphQL request is detected as an introspection query.\n\nThe condition is evaluated in the context of the incoming request and have access to the metadata field `%downstream_http_req` (fields: `body`, `uri`, `query_string`, `method`, `headers`).\n\nThe condition must return a boolean value: return `true` to continue and disable the introspection, and `false` to allow the introspection to run.\n\nIn case of a runtime error, or an unexpected return value, the script will be ignored and introspection will be disabled for the incoming request.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, - { - "type": "null" + "endpoint": { + "description": "The HTTP(S) endpoint URL for the GraphQL source.", + "type": "string" + } + } + }, + "MockedSourceConfig": { + "description": "A mocked upstream with a static response for all executed operations.", + "type": "object", + "required": [ + "response_data" + ], + "properties": { + "response_data": { + "$ref": "#/definitions/LocalFileReference" + } + } + }, + "LocalFileReference": { + "type": "string", + "format": "path" + }, + "FederationSourceConfig": { + "description": "A source capable of loading a Supergraph schema based on the [Apollo Federation specification](https://www.apollographql.com/docs/federation/).\n\nThe loaded supergraph will be used to orchestrate the execution of the queries across the federated sources.\n\nThe input for this source can be a local file, an environment variable, or a remote endpoint.\n\nThe content of the Supergraph input needs to be a valid GraphQL SDL schema, with the Apollo Federation execution directives, usually produced by a schema registry.", + "examples": [ + { + "$metadata": { + "description": "This example is loading a Supergraph schema from a remote endpoint, using the Hive CDN. ", + "title": "Hive" + }, + "config": { + "expose_query_plan": false, + "supergraph": { + "remote": { + "fetch_every": "10s", + "headers": { + "X-Hive-CDN-Key": "CDN_TOKEN" + }, + "url": "https://cdn.graphql-hive.com/artifacts/v1/TARGET_ID/supergraph" + } } - ] + }, + "id": "my-source", + "type": "federation" + }, + { + "$metadata": { + "description": null, + "title": "From a file" + }, + "config": { + "expose_query_plan": false, + "supergraph": { + "file": "./supergraph.graphql" + } + }, + "id": "my-source", + "type": "federation" + }, + { + "$metadata": { + "description": null, + "title": "From Env Var" + }, + "config": { + "expose_query_plan": false, + "supergraph": { + "env": "SUPERGRAPH" + } + }, + "id": "my-source", + "type": "federation" + } + ], + "type": "object", + "required": [ + "supergraph" + ], + "properties": { + "supergraph": { + "description": "The endpoint URL for the GraphQL source.", + "$ref": "#/definitions/SupergraphSourceConfig" + }, + "expose_query_plan": { + "description": "Exposes the query plan as JSON under \"extensions\"", + "default": false, + "type": "boolean" } } }, + "SupergraphSourceConfig": { + "oneOf": [ + { + "title": "file", + "description": "The file path for the Supergraph schema.\n\n> This provider is not supported on WASM runtime.", + "type": "object", + "required": [ + "file" + ], + "properties": { + "file": { + "$ref": "#/definitions/LocalFileReference" + } + }, + "additionalProperties": false + }, + { + "title": "env", + "description": "The environment variable that contains the Supergraph schema.", + "type": "object", + "required": [ + "env" + ], + "properties": { + "env": { + "type": "string" + } + }, + "additionalProperties": false + }, + { + "title": "remote", + "description": "The remote endpoint where the Supergraph schema can be fetched.", + "type": "object", + "required": [ + "remote" + ], + "properties": { + "remote": { + "type": "object", + "required": [ + "url" + ], + "properties": { + "url": { + "description": "The URL endpoint from where to fetch the Supergraph schema.", + "type": "string" + }, + "headers": { + "description": "Optional headers to include in the request (ex: for authentication)", + "type": [ + "object", + "null" + ], + "additionalProperties": { + "type": "string" + } + }, + "fetch_every": { + "description": "Polling interval for fetching the Supergraph schema from the remote.", + "default": "1m", + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + } + } + } + }, + "additionalProperties": false + } + ] + }, "Duration": { "type": "object", "required": [ @@ -220,14 +396,14 @@ "secs" ], "properties": { - "nanos": { + "secs": { "type": "integer", - "format": "uint32", + "format": "uint64", "minimum": 0.0 }, - "secs": { + "nanos": { "type": "integer", - "format": "uint64", + "format": "uint32", "minimum": 0.0 } } @@ -237,80 +413,80 @@ "examples": [ { "$metadata": { - "title": "Basic Example", - "description": "This example demonstrate how to declare a GraphQL source, and expose it as a GraphQL endpoint. The endpoint also exposes a GraphiQL interface." + "description": "This example demonstrate how to declare a GraphQL source, and expose it as a GraphQL endpoint. The endpoint also exposes a GraphiQL interface.", + "title": "Basic Example" }, - "sources": [ - { - "type": "graphql", - "id": "my-source", - "config": { - "endpoint": "https://my-source.com/graphql" - } - } - ], "endpoints": [ { - "path": "/graphql", "from": "my-source", + "path": "/graphql", "plugins": [ { "type": "graphiql" } ] } - ] - }, - { - "$metadata": { - "title": "Multiple Endpoints", - "description": "This example shows how to expose a single GraphQL source with different plugins applied to it. In this example, we expose the same, one time with persised operations, and one time with HTTP GET for arbitrary queries." - }, + ], "sources": [ { - "type": "graphql", - "id": "my-source", "config": { "endpoint": "https://my-source.com/graphql" - } + }, + "id": "my-source", + "type": "graphql" } - ], + ] + }, + { + "$metadata": { + "description": "This example shows how to expose a single GraphQL source with different plugins applied to it. In this example, we expose the same, one time with persised operations, and one time with HTTP GET for arbitrary queries.", + "title": "Multiple Endpoints" + }, "endpoints": [ { - "path": "/trusted", "from": "my-source", + "path": "/trusted", "plugins": [ { - "type": "trusted_documents", "config": { - "store": { - "source": "file", - "path": "store.json", - "format": "json_key_value" - }, + "allow_untrusted": false, "protocols": [ { - "type": "document_id", - "field_name": "" + "field_name": "", + "type": "document_id" } ], - "allow_untrusted": false - } + "store": { + "format": "json_key_value", + "path": "store.json", + "source": "file" + } + }, + "type": "trusted_documents" } ] }, { - "path": "/data", "from": "my-source", + "path": "/data", "plugins": [ { - "type": "http_get", "config": { "mutations": false - } + }, + "type": "http_get" } ] } + ], + "sources": [ + { + "config": { + "endpoint": "https://my-source.com/graphql" + }, + "id": "my-source", + "type": "graphql" + } ] } ], @@ -320,14 +496,14 @@ "path" ], "properties": { - "from": { - "description": "The identifier of the `Source` to be used.\n\nThis must match the `id` field of a `Source` definition.", - "type": "string" - }, "path": { "description": "A valid HTTP path to listen on for this endpoint. This will be used for the main GraphQL endpoint as well as for the GraphiQL endpoint. In addition, plugins that extends the HTTP layer will use this path as a base path.", "type": "string" }, + "from": { + "description": "The identifier of the `Source` to be used.\n\nThis must match the `id` field of a `Source` definition.", + "type": "string" + }, "plugins": { "description": "A list of unique plugins to be applied to this endpoint. These plugins will be applied after the global plugins.\n\nOrder of plugins is important: plugins are applied in the order they are defined.", "type": [ @@ -340,894 +516,852 @@ } } }, - "FederationSourceConfig": { - "description": "A source capable of loading a Supergraph schema based on the [Apollo Federation specification](https://www.apollographql.com/docs/federation/).\n\nThe loaded supergraph will be used to orchestrate the execution of the queries across the federated sources.\n\nThe input for this source can be a local file, an environment variable, or a remote endpoint.\n\nThe content of the Supergraph input needs to be a valid GraphQL SDL schema, with the Apollo Federation execution directives, usually produced by a schema registry.", - "examples": [ - { - "$metadata": { - "title": "Hive", - "description": "This example is loading a Supergraph schema from a remote endpoint, using the Hive CDN. " - }, - "type": "federation", - "id": "my-source", - "config": { - "supergraph": { - "remote": { - "url": "https://cdn.graphql-hive.com/artifacts/v1/TARGET_ID/supergraph", - "headers": { - "X-Hive-CDN-Key": "CDN_TOKEN" - }, - "fetch_every": "10s" - } - }, - "expose_query_plan": false - } - }, + "PluginDefinition": { + "oneOf": [ { - "$metadata": { - "title": "From a file", - "description": null - }, - "type": "federation", - "id": "my-source", - "config": { - "supergraph": { - "file": "./supergraph.graphql" + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "graphiql" + ] }, - "expose_query_plan": false - } - }, - { - "$metadata": { - "title": "From Env Var", - "description": null - }, - "type": "federation", - "id": "my-source", - "config": { - "supergraph": { - "env": "SUPERGRAPH" + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] }, - "expose_query_plan": false + "config": { + "anyOf": [ + { + "$ref": "#/definitions/GraphiQLPluginConfig" + }, + { + "type": "null" + } + ] + } } - } - ], - "type": "object", - "required": [ - "supergraph" - ], - "properties": { - "expose_query_plan": { - "description": "Exposes the query plan as JSON under \"extensions\"", - "default": false, - "type": "boolean" }, - "supergraph": { - "description": "The endpoint URL for the GraphQL source.", - "$ref": "#/definitions/SupergraphSourceConfig" - } - } - }, - "GraphQLSourceConfig": { - "description": "An upstream based on a simple, single GraphQL endpoint.\n\nBy using this source, you can easily wrap an existing GraphQL upstream server, and enrich it with features and plugins.", - "examples": [ { - "$metadata": { - "title": "Simple", - "description": null - }, - "type": "graphql", - "id": "my-source", - "config": { - "endpoint": "https://my-source.com/graphql" + "type": "object", + "required": [ + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "cors" + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { + "anyOf": [ + { + "$ref": "#/definitions/CorsPluginConfig" + }, + { + "type": "null" + } + ] + } } - } - ], - "type": "object", - "required": [ - "endpoint" - ], - "properties": { - "endpoint": { - "description": "The HTTP(S) endpoint URL for the GraphQL source.", - "type": "string" - } - } - }, - "GraphiQLPluginConfig": { - "description": "This plugin adds a GraphiQL interface to your Endpoint.\n\nThis plugin is rendering the GraphiQL interface for HTTP `GET` requests, that are not intercepted by other plugins.", - "examples": [ - { - "$metadata": { - "title": "Enable GraphiQL", - "description": null - }, - "type": "graphiql", - "enabled": true, - "config": {} - } - ], - "type": "object", - "properties": { - "headers_editor_enabled": { - "description": "Enable/disable the HTTP headers editor in the GraphiQL interface.", - "default": true, - "type": [ - "boolean", - "null" - ] - } - } - }, - "HttpGetPluginConfig": { - "description": "The `http_get` plugin allows you to expose your GraphQL API over HTTP `GET` requests. This feature is fully compliant with the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/).\n\nBy enabling this plugin, you can execute GraphQL queries and mutations over HTTP `GET` requests, using HTTP query parameters, for example:\n\n`GET /graphql?query=query%20%7B%20__typename%20%7D`\n\n### Query Parameters\n\nFor complete documentation of the supported query parameters, see the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/#sec-GET).\n\n- `query`: The GraphQL query to execute\n\n- `variables` (optional): A JSON-encoded string containing the GraphQL variables\n\n- `operationName` (optional): The name of the GraphQL operation to execute\n\n### Headers\n\nTo execute GraphQL queries over HTTP `GET` requests, you must set the `Content-Type` header to `application/json`, **or** the `Accept` header to `application/x-www-form-urlencoded` / `application/graphql-response+json`.", - "examples": [ - { - "$metadata": { - "title": "Simple", - "description": null - }, - "type": "http_get", - "enabled": true, - "config": {} }, { - "$metadata": { - "title": "Enable Mutations", - "description": "This example enables mutations over HTTP GET requests." - }, - "type": "http_get", - "enabled": true, - "config": { - "mutations": true - } - } - ], - "type": "object", - "properties": { - "mutations": { - "description": "Allow mutations over GET requests.\n\n**The option is disabled by default:** this restriction is necessary to conform with the long-established semantics of safe methods within HTTP.", - "default": false, - "type": [ - "boolean", - "null" - ] - } - } - }, - "JwksProviderSourceConfig": { - "oneOf": [ - { - "title": "local", - "description": "A local file on the file-system. This file will be read once on startup and cached.", + "description": "Configuration for the Disable Introspection plugin.", "type": "object", "required": [ - "path", - "source" + "type" ], "properties": { - "path": { - "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", - "$ref": "#/definitions/LocalFileReference" - }, - "source": { + "type": { "type": "string", "enum": [ - "local" + "disable_introspection" + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { + "anyOf": [ + { + "$ref": "#/definitions/DisableIntrospectionPluginConfig" + }, + { + "type": "null" + } ] } } }, { - "title": "remote", - "description": "A remote JWKS provider. The JWKS will be fetched via HTTP/HTTPS and cached.", "type": "object", "required": [ - "source", - "url" + "type" ], "properties": { - "cache_duration": { - "description": "Duration after which the cached JWKS should be expired. If not specified, the default value will be used.", - "default": "10m", + "type": { + "type": "string", + "enum": [ + "http_get" + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { "anyOf": [ { - "$ref": "#/definitions/Duration" + "$ref": "#/definitions/HttpGetPluginConfig" }, { "type": "null" } ] + } + } + }, + { + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "vrl" + ] }, - "prefetch": { - "description": "If set to `true`, the JWKS will be fetched on startup and cached. In case of invalid JWKS, the error will be ignored and the plugin will try to fetch again when server receives the first request. If set to `false`, the JWKS will be fetched on-demand, when the first request comes in.", + "enabled": { + "default": true, "type": [ "boolean", "null" ] }, - "source": { + "config": { + "$ref": "#/definitions/VrlPluginConfig" + } + } + }, + { + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "type": { "type": "string", "enum": [ - "remote" + "trusted_documents" ] }, - "url": { - "description": "The URL to fetch the JWKS key set from, via HTTP/HTTPS.", - "type": "string" + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { + "$ref": "#/definitions/TrustedDocumentsPluginConfig" } } - } - ] - }, - "JwtAuthPluginConfig": { - "description": "The `jwt_auth` plugin implements the [JSON Web Tokens](https://jwt.io/introduction) specification.\n\nIt can be used to verify the JWT signature, and optionally validate the token issuer and audience. It can also forward the token and its claims to the upstream service.\n\nThe JWKS configuration can be either a local file on the file-system, or a remote JWKS provider.\n\nBy default, the plugin will look for the JWT token in the `Authorization` header, with the `Bearer` prefix.\n\nYou can also configure the plugin to reject requests that don't have a valid JWT token.", - "examples": [ + }, { - "$metadata": { - "title": "Local JWKS", - "description": "This example is loading a JWKS file from the local file-system. The token is looked up in the `Authorization` header." - }, - "type": "jwt_auth", - "enabled": true, - "config": { - "lookup_locations": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "jwks_providers": [ - { - "source": "local", - "path": "jwks.json" - } - ] + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "jwt_auth" + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { + "$ref": "#/definitions/JwtAuthPluginConfig" + } } }, + { + "type": "object", + "required": [ + "config", + "type" + ], + "properties": { + "type": { + "type": "string", + "enum": [ + "telemetry" + ] + }, + "enabled": { + "default": true, + "type": [ + "boolean", + "null" + ] + }, + "config": { + "$ref": "#/definitions/TelemetryPluginConfig" + } + } + } + ] + }, + "GraphiQLPluginConfig": { + "description": "This plugin adds a GraphiQL interface to your Endpoint.\n\nThis plugin is rendering the GraphiQL interface for HTTP `GET` requests, that are not intercepted by other plugins.", + "examples": [ { "$metadata": { - "title": "Remote JWKS with prefetch", - "description": "This example is loading a remote JWKS, when the server starts (prefetch). The token is looked up in the `Authorization` header." + "description": null, + "title": "Enable GraphiQL" }, - "type": "jwt_auth", + "config": {}, "enabled": true, - "config": { - "lookup_locations": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ] - } - }, + "type": "graphiql" + } + ], + "type": "object", + "properties": { + "headers_editor_enabled": { + "description": "Enable/disable the HTTP headers editor in the GraphiQL interface.", + "default": true, + "type": [ + "boolean", + "null" + ] + } + } + }, + "CorsPluginConfig": { + "description": "The `cors` plugin enables [Cross-Origin Resource Sharing (CORS)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS) configuration for your GraphQL API.\n\nBy using this plugin, you can define rules for allowing cross-origin requests to your GraphQL server. This is essential for web applications that need to interact with your API from different domains.", + "examples": [ { "$metadata": { - "title": "Reject Unauthenticated", - "description": "This example is loading a remote JWKS, and looks for the token in the `auth` cookie. If the token is not present, the request will be rejected." + "description": "This example demonstrates how to configure the CORS plugin with a strict list of methods, headers and origins.", + "title": "Strict CORS" }, - "type": "jwt_auth", - "enabled": true, "config": { - "reject_unauthenticated_requests": true, - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ], - "lookup_locations": [ - { - "source": "cookies", - "name": "auth" - } - ] - } + "allow_credentials": true, + "allow_private_network": false, + "allowed_headers": "Content-Type, Authorization", + "allowed_methods": "GET, POST", + "allowed_origin": "https://example.com", + "max_age": 3600 + }, + "enabled": true, + "type": "cors" }, { "$metadata": { - "title": "Claims Forwarding", - "description": "This example is loading a remote JWKS, and looks for the token in the `jwt` cookie. If the token is not present, the request will be rejected. The token and its claims will be forwarded to the upstream service in the `X-Auth-Token` and `X-Auth-Claims` headers." + "description": "This example demonstrates how to configure the CORS plugin with a permissive setup.", + "title": "Permissive CORS" }, - "type": "jwt_auth", - "enabled": true, "config": { - "forward_claims_to_upstream_header": "X-Auth-Claims", - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": true - } - ], - "lookup_locations": [ - { - "source": "cookies", - "name": "jwt" - } - ], - "reject_unauthenticated_requests": true, - "forward_token_to_upstream_header": "X-Auth-Token" - } + "allow_credentials": true, + "allow_private_network": true, + "allowed_headers": "*", + "allowed_methods": "*", + "allowed_origin": "*", + "exposed_headers": "*", + "max_age": 3600 + }, + "enabled": true, + "type": "cors" }, { "$metadata": { - "title": "Strict Validation", - "description": "This example is using strict validation, where the token issuer and audience are checked." + "description": "This example demonstrates how to configure the CORS plugin with a reflect Origin setup.", + "title": "Reflect Origin" }, - "type": "jwt_auth", - "enabled": true, "config": { - "lookup_locations": [ - { - "source": "cookies", - "name": "jwt" - } - ], - "jwks_providers": [ - { - "source": "remote", - "url": "https://example.com/jwks.json", - "cache_duration": "10m", - "prefetch": null - } - ], - "issuers": [ - "https://example.com" - ], - "audiences": [ - "realm.myapp.com" - ] - } + "allow_credentials": true, + "allow_private_network": false, + "allowed_headers": "*", + "allowed_methods": "GET, POST", + "allowed_origin": "reflect", + "exposed_headers": "*", + "max_age": 3600 + }, + "enabled": true, + "type": "cors" } ], "type": "object", - "required": [ - "jwks_providers" - ], "properties": { - "allowed_algorithms": { - "description": "List of allowed algorithms for verifying the JWT signature. If not specified, the default list of all supported algorithms in [`jsonwebtoken` crate](https://crates.io/crates/jsonwebtoken) are used.", - "default": [ - "HS256", - "HS384", - "HS512", - "RS256", - "RS384", - "RS512", - "ES256", - "ES384", - "PS256", - "PS384", - "PS512", - "EdDSA" - ], + "allow_credentials": { + "description": "`Access-Control-Allow-Credentials`: Specifies whether to include credentials in the CORS headers. Credentials can include cookies, authorization headers, or TLS client certificates. Indicates whether the response to the request can be exposed when the credentials flag is true.", + "default": false, "type": [ - "array", + "boolean", "null" - ], - "items": { - "type": "string" - } + ] }, - "audiences": { - "description": "The list of [JWT audiences](https://tools.ietf.org/html/rfc7519#section-4.1.3) are allowed to access. If this field is set, the token's `aud` field must be one of the values in this list, otherwise the token's `aud` field is not checked.", + "allowed_methods": { + "description": "`Access-Control-Allow-Methods`: Defines the HTTP methods allowed when accessing the resource. This is used in response to a CORS preflight request. Specifies the method or methods allowed when accessing the resource in response to a preflight request. You can also specify a special value \"*\" to allow any HTTP method to access the resource.", + "default": "*", "type": [ - "array", + "string", "null" - ], - "items": { - "type": "string" - } + ] }, - "forward_claims_to_upstream_header": { - "description": "Forward the JWT claims to the upstream service in the specified header.", + "allowed_origin": { + "description": "`Access-Control-Allow-Origin`: Determines which origins are allowed to access the resource. It can be a specific origin or a wildcard for allowing any origin. You can also specify a special value \"*\" to allow any origin to access the resource. You can also specify a special value \"reflect\" to allow the origin of the incoming request to access the resource.", + "default": "*", "type": [ "string", "null" ] }, - "forward_token_to_upstream_header": { - "description": "Forward the JWT token to the upstream service in the specified header.", + "allowed_headers": { + "description": "`Access-Control-Allow-Headers`: Lists the headers allowed in actual requests. This helps in specifying which headers can be used when making the actual request. Used in response to a preflight request to indicate which HTTP headers can be used when making the actual request. You can also specify a special value \"*\" to allow any headers to be used when making the actual request, and the `Access-Control-Request-Headers` will be used from the incoming request.", + "default": "*", "type": [ "string", "null" ] }, - "issuers": { - "description": "Specify the [principal](https://tools.ietf.org/html/rfc7519#section-4.1.1) that issued the JWT, usually a URL or an email address. If specified, it has to match the `iss` field in JWT, otherwise the token's `iss` field is not checked.", + "exposed_headers": { + "description": "`Access-Control-Expose-Headers`: The \"Access-Control-Expose-Headers\" response header allows a server to indicate which response headers should be made available to scripts running in the browser, in response to a cross-origin request. You can also specify a special value \"*\" to allow any headers to be exposed to scripts running in the browser.", + "default": "*", "type": [ - "array", + "string", "null" - ], - "items": { - "type": "string" - } - }, - "jwks_providers": { - "description": "A list of JWKS providers to use for verifying the JWT signature. Can be either a path to a local JSON of the file-system, or a URL to a remote JWKS provider.", - "type": "array", - "items": { - "$ref": "#/definitions/JwksProviderSourceConfig" - } - }, - "lookup_locations": { - "description": "A list of locations to look up for the JWT token in the incoming HTTP request. The first one that is found will be used.", - "default": [ - { - "source": "header", - "name": "Authorization", - "prefix": "Bearer" - } - ], - "type": "array", - "items": { - "$ref": "#/definitions/JwtAuthPluginLookupLocation" - } + ] }, - "reject_unauthenticated_requests": { - "description": "If set to `true`, the entire request will be rejected if the JWT token is not present in the request.", + "allow_private_network": { + "description": "`Access-Control-Allow-Private-Network`: Indicates whether requests from private networks are allowed when originating from public networks.", + "default": false, "type": [ "boolean", "null" ] + }, + "max_age": { + "description": "`Access-Control-Max-Age`: Indicates how long the results of a preflight request can be cached. This field represents the duration in seconds.", + "type": [ + "integer", + "null" + ], + "format": "uint64", + "minimum": 0.0 } } }, - "JwtAuthPluginLookupLocation": { - "oneOf": [ + "DisableIntrospectionPluginConfig": { + "description": "The `disable_introspection` plugin allows you to disable introspection for your GraphQL API.\n\nA [GraphQL introspection query](https://graphql.org/learn/introspection/) is a special GraphQL query that returns information about the GraphQL schema of your API.\n\nIt it [recommended to disable introspection for production environments](https://escape.tech/blog/should-i-disable-introspection-in-graphql/), unless you have a specific use-case for it.\n\nIt can either disable introspection for all requests, or only for requests that match a specific condition (using VRL scripting language).", + "examples": [ { - "title": "header", - "type": "object", - "required": [ - "name", - "source" - ], - "properties": { - "name": { - "type": "string" - }, - "prefix": { - "type": [ - "string", - "null" - ] + "$metadata": { + "description": "This example disables introspection for all requests for the configured Endpoint.", + "title": "Disable Introspection" + }, + "config": {}, + "enabled": true, + "type": "disable_introspection" + }, + { + "$metadata": { + "description": "This example disables introspection for all requests that doesn't have the \"bypass-introspection\" HTTP header.", + "title": "Conditional" + }, + "config": { + "condition": { + "content": "%downstream_http_req.headers.\"bypass-introspection\" != \"1\"", + "from": "inline" + } + }, + "enabled": true, + "type": "disable_introspection" + } + ], + "type": "object", + "properties": { + "condition": { + "description": "A VRL condition that determines whether to disable introspection for the request. This condition is evaluated only if the incoming GraphQL request is detected as an introspection query.\n\nThe condition is evaluated in the context of the incoming request and have access to the metadata field `%downstream_http_req` (fields: `body`, `uri`, `query_string`, `method`, `headers`).\n\nThe condition must return a boolean value: return `true` to continue and disable the introspection, and `false` to allow the introspection to run.\n\nIn case of a runtime error, or an unexpected return value, the script will be ignored and introspection will be disabled for the incoming request.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" }, - "source": { - "type": "string", - "enum": [ - "header" - ] + { + "type": "null" } - } - }, + ] + } + } + }, + "VrlConfigReference": { + "oneOf": [ { - "title": "query_param", + "title": "inline", + "description": "Inline string for a VRL code snippet. The string is parsed and executed as a VRL plugin.", "type": "object", "required": [ - "name", - "source" + "content", + "from" ], "properties": { - "name": { - "type": "string" - }, - "source": { + "from": { "type": "string", "enum": [ - "query_param" + "inline" ] + }, + "content": { + "type": "string" } } }, { - "title": "cookies", + "title": "file", + "description": "File reference to a VRL file. The file is loaded and executed as a VRL plugin.", "type": "object", "required": [ - "name", - "source" + "from", + "path" ], "properties": { - "name": { - "type": "string" - }, - "source": { + "from": { "type": "string", "enum": [ - "cookies" + "file" ] + }, + "path": { + "$ref": "#/definitions/LocalFileReference" } } } ] }, - "Level": { - "type": "string", - "enum": [ - "trace", - "debug", - "info", - "warn", - "error" - ] - }, - "LocalFileReference": { - "type": "string", - "format": "path" - }, - "LoggerConfig": { + "HttpGetPluginConfig": { + "description": "The `http_get` plugin allows you to expose your GraphQL API over HTTP `GET` requests. This feature is fully compliant with the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/).\n\nBy enabling this plugin, you can execute GraphQL queries and mutations over HTTP `GET` requests, using HTTP query parameters, for example:\n\n`GET /graphql?query=query%20%7B%20__typename%20%7D`\n\n### Query Parameters\n\nFor complete documentation of the supported query parameters, see the [GraphQL over HTTP specification](https://graphql.github.io/graphql-over-http/draft/#sec-GET).\n\n- `query`: The GraphQL query to execute\n\n- `variables` (optional): A JSON-encoded string containing the GraphQL variables\n\n- `operationName` (optional): The name of the GraphQL operation to execute\n\n### Headers\n\nTo execute GraphQL queries over HTTP `GET` requests, you must set the `Content-Type` header to `application/json`, **or** the `Accept` header to `application/x-www-form-urlencoded` / `application/graphql-response+json`.", + "examples": [ + { + "$metadata": { + "description": null, + "title": "Simple" + }, + "config": {}, + "enabled": true, + "type": "http_get" + }, + { + "$metadata": { + "description": "This example enables mutations over HTTP GET requests.", + "title": "Enable Mutations" + }, + "config": { + "mutations": true + }, + "enabled": true, + "type": "http_get" + } + ], "type": "object", "properties": { - "level": { - "description": "Log level", - "default": "info", - "$ref": "#/definitions/Level" + "mutations": { + "description": "Allow mutations over GET requests.\n\n**The option is disabled by default:** this restriction is necessary to conform with the long-established semantics of safe methods within HTTP.", + "default": false, + "type": [ + "boolean", + "null" + ] } } }, - "PluginDefinition": { - "oneOf": [ + "VrlPluginConfig": { + "description": "To simplify the process of extending the functionality of the GraphQL Gateway, we adopted a Rust-based script language called [VRL](https://vector.dev/docs/reference/vrl/).\n\nVRL language is intended for writing simple scripts that can be executed in the context of the GraphQL Gateway. VRL is focused around safety and performance: the script is compiled into Rust code when the server starts, and executed as a native Rust code ([you can find a comparison between VRL and other scripting languages here](https://github.com/YassinEldeeb/rust-embedded-langs-vs-native-benchmark)).\n\n> VRL was initially created to allow users to extend [Vector](https://vector.dev/), a high-performance observability data router, and adopted for Conductor to allow developers to extend the functionality of the GraphQL Gateway easily.\n\n### Writing VRL\n\nVRL is an expression-oriented language. A VRL program consists entirely of expressions, with every expression returning a value. You can define variables, call functions, and use operators to manipulate values.\n\n#### Variables and Functions\n\nThe following program defines a variable `myVar` with the value `\"myValue\"` and prints it to the console:\n\n```vrl\n\nmyVar = \"my value\"\n\nlog(myVar, level:\"info\")\n\n```\n\n#### Assignment\n\nThe `.` is used to set output values. In this example, we are setting the `x-authorization` header of the upstream HTTP request to `my-value`.\n\nHere's an example for a VRL program that extends Conductor's behavior by adding a custom HTTP header to all upstream HTTP requests:\n\n```vrl\n\n.upstream_http_req.headers.\"x-authorization\" = \"my-value\"\n\n```\n\n#### Metadata\n\nThe `%` is used to access metadata values. Note that metadata values are read only.\n\nThe following program is printing a metadata value to the console:\n\n```vrl\n\nlog(%downstream_http_req.headers.authorization, level:\"info\")\n\n```\n\n#### Further Reading\n\n- [VRL Playground](https://playground.vrl.dev/)\n\n- [VRL concepts documentation](https://vector.dev/docs/reference/vrl/#concepts)\n\n- [VRL syntax documentation](https://vector.dev/docs/reference/vrl/expressions/)\n\n- [Compiler errors documentation](https://vector.dev/docs/reference/vrl/errors/)\n\n- [VRL program examples](https://vector.dev/docs/reference/vrl/examples/)\n\n### Runtime Failure Handling\n\nSome VRL functions are fallible, meaning that they can error. Any potential errors thrown by fallible functions must be handled, a requirement enforced at compile time.\n\n```vrl\n\n# This function is fallible, and can create errors, so it must be handled.\n\nparsed, err = parse_json(\"invalid json\")\n\n```\n\nVRL function calls can be marked as infallible by adding a `!` suffix to the function call: (note that this might lead to runtime errors)\n\n```vrl\n\nparsed = parse_json!(\"invalid json\")\n\n```\n\n> In case of a runtime error of a fallible function call, an error will be returned to the end-user, and the gateway will not continue with the execution.\n\n### Input/Output\n\n#### `on_downstream_http_request`\n\nThe `on_downstream_http_request` hook is executed when a downstream HTTP request is received to the gateway from the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_req.body` (type: `string`): The body string of the incoming HTTP request.\n\n- `%downstream_http_req.uri` (type: `string`): The URI of the incoming HTTP request.\n\n- `%downstream_http_req.query_string` (type: `string`): The query string of the incoming HTTP request.\n\n- `%downstream_http_req.method` (type: `string`): The HTTP method of the incoming HTTP request.\n\n- `%downstream_http_req.headers` (type: `object`): The HTTP headers of the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, the gateway will skip the lookup phase, and will use this GraphQL operation instead.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can specify the executable operation by setting this value.\n\n- `.graphql.variables` (type: `object`): The GraphQL variables to be used when executing the GraphQL operation.\n\n- `.graphql.extensions` (type: `object`): The GraphQL extensions to be used when executing the GraphQL operation.\n\n#### `on_downstream_graphql_request`\n\nThe `on_downstream_graphql_request` hook is executed when a GraphQL operation is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_graphql_req.operation` (type: `string`): The GraphQL operation string, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.operation_name`(type: `string`) : If multiple GraphQL operations are set in `%downstream_graphql_req.operation`, you can specify the executable operation by setting this value.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, it will override the existing operation.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can override the extracted value by setting this field.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request. Setting this value will override the existing variables.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request. Setting this value will override the existing extensions.\n\n#### `on_upstream_http_request`\n\nThe `on_upstream_http_request` hook is executed when an HTTP request is about to be sent to the upstream GraphQL server.\n\nThe following metadata inputs are available to the hook:\n\n- `%upstream_http_req.body` (type: `string`): The body string of the planned HTTP request.\n\n- `%upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request.\n\n- `%upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request.\n\n- `%upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request.\n\n- `%upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request.\n\nThe following output values are available to the hook:\n\n- `.upstream_http_req.body` (type: `string`): The body string of the planned HTTP request. Setting this value will override the existing body.\n\n- `.upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request. Setting this value will override the existing URI.\n\n- `.upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request. Setting this value will override the existing query string.\n\n- `.upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request. Setting this value will override the existing HTTP method.\n\n- `.upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n#### `on_downstream_http_response`\n\nThe `on_downstream_http_response` hook is executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_res.body` (type: `string`): The body string of the HTTP response.\n\n- `%downstream_http_res.status` (type: `number`): The status code of the HTTP response.\n\n- `%downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response.\n\nThe following output values are available to the hook:\n\n- `.downstream_http_res.body` (type: `string`): The body string of the HTTP response. Setting this value will override the existing body.\n\n- `.downstream_http_res.status` (type: `number`): The status code of the HTTP response. Setting this value will override the existing status code.\n\n- `.downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n### Shared State\n\nDuring the execution of VRL programs, Conductor configures a shared state object for every incoming HTTP request.\n\nThis means that you can create type-safe shared state objects, and use them to share data between different VRL programs and hooks.\n\nYou can find an example for this in the **Examples** section below.\n\n### Available Functions", + "examples": [ { - "type": "object", - "required": [ - "type" - ], - "properties": { - "config": { - "anyOf": [ - { - "$ref": "#/definitions/GraphiQLPluginConfig" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "type": { - "type": "string", - "enum": [ - "graphiql" - ] + "$metadata": { + "description": "Load and execute VRL plugins using inline configuration.", + "title": "Inline" + }, + "config": { + "on_upstream_http_request": { + "content": ".upstream_http_req.headers.\"x-authorization\" = \"some-value\"\n ", + "from": "inline" } - } + }, + "enabled": true, + "type": "vrl" }, { - "type": "object", - "required": [ - "type" - ], - "properties": { - "config": { - "anyOf": [ - { - "$ref": "#/definitions/CorsPluginConfig" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "type": { - "type": "string", - "enum": [ - "cors" - ] + "$metadata": { + "description": "Load and execute VRL plugins using an external '.vrl' file.", + "title": "File" + }, + "config": { + "on_upstream_http_request": { + "from": "file", + "path": "my_plugin.vrl" } - } + }, + "enabled": true, + "type": "vrl" }, { - "description": "Configuration for the Disable Introspection plugin.", - "type": "object", - "required": [ - "type" - ], - "properties": { - "config": { - "anyOf": [ - { - "$ref": "#/definitions/DisableIntrospectionPluginConfig" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] + "$metadata": { + "description": "This example is using the shared-state feature to store the headers from the incoming HTTP request, and it pass it through to upstream calls.", + "title": "Headers Passthrough" + }, + "config": { + "on_downstream_http_request": { + "content": "incoming_headers = %downstream_http_req.headers\n ", + "from": "inline" }, - "type": { - "type": "string", - "enum": [ - "disable_introspection" - ] + "on_upstream_http_request": { + "content": ".upstream_http_req.headers = incoming_headers\n ", + "from": "inline" } - } + }, + "enabled": true, + "type": "vrl" }, { - "type": "object", - "required": [ - "type" - ], - "properties": { - "config": { - "anyOf": [ - { - "$ref": "#/definitions/HttpGetPluginConfig" - }, - { - "type": "null" - } - ] - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] + "$metadata": { + "description": "The following example is configuring a variable, and use it later", + "title": "Shared State" + }, + "config": { + "on_downstream_http_request": { + "content": "authorization_header = %downstream_http_req.headers.authorization\n ", + "from": "inline" }, - "type": { - "type": "string", - "enum": [ - "http_get" - ] + "on_upstream_http_request": { + "content": ".upstream_http_req.headers.\"x-auth\" = authorization_header\n ", + "from": "inline" } - } + }, + "enabled": true, + "type": "vrl" }, { - "type": "object", - "required": [ - "config", - "type" - ], - "properties": { - "config": { - "$ref": "#/definitions/VrlPluginConfig" + "$metadata": { + "description": "The following example rejects all incoming requests that doesn't have the \"authorization\" header set.", + "title": "Short Circuit" + }, + "config": { + "on_downstream_http_request": { + "content": "if %downstream_http_req.headers.authorization == null {\nshort_circuit!(403, \"Missing authorization header\")\n}\n ", + "from": "inline" + } + }, + "enabled": true, + "type": "vrl" + }, + { + "$metadata": { + "description": "The following example is using a custom GraphQL extraction, overriding the default gateway behavior. In this example, we parse the incoming body as JSON and use the parsed value to find the GraphQL operation. Assuming the body structure is: `{ \"runThisQuery\": \"query { __typename }\", \"variables\": { }`.", + "title": "Custom GraphQL Extraction" + }, + "config": { + "on_downstream_http_request": { + "content": "parsed_body = parse_json!(%downstream_http_req.body)\n.graphql.operation = parsed_body.runThisQuery\n.graphql.variables = parsed_body.variables\n ", + "from": "inline" + } + }, + "enabled": true, + "type": "vrl" + } + ], + "type": "object", + "properties": { + "on_downstream_http_request": { + "description": "A hook executed when a downstream HTTP request is received to the gateway from the end-user. This hook allow you to extract information from the request, for later use, or to reject a request quickly.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] + { + "type": "null" + } + ] + }, + "on_downstream_graphql_request": { + "description": "A hook executed when a GraphQL query is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent. This hooks allow you to easily manipulate the incoming GraphQL request.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" }, - "type": { - "type": "string", - "enum": [ - "vrl" - ] + { + "type": "null" } - } + ] }, - { - "type": "object", - "required": [ - "config", - "type" - ], - "properties": { - "config": { - "$ref": "#/definitions/TrustedDocumentsPluginConfig" + "on_upstream_http_request": { + "description": "A hook executed when an HTTP request is about to be sent to the upstream GraphQL server. This hook allow you to manipulate upstream HTTP calls easily.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] + { + "type": "null" + } + ] + }, + "on_downstream_http_response": { + "description": "A hook executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user. This hook allow you to manipulate the end-user response easily.", + "anyOf": [ + { + "$ref": "#/definitions/VrlConfigReference" }, - "type": { - "type": "string", - "enum": [ - "trusted_documents" - ] + { + "type": "null" + } + ] + } + } + }, + "TrustedDocumentsPluginConfig": { + "description": "This plugin allows you to define a list of trusted GraphQL documents that can be executed by the gateway (also called **Persisted Operations**).\n\nFor additional information, please refer to [Trusted Documents](https://benjie.dev/graphql/trusted-documents).", + "examples": [ + { + "$metadata": { + "description": "This example is using a local file called `trusted_documents.json` as a store, using the Key->Value map format. The protocol exposed is based on HTTP `POST`, using the `documentId` parameter from the request body.", + "title": "Local File Store" + }, + "config": { + "protocols": [ + { + "field_name": "documentId", + "type": "document_id" + } + ], + "store": { + "format": "json_key_value", + "path": "trusted_documents.json", + "source": "file" + } + }, + "enabled": true, + "type": "trusted_documents" + }, + { + "$metadata": { + "description": "This example uses a local file store called `trusted_documents.json`, using the Key->Value map format. The protocol exposed is based on HTTP `GET`, and extracts all parameters from the query string.", + "title": "HTTP GET" + }, + "config": { + "protocols": [ + { + "document_id_from": { + "name": "documentId", + "source": "search_query" + }, + "operation_name_from": { + "name": "operationName", + "source": "search_query" + }, + "type": "http_get", + "variables_from": { + "name": "variables", + "source": "search_query" + } + } + ], + "store": { + "format": "json_key_value", + "path": "trusted_documents.json", + "source": "file" } + }, + "enabled": true, + "type": "trusted_documents" + } + ], + "type": "object", + "required": [ + "protocols", + "store" + ], + "properties": { + "store": { + "description": "The store defines the source of trusted documents. The store contents is a list of hashes and GraphQL documents that are allowed to be executed.", + "$ref": "#/definitions/TrustedDocumentsPluginStoreConfig" + }, + "protocols": { + "description": "A list of protocols to be exposed by this plugin. Each protocol defines how to obtain the document ID from the incoming request. You can specify multiple kinds of protocols, if needed.", + "type": "array", + "items": { + "$ref": "#/definitions/TrustedDocumentsProtocolConfig" } }, + "allow_untrusted": { + "description": "By default, this plugin does not allow untrusted operations to be executed. This is a security measure to prevent accidental exposure of operations that are not persisted.", + "type": [ + "boolean", + "null" + ] + } + } + }, + "TrustedDocumentsPluginStoreConfig": { + "oneOf": [ { + "title": "file", + "description": "File-based store configuration. The path specified is relative to the location of the root configuration file. The file contents are loaded into memory on startup. The file is not reloaded automatically. The file format is specified by the `format` field, based on the structure of your file.", "type": "object", "required": [ - "config", - "type" + "format", + "path", + "source" ], "properties": { - "config": { - "$ref": "#/definitions/JwtAuthPluginConfig" - }, - "enabled": { - "default": true, - "type": [ - "boolean", - "null" - ] - }, - "type": { + "source": { "type": "string", "enum": [ - "jwt_auth" + "file" ] + }, + "path": { + "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", + "$ref": "#/definitions/LocalFileReference" + }, + "format": { + "description": "The format and the expected structure of the loaded store file.", + "$ref": "#/definitions/TrustedDocumentsFileFormat" } } } ] }, - "ServerConfig": { - "type": "object", - "properties": { - "host": { - "description": "The host to listen on, default to 127.0.0.1", - "default": "127.0.0.1", - "type": "string" + "TrustedDocumentsFileFormat": { + "oneOf": [ + { + "title": "apollo_persisted_query_manifest", + "description": "JSON file formated based on [Apollo Persisted Query Manifest](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#1-generate-operation-manifest).", + "type": "string", + "enum": [ + "apollo_persisted_query_manifest" + ] }, - "port": { - "description": "The port to listen on, default to 9000", - "default": 9000, - "type": "integer", - "format": "uint16", - "minimum": 0.0 + { + "title": "json_key_value", + "description": "A simple JSON map of key-value pairs.\n\nExample: `{\"key1\": \"query { __typename }\"}`", + "type": "string", + "enum": [ + "json_key_value" + ] } - } + ] }, - "SourceDefinition": { - "description": "A source definition for a GraphQL endpoint or a federated GraphQL implementation.", + "TrustedDocumentsProtocolConfig": { "oneOf": [ { - "description": "A simple, single GraphQL endpoint", + "title": "apollo_manifest_extensions", + "description": "This protocol is based on [Apollo's Persisted Query Extensions](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#2-publish-operation-manifest). The GraphQL operation key is sent over `POST` and contains `extensions` field with the GraphQL document hash.\n\nExample: `POST /graphql {\"extensions\": {\"persistedQuery\": {\"version\": 1, \"sha256Hash\": \"123\"}}`", "type": "object", "required": [ - "config", - "id", "type" ], "properties": { - "config": { - "description": "The configuration for the GraphQL source.", - "$ref": "#/definitions/GraphQLSourceConfig" - }, - "id": { - "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", - "type": "string" - }, "type": { "type": "string", "enum": [ - "graphql" + "apollo_manifest_extensions" ] } } }, { - "description": "federation endpoint", + "title": "document_id", + "description": "This protocol is based on a `POST` request with a JSON body containing a field with the document ID. By default, the field name is `documentId`.\n\nExample: `POST /graphql {\"documentId\": \"123\", \"variables\": {\"code\": \"AF\"}, \"operationName\": \"test\"}`", "type": "object", "required": [ - "config", - "id", "type" ], "properties": { - "config": { - "description": "The configuration for the GraphQL source.", - "$ref": "#/definitions/FederationSourceConfig" - }, - "id": { - "description": "The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition.", - "type": "string" - }, "type": { "type": "string", "enum": [ - "federation" + "document_id" ] - } - } - } - ] - }, - "SupergraphSourceConfig": { - "oneOf": [ - { - "title": "file", - "description": "The file path for the Supergraph schema.\n\n> This provider is not supported on WASM runtime.", - "type": "object", - "required": [ - "file" - ], - "properties": { - "file": { - "$ref": "#/definitions/LocalFileReference" - } - }, - "additionalProperties": false - }, - { - "title": "env", - "description": "The environment variable that contains the Supergraph schema.", - "type": "object", - "required": [ - "env" - ], - "properties": { - "env": { + }, + "field_name": { + "description": "The name of the JSON field containing the document ID in the incoming request.", + "default": "documentId", "type": "string" } - }, - "additionalProperties": false + } }, { - "title": "remote", - "description": "The remote endpoint where the Supergraph schema can be fetched.", + "title": "http_get", + "description": "This protocol is based on a HTTP `GET` request. You can customize where to fetch each one of the parameters from. Each request parameter can be obtained from a different source: query, path, or header. By defualt, all parameters are obtained from the query string.\n\nUnlike other protocols, this protocol does not support sending GraphQL mutations.\n\nExample: `GET /graphql?documentId=123&variables=%7B%22code%22%3A%22AF%22%7D&operationName=test`", "type": "object", "required": [ - "remote" + "type" ], "properties": { - "remote": { - "type": "object", - "required": [ - "url" - ], - "properties": { - "fetch_every": { - "description": "Polling interval for fetching the Supergraph schema from the remote.", - "default": "1m", - "anyOf": [ - { - "$ref": "#/definitions/Duration" - }, - { - "type": "null" - } - ] - }, - "headers": { - "description": "Optional headers to include in the request (ex: for authentication)", - "type": [ - "object", - "null" - ], - "additionalProperties": { - "type": "string" - } - }, - "url": { - "description": "The URL endpoint from where to fetch the Supergraph schema.", - "type": "string" - } - } + "type": { + "type": "string", + "enum": [ + "http_get" + ] + }, + "document_id_from": { + "description": "Instructions for fetching the document ID parameter from the incoming HTTP request.", + "default": { + "name": "documentId", + "source": "search_query" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + }, + "variables_from": { + "description": "Instructions for fetching the variables parameter from the incoming HTTP request. GraphQL variables must be passed as a JSON-encoded string.", + "default": { + "name": "variables", + "source": "search_query" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + }, + "operation_name_from": { + "description": "Instructions for fetching the operationName parameter from the incoming HTTP request.", + "default": { + "name": "operationName", + "source": "search_query" + }, + "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" } - }, - "additionalProperties": false + } } ] }, @@ -1242,15 +1376,15 @@ "source" ], "properties": { - "name": { - "description": "The name of the HTTP query parameter.", - "type": "string" - }, "source": { "type": "string", "enum": [ "search_query" ] + }, + "name": { + "description": "The name of the HTTP query parameter.", + "type": "string" } } }, @@ -1263,17 +1397,17 @@ "source" ], "properties": { - "position": { - "description": "The numeric value specific the location of the argument (starting from 0).", - "type": "integer", - "format": "uint", - "minimum": 0.0 - }, "source": { "type": "string", "enum": [ "path" ] + }, + "position": { + "description": "The numeric value specific the location of the argument (starting from 0).", + "type": "integer", + "format": "uint", + "minimum": 0.0 } } }, @@ -1286,421 +1420,517 @@ "source" ], "properties": { - "name": { - "description": "The name of the HTTP header.", - "type": "string" - }, "source": { "type": "string", "enum": [ "header" ] + }, + "name": { + "description": "The name of the HTTP header.", + "type": "string" } } } ] }, - "TrustedDocumentsFileFormat": { - "oneOf": [ + "JwtAuthPluginConfig": { + "description": "The `jwt_auth` plugin implements the [JSON Web Tokens](https://jwt.io/introduction) specification.\n\nIt can be used to verify the JWT signature, and optionally validate the token issuer and audience. It can also forward the token and its claims to the upstream service.\n\nThe JWKS configuration can be either a local file on the file-system, or a remote JWKS provider.\n\nBy default, the plugin will look for the JWT token in the `Authorization` header, with the `Bearer` prefix.\n\nYou can also configure the plugin to reject requests that don't have a valid JWT token.", + "examples": [ { - "title": "apollo_persisted_query_manifest", - "description": "JSON file formated based on [Apollo Persisted Query Manifest](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#1-generate-operation-manifest).", - "type": "string", - "enum": [ - "apollo_persisted_query_manifest" - ] + "$metadata": { + "description": "This example is loading a JWKS file from the local file-system. The token is looked up in the `Authorization` header.", + "title": "Local JWKS" + }, + "config": { + "jwks_providers": [ + { + "path": "jwks.json", + "source": "local" + } + ], + "lookup_locations": [ + { + "name": "Authorization", + "prefix": "Bearer", + "source": "header" + } + ] + }, + "enabled": true, + "type": "jwt_auth" }, - { - "title": "json_key_value", - "description": "A simple JSON map of key-value pairs.\n\nExample: `{\"key1\": \"query { __typename }\"}`", - "type": "string", - "enum": [ - "json_key_value" - ] - } - ] - }, - "TrustedDocumentsPluginConfig": { - "description": "This plugin allows you to define a list of trusted GraphQL documents that can be executed by the gateway (also called **Persisted Operations**).\n\nFor additional information, please refer to [Trusted Documents](https://benjie.dev/graphql/trusted-documents).", - "examples": [ { "$metadata": { - "title": "Local File Store", - "description": "This example is using a local file called `trusted_documents.json` as a store, using the Key->Value map format. The protocol exposed is based on HTTP `POST`, using the `documentId` parameter from the request body." + "description": "This example is loading a remote JWKS, when the server starts (prefetch). The token is looked up in the `Authorization` header.", + "title": "Remote JWKS with prefetch" + }, + "config": { + "jwks_providers": [ + { + "cache_duration": "10m", + "prefetch": true, + "source": "remote", + "url": "https://example.com/jwks.json" + } + ], + "lookup_locations": [ + { + "name": "Authorization", + "prefix": "Bearer", + "source": "header" + } + ] }, - "type": "trusted_documents", "enabled": true, + "type": "jwt_auth" + }, + { + "$metadata": { + "description": "This example is loading a remote JWKS, and looks for the token in the `auth` cookie. If the token is not present, the request will be rejected.", + "title": "Reject Unauthenticated" + }, "config": { - "protocols": [ + "jwks_providers": [ { - "type": "document_id", - "field_name": "documentId" + "cache_duration": "10m", + "prefetch": true, + "source": "remote", + "url": "https://example.com/jwks.json" } ], - "store": { - "source": "file", - "path": "trusted_documents.json", - "format": "json_key_value" - } - } + "lookup_locations": [ + { + "name": "auth", + "source": "cookies" + } + ], + "reject_unauthenticated_requests": true + }, + "enabled": true, + "type": "jwt_auth" }, { "$metadata": { - "title": "HTTP GET", - "description": "This example uses a local file store called `trusted_documents.json`, using the Key->Value map format. The protocol exposed is based on HTTP `GET`, and extracts all parameters from the query string." + "description": "This example is loading a remote JWKS, and looks for the token in the `jwt` cookie. If the token is not present, the request will be rejected. The token and its claims will be forwarded to the upstream service in the `X-Auth-Token` and `X-Auth-Claims` headers.", + "title": "Claims Forwarding" + }, + "config": { + "forward_claims_to_upstream_header": "X-Auth-Claims", + "forward_token_to_upstream_header": "X-Auth-Token", + "jwks_providers": [ + { + "cache_duration": "10m", + "prefetch": true, + "source": "remote", + "url": "https://example.com/jwks.json" + } + ], + "lookup_locations": [ + { + "name": "jwt", + "source": "cookies" + } + ], + "reject_unauthenticated_requests": true }, - "type": "trusted_documents", "enabled": true, + "type": "jwt_auth" + }, + { + "$metadata": { + "description": "This example is using strict validation, where the token issuer and audience are checked.", + "title": "Strict Validation" + }, "config": { - "protocols": [ + "audiences": [ + "realm.myapp.com" + ], + "issuers": [ + "https://example.com" + ], + "jwks_providers": [ { - "type": "http_get", - "document_id_from": { - "source": "search_query", - "name": "documentId" - }, - "variables_from": { - "source": "search_query", - "name": "variables" - }, - "operation_name_from": { - "source": "search_query", - "name": "operationName" - } + "cache_duration": "10m", + "prefetch": null, + "source": "remote", + "url": "https://example.com/jwks.json" } ], - "store": { - "source": "file", - "path": "trusted_documents.json", - "format": "json_key_value" - } - } + "lookup_locations": [ + { + "name": "jwt", + "source": "cookies" + } + ] + }, + "enabled": true, + "type": "jwt_auth" } ], "type": "object", "required": [ - "protocols", - "store" + "jwks_providers" ], "properties": { - "allow_untrusted": { - "description": "By default, this plugin does not allow untrusted operations to be executed. This is a security measure to prevent accidental exposure of operations that are not persisted.", + "jwks_providers": { + "description": "A list of JWKS providers to use for verifying the JWT signature. Can be either a path to a local JSON of the file-system, or a URL to a remote JWKS provider.", + "type": "array", + "items": { + "$ref": "#/definitions/JwksProviderSourceConfig" + } + }, + "issuers": { + "description": "Specify the [principal](https://tools.ietf.org/html/rfc7519#section-4.1.1) that issued the JWT, usually a URL or an email address. If specified, it has to match the `iss` field in JWT, otherwise the token's `iss` field is not checked.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "audiences": { + "description": "The list of [JWT audiences](https://tools.ietf.org/html/rfc7519#section-4.1.3) are allowed to access. If this field is set, the token's `aud` field must be one of the values in this list, otherwise the token's `aud` field is not checked.", + "type": [ + "array", + "null" + ], + "items": { + "type": "string" + } + }, + "lookup_locations": { + "description": "A list of locations to look up for the JWT token in the incoming HTTP request. The first one that is found will be used.", + "default": [ + { + "name": "Authorization", + "prefix": "Bearer", + "source": "header" + } + ], + "type": "array", + "items": { + "$ref": "#/definitions/JwtAuthPluginLookupLocation" + } + }, + "reject_unauthenticated_requests": { + "description": "If set to `true`, the entire request will be rejected if the JWT token is not present in the request.", "type": [ "boolean", "null" ] }, - "protocols": { - "description": "A list of protocols to be exposed by this plugin. Each protocol defines how to obtain the document ID from the incoming request. You can specify multiple kinds of protocols, if needed.", - "type": "array", + "allowed_algorithms": { + "description": "List of allowed algorithms for verifying the JWT signature. If not specified, the default list of all supported algorithms in [`jsonwebtoken` crate](https://crates.io/crates/jsonwebtoken) are used.", + "default": [ + "HS256", + "HS384", + "HS512", + "RS256", + "RS384", + "RS512", + "ES256", + "ES384", + "PS256", + "PS384", + "PS512", + "EdDSA" + ], + "type": [ + "array", + "null" + ], "items": { - "$ref": "#/definitions/TrustedDocumentsProtocolConfig" + "type": "string" } }, - "store": { - "description": "The store defines the source of trusted documents. The store contents is a list of hashes and GraphQL documents that are allowed to be executed.", - "$ref": "#/definitions/TrustedDocumentsPluginStoreConfig" + "forward_token_to_upstream_header": { + "description": "Forward the JWT token to the upstream service in the specified header.", + "type": [ + "string", + "null" + ] + }, + "forward_claims_to_upstream_header": { + "description": "Forward the JWT claims to the upstream service in the specified header.", + "type": [ + "string", + "null" + ] + } + } + }, + "JwksProviderSourceConfig": { + "oneOf": [ + { + "title": "local", + "description": "A local file on the file-system. This file will be read once on startup and cached.", + "type": "object", + "required": [ + "path", + "source" + ], + "properties": { + "source": { + "type": "string", + "enum": [ + "local" + ] + }, + "path": { + "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", + "$ref": "#/definitions/LocalFileReference" + } + } + }, + { + "title": "remote", + "description": "A remote JWKS provider. The JWKS will be fetched via HTTP/HTTPS and cached.", + "type": "object", + "required": [ + "source", + "url" + ], + "properties": { + "source": { + "type": "string", + "enum": [ + "remote" + ] + }, + "url": { + "description": "The URL to fetch the JWKS key set from, via HTTP/HTTPS.", + "type": "string" + }, + "cache_duration": { + "description": "Duration after which the cached JWKS should be expired. If not specified, the default value will be used.", + "default": "10m", + "anyOf": [ + { + "$ref": "#/definitions/Duration" + }, + { + "type": "null" + } + ] + }, + "prefetch": { + "description": "If set to `true`, the JWKS will be fetched on startup and cached. In case of invalid JWKS, the error will be ignored and the plugin will try to fetch again when server receives the first request. If set to `false`, the JWKS will be fetched on-demand, when the first request comes in.", + "type": [ + "boolean", + "null" + ] + } + } } - } + ] }, - "TrustedDocumentsPluginStoreConfig": { + "JwtAuthPluginLookupLocation": { "oneOf": [ { - "title": "file", - "description": "File-based store configuration. The path specified is relative to the location of the root configuration file. The file contents are loaded into memory on startup. The file is not reloaded automatically. The file format is specified by the `format` field, based on the structure of your file.", + "title": "header", "type": "object", "required": [ - "format", - "path", + "name", "source" ], "properties": { - "format": { - "description": "The format and the expected structure of the loaded store file.", - "$ref": "#/definitions/TrustedDocumentsFileFormat" + "source": { + "type": "string", + "enum": [ + "header" + ] }, - "path": { - "description": "A path to a local file on the file-system. Relative to the location of the root configuration file.", - "$ref": "#/definitions/LocalFileReference" + "name": { + "type": "string" }, + "prefix": { + "type": [ + "string", + "null" + ] + } + } + }, + { + "title": "query_param", + "type": "object", + "required": [ + "name", + "source" + ], + "properties": { "source": { "type": "string", "enum": [ - "file" + "query_param" ] + }, + "name": { + "type": "string" } } - } - ] - }, - "TrustedDocumentsProtocolConfig": { - "oneOf": [ + }, { - "title": "apollo_manifest_extensions", - "description": "This protocol is based on [Apollo's Persisted Query Extensions](https://www.apollographql.com/docs/kotlin/advanced/persisted-queries/#2-publish-operation-manifest). The GraphQL operation key is sent over `POST` and contains `extensions` field with the GraphQL document hash.\n\nExample: `POST /graphql {\"extensions\": {\"persistedQuery\": {\"version\": 1, \"sha256Hash\": \"123\"}}`", + "title": "cookies", "type": "object", "required": [ - "type" + "name", + "source" ], "properties": { - "type": { + "source": { "type": "string", "enum": [ - "apollo_manifest_extensions" + "cookies" ] + }, + "name": { + "type": "string" } } + } + ] + }, + "TelemetryPluginConfig": { + "description": "The `telemetry` plugin exports traces information about Conductor to a telemetry backend.\n\n\n\nAt the moment, this plugin is not supported on WASM (CloudFlare Worker) runtime.\n\nYou may follow [this GitHub issue](https://github.com/the-guild-org/conductor/issues/354) for additional information.\n\n\n\nThe telemetry plugin exports traces information about the following aspects of Conductor:\n\n- GraphQL parser (timing)\n\n- GraphQL execution (operation type, operation body, operation name, timing, errors)\n\n- Query planning (timing, operation body, operation name)\n\n- Incoming HTTP requests (attributes, timing, errors)\n\n- Outgoing HTTP requests (attributes, timing, errors)\n\nWhen used with a telemtry backend, you can expect to see the following information:\n\n![img](/assets/telemetry.png)", + "type": "object", + "required": [ + "targets" + ], + "properties": { + "service_name": { + "description": "Configures the service name that reports the telemetry data. This will appear in the telemetry data as the `service.name` attribute.", + "default": "conductor", + "type": "string" }, + "targets": { + "description": "A list of telemetry targets to send telemetry data to.\n\nThe telemtry data is scoped per endpoint, and you can specify multiple targets if you need to export stats to multiple backends.", + "type": "array", + "items": { + "$ref": "#/definitions/TelemetryTarget" + } + } + } + }, + "TelemetryTarget": { + "oneOf": [ { - "title": "document_id", - "description": "This protocol is based on a `POST` request with a JSON body containing a field with the document ID. By default, the field name is `documentId`.\n\nExample: `POST /graphql {\"documentId\": \"123\", \"variables\": {\"code\": \"AF\"}, \"operationName\": \"test\"}`", + "title": "stdout", + "description": "Sends telemetry data to `stdout` in a human-readable format.\n\nUse this source for debugging purposes, or if you want to pipe the telemetry data to another process.", "type": "object", "required": [ "type" ], "properties": { - "field_name": { - "description": "The name of the JSON field containing the document ID in the incoming request.", - "default": "documentId", - "type": "string" - }, "type": { "type": "string", "enum": [ - "document_id" + "stdout" ] } } }, { - "title": "http_get", - "description": "This protocol is based on a HTTP `GET` request. You can customize where to fetch each one of the parameters from. Each request parameter can be obtained from a different source: query, path, or header. By defualt, all parameters are obtained from the query string.\n\nUnlike other protocols, this protocol does not support sending GraphQL mutations.\n\nExample: `GET /graphql?documentId=123&variables=%7B%22code%22%3A%22AF%22%7D&operationName=test`", + "title": "Open Telemetry (OTLP)", + "description": "Sends telemetry traces data to an [OpenTelemetry](https://opentelemetry.io/) backend, using the [OTLP protocol](https://opentelemetry.io/docs/specs/otel/protocol/).\n\nYou can find [here a list backends that supports the OTLP format](https://github.com/magsther/awesome-opentelemetry#open-source).", "type": "object", "required": [ + "endpoint", "type" ], "properties": { - "document_id_from": { - "description": "Instructions for fetching the document ID parameter from the incoming HTTP request.", - "default": { - "source": "search_query", - "name": "documentId" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" - }, - "operation_name_from": { - "description": "Instructions for fetching the operationName parameter from the incoming HTTP request.", - "default": { - "source": "search_query", - "name": "operationName" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" - }, "type": { "type": "string", "enum": [ - "http_get" + "otlp" ] }, - "variables_from": { - "description": "Instructions for fetching the variables parameter from the incoming HTTP request. GraphQL variables must be passed as a JSON-encoded string.", - "default": { - "source": "search_query", - "name": "variables" - }, - "$ref": "#/definitions/TrustedDocumentHttpGetParameterLocation" + "endpoint": { + "description": "The OTLP backend endpoint. The format is based on full URL, e.g. `http://localhost:7201`.", + "type": "string" + }, + "protocol": { + "description": "The OTLP transport to use to export telemetry data.", + "default": "grpc", + "$ref": "#/definitions/OtlpProtcol" + }, + "timeout": { + "description": "Export timeout. You can use the human-readable format in this field, e.g. `10s`.", + "default": "10s", + "type": "string" + }, + "gzip_compression": { + "description": "Whether to use gzip compression when sending telemetry data.\n\nPlease verify your backend supports and enables `gzip` compression before enabling this option.", + "default": false, + "type": "boolean" } } - } - ] - }, - "VrlConfigReference": { - "oneOf": [ + }, { - "title": "inline", - "description": "Inline string for a VRL code snippet. The string is parsed and executed as a VRL plugin.", + "title": "Datadog", + "description": "Sends telemetry traces data to a [Datadog](https://www.datadoghq.com/) agent (local or remote).\n\nTo get started with Datadog, make sure you have a [Datadog agent running](https://docs.datadoghq.com/agent/?tab=source).", "type": "object", "required": [ - "content", - "from" + "type" ], "properties": { - "content": { - "type": "string" - }, - "from": { + "type": { "type": "string", "enum": [ - "inline" + "datadog" ] + }, + "agent_endpoint": { + "description": "The Datadog agent endpoint. The format is based on hostname and port only, e.g. `127.0.0.1:8126`.", + "default": "127.0.0.1:8126", + "type": "string" } } }, { - "title": "file", - "description": "File reference to a VRL file. The file is loaded and executed as a VRL plugin.", + "title": "Jaeger", + "description": "Sends telemetry traces data to a [Jaeger](https://www.jaegertracing.io/) backend, using the native protocol of [Jaeger (UDP) using `thrift`](https://www.jaegertracing.io/docs/next-release/getting-started/).\n\n> Note: Jaeger also [supports OTLP format](https://opentelemetry.io/blog/2022/jaeger-native-otlp/), so it's preferred to use the `otlp` target.\n\nTo get started with Jaeger, make sure you have a Jaeger backend running, and then use the following command to start the Jaeger backend and UI in your local machine, using Docker:\n\n`docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest`", "type": "object", "required": [ - "from", - "path" + "type" ], "properties": { - "from": { + "type": { "type": "string", "enum": [ - "file" + "jaeger" ] }, - "path": { - "$ref": "#/definitions/LocalFileReference" + "endpoint": { + "description": "The UDP endpoint of the Jaeger backend. The format is based on hostname and port only, e.g. `127.0.0.1:6831`.", + "default": "127.0.0.1:6831", + "type": "string" } } } ] }, - "VrlPluginConfig": { - "description": "To simplify the process of extending the functionality of the GraphQL Gateway, we adopted a Rust-based script language called [VRL](https://vector.dev/docs/reference/vrl/).\n\nVRL language is intended for writing simple scripts that can be executed in the context of the GraphQL Gateway. VRL is focused around safety and performance: the script is compiled into Rust code when the server starts, and executed as a native Rust code ([you can find a comparison between VRL and other scripting languages here](https://github.com/YassinEldeeb/rust-embedded-langs-vs-native-benchmark)).\n\n> VRL was initially created to allow users to extend [Vector](https://vector.dev/), a high-performance observability data router, and adopted for Conductor to allow developers to extend the functionality of the GraphQL Gateway easily.\n\n### Writing VRL\n\nVRL is an expression-oriented language. A VRL program consists entirely of expressions, with every expression returning a value. You can define variables, call functions, and use operators to manipulate values.\n\n#### Variables and Functions\n\nThe following program defines a variable `myVar` with the value `\"myValue\"` and prints it to the console:\n\n```vrl\n\nmyVar = \"my value\"\n\nlog(myVar, level:\"info\")\n\n```\n\n#### Assignment\n\nThe `.` is used to set output values. In this example, we are setting the `x-authorization` header of the upstream HTTP request to `my-value`.\n\nHere's an example for a VRL program that extends Conductor's behavior by adding a custom HTTP header to all upstream HTTP requests:\n\n```vrl\n\n.upstream_http_req.headers.\"x-authorization\" = \"my-value\"\n\n```\n\n#### Metadata\n\nThe `%` is used to access metadata values. Note that metadata values are read only.\n\nThe following program is printing a metadata value to the console:\n\n```vrl\n\nlog(%downstream_http_req.headers.authorization, level:\"info\")\n\n```\n\n#### Further Reading\n\n- [VRL Playground](https://playground.vrl.dev/)\n\n- [VRL concepts documentation](https://vector.dev/docs/reference/vrl/#concepts)\n\n- [VRL syntax documentation](https://vector.dev/docs/reference/vrl/expressions/)\n\n- [Compiler errors documentation](https://vector.dev/docs/reference/vrl/errors/)\n\n- [VRL program examples](https://vector.dev/docs/reference/vrl/examples/)\n\n### Runtime Failure Handling\n\nSome VRL functions are fallible, meaning that they can error. Any potential errors thrown by fallible functions must be handled, a requirement enforced at compile time.\n\n```vrl\n\n# This function is fallible, and can create errors, so it must be handled.\n\nparsed, err = parse_json(\"invalid json\")\n\n```\n\nVRL function calls can be marked as infallible by adding a `!` suffix to the function call: (note that this might lead to runtime errors)\n\n```vrl\n\nparsed = parse_json!(\"invalid json\")\n\n```\n\n> In case of a runtime error of a fallible function call, an error will be returned to the end-user, and the gateway will not continue with the execution.\n\n### Input/Output\n\n#### `on_downstream_http_request`\n\nThe `on_downstream_http_request` hook is executed when a downstream HTTP request is received to the gateway from the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_req.body` (type: `string`): The body string of the incoming HTTP request.\n\n- `%downstream_http_req.uri` (type: `string`): The URI of the incoming HTTP request.\n\n- `%downstream_http_req.query_string` (type: `string`): The query string of the incoming HTTP request.\n\n- `%downstream_http_req.method` (type: `string`): The HTTP method of the incoming HTTP request.\n\n- `%downstream_http_req.headers` (type: `object`): The HTTP headers of the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, the gateway will skip the lookup phase, and will use this GraphQL operation instead.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can specify the executable operation by setting this value.\n\n- `.graphql.variables` (type: `object`): The GraphQL variables to be used when executing the GraphQL operation.\n\n- `.graphql.extensions` (type: `object`): The GraphQL extensions to be used when executing the GraphQL operation.\n\n#### `on_downstream_graphql_request`\n\nThe `on_downstream_graphql_request` hook is executed when a GraphQL operation is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_graphql_req.operation` (type: `string`): The GraphQL operation string, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.operation_name`(type: `string`) : If multiple GraphQL operations are set in `%downstream_graphql_req.operation`, you can specify the executable operation by setting this value.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request.\n\nThe following output values are available to the hook:\n\n- `.graphql.operation` (type: `string`): The GraphQL operation string to be executed. If this value is set, it will override the existing operation.\n\n- `.graphql.operation_name` (type: `string`): If multiple GraphQL operations are set in `.graphql.operation`, you can override the extracted value by setting this field.\n\n- `%downstream_graphql_req.variables` (type: `object`): The GraphQL variables, as extracted from the incoming HTTP request. Setting this value will override the existing variables.\n\n- `%downstream_graphql_req.extensions` (type: `object`): The GraphQL extensions, as extracted from the incoming HTTP request. Setting this value will override the existing extensions.\n\n#### `on_upstream_http_request`\n\nThe `on_upstream_http_request` hook is executed when an HTTP request is about to be sent to the upstream GraphQL server.\n\nThe following metadata inputs are available to the hook:\n\n- `%upstream_http_req.body` (type: `string`): The body string of the planned HTTP request.\n\n- `%upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request.\n\n- `%upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request.\n\n- `%upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request.\n\n- `%upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request.\n\nThe following output values are available to the hook:\n\n- `.upstream_http_req.body` (type: `string`): The body string of the planned HTTP request. Setting this value will override the existing body.\n\n- `.upstream_http_req.uri` (type: `string`): The URI of the planned HTTP request. Setting this value will override the existing URI.\n\n- `.upstream_http_req.query_string` (type: `string`): The query string of the planned HTTP request. Setting this value will override the existing query string.\n\n- `.upstream_http_req.method` (type: `string`): The HTTP method of the planned HTTP request. Setting this value will override the existing HTTP method.\n\n- `.upstream_http_req.headers` (type: `object`): The HTTP headers of the planned HTTP request. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n#### `on_downstream_http_response`\n\nThe `on_downstream_http_response` hook is executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user.\n\nThe following metadata inputs are available to the hook:\n\n- `%downstream_http_res.body` (type: `string`): The body string of the HTTP response.\n\n- `%downstream_http_res.status` (type: `number`): The status code of the HTTP response.\n\n- `%downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response.\n\nThe following output values are available to the hook:\n\n- `.downstream_http_res.body` (type: `string`): The body string of the HTTP response. Setting this value will override the existing body.\n\n- `.downstream_http_res.status` (type: `number`): The status code of the HTTP response. Setting this value will override the existing status code.\n\n- `.downstream_http_res.headers` (type: `object`): The HTTP headers of the HTTP response. Headers set here will only extend the existing headers. You can use `null` value if you wish to remove an existing header.\n\n### Shared State\n\nDuring the execution of VRL programs, Conductor configures a shared state object for every incoming HTTP request.\n\nThis means that you can create type-safe shared state objects, and use them to share data between different VRL programs and hooks.\n\nYou can find an example for this in the **Examples** section below.\n\n### Available Functions", - "examples": [ - { - "$metadata": { - "title": "Inline", - "description": "Load and execute VRL plugins using inline configuration." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers.\"x-authorization\" = \"some-value\"\n " - } - } - }, - { - "$metadata": { - "title": "File", - "description": "Load and execute VRL plugins using an external '.vrl' file." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "file", - "path": "my_plugin.vrl" - } - } - }, - { - "$metadata": { - "title": "Headers Passthrough", - "description": "This example is using the shared-state feature to store the headers from the incoming HTTP request, and it pass it through to upstream calls." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers = incoming_headers\n " - }, - "on_downstream_http_request": { - "from": "inline", - "content": "incoming_headers = %downstream_http_req.headers\n " - } - } - }, - { - "$metadata": { - "title": "Shared State", - "description": "The following example is configuring a variable, and use it later" - }, - "type": "vrl", - "enabled": true, - "config": { - "on_upstream_http_request": { - "from": "inline", - "content": ".upstream_http_req.headers.\"x-auth\" = authorization_header\n " - }, - "on_downstream_http_request": { - "from": "inline", - "content": "authorization_header = %downstream_http_req.headers.authorization\n " - } - } - }, - { - "$metadata": { - "title": "Short Circuit", - "description": "The following example rejects all incoming requests that doesn't have the \"authorization\" header set." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_downstream_http_request": { - "from": "inline", - "content": "if %downstream_http_req.headers.authorization == null {\nshort_circuit!(403, \"Missing authorization header\")\n}\n " - } - } - }, + "OtlpProtcol": { + "oneOf": [ { - "$metadata": { - "title": "Custom GraphQL Extraction", - "description": "The following example is using a custom GraphQL extraction, overriding the default gateway behavior. In this example, we parse the incoming body as JSON and use the parsed value to find the GraphQL operation. Assuming the body structure is: `{ \"runThisQuery\": \"query { __typename }\", \"variables\": { }`." - }, - "type": "vrl", - "enabled": true, - "config": { - "on_downstream_http_request": { - "from": "inline", - "content": "parsed_body = parse_json!(%downstream_http_req.body)\n.graphql.operation = parsed_body.runThisQuery\n.graphql.variables = parsed_body.variables\n " - } - } - } - ], - "type": "object", - "properties": { - "on_downstream_graphql_request": { - "description": "A hook executed when a GraphQL query is extracted from a downstream HTTP request, and before the upstream GraphQL request is sent. This hooks allow you to easily manipulate the incoming GraphQL request.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, - { - "type": "null" - } - ] - }, - "on_downstream_http_request": { - "description": "A hook executed when a downstream HTTP request is received to the gateway from the end-user. This hook allow you to extract information from the request, for later use, or to reject a request quickly.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, - { - "type": "null" - } - ] - }, - "on_downstream_http_response": { - "description": "A hook executed when a GraphQL response is received from the upstream GraphQL server, and before the response is sent to the end-user. This hook allow you to manipulate the end-user response easily.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, - { - "type": "null" - } + "title": "grpc", + "description": "Uses GRPC with `tonic` to send telemetry data.", + "type": "string", + "enum": [ + "grpc" ] }, - "on_upstream_http_request": { - "description": "A hook executed when an HTTP request is about to be sent to the upstream GraphQL server. This hook allow you to manipulate upstream HTTP calls easily.", - "anyOf": [ - { - "$ref": "#/definitions/VrlConfigReference" - }, - { - "type": "null" - } + { + "title": "http", + "description": "Uses HTTP with `http-proto` to send telemetry data.", + "type": "string", + "enum": [ + "http" ] } - } + ] } } } \ No newline at end of file diff --git a/libs/config/src/lib.rs b/libs/config/src/lib.rs index ac543d87..cf5f88ac 100644 --- a/libs/config/src/lib.rs +++ b/libs/config/src/lib.rs @@ -3,11 +3,11 @@ pub mod interpolate; use conductor_common::serde_utils::{ JsonSchemaExample, JsonSchemaExampleMetadata, LocalFileReference, BASE_PATH, }; +use conductor_logger::config::LoggerConfigFormat; use interpolate::interpolate; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::{collections::HashMap, fs::read_to_string, path::Path, time::Duration}; -use tracing::{error, warn}; /// This section describes the top-level configuration object for Conductor gateway. /// @@ -86,7 +86,6 @@ use tracing::{error, warn}; #[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] pub struct ConductorConfig { #[serde(default, skip_serializing_if = "Option::is_none")] - /// Configuration for the HTTP server. pub server: Option, #[serde(default, skip_serializing_if = "Option::is_none")] @@ -275,6 +274,16 @@ pub enum PluginDefinition { enabled: Option, config: jwt_auth_plugin::Config, }, + + #[serde(rename = "telemetry")] + TelemetryPlugin { + #[serde( + default = "default_plugin_enabled", + skip_serializing_if = "Option::is_none" + )] + enabled: Option, + config: telemetry_plugin::Config, + }, } #[derive(Deserialize, Serialize, Default, Debug, Clone, Copy, JsonSchema)] @@ -304,11 +313,64 @@ impl Level { } } -#[derive(Deserialize, Serialize, Debug, Clone, Default, JsonSchema)] +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] pub struct LoggerConfig { + /// Environment filter configuration as a string. This allows extremely powerful control over Conductor's logging. + /// + /// The `filter` can specify various directives to filter logs based on module paths, span names, + /// and specific fields. These directives can also be combined using commas as a separator. + /// + /// **Basic Usage:** + /// + /// - `info` (logs all messages at info level and higher across all modules) + /// + /// - `error` (logs all messages at error level only, as it's the highest level of severity) + /// + /// **Module-Specific Logging:** + /// + /// - `conductor::gateway=debug` (logs all debug messages for the 'conductor::gateway' module) + /// + /// - `conductor::engine::source=trace` (logs all trace messages for the 'conductor::engine::source' module) + /// + /// **Combining Directives:** + /// + /// - `conductor::gateway=info,conductor::engine::source=trace` (sets info level for the gateway module and trace level for the engine's source module) + /// + /// The syntax of directives is very flexible, allowing complex logging configurations. + /// + /// See [tracing_subscriber::EnvFilter](https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html) for more information. + #[serde(default = "default_log_filter")] + pub filter: String, + /// Configured the logger format. See options below. + /// + /// - `pretty` format is human-readable, ideal for development and debugging. + /// + /// - `json` format is structured, suitable for production environments and log analysis tools. + /// + /// By default, `pretty` is used in TTY environments, and `json` is used in non-TTY environments. + #[serde(default)] + pub format: LoggerConfigFormat, + /// Emits performance information on in crucial areas of the gateway. + /// + /// Look for `close` and `idle` spans printed in the logs. + /// + /// Note: this option is not enabled on WASM runtime, and will be ignored if specified. #[serde(default)] - /// Log level - pub level: Level, + pub print_performance_info: bool, +} + +impl Default for LoggerConfig { + fn default() -> Self { + Self { + filter: default_log_filter(), + format: LoggerConfigFormat::default(), + print_performance_info: false, + } + } +} + +fn default_log_filter() -> String { + "info".to_string() } #[derive(Deserialize, Serialize, Debug, Clone, JsonSchema, Default)] @@ -324,6 +386,7 @@ pub struct ServerConfig { fn default_server_port() -> u16 { 9000 } + fn default_server_host() -> String { "127.0.0.1".to_string() } @@ -340,6 +403,14 @@ pub enum SourceDefinition { /// The configuration for the GraphQL source. config: GraphQLSourceConfig, }, + #[serde(rename = "mocl")] + /// A simple, single GraphQL endpoint + Mock { + /// The identifier of the source. This is used to reference the source in the `from` field of an endpoint definition. + id: String, + /// The configuration for the GraphQL source. + config: MockedSourceConfig, + }, #[serde(rename = "federation")] /// federation endpoint Federation { @@ -502,7 +573,8 @@ pub async fn load_config( let path = Path::new(file_path); // @expected: 👇 - let raw_contents = read_to_string(file_path).expect("Failed to read config file"); + let raw_contents = read_to_string(file_path) + .unwrap_or_else(|e| panic!("Failed to read config file \"{}\": {}", file_path, e)); let base_path = path.parent().unwrap_or_else(|| Path::new("")).to_path_buf(); BASE_PATH.with(|bp| { @@ -524,13 +596,14 @@ pub fn parse_config_contents( config_string = interpolated_content; for warning in warnings { - warn!(warning); + println!("warning: {}", warning); } } Err(errors) => { for error in errors { - error!(error); + println!("error: {}", error); } + // @expected: 👇 panic!("Failed to interpolate config file, please resolve the above errors"); } diff --git a/libs/e2e_tests/Cargo.toml b/libs/e2e_tests/Cargo.toml index 3157c939..df3d57fa 100644 --- a/libs/e2e_tests/Cargo.toml +++ b/libs/e2e_tests/Cargo.toml @@ -12,6 +12,7 @@ serde_json = { workspace = true } tokio = { workspace = true, features = ["full"] } conductor_common = { path = "../common", features = ["test_utils"] } conductor_config = { path = "../config" } +conductor_tracing = { path = "../tracing", features = ["test_utils"] } conductor_engine = { path = "../engine", features = ["test_utils"] } httpmock = "0.7.0" lazy_static = { version = "1.4.0" } @@ -20,6 +21,10 @@ trusted_documents_plugin = { path = "../../plugins/trusted_documents" } disable_introspection_plugin = { path = "../../plugins/disable_introspection" } graphiql_plugin = { path = "../../plugins/graphiql" } http_get_plugin = { path = "../../plugins/http_get" } +telemetry_plugin = { path = "../../plugins/telemetry", features = [ + "test_utils", +] } match_content_type_plugin = { path = "../../plugins/match_content_type" } vrl_plugin = { path = "../../plugins/vrl" } jwt_auth_plugin = { path = "../../plugins/jwt_auth" } +minitrace = { workspace = true, features = ["enable"] } diff --git a/libs/e2e_tests/suite.rs b/libs/e2e_tests/suite.rs index e95c3795..8124a056 100644 --- a/libs/e2e_tests/suite.rs +++ b/libs/e2e_tests/suite.rs @@ -25,9 +25,12 @@ impl TestSuite { let mock_server = self.mock_server.unwrap_or_else(MockServer::start); let mock = mock_server.mock(mock_fn); - let source = GraphQLSourceRuntime::new(GraphQLSourceConfig { - endpoint: mock_server.url("/graphql"), - }); + let source = GraphQLSourceRuntime::new( + "test".to_string(), + GraphQLSourceConfig { + endpoint: mock_server.url("/graphql"), + }, + ); let response = ConductorGateway::execute_test(Arc::new(Box::new(source)), self.plugins, request).await; @@ -55,9 +58,12 @@ impl TestSuite { ); }); - let source = GraphQLSourceRuntime::new(GraphQLSourceConfig { - endpoint: mock_server.url("/graphql"), - }); + let source = GraphQLSourceRuntime::new( + "test".to_string(), + GraphQLSourceConfig { + endpoint: mock_server.url("/graphql"), + }, + ); ConductorGateway::execute_test(Arc::new(Box::new(source)), self.plugins, request).await } diff --git a/libs/e2e_tests/tests/mod.rs b/libs/e2e_tests/tests/mod.rs index 1a25a544..627e738c 100644 --- a/libs/e2e_tests/tests/mod.rs +++ b/libs/e2e_tests/tests/mod.rs @@ -1,3 +1,4 @@ pub mod plugin_cors; pub mod plugin_disable_introspection; +pub mod plugin_telemetry; pub mod plugin_vrl; diff --git a/libs/e2e_tests/tests/plugin_jwt.rs b/libs/e2e_tests/tests/plugin_jwt.rs index 7d7e60a4..fbffccf2 100644 --- a/libs/e2e_tests/tests/plugin_jwt.rs +++ b/libs/e2e_tests/tests/plugin_jwt.rs @@ -106,10 +106,7 @@ pub mod jwt { when .method(POST) .path("/graphql") - .header( - "x-forwarded-claims", - "{\"my_claim\":\"test\",\"exp\":1924942936}", - ) + .header_exists("x-forwarded-claims") .header("x-forwarded-token", token); then .status(200) diff --git a/libs/e2e_tests/tests/plugin_telemetry.rs b/libs/e2e_tests/tests/plugin_telemetry.rs new file mode 100644 index 00000000..ee621905 --- /dev/null +++ b/libs/e2e_tests/tests/plugin_telemetry.rs @@ -0,0 +1,116 @@ +pub mod telemetry { + use conductor_common::{graphql::GraphQLRequest, plugin::CreatablePlugin}; + use conductor_tracing::{minitrace_mgr::MinitraceManager, otel_attrs::*}; + use e2e::suite::TestSuite; + use minitrace::{ + collector::{Config, SpanContext, SpanId}, + future::FutureExt, + Span, + }; + use tokio::test; + + #[test] + async fn spans() { + let (spans, reporter) = conductor_tracing::minitrace_mgr::test_utils::TestReporter::new(); + let plugin = telemetry_plugin::Plugin::create(telemetry_plugin::Config { + targets: vec![telemetry_plugin::Target::Stdout], + ..Default::default() + }) + .await + .unwrap(); + + let mut minitrace_mgr = MinitraceManager::default(); + plugin.configure_tracing_for_test(0, Box::new(reporter), &mut minitrace_mgr); + minitrace::set_reporter(minitrace_mgr.build_reporter(), Config::default()); + + let test = TestSuite { + plugins: vec![plugin], + ..Default::default() + }; + + let span_context = SpanContext::new(MinitraceManager::generate_trace_id(0), SpanId::default()); + let root_span = Span::root("root", span_context); + test + .run_graphql_request(GraphQLRequest::default()) + .in_span(root_span) + .await; + + minitrace::flush(); + + let spans = spans.lock().unwrap(); + + assert_eq!(spans.len(), 6); + // Make sure all spans inherit the same trace id + assert!(spans.iter().all(|v| v.trace_id == span_context.trace_id)); + + let execute = spans + .iter() + .find(|v| v.name == "execute") + .expect("failed to find span"); + assert!(execute.properties.is_empty()); + let graphql_parse = spans + .iter() + .find(|v| v.name == "graphql_parse") + .expect("failed to find span"); + assert!(graphql_parse.properties.is_empty()); + assert_eq!(graphql_parse.parent_id, execute.span_id); + let query = spans + .iter() + .find(|v| v.name == "query") + .expect("failed to find span"); + assert_eq!(query.properties.len(), 2); + assert_eq!( + query.properties[0], + (GRAPHQL_DOCUMENT.into(), "query { __typename }".into()) + ); + assert_eq!( + query.properties[1], + (GRAPHQL_OPERATION_TYPE.into(), "query".into()) + ); + assert_eq!(query.parent_id, execute.span_id); + + let upstream_call = spans + .iter() + .find(|v| v.name == "upstream_call") + .expect("failed to find span"); + assert_eq!(upstream_call.properties.len(), 1); + assert_eq!( + upstream_call.properties[0], + (CONDUCTOR_SOURCE.into(), "test".into()) + ); + assert_eq!(upstream_call.parent_id, query.span_id); + + let upstream_http_post = spans + .iter() + .find(|v| v.name == "POST /graphql") + .expect("failed to find span"); + assert_eq!(upstream_http_post.properties.len(), 8); + assert_eq!(upstream_http_post.parent_id, upstream_call.span_id); + assert_eq!( + upstream_http_post.properties[0], + (HTTP_METHOD.into(), "POST".into()) + ); + assert_eq!( + upstream_http_post.properties[1], + (HTTP_SCHEME.into(), "http".into()) + ); + assert_eq!( + upstream_http_post.properties[2], + (HTTP_HOST.into(), "127.0.0.1".into()) + ); + assert_eq!(upstream_http_post.properties[3].0, HTTP_URL); + assert_eq!(upstream_http_post.properties[4].0, NET_HOST_PORT); + assert_eq!( + upstream_http_post.properties[5], + (OTEL_KIND.into(), "client".into()) + ); + assert_eq!( + upstream_http_post.properties[6], + (SPAN_KIND.into(), "consumer".into()) + ); + assert_eq!( + upstream_http_post.properties[7], + (HTTP_STATUS_CODE.into(), "200".into()) + ); + } +} diff --git a/libs/engine/Cargo.toml b/libs/engine/Cargo.toml index 3a1331a8..34174922 100644 --- a/libs/engine/Cargo.toml +++ b/libs/engine/Cargo.toml @@ -10,6 +10,8 @@ path = "src/lib.rs" test_utils = [] [dependencies] +ureq = "2.9.1" +humantime = "2.1.0" tracing = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -17,13 +19,13 @@ async-trait = { workspace = true } thiserror = { workspace = true } futures = { workspace = true } reqwest = { workspace = true } +reqwest-middleware = { workspace = true } vrl = { workspace = true } base64 = { workspace = true } anyhow = { workspace = true } -ureq = "2.9.1" -humantime = "2.1.0" conductor_common = { path = "../common" } conductor_config = { path = "../config" } +conductor_tracing = { path = "../tracing" } wasm_polyfills = { path = "../wasm_polyfills" } cors_plugin = { path = "../../plugins/cors" } trusted_documents_plugin = { path = "../../plugins/trusted_documents" } @@ -34,7 +36,9 @@ match_content_type_plugin = { path = "../../plugins/match_content_type" } vrl_plugin = { path = "../../plugins/vrl" } jwt_auth_plugin = { path = "../../plugins/jwt_auth" } federation_query_planner = { path = "../../libs/federation_query_planner" } - +telemetry_plugin = { path = "../../plugins/telemetry" } +minitrace = { workspace = true } +minitrace_reqwest = { path = "../minitrace_reqwest" } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] tokio = { workspace = true } diff --git a/libs/engine/src/gateway.rs b/libs/engine/src/gateway.rs index 0a05621b..dfa14511 100644 --- a/libs/engine/src/gateway.rs +++ b/libs/engine/src/gateway.rs @@ -3,23 +3,33 @@ use std::{fmt::Debug, sync::Arc}; use conductor_common::{ execute::RequestExecutionContext, graphql::{ExtractGraphQLOperationError, GraphQLRequest, GraphQLResponse, ParsedGraphQLRequest}, - http::{ConductorHttpRequest, ConductorHttpResponse, Method, StatusCode, Url}, + http::{ConductorHttpRequest, ConductorHttpResponse, Url}, + plugin::PluginError, }; use conductor_config::{ConductorConfig, EndpointDefinition, SourceDefinition}; -use futures::future::join_all; -use tracing::{debug, error}; +use conductor_tracing::{ + minitrace_mgr::MinitraceManager, + otel_attrs::CONDUCTOR_SOURCE, + otel_utils::{create_graphql_error_span_properties, create_graphql_span}, +}; +use minitrace::{future::FutureExt, trace, Span}; +use reqwest::{Method, StatusCode}; +use tracing::error; use crate::{ plugin_manager::PluginManager, source::{ federation_source::FederationSourceRuntime, graphql_source::GraphQLSourceRuntime, + mock_source::MockedSourceRuntime, runtime::{SourceError, SourceRuntime}, }, }; #[derive(Debug)] pub struct ConductorGatewayRouteData { + pub endpoint: String, + pub tenant_id: u32, pub plugin_manager: Arc, pub to: Arc>, } @@ -38,7 +48,7 @@ pub struct ConductorGateway { #[derive(Debug, thiserror::Error)] pub enum GatewayError { #[error("failed to initialize plugins manager")] - PluginManagerInitError, + PluginManagerInitError(PluginError), #[error("failed to match route to endpoint: \"{0}\"")] MissingEndpoint(String), #[error("failed to locate source named \"{0}\", or it's not configured correctly.")] @@ -58,19 +68,24 @@ impl ConductorGateway { fn create_source(lookup: &str, def: &SourceDefinition) -> Option> { match def { - SourceDefinition::GraphQL { id, config } if id == lookup => { - Some(Box::new(GraphQLSourceRuntime::new(config.clone()))) - } - SourceDefinition::Federation { id, config } if id == lookup => { - Some(Box::new(FederationSourceRuntime::new(config.clone()))) - } + SourceDefinition::GraphQL { id, config } if id == lookup => Some(Box::new( + GraphQLSourceRuntime::new(id.clone(), config.clone()), + )), + SourceDefinition::Federation { id, config } if id == lookup => Some(Box::new( + FederationSourceRuntime::new(id.clone(), config.clone()), + )), + SourceDefinition::Mock { id, config } if id == lookup => Some(Box::new( + MockedSourceRuntime::new(id.clone(), config.clone()), + )), _ => None, } } async fn construct_endpoint( + tenant_id: u32, config_object: &ConductorConfig, endpoint_config: &EndpointDefinition, + tracing_manager: &mut Option<&mut MinitraceManager>, ) -> Result { let global_plugins = &config_object.plugins; let combined_plugins = global_plugins @@ -80,9 +95,9 @@ impl ConductorGateway { .cloned() .collect::>(); - let plugin_manager = PluginManager::new(&Some(combined_plugins)) + let plugin_manager = PluginManager::new(&Some(combined_plugins), tracing_manager, tenant_id) .await - .map_err(|_| GatewayError::PluginManagerInitError)?; + .map_err(GatewayError::PluginManagerInitError)?; let upstream_source: Box = config_object .sources @@ -91,31 +106,40 @@ impl ConductorGateway { .ok_or(GatewayError::MissingSource(endpoint_config.from.clone()))?; let route_data = ConductorGatewayRouteData { + endpoint: endpoint_config.path.clone(), to: Arc::new(upstream_source), plugin_manager: Arc::new(plugin_manager), + tenant_id, }; Ok(route_data) } - pub async fn new(config_object: &ConductorConfig) -> Result { - let route_mapping = join_all( - config_object - .endpoints - .iter() - .map(move |endpoint_config| async move { - match Self::construct_endpoint(config_object, endpoint_config).await { - Ok(route_data) => ConductorGatewayRoute { - base_path: endpoint_config.path.clone(), - route_data: Arc::new(route_data), - }, - // @expected: if we are unable to construct the endpoints and attach them onto the gateway's http server, we have to exit - Err(e) => panic!("failed to construct endpoint: {:?}", e), - } - }) - .collect::>(), - ) - .await; + pub async fn new( + config_object: &ConductorConfig, + tracing_manager: &mut Option<&mut MinitraceManager>, + ) -> Result { + let mut route_mapping: Vec = vec![]; + + for (index, endpoint_config) in config_object.endpoints.iter().enumerate() { + let route_data = match Self::construct_endpoint( + index.try_into().unwrap(), + config_object, + endpoint_config, + tracing_manager, + ) + .await + { + Ok(route_data) => ConductorGatewayRoute { + base_path: endpoint_config.path.clone(), + route_data: Arc::new(route_data), + }, + // @expected: if we are unable to construct the endpoints and attach them onto the gateway's http server, we have to exit + Err(e) => panic!("failed to construct endpoint: {:?}", e), + }; + + route_mapping.push(route_data); + } Ok(Self { routes: route_mapping, @@ -130,8 +154,10 @@ impl ConductorGateway { ) -> ConductorHttpResponse { let plugin_manager = PluginManager::new_from_vec(plugins); let route_data = ConductorGatewayRouteData { + endpoint: "/".to_string(), plugin_manager: Arc::new(plugin_manager), to: source, + tenant_id: 0, }; let gw = Self { routes: vec![ConductorGatewayRoute { @@ -144,7 +170,7 @@ impl ConductorGateway { ConductorGateway::execute(request, &gw.routes[0].route_data).await } - #[tracing::instrument(skip(request, route_data), name = "ConductorGateway::execute")] + #[trace(name = "execute")] pub async fn execute( request: ConductorHttpRequest, route_data: &ConductorGatewayRouteData, @@ -176,7 +202,6 @@ impl ConductorGateway { if request_ctx.downstream_graphql_request.is_none() && request_ctx.downstream_http_request.method == Method::POST { - debug!("captured POST request, trying to handle as GraphQL POST flow"); let (_, accept, result) = GraphQLRequest::new_from_http_post(&request_ctx.downstream_http_request); @@ -211,55 +236,75 @@ impl ConductorGateway { } } - // Step 2.5: In case of invalid request at this point, we can fail and return an error. - if request_ctx.has_failed_extraction() { - return ConductorHttpResponse { - body: GraphQLResponse::new_error("failed to extract GraphQL request from HTTP request") - .into(), - status: StatusCode::BAD_REQUEST, - headers: Default::default(), - }; - } + // Verify that we have a GraphQL request at this point. + match request_ctx.downstream_graphql_request.as_ref() { + Some(gql_operation) => { + let mut _graphql_span = create_graphql_span(gql_operation); - // Step 3: Execute plugins on the extracted GraphQL request. - route_data - .plugin_manager - .on_downstream_graphql_request(&mut request_ctx) - .await; - - // Step 3.5: In case of short circuit, return the response right now. - if request_ctx.is_short_circuit() { - if let Some(mut sc_response) = request_ctx.short_circuit_response.take() { + // Step 3: Execute plugins on the extracted GraphQL request. route_data .plugin_manager - .on_downstream_http_response(&mut request_ctx, &mut sc_response); + .on_downstream_graphql_request(&mut request_ctx) + .await; - return sc_response; - } else { - return ExtractGraphQLOperationError::FailedToCreateResponseBody.into_response(None); - } - } + // Step 3.5: In case of short circuit, return the response right now. + if request_ctx.is_short_circuit() { + if let Some(mut sc_response) = request_ctx.short_circuit_response.take() { + route_data + .plugin_manager + .on_downstream_http_response(&mut request_ctx, &mut sc_response); - let upstream_response = route_data.to.execute(route_data, &mut request_ctx).await; - let final_response = match upstream_response { - Ok(response) => response, - Err(e) => match e { - SourceError::ShortCircuit => { - return match request_ctx.short_circuit_response { - Some(e) => e, - None => ExtractGraphQLOperationError::FailedToCreateResponseBody.into_response(None), + return sc_response; + } else { + return ExtractGraphQLOperationError::FailedToCreateResponseBody.into_response(None); } } - e => e.into(), - }, - }; - let mut http_response: ConductorHttpResponse = final_response.into(); + let upstream_span = Span::enter_with_parent("upstream_call", &_graphql_span) + .with_property(|| (CONDUCTOR_SOURCE, route_data.to.name().to_string())); + let upstream_response = route_data + .to + .execute(route_data, &mut request_ctx) + .in_span(upstream_span) + .await; + + let final_response = match upstream_response { + Ok(response) => response, + Err(e) => match e { + SourceError::ShortCircuit => { + return match request_ctx.short_circuit_response { + Some(e) => e, + None => { + ExtractGraphQLOperationError::FailedToCreateResponseBody.into_response(None) + } + } + } + e => e.into(), + }, + }; + + if let Some(errors) = final_response.errors.as_ref() { + _graphql_span = + _graphql_span.with_properties(|| create_graphql_error_span_properties(errors)); + } + + let mut http_response: ConductorHttpResponse = final_response.into(); - route_data - .plugin_manager - .on_downstream_http_response(&mut request_ctx, &mut http_response); + route_data + .plugin_manager + .on_downstream_http_response(&mut request_ctx, &mut http_response); - http_response + http_response + } + None => { + // Step 2.5: In case of invalid request at this point, we can fail and return an error. + ConductorHttpResponse { + body: GraphQLResponse::new_error("failed to extract GraphQL request from HTTP request") + .into(), + status: StatusCode::BAD_REQUEST, + headers: Default::default(), + } + } + } } } diff --git a/libs/engine/src/plugin_manager.rs b/libs/engine/src/plugin_manager.rs index 67c5d133..3f685d68 100644 --- a/libs/engine/src/plugin_manager.rs +++ b/libs/engine/src/plugin_manager.rs @@ -5,7 +5,9 @@ use conductor_common::{ plugin::{CreatablePlugin, Plugin, PluginError}, }; use conductor_config::PluginDefinition; -use reqwest::{Error, Response}; +use conductor_tracing::minitrace_mgr::MinitraceManager; +use reqwest::Response; + #[derive(Debug, Default)] pub struct PluginManager { plugins: Vec>, @@ -23,18 +25,20 @@ impl PluginManager { pm } - pub async fn create_plugin( - config: T::Config, - ) -> Result, PluginError> { + pub async fn create_plugin(config: T::Config) -> Result, PluginError> { T::create(config).await } - pub async fn new(plugins_config: &Option>) -> Result { + pub async fn new( + plugins_config: &Option>, + tracing_manager: &mut Option<&mut MinitraceManager>, + tenant_id: u32, + ) -> Result { let mut instance = PluginManager::default(); if let Some(config_defs) = plugins_config { for plugin_def in config_defs.iter() { - let plugin = match plugin_def { + let plugin: Box = match plugin_def { PluginDefinition::GraphiQLPlugin { enabled: Some(true), config, @@ -76,6 +80,21 @@ impl PluginManager { enabled: Some(true), config, } => Self::create_plugin::(config.clone()).await?, + PluginDefinition::TelemetryPlugin { + enabled: Some(true), + config, + } => { + if tracing_manager.is_some() { + let plugin = Self::create_plugin::(config.clone()).await?; + plugin.configure_tracing(tenant_id, tracing_manager.as_mut().unwrap())?; + + plugin + } else { + return Err(PluginError::PluginNotSupportedInRuntime { + name: "telemetry".to_string(), + }); + } + } // In case plugin is not enabled, we are skipping it. Also when we don't have a match, so watch out for this one if you add a new plugin. _ => continue, }; @@ -84,7 +103,7 @@ impl PluginManager { } }; - // We want to make sure to register this one last, in order to ensure it's setting the value correctly + // We want to make sure to register these last, in order to ensure it's setting the value correctly for p in PluginManager::default_plugins() { instance.register_boxed_plugin(p); } @@ -104,7 +123,12 @@ impl PluginManager { self.plugins.push(Box::new(plugin)); } - #[tracing::instrument(level = "debug", skip(self, context))] + #[tracing::instrument( + level = "debug", + skip(self, context), + name = "on_downstream_http_request" + )] + #[inline] pub async fn on_downstream_http_request(&self, context: &mut RequestExecutionContext) { let p = &self.plugins; @@ -117,7 +141,12 @@ impl PluginManager { } } - #[tracing::instrument(level = "debug", skip(self, context, response))] + #[tracing::instrument( + level = "debug", + skip(self, context, response), + name = "on_downstream_http_response" + )] + #[inline] pub fn on_downstream_http_response( &self, context: &mut RequestExecutionContext, @@ -134,7 +163,12 @@ impl PluginManager { } } - #[tracing::instrument(level = "debug", skip(self, context))] + #[tracing::instrument( + level = "debug", + skip(self, context), + name = "on_downstream_graphql_request" + )] + #[inline] pub async fn on_downstream_graphql_request(&self, context: &mut RequestExecutionContext) { let p = &self.plugins; @@ -147,7 +181,8 @@ impl PluginManager { } } - #[tracing::instrument(level = "debug", skip(self, req))] + #[tracing::instrument(level = "debug", skip(self, req), name = "on_upstream_graphql_request")] + #[inline] pub async fn on_upstream_graphql_request<'a>(&self, req: &mut GraphQLRequest) { let p = &self.plugins; @@ -156,7 +191,12 @@ impl PluginManager { } } - #[tracing::instrument(level = "debug", skip(self, ctx, request))] + #[tracing::instrument( + level = "debug", + skip(self, ctx, request), + name = "on_upstream_http_request" + )] + #[inline] pub async fn on_upstream_http_request<'a>( &self, ctx: &mut RequestExecutionContext, @@ -173,11 +213,16 @@ impl PluginManager { } } - #[tracing::instrument(level = "debug", skip(self, ctx, response))] + #[tracing::instrument( + level = "debug", + skip(self, ctx, response), + name = "on_upstream_http_response" + )] + #[inline] pub async fn on_upstream_http_response<'a>( &self, ctx: &mut RequestExecutionContext, - response: &Result, + response: &Result, ) { let p = &self.plugins; diff --git a/libs/engine/src/source/federation_source.rs b/libs/engine/src/source/federation_source.rs index 3c1c040e..d5b54ea1 100644 --- a/libs/engine/src/source/federation_source.rs +++ b/libs/engine/src/source/federation_source.rs @@ -6,11 +6,14 @@ use conductor_common::graphql::GraphQLResponse; use conductor_config::{FederationSourceConfig, SupergraphSourceConfig}; use federation_query_planner::execute_federation; use federation_query_planner::supergraph::{parse_supergraph, Supergraph}; +use minitrace_reqwest::{traced_reqwest, TracedHttpClient}; use std::collections::HashMap; use std::{future::Future, pin::Pin}; #[derive(Debug)] pub struct FederationSourceRuntime { + pub client: TracedHttpClient, + pub identifier: String, pub config: FederationSourceConfig, pub supergraph: Supergraph, } @@ -99,14 +102,23 @@ pub fn load_supergraph( "Registered supergraph schema fetch interval to update every: {:?}", interval ); + let client = wasm_polyfills::create_http_client() + .build() + .unwrap_or_else(|_| { + // @expected: without a fetcher, there's no executor, without an executor, there's no gateway. + panic!("Failed while initializing the executor's fetcher for Federation source"); + }); + let mut runtime = FederationSourceRuntime { + client: traced_reqwest(client), + identifier: "test".to_string(), config: config.clone(), supergraph: supergraph.clone(), }; let url = url.clone(); let headers = headers.clone(); - let interval_spawn = interval.clone(); + let interval_spawn = *interval; tokio::spawn(async move { runtime .start_periodic_fetch(url, headers, interval_spawn) @@ -120,13 +132,27 @@ pub fn load_supergraph( } impl FederationSourceRuntime { - pub fn new(config: FederationSourceConfig) -> Self { + pub fn new(identifier: String, config: FederationSourceConfig) -> Self { + let client = wasm_polyfills::create_http_client() + .build() + .unwrap_or_else(|_| { + // @expected: without a fetcher, there's no executor, without an executor, there's no gateway. + panic!("Failed while initializing the executor's fetcher for Federation source"); + }); + + let fetcher = traced_reqwest(client); + let supergraph = match load_supergraph(&config) { Ok(e) => e, Err(e) => panic!("{e}"), }; - Self { config, supergraph } + Self { + client: fetcher, + identifier, + config, + supergraph, + } } pub async fn update_supergraph(&mut self, new_schema: String) { @@ -158,6 +184,10 @@ impl FederationSourceRuntime { } impl SourceRuntime for FederationSourceRuntime { + fn name(&self) -> &str { + &self.identifier + } + fn execute<'a>( &'a self, _route_data: &'a ConductorGatewayRouteData, @@ -179,7 +209,7 @@ impl SourceRuntime for FederationSourceRuntime { let operation = downstream_request.parsed_operation; - match execute_federation(&self.supergraph, operation).await { + match execute_federation(&self.client, &self.supergraph, operation).await { Ok((response_data, query_plan)) => { let mut response = serde_json::from_str::(&response_data).unwrap(); diff --git a/libs/engine/src/source/graphql_source.rs b/libs/engine/src/source/graphql_source.rs index 064edb18..2bf42d93 100644 --- a/libs/engine/src/source/graphql_source.rs +++ b/libs/engine/src/source/graphql_source.rs @@ -6,7 +6,8 @@ use conductor_common::{ http::{ConductorHttpRequest, CONTENT_TYPE}, }; use conductor_config::GraphQLSourceConfig; -use reqwest::{header::HeaderValue, Client, Method, StatusCode}; +use minitrace_reqwest::{traced_reqwest, TracedHttpClient}; +use reqwest::{header::HeaderValue, Method, StatusCode}; use tracing::debug; use crate::gateway::ConductorGatewayRouteData; @@ -15,13 +16,14 @@ use super::runtime::{SourceError, SourceRuntime}; #[derive(Debug)] pub struct GraphQLSourceRuntime { - pub fetcher: Client, + pub fetcher: TracedHttpClient, pub config: GraphQLSourceConfig, + pub identifier: String, } impl GraphQLSourceRuntime { - pub fn new(config: GraphQLSourceConfig) -> Self { - let fetcher = wasm_polyfills::create_http_client() + pub fn new(identifier: String, config: GraphQLSourceConfig) -> Self { + let client = wasm_polyfills::create_http_client() .build() .unwrap_or_else(|_| { // @expected: without a fetcher, there's no executor, without an executor, there's no gateway. @@ -31,11 +33,21 @@ impl GraphQLSourceRuntime { ) }); - Self { fetcher, config } + let fetcher = traced_reqwest(client); + + Self { + identifier, + fetcher, + config, + } } } impl SourceRuntime for GraphQLSourceRuntime { + fn name(&self) -> &str { + &self.identifier + } + fn execute<'a>( &'a self, route_data: &'a ConductorGatewayRouteData, @@ -59,6 +71,7 @@ impl SourceRuntime for GraphQLSourceRuntime { .on_upstream_graphql_request(source_req) .await; + // TODO: improve this by implementing https://github.com/the-guild-org/conductor-t2/issues/205 let mut conductor_http_request = ConductorHttpRequest { body: source_req.into(), uri: endpoint.to_string(), @@ -81,7 +94,7 @@ impl SourceRuntime for GraphQLSourceRuntime { } debug!( - "going to send upstream request from the following input: {:?}", + "dispatching upstream http request from the following input: {:?}", conductor_http_request ); @@ -105,7 +118,7 @@ impl SourceRuntime for GraphQLSourceRuntime { Err(e) => return Ok(GraphQLResponse::new_error(&e.to_string())), }; - // DOTAN: Yassin, should we use the improved JSON parser here? + // DOTAN: Should we use the improved JSON parser here? let response = match serde_json::from_slice::(&body) { Ok(response) => response, Err(e) => { diff --git a/libs/engine/src/source/mock_source.rs b/libs/engine/src/source/mock_source.rs index ca193bfa..a300bd7b 100644 --- a/libs/engine/src/source/mock_source.rs +++ b/libs/engine/src/source/mock_source.rs @@ -6,15 +6,20 @@ use super::runtime::SourceRuntime; #[derive(Debug)] pub struct MockedSourceRuntime { pub config: MockedSourceConfig, + pub identifier: String, } impl MockedSourceRuntime { - pub fn new(config: MockedSourceConfig) -> Self { - Self { config } + pub fn new(identifier: String, config: MockedSourceConfig) -> Self { + Self { config, identifier } } } impl SourceRuntime for MockedSourceRuntime { + fn name(&self) -> &str { + &self.identifier + } + fn execute<'a>( &'a self, _route_data: &'a crate::gateway::ConductorGatewayRouteData, diff --git a/libs/engine/src/source/runtime.rs b/libs/engine/src/source/runtime.rs index 871a4aa3..20cbde11 100644 --- a/libs/engine/src/source/runtime.rs +++ b/libs/engine/src/source/runtime.rs @@ -1,17 +1,18 @@ use std::{fmt::Debug, future::Future, pin::Pin}; +use crate::gateway::ConductorGatewayRouteData; use conductor_common::{ execute::RequestExecutionContext, graphql::GraphQLResponse, http::StatusCode, }; -use crate::gateway::ConductorGatewayRouteData; - pub trait SourceRuntime: Debug + Send + Sync + 'static { fn execute<'a>( &'a self, _route_data: &'a ConductorGatewayRouteData, _request_context: &'a mut RequestExecutionContext, ) -> Pin> + 'a)>>; + + fn name(&self) -> &str; } #[derive(thiserror::Error, Debug)] @@ -21,13 +22,24 @@ pub enum SourceError { #[error("short circuit")] ShortCircuit, #[error("network error: {0}")] - NetworkError(reqwest::Error), + NetworkError(reqwest_middleware::Error), #[error("upstream planning error: {0}")] UpstreamPlanningError(anyhow::Error), } +impl SourceError { + pub fn http_status_code(&self) -> StatusCode { + match self { + Self::UnexpectedHTTPStatusError(_) => StatusCode::BAD_GATEWAY, + Self::ShortCircuit => StatusCode::INTERNAL_SERVER_ERROR, + Self::NetworkError(_) => StatusCode::BAD_GATEWAY, + Self::UpstreamPlanningError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + impl From for GraphQLResponse { fn from(error: SourceError) -> Self { - GraphQLResponse::new_error(&error.to_string()) + GraphQLResponse::new_error_with_code(&error.to_string(), error.http_status_code()) } } diff --git a/libs/federation_query_planner/Cargo.toml b/libs/federation_query_planner/Cargo.toml index 25ac4dfe..e6587447 100644 --- a/libs/federation_query_planner/Cargo.toml +++ b/libs/federation_query_planner/Cargo.toml @@ -8,6 +8,8 @@ bench = false [dependencies] serde = { workspace = true } +wasm_polyfills = { path = "../wasm_polyfills" } +conductor_tracing = { path = "../tracing" } serde_json = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } @@ -18,6 +20,8 @@ futures = { workspace = true } lazy_static = "1.4.0" tracing = { workspace = true } async-graphql = { version = "7.0.1", features = ["dynamic-schema"] } +minitrace = { workspace = true } +minitrace_reqwest = { path = "../minitrace_reqwest" } [dev-dependencies] insta = { version = "1.34.0", features = ["yaml", "json"] } diff --git a/libs/federation_query_planner/src/executor.rs b/libs/federation_query_planner/src/executor.rs index eba5e9b8..a9ebeda3 100644 --- a/libs/federation_query_planner/src/executor.rs +++ b/libs/federation_query_planner/src/executor.rs @@ -9,15 +9,13 @@ use anyhow::{anyhow, Result}; use async_graphql::{dynamic::*, Error, Value}; use futures::future::join_all; use futures::Future; -use lazy_static::lazy_static; +use minitrace::future::FutureExt; +use minitrace::Span; use serde::{Deserialize, Serialize}; use serde_json::Value as SerdeValue; -lazy_static! { - static ref CLIENT: reqwest::Client = reqwest::Client::new(); -} - pub async fn execute_query_plan( + client: &minitrace_reqwest::TracedHttpClient, query_plan: &QueryPlan, supergraph: &Supergraph, ) -> Result>> { @@ -26,7 +24,7 @@ pub async fn execute_query_plan( for step in &query_plan.parallel_steps { match step { Parallel::Sequential(query_steps) => { - let future = execute_sequential(query_steps, supergraph); + let future = execute_sequential(client, query_steps, supergraph); all_futures.push(future); } } @@ -41,6 +39,7 @@ pub async fn execute_query_plan( } async fn execute_sequential( + client: &minitrace_reqwest::TracedHttpClient, query_steps: &Vec, supergraph: &Supergraph, ) -> Result> { @@ -48,7 +47,7 @@ async fn execute_sequential( let mut entity_arguments: Option = None; for (i, query_step) in query_steps.iter().enumerate() { - let data = execute_query_step(query_step, supergraph, entity_arguments.clone()).await; + let data = execute_query_step(client, query_step, supergraph, entity_arguments.clone()).await; match data { Ok(data) => { @@ -193,6 +192,7 @@ fn dynamically_build_schema_from_supergraph(supergraph: &Supergraph) -> Schema { } async fn execute_query_step( + client: &minitrace_reqwest::TracedHttpClient, query_step: &QueryStep, supergraph: &Supergraph, entity_arguments: Option, @@ -220,6 +220,13 @@ async fn execute_query_step( extensions: None, }) } else { + let span = Span::enter_with_local_parent(format!("subgraph {}", query_step.service_name)) + .with_properties(|| { + [ + ("service_name", query_step.service_name.clone()), + ("graphql.document", query_step.query.clone()), + ] + }); let url = supergraph.subgraphs.get(&query_step.service_name).unwrap(); let variables_object = if let Some(arguments) = &entity_arguments { @@ -228,7 +235,8 @@ async fn execute_query_step( SerdeValue::Object(serde_json::Map::new()) }; - let response = match CLIENT + // TODO: improve this by implementing https://github.com/the-guild-org/conductor-t2/issues/205 + let response = match client .post(url) .header("Content-Type", "application/json") .body( @@ -239,6 +247,7 @@ async fn execute_query_step( .to_string(), ) .send() + .in_span(span) .await { Ok(resp) => resp, diff --git a/libs/federation_query_planner/src/lib.rs b/libs/federation_query_planner/src/lib.rs index 837e1193..2098c3c8 100644 --- a/libs/federation_query_planner/src/lib.rs +++ b/libs/federation_query_planner/src/lib.rs @@ -19,6 +19,7 @@ pub mod type_merge; pub mod user_query; pub async fn execute_federation( + client: &minitrace_reqwest::TracedHttpClient, supergraph: &Supergraph, parsed_user_query: Document<'static, String>, ) -> Result<(String, QueryPlan)> { @@ -28,7 +29,7 @@ pub async fn execute_federation( // println!("query plan: {:#?}", query_plan); - let response_vec = execute_query_plan(&query_plan, supergraph).await?; + let response_vec = execute_query_plan(client, &query_plan, supergraph).await?; // println!("response: {:#?}", json!(response_vec).to_string()); @@ -327,7 +328,7 @@ type User let supergraph = parse_supergraph(&supergraph_schema).unwrap(); let mut user_query = parse_user_query(graphql_parser::parse_query(query).unwrap()).unwrap(); - let query_plan = plan_for_user_query(&supergraph, &mut user_query).unwrap(); + let _query_plan = plan_for_user_query(&supergraph, &mut user_query).unwrap(); // TODO: fix ordering, it fails bc ordering of fields in a query plan is dynamic // insta::assert_json_snapshot!(query_plan); diff --git a/libs/federation_query_planner/src/query_planner.rs b/libs/federation_query_planner/src/query_planner.rs index 07d4caee..382299a9 100644 --- a/libs/federation_query_planner/src/query_planner.rs +++ b/libs/federation_query_planner/src/query_planner.rs @@ -92,7 +92,12 @@ fn build_intermediate_structure( let (graphql_type_name, fragment) = fragments .iter() .find(|(key, _)| key == &fragment_name) - .unwrap_or_else(|| panic!("{} fragment is not defined in your query!", fragment_name)); + .unwrap_or_else(|| { + panic!( + "fragment named \"{}\" is not defined in your query!", + fragment_name + ) + }); let mut fragment_fields = fragment.fields.clone(); let next_gql_type: &GraphQLType = match supergraph.types.get(graphql_type_name) { diff --git a/libs/logger/Cargo.toml b/libs/logger/Cargo.toml new file mode 100644 index 00000000..adfecc2a --- /dev/null +++ b/libs/logger/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "conductor_logger" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +schemars = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true, features = [ + 'time', + 'json', + 'env-filter', +] } +atty = "0.2.14" +tracing-web = "0.1.3" diff --git a/libs/logger/src/config.rs b/libs/logger/src/config.rs new file mode 100644 index 00000000..9a9c51e4 --- /dev/null +++ b/libs/logger/src/config.rs @@ -0,0 +1,72 @@ +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; + +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema, PartialEq)] +/// A source definition for a GraphQL endpoint or a federated GraphQL implementation. +pub enum LoggerConfigFormat { + /// This logging format outputs minimal, compact logs. It focuses on the essential parts of the log message and its fields, making it suitable for production environments where performance and log size are crucial. + /// + /// Pros: + /// + /// - Efficient in terms of space and performance. + /// + /// - Easy to read for brief messages and simple logs. + /// + /// Cons: + /// + /// - May lack detailed context, making debugging a bit more challenging. + #[serde(rename = "compact")] + #[schemars(title = "compact")] + Compact, + + /// The pretty format is designed for enhanced readability, featuring more verbose output including well-formatted fields and context. Ideal for development and debugging purposes. + /// + /// Pros: + /// + /// - Highly readable and provides detailed context. + /// + /// - Easier to understand complex log messages. + /// + /// Cons: + /// + /// - More verbose, resulting in larger log sizes. + /// + /// - Potentially slower performance due to the additional formatting overhead. + #[serde(rename = "pretty")] + #[schemars(title = "pretty")] + Pretty, + + /// This format outputs logs in JSON. It is particularly useful when integrating with tools that consume or process JSON logs, such as log aggregators and analysis systems. + /// + /// Pros: + /// + /// - Structured format makes it easy to parse and integrate with various tools. + /// + /// - Consistent and predictable output. + /// + /// Cons: + /// + /// - Can be verbose and harder to read directly by developers. + /// + /// - Slightly more overhead compared to simpler formats like compact. + #[serde(rename = "json")] + #[schemars(title = "json")] + Json, +} + +impl Default for LoggerConfigFormat { + // In development, we wish to see some more details and code locations. + #[cfg(debug_assertions)] + fn default() -> Self { + LoggerConfigFormat::Pretty + } + + #[cfg(not(debug_assertions))] + fn default() -> Self { + if atty::is(atty::Stream::Stdout) { + LoggerConfigFormat::Compact + } else { + LoggerConfigFormat::Json + } + } +} diff --git a/libs/logger/src/lib.rs b/libs/logger/src/lib.rs new file mode 100644 index 00000000..ce25e07c --- /dev/null +++ b/libs/logger/src/lib.rs @@ -0,0 +1,2 @@ +pub mod config; +pub mod logger_layer; diff --git a/libs/logger/src/logger_layer.rs b/libs/logger/src/logger_layer.rs new file mode 100644 index 00000000..25e07ae7 --- /dev/null +++ b/libs/logger/src/logger_layer.rs @@ -0,0 +1,83 @@ +use crate::config::LoggerConfigFormat; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::Registry; +use tracing_subscriber::{ + fmt::{self, time::UtcTime}, + Layer, +}; + +#[cfg(target_arch = "wasm32")] +pub fn build_logger( + format: &LoggerConfigFormat, + filter: &str, + print_performance_info: bool, +) -> Result + Send + Sync>, tracing_subscriber::filter::ParseError> { + // TL;DR: WASM logger config + // ANSI is not supported in all log processors, so we can disable it. + // We are using a custom timer because std::time is not available in WASM. + // Writer is configured to MakeWebConsoleWriter so all logs will go to JS `console`. + + let timer = UtcTime::rfc_3339(); + let filter = EnvFilter::try_new(filter)?; + + if print_performance_info { + println!("Logger flag \"print_performance_info\" is not supported in WASM runtime, ignoring."); + } + + Ok(match format { + LoggerConfigFormat::Json => fmt::Layer::::default() + .json() + .with_ansi(false) + .with_timer(timer) + .with_writer(tracing_web::MakeWebConsoleWriter::new()) + .with_filter(filter) + .boxed(), + LoggerConfigFormat::Pretty => fmt::Layer::::default() + .pretty() + .with_timer(timer) + .with_writer(tracing_web::MakeWebConsoleWriter::new()) + .with_filter(filter) + .boxed(), + LoggerConfigFormat::Compact => fmt::Layer::::default() + .compact() + .with_timer(timer) + .with_writer(tracing_web::MakeWebConsoleWriter::new()) + .with_filter(filter) + .boxed(), + }) +} + +#[cfg(not(target_arch = "wasm32"))] +pub fn build_logger( + format: &LoggerConfigFormat, + filter: &str, + print_performance_info: bool, +) -> Result + Send + Sync>, tracing_subscriber::filter::ParseError> { + let timer = UtcTime::rfc_3339(); + let filter = EnvFilter::try_new(filter)?; + let performance_spans = match print_performance_info { + true => tracing_subscriber::fmt::format::FmtSpan::CLOSE, + false => tracing_subscriber::fmt::format::FmtSpan::NONE, + }; + + Ok(match format { + LoggerConfigFormat::Json => fmt::Layer::::default() + .json() + .with_timer(timer) + .with_span_events(performance_spans) + .with_filter(filter) + .boxed(), + LoggerConfigFormat::Pretty => fmt::Layer::::default() + .pretty() + .with_timer(timer) + .with_span_events(performance_spans) + .with_filter(filter) + .boxed(), + LoggerConfigFormat::Compact => fmt::Layer::::default() + .compact() + .with_timer(timer) + .with_span_events(performance_spans) + .with_filter(filter) + .boxed(), + }) +} diff --git a/libs/minitrace_reqwest/Cargo.toml b/libs/minitrace_reqwest/Cargo.toml new file mode 100644 index 00000000..2ecac40d --- /dev/null +++ b/libs/minitrace_reqwest/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "minitrace_reqwest" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[dependencies] +minitrace = { workspace = true } +reqwest-middleware = { workspace = true } +conductor_tracing = { path = "../tracing" } +reqwest = { workspace = true } +async-trait = { workspace = true } +task-local-extensions = "0.1.4" diff --git a/libs/minitrace_reqwest/src/lib.rs b/libs/minitrace_reqwest/src/lib.rs new file mode 100644 index 00000000..3bed6032 --- /dev/null +++ b/libs/minitrace_reqwest/src/lib.rs @@ -0,0 +1,120 @@ +use conductor_tracing::otel_attrs::*; +use minitrace::Span; +use reqwest::{Request, Response, StatusCode}; +use reqwest_middleware::ClientBuilder; +use reqwest_middleware::ClientWithMiddleware; +use reqwest_middleware::{Error, Middleware, Next, Result}; +use task_local_extensions::Extensions; + +#[derive(Debug)] +pub struct MinitraceReqwestMiddleware; + +#[inline] +fn get_span_status(request_status: StatusCode) -> Option<&'static str> { + // adapted from: https://github.com/TrueLayer/reqwest-middleware/blob/main/reqwest-tracing/src/reqwest_otel_span_builder.rs#L149 + match request_status.as_u16() { + // Span Status MUST be left unset if HTTP status code was in the 1xx, 2xx or 3xx ranges, unless there was + // another error (e.g., network error receiving the response body; or 3xx codes with max redirects exceeded), + // in which case status MUST be set to Error. + 100..=399 => None, + // For HTTP status codes in the 4xx range span status MUST be left unset in case of SpanKind.SERVER and MUST be + // set to Error in case of SpanKind.CLIENT. + 400..=499 => Some("ERROR"), + // For HTTP status codes in the 5xx range, as well as any other code the client failed to interpret, span + // status MUST be set to Error. + _ => Some("ERROR"), + } +} + +impl MinitraceReqwestMiddleware { + #[inline] + pub fn response_properties( + &self, + res: &Result, + ) -> impl IntoIterator { + let mut properties: Vec<(&'static str, String)> = vec![]; + + match &res { + Ok(response) => { + let status_code = response.status().as_u16(); + + let span_status = get_span_status(response.status()); + if let Some(span_status) = span_status { + properties.push((OTEL_STATUS_CODE, span_status.to_string())); + properties.push((ERROR_INDICATOR, "true".to_string())); + } + properties.push((HTTP_STATUS_CODE, status_code.to_string())); + } + Err(e) => { + let error_message = e.to_string(); + let error_cause_chain = format!("{:?}", e); + properties.push((OTEL_STATUS_CODE, "ERROR".to_string())); + properties.push((ERROR_MESSAGE, error_message.to_string())); + properties.push((ERROR_INDICATOR, "true".to_string())); + properties.push((ERROR_CAUSE_CHAIN, error_cause_chain.to_string())); + + if let Error::Reqwest(e) = e { + if let Some(status) = e.status() { + properties.push((HTTP_STATUS_CODE, status.as_u16().to_string())); + } + } + } + }; + + properties + } + + #[inline] + pub fn request_properties( + &self, + req: &Request, + ) -> (String, impl IntoIterator) { + let method = req.method(); + let url = req.url(); + let scheme = url.scheme(); + let host = url.host_str().unwrap_or(""); + let host_port = url.port().unwrap_or(0) as i64; + let otel_name = format!("{} {}", method, url.path()); + + ( + otel_name, + vec![ + (HTTP_METHOD, method.to_string()), + (HTTP_SCHEME, scheme.to_string()), + (HTTP_HOST, host.to_string()), + (HTTP_URL, url.to_string()), + (NET_HOST_PORT, host_port.to_string()), + (OTEL_KIND, "client".to_string()), + (SPAN_KIND, "consumer".to_string()), + ], + ) + } +} + +#[cfg_attr(not(target_arch = "wasm32"), async_trait::async_trait)] +#[cfg_attr(target_arch = "wasm32", async_trait::async_trait(?Send))] +impl Middleware for MinitraceReqwestMiddleware { + async fn handle( + &self, + req: Request, + extensions: &mut Extensions, + next: Next<'_>, + ) -> Result { + let (span_name, properties) = self.request_properties(&req); + let mut _span_guard = Span::enter_with_local_parent(span_name).with_properties(|| properties); + + let response = next.run(req, extensions).await; + + _span_guard = _span_guard.with_properties(|| self.response_properties(&response)); + + response + } +} + +pub fn traced_reqwest(raw_client: reqwest::Client) -> TracedHttpClient { + ClientBuilder::new(raw_client) + .with(MinitraceReqwestMiddleware) + .build() +} + +pub type TracedHttpClient = ClientWithMiddleware; diff --git a/libs/tracing/Cargo.toml b/libs/tracing/Cargo.toml new file mode 100644 index 00000000..1717fc3e --- /dev/null +++ b/libs/tracing/Cargo.toml @@ -0,0 +1,26 @@ +[package] +name = "conductor_tracing" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[features] +test_utils = [] + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +schemars = { workspace = true } +tracing = { workspace = true } +conductor_common = { path = "../common" } +wasm_polyfills = { path = "../wasm_polyfills" } +opentelemetry = { version = "0.21.0" } +opentelemetry_sdk = { version = "0.21.1", features = ["trace"] } +tracing-opentelemetry = "0.22.0" +reqwest = { workspace = true } +reqwest-middleware = { workspace = true } +task-local-extensions = "0.1.4" +minitrace = { workspace = true } +rand = "0.8.5" diff --git a/libs/tracing/src/lib.rs b/libs/tracing/src/lib.rs new file mode 100644 index 00000000..391dff05 --- /dev/null +++ b/libs/tracing/src/lib.rs @@ -0,0 +1,3 @@ +pub mod minitrace_mgr; +pub mod otel_attrs; +pub mod otel_utils; diff --git a/libs/tracing/src/minitrace_mgr.rs b/libs/tracing/src/minitrace_mgr.rs new file mode 100644 index 00000000..3279026d --- /dev/null +++ b/libs/tracing/src/minitrace_mgr.rs @@ -0,0 +1,183 @@ +use std::collections::HashMap; + +use minitrace::collector::{Reporter, SpanRecord, TraceId}; + +#[derive(Default)] +pub struct MinitraceManager { + reporters: HashMap>, +} + +impl std::fmt::Debug for MinitraceManager { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("MinitraceManager").finish() + } +} + +impl MinitraceManager { + pub fn add_reporter(&mut self, tenant_id: u32, reporter: Box) { + self.reporters.insert(tenant_id, reporter); + } + + pub fn generate_trace_id(tenant_id: u32) -> TraceId { + let uniq: u32 = rand::random(); + + TraceId(((tenant_id as u128) << 32) | (uniq as u128)) + } + + pub fn extract_tenant_id(trace_id: TraceId) -> u32 { + (trace_id.0 >> 32) as u32 + } + + pub fn build_reporter(self) -> impl Reporter { + let mut routed_reporter = + RoutedReporter::new(|span| Some(Self::extract_tenant_id(span.trace_id))); + + for (tenant_id, reporter) in self.reporters { + routed_reporter = routed_reporter.with_reporter(tenant_id, reporter); + } + + routed_reporter + } +} + +type RouterFn = fn(&SpanRecord) -> Option; + +struct RoutedReporter { + reporters: HashMap>, + router_fn: RouterFn, +} + +impl RoutedReporter { + pub fn new(router_fn: RouterFn) -> Self { + Self { + reporters: HashMap::new(), + router_fn, + } + } + + pub fn with_reporter(mut self, tenant_id: u32, reporter: Box) -> Self { + self.reporters.insert(tenant_id, reporter); + + self + } +} + +impl Reporter for RoutedReporter { + fn report(&mut self, spans: &[SpanRecord]) { + let mut chunks: HashMap> = HashMap::new(); + + for span in spans { + if let Some(key) = (self.router_fn)(span) { + let chunk = chunks.entry(key).or_default(); + chunk.push(span.clone()); + } else { + tracing::warn!("no key for span: {:?}, dropping span", span); + } + } + + for (key, chunk) in chunks { + if let Some(reporter) = self.reporters.get_mut(&key) { + reporter.report(chunk.as_slice()); + } + } + } +} + +#[cfg(feature = "test_utils")] +pub mod test_utils { + use std::sync::{Arc, Mutex}; + + use minitrace::collector::{Reporter, SpanRecord}; + + pub struct TestReporter { + captured_spans: Arc>>, + } + + impl TestReporter { + pub fn new() -> (Arc>>, Self) { + let spans: Arc>> = Arc::new(Mutex::new(vec![])); + + ( + spans.clone(), + Self { + captured_spans: spans, + }, + ) + } + } + + impl Reporter for TestReporter { + fn report(&mut self, spans: &[SpanRecord]) { + for span in spans.iter() { + self.captured_spans.lock().unwrap().push(span.clone()); + } + } + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn routed_reporter() { + let (spans0, reporter0) = test_utils::TestReporter::new(); + let (spans1, reporter1) = test_utils::TestReporter::new(); + let mut routed_reporter = + RoutedReporter::new(|span| Some(MinitraceManager::extract_tenant_id(span.trace_id))) + .with_reporter(0, Box::new(reporter0)) + .with_reporter(1, Box::new(reporter1)); + + routed_reporter.report(&vec![ + // This one goes to tenant 2, it does not exists + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(2), + ..Default::default() + }, + // This one goes to tenant 0 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(0), + ..Default::default() + }, + // This one goes to tenant 0 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(0), + ..Default::default() + }, + // This one goes to tenant 1 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(1), + ..Default::default() + }, + // This one goes to tenant 2, it does not exists + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(2), + ..Default::default() + }, + ]); + + routed_reporter.report(&vec![ + // This one goes to tenant 1 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(1), + ..Default::default() + }, + // This one goes to tenant 1 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(1), + ..Default::default() + }, + // This one goes to tenant 2 + SpanRecord { + trace_id: MinitraceManager::generate_trace_id(2), + ..Default::default() + }, + ]); + + let spans0 = spans0.lock().unwrap(); + let spans1 = spans1.lock().unwrap(); + + assert_eq!(spans0.len(), 2); + assert_eq!(spans1.len(), 3); + } +} diff --git a/libs/tracing/src/otel_attrs.rs b/libs/tracing/src/otel_attrs.rs new file mode 100644 index 00000000..4cfc2765 --- /dev/null +++ b/libs/tracing/src/otel_attrs.rs @@ -0,0 +1,38 @@ +// https://opentelemetry.io/docs/specs/semconv/http/http-spans/ +pub const HTTP_METHOD: &str = "http.method"; +pub const HTTP_SCHEME: &str = "http.scheme"; +pub const HTTP_HOST: &str = "http.host"; +pub const HTTP_URL: &str = "http.url"; +pub const HTTP_ROUTE: &str = "http.route"; +pub const HTTP_FLAVOR: &str = "http.flavor"; +pub const HTTP_CLIENT_IP: &str = "http.client_ip"; +pub const NET_HOST_PORT: &str = "net.host.port"; +pub const OTEL_KIND: &str = "otel.kind"; +pub const SPAN_KIND: &str = "span.kind"; +pub const SPAN_TYPE: &str = "span.type"; +pub const HTTP_STATUS_CODE: &str = "http.status_code"; +pub const HTTP_USER_AGENT: &str = "http.user_agent"; +pub const HTTP_TARGET: &str = "http.target"; +pub const REQUEST_ID: &str = "request_id"; +pub const TRACE_ID: &str = "trace_id"; + +// https://opentelemetry.io/docs/specs/semconv/attributes-registry/error/ +pub const ERROR_TYPE: &str = "error.type"; +pub const ERROR_MESSAGE: &str = "error.message"; +pub const ERROR_CAUSE_CHAIN: &str = "error.cause_chain"; +pub const OTEL_STATUS_CODE: &str = "otel.status_code"; // "ERROR" / "OK" +pub const EXCEPTION_MESSAGE: &str = "exception.message"; +pub const EXCEPTION_DETAILS: &str = "exception.message"; + +// Specific to Jaeger +pub const ERROR_INDICATOR: &str = "error"; // "true" + +// https://opentelemetry.io/docs/specs/semconv/database/graphql/ +pub const GRAPHQL_DOCUMENT: &str = "graphql.document"; +pub const GRAPHQL_OPERATION_TYPE: &str = "graphql.operation.type"; +pub const GRAPHQL_OPERATION_NAME: &str = "graphql.operation.name"; +pub const GRAPHQL_ERROR_COUNT: &str = "graphql.error.count"; + +// Conductor-specific +pub const CONDUCTOR_ENDPOINT: &str = "conductor.endpoint"; +pub const CONDUCTOR_SOURCE: &str = "conductor.source"; diff --git a/libs/tracing/src/otel_utils.rs b/libs/tracing/src/otel_utils.rs new file mode 100644 index 00000000..660b21ea --- /dev/null +++ b/libs/tracing/src/otel_utils.rs @@ -0,0 +1,66 @@ +use conductor_common::graphql::GraphQLError; +use conductor_common::graphql::ParsedGraphQLRequest; +use conductor_common::Definition; +use conductor_common::OperationDefinition; +use minitrace::Span; + +use crate::otel_attrs::*; + +// Based on https://opentelemetry.io/docs/specs/semconv/database/graphql/ +#[inline] +pub fn create_graphql_span(request: &ParsedGraphQLRequest) -> Span { + let excutable_op = request.executable_operation(); + + let (op_type, op_name): (Option<&str>, Option<&String>) = match excutable_op { + Some(Definition::Operation(op)) => match op { + OperationDefinition::Query(o) => (Some("query"), o.name.as_ref()), + OperationDefinition::SelectionSet(_) => (Some("query"), None), + OperationDefinition::Mutation(o) => (Some("mutation"), o.name.as_ref()), + OperationDefinition::Subscription(o) => (Some("subscription"), o.name.as_ref()), + }, + _ => (None, None), + }; + + let otel_name = match (op_type, op_name) { + (Some(op_type), Some(op_name)) => format!("{} {}", op_type, op_name), + (Some(op_type), None) => op_type.to_string(), + _ => "GraphQL Operation".to_string(), + }; + + let mut properties: Vec<(&str, String)> = Vec::new(); + properties.push((GRAPHQL_DOCUMENT, request.request.operation.to_string())); + + if let Some(op_type) = op_type { + properties.push((GRAPHQL_OPERATION_TYPE, op_type.to_string())); + } + + if let Some(op_name) = op_name { + properties.push((GRAPHQL_OPERATION_NAME, op_name.to_string())); + } + + Span::enter_with_local_parent(otel_name).with_properties(|| properties) +} + +#[inline] +pub fn create_graphql_error_span_properties( + errors: &Vec, +) -> impl IntoIterator { + let mut properties: Vec<(&str, String)> = Vec::new(); + + if !errors.is_empty() { + properties.push((GRAPHQL_ERROR_COUNT, errors.len().to_string())); + properties.push((ERROR_TYPE, "graphql".to_string())); + properties.push((OTEL_STATUS_CODE, "ERROR".to_string())); + + let errors_str = errors + .iter() + .map(|e| e.message.clone()) + .collect::>() + .join(", "); + + properties.push((ERROR_INDICATOR, "true".to_string())); + properties.push((ERROR_MESSAGE, errors_str)); + } + + properties +} diff --git a/libs/wasm_polyfills/Cargo.toml b/libs/wasm_polyfills/Cargo.toml index f3f7bed9..ffb1968c 100644 --- a/libs/wasm_polyfills/Cargo.toml +++ b/libs/wasm_polyfills/Cargo.toml @@ -10,7 +10,7 @@ path = "src/lib.rs" reqwest = { workspace = true } [target.'cfg(target_arch = "wasm32")'.dependencies] -wasm-bindgen-futures = { version = "0.4" } +wasm-bindgen-futures = { version = "0.4.40" } send_wrapper = { version = "0.6", features = ["futures"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] diff --git a/libs/wasm_polyfills/src/lib.rs b/libs/wasm_polyfills/src/lib.rs index d6f8776f..4a91744a 100644 --- a/libs/wasm_polyfills/src/lib.rs +++ b/libs/wasm_polyfills/src/lib.rs @@ -1,11 +1,13 @@ use core::future::Future; #[cfg(target_arch = "wasm32")] +#[inline] pub fn call_async(future: impl Future) -> impl Future + Send { send_wrapper::SendWrapper::new(future) } #[cfg(not(target_arch = "wasm32"))] +#[inline] pub fn call_async(future: F) -> F where F: Future, @@ -26,3 +28,6 @@ pub fn create_http_client() -> reqwest::ClientBuilder { pub fn create_http_client() -> reqwest::ClientBuilder { reqwest::Client::builder() } + +#[cfg(target_arch = "wasm32")] +pub use wasm_bindgen_futures::spawn_local; diff --git a/plugins/cors/src/plugin.rs b/plugins/cors/src/plugin.rs index 4b7329b4..8cd31e3e 100644 --- a/plugins/cors/src/plugin.rs +++ b/plugins/cors/src/plugin.rs @@ -20,7 +20,7 @@ static ACCESS_CONTROL_ALLOW_PRIVATE_NETWORK: &str = "Access-Control-Allow-Privat impl CreatablePlugin for CorsPlugin { type Config = CorsPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { Ok(Box::new(Self(config))) } } diff --git a/plugins/disable_introspection/src/plugin.rs b/plugins/disable_introspection/src/plugin.rs index 7db0ae3f..2f14e8c4 100644 --- a/plugins/disable_introspection/src/plugin.rs +++ b/plugins/disable_introspection/src/plugin.rs @@ -24,7 +24,7 @@ pub struct DisableIntrospectionPlugin { impl CreatablePlugin for DisableIntrospectionPlugin { type Config = DisableIntrospectionPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { let instance = match &config.condition { Some(condition) => match vrl::compiler::compile(condition.contents(), &vrl_fns()) { Err(err) => { diff --git a/plugins/graphiql/src/plugin.rs b/plugins/graphiql/src/plugin.rs index f407f5df..058f2b5f 100644 --- a/plugins/graphiql/src/plugin.rs +++ b/plugins/graphiql/src/plugin.rs @@ -20,7 +20,7 @@ pub struct GraphiQLPlugin { impl CreatablePlugin for GraphiQLPlugin { type Config = GraphiQLPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { Ok(Box::new(Self { config })) } } diff --git a/plugins/http_get/src/plugin.rs b/plugins/http_get/src/plugin.rs index 61e94834..4672ff42 100644 --- a/plugins/http_get/src/plugin.rs +++ b/plugins/http_get/src/plugin.rs @@ -20,7 +20,7 @@ pub struct HttpGetPlugin(HttpGetPluginConfig); impl CreatablePlugin for HttpGetPlugin { type Config = HttpGetPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { Ok(Box::new(Self(config))) } } diff --git a/plugins/jwt_auth/src/plugin.rs b/plugins/jwt_auth/src/plugin.rs index dae94efb..cb9e3d3e 100644 --- a/plugins/jwt_auth/src/plugin.rs +++ b/plugins/jwt_auth/src/plugin.rs @@ -99,7 +99,7 @@ impl From for StatusCode { impl CreatablePlugin for JwtAuthPlugin { type Config = JwtAuthPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { let providers = config .jwks_providers .iter() diff --git a/plugins/telemetry/Cargo.toml b/plugins/telemetry/Cargo.toml new file mode 100644 index 00000000..52308530 --- /dev/null +++ b/plugins/telemetry/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "telemetry_plugin" +version = "0.0.0" +edition = "2021" + +[lib] +path = "src/lib.rs" + +[features] +test_utils = [] + +[dependencies] +serde = { workspace = true } +serde_json = { workspace = true } +async-trait = { workspace = true } +conductor_common = { path = "../../libs/common" } +conductor_tracing = { path = "../../libs/tracing" } +schemars = { workspace = true } +humantime-serde = "1.1.1" +opentelemetry = { version = "0.21.0", features = ["trace"] } +opentelemetry_sdk = { version = "0.21.2", features = ["trace"] } +minitrace = { workspace = true } + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +minitrace-datadog = "0.6.2" +minitrace-jaeger = "0.6.2" +minitrace-opentelemetry = { git = "https://github.com/dotansimha/minitrace-rust.git", rev = "2f3bad8297db0f9b980e3f69e73401ce957dccd1" } +opentelemetry-otlp = { version = "0.14.0" } diff --git a/plugins/telemetry/src/config.rs b/plugins/telemetry/src/config.rs new file mode 100644 index 00000000..05d11d9b --- /dev/null +++ b/plugins/telemetry/src/config.rs @@ -0,0 +1,145 @@ +use std::time::Duration; + +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use std::net::SocketAddr; + +#[derive(Deserialize, Serialize, Debug, Clone, Default, JsonSchema)] +/// The `telemetry` plugin exports traces information about Conductor to a telemetry backend. +/// +/// +/// +/// At the moment, this plugin is not supported on WASM (CloudFlare Worker) runtime. +/// +/// You may follow [this GitHub issue](https://github.com/the-guild-org/conductor/issues/354) for additional information. +/// +/// +/// +/// The telemetry plugin exports traces information about the following aspects of Conductor: +/// +/// - GraphQL parser (timing) +/// +/// - GraphQL execution (operation type, operation body, operation name, timing, errors) +/// +/// - Query planning (timing, operation body, operation name) +/// +/// - Incoming HTTP requests (attributes, timing, errors) +/// +/// - Outgoing HTTP requests (attributes, timing, errors) +/// +/// When used with a telemtry backend, you can expect to see the following information: +/// +/// ![img](/assets/telemetry.png) +/// +pub struct TelemetryPluginConfig { + /// Configures the service name that reports the telemetry data. This will appear in the telemetry data as the `service.name` attribute. + #[serde(default = "default_service_name")] + pub service_name: String, + /// A list of telemetry targets to send telemetry data to. + /// + /// The telemtry data is scoped per endpoint, and you can specify multiple targets if you need to export stats to multiple backends. + pub targets: Vec, +} + +fn default_service_name() -> String { + "conductor".to_string() +} + +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] +#[serde(tag = "type")] +pub enum TelemetryTarget { + /// Sends telemetry data to `stdout` in a human-readable format. + /// + /// Use this source for debugging purposes, or if you want to pipe the telemetry data to another process. + #[serde(rename = "stdout")] + #[schemars(title = "stdout")] + Stdout, + /// Sends telemetry traces data to an [OpenTelemetry](https://opentelemetry.io/) backend, using the [OTLP protocol](https://opentelemetry.io/docs/specs/otel/protocol/). + /// + /// You can find [here a list backends that supports the OTLP format](https://github.com/magsther/awesome-opentelemetry#open-source). + #[serde(rename = "otlp")] + #[schemars(title = "Open Telemetry (OTLP)")] + Otlp { + /// The OTLP backend endpoint. The format is based on full URL, e.g. `http://localhost:7201`. + endpoint: String, + #[serde(default = "default_otlp_protocol")] + /// The OTLP transport to use to export telemetry data. + protocol: OtlpProtcol, + #[serde( + deserialize_with = "humantime_serde::deserialize", + serialize_with = "humantime_serde::serialize", + default = "default_otlp_timeout" + )] + #[schemars(with = "String")] + /// Export timeout. You can use the human-readable format in this field, e.g. `10s`. + timeout: Duration, + #[serde(default)] + /// Whether to use gzip compression when sending telemetry data. + /// + /// Please verify your backend supports and enables `gzip` compression before enabling this option. + gzip_compression: bool, + }, + /// Sends telemetry traces data to a [Datadog](https://www.datadoghq.com/) agent (local or remote). + /// + /// To get started with Datadog, make sure you have a [Datadog agent running](https://docs.datadoghq.com/agent/?tab=source). + #[serde(rename = "datadog")] + #[schemars(title = "Datadog")] + Datadog { + /// The Datadog agent endpoint. The format is based on hostname and port only, e.g. `127.0.0.1:8126`. + #[serde(default = "default_datadog_agent_endpoint")] + agent_endpoint: SocketAddr, + }, + /// Sends telemetry traces data to a [Jaeger](https://www.jaegertracing.io/) backend, using the native protocol of [Jaeger (UDP) using `thrift`](https://www.jaegertracing.io/docs/next-release/getting-started/). + /// + /// > Note: Jaeger also [supports OTLP format](https://opentelemetry.io/blog/2022/jaeger-native-otlp/), so it's preferred to use the `otlp` target. + /// + /// To get started with Jaeger, make sure you have a Jaeger backend running, + /// and then use the following command to start the Jaeger backend and UI in your local machine, using Docker: + /// + /// `docker run -d -p6831:6831/udp -p6832:6832/udp -p16686:16686 jaegertracing/all-in-one:latest` + #[serde(rename = "jaeger")] + #[schemars(title = "Jaeger")] + Jaeger { + /// The UDP endpoint of the Jaeger backend. The format is based on hostname and port only, e.g. `127.0.0.1:6831`. + #[serde(default = "default_jaeger_endpoint")] + endpoint: SocketAddr, + }, +} + +fn default_jaeger_endpoint() -> SocketAddr { + "127.0.0.1:6831".parse().unwrap() +} + +fn default_datadog_agent_endpoint() -> SocketAddr { + "127.0.0.1:8126".parse().unwrap() +} + +fn default_otlp_protocol() -> OtlpProtcol { + OtlpProtcol::Grpc +} + +fn default_otlp_timeout() -> Duration { + Duration::from_secs(10) +} + +#[derive(Deserialize, Serialize, Debug, Clone, JsonSchema)] +pub enum OtlpProtcol { + /// Uses GRPC with `tonic` to send telemetry data. + #[schemars(title = "grpc")] + #[serde(rename = "grpc")] + Grpc, + /// Uses HTTP with `http-proto` to send telemetry data. + #[schemars(title = "http")] + #[serde(rename = "http")] + Http, +} + +#[cfg(not(target_arch = "wasm32"))] +impl From for opentelemetry_otlp::Protocol { + fn from(value: OtlpProtcol) -> Self { + match value { + OtlpProtcol::Grpc => opentelemetry_otlp::Protocol::Grpc, + OtlpProtcol::Http => opentelemetry_otlp::Protocol::HttpBinary, + } + } +} diff --git a/plugins/telemetry/src/lib.rs b/plugins/telemetry/src/lib.rs new file mode 100644 index 00000000..83661ce4 --- /dev/null +++ b/plugins/telemetry/src/lib.rs @@ -0,0 +1,6 @@ +mod config; +mod plugin; + +pub use config::TelemetryPluginConfig as Config; +pub use config::TelemetryTarget as Target; +pub use plugin::TelemetryPlugin as Plugin; diff --git a/plugins/telemetry/src/plugin.rs b/plugins/telemetry/src/plugin.rs new file mode 100644 index 00000000..0904252d --- /dev/null +++ b/plugins/telemetry/src/plugin.rs @@ -0,0 +1,120 @@ +use crate::config::{TelemetryPluginConfig, TelemetryTarget}; +use conductor_common::plugin::{CreatablePlugin, Plugin, PluginError}; + +use conductor_tracing::minitrace_mgr::MinitraceManager; +use minitrace::collector::Reporter; +use opentelemetry::trace::TraceError; + +#[derive(Debug)] +pub struct TelemetryPlugin { + config: TelemetryPluginConfig, +} + +#[async_trait::async_trait(?Send)] +impl CreatablePlugin for TelemetryPlugin { + type Config = TelemetryPluginConfig; + + async fn create(config: Self::Config) -> Result, PluginError> { + Ok(Box::new(Self { config })) + } +} + +#[cfg(not(target_arch = "wasm32"))] +static LIB_NAME: &str = "conductor"; + +impl TelemetryPlugin { + #[cfg(target_arch = "wasm32")] + fn compose_reporter( + _service_name: &String, + _target: &TelemetryTarget, + ) -> Result, TraceError> { + Err(TraceError::Other( + "plugin is not supported in this runtime".into(), + )) + } + + #[cfg(not(target_arch = "wasm32"))] + fn compose_reporter( + service_name: &String, + target: &TelemetryTarget, + ) -> Result, TraceError> { + use minitrace::collector::ConsoleReporter; + use minitrace_opentelemetry::OpenTelemetryReporter; + + let reporter: Box = match target { + TelemetryTarget::Stdout => Box::new(ConsoleReporter), + TelemetryTarget::Jaeger { endpoint } => Box::new(minitrace_jaeger::JaegerReporter::new( + *endpoint, + service_name, + )?), + TelemetryTarget::Datadog { agent_endpoint } => Box::new( + minitrace_datadog::DatadogReporter::new(*agent_endpoint, service_name, LIB_NAME, "web"), + ), + TelemetryTarget::Otlp { + endpoint, + protocol, + timeout, + gzip_compression, + } => { + use opentelemetry::trace::SpanKind; + use opentelemetry::{InstrumentationLibrary, KeyValue}; + use opentelemetry_otlp::WithExportConfig; + use opentelemetry_sdk::Resource; + use std::borrow::Cow; + + let lib = + InstrumentationLibrary::new(LIB_NAME, None::<&'static str>, None::<&'static str>, None); + let resource = Cow::Owned(Resource::new([KeyValue::new( + "service.name", + service_name.clone(), + )])); + + let mut exporter = opentelemetry_otlp::new_exporter() + .tonic() + .with_endpoint(endpoint) + .with_protocol(protocol.clone().into()) + .with_timeout(*timeout); + + if *gzip_compression { + exporter = exporter.with_compression(opentelemetry_otlp::Compression::Gzip); + } + + Box::new(OpenTelemetryReporter::new( + exporter.build_span_exporter()?, + SpanKind::Server, + resource, + lib, + )) + } + }; + + Ok(reporter) + } + + #[cfg(feature = "test_utils")] + pub fn configure_tracing_for_test( + &self, + tenant_id: u32, + reporter: Box, + tracing_manager: &mut MinitraceManager, + ) { + tracing_manager.add_reporter(tenant_id, reporter); + } + + pub fn configure_tracing( + &self, + tenant_id: u32, + tracing_manager: &mut MinitraceManager, + ) -> Result<(), PluginError> { + for target in &self.config.targets { + let reporter = Self::compose_reporter(&self.config.service_name, target) + .map_err(|e| PluginError::InitError { source: e.into() })?; + tracing_manager.add_reporter(tenant_id, reporter); + } + + Ok(()) + } +} + +#[async_trait::async_trait(?Send)] +impl Plugin for TelemetryPlugin {} diff --git a/plugins/trusted_documents/src/plugin.rs b/plugins/trusted_documents/src/plugin.rs index a9e9e2c9..fb8174c7 100644 --- a/plugins/trusted_documents/src/plugin.rs +++ b/plugins/trusted_documents/src/plugin.rs @@ -35,7 +35,7 @@ pub enum TrustedDocumentsPluginError { impl CreatablePlugin for TrustedDocumentsPlugin { type Config = TrustedDocumentsPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { debug!("creating trusted operations plugin"); let store: Box = match &config.store { diff --git a/plugins/vrl/src/plugin.rs b/plugins/vrl/src/plugin.rs index ce0c5933..f508c275 100644 --- a/plugins/vrl/src/plugin.rs +++ b/plugins/vrl/src/plugin.rs @@ -25,7 +25,7 @@ pub struct VrlPlugin { impl CreatablePlugin for VrlPlugin { type Config = VrlPluginConfig; - async fn create(config: Self::Config) -> Result, PluginError> { + async fn create(config: Self::Config) -> Result, PluginError> { let fns: Vec> = vrl_fns(); let on_downstream_http_request = config diff --git a/test_config/config.yaml b/test_config/config.yaml index 6446fea8..fb9b40d1 100644 --- a/test_config/config.yaml +++ b/test_config/config.yaml @@ -2,7 +2,8 @@ server: port: 8000 logger: - level: debug + filter: debug + format: compact sources: - id: countries @@ -18,10 +19,19 @@ sources: file: ./supergraph.graphql endpoints: + - path: /other-thing + from: countries + plugins: + - type: graphiql + - path: /graphql from: countries plugins: - type: graphiql + - type: telemetry + config: + targets: + - type: datadog - type: cors config: allow_credentials: true @@ -75,4 +85,4 @@ endpoints: - path: /federation from: fed plugins: - - type: graphiql + - type: graphiql \ No newline at end of file diff --git a/test_config/worker.yaml b/test_config/worker.yaml index 3752447a..8e608c82 100644 --- a/test_config/worker.yaml +++ b/test_config/worker.yaml @@ -1,21 +1,16 @@ logger: - level: debug - + filter: debug + format: pretty + sources: - id: countries type: graphql config: endpoint: https://countries.trevorblades.com/ - - id: gateways-benchmark - type: federation - config: - supergraph: - file: ./temp/supergraph.graphql - endpoints: - path: /graphql from: countries plugins: - type: http_get - - type: graphiql + - type: graphiql \ No newline at end of file diff --git a/tests/graphql-over-http/config.yaml b/tests/graphql-over-http/config.yaml index 8a4975ab..43ea4a1d 100644 --- a/tests/graphql-over-http/config.yaml +++ b/tests/graphql-over-http/config.yaml @@ -2,7 +2,7 @@ server: port: 9000 logger: - level: debug + filter: debug format: pretty sources: diff --git a/website/public/assets/telemetry.png b/website/public/assets/telemetry.png new file mode 100644 index 0000000000000000000000000000000000000000..4444ef17fbc049d6336b232516cf38890a36b519 GIT binary patch literal 278869 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

  • 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

    i5Ue0)6!Bi2QlQw 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 JFW$QQKLGW>ryT$Q literal 0 HcmV?d00001 diff --git a/website/src/components/index-page.tsx b/website/src/components/index-page.tsx index f256343c..b2483b99 100644 --- a/website/src/components/index-page.tsx +++ b/website/src/components/index-page.tsx @@ -118,27 +118,27 @@ export function IndexPage() { { title: 'Security', description: - 'Built-in plugins for popular authentication flows (Basic/JWT/Auth0/...). Also, hardening plugins like rate-limit are available.', + 'Built-in plugins for popular authentication flows (JWT with JWKS). Also, hardening plugins like rate-limit are available.', icon: , }, { title: 'Monitoring', description: - 'Monitor your service with built-in support for Prometheus, StatD and OpenTelemetry.', + 'Monitor your service with built-in support for telemetry (OpenTelemetry, Jaeger, DataDog).', icon: , }, - { - title: 'GraphQL to REST', - description: ( -

    - Expose any GraphQL schemas as REST service, powered by{' '} - - SOFA - -

    - ), - icon: , - }, + // { + // title: 'GraphQL to REST', + // description: ( + //

    + // Expose any GraphQL schemas as REST service, powered by{' '} + // + // SOFA + // + //

    + // ), + // icon: , + // }, { title: 'and many more', icon: , diff --git a/website/src/lib/json-schema-ui.tsx b/website/src/lib/json-schema-ui.tsx index e851c7a9..ab2a371f 100644 --- a/website/src/lib/json-schema-ui.tsx +++ b/website/src/lib/json-schema-ui.tsx @@ -1,4 +1,5 @@ -import { PropsWithChildren, ReactElement, ReactNode } from 'react'; +import { PropsWithChildren, ReactElement } from 'react'; +import NextImage from 'next/image'; import clsx from 'clsx'; import * as mdxComponents from 'nextra/components'; import { useMDXComponents } from 'nextra/mdx'; @@ -74,4 +75,5 @@ function DocumentationContainer( export const components = { ...mdxComponents, DocumentationContainer, + NextImage, }; diff --git a/website/src/lib/json-schema.ts b/website/src/lib/json-schema.ts index 852cf9ac..350aa6b9 100644 --- a/website/src/lib/json-schema.ts +++ b/website/src/lib/json-schema.ts @@ -94,7 +94,11 @@ function visitDefinition( return ` -${makeDescription((rawDef as JSONSchema7).description || fieldDef.description)} +${makeDescription( + (rawDef as JSONSchema7).description || + fieldDef.description || + `To use this variation, please specify the \`type: ${fieldDef.enum[0]}\` in your configuration.`, +)} `; } diff --git a/website/src/pages/_meta.ts b/website/src/pages/_meta.ts index 3b84489a..ad0dbbfd 100644 --- a/website/src/pages/_meta.ts +++ b/website/src/pages/_meta.ts @@ -8,7 +8,7 @@ export default { }, }, docs: { - title: 'Docs', + title: 'Documentation', type: 'page', }, }; diff --git a/website/src/pages/docs/plugins/_meta.ts b/website/src/pages/docs/plugins/_meta.ts index 432f0448..3aceba54 100644 --- a/website/src/pages/docs/plugins/_meta.ts +++ b/website/src/pages/docs/plugins/_meta.ts @@ -1,5 +1,6 @@ export default { graphiql: 'GraphiQL', + telemetry: 'Telemetry', vrl: 'VRL (Custom Plugins)', cors: 'CORS', jwt: 'JWT Authentication', diff --git a/website/src/pages/docs/plugins/telemetry.mdx b/website/src/pages/docs/plugins/telemetry.mdx new file mode 100644 index 00000000..647792c0 --- /dev/null +++ b/website/src/pages/docs/plugins/telemetry.mdx @@ -0,0 +1,11 @@ +--- +title: Telemetry +--- + +import { RemoteContent } from 'nextra/components' +import { getStaticPropsFactory } from '@/lib/json-schema' +import { components } from '@/lib/json-schema-ui' + +export const getStaticProps = getStaticPropsFactory('TelemetryPluginConfig', 'Telemetry') + + diff --git a/website/theme.config.tsx b/website/theme.config.tsx index 807b3fa2..3b9f09d4 100644 --- a/website/theme.config.tsx +++ b/website/theme.config.tsx @@ -4,6 +4,6 @@ import { defineConfig, FooterExtended } from '@theguild/components'; export default defineConfig({ docsRepositoryBase: 'https://github.com/the-guild-org/conductor/tree/master/website', // base URL for the docs repository - logoLink: '/docs', + logoLink: '/', siteName: 'CONDUCTOR', }); From 95caa28f002e3528e25fa4d5a6f2840ef132250b Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 15:58:54 +0200 Subject: [PATCH 2/7] ok --- .github/workflows/benchmark.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index eadadc02..07a65a90 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -44,8 +44,11 @@ jobs: name: k6 env: K6_VERSION: 0.48.0 - runs-on: benchmark-runner + runs-on: ubuntu-22.04 steps: + - name: adjust os + run: ulimit -n 10000 + - name: checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 From bbe61924ace4a4cefabcbf6fbfb0c7a569160946 Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 16:53:27 +0200 Subject: [PATCH 3/7] ok --- .github/workflows/benchmark.yaml | 2 +- benchmark/k6.js | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 07a65a90..824f8d3e 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -47,7 +47,7 @@ jobs: runs-on: ubuntu-22.04 steps: - name: adjust os - run: ulimit -n 10000 + run: ulimit -n 30000 - name: checkout uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 # v4 diff --git a/benchmark/k6.js b/benchmark/k6.js index 02a415d8..2e70c219 100644 --- a/benchmark/k6.js +++ b/benchmark/k6.js @@ -95,6 +95,7 @@ export default function () { }), { headers: { "Content-Type": "application/json" }, + responseType: "text", } ); From bc508fa335751b1727cd4398ba888640483f3a82 Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 17:03:57 +0200 Subject: [PATCH 4/7] tiny fix --- bin/cloudflare_worker/src/lib.rs | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/bin/cloudflare_worker/src/lib.rs b/bin/cloudflare_worker/src/lib.rs index c4bf56fb..c78f1054 100644 --- a/bin/cloudflare_worker/src/lib.rs +++ b/bin/cloudflare_worker/src/lib.rs @@ -11,7 +11,7 @@ use tracing::{Instrument, Span}; use tracing_subscriber::prelude::*; use worker::*; -#[tracing::instrument(level = "debug", skip(url, req), name = "transform_http_request")] +#[tracing::instrument(level = "debug", skip(url, req), name = "transform_request")] async fn transform_req(url: &Url, mut req: Request) -> Result { let mut headers_map = HttpHeadersMap::new(); @@ -35,11 +35,7 @@ async fn transform_req(url: &Url, mut req: Request) -> Result Result { let mut response_headers = Headers::new(); for (k, v) in conductor_response.headers.into_iter() { From 4f4603bb05c8803cc08b277a76239b3816c70f70 Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 17:05:50 +0200 Subject: [PATCH 5/7] ok --- bin/cloudflare_worker/src/lib.rs | 70 +++----------------------------- 1 file changed, 6 insertions(+), 64 deletions(-) diff --git a/bin/cloudflare_worker/src/lib.rs b/bin/cloudflare_worker/src/lib.rs index c78f1054..8b4e65bf 100644 --- a/bin/cloudflare_worker/src/lib.rs +++ b/bin/cloudflare_worker/src/lib.rs @@ -1,17 +1,14 @@ use std::str::FromStr; use conductor_common::http::{ - header::USER_AGENT, ConductorHttpRequest, ConductorHttpResponse, HeaderName, HeaderValue, - HttpHeadersMap, Method, + ConductorHttpRequest, ConductorHttpResponse, HeaderName, HeaderValue, HttpHeadersMap, Method, }; use conductor_config::parse_config_contents; -use conductor_engine::gateway::{ConductorGateway, ConductorGatewayRouteData, GatewayError}; +use conductor_engine::gateway::{ConductorGateway, GatewayError}; use std::panic; -use tracing::{Instrument, Span}; use tracing_subscriber::prelude::*; use worker::*; -#[tracing::instrument(level = "debug", skip(url, req), name = "transform_request")] async fn transform_req(url: &Url, mut req: Request) -> Result { let mut headers_map = HttpHeadersMap::new(); @@ -52,42 +49,6 @@ fn transform_res(conductor_response: ConductorHttpResponse) -> Result }) } -fn build_root_span(route_date: &ConductorGatewayRouteData, req: &Request) -> Span { - let method_str = req.method().to_string(); - let path_str = req.path(); - let name = format!("{} {}", method_str, path_str); - let http_protocol = req.cf().map(|v| v.http_protocol()); - let url = req.url().ok(); - let host = url.as_ref().and_then(|v| v.host().map(|v| v.to_string())); - let scheme = url.as_ref().map(|v| v.scheme().to_string()); - let user_agent = req.headers().get(USER_AGENT.as_str()).ok().and_then(|v| v); - // Based on https://developers.cloudflare.com/network/true-client-ip-header/ - let client_ip = req - .headers() - .get("true-client-ip") - .map_err(|_| req.headers().get("cf-connecting-ip")) - .ok() - .and_then(|v| v); - - tracing::info_span!( - "HTTP request", - "otel.name" = name, - "otel.kind" = "server", - endpoint = route_date.endpoint, - "http.method" = method_str, - "http.flavor" = http_protocol, - "http.host" = host, - "http.scheme" = scheme, - "http.path" = path_str, - "http.client_ip" = client_ip, - "http.user_agent" = user_agent, - "otel.status_code" = tracing::field::Empty, - "http.status_code" = tracing::field::Empty, - "trace_id" = tracing::field::Empty, - "request_id" = tracing::field::Empty, - ) -} - async fn run_flow(req: Request, env: Env) -> Result { let conductor_config_str = env.var("CONDUCTOR_CONFIG").map(|v| v.to_string()); let get_env_value = |key: &str| env.var(key).map(|s| s.to_string()).ok(); @@ -115,20 +76,10 @@ async fn run_flow(req: Request, env: Env) -> Result { match gw.match_route(&url) { Ok(route_data) => { - let root_span = build_root_span(route_data, &req); - - async move { - let conductor_req = transform_req(&url, req).await?; - let conductor_response = ConductorGateway::execute(conductor_req, route_data).await; + let conductor_req = transform_req(&url, req).await?; + let conductor_response = ConductorGateway::execute(conductor_req, route_data).await; - let status_code = conductor_response.status.as_u16(); - Span::current().record("otel.status_code", status_code); - Span::current().record("http.status_code", status_code); - - transform_res(conductor_response) - } - .instrument(root_span) - .await + transform_res(conductor_response) } Err(GatewayError::MissingEndpoint(_)) => { Response::error("failed to locate endpoint".to_string(), 404) @@ -154,16 +105,7 @@ async fn main(req: Request, env: Env, _ctx: Context) -> Result { let result = run_flow(req, env).await; match result { - Ok(response) => { - // todo: flush - // if let Some(tracing_manager) = tracing_manager { - // ctx.wait_until(async move { - // tracing_manager.shutdown().await; - // }); - // } - - Ok(response) - } + Ok(response) => Ok(response), Err(e) => Response::error(e.to_string(), 500), } } From b63eddc23ffcc01fbeb463e5f563590c13c91cc1 Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 17:35:29 +0200 Subject: [PATCH 6/7] ok --- .github/workflows/benchmark.yaml | 2 +- benchmark/k6.js | 2 +- tests/test-server/Cargo.lock | 28 ++++++++++++++-------------- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/.github/workflows/benchmark.yaml b/.github/workflows/benchmark.yaml index 824f8d3e..d91f7d57 100644 --- a/.github/workflows/benchmark.yaml +++ b/.github/workflows/benchmark.yaml @@ -80,7 +80,7 @@ jobs: - uses: JarvusInnovations/background-action@v1 name: run gateway in background with: - run: cargo run --bin conductor -- ./benchmark/gw.yaml + run: cargo run --release --bin conductor -- ./benchmark/gw.yaml wait-on: http-get://127.0.0.1:9000/_health tail: true wait-for: 10m diff --git a/benchmark/k6.js b/benchmark/k6.js index 2e70c219..afd9cae3 100644 --- a/benchmark/k6.js +++ b/benchmark/k6.js @@ -10,7 +10,7 @@ import { Rate } from "k6/metrics"; export const validGraphQLResponse = new Rate("valid_graphql_response"); export const validHttpCode = new Rate("valid_http_code"); -const RPS = 500; +const RPS = 1000; const TIME_SECONDS = 60; const SCENARIO_NAME = `rps_${RPS}`; const REQ_THRESHOLD = RPS * TIME_SECONDS - 1; diff --git a/tests/test-server/Cargo.lock b/tests/test-server/Cargo.lock index a8f0fee8..23ef2c9c 100644 --- a/tests/test-server/Cargo.lock +++ b/tests/test-server/Cargo.lock @@ -297,9 +297,9 @@ dependencies = [ [[package]] name = "async-graphql" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ad990024653fd2d0321a568f64e620404a894047b2ab8c475f7452c8bb82cf6" +checksum = "b16926f97f683ff3b47b035cc79622f3d6a374730b07a5d9051e81e88b5f1904" dependencies = [ "async-graphql-derive", "async-graphql-parser", @@ -323,16 +323,16 @@ dependencies = [ "serde", "serde_json", "serde_urlencoded", - "static_assertions", + "static_assertions_next", "tempfile", "thiserror", ] [[package]] name = "async-graphql-actix-web" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1ecf394c0258dc1ffdf28542c69d0beba868a6ab6d9d84d0c3ba9f1cc4267c4" +checksum = "9bfe82dd8c3107c3a8ac4c61d64d47b7b149d0728d82258bac04c6b681d87a7a" dependencies = [ "actix", "actix-http", @@ -349,9 +349,9 @@ dependencies = [ [[package]] name = "async-graphql-derive" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3909cc7228128099b603d057e5a920b9499ce24299f8f680d5d1f213d7b830c0" +checksum = "a6a7349168b79030e3172a620f4f0e0062268a954604e41475eff082380fe505" dependencies = [ "Inflector", "async-graphql-parser", @@ -366,9 +366,9 @@ dependencies = [ [[package]] name = "async-graphql-parser" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ceb02570faf16e3b6775cc286b1f0fb2e4eb846144a08c130ca5ad6e25219fe" +checksum = "58fdc0adf9f53c2b65bb0ff5170cba1912299f248d0e48266f444b6f005deb1d" dependencies = [ "async-graphql-value", "pest", @@ -378,9 +378,9 @@ dependencies = [ [[package]] name = "async-graphql-value" -version = "7.0.0" +version = "7.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516317bb55d143cc47941c0cb952134dd207c5ab3c839ee226eedd6dd9a96f43" +checksum = "7cf4d4e86208f4f9b81a503943c07e6e7f29ad3505e6c9ce6431fe64dc241681" dependencies = [ "bytes", "indexmap", @@ -1388,10 +1388,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" [[package]] -name = "static_assertions" -version = "1.1.0" +name = "static_assertions_next" +version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" +checksum = "d7beae5182595e9a8b683fa98c4317f956c9a2dec3b9716990d20023cc60c766" [[package]] name = "strsim" From 77aad5a0f642c55ecdd551e6b176a8705e8c6f0b Mon Sep 17 00:00:00 2001 From: Dotan Simha Date: Tue, 23 Jan 2024 17:46:57 +0200 Subject: [PATCH 7/7] adjustments --- benchmark/k6.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmark/k6.js b/benchmark/k6.js index afd9cae3..c288ec8e 100644 --- a/benchmark/k6.js +++ b/benchmark/k6.js @@ -29,7 +29,7 @@ export const options = { // The following two are here to make sure the runtime (CI, local) is capable of producing the desired RPS [`iterations{scenario:${SCENARIO_NAME}}`]: [`count>=${REQ_THRESHOLD}`], [`http_reqs{scenario:${SCENARIO_NAME}}`]: [`count>=${REQ_THRESHOLD}`], - [`http_req_duration{scenario:${SCENARIO_NAME}}`]: ["avg<=2"], + [`http_req_duration{scenario:${SCENARIO_NAME}}`]: ["avg<=1", "p(99)<=1"], [`http_req_failed{scenario:${SCENARIO_NAME}}`]: ["rate==0"], [`${validGraphQLResponse.name}{scenario:${SCENARIO_NAME}}`]: ["rate==1"], [`${validHttpCode.name}{scenario:${SCENARIO_NAME}}`]: ["rate==1"],