diff --git a/.changeset/@graphql-hive_gateway-1030-dependencies.md b/.changeset/@graphql-hive_gateway-1030-dependencies.md new file mode 100644 index 000000000..fc6e6bbec --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1030-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-1171-dependencies.md b/.changeset/@graphql-hive_gateway-1171-dependencies.md new file mode 100644 index 000000000..67ded21e9 --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1171-dependencies.md @@ -0,0 +1,15 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Added dependency [`@opentelemetry/api@^1.9.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api/v/1.9.0) (to `dependencies`) +- Added dependency [`@opentelemetry/context-zone@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-zone/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/core@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/core/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/exporter-jaeger@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-jaeger/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/exporter-zipkin@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-zipkin/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/propagator-b3@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/propagator-b3/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/propagator-jaeger@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/propagator-jaeger/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sampler-jaeger-remote@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sampler-jaeger-remote/v/0.202.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-metrics@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-metrics/v/2.0.1) (to `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-1293-dependencies.md b/.changeset/@graphql-hive_gateway-1293-dependencies.md new file mode 100644 index 000000000..4d736887b --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1293-dependencies.md @@ -0,0 +1,8 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Added dependency [`@opentelemetry/api-logs@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.202.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-logs@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.202.0) (to `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-1300-dependencies.md b/.changeset/@graphql-hive_gateway-1300-dependencies.md new file mode 100644 index 000000000..4b06cf543 --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1300-dependencies.md @@ -0,0 +1,8 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Added dependency [`@opentelemetry/context-async-hooks@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-async-hooks/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-trace-base@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/2.0.1) (to `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-1343-dependencies.md b/.changeset/@graphql-hive_gateway-1343-dependencies.md new file mode 100644 index 000000000..8e3bac214 --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1343-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/3.0.0) (from `^2.0.1`, in `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-956-dependencies.md b/.changeset/@graphql-hive_gateway-956-dependencies.md new file mode 100644 index 000000000..f89244dbf --- /dev/null +++ b/.changeset/@graphql-hive_gateway-956-dependencies.md @@ -0,0 +1,21 @@ +--- +'@graphql-hive/gateway': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) +- Added dependency [`@opentelemetry/api@^1.9.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api/v/1.9.0) (to `dependencies`) +- Added dependency [`@opentelemetry/api-logs@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.203.0) (to `dependencies`) +- Added dependency [`@opentelemetry/context-async-hooks@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-async-hooks/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/context-zone@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-zone/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/core@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/core/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/exporter-jaeger@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-jaeger/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/exporter-zipkin@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-zipkin/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/propagator-b3@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/propagator-b3/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/propagator-jaeger@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/propagator-jaeger/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sampler-jaeger-remote@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sampler-jaeger-remote/v/0.203.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-logs@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.203.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-metrics@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-metrics/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-trace-base@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/2.0.1) (to `dependencies`) +- Removed dependency [`@graphql-mesh/plugin-mock@^0.105.8` ↗︎](https://www.npmjs.com/package/@graphql-mesh/plugin-mock/v/0.105.8) (from `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-runtime-1030-dependencies.md b/.changeset/@graphql-hive_gateway-runtime-1030-dependencies.md new file mode 100644 index 000000000..102b3e27b --- /dev/null +++ b/.changeset/@graphql-hive_gateway-runtime-1030-dependencies.md @@ -0,0 +1,9 @@ +--- +'@graphql-hive/gateway-runtime': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/instrumentation@^1.0.0` ↗︎](https://www.npmjs.com/package/@envelop/instrumentation/v/1.0.0) (to `dependencies`) +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) +- Removed dependency [`@graphql-hive/logger-json@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger-json/v/workspace:^) (from `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-runtime-1360-dependencies.md b/.changeset/@graphql-hive_gateway-runtime-1360-dependencies.md new file mode 100644 index 000000000..f50ad463e --- /dev/null +++ b/.changeset/@graphql-hive_gateway-runtime-1360-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway-runtime': patch +--- + +dependencies updates: + +- Added dependency [`@opentelemetry/api@^1.9.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api/v/1.9.0) (to `dependencies`) diff --git a/.changeset/@graphql-hive_gateway-runtime-956-dependencies.md b/.changeset/@graphql-hive_gateway-runtime-956-dependencies.md new file mode 100644 index 000000000..102b3e27b --- /dev/null +++ b/.changeset/@graphql-hive_gateway-runtime-956-dependencies.md @@ -0,0 +1,9 @@ +--- +'@graphql-hive/gateway-runtime': patch +--- + +dependencies updates: + +- Added dependency [`@envelop/instrumentation@^1.0.0` ↗︎](https://www.npmjs.com/package/@envelop/instrumentation/v/1.0.0) (to `dependencies`) +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) +- Removed dependency [`@graphql-hive/logger-json@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger-json/v/workspace:^) (from `dependencies`) diff --git a/.changeset/@graphql-hive_nestjs-1293-dependencies.md b/.changeset/@graphql-hive_nestjs-1293-dependencies.md new file mode 100644 index 000000000..0af93d273 --- /dev/null +++ b/.changeset/@graphql-hive_nestjs-1293-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/nestjs': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-hive_nestjs-956-dependencies.md b/.changeset/@graphql-hive_nestjs-956-dependencies.md new file mode 100644 index 000000000..0af93d273 --- /dev/null +++ b/.changeset/@graphql-hive_nestjs-956-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/nestjs': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_fusion-runtime-1030-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-1030-dependencies.md new file mode 100644 index 000000000..d381bdd14 --- /dev/null +++ b/.changeset/@graphql-mesh_fusion-runtime-1030-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/fusion-runtime': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_fusion-runtime-956-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-956-dependencies.md new file mode 100644 index 000000000..d381bdd14 --- /dev/null +++ b/.changeset/@graphql-mesh_fusion-runtime-956-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/fusion-runtime': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1171-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1171-dependencies.md new file mode 100644 index 000000000..248438bb8 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1171-dependencies.md @@ -0,0 +1,18 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/context-async-hooks@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-async-hooks/v/2.0.1) (from `^2.0.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/core@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/core/v/2.0.1) (from `^2.0.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-grpc@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-grpc/v/0.202.0) (from `^0.200.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-http@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-http/v/0.202.0) (from `^0.200.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/instrumentation@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/instrumentation/v/0.202.0) (from `^0.200.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/resources@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/resources/v/2.0.1) (from `^2.0.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/sdk-trace-base@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/2.0.1) (from `^2.0.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/semantic-conventions@^1.34.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/semantic-conventions/v/1.34.0) (from `^1.28.0`, in `dependencies`) +- Added dependency [`@opentelemetry/auto-instrumentations-node@^0.60.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node/v/0.60.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-node@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-node/v/0.202.0) (to `dependencies`) +- Removed dependency [`@opentelemetry/exporter-zipkin@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-zipkin/v/2.0.0) (from `dependencies`) +- Removed dependency [`@opentelemetry/sdk-trace-web@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-web/v/2.0.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1293-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1293-dependencies.md new file mode 100644 index 000000000..4811d2789 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1293-dependencies.md @@ -0,0 +1,9 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) +- Added dependency [`@opentelemetry/api-logs@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.202.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-logs@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.202.0) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1300-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1300-dependencies.md new file mode 100644 index 000000000..8d40fc8f0 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1300-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/core@^0.13.0` ↗︎](https://www.npmjs.com/package/@graphql-hive/core/v/0.13.0) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1343-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1343-dependencies.md new file mode 100644 index 000000000..bf78dab6f --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1343-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/3.0.0) (from `^2.0.1`, in `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1360-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1360-dependencies.md new file mode 100644 index 000000000..6ba5aeae9 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1360-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/auto-instrumentations-node@^0.62.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node/v/0.62.1) (from `^0.62.0`, in `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-875-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-875-dependencies.md new file mode 100644 index 000000000..6d39fd729 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-875-dependencies.md @@ -0,0 +1,14 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/context-async-hooks@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-async-hooks/v/2.0.0) (from `^1.30.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-grpc@^0.200.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-grpc/v/0.200.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-http@^0.200.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-http/v/0.200.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-zipkin@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-zipkin/v/2.0.0) (from `^1.29.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/instrumentation@^0.200.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/instrumentation/v/0.200.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/resources@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/resources/v/2.0.0) (from `^1.29.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/sdk-trace-base@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/2.0.0) (from `^1.29.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/sdk-trace-web@^2.0.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-web/v/2.0.0) (from `^1.29.0`, in `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-956-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-956-dependencies.md new file mode 100644 index 000000000..2ae6d3aa9 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-956-dependencies.md @@ -0,0 +1,24 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Updated dependency [`@opentelemetry/core@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/core/v/2.0.1) (from `^1.30.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-grpc@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-grpc/v/0.203.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/exporter-trace-otlp-http@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-http/v/0.203.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/instrumentation@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/instrumentation/v/0.203.0) (from `^0.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/resources@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/resources/v/2.0.1) (from `^1.29.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/sdk-trace-base@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-base/v/2.0.1) (from `^1.29.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/semantic-conventions@^1.36.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/semantic-conventions/v/1.36.0) (from `^1.28.0`, in `dependencies`) +- Updated dependency [`@whatwg-node/promise-helpers@1.3.0` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.3.0) (from `^1.3.0`, in `dependencies`) +- Added dependency [`@graphql-hive/core@^0.13.0` ↗︎](https://www.npmjs.com/package/@graphql-hive/core/v/0.13.0) (to `dependencies`) +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) +- Added dependency [`@opentelemetry/api-logs@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.203.0) (to `dependencies`) +- Added dependency [`@opentelemetry/auto-instrumentations-node@^0.62.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node/v/0.62.0) (to `dependencies`) +- Added dependency [`@opentelemetry/context-async-hooks@^2.0.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/context-async-hooks/v/2.0.1) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-logs@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.203.0) (to `dependencies`) +- Added dependency [`@opentelemetry/sdk-node@^0.203.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-node/v/0.203.0) (to `dependencies`) +- Removed dependency [`@azure/monitor-opentelemetry-exporter@^1.0.0-beta.27` ↗︎](https://www.npmjs.com/package/@azure/monitor-opentelemetry-exporter/v/1.0.0) (from `dependencies`) +- Removed dependency [`@opentelemetry/exporter-zipkin@^1.29.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-zipkin/v/1.29.0) (from `dependencies`) +- Removed dependency [`@opentelemetry/sdk-trace-web@^1.29.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-trace-web/v/1.29.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-957-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-957-dependencies.md new file mode 100644 index 000000000..042730193 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-957-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +dependencies updates: + +- Removed dependency [`@azure/monitor-opentelemetry-exporter@^1.0.0-beta.27` ↗︎](https://www.npmjs.com/package/@azure/monitor-opentelemetry-exporter/v/1.0.0) (from `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-prometheus-1030-dependencies.md b/.changeset/@graphql-mesh_plugin-prometheus-1030-dependencies.md new file mode 100644 index 000000000..02e9f8bf5 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-prometheus-1030-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-prometheus': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-prometheus-956-dependencies.md b/.changeset/@graphql-mesh_plugin-prometheus-956-dependencies.md new file mode 100644 index 000000000..02e9f8bf5 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-prometheus-956-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/plugin-prometheus': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_transport-common-1030-dependencies.md b/.changeset/@graphql-mesh_transport-common-1030-dependencies.md new file mode 100644 index 000000000..2096b7a2b --- /dev/null +++ b/.changeset/@graphql-mesh_transport-common-1030-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/transport-common': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_transport-common-956-dependencies.md b/.changeset/@graphql-mesh_transport-common-956-dependencies.md new file mode 100644 index 000000000..2096b7a2b --- /dev/null +++ b/.changeset/@graphql-mesh_transport-common-956-dependencies.md @@ -0,0 +1,7 @@ +--- +'@graphql-mesh/transport-common': patch +--- + +dependencies updates: + +- Added dependency [`@graphql-hive/logger@workspace:^` ↗︎](https://www.npmjs.com/package/@graphql-hive/logger/v/workspace:^) (to `dependencies`) diff --git a/.changeset/big-dolls-invent.md b/.changeset/big-dolls-invent.md new file mode 100644 index 000000000..26ccb4b89 --- /dev/null +++ b/.changeset/big-dolls-invent.md @@ -0,0 +1,10 @@ +--- +'@graphql-mesh/fusion-runtime': minor +--- + +Breaking Change: Removed `subgraphNameByExecutionRequest` weak map. Subgraph name is now stored in the execution request itself. + +```diff +- const subgraphName = subgraphNameByExecutionRequest.get(executionRequest) ++ const subgraphName = executionRequest.subgraphName +``` diff --git a/.changeset/dry-humans-mix.md b/.changeset/dry-humans-mix.md new file mode 100644 index 000000000..6b9057ed8 --- /dev/null +++ b/.changeset/dry-humans-mix.md @@ -0,0 +1,33 @@ +--- +'@graphql-mesh/hmac-upstream-signature': major +'@graphql-hive/plugin-deduplicate-request': major +'@graphql-mesh/transport-http-callback': major +'@graphql-mesh/plugin-opentelemetry': major +'@graphql-tools/executor-graphql-ws': major +'@graphql-tools/stitching-directives': major +'@graphql-mesh/plugin-prometheus': major +'@graphql-hive/plugin-aws-sigv4': major +'@graphql-mesh/transport-common': major +'@graphql-tools/executor-common': major +'@graphql-mesh/plugin-jwt-auth': major +'@graphql-mesh/transport-http': major +'@graphql-tools/batch-delegate': major +'@graphql-tools/executor-http': major +'@graphql-mesh/fusion-runtime': major +'@graphql-tools/batch-execute': major +'@graphql-mesh/transport-ws': major +'@graphql-tools/federation': major +'@graphql-tools/delegate': major +'@graphql-hive/importer': major +'@graphql-hive/gateway': major +'@graphql-hive/gateway-runtime': major +'@graphql-hive/nestjs': major +'@graphql-hive/pubsub': major +'@graphql-hive/signal': major +'@graphql-tools/stitch': major +'@graphql-tools/wrap': major +--- + +Drop Node 18 support + +Least supported Node version is now v20. diff --git a/.changeset/itchy-ways-cross.md b/.changeset/itchy-ways-cross.md new file mode 100644 index 000000000..331529560 --- /dev/null +++ b/.changeset/itchy-ways-cross.md @@ -0,0 +1,19 @@ +--- +'@graphql-mesh/hmac-upstream-signature': major +'@graphql-mesh/transport-http-callback': major +'@graphql-mesh/plugin-opentelemetry': major +'@graphql-mesh/plugin-prometheus': major +'@graphql-mesh/transport-common': major +'@graphql-mesh/plugin-jwt-auth': major +'@graphql-mesh/fusion-runtime': major +'@graphql-mesh/transport-ws': major +'@graphql-hive/gateway': major +'@graphql-hive/gateway-runtime': major +'@graphql-hive/nestjs': major +--- + +Introduce and use the new Hive Logger + +- [Read more about it on the Hive Logger documentation here.](https://the-guild.dev/graphql/hive/docs/logger) + +- If coming from Hive Gateway v1, [read the migration guide here.](https://the-guild.dev/graphql/hive/docs/migration-guides/gateway-v1-v2) diff --git a/.changeset/lovely-turtles-sing.md b/.changeset/lovely-turtles-sing.md new file mode 100644 index 000000000..d9a89fda8 --- /dev/null +++ b/.changeset/lovely-turtles-sing.md @@ -0,0 +1,6 @@ +--- +'@graphql-tools/delegate': minor +'@graphql-hive/gateway-runtime': minor +--- + +Added `subgraphName` to `ExecutionRequest` for easier plugin developpment. diff --git a/.changeset/nervous-carrots-allow.md b/.changeset/nervous-carrots-allow.md new file mode 100644 index 000000000..78b884f7f --- /dev/null +++ b/.changeset/nervous-carrots-allow.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +--- + +Fix the types exporters factories, the configuration is actually optional. All parameters can be determined from environement variables. diff --git a/.changeset/new-buttons-thank.md b/.changeset/new-buttons-thank.md new file mode 100644 index 000000000..91144360f --- /dev/null +++ b/.changeset/new-buttons-thank.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway': major +--- + +Disable forking even if NODE_ENV=production + +Forking workers for concurrent processing is a delicate process and if not done carefully can lead to performance degradations. It should be configured with careful consideration by advanced users. diff --git a/.changeset/nine-pears-peel.md b/.changeset/nine-pears-peel.md new file mode 100644 index 000000000..51da34732 --- /dev/null +++ b/.changeset/nine-pears-peel.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/plugin-opentelemetry': minor +--- + +Add a configurable sampling rate. The sampling strategy relies on a determenistic probability sampler with a parent priority, meaning that if a span is sampled, all its children spans will also be sampled. diff --git a/.changeset/pink-sloths-mate.md b/.changeset/pink-sloths-mate.md new file mode 100644 index 000000000..415d7ec40 --- /dev/null +++ b/.changeset/pink-sloths-mate.md @@ -0,0 +1,142 @@ +--- +'@graphql-mesh/plugin-opentelemetry': major +--- + +The OpenTelemetry integration have been entirely overhauled. + +**This Release contains breaking changes, please read [Breaking Changes](#breaking-changes) section below** + +## Improvements + +### Span parenting + +The spans of the different phases of the request handling have been fixed. + +Now, spans are parented as expected, and Hive Gateway is now compatible with Grafana's "critical path" feature. + +#### Context Manager + +By default, if `initializeNodeSDK` is `true` (default), the plugin will try to install an `AsyncLocalStorage` based Context Manager. + +You can configure an alternative context manager (or entirely disable it) with `contextManager` new option. + +#### Extended span coverage + +Spans also now covers the entire duration of each phases, including the plugin hooks execution. + +### Custom spans and standard instrumentation support + +We are now fully compatible with OpenTelemetry Context, meaning you can now create custom spans +inside your plugins, or enable standard OTEL instrumentation like Node SDK. + +The custom spans will be parented correctly thanks to OTEL Context. + +```ts +const useMyPlugin = () => { + const tracer = otel.trace.getTracer('hive-gateway'); + return { + async onExecute() { + await otel.startActiveSpan('my-custom-span', async () => { + // do something + }); + }, + }; +}; +``` + +You can also enable Node SDK standard instrumentations (or instrumentation specific to your runtime). +They will also be parented correctly: + +```ts +// otel-setup.ts +import otel from '@opentelemetry/api'; +import { getNodeAutoInstrumentations } from '@opentelemetry/auto-instrumentations-node'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { NodeSDK } from '@opentelemetry/sdk-node'; +import './setup.js'; +import { defineConfig } from '@graphql-hive/gateway'; + +const sdk = new NodeSDK({ + traceExporter: new OTLPTraceExporter({ + url: 'http://localhost:4318/v1/traces', + }), + // Enable Node standard instrumentations + instrumentations: [getNodeAutoInstrumentations()], + serviceName: 'hive-gateway', +}); + +sdk.start(); + +// This is required for the OTEL context to be properly propagated and spans correlated with Hive's integration. +otel.context.setGlobalContextManager(new AsyncLocalStorageContextManager()); + +// gateway.config.ts +import { defineConfig } from '@graphql-hive/gateway'; + +export const gatewayConfig = defineConfig({ + opentelemetry: { + initializeNodeSDK: false, + }, +}); +``` + +### New `graphql.operation` span with Batched Queries support + +The plugin now exports a new span `graphql.operation ` which represent the handling of a graphql operation. + +This enables the support of Batched queries. If enabled the root `POST /graphql` span will contain +one `graphql.operation ` span for each graphql operation contained in the HTTP request. + +### Support of Upstream Retry + +The plugin now support standard OTEL attribute for request retry (`http.request.resend_count`). + +If enabled, you will see one `http.fetch` span for each try under `subgraph.execute ()` spans. + +### Support of custom attributes + +Thanks to OTEL Context, you can now add custom attributes to the current span: + +```ts +import otel from '@opentelemetry/api' + +const useMyPlugin = () => ({ + async onRequestParse({ request }) => ({ + const userId = await getUserIdForRequest(request); + otel.trace.getSpan()?.setAttribute('user_id', userId); + }) +}) +``` + +## Breaking Changes + +### Spans Parenting + +Spans are now parented correctly, which can break your Grafana (or other visualization and alerting tools) setup. +Please carefully review your span queries to check if they rely on span parent. + +### Spans configuration + +Spans can be skipped based on the result of a predicate function. The parameter of those functions have been narrowed down, and contains less data. + +If your configuration contains skip functions, please review the types to adapt to the new API. + +### Async Local Storage Context Manager + +When `initializeNodeSDK` is set to `true` (the default), the plugin tries to enable an Async Local Storage based Context Manager. +This is needed to ensure correct correlation of spans created outside of the plugin. + +While this should not break anything, the usage of `AsyncLocalStorage` can slightly reduce performances of the Gateway. + +If you don't need to correlate with any OTEL official instrumentations or don't need OTEL context for custom spans, you can disable it by setting the `contextManager` option: + +```ts +import { defineConfig } from '@graphql-hive/gateway'; + +export const gatewayConfig = defineConfig({ + opentelemetry: { + contextManager: false, + }, +}); +``` diff --git a/.changeset/real-zoos-relate.md b/.changeset/real-zoos-relate.md new file mode 100644 index 000000000..99892d586 --- /dev/null +++ b/.changeset/real-zoos-relate.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/plugin-opentelemetry': minor +--- + +Add support of Yoga. This plugin is now usable in Yoga too, which allows for better opentelemetry traces in subgraphs. diff --git a/.changeset/red-teachers-love.md b/.changeset/red-teachers-love.md new file mode 100644 index 000000000..2fd81030b --- /dev/null +++ b/.changeset/red-teachers-love.md @@ -0,0 +1,10 @@ +--- +'@graphql-hive/plugin-aws-sigv4': patch +'@graphql-mesh/fusion-runtime': patch +'@graphql-tools/batch-execute': patch +'@graphql-tools/delegate': patch +'@graphql-hive/gateway-runtime': patch +'@graphql-tools/wrap': patch +--- + +Fixed subgraph name being lost when execution requests get batched together. diff --git a/.changeset/strong-islands-laugh.md b/.changeset/strong-islands-laugh.md new file mode 100644 index 000000000..dc29b6756 --- /dev/null +++ b/.changeset/strong-islands-laugh.md @@ -0,0 +1,5 @@ +--- +'@graphql-hive/gateway-runtime': minor +--- + +No details landing page and improvements around it diff --git a/.changeset/strong-paws-complain.md b/.changeset/strong-paws-complain.md new file mode 100644 index 000000000..d0dac2db9 --- /dev/null +++ b/.changeset/strong-paws-complain.md @@ -0,0 +1,6 @@ +--- +'@graphql-mesh/plugin-opentelemetry': patch +'@graphql-hive/gateway': patch +--- + +Patch the `@opentelemetry/sdk-trace-base` package to fix span start time precision being millisecond instead of nanosecond. diff --git a/.changeset/thick-eyes-whisper.md b/.changeset/thick-eyes-whisper.md new file mode 100644 index 000000000..abafcadfb --- /dev/null +++ b/.changeset/thick-eyes-whisper.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway': major +--- + +Remove mocking plugin from Hive Gateway built-ins + +There is no need to provide the `useMock` plugin alongside Hive Gateway built-ins. Not only is the mock plugin 2MB in size (minified), but installing and using it is very simple. diff --git a/.changeset/thirty-dolls-help.md b/.changeset/thirty-dolls-help.md new file mode 100644 index 000000000..853a84696 --- /dev/null +++ b/.changeset/thirty-dolls-help.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway': major +--- + +Load schema on initialization + +Failing to start if the schema is not loaded for whatever reason. diff --git a/.changeset/tiny-monkeys-bake.md b/.changeset/tiny-monkeys-bake.md new file mode 100644 index 000000000..fc169f671 --- /dev/null +++ b/.changeset/tiny-monkeys-bake.md @@ -0,0 +1,5 @@ +--- +'@graphql-mesh/plugin-opentelemetry': major +--- + +**Breaking Change**: Removal of the Azure exporter (`createAzureMonitorExporter`). Please use `@azure/monitor-opentelemetry-exporter` directly instead. diff --git a/.changeset/tough-elephants-shop.md b/.changeset/tough-elephants-shop.md new file mode 100644 index 000000000..9f3e6234e --- /dev/null +++ b/.changeset/tough-elephants-shop.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/logger': patch +--- + +Introducing Hive Logger + +[Read more about it on the Hive Logger documentation website.](https://the-guild.dev/graphql/hive/docs/logger) diff --git a/.changeset/unlucky-pigs-sniff.md b/.changeset/unlucky-pigs-sniff.md new file mode 100644 index 000000000..378088eb9 --- /dev/null +++ b/.changeset/unlucky-pigs-sniff.md @@ -0,0 +1,7 @@ +--- +'@graphql-hive/gateway-runtime': major +--- + +GraphQL multipart request support is disabled by default + +The only objective of [GraphQL multipart request spec](https://github.com/jaydenseric/graphql-multipart-request-spec) is to support file uploads; however, file uploads are not native to GraphQL and are generally considered an anti-pattern. diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0a32ac7c..5be22269c 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -20,7 +20,6 @@ jobs: fail-fast: false matrix: node-version: - - 18 - 20 - 22 - 23 @@ -55,7 +54,6 @@ jobs: fail-fast: false matrix: node-version: - - 18 - 20 - 22 - 23 @@ -84,10 +82,6 @@ jobs: matrix: setup: # Node - - workflow-name: Node 18 on Ubuntu - os: ubuntu-latest - gateway-runner: node - node-version: 18 - workflow-name: Node 20 on Ubuntu os: ubuntu-latest gateway-runner: node diff --git a/.prettierignore b/.prettierignore index 3b5250631..129357534 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1,7 +1,7 @@ .yarn/* !.yarn/custom-plugins Dockerfile -packages/runtime/src/landing-page-html.ts +packages/runtime/src/landing-page.generated.ts __generated__ .changeset/* !.changeset/README.md @@ -12,5 +12,6 @@ __generated__ /packages/importer/tests/fixtures/syntax-error.ts /e2e/config-syntax-error/gateway.config.ts /e2e/config-syntax-error/custom-resolvers.ts +/e2e/load-on-init/malformed.graphql CHANGELOG.md /internal/heapsnapshot/dist/ diff --git a/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch b/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch deleted file mode 100644 index 83fe88107..000000000 --- a/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch +++ /dev/null @@ -1,20 +0,0 @@ -diff --git a/package.json b/package.json -index 8494583b1604d739d2120da81dee9c6f5e053eeb..d1edafb9303aa18a5e617321ae81e237c3a6118e 100644 ---- a/package.json -+++ b/package.json -@@ -3,13 +3,13 @@ - "version": "0.56.0", - "description": "OpenTelemetry Collector Trace Exporter allows user to send collected traces to the OpenTelemetry Collector", - "main": "build/src/index.js", -- "module": "build/esm/index.js", -+ "module": "build/esnext/index.js", - "esnext": "build/esnext/index.js", - "types": "build/src/index.d.ts", - "repository": "open-telemetry/opentelemetry-js", - "browser": { - "./src/platform/index.ts": "./src/platform/browser/index.ts", -- "./build/esm/platform/index.js": "./build/esm/platform/browser/index.js", -+ "./build/esm/platform/index.js": "./build/esnext/platform/browser/index.js", - "./build/esnext/platform/index.js": "./build/esnext/platform/browser/index.js", - "./build/src/platform/index.js": "./build/src/platform/browser/index.js" - }, diff --git a/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch b/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch new file mode 100644 index 000000000..59ae3bfd6 --- /dev/null +++ b/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch @@ -0,0 +1,29 @@ +diff --git a/build/esnext/transport/http-exporter-transport.js b/build/esnext/transport/http-exporter-transport.js +index e63fda75feb67686bad9b692a046ebb1af2bd8a0..93e9701d10f6aea530f38330daea87199e514452 100644 +--- a/build/esnext/transport/http-exporter-transport.js ++++ b/build/esnext/transport/http-exporter-transport.js +@@ -20,7 +20,7 @@ class HttpExporterTransport { + this._parameters = _parameters; + } + async send(data, timeoutMillis) { +- const { agent, send } = this._loadUtils(); ++ const { agent, send } = await this._loadUtils(); + return new Promise(resolve => { + send(this._parameters, agent, data, result => { + resolve(result); +@@ -30,13 +30,11 @@ class HttpExporterTransport { + shutdown() { + // intentionally left empty, nothing to do. + } +- _loadUtils() { ++ async _loadUtils() { + let utils = this._utils; + if (utils === null) { + // Lazy require to ensure that http/https is not required before instrumentations can wrap it. +- const { sendWithHttp, createHttpAgent, +- // eslint-disable-next-line @typescript-eslint/no-require-imports +- } = require('./http-transport-utils'); ++ const { sendWithHttp, createHttpAgent } = await import('./http-transport-utils'); + utils = this._utils = { + agent: createHttpAgent(this._parameters.url, this._parameters.agentOptions), + send: sendWithHttp, diff --git a/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch b/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch deleted file mode 100644 index 74a89b4c2..000000000 --- a/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch +++ /dev/null @@ -1,136 +0,0 @@ -diff --git a/build/esm/transport/http-exporter-transport.js b/build/esm/transport/http-exporter-transport.js -index ef266685d979dc9112b3d5aa4c39b580be0cf1aa..47f7cfaa2f347f22c284fdae61250fe413aba5fc 100644 ---- a/build/esm/transport/http-exporter-transport.js -+++ b/build/esm/transport/http-exporter-transport.js -@@ -13,75 +13,49 @@ - * See the License for the specific language governing permissions and - * limitations under the License. - */ --var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { -- function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } -- return new (P || (P = Promise))(function (resolve, reject) { -- function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } -- function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } -- function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } -- step((generator = generator.apply(thisArg, _arguments || [])).next()); -- }); --}; --var __generator = (this && this.__generator) || function (thisArg, body) { -- var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g; -- return g = { next: verb(0), "throw": verb(1), "return": verb(2) }, typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g; -- function verb(n) { return function (v) { return step([n, v]); }; } -- function step(op) { -- if (f) throw new TypeError("Generator is already executing."); -- while (_) try { -- if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t; -- if (y = 0, t) op = [op[0] & 2, t.value]; -- switch (op[0]) { -- case 0: case 1: t = op; break; -- case 4: _.label++; return { value: op[1], done: false }; -- case 5: _.label++; y = op[1]; op = [0]; continue; -- case 7: op = _.ops.pop(); _.trys.pop(); continue; -- default: -- if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; } -- if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; } -- if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; } -- if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; } -- if (t[2]) _.ops.pop(); -- _.trys.pop(); continue; -- } -- op = body.call(thisArg, _); -- } catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; } -- if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true }; -- } --}; --var HttpExporterTransport = /** @class */ (function () { -- function HttpExporterTransport(_parameters) { -- this._parameters = _parameters; -- this._send = null; -- this._agent = null; -+class HttpExporterTransport { -+ constructor(_parameters) { -+ this._parameters = _parameters; -+ this._send = null; -+ this._agent = null; -+ } -+ async send(data, timeoutMillis) { -+ if (this._send == null) { -+ // Lazy require to ensure that http/https is not required before instrumentations can wrap it. -+ const { -+ sendWithHttp, -+ createHttpAgent, -+ // eslint-disable-next-line @typescript-eslint/no-var-requires -+ } = await import("./http-transport-utils"); -+ this._agent = createHttpAgent( -+ this._parameters.url, -+ this._parameters.agentOptions -+ ); -+ this._send = sendWithHttp; - } -- HttpExporterTransport.prototype.send = function (data, timeoutMillis) { -- return __awaiter(this, void 0, void 0, function () { -- var _a, sendWithHttp, createHttpAgent; -- var _this = this; -- return __generator(this, function (_b) { -- if (this._send == null) { -- _a = require('./http-transport-utils'), sendWithHttp = _a.sendWithHttp, createHttpAgent = _a.createHttpAgent; -- this._agent = createHttpAgent(this._parameters.url, this._parameters.agentOptions); -- this._send = sendWithHttp; -- } -- return [2 /*return*/, new Promise(function (resolve) { -- var _a; -- // this will always be defined -- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -- (_a = _this._send) === null || _a === void 0 ? void 0 : _a.call(_this, _this._parameters, _this._agent, data, function (result) { -- resolve(result); -- }, timeoutMillis); -- })]; -- }); -- }); -- }; -- HttpExporterTransport.prototype.shutdown = function () { -- // intentionally left empty, nothing to do. -- }; -- return HttpExporterTransport; --}()); -+ return new Promise((resolve) => { -+ var _a; -+ // this will always be defined -+ // eslint-disable-next-line @typescript-eslint/no-non-null-assertion -+ (_a = this._send) === null || _a === void 0 -+ ? void 0 -+ : _a.call( -+ this, -+ this._parameters, -+ this._agent, -+ data, -+ (result) => { -+ resolve(result); -+ }, -+ timeoutMillis -+ ); -+ }); -+ } -+ shutdown() { -+ // intentionally left empty, nothing to do. -+ } -+} - export function createHttpExporterTransport(parameters) { -- return new HttpExporterTransport(parameters); -+ return new HttpExporterTransport(parameters); - } - //# sourceMappingURL=http-exporter-transport.js.map -\ No newline at end of file -diff --git a/build/esnext/transport/http-exporter-transport.js b/build/esnext/transport/http-exporter-transport.js -index e6b76f301baeb19f507b6072c0598e1d98ceebbb..32a62444cf4ef7ab2346df5b0ac2fb079c6b4268 100644 ---- a/build/esnext/transport/http-exporter-transport.js -+++ b/build/esnext/transport/http-exporter-transport.js -@@ -24,7 +24,7 @@ class HttpExporterTransport { - // Lazy require to ensure that http/https is not required before instrumentations can wrap it. - const { sendWithHttp, createHttpAgent, - // eslint-disable-next-line @typescript-eslint/no-var-requires -- } = require('./http-transport-utils'); -+ } = await import('./http-transport-utils'); - this._agent = createHttpAgent(this._parameters.url, this._parameters.agentOptions); - this._send = sendWithHttp; - } diff --git a/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch b/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch deleted file mode 100644 index cb6e20634..000000000 --- a/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch +++ /dev/null @@ -1,178 +0,0 @@ -diff --git a/build/esm/detectors/platform/node/machine-id/getMachineId.js b/build/esm/detectors/platform/node/machine-id/getMachineId.js -index 267f4af2d1a33a3e17ef1009da5669d3683694f6..61a6e8a77417c039c298bf12ec85daa20ea669fc 100644 ---- a/build/esm/detectors/platform/node/machine-id/getMachineId.js -+++ b/build/esm/detectors/platform/node/machine-id/getMachineId.js -@@ -13,23 +13,34 @@ - * See the License for the specific language governing permissions and - * limitations under the License. - */ --import * as process from 'process'; --var getMachineId; --switch (process.platform) { -- case 'darwin': -- (getMachineId = require('./getMachineId-darwin').getMachineId); -- break; -- case 'linux': -- (getMachineId = require('./getMachineId-linux').getMachineId); -- break; -- case 'freebsd': -- (getMachineId = require('./getMachineId-bsd').getMachineId); -- break; -- case 'win32': -- (getMachineId = require('./getMachineId-win').getMachineId); -- break; -+function getMachineId() { -+ switch (process.platform) { -+ case "darwin": -+ return import("./getMachineId-darwin").then(m => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "linux": -+ return import("./getMachineId-linux").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "freebsd": -+ return import("./getMachineId-bsd").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "win32": -+ return import("./getMachineId-win").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); - default: -- (getMachineId = require('./getMachineId-unsupported').getMachineId); -+ return import("./getMachineId-unsupported").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ } - } -+ - export { getMachineId }; -\ No newline at end of file --//# sourceMappingURL=getMachineId.js.map -\ No newline at end of file -diff --git a/build/esnext/detectors/platform/node/machine-id/getMachineId.js b/build/esnext/detectors/platform/node/machine-id/getMachineId.js -index 6600fb658cb3330dde569141fafd51c9b2f215e1..e3ec2a33ba6de54910acc571794d791f8a413172 100644 ---- a/build/esnext/detectors/platform/node/machine-id/getMachineId.js -+++ b/build/esnext/detectors/platform/node/machine-id/getMachineId.js -@@ -13,23 +13,34 @@ - * See the License for the specific language governing permissions and - * limitations under the License. - */ --import * as process from 'process'; --let getMachineId; --switch (process.platform) { -- case 'darwin': -- ({ getMachineId } = require('./getMachineId-darwin')); -- break; -- case 'linux': -- ({ getMachineId } = require('./getMachineId-linux')); -- break; -- case 'freebsd': -- ({ getMachineId } = require('./getMachineId-bsd')); -- break; -- case 'win32': -- ({ getMachineId } = require('./getMachineId-win')); -- break; -+function getMachineId() { -+ switch (process.platform) { -+ case "darwin": -+ return import("./getMachineId-darwin").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "linux": -+ return import("./getMachineId-linux").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "freebsd": -+ return import("./getMachineId-bsd").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "win32": -+ return import("./getMachineId-win").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); - default: -- ({ getMachineId } = require('./getMachineId-unsupported')); -+ return import("./getMachineId-unsupported").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ } - } - export { getMachineId }; - //# sourceMappingURL=getMachineId.js.map -\ No newline at end of file -diff --git a/build/src/detectors/platform/node/machine-id/getMachineId.js b/build/src/detectors/platform/node/machine-id/getMachineId.js -index 9c1877c31fda380b7c6e64491400972fb8dc297c..b9f75f108d4e9ec3b1abce02184fe5dbd6964f2d 100644 ---- a/build/src/detectors/platform/node/machine-id/getMachineId.js -+++ b/build/src/detectors/platform/node/machine-id/getMachineId.js -@@ -1,6 +1,5 @@ - "use strict"; - Object.defineProperty(exports, "__esModule", { value: true }); --exports.getMachineId = void 0; - /* - * Copyright The OpenTelemetry Authors - * -@@ -16,23 +15,33 @@ exports.getMachineId = void 0; - * See the License for the specific language governing permissions and - * limitations under the License. - */ --const process = require("process"); --let getMachineId; --exports.getMachineId = getMachineId; --switch (process.platform) { -- case 'darwin': -- (exports.getMachineId = getMachineId = require('./getMachineId-darwin').getMachineId); -- break; -- case 'linux': -- (exports.getMachineId = getMachineId = require('./getMachineId-linux').getMachineId); -- break; -- case 'freebsd': -- (exports.getMachineId = getMachineId = require('./getMachineId-bsd').getMachineId); -- break; -- case 'win32': -- (exports.getMachineId = getMachineId = require('./getMachineId-win').getMachineId); -- break; -+exports.getMachineId = function getMachineId() { -+ switch (process.platform) { -+ case "darwin": -+ return import("./getMachineId-darwin").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "linux": -+ return import("./getMachineId-linux").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "freebsd": -+ return import("./getMachineId-bsd").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ case "win32": -+ return import("./getMachineId-win").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); - default: -- (exports.getMachineId = getMachineId = require('./getMachineId-unsupported').getMachineId); -+ return import("./getMachineId-unsupported").then((m) => { -+ const getMachineId = m.default?.getMachineId || m.getMachineId; -+ return getMachineId(); -+ }); -+ } - } --//# sourceMappingURL=getMachineId.js.map -\ No newline at end of file -+ diff --git a/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch b/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch new file mode 100644 index 000000000..65aed5508 --- /dev/null +++ b/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch @@ -0,0 +1,39 @@ +diff --git a/build/esm/Span.js b/build/esm/Span.js +index 185835fdc5667eddb072891618607ce213bb6625..5554c8bde3f6f44504587a88e115104a01d39ec4 100644 +--- a/build/esm/Span.js ++++ b/build/esm/Span.js +@@ -66,7 +66,7 @@ export class SpanImpl { + this.parentSpanContext = opts.parentSpanContext; + this.kind = opts.kind; + this.links = opts.links || []; +- this.startTime = this._getTime(opts.startTime ?? now); ++ this.startTime = this._getTime(opts.startTime ?? hrTime(this._performanceStartTime + this._performanceOffset)); + this.resource = opts.resource; + this.instrumentationScope = opts.scope; + if (opts.attributes != null) { +diff --git a/build/esnext/Span.js b/build/esnext/Span.js +index 185835fdc5667eddb072891618607ce213bb6625..5554c8bde3f6f44504587a88e115104a01d39ec4 100644 +--- a/build/esnext/Span.js ++++ b/build/esnext/Span.js +@@ -66,7 +66,7 @@ export class SpanImpl { + this.parentSpanContext = opts.parentSpanContext; + this.kind = opts.kind; + this.links = opts.links || []; +- this.startTime = this._getTime(opts.startTime ?? now); ++ this.startTime = this._getTime(opts.startTime ?? hrTime(this._performanceStartTime + this._performanceOffset)); + this.resource = opts.resource; + this.instrumentationScope = opts.scope; + if (opts.attributes != null) { +diff --git a/build/src/Span.js b/build/src/Span.js +index 5a807fe223a70e5e077b66ad74b8efe2eaabd8fa..6388deff9c1e83946f16b04cbfaf884b6f350d29 100644 +--- a/build/src/Span.js ++++ b/build/src/Span.js +@@ -69,7 +69,7 @@ class SpanImpl { + this.parentSpanContext = opts.parentSpanContext; + this.kind = opts.kind; + this.links = opts.links || []; +- this.startTime = this._getTime(opts.startTime ?? now); ++ this.startTime = this._getTime(opts.startTime ?? hrTime(this._performanceStartTime + this._performanceOffset)); + this.resource = opts.resource; + this.instrumentationScope = opts.scope; + if (opts.attributes != null) { diff --git a/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch b/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch new file mode 100644 index 000000000..4c3533f83 --- /dev/null +++ b/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch @@ -0,0 +1,13 @@ +diff --git a/build/src/Span.js b/build/src/Span.js +index 12c33551fa559f3657e1d9074cadb34b7e73a675..63ac0075b94eae4c519d94c5c141c7002d21e412 100644 +--- a/build/src/Span.js ++++ b/build/src/Span.js +@@ -69,7 +69,7 @@ class SpanImpl { + this.parentSpanContext = opts.parentSpanContext; + this.kind = opts.kind; + this.links = opts.links || []; +- this.startTime = this._getTime(opts.startTime ?? hrTime(this._performanceStartTime + this._performanceOffset)); ++ this.startTime = this._getTime(opts.startTime ?? core_1.hrTime(this._performanceStartTime + this._performanceOffset)); + this.resource = opts.resource; + this.instrumentationScope = opts.scope; + if (opts.attributes != null) { diff --git a/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch b/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch new file mode 100644 index 000000000..08444c65f --- /dev/null +++ b/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch @@ -0,0 +1,48 @@ +diff --git a/dist/es/index.js b/dist/es/index.js +index 505218ea7b14fb8773fe805b622f9071d5da59dd..263631008120b7bf2bd678220dca6394ed30c026 100644 +--- a/dist/es/index.js ++++ b/dist/es/index.js +@@ -478,28 +478,10 @@ async function resolvePackageTarget(context, { target, patternMatch, isImports } + } + // Otherwise, if target is a non-null Object, then + if (target && typeof target === 'object') { +- // For each property of target +- for (const [key, value] of Object.entries(target)) { +- // If exports contains any index property keys, as defined in ECMA-262 6.1.7 Array Index, throw an Invalid Package Configuration error. +- // TODO: We do not check if the key is a number here... +- // If key equals "default" or conditions contains an entry for the key, then +- if (key === 'default' || context.conditions.includes(key)) { +- // Let targetValue be the value of the property in target. +- // Let resolved be the result of PACKAGE_TARGET_RESOLVE of the targetValue +- const resolved = await resolvePackageTarget(context, { +- target: value, +- patternMatch, +- isImports +- }); +- // If resolved is equal to undefined, continue the loop. +- // Return resolved. +- if (resolved !== undefined) { +- return resolved; +- } +- } +- } +- // Return undefined. +- return undefined; ++ const key = context.conditions.find(condition => condition in target) ?? 'default'; ++ return key in target ++ ? resolvePackageTarget(context, { target: target[key], patternMatch, isImports }) ++ : undefined; + } + // Otherwise, if target is null, return null. + if (target === null) { +@@ -1072,8 +1054,8 @@ function nodeResolve(opts = {}) { + ? 'development' + : 'production' + ]; +- const conditionsEsm = [...baseConditionsEsm, ...exportConditions, ...devProdCondition]; +- const conditionsCjs = [...baseConditionsCjs, ...exportConditions, ...devProdCondition]; ++ const conditionsEsm = [...baseConditionsEsm, ...exportConditions, ...devProdCondition].reverse(); ++ const conditionsCjs = [...baseConditionsCjs, ...exportConditions, ...devProdCondition].reverse(); + const packageInfoCache = new Map(); + const idToPackageInfo = new Map(); + const mainFields = getMainFields(options); diff --git a/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch b/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch new file mode 100644 index 000000000..2e834077d --- /dev/null +++ b/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch @@ -0,0 +1,15 @@ +diff --git a/lib/ansi-color.js b/lib/ansi-color.js +index 1062c87b46c5515a2d0c813b38de333b88149d0b..9d8838916773350f6e726ee96bccbe235cef4ce5 100644 +--- a/lib/ansi-color.js ++++ b/lib/ansi-color.js +@@ -32,8 +32,8 @@ exports.set = function(str, color) { + var color_attrs = color.split("+"); + var ansi_str = ""; + for(var i=0, attr; attr = color_attrs[i]; i++) { +- ansi_str += "\033[" + ANSI_CODES[attr] + "m"; ++ ansi_str += "\x1b[" + ANSI_CODES[attr] + "m"; + } +- ansi_str += str + "\033[" + ANSI_CODES["off"] + "m"; ++ansi_str += str + "\x1b[" + ANSI_CODES["off"] + "m"; + return ansi_str; + }; diff --git a/DEPS_RESOLUTIONS_NOTES.md b/DEPS_RESOLUTIONS_NOTES.md index 776a89e1d..d780a88c5 100644 --- a/DEPS_RESOLUTIONS_NOTES.md +++ b/DEPS_RESOLUTIONS_NOTES.md @@ -5,7 +5,7 @@ Here we collect reasons and write explanations about why some resolutions or pat ### pkgroll 1. https://github.com/privatenumber/pkgroll/issues/101 (added `interop: "auto"` to `getRollupConfigs` outputs) -1. Skip libchecking while generating type declarations because we never bundle `@types` by disabling `respectExternal` ([read more](https://github.com/Swatinem/rollup-plugin-dts?tab=readme-ov-file#what-to-expect)) +2. Skip libchecking while generating type declarations because we never bundle `@types` by disabling `respectExternal` ([read more](https://github.com/Swatinem/rollup-plugin-dts?tab=readme-ov-file#what-to-expect)) ### tsx @@ -14,3 +14,16 @@ Here we collect reasons and write explanations about why some resolutions or pat ### vitest-tsconfig-paths 1. Resolve tsconfig paths in modules that have been [inlined](https://vitest.dev/config/#server-deps-inline). + +### @opentelemetry/otlp-exporter-base + +1. Use `import` instead of `require` for dynamic resolution in ES Module + +### @rollup/plugin-node-resolve + +1. Give priority to additional `exportConditions` provided in configuration. This allows to not patch most OTEL packages to point to esnext build. + +### ansi-color + +1. Used by OTEl packages (NodeSDK). +2. Contains a legacy byte code syntax, forbidden in strict mode used by Hive Gateway. diff --git a/babel.config.cjs b/babel.config.cjs index b5586a0c4..3bb0a78f3 100644 --- a/babel.config.cjs +++ b/babel.config.cjs @@ -11,5 +11,6 @@ module.exports = { ['@babel/plugin-proposal-decorators', { version: '2023-11' }], '@babel/plugin-transform-class-properties', '@babel/plugin-proposal-explicit-resource-management', + '@babel/plugin-transform-private-methods', ], }; diff --git a/e2e/cloudflare-workers/cloudflare-workers.e2e.ts b/e2e/cloudflare-workers/cloudflare-workers.e2e.ts index 3b99bc2a9..445ce1cb4 100644 --- a/e2e/cloudflare-workers/cloudflare-workers.e2e.ts +++ b/e2e/cloudflare-workers/cloudflare-workers.e2e.ts @@ -41,15 +41,18 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( type JaegerTracesApiResponse = { data: Array<{ traceID: string; - spans: Array<{ - traceID: string; - spanID: string; - operationName: string; - tags: Array<{ key: string; value: string; type: string }>; - }>; + spans: JaegerTraceSpan[]; }>; }; + type JaegerTraceSpan = { + traceID: string; + spanID: string; + operationName: string; + tags: Array<{ key: string; value: string; type: string }>; + references: Array<{ refType: string; spanID: string; traceID: string }>; + }; + async function getJaegerTraces( service: string, expectedDataLength: number, @@ -58,7 +61,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( const url = `http://0.0.0.0:${jaeger.additionalPorts[16686]}/api/traces?service=${service}`; let res!: JaegerTracesApiResponse; - const signal = AbortSignal.timeout(2000); + const signal = AbortSignal.timeout(2_000); while (!signal.aborted) { try { res = await fetch(url).then((r) => r.json()); @@ -77,7 +80,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( async function wrangler(env: { OTLP_EXPORTER_URL: string; - OTLP_SERVICE_NAME: string; + OTEL_SERVICE_NAME: string; }) { const port = await getAvailablePort(); await spawn([ @@ -89,7 +92,9 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( '--var', 'OTLP_EXPORTER_URL:' + env.OTLP_EXPORTER_URL, '--var', - 'OTLP_SERVICE_NAME:' + env.OTLP_SERVICE_NAME, + 'OTEL_SERVICE_NAME:' + env.OTEL_SERVICE_NAME, + '--var', + 'OTEL_LOG_LEVEL:debug', ...(isDebug() ? ['--var', 'DEBUG:1'] : []), ]); const hostname = await getLocalhost(port); @@ -122,7 +127,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( const serviceName = 'mesh-e2e-test-1'; const { execute } = await wrangler({ OTLP_EXPORTER_URL: `${jaegerHostname}:${jaeger.port}/v1/traces`, - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, }); await expect(execute({ query: TEST_QUERY })).resolves @@ -144,7 +149,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( expect(relevantTraces.length).toBe(1); const relevantTrace = relevantTraces[0]; expect(relevantTrace).toBeDefined(); - expect(relevantTrace?.spans.length).toBe(5); + expect(relevantTrace?.spans.length).toBe(8); expect(relevantTrace?.spans).toContainEqual( expect.objectContaining({ operationName: 'POST /graphql' }), @@ -169,7 +174,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( const serviceName = 'mesh-e2e-test-4'; const { url } = await wrangler({ OTLP_EXPORTER_URL: `${jaegerHostname}:${jaeger.port}/v1/traces`, - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, }); await fetch(`${url}/non-existing`).catch(() => {}); @@ -207,7 +212,7 @@ describe.skipIf(gatewayRunner !== 'node' || process.version.startsWith('v1'))( const serviceName = 'mesh-e2e-test-5'; const { url, execute } = await wrangler({ OTLP_EXPORTER_URL: `${jaegerHostname}:${jaeger.port}/v1/traces`, - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, }); await expect( diff --git a/e2e/cloudflare-workers/src/index.ts b/e2e/cloudflare-workers/src/index.ts index ae9acc629..211b7bbe4 100644 --- a/e2e/cloudflare-workers/src/index.ts +++ b/e2e/cloudflare-workers/src/index.ts @@ -1,19 +1,16 @@ import { ExportedHandler, Response } from '@cloudflare/workers-types'; import { createGatewayRuntime, - DisposableSymbols, GatewayPlugin, } from '@graphql-hive/gateway-runtime'; -import { - createOtlpHttpExporter, - useOpenTelemetry, -} from '@graphql-mesh/plugin-opentelemetry'; +import { useOpenTelemetry } from '@graphql-mesh/plugin-opentelemetry'; +import { openTelemetrySetup } from '@graphql-mesh/plugin-opentelemetry/setup'; import http from '@graphql-mesh/transport-http'; -import { fakePromise } from '@graphql-tools/utils'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; interface Env { OTLP_EXPORTER_URL: string; - OTLP_SERVICE_NAME: string; + OTEL_SERVICE_NAME: string; DEBUG: string; } @@ -37,36 +34,33 @@ const useOnFetchTracer = (): GatewayPlugin => { }; }; -export default { - async fetch(req, env, ctx) { - const runtime = createGatewayRuntime({ - proxy: { - endpoint: 'https://countries.trevorblades.com', - }, - transports: { - http, +let runtime: ReturnType; +function getRuntime(env: Env) { + if (!runtime) { + openTelemetrySetup({ + contextManager: null, + resource: { serviceName: env.OTEL_SERVICE_NAME, serviceVersion: '1.0.0' }, + traces: { + exporter: new OTLPTraceExporter({ url: env['OTLP_EXPORTER_URL'] }), + batching: false, // Disable batching to speedup tests }, + }); + + runtime = createGatewayRuntime({ + proxy: { endpoint: 'https://countries.trevorblades.com' }, + transports: { http }, plugins: (ctx) => [ - useOpenTelemetry({ - ...ctx, - exporters: [ - createOtlpHttpExporter( - { - url: env['OTLP_EXPORTER_URL'], - }, - // Batching config is set in order to make it easier to test. - { - scheduledDelayMillis: 1, - }, - ), - ], - serviceName: env['OTLP_SERVICE_NAME'], - }), + useOpenTelemetry({ ...ctx, traces: true }), useOnFetchTracer(), ], }); - const res = await runtime(req, env, ctx); - ctx.waitUntil(fakePromise(runtime[DisposableSymbols.asyncDispose]())); + } + return runtime; +} + +export default { + async fetch(req, env, ctx) { + const res = await getRuntime(env)(req, env, ctx); return res as unknown as Response; }, } satisfies ExportedHandler; diff --git a/e2e/config-syntax-error/config-syntax-error.e2e.ts b/e2e/config-syntax-error/config-syntax-error.e2e.ts index d168008eb..433d0cb74 100644 --- a/e2e/config-syntax-error/config-syntax-error.e2e.ts +++ b/e2e/config-syntax-error/config-syntax-error.e2e.ts @@ -31,8 +31,8 @@ it.skipIf( }), ).rejects.toThrowError( gatewayRunner === 'bun' || gatewayRunner === 'bun-docker' - ? /error: Expected "{" but found "hello"(.|\n)*\/custom-resolvers.ts:8:11/ - : /SyntaxError \[Error\]: Error transforming .*(\/|\\)custom-resolvers.ts: Unexpected token, expected "{" \(8:11\)/, + ? /Expected \\"{\\" but found \\"hello\\"(.|\n)*\/custom-resolvers.ts/ + : /Error transforming .*(\/|\\)custom-resolvers.ts: Unexpected token, expected \\"{\\" \(8:11\)/, ); }, ); diff --git a/e2e/file-upload/gateway.config.ts b/e2e/file-upload/gateway.config.ts new file mode 100644 index 000000000..1d12e95a7 --- /dev/null +++ b/e2e/file-upload/gateway.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@graphql-hive/gateway'; + +export const gatewayConfig = defineConfig({ + multipart: true, +}); diff --git a/e2e/graphos-polling/services/gateway-fastify.ts b/e2e/graphos-polling/services/gateway-fastify.ts index 2e038bab7..329fedcd8 100644 --- a/e2e/graphos-polling/services/gateway-fastify.ts +++ b/e2e/graphos-polling/services/gateway-fastify.ts @@ -1,10 +1,10 @@ -import { createGatewayRuntime } from '@graphql-hive/gateway-runtime'; -import { createLoggerFromPino } from '@graphql-hive/logger-pino'; -import { - createOtlpHttpExporter, - useOpenTelemetry, -} from '@graphql-mesh/plugin-opentelemetry'; +import { createGatewayRuntime, Logger } from '@graphql-hive/gateway-runtime'; +import { PinoLogWriter } from '@graphql-hive/logger/writers/pino'; +import { useOpenTelemetry } from '@graphql-mesh/plugin-opentelemetry'; +import { openTelemetrySetup } from '@graphql-mesh/plugin-opentelemetry/setup'; import { Opts } from '@internal/testing'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; import fastify, { type FastifyReply, type FastifyRequest } from 'fastify'; /* --- E2E TEST SPECIFIC CONFIGURATION START--- */ @@ -17,6 +17,15 @@ const port = opts.getServicePort('gateway-fastify'); /*--- E2E TEST SPECIFIC CONFIGURATION END--- */ +openTelemetrySetup({ + contextManager: new AsyncLocalStorageContextManager(), + traces: { + exporter: new OTLPTraceExporter({ url: process.env['OTLP_EXPORTER_URL'] }), + // Do not batch for test + batching: false, + }, +}); + const requestIdHeader = 'x-guild-request-id'; const app = fastify({ @@ -43,8 +52,7 @@ export interface FastifyContext { } const gw = createGatewayRuntime({ - // Integrate Fastify's logger / Pino with the gateway logger - logging: createLoggerFromPino(app.log), + logging: new Logger({ writers: [new PinoLogWriter(app.log)] }), // Align with Fastify requestId: { // Use the same header name as Fastify @@ -72,16 +80,7 @@ const gw = createGatewayRuntime({ plugins: (ctx) => [ useOpenTelemetry({ ...ctx, - exporters: [ - createOtlpHttpExporter( - { - url: process.env['OTLP_EXPORTER_URL'], - }, - { - scheduledDelayMillis: 1, - }, - ), - ], + traces: true, }), ], }); diff --git a/e2e/header-propagation/gateway.config.ts b/e2e/header-propagation/gateway.config.ts index 4cd0b68d2..cf13366b2 100644 --- a/e2e/header-propagation/gateway.config.ts +++ b/e2e/header-propagation/gateway.config.ts @@ -2,7 +2,11 @@ import { defineConfig } from '@graphql-hive/gateway'; export const gatewayConfig = defineConfig({ propagateHeaders: { - fromClientToSubgraphs({ request }) { + fromClientToSubgraphs({ request, subgraphName }) { + if (subgraphName !== 'upstream') { + return; + } + return { authorization: request.headers.get('authorization') ?? 'default', 'session-cookie-id': diff --git a/e2e/header-propagation/header-propagation.e2e.ts b/e2e/header-propagation/header-propagation.e2e.ts index f1fd0cf6f..0af48dc74 100644 --- a/e2e/header-propagation/header-propagation.e2e.ts +++ b/e2e/header-propagation/header-propagation.e2e.ts @@ -34,6 +34,41 @@ it('propagates headers to subgraphs', async () => { }); }); +it('propagates headers to subgraphs with batching', async () => { + await using gw = await gateway({ + supergraph: { + with: 'apollo', + services: [await service('upstream')], + }, + }); + const result = await gw.execute({ + query: /* GraphQL */ ` + query { + h1: headers { + sessionCookieId + } + h2: headers { + authorization + } + } + `, + headers: { + authorization: 'Bearer token', + 'session-cookie-id': 'session-cookie', + }, + }); + expect(result).toEqual({ + data: { + h1: { + sessionCookieId: 'session-cookie', + }, + h2: { + authorization: 'Bearer token', + }, + }, + }); +}); + it('sends default headers to subgraphs', async () => { await using gw = await gateway({ supergraph: { diff --git a/e2e/header-propagation/services/upstream.ts b/e2e/header-propagation/services/upstream.ts index cbe43198c..c42692488 100644 --- a/e2e/header-propagation/services/upstream.ts +++ b/e2e/header-propagation/services/upstream.ts @@ -31,8 +31,8 @@ const server = new ApolloServer({ } type Headers { - authorization: String! - sessionCookieId: String! + authorization: String + sessionCookieId: String } `), resolvers: resolvers as GraphQLResolverMap, diff --git a/e2e/hmac-auth-https/package.json b/e2e/hmac-auth-https/package.json index 17da7e851..5e32630a7 100644 --- a/e2e/hmac-auth-https/package.json +++ b/e2e/hmac-auth-https/package.json @@ -9,6 +9,7 @@ "@apollo/server": "^4.12.2", "@apollo/subgraph": "^2.11.2", "@graphql-hive/gateway": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-mesh/compose-cli": "^1.4.12", "@graphql-mesh/hmac-upstream-signature": "workspace:^", "@graphql-mesh/plugin-jwt-auth": "workspace:^", diff --git a/e2e/hmac-auth-https/services/users/index.ts b/e2e/hmac-auth-https/services/users/index.ts index c77a92e52..a3d375c0c 100644 --- a/e2e/hmac-auth-https/services/users/index.ts +++ b/e2e/hmac-auth-https/services/users/index.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { createServer } from 'https'; import { join } from 'path'; import { buildSubgraphSchema } from '@apollo/subgraph'; +import { Logger } from '@graphql-hive/logger'; import { useHmacSignatureValidation } from '@graphql-mesh/hmac-upstream-signature'; import { JWTExtendContextFields, @@ -20,6 +21,7 @@ const yoga = createYoga({ logging: true, plugins: [ useHmacSignatureValidation({ + log: new Logger({ level: 'debug' }), secret: 'HMAC_SIGNING_SECRET', }), useForwardedJWT({}), diff --git a/e2e/load-on-init/load-on-init.e2e.ts b/e2e/load-on-init/load-on-init.e2e.ts new file mode 100644 index 000000000..e722056d7 --- /dev/null +++ b/e2e/load-on-init/load-on-init.e2e.ts @@ -0,0 +1,29 @@ +import path from 'node:path'; +import { createTenv } from '@internal/e2e'; +import { expect, it } from 'vitest'; + +const { gateway } = createTenv(__dirname); + +it('should load the supergraph on init', async () => { + await expect( + gateway({ + supergraph: path.join(__dirname, 'malformed.graphql'), + }), + ).rejects.toThrowError(/Syntax Error: Unexpected Name \\"skema\\"./); +}); + +it('should load the subgraph on init', async () => { + await expect( + gateway({ + subgraph: path.join(__dirname, 'malformed.graphql'), + }), + ).rejects.toThrowError(/Syntax Error: Unexpected Name \\"skema\\"./); +}); + +it('should load the proxy schema on init', async () => { + await expect( + gateway({ + args: ['proxy', 'http://localhost:65432'], + }), + ).rejects.toThrowError(/DOWNSTREAM_SERVICE_ERROR/); +}); diff --git a/e2e/load-on-init/malformed.graphql b/e2e/load-on-init/malformed.graphql new file mode 100644 index 000000000..ba5348aa5 --- /dev/null +++ b/e2e/load-on-init/malformed.graphql @@ -0,0 +1,3 @@ +skema { + query: Query +} diff --git a/e2e/load-on-init/package.json b/e2e/load-on-init/package.json new file mode 100644 index 000000000..b6e033a82 --- /dev/null +++ b/e2e/load-on-init/package.json @@ -0,0 +1,4 @@ +{ + "name": "@e2e/load-on-init", + "private": true +} diff --git a/e2e/opentelemetry/gateway.config.ts b/e2e/opentelemetry/gateway.config.ts index 071b4fd24..d6f95f82a 100644 --- a/e2e/opentelemetry/gateway.config.ts +++ b/e2e/opentelemetry/gateway.config.ts @@ -1,10 +1,13 @@ -import { - createOtlpGrpcExporter, - createOtlpHttpExporter, - defineConfig, - GatewayPlugin, -} from '@graphql-hive/gateway'; +import { defineConfig, GatewayPlugin } from '@graphql-hive/gateway'; +import { openTelemetrySetup } from '@graphql-mesh/plugin-opentelemetry/setup'; import type { MeshFetchRequestInit } from '@graphql-mesh/types'; +import { trace } from '@opentelemetry/api'; +import { + getNodeAutoInstrumentations, + getResourceDetectors, +} from '@opentelemetry/auto-instrumentations-node'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { NodeSDK, resources, tracing } from '@opentelemetry/sdk-node'; // The following plugin is used to trace the fetch calls made by Mesh. const useOnFetchTracer = (): GatewayPlugin => { @@ -26,35 +29,62 @@ const useOnFetchTracer = (): GatewayPlugin => { }; }; +if (process.env['DISABLE_OPENTELEMETRY_SETUP'] !== '1') { + const { OTLPTraceExporter } = + process.env['OTLP_EXPORTER_TYPE'] === 'http' + ? await import(`@opentelemetry/exporter-trace-otlp-http`) + : await import(`@opentelemetry/exporter-trace-otlp-grpc`); + + const exporter = new OTLPTraceExporter({ + url: process.env['OTLP_EXPORTER_URL'], + }); + + const resource = resources.resourceFromAttributes({ + 'custom.resource': 'custom value', + }); + + // The NodeSDK only actually work in Node. For other envs, it's better to use our own configurator + const runner = process.env['E2E_GATEWAY_RUNNER']; + if (runner === 'node' || runner === 'docker') { + const sdk = new NodeSDK({ + // Use spanProcessor instead of spanExporter to remove batching for test speed + spanProcessors: [new tracing.SimpleSpanProcessor(exporter)], + resource, + instrumentations: getNodeAutoInstrumentations(), + resourceDetectors: getResourceDetectors(), + }); + + sdk.start(); + ['SIGTERM', 'SIGINT'].forEach((sig) => + process.on(sig, () => sdk.shutdown()), + ); + } else { + openTelemetrySetup({ + contextManager: new AsyncLocalStorageContextManager(), + resource, + traces: { + exporter, + // Disable batching to speedup tests + batching: false, + }, + }); + } +} + export const gatewayConfig = defineConfig({ openTelemetry: { - exporters: [ - process.env['OTLP_EXPORTER_TYPE'] === 'grpc' - ? createOtlpGrpcExporter( - { - url: process.env['OTLP_EXPORTER_URL'], - }, - // Batching config is set in order to make it easier to test. - { - scheduledDelayMillis: 1, - }, - ) - : createOtlpHttpExporter( - { - url: process.env['OTLP_EXPORTER_URL'], - }, - // Batching config is set in order to make it easier to test. - { - scheduledDelayMillis: 1, - }, - ), - ], - serviceName: process.env['OTLP_SERVICE_NAME'], + traces: true, }, - plugins: () => - process.env['MEMTEST'] + plugins: () => [ + { + onExecute() { + trace.getActiveSpan()?.setAttribute('custom.attribute', 'custom value'); + }, + }, + ...(process.env['MEMTEST'] ? [ // disable the plugin in memtests because the upstreamCallHeaders will grew forever reporting a false positive leak ] - : [useOnFetchTracer()], + : [useOnFetchTracer()]), + ], }); diff --git a/e2e/opentelemetry/opentelemetry.e2e.ts b/e2e/opentelemetry/opentelemetry.e2e.ts index 8536264a9..e9851ad68 100644 --- a/e2e/opentelemetry/opentelemetry.e2e.ts +++ b/e2e/opentelemetry/opentelemetry.e2e.ts @@ -17,6 +17,17 @@ const JAEGER_HOSTNAME = const exampleSetup = createExampleSetup(__dirname); +const runner = { + docker: { + volumes: [ + { + host: __dirname + '/otel-setup.ts', + container: `/gateway/otel-setup.ts`, + }, + ], + }, +}; + beforeAll(async () => { supergraph = await exampleSetup.supergraph(); }); @@ -24,17 +35,32 @@ beforeAll(async () => { type JaegerTracesApiResponse = { data: Array<{ traceID: string; - spans: Array<{ - traceID: string; - spanID: string; - operationName: string; - tags: Array<{ key: string; value: string; type: string }>; - }>; + spans: JaegerTraceSpan[]; + processes: { [key: string]: JaegerTraceResource }; }>; }; +type JaegerTraceTag = { + key: string; + type: string; + value: string; +}; + +type JaegerTraceResource = { + serviceName: string; + tags: JaegerTraceTag[]; +}; + +type JaegerTraceSpan = { + traceID: string; + spanID: string; + operationName: string; + tags: Array; + references: Array<{ refType: string; spanID: string; traceID: string }>; +}; + describe('OpenTelemetry', () => { - (['grpc', 'http'] as const).forEach((OTLP_EXPORTER_TYPE) => { + (['http'] as const).forEach((OTLP_EXPORTER_TYPE) => { describe(`exporter > ${OTLP_EXPORTER_TYPE}`, () => { let jaeger: Container; beforeAll(async () => { @@ -64,19 +90,56 @@ describe('OpenTelemetry', () => { async function expectJaegerTraces( service: string, - checkFn: (res: JaegerTracesApiResponse) => void | PromiseLike, + checkFn: ( + res: JaegerTracesApiResponse, + abort: AbortController, + ) => void | PromiseLike, ): Promise { const url = `http://0.0.0.0:${jaeger.additionalPorts[16686]}/api/traces?service=${service}`; let res!: JaegerTracesApiResponse; let err: any; - const signal = AbortSignal.timeout(15_000); + const timeout = AbortSignal.timeout(15_000); + const abort = new AbortController(); + const signal = AbortSignal.any([timeout, abort.signal]); while (!signal.aborted) { try { res = await fetch(url, { signal }).then((r) => r.json()); - await checkFn(res); + await checkFn(res, abort); return; } catch (e) { + if (signal.aborted) { + const relevantTrace = res.data.find((trace) => + trace.spans.some( + (span) => span.operationName === 'POST /graphql', + ), + ); + const actualError = timeout.aborted ? err : e; + console.error( + actualError, + '\nTraces was:', + Object.fromEntries( + res.data.map(({ traceID, spans }) => [ + traceID, + spans.map((s) => s.operationName), + ]), + ), + '\nSpan tree was:', + relevantTrace + ? '\n' + + printSpanTree( + buildSpanTree(relevantTrace.spans, 'POST /graphql'), + ) + : 'no trace containing "POST /graphql" span found', + ); + throw actualError; + } + if (abort.signal.aborted) { + throw e; + } + if (timeout.aborted) { + throw err; + } err = e; } } @@ -85,11 +148,13 @@ describe('OpenTelemetry', () => { it('should report telemetry metrics correctly to jaeger', async () => { const serviceName = 'mesh-e2e-test-1'; const { execute } = await gateway({ + runner, supergraph, env: { OTLP_EXPORTER_TYPE, OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE], - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', }, }); @@ -552,58 +617,226 @@ describe('OpenTelemetry', () => { }, }); await expectJaegerTraces(serviceName, (traces) => { - expect(traces.data.length).toBe(2); const relevantTraces = traces.data.filter((trace) => trace.spans.some((span) => span.operationName === 'POST /graphql'), ); expect(relevantTraces.length).toBe(1); const relevantTrace = relevantTraces[0]; expect(relevantTrace).toBeDefined(); - expect(relevantTrace?.spans.length).toBe(11); + expect(relevantTrace!.spans.length).toBe(20); - expect(relevantTrace?.spans).toContainEqual( - expect.objectContaining({ operationName: 'POST /graphql' }), - ); - expect(relevantTrace?.spans).toContainEqual( - expect.objectContaining({ operationName: 'graphql.parse' }), + const resource = relevantTrace!.processes['p1']; + expect(resource).toBeDefined(); + + const tags = resource!.tags.map(({ key, value }) => ({ key, value })); + const tagKeys = resource!.tags.map(({ key }) => key); + expect(resource!.serviceName).toBe(serviceName); + [ + ['custom.resource', 'custom value'], + ['otel.library.name', 'gateway'], + ].forEach(([key, value]) => { + return expect(tags).toContainEqual({ key, value }); + }); + + if ( + process.env['E2E_GATEWAY_RUNNER'] === 'node' || + process.env['E2E_GATEWAY_RUNNER'] === 'docker' + ) { + const expectedTags = [ + 'process.owner', + 'host.arch', + 'os.type', + 'service.instance.id', + ]; + if (process.env['E2E_GATEWAY_RUNNER'] === 'docker') { + expectedTags.push('container.id'); + } + expectedTags.forEach((key) => { + return expect(tagKeys).toContain(key); + }); + } + + const spanTree = buildSpanTree(relevantTrace!.spans, 'POST /graphql'); + expect(spanTree).toBeDefined(); + + expect(spanTree!.children).toHaveLength(1); + + const operationSpan = spanTree!.children[0]; + const expectedOperationChildren = [ + 'graphql.parse', + 'graphql.validate', + 'graphql.context', + 'graphql.execute', + ]; + expect(operationSpan!.children).toHaveLength(4); + for (const operationName of expectedOperationChildren) { + expect(operationSpan?.children).toContainEqual( + expect.objectContaining({ + span: expect.objectContaining({ operationName }), + }), + ); + } + + expect( + operationSpan!.children + .find(({ span }) => span.operationName === 'graphql.execute') + ?.span.tags.find(({ key }) => key === 'custom.attribute'), + ).toMatchObject({ value: 'custom value' }); + + const executeSpan = operationSpan!.children.find( + ({ span }) => span.operationName === 'graphql.execute', ); - expect(relevantTrace?.spans).toContainEqual( - expect.objectContaining({ operationName: 'graphql.validate' }), + + const expectedExecuteChildren = [ + ['subgraph.execute (accounts)', 2], + ['subgraph.execute (products)', 2], + ['subgraph.execute (inventory)', 1], + ['subgraph.execute (reviews)', 2], + ] as const; + + for (const [operationName, count] of expectedExecuteChildren) { + const matchingChildren = executeSpan!.children.filter( + ({ span }) => span.operationName === operationName, + ); + expect(matchingChildren).toHaveLength(count); + for (const child of matchingChildren) { + expect(child.children).toHaveLength(1); + expect(child.children).toContainEqual( + expect.objectContaining({ + span: expect.objectContaining({ + operationName: 'http.fetch', + }), + }), + ); + } + } + }); + }); + + it('should report telemetry metrics correctly to jaeger using cli options', async () => { + const serviceName = 'mesh-e2e-test-1'; + const { execute } = await gateway({ + runner, + supergraph, + env: { + DISABLED_OPENTELEMETRY_SETUP: '1', + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', + }, + args: [ + '--opentelemetry', + urls[OTLP_EXPORTER_TYPE], + '--opentelemetry-exporter-type', + `otlp-${OTLP_EXPORTER_TYPE}`, + ], + }); + + await expect( + execute({ query: exampleSetup.query }), + ).resolves.toMatchObject({ + data: {}, + }); + await expectJaegerTraces(serviceName, (traces) => { + const relevantTraces = traces.data.filter((trace) => + trace.spans.some((span) => span.operationName === 'POST /graphql'), ); - expect(relevantTrace?.spans).toContainEqual( - expect.objectContaining({ operationName: 'graphql.execute' }), + expect(relevantTraces.length).toBe(1); + const relevantTrace = relevantTraces[0]; + expect(relevantTrace).toBeDefined(); + expect(relevantTrace!.spans.length).toBe(20); + + const resource = relevantTrace!.processes['p1']; + expect(resource).toBeDefined(); + + const tags = resource!.tags.map(({ key, value }) => ({ key, value })); + const tagKeys = resource!.tags.map(({ key }) => key); + expect(resource!.serviceName).toBe(serviceName); + [ + ['custom.resource', 'custom value'], + ['otel.library.name', 'gateway'], + ].forEach(([key, value]) => { + return expect(tags).toContainEqual({ key, value }); + }); + + if ( + process.env['E2E_GATEWAY_RUNNER'] === 'node' || + process.env['E2E_GATEWAY_RUNNER'] === 'docker' + ) { + const expectedTags = [ + 'process.owner', + 'host.arch', + 'os.type', + 'service.instance.id', + ]; + if (process.env['E2E_GATEWAY_RUNNER'] === 'docker') { + expectedTags.push('container.id'); + } + expectedTags.forEach((key) => { + return expect(tagKeys).toContain(key); + }); + } + + const spanTree = buildSpanTree(relevantTrace!.spans, 'POST /graphql'); + expect(spanTree).toBeDefined(); + + expect(spanTree!.children).toHaveLength(1); + + const operationSpan = spanTree!.children[0]; + const expectedOperationChildren = [ + 'graphql.parse', + 'graphql.validate', + 'graphql.context', + 'graphql.execute', + ]; + expect(operationSpan!.children).toHaveLength(4); + for (const operationName of expectedOperationChildren) { + expect(operationSpan?.children).toContainEqual( + expect.objectContaining({ + span: expect.objectContaining({ operationName }), + }), + ); + } + + const executeSpan = operationSpan!.children.find( + ({ span }) => span.operationName === 'graphql.execute', ); - expect( - relevantTrace?.spans.filter( - (r) => r.operationName === 'subgraph.execute (accounts)', - ).length, - ).toBe(2); - expect( - relevantTrace?.spans.filter( - (r) => r.operationName === 'subgraph.execute (products)', - ).length, - ).toBe(2); - expect( - relevantTrace?.spans.filter( - (r) => r.operationName === 'subgraph.execute (inventory)', - ).length, - ).toBe(1); - expect( - relevantTrace?.spans.filter( - (r) => r.operationName === 'subgraph.execute (reviews)', - ).length, - ).toBe(2); + + const expectedExecuteChildren = [ + ['subgraph.execute (accounts)', 2], + ['subgraph.execute (products)', 2], + ['subgraph.execute (inventory)', 1], + ['subgraph.execute (reviews)', 2], + ] as const; + + for (const [operationName, count] of expectedExecuteChildren) { + const matchingChildren = executeSpan!.children.filter( + ({ span }) => span.operationName === operationName, + ); + expect(matchingChildren).toHaveLength(count); + for (const child of matchingChildren) { + expect(child.children).toHaveLength(1); + expect(child.children).toContainEqual( + expect.objectContaining({ + span: expect.objectContaining({ + operationName: 'http.fetch', + }), + }), + ); + } + } }); }); it('should report parse failures correctly', async () => { const serviceName = 'mesh-e2e-test-2'; const { execute } = await gateway({ + runner, supergraph, env: { OTLP_EXPORTER_TYPE, OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE], - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', }, }); @@ -627,12 +860,11 @@ describe('OpenTelemetry', () => { } `); await expectJaegerTraces(serviceName, (traces) => { - expect(traces.data.length).toBe(2); const relevantTrace = traces.data.find((trace) => trace.spans.some((span) => span.operationName === 'POST /graphql'), ); expect(relevantTrace).toBeDefined(); - expect(relevantTrace?.spans.length).toBe(2); + expect(relevantTrace?.spans.length).toBe(3); expect(relevantTrace?.spans).toContainEqual( expect.objectContaining({ operationName: 'POST /graphql' }), @@ -654,7 +886,7 @@ describe('OpenTelemetry', () => { value: 'Syntax Error: Expected Name, found .', }), expect.objectContaining({ - key: 'graphql.error.count', + key: 'hive.graphql.error.count', value: 1, }), ]), @@ -674,11 +906,13 @@ describe('OpenTelemetry', () => { it('should report validate failures correctly', async () => { const serviceName = 'mesh-e2e-test-3'; const { execute } = await gateway({ + runner, supergraph, env: { OTLP_EXPORTER_TYPE, OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE], - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', }, }); @@ -702,12 +936,11 @@ describe('OpenTelemetry', () => { } `); await expectJaegerTraces(serviceName, (traces) => { - expect(traces.data.length).toBe(2); const relevantTrace = traces.data.find((trace) => trace.spans.some((span) => span.operationName === 'POST /graphql'), ); expect(relevantTrace).toBeDefined(); - expect(relevantTrace?.spans.length).toBe(3); + expect(relevantTrace?.spans.length).toBe(4); expect(relevantTrace?.spans).toContainEqual( expect.objectContaining({ operationName: 'POST /graphql' }), @@ -733,7 +966,7 @@ describe('OpenTelemetry', () => { 'Cannot query field "nonExistentField" on type "Query".', }), expect.objectContaining({ - key: 'graphql.error.count', + key: 'hive.graphql.error.count', value: 1, }), ]), @@ -753,17 +986,18 @@ describe('OpenTelemetry', () => { it('should report http failures', async () => { const serviceName = 'mesh-e2e-test-4'; const { port } = await gateway({ + runner, supergraph, env: { OTLP_EXPORTER_TYPE, OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE], - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', }, }); const path = '/non-existing'; await fetch(`http://0.0.0.0:${port}${path}`).catch(() => {}); await expectJaegerTraces(serviceName, (traces) => { - expect(traces.data.length).toBe(2); const relevantTrace = traces.data.find((trace) => trace.spans.some((span) => span.operationName === 'GET ' + path), ); @@ -796,11 +1030,13 @@ describe('OpenTelemetry', () => { const traceId = '0af7651916cd43dd8448eb211c80319c'; const serviceName = 'mesh-e2e-test-5'; const { execute, port } = await gateway({ + runner, supergraph, env: { OTLP_EXPORTER_TYPE, OTLP_EXPORTER_URL: urls[OTLP_EXPORTER_TYPE], - OTLP_SERVICE_NAME: serviceName, + OTEL_SERVICE_NAME: serviceName, + OTEL_SERVICE_VERSION: '1.0.0', }, }); @@ -1281,8 +1517,6 @@ describe('OpenTelemetry', () => { ); await expectJaegerTraces(serviceName, (traces) => { - expect(traces.data.length).toBe(3); - const relevantTraces = traces.data.filter((trace) => trace.spans.some((span) => span.operationName === 'POST /graphql'), ); @@ -1309,3 +1543,39 @@ describe('OpenTelemetry', () => { }); }); }); + +type TraceTreeNode = { + span: JaegerTraceSpan; + children: TraceTreeNode[]; +}; +function buildSpanTree( + spans: JaegerTraceSpan[], + rootName: string, +): TraceTreeNode | undefined { + function buildNode(root: JaegerTraceSpan): TraceTreeNode { + return { + span: root, + children: spans + .filter((span) => + span.references.find( + (ref) => ref.refType === 'CHILD_OF' && ref.spanID === root.spanID, + ), + ) + .map(buildNode), + }; + } + + const root = spans.find((span) => span.operationName === rootName); + return root && buildNode(root); +} + +function printSpanTree(node: TraceTreeNode | undefined, prefix = ''): string { + if (!node) { + return ''; + } + const childrenSting = node.children + .map((c): string => printSpanTree(c, prefix + ' |')) + .join(''); + + return `${prefix}-- ${node.span.operationName}\n${childrenSting}`; +} diff --git a/e2e/retry-timeout/gateway.config.ts b/e2e/retry-timeout/gateway.config.ts index 0d4e508f8..a929fb4ba 100644 --- a/e2e/retry-timeout/gateway.config.ts +++ b/e2e/retry-timeout/gateway.config.ts @@ -6,13 +6,13 @@ export const gatewayConfig = defineConfig({ maxRetries: 4, }, upstreamTimeout: 300, - plugins(ctx) { + plugins() { return [ ...(process.env['DEDUPLICATE_REQUEST'] ? [useDeduplicateRequest()] : []), { - onFetch() { + onFetch({ context }) { i++; - ctx.logger.info(`[FETCHING] #${i}`); + context.log.info(`[FETCHING] #${i}`); }, }, ]; diff --git a/e2e/supergraph-file-watcher/supergraph-file-watcher.e2e.ts b/e2e/supergraph-file-watcher/supergraph-file-watcher.e2e.ts index dcd53d781..ef786e3a0 100644 --- a/e2e/supergraph-file-watcher/supergraph-file-watcher.e2e.ts +++ b/e2e/supergraph-file-watcher/supergraph-file-watcher.e2e.ts @@ -45,7 +45,7 @@ it('should detect supergraph file change and reload schema', async () => { for (;;) { timeout.throwIfAborted(); await setTimeout(100); - if (gw.getStd('both').match(/invalidating supergraph/i)) { + if (gw.getStd('both').match(/supergraph changed/i)) { break; } } diff --git a/examples/file-upload/example.tar.gz b/examples/file-upload/example.tar.gz index ee1e91305..feaa7a435 100644 Binary files a/examples/file-upload/example.tar.gz and b/examples/file-upload/example.tar.gz differ diff --git a/examples/file-upload/gateway.config.ts b/examples/file-upload/gateway.config.ts new file mode 100644 index 000000000..1d12e95a7 --- /dev/null +++ b/examples/file-upload/gateway.config.ts @@ -0,0 +1,5 @@ +import { defineConfig } from '@graphql-hive/gateway'; + +export const gatewayConfig = defineConfig({ + multipart: true, +}); diff --git a/examples/hmac-auth-https/example.tar.gz b/examples/hmac-auth-https/example.tar.gz index 912a3b30c..c15593ec9 100644 Binary files a/examples/hmac-auth-https/example.tar.gz and b/examples/hmac-auth-https/example.tar.gz differ diff --git a/examples/hmac-auth-https/package-lock.json b/examples/hmac-auth-https/package-lock.json index fb863d613..469e07c9a 100644 --- a/examples/hmac-auth-https/package-lock.json +++ b/examples/hmac-auth-https/package-lock.json @@ -11,6 +11,7 @@ "@apollo/server": "^4.12.2", "@apollo/subgraph": "^2.11.2", "@graphql-hive/gateway": "^1.16.3", + "@graphql-hive/logger": "^1.0.0", "@graphql-mesh/compose-cli": "^1.4.12", "@graphql-mesh/hmac-upstream-signature": "^1.2.31", "@graphql-mesh/plugin-jwt-auth": "^1.5.8", @@ -2191,6 +2192,16 @@ "node": ">=18.0.0" } }, + "node_modules/@graphql-hive/logger": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@graphql-hive/logger/-/logger-1.0.0.tgz", + "integrity": "sha512-66kcaBS2td3wA9aUAnKCkDfOiuV8CzTOI1kKmiHzsWiSvrFIrqeOgXd+gb9mTAFl9MFuQTaJfS6qb/jnSv+eOQ==", + "deprecated": "Accidental publish of v1. Please use \"next\" releases instead.", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@graphql-hive/logger-json": { "version": "0.0.7", "resolved": "https://registry.npmjs.org/@graphql-hive/logger-json/-/logger-json-0.0.7.tgz", diff --git a/examples/hmac-auth-https/package.json b/examples/hmac-auth-https/package.json index 2845a1ac6..d5f15c361 100644 --- a/examples/hmac-auth-https/package.json +++ b/examples/hmac-auth-https/package.json @@ -13,6 +13,7 @@ "@apollo/server": "^4.12.2", "@apollo/subgraph": "^2.11.2", "@graphql-hive/gateway": "^1.16.3", + "@graphql-hive/logger": "^1.0.0", "@graphql-mesh/compose-cli": "^1.4.12", "@graphql-mesh/hmac-upstream-signature": "^1.2.31", "@graphql-mesh/plugin-jwt-auth": "^1.5.8", diff --git a/examples/hmac-auth-https/services/users/index.ts b/examples/hmac-auth-https/services/users/index.ts index 22429fd51..c03522fb5 100644 --- a/examples/hmac-auth-https/services/users/index.ts +++ b/examples/hmac-auth-https/services/users/index.ts @@ -2,6 +2,7 @@ import { readFileSync } from 'fs'; import { createServer } from 'https'; import { join } from 'path'; import { buildSubgraphSchema } from '@apollo/subgraph'; +import { Logger } from '@graphql-hive/logger'; import { useHmacSignatureValidation } from '@graphql-mesh/hmac-upstream-signature'; import { JWTExtendContextFields, @@ -19,6 +20,7 @@ const yoga = createYoga({ logging: true, plugins: [ useHmacSignatureValidation({ + log: new Logger({ level: 'debug' }), secret: 'HMAC_SIGNING_SECRET', }), useForwardedJWT({}), diff --git a/internal/e2e/src/tenv.ts b/internal/e2e/src/tenv.ts index 3cbc65ea2..676efef34 100644 --- a/internal/e2e/src/tenv.ts +++ b/internal/e2e/src/tenv.ts @@ -425,8 +425,11 @@ export function createTenv(cwd: string): Tenv { switch (gatewayRunner) { case 'bun-docker': case 'docker': { - const volumes: ContainerOptions['volumes'] = - runner?.docker?.volumes || []; + const volumes: ContainerOptions['volumes'] = []; + + if (runner?.docker?.volumes) { + volumes.push(...runner.docker.volumes); + } if (supergraph) { supergraph = await handleDockerHostNameInURLOrAtPath( diff --git a/package.json b/package.json index 95a293b7f..d1917fdf3 100644 --- a/package.json +++ b/package.json @@ -20,9 +20,10 @@ "check:missing-peer-deps": "yarn install | grep YN0002 && exit 1 || exit 0", "check:types": "yarn tsc", "format": "yarn check:format --write", + "start": "yarn workspace @graphql-hive/gateway start", "test": "vitest --project unit", "test:bun": "bun test --bail", - "test:e2e": "vitest --project e2e", + "test:e2e": "vitest --project e2e --slowTestThreshold 1000", "test:leaks": "cross-env \"LEAK_TEST=1\" jest --detectOpenHandles --detectLeaks", "test:mem": "vitest --project memtest" }, @@ -32,6 +33,7 @@ "@babel/plugin-proposal-explicit-resource-management": "7.27.4", "@babel/plugin-transform-class-properties": "7.27.1", "@babel/plugin-transform-class-static-block": "7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", "@babel/preset-env": "7.28.0", "@babel/preset-typescript": "7.27.1", "@changesets/changelog-github": "^0.5.0", @@ -64,10 +66,11 @@ "@graphql-mesh/types": "0.104.6", "@graphql-mesh/utils": "0.104.6", "@graphql-tools/delegate": "workspace:^", - "@opentelemetry/exporter-trace-otlp-http": "patch:@opentelemetry/exporter-trace-otlp-http@npm:0.56.0#~/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch", - "@opentelemetry/otlp-exporter-base": "patch:@opentelemetry/otlp-exporter-base@npm:0.56.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch", - "@opentelemetry/resources": "patch:@opentelemetry/resources@npm:1.29.0#~/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch", + "@graphql-tools/utils": "10.9.0-alpha-20250710200000-fde1c74a0c2fa4f651cbeed5b2091aeda7afb162", + "@opentelemetry/otlp-exporter-base@npm:0.203.0": "patch:@opentelemetry/otlp-exporter-base@npm%3A0.203.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch", + "@rollup/plugin-node-resolve@npm:^15.2.3": "patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch", "@vitest/snapshot": "patch:@vitest/snapshot@npm:3.1.2#~/.yarn/patches/@vitest-snapshot-npm-3.1.1-4d18cf86dc.patch", + "ansi-color@npm:^0.2.1": "patch:ansi-color@npm%3A0.2.1#~/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch", "brace-expansion": "2.0.2", "cookie": "^1.0.0", "cross-spawn": "7.0.6", @@ -78,6 +81,7 @@ "pkgroll": "patch:pkgroll@npm:2.5.1#~/.yarn/patches/pkgroll-npm-2.5.1-9b062c22ca.patch", "tar-fs": "3.0.10", "tsx": "patch:tsx@npm%3A4.20.3#~/.yarn/patches/tsx-npm-4.20.3-7de67a623f.patch", - "vite": "6.3.5" + "vite": "6.3.5", + "@opentelemetry/sdk-trace-base@npm:2.0.1": "patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch" } } diff --git a/packages/batch-delegate/package.json b/packages/batch-delegate/package.json index ca7af86dd..7173cb047 100644 --- a/packages/batch-delegate/package.json +++ b/packages/batch-delegate/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/batch-execute/package.json b/packages/batch-execute/package.json index 0a25bc523..2a4a0516d 100644 --- a/packages/batch-execute/package.json +++ b/packages/batch-execute/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/batch-execute/src/mergeRequests.ts b/packages/batch-execute/src/mergeRequests.ts index 27ca9811f..a50420174 100644 --- a/packages/batch-execute/src/mergeRequests.ts +++ b/packages/batch-execute/src/mergeRequests.ts @@ -61,6 +61,7 @@ export function mergeRequests( request: ExecutionRequest, ) => Record, ): ExecutionRequest { + const subgraphName = requests[0]!.subgraphName; const mergedVariables: Record = Object.create(null); const mergedVariableDefinitions: Array = []; const mergedSelections: Array = []; @@ -114,6 +115,7 @@ export function mergeRequests( } return { + subgraphName, document: { kind: Kind.DOCUMENT, definitions: [mergedOperationDefinition, ...mergedFragmentDefinitions], diff --git a/packages/delegate/package.json b/packages/delegate/package.json index a62b51832..5ca02ea92 100644 --- a/packages/delegate/package.json +++ b/packages/delegate/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/delegate/src/createRequest.ts b/packages/delegate/src/createRequest.ts index e15eda92d..bea134691 100644 --- a/packages/delegate/src/createRequest.ts +++ b/packages/delegate/src/createRequest.ts @@ -37,6 +37,7 @@ export function getDelegatingOperation( } export function createRequest({ + subgraphName, sourceSchema, sourceParentType, sourceFieldName, @@ -167,6 +168,7 @@ export function createRequest({ }; return { + subgraphName, document, variables: newVariables, rootValue: targetRootValue, diff --git a/packages/delegate/src/delegateToSchema.ts b/packages/delegate/src/delegateToSchema.ts index 23641f4cf..bd587b5e3 100644 --- a/packages/delegate/src/delegateToSchema.ts +++ b/packages/delegate/src/delegateToSchema.ts @@ -54,6 +54,7 @@ export function delegateToSchema< } = options; const request = createRequest({ + subgraphName: (schema as SubschemaConfig).name, sourceSchema: info.schema, sourceParentType: info.parentType, sourceFieldName: info.fieldName, diff --git a/packages/delegate/src/finalizeGatewayRequest.ts b/packages/delegate/src/finalizeGatewayRequest.ts index e9cb37e5f..b637061ab 100644 --- a/packages/delegate/src/finalizeGatewayRequest.ts +++ b/packages/delegate/src/finalizeGatewayRequest.ts @@ -666,9 +666,6 @@ function filterSelectionSet( leave: (node) => { const type = typeInfo.getType(); if (type == null) { - // console.warn( - // `Invalid type for node: ${typeInfo.getParentType()?.name}.${node.name.value}`, - // ); return null; } const namedType = getNamedType(type); diff --git a/packages/delegate/src/types.ts b/packages/delegate/src/types.ts index 812872474..0c864b171 100644 --- a/packages/delegate/src/types.ts +++ b/packages/delegate/src/types.ts @@ -99,6 +99,7 @@ export interface IDelegateRequestOptions< } export interface ICreateRequest { + subgraphName: string | undefined; sourceSchema?: GraphQLSchema; sourceParentType?: GraphQLObjectType; sourceFieldName?: string; diff --git a/packages/delegate/tests/batchExecution.test.ts b/packages/delegate/tests/batchExecution.test.ts index 6ec9a1607..869ae92dc 100644 --- a/packages/delegate/tests/batchExecution.test.ts +++ b/packages/delegate/tests/batchExecution.test.ts @@ -47,9 +47,17 @@ describe('batch execution', () => { resolvers: { Query: { field1: (_parent, _args, context, info) => - delegateToSchema({ schema: innerSubschemaConfig, context, info }), + delegateToSchema({ + schema: innerSubschemaConfig, + context, + info, + }), field2: (_parent, _args, context, info) => - delegateToSchema({ schema: innerSubschemaConfig, context, info }), + delegateToSchema({ + schema: innerSubschemaConfig, + context, + info, + }), }, }, }); diff --git a/packages/delegate/tests/createRequest.test.ts b/packages/delegate/tests/createRequest.test.ts index 22398f0a1..63447e780 100644 --- a/packages/delegate/tests/createRequest.test.ts +++ b/packages/delegate/tests/createRequest.test.ts @@ -39,6 +39,7 @@ describe('bare requests', () => { Query: { delegate: (_root, args, _context, info) => { const request = createRequest({ + subgraphName: 'inner', fieldNodes: [ { kind: Kind.FIELD, @@ -139,6 +140,7 @@ describe('bare requests', () => { Query: { delegate: (_root, args, _context, info) => { const request = createRequest({ + subgraphName: 'inner', fieldNodes: [ { kind: Kind.FIELD, @@ -220,6 +222,7 @@ describe('bare requests', () => { Query: { delegate: (_source, _args, _context, info) => { const request = createRequest({ + subgraphName: 'inner', fieldNodes: [ { kind: Kind.FIELD, diff --git a/packages/executors/common/package.json b/packages/executors/common/package.json index 09bb1478d..a6800264c 100644 --- a/packages/executors/common/package.json +++ b/packages/executors/common/package.json @@ -11,7 +11,7 @@ "author": "Arda TANRIKULU ", "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/executors/graphql-ws/package.json b/packages/executors/graphql-ws/package.json index 989cfa165..c02df912a 100644 --- a/packages/executors/graphql-ws/package.json +++ b/packages/executors/graphql-ws/package.json @@ -11,7 +11,7 @@ "author": "Arda TANRIKULU ", "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/executors/http/package.json b/packages/executors/http/package.json index a0c7ac2b7..8b23e82a5 100644 --- a/packages/executors/http/package.json +++ b/packages/executors/http/package.json @@ -11,7 +11,7 @@ "author": "Arda TANRIKULU ", "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/federation/package.json b/packages/federation/package.json index e1dadd29d..ddd6f7b4b 100644 --- a/packages/federation/package.json +++ b/packages/federation/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/fusion-runtime/package.json b/packages/fusion-runtime/package.json index d5bbc2488..a2519d9e1 100644 --- a/packages/fusion-runtime/package.json +++ b/packages/fusion-runtime/package.json @@ -15,7 +15,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,6 +45,7 @@ "dependencies": { "@envelop/core": "^5.3.0", "@envelop/instrumentation": "^1.0.0", + "@graphql-hive/logger": "workspace:^", "@graphql-mesh/cross-helpers": "^0.4.10", "@graphql-mesh/transport-common": "workspace:^", "@graphql-mesh/types": "^0.104.7", diff --git a/packages/fusion-runtime/src/executor.ts b/packages/fusion-runtime/src/executor.ts index 2746160de..094aca8a4 100644 --- a/packages/fusion-runtime/src/executor.ts +++ b/packages/fusion-runtime/src/executor.ts @@ -34,7 +34,7 @@ export function getExecutorForUnifiedGraph( () => unifiedGraphManager.getContext(execReq.context), (context) => { function handleExecutor(executor: Executor) { - opts?.transportContext?.logger?.debug( + opts?.transportContext?.log.debug( 'Executing request on unified graph', () => print(execReq.document), ); @@ -50,7 +50,7 @@ export function getExecutorForUnifiedGraph( return handleMaybePromise( () => unifiedGraphManager.getUnifiedGraph(), (unifiedGraph) => { - opts?.transportContext?.logger?.debug( + opts?.transportContext?.log.debug( 'Executing request on unified graph', () => print(execReq.document), ); @@ -70,9 +70,7 @@ export function getExecutorForUnifiedGraph( enumerable: true, get() { return function unifiedGraphExecutorDispose() { - opts?.transportContext?.logger?.debug( - 'Disposing unified graph executor', - ); + opts?.transportContext?.log.debug('Disposing unified graph executor'); return unifiedGraphManager[DisposableSymbols.asyncDispose](); }; }, diff --git a/packages/fusion-runtime/src/federation/supergraph.ts b/packages/fusion-runtime/src/federation/supergraph.ts index 7589136dd..ec33091e9 100644 --- a/packages/fusion-runtime/src/federation/supergraph.ts +++ b/packages/fusion-runtime/src/federation/supergraph.ts @@ -1,7 +1,7 @@ +import { LegacyLogger, Logger } from '@graphql-hive/logger'; import type { YamlConfig } from '@graphql-mesh/types'; import { getInContextSDK, - requestIdByRequest, resolveAdditionalResolversWithoutImport, } from '@graphql-mesh/utils'; import type { @@ -158,7 +158,8 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ onDelegateHooks, additionalTypeDefs: additionalTypeDefsFromConfig = [], additionalResolvers: additionalResolversFromConfig = [], - logger, + // no logger was provided, use a muted logger for consistency across plugin hooks + log: rootLog = new Logger({ level: false }), }: UnifiedGraphHandlerOpts): UnifiedGraphHandlerResult { const additionalTypeDefs = [...asArray(additionalTypeDefsFromConfig)]; const additionalResolvers = [...asArray(additionalResolversFromConfig)]; @@ -230,7 +231,7 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ originalResolver, typeName, onDelegationStageExecuteHooks, - logger, + rootLog, ); } } @@ -278,7 +279,7 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ executableUnifiedGraph, // @ts-expect-error Legacy Mesh RawSource is not compatible with new Mesh subschemas, - logger, + LegacyLogger.from(rootLog), onDelegateHooks || [], ); const stitchingInfo = executableUnifiedGraph.extensions?.[ @@ -306,16 +307,9 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ delegationPlanBuilder = newDelegationPlanBuilder; } const onDelegationPlanDoneHooks: OnDelegationPlanDoneHook[] = []; - let currentLogger = logger; - let requestId: string | undefined; - if (context?.request) { - requestId = requestIdByRequest.get(context.request); - if (requestId) { - currentLogger = currentLogger?.child({ requestId }); - } - } + let log = context.log as Logger; if (sourceSubschema.name) { - currentLogger = currentLogger?.child({ + log = log.child({ subgraph: sourceSubschema.name, }); } @@ -328,7 +322,7 @@ export const handleFederationSupergraph: UnifiedGraphHandler = function ({ variables, fragments, fieldNodes, - logger: currentLogger, + log, context, info, delegationPlanBuilder, diff --git a/packages/fusion-runtime/src/unifiedGraphManager.ts b/packages/fusion-runtime/src/unifiedGraphManager.ts index c8f5b3379..7330a18ef 100644 --- a/packages/fusion-runtime/src/unifiedGraphManager.ts +++ b/packages/fusion-runtime/src/unifiedGraphManager.ts @@ -1,8 +1,9 @@ +import type { Logger } from '@graphql-hive/logger'; import type { TransportContext, TransportEntry, } from '@graphql-mesh/transport-common'; -import type { Logger, OnDelegateHook } from '@graphql-mesh/types'; +import type { OnDelegateHook } from '@graphql-mesh/types'; import { dispose, isDisposable } from '@graphql-mesh/utils'; import { CRITICAL_ERROR } from '@graphql-tools/executor'; import type { @@ -68,8 +69,7 @@ export interface UnifiedGraphHandlerOpts { onDelegationPlanHooks?: OnDelegationPlanHook[]; onDelegationStageExecuteHooks?: OnDelegationStageExecuteHook[]; onDelegateHooks?: OnDelegateHook[]; - - logger?: Logger; + log?: Logger; } export interface UnifiedGraphHandlerResult { @@ -81,7 +81,7 @@ export interface UnifiedGraphHandlerResult { export interface UnifiedGraphManagerOptions { getUnifiedGraph( - ctx: TransportContext, + ctx: TransportContext | undefined, ): MaybePromise; // Handle the unified graph by any specification handleUnifiedGraph?: UnifiedGraphHandler; @@ -105,7 +105,6 @@ export interface UnifiedGraphManagerOptions { */ batch?: boolean; instrumentation?: () => Instrumentation | undefined; - onUnifiedGraphChange?(newUnifiedGraph: GraphQLSchema): void; } @@ -114,7 +113,16 @@ export type Instrumentation = { * Wrap each subgraph execution request. This can happen multiple time for the same graphql operation. */ subgraphExecute?: ( - payload: { executionRequest: ExecutionRequest }, + payload: { executionRequest: ExecutionRequest; subgraphName: string }, + wrapped: () => MaybePromise, + ) => MaybePromise; + /** + * Wrap each supergraph schema loading. + * + * Note: this span is only available when an Async compatible context manager is available + */ + schema?: ( + payload: null, wrapped: () => MaybePromise, ) => MaybePromise; }; @@ -148,7 +156,7 @@ export class UnifiedGraphManager implements AsyncDisposable { this.onDelegationStageExecuteHooks = opts?.onDelegationStageExecuteHooks || []; if (opts.pollingInterval != null) { - opts.transportContext?.logger?.debug( + opts.transportContext?.log.debug( `Starting polling to Supergraph with interval ${millisecondsToStr(opts.pollingInterval)}`, ); } @@ -161,16 +169,16 @@ export class UnifiedGraphManager implements AsyncDisposable { this.lastLoadTime != null && Date.now() - this.lastLoadTime >= this.opts.pollingInterval ) { - this.opts?.transportContext?.logger?.debug(`Polling Supergraph`); + this.opts?.transportContext?.log.debug(`Polling Supergraph`); this.polling$ = handleMaybePromise( () => this.getAndSetUnifiedGraph(), () => { this.polling$ = undefined; }, (err) => { - this.opts.transportContext?.logger?.error( - 'Failed to poll Supergraph', + this.opts.transportContext?.log.error( err, + 'Failed to poll Supergraph', ); this.polling$ = undefined; }, @@ -178,19 +186,21 @@ export class UnifiedGraphManager implements AsyncDisposable { } if (!this.unifiedGraph) { if (!this.initialUnifiedGraph$) { - this.opts?.transportContext?.logger?.debug( + this.opts?.transportContext?.log.debug( 'Fetching the initial Supergraph', ); if (this.opts.transportContext?.cache) { - this.opts.transportContext?.logger?.debug( - `Searching for Supergraph in cache under key "${UNIFIEDGRAPH_CACHE_KEY}"...`, + this.opts.transportContext?.log.debug( + { key: UNIFIEDGRAPH_CACHE_KEY }, + 'Searching for Supergraph in cache...', ); this.initialUnifiedGraph$ = handleMaybePromise( () => this.opts.transportContext?.cache?.get(UNIFIEDGRAPH_CACHE_KEY), (cachedUnifiedGraph) => { if (cachedUnifiedGraph) { - this.opts.transportContext?.logger?.debug( + this.opts.transportContext?.log.debug( + { key: UNIFIEDGRAPH_CACHE_KEY }, 'Found Supergraph in cache', ); return this.handleLoadedUnifiedGraph(cachedUnifiedGraph, true); @@ -208,7 +218,8 @@ export class UnifiedGraphManager implements AsyncDisposable { () => this.initialUnifiedGraph$!, (v) => { this.initialUnifiedGraph$ = undefined; - this.opts.transportContext?.logger?.debug( + this.opts.transportContext?.log.debug( + { key: UNIFIEDGRAPH_CACHE_KEY }, 'Initial Supergraph fetched', ); return v; @@ -231,7 +242,7 @@ export class UnifiedGraphManager implements AsyncDisposable { this.lastLoadedUnifiedGraph != null && compareSchemas(loadedUnifiedGraph, this.lastLoadedUnifiedGraph) ) { - this.opts.transportContext?.logger?.debug( + this.opts.transportContext?.log.debug( 'Supergraph has not been changed, skipping...', ); this.lastLoadTime = Date.now(); @@ -258,17 +269,18 @@ export class UnifiedGraphManager implements AsyncDisposable { // 60 seconds making sure the unifiedgraph is not kept forever // NOTE: we default to 60s because Cloudflare KV TTL does not accept anything less 60; - this.opts.transportContext.logger?.debug( - `Caching Supergraph with TTL ${ttl}s`, + this.opts.transportContext?.log.debug( + { ttl, key: UNIFIEDGRAPH_CACHE_KEY }, + 'Caching Supergraph', ); - const logCacheSetError = (e: unknown) => { - this.opts.transportContext?.logger?.debug( - `Unable to store Supergraph in cache under key "${UNIFIEDGRAPH_CACHE_KEY}" with TTL ${ttl}s`, - e, + const logCacheSetError = (err: unknown) => { + this.opts.transportContext?.log.debug( + { err, ttl, key: UNIFIEDGRAPH_CACHE_KEY }, + 'Unable to cache Supergraph', ); }; try { - const cacheSet$ = this.opts.transportContext.cache.set( + const cacheSet$ = this.opts.transportContext?.cache.set( UNIFIEDGRAPH_CACHE_KEY, serializedUnifiedGraph, { ttl }, @@ -280,10 +292,10 @@ export class UnifiedGraphManager implements AsyncDisposable { } catch (e) { logCacheSetError(e); } - } catch (e) { - this.opts.transportContext.logger?.error( + } catch (err: any) { + this.opts.transportContext?.log.error( + err, 'Failed to initiate caching of Supergraph', - e, ); } } @@ -309,7 +321,7 @@ export class UnifiedGraphManager implements AsyncDisposable { onDelegationPlanHooks: this.onDelegationPlanHooks, onDelegationStageExecuteHooks: this.onDelegationStageExecuteHooks, onDelegateHooks: this.opts.onDelegateHooks, - logger: this.opts.transportContext?.logger, + log: this.opts.transportContext?.log, }); const transportExecutorStack = new AsyncDisposableStack(); const onSubgraphExecute = getOnSubgraphExecute({ @@ -351,7 +363,7 @@ export class UnifiedGraphManager implements AsyncDisposable { }, }, ); - this.opts.transportContext?.logger?.debug( + this.opts.transportContext?.log.debug( 'Supergraph has been changed, updating...', ); } @@ -363,9 +375,9 @@ export class UnifiedGraphManager implements AsyncDisposable { }, (err) => { this.disposeReason = undefined; - this.opts.transportContext?.logger?.error( - 'Failed to dispose the existing transports and executors', + this.opts.transportContext?.log.error( err, + 'Failed to dispose the existing transports and executors', ); return this.unifiedGraph!; }, @@ -383,14 +395,11 @@ export class UnifiedGraphManager implements AsyncDisposable { private getAndSetUnifiedGraph(): MaybePromise { return handleMaybePromise( - () => this.opts.getUnifiedGraph(this.opts.transportContext || {}), + () => this.opts.getUnifiedGraph(this.opts.transportContext), (loadedUnifiedGraph: string | GraphQLSchema | DocumentNode) => this.handleLoadedUnifiedGraph(loadedUnifiedGraph), (err) => { - this.opts.transportContext?.logger?.error( - 'Failed to load Supergraph', - err, - ); + this.opts.transportContext?.log.error(err, 'Failed to load Supergraph'); this.lastLoadTime = Date.now(); this.disposeReason = undefined; this.polling$ = undefined; diff --git a/packages/fusion-runtime/src/utils.ts b/packages/fusion-runtime/src/utils.ts index c580c60e5..6943f1236 100644 --- a/packages/fusion-runtime/src/utils.ts +++ b/packages/fusion-runtime/src/utils.ts @@ -1,4 +1,6 @@ import { getInstrumented } from '@envelop/instrumentation'; +import { LegacyLogger, Logger } from '@graphql-hive/logger'; +import { loggerForRequest } from '@graphql-hive/logger/request'; import { defaultPrintFn, type Transport, @@ -7,13 +9,7 @@ import { type TransportGetSubgraphExecutor, type TransportGetSubgraphExecutorOptions, } from '@graphql-mesh/transport-common'; -import type { Logger } from '@graphql-mesh/types'; -import { - isDisposable, - iterateAsync, - loggerForExecutionRequest, - requestIdByRequest, -} from '@graphql-mesh/utils'; +import { isDisposable, iterateAsync } from '@graphql-mesh/utils'; import { getBatchingExecutor } from '@graphql-tools/batch-execute'; import { DelegationPlanBuilder, @@ -112,22 +108,15 @@ function getTransportExecutor({ transports = defaultTransportsGetter, getDisposeReason, }: { - transportContext: TransportContext; + transportContext: TransportContext | undefined; transportEntry: TransportEntry; subgraphName?: string; subgraph: GraphQLSchema; transports?: Transports; getDisposeReason?: () => GraphQLError | undefined; }): MaybePromise { - // TODO const kind = transportEntry?.kind || ''; - let logger = transportContext?.logger; - if (logger) { - if (subgraphName) { - logger = logger.child({ subgraph: subgraphName }); - } - logger?.debug(`Loading transport "${kind}"`); - } + transportContext?.log.debug(`Loading transport "${kind}"`); return handleMaybePromise( () => typeof transports === 'function' ? transports(kind) : transports[kind], @@ -154,6 +143,11 @@ function getTransportExecutor({ `Transport "${kind}" "getSubgraphExecutor" is not a function`, ); } + const log = + transportContext?.log || + // if the logger is not provided by the context, create a new silent one just for consistency in the hooks + new Logger({ level: false }); + const logger = transportContext?.logger || LegacyLogger.from(log); return getSubgraphExecutor({ subgraphName, subgraph, @@ -170,23 +164,20 @@ function getTransportExecutor({ }, getDisposeReason, ...transportContext, + log, + logger, }); }, ); } -export const subgraphNameByExecutionRequest = new WeakMap< - ExecutionRequest, - string ->(); - /** * This function creates a executor factory that uses the transport packages, * and wraps them with the hooks */ export function getOnSubgraphExecute({ onSubgraphExecuteHooks, - transportContext = {}, + transportContext, transportEntryMap, getSubgraphSchema, transportExecutorStack, @@ -210,22 +201,22 @@ export function getOnSubgraphExecute({ subgraphName: string, executionRequest: ExecutionRequest, ) { - subgraphNameByExecutionRequest.set(executionRequest, subgraphName); let executor: Executor | undefined = subgraphExecutorMap.get(subgraphName); // If the executor is not initialized yet, initialize it if (executor == null) { - let logger = transportContext?.logger; - if (logger) { - const requestId = requestIdByRequest.get( - executionRequest.context?.request, - ); - if (requestId) { - logger = logger.child({ requestId }); - } + if (transportContext) { + let log = executionRequest.context?.request + ? loggerForRequest( + transportContext.log, + executionRequest.context.request, + ) + : transportContext.log; if (subgraphName) { - logger = logger.child({ subgraph: subgraphName }); + log = log.child({ subgraph: subgraphName }); } - logger.debug(`Initializing executor`); + // overwrite the log in the transport context because now it contains more details + transportContext.log = log; + log.debug('Initializing executor'); } // Lazy executor that loads transport executor on demand executor = function lazyExecutor(subgraphExecReq: ExecutionRequest) { @@ -256,6 +247,7 @@ export function getOnSubgraphExecute({ transportEntryMap, transportContext, getSubgraphSchema, + instrumentation, }); // Caches the executor for future use subgraphExecutorMap.set(subgraphName, executor); @@ -266,20 +258,14 @@ export function getOnSubgraphExecute({ // Caches the lazy executor to prevent race conditions subgraphExecutorMap.set(subgraphName, executor); } + if (batch) { executor = getBatchingExecutor( executionRequest.context || subgraphExecutorMap, executor, ); } - const originalExecutor = executor; - executor = (executionRequest) => { - const subgraphInstrumentation = instrumentation()?.subgraphExecute; - return getInstrumented({ executionRequest }).asyncFn( - subgraphInstrumentation, - originalExecutor, - )(executionRequest); - }; + return executor(executionRequest); }; } @@ -291,6 +277,7 @@ export interface WrapExecuteWithHooksOptions { transportEntryMap?: Record; getSubgraphSchema: (subgraphName: string) => GraphQLSchema; transportContext?: TransportContext; + instrumentation: () => Instrumentation | undefined; } declare module 'graphql' { @@ -310,28 +297,20 @@ export function wrapExecutorWithHooks({ transportEntryMap, getSubgraphSchema, transportContext, + instrumentation, }: WrapExecuteWithHooksOptions): Executor { - return function executorWithHooks(baseExecutionRequest: ExecutionRequest) { + function executorWithHooks(baseExecutionRequest: ExecutionRequest) { baseExecutionRequest.info = baseExecutionRequest.info || ({} as GraphQLResolveInfo); baseExecutionRequest.info.executionRequest = baseExecutionRequest; - // TODO: Fix this in onFetch hook handler of @graphql-mesh/utils + // this rootValue will be set in the info value for field.resolvers in non-graphql requests // TODO: Also consider if a subgraph can ever rely on the gateway's rootValue? baseExecutionRequest.rootValue = { executionRequest: baseExecutionRequest, }; - - const requestId = - baseExecutionRequest.context?.request && - requestIdByRequest.get(baseExecutionRequest.context.request); - let execReqLogger = transportContext?.logger; - if (execReqLogger) { - if (requestId) { - execReqLogger = execReqLogger.child({ requestId }); - } - loggerForExecutionRequest.set(baseExecutionRequest, execReqLogger); - } - execReqLogger = execReqLogger?.child?.({ subgraph: subgraphName }); + const log = + transportContext?.log.child({ subgraph: subgraphName }) || + new Logger({ attrs: { subgraph: subgraphName } }); if (onSubgraphExecuteHooks.length === 0) { return baseExecutor(baseExecutionRequest); } @@ -357,11 +336,10 @@ export function wrapExecutorWithHooks({ }, executor, setExecutor(newExecutor) { - execReqLogger?.debug('executor has been updated'); + log.debug('executor has been updated'); executor = newExecutor; }, - requestId, - logger: execReqLogger, + log: log, }), onSubgraphExecuteDoneHooks, ), @@ -381,10 +359,7 @@ export function wrapExecutorWithHooks({ onSubgraphExecuteDoneHook({ result: currentResult, setResult(newResult: ExecutionResult) { - execReqLogger?.debug( - 'overriding result with: ', - newResult, - ); + log.debug('overriding result with: ', newResult); currentResult = newResult; }, }), @@ -424,10 +399,7 @@ export function wrapExecutorWithHooks({ onNext({ result: currentResult, setResult: (res) => { - execReqLogger?.debug( - 'overriding result with: ', - res, - ); + log.debug('overriding result with: ', res); currentResult = res; }, @@ -447,6 +419,14 @@ export function wrapExecutorWithHooks({ ); }, ); + } + + return function instrumentedExecutor(executionRequest: ExecutionRequest) { + const subgraphInstrument = instrumentation()?.subgraphExecute; + return getInstrumented({ executionRequest, subgraphName }).asyncFn( + subgraphInstrument, + executorWithHooks, + )(executionRequest); }; } @@ -468,8 +448,7 @@ export interface OnSubgraphExecutePayload { setExecutionRequest(executionRequest: ExecutionRequest): void; executor: Executor; setExecutor(executor: Executor): void; - requestId?: string; - logger?: Logger; + log: Logger; } export interface OnSubgraphExecuteDonePayload { @@ -510,8 +489,7 @@ export interface OnDelegationPlanHookPayload { fragments: Record; fieldNodes: SelectionNode[]; context: TContext; - requestId?: string; - logger?: Logger; + log: Logger; info?: GraphQLResolveInfo; delegationPlanBuilder: DelegationPlanBuilder; setDelegationPlanBuilder(delegationPlanBuilder: DelegationPlanBuilder): void; @@ -547,8 +525,7 @@ export interface OnDelegationStageExecutePayload { typeName: string; - requestId?: string; - logger?: Logger; + log: Logger; } export type OnDelegationStageExecuteDoneHook = ( @@ -595,19 +572,11 @@ export function wrapMergedTypeResolver>( originalResolver: MergedTypeResolver, typeName: string, onDelegationStageExecuteHooks: OnDelegationStageExecuteHook[], - baseLogger?: Logger, + log: Logger, ): MergedTypeResolver { return (object, context, info, subschema, selectionSet, key, type) => { - let logger = baseLogger; - let requestId: string | undefined; - if (logger && context['request']) { - requestId = requestIdByRequest.get(context['request']); - if (requestId) { - logger = logger.child({ requestId }); - } - } if (subschema.name) { - logger = logger?.child({ subgraph: subschema.name }); + log = log.child({ subgraph: subschema.name }); } let resolver = originalResolver as MergedTypeResolver; function setResolver(newResolver: MergedTypeResolver) { @@ -626,8 +595,7 @@ export function wrapMergedTypeResolver>( key, typeName, type, - requestId, - logger, + log, resolver, setResolver, }); diff --git a/packages/fusion-runtime/tests/polling.test.ts b/packages/fusion-runtime/tests/polling.test.ts index b8e9829ea..cb7f7ab43 100644 --- a/packages/fusion-runtime/tests/polling.test.ts +++ b/packages/fusion-runtime/tests/polling.test.ts @@ -1,4 +1,5 @@ import { setTimeout } from 'timers/promises'; +import { LegacyLogger, Logger } from '@graphql-hive/logger'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { getExecutorForUnifiedGraph } from '@graphql-mesh/fusion-runtime'; import { @@ -21,7 +22,6 @@ import { import { ExecutionResult, GraphQLSchema, parse } from 'graphql'; import { createSchema } from 'graphql-yoga'; import { describe, expect, it, vi } from 'vitest'; -import { getDefaultLogger } from '../../runtime/src/getDefaultLogger'; import { UnifiedGraphManager } from '../src/unifiedGraphManager'; describe('Polling', () => { @@ -374,20 +374,18 @@ describe('Polling', () => { const unifiedGraphFetcher = vi.fn(() => { return graphDeferred ? graphDeferred.promise : unifiedGraph; }); - const logger = getDefaultLogger(); + const log = new Logger(); await using executor = getExecutorForUnifiedGraph({ getUnifiedGraph: unifiedGraphFetcher, pollingInterval: 10_000, - transportContext: { - logger, - }, + transportContext: { log, logger: LegacyLogger.from(log) }, transports() { - logger.debug('transports'); + log.debug('transports'); return { getSubgraphExecutor() { - logger.debug('getSubgraphExecutor'); + log.debug('getSubgraphExecutor'); return function dynamicExecutor(...args) { - logger.debug('dynamicExecutor'); + log.debug('dynamicExecutor'); return createDefaultExecutor(schema)(...args); }; }, diff --git a/packages/fusion-runtime/tests/runtime.test.ts b/packages/fusion-runtime/tests/runtime.test.ts index ab2171a66..0224582b2 100644 --- a/packages/fusion-runtime/tests/runtime.test.ts +++ b/packages/fusion-runtime/tests/runtime.test.ts @@ -1,4 +1,5 @@ import { buildSubgraphSchema } from '@apollo/subgraph'; +import { Logger } from '@graphql-hive/logger'; import { OnDelegationPlanDoneHook, OnDelegationPlanHook, @@ -264,7 +265,7 @@ describe('onDelegationPlanHook', () => { context, delegationPlanBuilder: expect.any(Function), setDelegationPlanBuilder: expect.any(Function), - logger: undefined, + log: expect.any(Logger), info: expect.any(Object), }); expect( diff --git a/packages/fusion-runtime/tests/utils.ts b/packages/fusion-runtime/tests/utils.ts index c723bf360..36eed7d76 100644 --- a/packages/fusion-runtime/tests/utils.ts +++ b/packages/fusion-runtime/tests/utils.ts @@ -1,3 +1,4 @@ +import { LegacyLogger, Logger } from '@graphql-hive/logger'; import { getUnifiedGraphGracefully, type SubgraphConfig, @@ -44,8 +45,13 @@ export function composeAndGetExecutor( subgraphs: SubgraphConfig[], opts?: Partial>, ) { + const log = new Logger({ level: false }); const manager = new UnifiedGraphManager({ getUnifiedGraph: () => getUnifiedGraphGracefully(subgraphs), + transportContext: { + log, + logger: LegacyLogger.from(log), + }, transports() { return { getSubgraphExecutor({ subgraphName }) { diff --git a/packages/gateway/package.json b/packages/gateway/package.json index 03cefeb1a..24513c918 100644 --- a/packages/gateway/package.json +++ b/packages/gateway/package.json @@ -15,7 +15,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "bin": { "hive-gateway": "./dist/bin.js" @@ -55,6 +55,7 @@ "@escape.tech/graphql-armor-max-tokens": "^2.5.0", "@graphql-hive/gateway-runtime": "workspace:^", "@graphql-hive/importer": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-hive/plugin-aws-sigv4": "workspace:^", "@graphql-hive/plugin-deduplicate-request": "workspace:^", "@graphql-hive/pubsub": "workspace:^", @@ -67,7 +68,6 @@ "@graphql-mesh/plugin-http-cache": "^0.105.8", "@graphql-mesh/plugin-jit": "^0.2.7", "@graphql-mesh/plugin-jwt-auth": "workspace:^", - "@graphql-mesh/plugin-mock": "^0.105.8", "@graphql-mesh/plugin-opentelemetry": "workspace:^", "@graphql-mesh/plugin-prometheus": "workspace:^", "@graphql-mesh/plugin-rate-limit": "^0.104.7", @@ -82,6 +82,19 @@ "@graphql-tools/load": "^8.1.2", "@graphql-tools/utils": "^10.9.1", "@graphql-yoga/render-graphiql": "^5.15.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/context-async-hooks": "^2.0.1", + "@opentelemetry/context-zone": "^2.0.1", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-jaeger": "^2.0.1", + "@opentelemetry/exporter-zipkin": "^2.0.1", + "@opentelemetry/propagator-b3": "^2.0.1", + "@opentelemetry/propagator-jaeger": "^2.0.1", + "@opentelemetry/sampler-jaeger-remote": "^0.203.0", + "@opentelemetry/sdk-logs": "^0.203.0", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-trace-base": "patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch", "commander": "^13.1.0", "dotenv": "^17.2.1", "graphql-ws": "^6.0.6", @@ -95,7 +108,7 @@ "@graphql-tools/executor": "^1.4.9", "@rollup/plugin-commonjs": "^28.0.0", "@rollup/plugin-json": "^6.1.0", - "@rollup/plugin-node-resolve": "^16.0.0", + "@rollup/plugin-node-resolve": "patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch", "@rollup/plugin-sucrase": "^5.0.2", "@tsconfig/node18": "^18.2.4", "@types/adm-zip": "^0.5.5", diff --git a/packages/gateway/rollup.config.js b/packages/gateway/rollup.config.js index d0b627972..915b62da8 100644 --- a/packages/gateway/rollup.config.js +++ b/packages/gateway/rollup.config.js @@ -62,9 +62,52 @@ const deps = { '../../node_modules/@escape.tech/graphql-armor-max-depth/dist/graphql-armor-max-depth.esm.js', 'node_modules/@escape.tech/graphql-armor-block-field-suggestions/index': '../../node_modules/@escape.tech/graphql-armor-block-field-suggestions/dist/graphql-armor-block-field-suggestions.esm.js', - // OpenTelemetry plugin is built-in but it dynamically imports the gRPC exporter, we therefore need to bundle it + // OpenTelemetry plugin is sometimes imported, and not re-used from the gateway itself. we therefore need to bundle it into node_modules + 'node_modules/@graphql-mesh/plugin-opentelemetry/index': + '../plugins/opentelemetry/src/index.ts', + // Since `async_hooks` is not available in all runtime, it have to be bundle separately + // The Async Local context manager of Opentelemetry can't be bundled correctly, so we use our own + // proxy export file. It just re-export otel's package, which makes rollup happy + 'node_modules/@opentelemetry/context-async-hooks/index': + '../plugins/opentelemetry/src/async-context-manager.ts', 'node_modules/@opentelemetry/exporter-trace-otlp-grpc/index': - '../../node_modules/@opentelemetry/exporter-trace-otlp-grpc/build/src/index.js', + '../plugins/opentelemetry/src/exporter-trace-otlp-grpc.ts', + 'node_modules/@opentelemetry/sdk-node/index': + '../plugins/opentelemetry/src/sdk-node.ts', + 'node_modules/@opentelemetry/auto-instrumentations-node/index': + '../plugins/opentelemetry/src/auto-instrumentations.ts', + 'node_modules/@graphql-mesh/plugin-opentelemetry/setup': + '../plugins/opentelemetry/src/setup.ts', + ...Object.fromEntries( + // To ease the OTEL setup, we need to bundle some important OTEL packages. + // Those are most used features. + [ + // Common API base + ['api'], + ['api-logs'], + ['core'], + ['resources', 'esm/'], + ['sdk-trace-base'], + ['sdk-metrics'], + ['sdk-logs'], + ['semantic-conventions'], + ['instrumentation'], + + // Exporters + ['exporter-trace-otlp-http'], + ['exporter-zipkin'], + + // Propagators + ['propagator-b3'], + ['propagator-jaeger'], + + // Context Managers + ['context-zone'], // An incomplete but Web compatible async context manager based on zone.js + ].map(([otelPackage, buildDir = 'esm']) => [ + `node_modules/@opentelemetry/${otelPackage}/index`, + `../../node_modules/@opentelemetry/${otelPackage}/build/${buildDir}/index.js`, + ]), + ), }; if ( @@ -99,7 +142,11 @@ export default defineConfig({ external: ['tuql'], plugins: [ tsConfigPaths(), // use tsconfig paths to resolve modules - nodeResolve({ preferBuiltins: true }), // resolve node_modules and bundle them too + nodeResolve({ + preferBuiltins: true, + mainFields: ['esnext', 'module', 'main'], + exportConditions: ['esnext'], + }), // resolve node_modules and bundle them too graphql(), // handle graphql imports commonjs({ strictRequires: true }), // convert commonjs to esm json(), // support importing json files to esm (needed for commonjs() plugin) @@ -119,6 +166,9 @@ function packagejson() { generateBundle(_outputs, bundles) { /** @type {string[]} */ const e2eModules = []; + /** @type Record, main?: string}> */ + const packages = {}; + for (const bundle of Object.values(bundles).filter((bundle) => { const bundleName = String(bundle.name); return ( @@ -138,23 +188,25 @@ function packagejson() { } const dir = path.dirname(bundle.fileName); const bundledFile = path.basename(bundle.fileName).replace(/\\/g, '/'); - /** @type {Record} */ - const pkg = { type: 'module' }; - if (bundledFile === 'index.mjs') { - pkg['main'] = bundledFile; - } else { - const mjsFile = path - .basename(bundle.fileName, '.mjs') - .replace(/\\/g, '/'); - // if the bundled file is not "index", then it's an package.json exports path - pkg['exports'] = { [`./${mjsFile}`]: `./${bundledFile}` }; - } + const pkgFileName = path.join(dir, 'package.json'); + const pkg = packages[pkgFileName] ?? { type: 'module' }; + const mjsFile = + bundledFile === 'index.mjs' + ? '.' + : './' + path.basename(bundle.fileName, '.mjs').replace(/\\/g, '/'); + // if the bundled file is not "index", then it's an package.json exports path + pkg.exports = { ...pkg.exports, [mjsFile]: `./${bundledFile}` }; + packages[pkgFileName] = pkg; + } + + for (const [fileName, pkg] of Object.entries(packages)) { this.emitFile({ type: 'asset', - fileName: path.join(dir, 'package.json'), + fileName, source: JSON.stringify(pkg), }); } + this.emitFile({ type: 'asset', fileName: path.join('e2e', 'package.json'), diff --git a/packages/gateway/src/bin.ts b/packages/gateway/src/bin.ts index 5fce1be5e..42edd5dc7 100644 --- a/packages/gateway/src/bin.ts +++ b/packages/gateway/src/bin.ts @@ -3,7 +3,7 @@ import 'dotenv/config'; // inject dotenv options to process.env import module from 'node:module'; import type { InitializeData } from '@graphql-hive/importer/hooks'; -import { getDefaultLogger } from '../../runtime/src/getDefaultLogger'; +import { Logger } from '@graphql-hive/logger'; import { enableModuleCachingIfPossible, handleNodeWarnings, run } from './cli'; // @inject-version globalThis.__VERSION__ here @@ -20,7 +20,7 @@ module.register('@graphql-hive/importer/hooks', { enableModuleCachingIfPossible(); handleNodeWarnings(); -const log = getDefaultLogger(); +const log = new Logger(); run({ log }).catch((err) => { log.error(err); diff --git a/packages/gateway/src/cli.ts b/packages/gateway/src/cli.ts index ed5075a42..ce830aad2 100644 --- a/packages/gateway/src/cli.ts +++ b/packages/gateway/src/cli.ts @@ -15,17 +15,17 @@ import { type GatewayGraphOSReportingOptions, type GatewayHiveReportingOptions, } from '@graphql-hive/gateway-runtime'; +import { Logger } from '@graphql-hive/logger'; import type { AWSSignv4PluginOptions } from '@graphql-hive/plugin-aws-sigv4'; import { HivePubSub } from '@graphql-hive/pubsub'; import type UpstashRedisCache from '@graphql-mesh/cache-upstash-redis'; import type { JWTAuthPluginOptions } from '@graphql-mesh/plugin-jwt-auth'; -import type { OpenTelemetryMeshPluginOptions } from '@graphql-mesh/plugin-opentelemetry'; +import type { OpenTelemetryGatewayPluginOptions } from '@graphql-mesh/plugin-opentelemetry'; import type { PrometheusPluginOptions } from '@graphql-mesh/plugin-prometheus'; -import type { KeyValueCache, Logger, YamlConfig } from '@graphql-mesh/types'; +import type { KeyValueCache, YamlConfig } from '@graphql-mesh/types'; import { renderGraphiQL } from '@graphql-yoga/render-graphiql'; -import { getEnvBool, getNodeEnv, isDebug } from '~internal/env'; +import { getEnvBool, isDebug } from '~internal/env'; import parseDuration from 'parse-duration'; -import { getDefaultLogger } from '../../runtime/src/getDefaultLogger'; import { addCommands } from './commands/index'; import { createDefaultConfigPaths } from './config'; import { getMaxConcurrency } from './getMaxConcurrency'; @@ -106,7 +106,7 @@ export interface GatewayCLIProxyConfig } export type KeyValueCacheFactoryFn = (ctx: { - logger: Logger; + log: Logger; pubsub: HivePubSub; cwd: string; }) => KeyValueCache; @@ -129,7 +129,10 @@ export interface GatewayCLIBuiltinPluginConfig { * * @see https://graphql-hive.com/docs/gateway/monitoring-tracing */ - openTelemetry?: Exclude; + openTelemetry?: Exclude< + OpenTelemetryGatewayPluginOptions, + GatewayConfigContext + >; /** * Configure Rate Limiting * @@ -222,7 +225,7 @@ export interface CLIContext { log: Logger; /** @default 'Hive Gateway' */ productName: string; - /** @default 'Federated GraphQL Gateway' */ + /** @default 'Unify and accelerate your data graph across diverse services with Hive Gateway, which seamlessly integrates with Apollo Federation.' */ productDescription: string; /** @default '@graphql-hive/gateway' */ productPackageName: string; @@ -253,9 +256,8 @@ export type AddCommand = (ctx: CLIContext, cli: CLI) => void; // we dont use `Option.default()` in the command definitions because we want the CLI options to // override the config file (with option defaults, config file will always be overwritten) -const maxFork = getMaxConcurrency(); export const defaultOptions = { - fork: getNodeEnv() === 'production' ? maxFork : 1, + fork: 1, host: platform().toLowerCase() === 'win32' || // is WSL? @@ -275,21 +277,22 @@ let cli = new Command() }) .addOption( new Option( - '--fork ', - `count of workers to spawn. uses "${maxFork}" (available parallelism) workers when NODE_ENV is "production", otherwise "1" (the main) worker (default: ${defaultOptions.fork})`, + '--fork ', + `number of workers to spawn. (default: ${defaultOptions.fork})`, ) .env('FORK') .argParser((v) => { - const count = parseInt(v); - if (isNaN(count)) { + const number = parseInt(v); + if (isNaN(number)) { throw new InvalidArgumentError('not a number.'); } - if (count > maxFork) { + const maxConcurrency = getMaxConcurrency(); + if (number > maxConcurrency) { throw new InvalidArgumentError( - `exceedes number of available parallelism "${maxFork}".`, + `exceedes number of available concurrency "${maxConcurrency}".`, ); } - return count; + return number; }), ) .addOption( @@ -341,31 +344,72 @@ let cli = new Command() // @ts-expect-error null, ) + .addOption( + new Option( + '--opentelemetry [exporter-endpoint]', + `Enable OpenTelemetry integration with an exporter using this option's value as endpoint. By default, it uses OTLP HTTP, use "--opentelemetry-exporter-type" to change the default.`, + ).env('OPENTELEMETRY'), + ) + .addOption( + new Option( + '--opentelemetry-exporter-type ', + `OpenTelemetry exporter type to use when setting up OpenTelemetry integration. Requires "--opentelemetry" to set the endpoint.`, + ) + .choices(['otlp-http', 'otlp-grpc']) + .default('otlp-http') + .env('OPENTELEMETRY_EXPORTER_TYPE'), + ) .addOption( new Option( '--hive-registry-token ', - '[DEPRECATED: please use "--hive-usage-target" and "--hive-usage-access-token"] Hive registry token for usage metrics reporting', + '[DEPRECATED] please use "--hive-target" and "--hive-access-token"', ).env('HIVE_REGISTRY_TOKEN'), ) .addOption( new Option( '--hive-usage-target ', - 'Hive registry target to which the usage data should be reported to. requires the "--hive-usage-access-token " option', + '[DEPRECATED] please use --hive-target instead.', ).env('HIVE_USAGE_TARGET'), ) + .addOption( + new Option( + '--hive-target ', + 'Hive registry target to which the usage and tracing data should be reported to. Requires either "--hive-access-token ", "--hive-usage-access-token " or "--hive-trace-access-token" option', + ).env('HIVE_TARGET'), + ) + .addOption( + new Option( + '--hive-access-token ', + 'Hive registry access token for usage metrics reporting and tracing. Enables both usage reporting and tracing. Requires the "--hive-target " option', + ).env('HIVE_ACCESS_TOKEN'), + ) .addOption( new Option( '--hive-usage-access-token ', - 'Hive registry access token for usage metrics reporting. requires the "--hive-usage-target " option', + `Hive registry access token for usage reporting. Enables Hive usage report. Requires the "--hive-target " option. It can't be used together with "--hive-access-token"`, ).env('HIVE_USAGE_ACCESS_TOKEN'), ) + .addOption( + new Option( + '--hive-trace-access-token ', + `Hive registry access token for tracing. Enables Hive tracing. Requires the "--hive-target " option. It can't be used together with "--hive-access-token"`, + ).env('HIVE_TRACE_ACCESS_TOKEN'), + ) + .addOption( + new Option( + '--hive-trace-endpoint ', + `Hive registry tracing endpoint.`, + ) + .env('HIVE_TRACE_ENDPOINT') + .default(`https://api.graphql-hive.com/otel/v1/traces`), + ) .option( '--hive-persisted-documents-endpoint ', - '[EXPERIMENTAL] Hive CDN endpoint for fetching the persisted documents. requires the "--hive-persisted-documents-token " option', + '[EXPERIMENTAL] Hive CDN endpoint for fetching the persisted documents. Requires the "--hive-persisted-documents-token " option', ) .option( '--hive-persisted-documents-token ', - '[EXPERIMENTAL] Hive persisted documents CDN endpoint token. requires the "--hive-persisted-documents-endpoint " option', + '[EXPERIMENTAL] Hive persisted documents CDN endpoint token. Requires the "--hive-persisted-documents-endpoint " option', ) .addOption( new Option( @@ -407,9 +451,10 @@ let cli = new Command() export async function run(userCtx: Partial) { const ctx: CLIContext = { - log: userCtx.log || getDefaultLogger(), + log: userCtx.log || new Logger(), productName: 'Hive Gateway', - productDescription: 'Federated GraphQL Gateway', + productDescription: + 'Unify and accelerate your data graph across diverse services with Hive Gateway, which seamlessly integrates with Apollo Federation.', productPackageName: '@graphql-hive/gateway', productLink: 'https://the-guild.dev/graphql/hive/docs/gateway', binName: 'hive-gateway', diff --git a/packages/gateway/src/commands/handleFork.ts b/packages/gateway/src/commands/handleFork.ts index c093a90f8..a5c56091b 100644 --- a/packages/gateway/src/commands/handleFork.ts +++ b/packages/gateway/src/commands/handleFork.ts @@ -1,5 +1,5 @@ import cluster, { type Worker } from 'node:cluster'; -import type { Logger } from '@graphql-mesh/types'; +import type { Logger } from '@graphql-hive/logger'; import { registerTerminateHandler } from '@graphql-mesh/utils'; /** @@ -22,25 +22,23 @@ export function handleFork(log: Logger, config: { fork?: number }): boolean { logData['code'] = code; } if (expectedToExit) { - workerLogger.debug('exited', logData); + workerLogger.debug(logData, 'exited'); } else { workerLogger.error( - 'exited unexpectedly. A restart is recommended to ensure the stability of the service', logData, + 'Exited unexpectedly. A restart is recommended to ensure the stability of the service', ); } workers.delete(worker); if (!expectedToExit && workers.size === 0) { - log.error(`All workers exited unexpectedly. Exiting`, logData); + log.error(logData, 'All workers exited unexpectedly. Exiting...'); process.exit(1); } }); workers.add(worker); } registerTerminateHandler((signal) => { - log.info('Killing workers', { - signal, - }); + log.info(`Killing workers on ${signal}`); expectedToExit = true; workers.forEach((w) => { w.kill(signal); @@ -49,7 +47,11 @@ export function handleFork(log: Logger, config: { fork?: number }): boolean { return true; } } catch (e) { - log.error(`Error while forking workers: `, e); + log.error( + // @ts-expect-error very likely an instanceof error + e, + 'Error while forking workers', + ); } return false; } diff --git a/packages/gateway/src/commands/handleLoggingOption.ts b/packages/gateway/src/commands/handleLoggingOption.ts deleted file mode 100644 index 4b9c213b6..000000000 --- a/packages/gateway/src/commands/handleLoggingOption.ts +++ /dev/null @@ -1,18 +0,0 @@ -import { - handleLoggingConfig as handleLoggingConfigRuntime, - LogLevel, -} from '@graphql-hive/gateway-runtime'; -import { Logger } from '@graphql-mesh/types'; -import { CLIContext } from '..'; - -export function handleLoggingConfig( - loggingConfig: - | boolean - | Logger - | LogLevel - | keyof typeof LogLevel - | undefined, - ctx: CLIContext, -) { - ctx.log = handleLoggingConfigRuntime(loggingConfig, ctx.log); -} diff --git a/packages/gateway/src/commands/handleOpenTelemetryConfig.ts b/packages/gateway/src/commands/handleOpenTelemetryConfig.ts new file mode 100644 index 000000000..40a04d8ed --- /dev/null +++ b/packages/gateway/src/commands/handleOpenTelemetryConfig.ts @@ -0,0 +1,121 @@ +import { fakePromise } from '@graphql-tools/utils'; +import { + BatchSpanProcessor, + SpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import type { CLIContext } from '..'; + +export async function handleOpenTelemetryConfig( + ctx: CLIContext, + cliOpts: { + hiveAccessToken: string | undefined; // TODO: Use it to enable tracing by default once stable + hiveTarget: string | undefined; + hiveTraceAccessToken: string | undefined; + hiveTraceEndpoint: string; + openTelemetryExporterType: 'otlp-http' | 'otlp-grpc' | undefined; + openTelemetry: boolean | string | undefined; + }, +): Promise { + const accessToken = cliOpts.hiveTraceAccessToken; // TODO: also use value of hiveAccessToken + const traceEndpoint = cliOpts.hiveTraceEndpoint; + const target = cliOpts.hiveTarget; + const openTelemetry = cliOpts.openTelemetry; + const exporterType = cliOpts.openTelemetryExporterType ?? 'otlp-http'; + + const log = ctx.log.child('[OpenTelemetry] '); + + if (openTelemetry || accessToken) { + log.debug( + { openTelemetry, exporterType, target, traceEndpoint }, + 'Initializing OpenTelemetry SDK', + ); + + return fakePromise().then(async () => { + const { openTelemetrySetup, HiveTracingSpanProcessor, getEnvVar } = + await import('@graphql-mesh/plugin-opentelemetry/setup'); + const processors: SpanProcessor[] = []; + + const logAttributes = { + traceEndpoints: [] as { + url: string | null; + type?: string; + target?: string; + }[], + contextManager: false, + }; + + let integrationName: string; + + if (openTelemetry) { + const otelEndpoint = + typeof openTelemetry === 'string' + ? openTelemetry + : getEnvVar('OTEL_EXPORTER_OTLP_ENDPOINT', undefined); + + log.debug({ exporterType, otelEndpoint }, 'Setting up OTLP Exporter'); + + integrationName = 'OpenTelemetry'; + logAttributes.traceEndpoints.push({ + url: otelEndpoint ?? null, + type: exporterType, + }); + + log.debug({ type: exporterType }, 'Loading OpenTelemetry exporter'); + + const { OTLPTraceExporter } = await import( + `@opentelemetry/exporter-trace-${exporterType}` + ); + + processors.push( + new BatchSpanProcessor(new OTLPTraceExporter({ url: otelEndpoint })), + ); + } + + if (accessToken) { + log.debug({ target, traceEndpoint }, 'Setting up Hive Tracing'); + + integrationName ??= 'Hive Tracing'; + if (!target) { + ctx.log.error( + 'Hive tracing needs a target. Please provide it through "--hive-target "', + ); + process.exit(1); + } + + logAttributes.traceEndpoints.push({ + url: traceEndpoint, + type: 'hive tracing', + target, + }); + + processors.push( + new HiveTracingSpanProcessor({ + accessToken, + target, + endpoint: traceEndpoint, + }), + ); + } + + log.debug('Trying to load AsyncLocalStorage based Context Manager'); + + const contextManager = await import('@opentelemetry/context-async-hooks') + .then((module) => { + logAttributes.contextManager = true; + return new module.AsyncLocalStorageContextManager(); + }) + .catch(() => null); + + openTelemetrySetup({ + traces: { processors }, + contextManager, + }); + + log.info(logAttributes, `${integrationName!} integration is enabled`); + + return true; + }); + } + + return false; +} diff --git a/packages/gateway/src/commands/handleReportingConfig.ts b/packages/gateway/src/commands/handleReportingConfig.ts index f6bea3354..3fac1aad0 100644 --- a/packages/gateway/src/commands/handleReportingConfig.ts +++ b/packages/gateway/src/commands/handleReportingConfig.ts @@ -6,9 +6,12 @@ import { } from '..'; export interface ReportingCLIOptions { + hiveTarget: string | undefined; hiveRegistryToken: string | undefined; hiveUsageTarget: string | undefined; + hiveAccessToken: string | undefined; hiveUsageAccessToken: string | undefined; + hiveTraceAccessToken: string | undefined; apolloGraphRef: string | undefined; apolloKey: string | undefined; } @@ -22,7 +25,7 @@ export function handleReportingConfig( ...(loadedConfig.reporting?.type === 'hive' ? { hiveRegistryToken: loadedConfig.reporting.token, - hiveUsageTarget: loadedConfig.reporting.target, + hiveTarget: loadedConfig.reporting.target, hiveUsageAccessToken: loadedConfig.reporting.token, } : {}), @@ -33,55 +36,78 @@ export function handleReportingConfig( } : {}), }; - const opts = { ...confOpts, ...cliOpts }; + const opts = { + ...confOpts, + ...cliOpts, + hiveTarget: + // cli arguments always take precedence over config + confOpts.hiveTarget ?? cliOpts.hiveTarget ?? cliOpts.hiveUsageTarget, + }; if (cliOpts.hiveRegistryToken && cliOpts.hiveUsageAccessToken) { ctx.log.error( - `Cannot use "--hive-registry-token" with "--hive-usage-access-token". Please use "--hive-usage-target" and "--hive-usage-access-token" or the config instead.`, + 'Cannot use "--hive-registry-token" with "--hive-usage-access-token". Please use "--hive-usage-target" and "--hive-usage-access-token" or the config instead.', + ); + process.exit(1); + } + + if (cliOpts.hiveUsageTarget && cliOpts.hiveTarget) { + ctx.log.error( + 'Cannot use "--hive-usage-target" with "--hive-target". Please only use "--hive-target"', ); process.exit(1); } - if (cliOpts.hiveRegistryToken && opts.hiveUsageTarget) { + if (cliOpts.hiveRegistryToken && opts.hiveTarget) { ctx.log.error( - `Cannot use "--hive-registry-token" with a target. Please use "--hive-usage-target" and "--hive-usage-access-token" or the config instead.`, + 'Cannot use "--hive-registry-token" with a target. Please use "--hive-usage-target" and "--hive-usage-access-token" or the config instead.', ); process.exit(1); } - if (opts.hiveUsageTarget && !opts.hiveUsageAccessToken) { + if ( + opts.hiveTarget && + !opts.hiveAccessToken && + !opts.hiveUsageAccessToken && + !opts.hiveTraceAccessToken + ) { ctx.log.error( - `Hive usage target needs an access token. Please provide it through the "--hive-usage-access-token " option or the config.`, + 'Hive usage target needs an access token. Please provide it through "--hive-access-token ", or specific "--hive-usage-access-token " and "--hive-trace-access-token" options, or the config.', ); process.exit(1); } - if (opts.hiveUsageAccessToken && !opts.hiveUsageTarget) { + if ( + (opts.hiveAccessToken || + opts.hiveUsageAccessToken || + opts.hiveTraceAccessToken) && + !opts.hiveTarget + ) { ctx.log.error( - `Hive usage access token needs a target. Please provide it through the "--hive-usage-target " option or the config.`, + 'Hive access token needs a target. Please provide it through the "--hive-target " option or the config.', ); process.exit(1); } const hiveUsageAccessToken = - opts.hiveUsageAccessToken || opts.hiveRegistryToken; + opts.hiveAccessToken || opts.hiveUsageAccessToken || opts.hiveRegistryToken; if (hiveUsageAccessToken) { // different logs w and w/o the target to disambiguate if (opts.hiveUsageTarget) { - ctx.log.info(`Configuring Hive usage reporting`); + ctx.log.info('Configuring Hive usage reporting'); } else { - ctx.log.info(`Configuring Hive registry reporting`); + ctx.log.info('Configuring Hive registry reporting'); } return { ...loadedConfig.reporting, type: 'hive', token: hiveUsageAccessToken, - target: opts.hiveUsageTarget, + target: opts.hiveTarget, }; } if (opts.apolloKey) { - ctx.log.info(`Configuring Apollo GraphOS registry reporting`); + ctx.log.info('Configuring Apollo GraphOS registry reporting'); if (!opts.apolloGraphRef?.includes('@')) { ctx.log.error( `Apollo GraphOS requires a graph ref in the format @. Please provide a valid graph ref ${opts.apolloGraphRef ? `not ${opts.apolloGraphRef}` : ''}.`, diff --git a/packages/gateway/src/commands/proxy.ts b/packages/gateway/src/commands/proxy.ts index 14bf10688..9f56530d7 100644 --- a/packages/gateway/src/commands/proxy.ts +++ b/packages/gateway/src/commands/proxy.ts @@ -1,6 +1,7 @@ import cluster from 'node:cluster'; import { createGatewayRuntime, + createLoggerFromLogging, type GatewayConfigProxy, } from '@graphql-hive/gateway-runtime'; import { PubSub } from '@graphql-hive/pubsub'; @@ -18,7 +19,7 @@ import { } from '../config'; import { startServerForRuntime } from '../servers/startServerForRuntime'; import { handleFork } from './handleFork'; -import { handleLoggingConfig } from './handleLoggingOption'; +import { handleOpenTelemetryConfig } from './handleOpenTelemetryConfig'; import { handleReportingConfig } from './handleReportingConfig'; export const addCommand: AddCommand = (ctx, cli) => @@ -34,16 +35,34 @@ export const addCommand: AddCommand = (ctx, cli) => ) .action(async function proxy(endpoint) { const { + opentelemetry, + opentelemetryExporterType, hiveCdnEndpoint, hiveCdnKey, hiveRegistryToken, + hiveTarget, hiveUsageTarget, + hiveAccessToken, hiveUsageAccessToken, + hiveTraceAccessToken, + hiveTraceEndpoint, maskedErrors, hivePersistedDocumentsEndpoint, hivePersistedDocumentsToken, ...opts } = this.optsWithGlobals(); + + ctx.log.info(`Starting ${ctx.productName} ${ctx.version} in proxy mode`); + + const openTelemetryEnabledByCLI = await handleOpenTelemetryConfig(ctx, { + openTelemetry: opentelemetry, + openTelemetryExporterType: opentelemetryExporterType, + hiveAccessToken, + hiveTarget, + hiveTraceAccessToken, + hiveTraceEndpoint, + }); + const loadedConfig = await loadConfig({ log: ctx.log, configPath: opts.configPath, @@ -69,11 +88,10 @@ export const addCommand: AddCommand = (ctx, cli) => const hiveCdnEndpointOpt = // TODO: take schema from optsWithGlobals once https://github.com/commander-js/extra-typings/pull/76 is merged this.opts().schema || hiveCdnEndpoint; - const hiveCdnLogger = ctx.log.child({ source: 'Hive CDN' }); if (hiveCdnEndpointOpt) { if (hiveCdnKey) { if (!isUrl(hiveCdnEndpointOpt)) { - hiveCdnLogger.error( + ctx.log.error( 'Endpoint must be a URL when providing --hive-cdn-key but got ' + hiveCdnEndpointOpt, ); @@ -102,8 +120,11 @@ export const addCommand: AddCommand = (ctx, cli) => const registryConfig: Pick = {}; const reporting = handleReportingConfig(ctx, loadedConfig, { hiveRegistryToken, + hiveTarget, hiveUsageTarget, + hiveAccessToken, hiveUsageAccessToken, + hiveTraceAccessToken, // proxy can only do reporting to hive registry apolloGraphRef: undefined, apolloKey: undefined, @@ -115,20 +136,23 @@ export const addCommand: AddCommand = (ctx, cli) => const pubsub = loadedConfig.pubsub || new PubSub(); const cwd = loadedConfig.cwd || process.cwd(); if (loadedConfig.logging != null) { - handleLoggingConfig(loadedConfig.logging, ctx); + ctx.log = createLoggerFromLogging(loadedConfig.logging); } const cache = await getCacheInstanceFromConfig(loadedConfig, { pubsub, - logger: ctx.log, + log: ctx.log, cwd, }); const builtinPlugins = await getBuiltinPluginsFromConfig( { ...loadedConfig, ...opts, + openTelemetry: openTelemetryEnabledByCLI + ? { ...loadedConfig.openTelemetry, traces: true } + : loadedConfig.openTelemetry, }, { - logger: ctx.log, + log: ctx.log, cache, pubsub, cwd, @@ -204,10 +228,14 @@ export async function runProxy({ log }: CLIContext, config: ProxyConfig) { return; } - log.info(`Proxying requests to ${config.proxy.endpoint}`); - const runtime = createGatewayRuntime(config); + log.info({ endpoint: config.proxy.endpoint }, 'Loading schema'); + + await runtime.getSchema(); + + log.info({ endpoint: config.proxy.endpoint }, 'Proxying requests'); + await startServerForRuntime(runtime, { ...config, log, diff --git a/packages/gateway/src/commands/subgraph.ts b/packages/gateway/src/commands/subgraph.ts index 64b5cdf1c..87a8028f9 100644 --- a/packages/gateway/src/commands/subgraph.ts +++ b/packages/gateway/src/commands/subgraph.ts @@ -3,6 +3,7 @@ import { lstat } from 'node:fs/promises'; import { isAbsolute, resolve } from 'node:path'; import { createGatewayRuntime, + createLoggerFromLogging, type GatewayConfigSubgraph, type UnifiedGraphConfig, } from '@graphql-hive/gateway-runtime'; @@ -22,7 +23,7 @@ import { } from '../config'; import { startServerForRuntime } from '../servers/startServerForRuntime'; import { handleFork } from './handleFork'; -import { handleLoggingConfig } from './handleLoggingOption'; +import { handleOpenTelemetryConfig } from './handleOpenTelemetryConfig'; import { handleReportingConfig } from './handleReportingConfig'; export const addCommand: AddCommand = (ctx, cli) => @@ -37,14 +38,34 @@ export const addCommand: AddCommand = (ctx, cli) => ) .action(async function subgraph(schemaPathOrUrl) { const { + opentelemetry, + opentelemetryExporterType, maskedErrors, hiveRegistryToken, + hiveTarget, hiveUsageTarget, + hiveAccessToken, hiveUsageAccessToken, + hiveTraceAccessToken, + hiveTraceEndpoint, hivePersistedDocumentsEndpoint, hivePersistedDocumentsToken, ...opts } = this.optsWithGlobals(); + + ctx.log.info(`Starting ${ctx.productName} ${ctx.version} as subgraph`); + + // Handle hive OTEL tracing before loading config so that the tracer provider is registered + // if users needs it in a custom plugin. + const openTelemetryEnabledByCLI = await handleOpenTelemetryConfig(ctx, { + openTelemetry: opentelemetry, + openTelemetryExporterType: opentelemetryExporterType, + hiveTarget, + hiveAccessToken, + hiveTraceAccessToken, + hiveTraceEndpoint, + }); + const loadedConfig = await loadConfig({ log: ctx.log, configPath: opts.configPath, @@ -62,8 +83,11 @@ export const addCommand: AddCommand = (ctx, cli) => const registryConfig: Pick = {}; const reporting = handleReportingConfig(ctx, loadedConfig, { hiveRegistryToken, + hiveTarget, hiveUsageTarget, + hiveAccessToken, hiveUsageAccessToken, + hiveTraceAccessToken, // subgraph can only do reporting to hive registry apolloGraphRef: undefined, apolloKey: undefined, @@ -75,20 +99,23 @@ export const addCommand: AddCommand = (ctx, cli) => const pubsub = loadedConfig.pubsub || new PubSub(); const cwd = loadedConfig.cwd || process.cwd(); if (loadedConfig.logging != null) { - handleLoggingConfig(loadedConfig.logging, ctx); + ctx.log = createLoggerFromLogging(loadedConfig.logging); } const cache = await getCacheInstanceFromConfig(loadedConfig, { pubsub, - logger: ctx.log, + log: ctx.log, cwd, }); const builtinPlugins = await getBuiltinPluginsFromConfig( { ...loadedConfig, ...opts, + openTelemetry: openTelemetryEnabledByCLI + ? { ...loadedConfig.openTelemetry, traces: true } + : loadedConfig.openTelemetry, }, { - logger: ctx.log, + log: ctx.log, cache, pubsub, cwd, @@ -183,13 +210,15 @@ export async function runSubgraph({ log }: CLIContext, config: SubgraphConfig) { const runtime = createGatewayRuntime(config); if (absSchemaPath) { - log.info(`Serving local subgraph from ${absSchemaPath}`); + log.info(`Loading local subgraph from ${absSchemaPath}`); } else if (isUrl(String(config.subgraph))) { - log.info(`Serving remote subgraph from ${config.subgraph}`); + log.info(`Loading remote subgraph from ${config.subgraph}`); } else { - log.info('Serving subgraph from config'); + log.info('Loading subgraph from config'); } + await runtime.getSchema(); + await startServerForRuntime(runtime, { ...config, log, diff --git a/packages/gateway/src/commands/supergraph.ts b/packages/gateway/src/commands/supergraph.ts index 7c6bbf31c..c1f1edfa9 100644 --- a/packages/gateway/src/commands/supergraph.ts +++ b/packages/gateway/src/commands/supergraph.ts @@ -4,6 +4,7 @@ import { isAbsolute, resolve } from 'node:path'; import { Option } from '@commander-js/extra-typings'; import { createGatewayRuntime, + createLoggerFromLogging, type GatewayConfigSupergraph, type GatewayGraphOSManagedFederationOptions, type GatewayHiveCDNOptions, @@ -29,7 +30,7 @@ import { } from '../config'; import { startServerForRuntime } from '../servers/startServerForRuntime'; import { handleFork } from './handleFork'; -import { handleLoggingConfig } from './handleLoggingOption'; +import { handleOpenTelemetryConfig } from './handleOpenTelemetryConfig'; import { handleReportingConfig } from './handleReportingConfig'; export const addCommand: AddCommand = (ctx, cli) => @@ -50,11 +51,17 @@ export const addCommand: AddCommand = (ctx, cli) => ) .action(async function supergraph(schemaPathOrUrl) { const { + opentelemetry, + opentelemetryExporterType, hiveCdnEndpoint, hiveCdnKey, hiveRegistryToken, hiveUsageTarget, + hiveTarget, + hiveAccessToken, hiveUsageAccessToken, + hiveTraceAccessToken, + hiveTraceEndpoint, maskedErrors, apolloGraphRef, apolloKey, @@ -66,6 +73,19 @@ export const addCommand: AddCommand = (ctx, cli) => // TODO: move to optsWithGlobals once https://github.com/commander-js/extra-typings/pull/76 is merged const { apolloUplink } = this.opts(); + ctx.log.info( + `Starting ${ctx.productName} ${ctx.version} with supergraph`, + ); + + const openTelemetryEnabledByCLI = await handleOpenTelemetryConfig(ctx, { + openTelemetry: opentelemetry, + openTelemetryExporterType: opentelemetryExporterType, + hiveAccessToken, + hiveTarget, + hiveTraceAccessToken, + hiveTraceEndpoint, + }); + const loadedConfig = await loadConfig({ log: ctx.log, configPath: opts.configPath, @@ -76,15 +96,14 @@ export const addCommand: AddCommand = (ctx, cli) => let supergraph: | UnifiedGraphConfig | GatewayHiveCDNOptions - | GatewayGraphOSManagedFederationOptions = 'supergraph.graphql'; + | GatewayGraphOSManagedFederationOptions = './supergraph.graphql'; if (schemaPathOrUrl) { - ctx.log.info(`Supergraph will be loaded from ${schemaPathOrUrl}`); + ctx.log.info(`Supergraph will be loaded from "${schemaPathOrUrl}"`); if (hiveCdnKey) { - ctx.log.info(`Using Hive CDN key`); + ctx.log.info('Using Hive CDN key'); if (!isUrl(schemaPathOrUrl)) { ctx.log.error( - 'Hive CDN endpoint must be a URL when providing --hive-cdn-key but got ' + - schemaPathOrUrl, + `Hive CDN endpoint must be a URL when providing --hive-cdn-key but got "${schemaPathOrUrl}"`, ); process.exit(1); } @@ -94,10 +113,10 @@ export const addCommand: AddCommand = (ctx, cli) => key: hiveCdnKey, }; } else if (apolloKey) { - ctx.log.info(`Using GraphOS API key`); + ctx.log.info('Using GraphOS API key'); if (!schemaPathOrUrl.includes('@')) { ctx.log.error( - `Apollo GraphOS requires a graph ref in the format @ when providing --apollo-key. Please provide a valid graph ref not ${schemaPathOrUrl}.`, + `Apollo GraphOS requires a graph ref in the format @ when providing --apollo-key. Please provide a valid graph ref not "${schemaPathOrUrl}".`, ); process.exit(1); } @@ -124,7 +143,7 @@ export const addCommand: AddCommand = (ctx, cli) => ); process.exit(1); } - ctx.log.info(`Using Hive CDN endpoint: ${hiveCdnEndpoint}`); + ctx.log.info(`Using Hive CDN endpoint ${hiveCdnEndpoint}`); supergraph = { type: 'hive', endpoint: hiveCdnEndpoint, @@ -139,11 +158,11 @@ export const addCommand: AddCommand = (ctx, cli) => } if (!apolloKey) { ctx.log.error( - `Apollo GraphOS requires an API key. Please provide an API key using the --apollo-key option.`, + 'Apollo GraphOS requires an API key. Please provide an API key using the --apollo-key option.', ); process.exit(1); } - ctx.log.info(`Using Apollo Graph Ref: ${apolloGraphRef}`); + ctx.log.info(`Using Apollo Graph Ref ${apolloGraphRef}`); supergraph = { type: 'graphos', apiKey: apolloKey, @@ -154,11 +173,14 @@ export const addCommand: AddCommand = (ctx, cli) => supergraph = loadedConfig.supergraph!; // TODO: assertion wont be necessary when exactOptionalPropertyTypes // TODO: how to provide hive-cdn-key? } else { - ctx.log.info(`Using default supergraph location: ${supergraph}`); + ctx.log.info(`Using default supergraph location "${supergraph}"`); } const registryConfig: Pick = {}; const reporting = handleReportingConfig(ctx, loadedConfig, { + hiveTarget, + hiveAccessToken, + hiveTraceAccessToken, hiveRegistryToken, hiveUsageTarget, hiveUsageAccessToken, @@ -172,20 +194,23 @@ export const addCommand: AddCommand = (ctx, cli) => const pubsub = loadedConfig.pubsub || new PubSub(); const cwd = loadedConfig.cwd || process.cwd(); if (loadedConfig.logging != null) { - handleLoggingConfig(loadedConfig.logging, ctx); + ctx.log = createLoggerFromLogging(loadedConfig.logging); } const cache = await getCacheInstanceFromConfig(loadedConfig, { pubsub, - logger: ctx.log, + log: ctx.log, cwd, }); const builtinPlugins = await getBuiltinPluginsFromConfig( { ...loadedConfig, ...opts, + openTelemetry: openTelemetryEnabledByCLI + ? { ...loadedConfig.openTelemetry, traces: true } + : loadedConfig.openTelemetry, }, { - logger: ctx.log, + log: ctx.log, cache, pubsub, cwd, @@ -225,7 +250,7 @@ export const addCommand: AddCommand = (ctx, cli) => loadedConfig.persistedDocuments.token); if (!token) { ctx.log.error( - `Hive persisted documents needs a CDN token. Please provide it through the "--hive-persisted-documents-token " option or the config.`, + 'Hive persisted documents needs a CDN token. Please provide it through the "--hive-persisted-documents-token " option or the config.', ); process.exit(1); } @@ -271,12 +296,12 @@ export async function runSupergraph( absSchemaPath = isAbsolute(supergraphPath) ? String(supergraphPath) : resolve(process.cwd(), supergraphPath); - log.info(`Reading supergraph from ${absSchemaPath}`); try { await lstat(absSchemaPath); - } catch { + } catch (err) { log.error( - `Could not read supergraph from ${absSchemaPath}. Make sure the file exists.`, + { path: absSchemaPath, err }, + 'Could not find supergraph. Make sure the file exists.', ); process.exit(1); } @@ -286,11 +311,14 @@ export async function runSupergraph( // Polling should not be enabled when watching the file delete config.pollingInterval; if (cluster.isPrimary) { - log.info(`Watching ${absSchemaPath} for changes`); + log.info({ path: absSchemaPath }, 'Watching supergraph file for changes'); const ctrl = new AbortController(); registerTerminateHandler((signal) => { - log.info(`Closing watcher for ${absSchemaPath} on ${signal}`); + log.info( + { path: absSchemaPath }, + `Closing watcher for supergraph on ${signal}`, + ); return ctrl.abort(`Process terminated on ${signal}`); }); @@ -302,7 +330,10 @@ export async function runSupergraph( // TODO: or should we just ignore? throw new Error(`Supergraph file was renamed to "${f.filename}"`); } - log.info(`${absSchemaPath} changed. Invalidating supergraph...`); + log.info( + { path: absSchemaPath }, + 'Supergraph changed. Invalidating...', + ); if (config.fork && config.fork > 1) { for (const workerId in cluster.workers) { cluster.workers[workerId]!.send('invalidateUnifiedGraph'); @@ -315,10 +346,16 @@ export async function runSupergraph( })() .catch((e) => { if (e.name === 'AbortError') return; - log.error(`Watcher for ${absSchemaPath} closed with an error`, e); + log.error( + { path: absSchemaPath, err: e }, + 'Supergraph watcher closed with an error', + ); }) .then(() => { - log.info(`Watcher for ${absSchemaPath} successfuly closed`); + log.info( + { path: absSchemaPath }, + 'Supergraph watcher successfuly closed', + ); }); } } @@ -353,21 +390,24 @@ export async function runSupergraph( const runtime = createGatewayRuntime(config); if (absSchemaPath) { - log.info(`Serving local supergraph from ${absSchemaPath}`); + log.info({ path: absSchemaPath }, 'Loading local supergraph'); } else if (isUrl(String(config.supergraph))) { - log.info(`Serving remote supergraph from ${config.supergraph}`); + log.info({ url: config.supergraph }, 'Loading remote supergraph'); } else if ( typeof config.supergraph === 'object' && 'type' in config.supergraph && config.supergraph.type === 'hive' ) { log.info( - `Serving supergraph from Hive CDN at ${config.supergraph.endpoint}`, + { endpoint: config.supergraph.endpoint }, + 'Loading supergraph from Hive CDN', ); } else { - log.info('Serving supergraph from config'); + log.info('Loading supergraph from config'); } + await runtime.getSchema(); + await startServerForRuntime(runtime, { ...config, log, diff --git a/packages/gateway/src/config.ts b/packages/gateway/src/config.ts index 9de1b580c..fcbda7647 100644 --- a/packages/gateway/src/config.ts +++ b/packages/gateway/src/config.ts @@ -5,8 +5,9 @@ import type { GatewayConfig, GatewayPlugin, } from '@graphql-hive/gateway-runtime'; +import { LegacyLogger, type Logger } from '@graphql-hive/logger'; import { HivePubSub } from '@graphql-hive/pubsub'; -import type { KeyValueCache, Logger } from '@graphql-mesh/types'; +import type { KeyValueCache } from '@graphql-mesh/types'; import type { GatewayCLIBuiltinPluginConfig } from './cli'; import type { ServerConfig } from './servers/types'; @@ -105,7 +106,7 @@ export async function getBuiltinPluginsFromConfig( config: GatewayCLIBuiltinPluginConfig, ctx: { cache: KeyValueCache; - logger: Logger; + log: Logger; pubsub: HivePubSub; cwd: string; }, @@ -125,12 +126,7 @@ export async function getBuiltinPluginsFromConfig( const { useOpenTelemetry } = await import( '@graphql-mesh/plugin-opentelemetry' ); - plugins.push( - useOpenTelemetry({ - logger: ctx.logger, - ...config.openTelemetry, - }), - ); + plugins.push(useOpenTelemetry({ ...config.openTelemetry, log: ctx.log })); } if (config.rateLimiting) { @@ -204,7 +200,7 @@ export async function getBuiltinPluginsFromConfig( */ export async function getCacheInstanceFromConfig( config: GatewayCLIBuiltinPluginConfig, - ctx: { logger: Logger; pubsub: HivePubSub; cwd: string }, + ctx: { log: Logger; pubsub: HivePubSub; cwd: string }, ): Promise { if (typeof config.cache === 'function') { return config.cache(ctx); @@ -219,6 +215,8 @@ export async function getCacheInstanceFromConfig( return new RedisCache({ ...ctx, ...config.cache, + // TODO: use new logger + logger: LegacyLogger.from(ctx.log), }) as KeyValueCache; } case 'cfw-kv': { @@ -241,7 +239,7 @@ export async function getCacheInstanceFromConfig( } } if (config.cache.type !== 'localforage') { - ctx.logger.warn( + ctx.log.warn( 'Unknown cache type, falling back to localforage', config.cache, ); diff --git a/packages/gateway/src/index.ts b/packages/gateway/src/index.ts index 5a01d9d6d..dc636d330 100644 --- a/packages/gateway/src/index.ts +++ b/packages/gateway/src/index.ts @@ -1,6 +1,6 @@ export * from './cli'; +export * from '@graphql-hive/logger'; export * from '@graphql-hive/gateway-runtime'; -export { LogLevel, DefaultLogger } from '@graphql-mesh/utils'; export { PubSub } from '@graphql-hive/pubsub'; export * from '@graphql-mesh/plugin-jwt-auth'; export * from '@graphql-mesh/plugin-opentelemetry'; @@ -8,7 +8,6 @@ export * from '@graphql-mesh/plugin-prometheus'; export { default as useRateLimit } from '@graphql-mesh/plugin-rate-limit'; export { default as useHttpCache } from '@graphql-mesh/plugin-http-cache'; export { useDeduplicateRequest } from '@graphql-hive/plugin-deduplicate-request'; -export { default as useMock } from '@graphql-mesh/plugin-mock'; export { default as useSnapshot } from '@graphql-mesh/plugin-snapshot'; export { default as CloudflareKVCacheStorage } from '@graphql-mesh/cache-cfw-kv'; export { default as RedisCacheStorage } from '@graphql-mesh/cache-redis'; diff --git a/packages/gateway/src/servers/nodeHttp.ts b/packages/gateway/src/servers/nodeHttp.ts index ad6b7e48c..978c916cb 100644 --- a/packages/gateway/src/servers/nodeHttp.ts +++ b/packages/gateway/src/servers/nodeHttp.ts @@ -98,12 +98,12 @@ export async function startNodeHttpServer>( gwRuntime.disposableStack.defer( () => new Promise((resolve, reject) => { - log.info(`Stopping the WebSocket server`); + log.info('Stopping the WebSocket server'); wsServer.close((err) => { if (err) { return reject(err); } - log.info(`Stopped the WebSocket server successfully`); + log.info('Stopped the WebSocket server successfully'); return resolve(); }); }), @@ -117,10 +117,10 @@ export async function startNodeHttpServer>( () => new Promise((resolve) => { process.stderr.write('\n'); - log.info(`Stopping the server`); + log.info('Stopping the server'); server.closeAllConnections(); server.close(() => { - log.info(`Stopped the server successfully`); + log.info('Stopped the server successfully'); return resolve(); }); }), diff --git a/packages/gateway/src/servers/startServerForRuntime.ts b/packages/gateway/src/servers/startServerForRuntime.ts index 6cd4e14a8..0826a0851 100644 --- a/packages/gateway/src/servers/startServerForRuntime.ts +++ b/packages/gateway/src/servers/startServerForRuntime.ts @@ -20,7 +20,7 @@ export function startServerForRuntime< ): MaybePromise { process.on('message', (message) => { if (message === 'invalidateUnifiedGraph') { - log.info(`Invalidating Supergraph`); + log.info('Invalidating Supergraph'); runtime.invalidateUnifiedGraph(); } }); diff --git a/packages/gateway/src/servers/types.ts b/packages/gateway/src/servers/types.ts index e29222e46..950901a53 100644 --- a/packages/gateway/src/servers/types.ts +++ b/packages/gateway/src/servers/types.ts @@ -1,4 +1,4 @@ -import { Logger } from '@graphql-mesh/types'; +import type { Logger } from '@graphql-hive/logger'; export interface ServerConfig { /** diff --git a/packages/importer/package.json b/packages/importer/package.json index 82eac97af..e6f141feb 100644 --- a/packages/importer/package.json +++ b/packages/importer/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/logger-json/CHANGELOG.md b/packages/logger-json/CHANGELOG.md deleted file mode 100644 index f5bd329ab..000000000 --- a/packages/logger-json/CHANGELOG.md +++ /dev/null @@ -1,60 +0,0 @@ -# @graphql-hive/logger-json - -## 0.0.7 - -### Patch Changes - -- [#1383](https://github.com/graphql-hive/gateway/pull/1383) [`a832e7b`](https://github.com/graphql-hive/gateway/commit/a832e7bf9a8f92c48fb9df8ca1bff5a008dcf420) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.7` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.7) (from `^0.104.5`, in `dependencies`) - - Updated dependency [`@graphql-mesh/utils@^0.104.7` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.7) (from `^0.104.5`, in `dependencies`) - -## 0.0.6 - -### Patch Changes - -- [#1333](https://github.com/graphql-hive/gateway/pull/1333) [`ffa3753`](https://github.com/graphql-hive/gateway/commit/ffa3753ccb9045c5b2d62af05edc7f1d78336cb3) Thanks [@enisdenjo](https://github.com/enisdenjo)! - Isomorphic environment variable getter with truthy value parsing - -## 0.0.5 - -### Patch Changes - -- [#1245](https://github.com/graphql-hive/gateway/pull/1245) [`29f537f`](https://github.com/graphql-hive/gateway/commit/29f537f7dfcf17f3911efd5845d7af1e532d2e85) Thanks [@enisdenjo](https://github.com/enisdenjo)! - dependencies updates: - - Updated dependency [`@graphql-mesh/utils@^0.104.5` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.5) (from `^0.104.2`, in `dependencies`) - -- [#1258](https://github.com/graphql-hive/gateway/pull/1258) [`3d24beb`](https://github.com/graphql-hive/gateway/commit/3d24beb7b15fd8109f86bbb3dfd514f6b8202741) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.5` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.5) (from `^0.104.0`, in `dependencies`) - -## 0.0.4 - -### Patch Changes - -- [#946](https://github.com/graphql-hive/gateway/pull/946) [`7d771d8`](https://github.com/graphql-hive/gateway/commit/7d771d89ff6d731b1025acfc5eb197541a6d5d35) Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: - - Updated dependency [`@graphql-mesh/utils@^0.104.2` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.2) (from `^0.104.1`, in `dependencies`) - -## 0.0.3 - -### Patch Changes - -- [#706](https://github.com/graphql-hive/gateway/pull/706) [`e393337`](https://github.com/graphql-hive/gateway/commit/e393337ecb40beffb79748b19b5aa8f2fd9197b7) Thanks [@EmrysMyrddin](https://github.com/EmrysMyrddin)! - dependencies updates: - - Updated dependency [`@graphql-mesh/utils@^0.104.1` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.1) (from `^0.104.0`, in `dependencies`) - -- [#775](https://github.com/graphql-hive/gateway/pull/775) [`33f7dfd`](https://github.com/graphql-hive/gateway/commit/33f7dfdb10eef2a1e7f6dffe0ce6e4bb3cc7c2c6) Thanks [@renovate](https://github.com/apps/renovate)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.0` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.0) (from `^0.103.18`, in `dependencies`) - - Updated dependency [`@graphql-mesh/utils@^0.104.0` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.0) (from `^0.103.18`, in `dependencies`) - -## 0.0.2 - -### Patch Changes - -- [#697](https://github.com/graphql-hive/gateway/pull/697) [`6cc87c6`](https://github.com/graphql-hive/gateway/commit/6cc87c6e9aa0cbb9eff517eeec92d57b9c96d39e) Thanks [@renovate](https://github.com/apps/renovate)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.103.18` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.103.18) (from `^0.103.16`, in `dependencies`) - - Updated dependency [`@graphql-mesh/utils@^0.103.18` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.103.18) (from `^0.103.16`, in `dependencies`) - -## 0.0.1 - -### Patch Changes - -- [#642](https://github.com/graphql-hive/gateway/pull/642) [`30e41a6`](https://github.com/graphql-hive/gateway/commit/30e41a6f5b97c42ae548564bce3f6e4a92b1225f) Thanks [@ardatan](https://github.com/ardatan)! - New JSON-based logger - - By default, it prints pretty still to the console unless NODE_ENV is production. - For JSON output, set the `LOG_FORMAT` environment variable to `json`. diff --git a/packages/logger-json/package.json b/packages/logger-json/package.json deleted file mode 100644 index 7f6febf79..000000000 --- a/packages/logger-json/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@graphql-hive/logger-json", - "version": "0.0.7", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/graphql-hive/gateway.git", - "directory": "packages/logger-json" - }, - "author": { - "email": "contact@the-guild.dev", - "name": "The Guild", - "url": "https://the-guild.dev" - }, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - }, - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "./package.json": "./package.json" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "pkgroll --clean-dist", - "prepack": "yarn build" - }, - "peerDependencies": { - "graphql": "^15.9.0 || ^16.9.0" - }, - "dependencies": { - "@graphql-mesh/cross-helpers": "^0.4.10", - "@graphql-mesh/types": "^0.104.7", - "@graphql-mesh/utils": "^0.104.7", - "cross-inspect": "^1.0.1", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "^16.9.0", - "pkgroll": "2.15.0" - }, - "sideEffects": false -} diff --git a/packages/logger-json/src/index.ts b/packages/logger-json/src/index.ts deleted file mode 100644 index ba6c3e853..000000000 --- a/packages/logger-json/src/index.ts +++ /dev/null @@ -1,185 +0,0 @@ -import type { LazyLoggerMessage, Logger } from '@graphql-mesh/types'; -import { LogLevel } from '@graphql-mesh/utils'; -import { getEnvStr } from '~internal/env'; -import { inspect } from 'cross-inspect'; - -export interface JSONLoggerOptions { - name?: string; - meta?: Record; - level?: LogLevel; - console?: Console; -} -function truthy(val: unknown) { - return ( - val === true || - val === 1 || - ['1', 't', 'true', 'y', 'yes'].includes(String(val)) - ); -} - -declare global { - var DEBUG: string; -} - -export class JSONLogger implements Logger { - name?: string; - meta: Record; - logLevel: LogLevel; - console: Console; - constructor(opts?: JSONLoggerOptions) { - this.name = opts?.name; - this.console = opts?.console || console; - this.meta = opts?.meta || {}; - const debugStrs = [getEnvStr('DEBUG'), globalThis.DEBUG]; - if (opts?.level != null) { - this.logLevel = opts.level; - } else { - this.logLevel = LogLevel.info; - for (const debugStr of debugStrs) { - if (debugStr) { - if (truthy(debugStr)) { - this.logLevel = LogLevel.debug; - break; - } - if (opts?.name) { - if (debugStr?.toString()?.includes(opts.name)) { - this.logLevel = LogLevel.debug; - break; - } - } - } - } - } - } - - log(...messageArgs: LazyLoggerMessage[]) { - if (this.logLevel > LogLevel.info) { - return; - } - const finalMessage = this.prepareFinalMessage('info', messageArgs); - this.console.log(finalMessage); - } - - warn(...messageArgs: LazyLoggerMessage[]) { - if (this.logLevel > LogLevel.warn) { - return; - } - const finalMessage = this.prepareFinalMessage('warn', messageArgs); - this.console.warn(finalMessage); - } - - info(...messageArgs: LazyLoggerMessage[]) { - if (this.logLevel > LogLevel.info) { - return; - } - const finalMessage = this.prepareFinalMessage('info', messageArgs); - this.console.info(finalMessage); - } - - error(...messageArgs: LazyLoggerMessage[]) { - if (this.logLevel > LogLevel.error) { - return; - } - const finalMessage = this.prepareFinalMessage('error', messageArgs); - this.console.error(finalMessage); - } - - debug(...messageArgs: LazyLoggerMessage[]) { - if (this.logLevel > LogLevel.debug) { - return; - } - const finalMessage = this.prepareFinalMessage('debug', messageArgs); - this.console.debug(finalMessage); - } - - child(nameOrMeta: string | Record) { - let newName: string | undefined; - let newMeta: Record; - if (typeof nameOrMeta === 'string') { - newName = this.name ? `${this.name}, ${nameOrMeta}` : nameOrMeta; - newMeta = this.meta; - } else if (typeof nameOrMeta === 'object') { - newName = this.name; - newMeta = { ...this.meta, ...nameOrMeta }; - } else { - throw new Error('Invalid argument type'); - } - return new JSONLogger({ - name: newName, - meta: newMeta, - level: this.logLevel, - console: this.console, - }); - } - - addPrefix(prefix: string | Record) { - if (typeof prefix === 'string') { - this.name = this.name ? `${this.name}, ${prefix}` : prefix; - } else if (typeof prefix === 'object') { - this.meta = { ...this.meta, ...prefix }; - } - return this; - } - - private prepareFinalMessage(level: string, messageArgs: LazyLoggerMessage[]) { - const flattenedMessageArgs = messageArgs - .flat(Infinity) - .flatMap((messageArg) => { - if (typeof messageArg === 'function') { - messageArg = messageArg(); - } - if (messageArg?.toJSON) { - messageArg = messageArg.toJSON(); - } - if (messageArg instanceof AggregateError) { - return messageArg.errors; - } - return messageArg; - }); - const finalMessage: Record = { - ...this.meta, - level, - time: new Date().toISOString(), - }; - if (this.name) { - finalMessage['name'] = this.name; - } - const extras: any[] = []; - for (let messageArg of flattenedMessageArgs) { - if (messageArg == null) { - continue; - } - const typeofMessageArg = typeof messageArg; - if ( - typeofMessageArg === 'string' || - typeofMessageArg === 'number' || - typeofMessageArg === 'boolean' - ) { - finalMessage['msg'] = finalMessage['msg'] - ? finalMessage['msg'] + ', ' + messageArg - : messageArg; - } else if (typeofMessageArg === 'object') { - if (messageArg instanceof Error) { - finalMessage['msg'] = finalMessage['msg'] - ? finalMessage['msg'] + ', ' + messageArg.message - : messageArg.message; - finalMessage['stack'] = messageArg.stack; - } else if ( - Object.prototype.toString.call(messageArg).startsWith('[object') - ) { - Object.assign(finalMessage, messageArg); - } else { - extras.push(messageArg); - } - } - } - if (extras.length) { - if (extras.length === 1) { - finalMessage['extras'] = inspect(extras[0]); - } else { - finalMessage['extras'] = extras.map((extra) => inspect(extra)); - } - } - return JSON.stringify(finalMessage); - } -} diff --git a/packages/logger-pino/CHANGELOG.md b/packages/logger-pino/CHANGELOG.md deleted file mode 100644 index 402139719..000000000 --- a/packages/logger-pino/CHANGELOG.md +++ /dev/null @@ -1,42 +0,0 @@ -# @graphql-hive/logger-pino - -## 1.0.3 - -### Patch Changes - -- [#1383](https://github.com/graphql-hive/gateway/pull/1383) [`a832e7b`](https://github.com/graphql-hive/gateway/commit/a832e7bf9a8f92c48fb9df8ca1bff5a008dcf420) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.7` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.7) (from `^0.104.5`, in `dependencies`) - - Updated dependency [`@graphql-mesh/utils@^0.104.7` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.7) (from `^0.104.5`, in `dependencies`) - -## 1.0.2 - -### Patch Changes - -- [#1245](https://github.com/graphql-hive/gateway/pull/1245) [`29f537f`](https://github.com/graphql-hive/gateway/commit/29f537f7dfcf17f3911efd5845d7af1e532d2e85) Thanks [@enisdenjo](https://github.com/enisdenjo)! - dependencies updates: - - Updated dependency [`@graphql-mesh/utils@^0.104.5` ↗︎](https://www.npmjs.com/package/@graphql-mesh/utils/v/0.104.5) (from `^0.104.2`, in `dependencies`) - -- [#1258](https://github.com/graphql-hive/gateway/pull/1258) [`3d24beb`](https://github.com/graphql-hive/gateway/commit/3d24beb7b15fd8109f86bbb3dfd514f6b8202741) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.5` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.5) (from `^0.104.0`, in `dependencies`) - -## 1.0.1 - -### Patch Changes - -- [#1156](https://github.com/graphql-hive/gateway/pull/1156) [`fb74009`](https://github.com/graphql-hive/gateway/commit/fb740098652dba2e9107981d1f4e362143478451) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`pino@^9.7.0` ↗︎](https://www.npmjs.com/package/pino/v/9.7.0) (from `^9.6.0`, in `peerDependencies`) - -## 1.0.0 - -### Major Changes - -- [#946](https://github.com/graphql-hive/gateway/pull/946) [`7d771d8`](https://github.com/graphql-hive/gateway/commit/7d771d89ff6d731b1025acfc5eb197541a6d5d35) Thanks [@ardatan](https://github.com/ardatan)! - New Pino integration (also helpful for Fastify integration); - - ```ts - import { defineConfig } from '@graphql-hive/gateway'; - import { createLoggerFromPino } from '@graphql-hive/logger-pino'; - import pino from 'pino'; - - export const gatewayConfig = defineConfig({ - logging: createLoggerFromPino(pino({ level: 'info' })), - }); - ``` diff --git a/packages/logger-pino/package.json b/packages/logger-pino/package.json deleted file mode 100644 index 3a3ce21a1..000000000 --- a/packages/logger-pino/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@graphql-hive/logger-pino", - "version": "1.0.3", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/graphql-hive/gateway.git", - "directory": "packages/logger-pino" - }, - "homepage": "https://the-guild.dev/graphql/hive/docs/gateway", - "author": { - "email": "contact@the-guild.dev", - "name": "The Guild", - "url": "https://the-guild.dev" - }, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - }, - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "./package.json": "./package.json" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "pkgroll --clean-dist", - "prepack": "yarn build" - }, - "peerDependencies": { - "graphql": "^15.9.0 || ^16.9.0", - "pino": "^9.7.0" - }, - "dependencies": { - "@graphql-mesh/types": "^0.104.7", - "@graphql-mesh/utils": "^0.104.7", - "@whatwg-node/disposablestack": "^0.0.6", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "16.11.0", - "pino": "^9.7.0", - "pkgroll": "2.15.0" - }, - "sideEffects": false -} diff --git a/packages/logger-pino/src/index.ts b/packages/logger-pino/src/index.ts deleted file mode 100644 index bd48c9a3a..000000000 --- a/packages/logger-pino/src/index.ts +++ /dev/null @@ -1,125 +0,0 @@ -import type { - LazyLoggerMessage, - Logger as MeshLogger, -} from '@graphql-mesh/types'; -import { LogLevel } from '@graphql-mesh/utils'; -import type pino from 'pino'; - -type PinoWithChild = pino.BaseLogger & { - child: (meta: any) => PinoWithChild; -}; - -function prepareArgs(messageArgs: LazyLoggerMessage[]): Parameters { - const flattenedMessageArgs = messageArgs - .flat(Infinity) - .flatMap((messageArg) => { - if (typeof messageArg === 'function') { - messageArg = messageArg(); - } - if (messageArg?.toJSON) { - messageArg = messageArg.toJSON(); - } - if (messageArg instanceof AggregateError) { - return messageArg.errors; - } - return messageArg; - }); - let message: string = ''; - const extras: any[] = []; - for (let messageArg of flattenedMessageArgs) { - if (messageArg == null) { - continue; - } - const typeofMessageArg = typeof messageArg; - if ( - typeofMessageArg === 'string' || - typeofMessageArg === 'number' || - typeofMessageArg === 'boolean' - ) { - message = message ? message + ', ' + messageArg : messageArg; - } else if (typeofMessageArg === 'object') { - extras.push(messageArg); - } - } - if (extras.length > 0) { - return [Object.assign({}, ...extras), message]; - } - return [message]; -} - -class PinoLoggerAdapter implements MeshLogger { - public name?: string; - constructor( - private pinoLogger: PinoWithChild, - private meta: Record = {}, - ) { - if (meta['name']) { - this.name = meta['name']; - } - } - - get level(): LogLevel { - if (this.pinoLogger.level) { - return LogLevel[this.pinoLogger.level as keyof typeof LogLevel]; - } - return LogLevel.silent; - } - - set level(level: LogLevel) { - this.pinoLogger.level = LogLevel[level]; - } - - isLevelEnabled(level: LogLevel) { - if (this.level > level) { - return false; - } - return true; - } - - log(...args: any[]) { - if (this.isLevelEnabled(LogLevel.info)) { - this.pinoLogger.info(...prepareArgs(args)); - } - } - info(...args: any[]) { - if (this.isLevelEnabled(LogLevel.info)) { - this.pinoLogger.info(...prepareArgs(args)); - } - } - warn(...args: any[]) { - if (this.isLevelEnabled(LogLevel.warn)) { - this.pinoLogger.warn(...prepareArgs(args)); - } - } - error(...args: any[]) { - if (this.isLevelEnabled(LogLevel.error)) { - this.pinoLogger.error(...prepareArgs(args)); - } - } - debug(...lazyArgs: LazyLoggerMessage[]) { - if (this.isLevelEnabled(LogLevel.debug)) { - this.pinoLogger.debug(...prepareArgs(lazyArgs)); - } - } - child(nameOrMeta: string | Record) { - if (typeof nameOrMeta === 'string') { - nameOrMeta = { - name: this.name - ? this.name.includes(nameOrMeta) - ? this.name - : `${this.name}, ${nameOrMeta}` - : nameOrMeta, - }; - } - return new PinoLoggerAdapter(this.pinoLogger.child(nameOrMeta), { - ...this.meta, - ...nameOrMeta, - }); - } -} - -export function createLoggerFromPino( - pinoLogger: PinoWithChild, -): PinoLoggerAdapter { - return new PinoLoggerAdapter(pinoLogger); -} diff --git a/packages/logger-pino/tests/pino.spec.ts b/packages/logger-pino/tests/pino.spec.ts deleted file mode 100644 index bde7486a7..000000000 --- a/packages/logger-pino/tests/pino.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -import { hostname } from 'node:os'; -import { Writable } from 'node:stream'; -import pino from 'pino'; -import { describe, expect, it } from 'vitest'; -import { createLoggerFromPino } from '../src'; - -describe('Pino', () => { - let log = ''; - let lastCallback = () => {}; - const stream = new Writable({ - write(chunk, _encoding, callback) { - log = chunk.toString('utf-8'); - lastCallback = callback; - }, - }); - const logLevels = ['error', 'warn', 'info', 'debug'] as const; - for (const level of logLevels) { - describe(`Level: ${level}`, () => { - it('basic', async () => { - const logger = pino( - { - level, - }, - stream, - ); - const loggerAdapter = createLoggerFromPino(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - loggerAdapter[level](...testData); - lastCallback(); - const logJson = JSON.parse(log); - expect(logJson).toEqual({ - level: pino.levels.values[level], - foo: 'bar', - msg: 'Hello, World, 42, true, Expensive', - pid: process.pid, - time: expect.any(Number), - hostname: hostname(), - }); - }); - it('child', async () => { - const logger = pino( - { - level, - }, - stream, - ); - const loggerAdapter = createLoggerFromPino(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child'); - childLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log); - expect(logJson).toEqual({ - level: pino.levels.values[level], - foo: 'bar', - msg: 'Hello, World, 42, true, Expensive', - name: 'child', - pid: process.pid, - time: expect.any(Number), - hostname: hostname(), - }); - }); - it('deduplicate names', async () => { - const logger = pino( - { - level, - }, - stream, - ); - const loggerAdapter = createLoggerFromPino(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child').child('child'); - childLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log); - expect(logJson).toEqual({ - level: pino.levels.values[level], - foo: 'bar', - msg: 'Hello, World, 42, true, Expensive', - name: 'child', - pid: process.pid, - time: expect.any(Number), - hostname: hostname(), - }); - }); - it('nested', async () => { - const logger = pino( - { - level, - }, - stream, - ); - const loggerAdapter = createLoggerFromPino(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child'); - const nestedLogger = childLogger.child('nested'); - nestedLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log); - expect(logJson).toEqual({ - level: pino.levels.values[level], - foo: 'bar', - msg: 'Hello, World, 42, true, Expensive', - name: 'child, nested', - pid: process.pid, - time: expect.any(Number), - hostname: hostname(), - }); - }); - }); - } -}); diff --git a/packages/logger-winston/CHANGELOG.md b/packages/logger-winston/CHANGELOG.md deleted file mode 100644 index 49a88f432..000000000 --- a/packages/logger-winston/CHANGELOG.md +++ /dev/null @@ -1,58 +0,0 @@ -# @graphql-hive/logger-winston - -## 1.0.4 - -### Patch Changes - -- [#1383](https://github.com/graphql-hive/gateway/pull/1383) [`a832e7b`](https://github.com/graphql-hive/gateway/commit/a832e7bf9a8f92c48fb9df8ca1bff5a008dcf420) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.7` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.7) (from `^0.104.5`, in `dependencies`) - -## 1.0.3 - -### Patch Changes - -- [#1258](https://github.com/graphql-hive/gateway/pull/1258) [`3d24beb`](https://github.com/graphql-hive/gateway/commit/3d24beb7b15fd8109f86bbb3dfd514f6b8202741) Thanks [@dependabot](https://github.com/apps/dependabot)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.5` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.5) (from `^0.104.0`, in `dependencies`) - -## 1.0.2 - -### Patch Changes - -- [#727](https://github.com/graphql-hive/gateway/pull/727) [`c54a080`](https://github.com/graphql-hive/gateway/commit/c54a080b8b9c477ed55dd7c23fc8fcae9139bec8) Thanks [@renovate](https://github.com/apps/renovate)! - dependencies updates: - - Updated dependency [`@whatwg-node/disposablestack@^0.0.6` ↗︎](https://www.npmjs.com/package/@whatwg-node/disposablestack/v/0.0.6) (from `^0.0.5`, in `dependencies`) - -- [#775](https://github.com/graphql-hive/gateway/pull/775) [`33f7dfd`](https://github.com/graphql-hive/gateway/commit/33f7dfdb10eef2a1e7f6dffe0ce6e4bb3cc7c2c6) Thanks [@renovate](https://github.com/apps/renovate)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.104.0` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.104.0) (from `^0.103.18`, in `dependencies`) - -## 1.0.1 - -### Patch Changes - -- [#696](https://github.com/graphql-hive/gateway/pull/696) [`a289faa`](https://github.com/graphql-hive/gateway/commit/a289faae1469eb46f1458be341d21909fe5f8f8f) Thanks [@ardatan](https://github.com/ardatan)! - dependencies updates: - - Updated dependency [`@graphql-mesh/types@^0.103.18` ↗︎](https://www.npmjs.com/package/@graphql-mesh/types/v/0.103.18) (from `^0.103.6`, in `dependencies`) - -## 1.0.0 - -### Major Changes - -- [#622](https://github.com/graphql-hive/gateway/pull/622) [`16f9bd9`](https://github.com/graphql-hive/gateway/commit/16f9bd981d5779c585c00bf79e790c94b00326f1) Thanks [@ardatan](https://github.com/ardatan)! - **Winston Adapter** - - Now you can integrate [Winston](https://github.com/winstonjs/winston) into Hive Gateway on Node.js - - ```ts - import { defineConfig } from '@graphql-hive/gateway'; - import { createLoggerFromWinston } from '@graphql-hive/winston'; - import { createLogger, format, transports } from 'winston'; - - // Create a Winston logger - const winstonLogger = createLogger({ - level: 'info', - format: format.combine(format.timestamp(), format.json()), - transports: [new transports.Console()], - }); - - export const gatewayConfig = defineConfig({ - // Create an adapter for Winston - logging: createLoggerFromWinston(winstonLogger), - }); - ``` diff --git a/packages/logger-winston/package.json b/packages/logger-winston/package.json deleted file mode 100644 index 40fb510cf..000000000 --- a/packages/logger-winston/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@graphql-hive/logger-winston", - "version": "1.0.4", - "type": "module", - "repository": { - "type": "git", - "url": "git+https://github.com/graphql-hive/gateway.git", - "directory": "packages/logger-winston" - }, - "homepage": "https://the-guild.dev/graphql/hive/docs/gateway", - "author": { - "email": "contact@the-guild.dev", - "name": "The Guild", - "url": "https://the-guild.dev" - }, - "license": "MIT", - "engines": { - "node": ">=18.0.0" - }, - "main": "./dist/index.js", - "types": "./dist/index.d.ts", - "exports": { - ".": { - "require": { - "types": "./dist/index.d.cts", - "default": "./dist/index.cjs" - }, - "import": { - "types": "./dist/index.d.ts", - "default": "./dist/index.js" - } - }, - "./package.json": "./package.json" - }, - "files": [ - "dist" - ], - "scripts": { - "build": "pkgroll --clean-dist", - "prepack": "yarn build" - }, - "peerDependencies": { - "graphql": "^15.9.0 || ^16.9.0", - "winston": "^3.17.0" - }, - "dependencies": { - "@graphql-mesh/types": "^0.104.7", - "@whatwg-node/disposablestack": "^0.0.6", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "16.11.0", - "pkgroll": "2.15.0", - "winston": "^3.17.0" - }, - "sideEffects": false -} diff --git a/packages/logger-winston/src/index.ts b/packages/logger-winston/src/index.ts deleted file mode 100644 index d050b60f0..000000000 --- a/packages/logger-winston/src/index.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { - LazyLoggerMessage, - Logger as MeshLogger, -} from '@graphql-mesh/types'; -import { DisposableSymbols } from '@whatwg-node/disposablestack'; -import type { Logger as WinstonLogger } from 'winston'; - -function prepareArgs(messageArgs: LazyLoggerMessage[]) { - const flattenedMessageArgs = messageArgs - .flat(Infinity) - .flatMap((messageArg) => { - if (typeof messageArg === 'function') { - messageArg = messageArg(); - } - if (messageArg?.toJSON) { - messageArg = messageArg.toJSON(); - } - if (messageArg instanceof AggregateError) { - return messageArg.errors; - } - return messageArg; - }); - let message: string = ''; - const extras: any[] = []; - for (let messageArg of flattenedMessageArgs) { - if (messageArg == null) { - continue; - } - const typeofMessageArg = typeof messageArg; - if ( - typeofMessageArg === 'string' || - typeofMessageArg === 'number' || - typeofMessageArg === 'boolean' - ) { - message = message ? message + ', ' + messageArg : messageArg; - } else if (typeofMessageArg === 'object') { - extras.push(messageArg); - } - } - return [message, ...extras] as const; -} - -class WinstonLoggerAdapter implements MeshLogger, Disposable { - public name?: string; - constructor( - private winstonLogger: WinstonLogger, - private meta: Record = {}, - ) { - if (meta['name']) { - this.name = meta['name']; - } - } - log(...args: any[]) { - if (this.winstonLogger.isInfoEnabled()) { - this.winstonLogger.info(...prepareArgs(args)); - } - } - info(...args: any[]) { - if (this.winstonLogger.isInfoEnabled()) { - this.winstonLogger.info(...prepareArgs(args)); - } - } - warn(...args: any[]) { - if (this.winstonLogger.isWarnEnabled()) { - this.winstonLogger.warn(...prepareArgs(args)); - } - } - error(...args: any[]) { - if (this.winstonLogger.isErrorEnabled()) { - this.winstonLogger.error(...prepareArgs(args)); - } - } - debug(...lazyArgs: LazyLoggerMessage[]) { - if (this.winstonLogger.isDebugEnabled()) { - this.winstonLogger.debug(...prepareArgs(lazyArgs)); - } - } - child(nameOrMeta: string | Record) { - if (typeof nameOrMeta === 'string') { - nameOrMeta = { - name: this.name - ? this.name.includes(nameOrMeta) - ? this.name - : `${this.name}, ${nameOrMeta}` - : nameOrMeta, - }; - } - return new WinstonLoggerAdapter(this.winstonLogger.child(nameOrMeta), { - ...this.meta, - ...nameOrMeta, - }); - } - [DisposableSymbols.dispose]() { - return this.winstonLogger.close(); - } -} - -export function createLoggerFromWinston( - winstonLogger: WinstonLogger, -): WinstonLoggerAdapter { - return new WinstonLoggerAdapter(winstonLogger); -} diff --git a/packages/logger-winston/tests/winston.spec.ts b/packages/logger-winston/tests/winston.spec.ts deleted file mode 100644 index 9b9034dad..000000000 --- a/packages/logger-winston/tests/winston.spec.ts +++ /dev/null @@ -1,147 +0,0 @@ -import { Writable } from 'node:stream'; -import { describe, expect, it } from 'vitest'; -import * as winston from 'winston'; -import { createLoggerFromWinston } from '../src'; - -describe('Winston', () => { - let log = ''; - let lastCallback = () => {}; - const stream = new Writable({ - write(chunk, _encoding, callback) { - log = chunk.toString('utf-8'); - lastCallback = callback; - }, - }); - const logLevels = ['error', 'warn', 'info', 'debug'] as const; - for (const level of logLevels) { - describe(`Level: ${level}`, () => { - it('basic', () => { - const logger = winston.createLogger({ - level, - format: winston.format.json(), - transports: [ - new winston.transports.Stream({ - stream, - }), - ], - }); - using loggerAdapter = createLoggerFromWinston(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - loggerAdapter[level](...testData); - lastCallback(); - const logJson = JSON.parse(log, (_key, value) => value); - expect(logJson).toEqual({ - level, - foo: 'bar', - message: 'Hello, World, 42, true, Expensive', - }); - }); - it('child', () => { - const logger = winston.createLogger({ - level, - format: winston.format.json(), - transports: [ - new winston.transports.Stream({ - stream, - }), - ], - }); - const loggerAdapter = createLoggerFromWinston(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child'); - childLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log, (_key, value) => value); - expect(logJson).toEqual({ - level, - foo: 'bar', - message: 'Hello, World, 42, true, Expensive', - name: 'child', - }); - }); - it('deduplicate names', () => { - const logger = winston.createLogger({ - level, - format: winston.format.json(), - transports: [ - new winston.transports.Stream({ - stream, - }), - ], - }); - const loggerAdapter = createLoggerFromWinston(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child').child('child'); - childLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log, (_key, value) => value); - expect(logJson).toEqual({ - level, - foo: 'bar', - message: 'Hello, World, 42, true, Expensive', - name: 'child', - }); - }); - it('nested', () => { - const logger = winston.createLogger({ - level, - format: winston.format.json(), - transports: [ - new winston.transports.Stream({ - stream, - }), - ], - }); - const loggerAdapter = createLoggerFromWinston(logger); - const testData = [ - 'Hello', - ['World'], - { foo: 'bar' }, - 42, - true, - null, - undefined, - () => 'Expensive', - ]; - const childLogger = loggerAdapter.child('child'); - const nestedLogger = childLogger.child('nested'); - nestedLogger[level](...testData); - lastCallback(); - const logJson = JSON.parse(log, (_key, value) => value); - expect(logJson).toEqual({ - level, - foo: 'bar', - message: 'Hello, World, 42, true, Expensive', - name: 'child, nested', - }); - }); - }); - } -}); diff --git a/packages/logger/README.md b/packages/logger/README.md new file mode 100644 index 000000000..d94c8cb12 --- /dev/null +++ b/packages/logger/README.md @@ -0,0 +1,733 @@ +# Hive Logger + +Lightweight and customizable logging utility designed for use within the GraphQL Hive ecosystem. It provides structured logging capabilities, making it easier to debug and monitor applications effectively. + +## Compatibility + +The Hive Logger is designed to work seamlessly in all JavaScript environments, including Node.js, browsers, and serverless platforms. Its lightweight design ensures minimal overhead, making it suitable for a wide range of applications. + +# Getting Started + +## Install + +```sh +npm i @graphql-hive/logger +``` + +## Basic Usage + +Create a default logger that set to the `info` log level writing to the console. + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger(); + +log.debug('I wont be logged by default'); + +log.info({ some: 'attributes' }, 'Hello %s!', 'world'); + +const child = log.child({ requestId: '123-456' }); + +child.warn({ more: 'attributes' }, 'Oh hello child!'); + +const err = new Error('Woah!'); + +child.error({ err }, 'Something went wrong!'); +``` + +Will produce the following output to the console output: + + +```sh +2025-04-10T14:00:00.000Z INF Hello world! + some: "attributes" +2025-04-10T14:00:00.000Z WRN Oh hello child! + requestId: "123-456" + more: "attributes" +2025-04-10T14:00:00.000Z ERR Something went wrong! + requestId: "123-456" + err: { + stack: "Error: Woah! + at (/project/example.js:13:13) + at ModuleJob.run (node:internal/modules/esm/module_job:274:25) + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26) + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)" + message: "Woah!" + name: "Error" + class: "Error" + } +``` + + +or if you wish to have JSON output, set the `LOG_JSON` environment variable to a truthy value: + + +```sh +$ LOG_JSON=1 node example.js + +{"some":"attributes","level":"info","msg":"Hello world!","timestamp":"2025-04-10T14:00:00.000Z"} +{"requestId":"123-456","more":"attributes","level":"info","msg":"Hello child!","timestamp":"2025-04-10T14:00:00.000Z"} +{"requestId":"123-456","err":{"stack":"Error: Woah!\n at (/project/example.js:13:13)\n at ModuleJob.run (node:internal/modules/esm/module_job:274:25)\n at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26)\n at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)","message":"Woah!","name":"Error","class":"Error"},"level":"error","msg":"Something went wrong!","timestamp":"2025-04-10T14:00:00.000Z"} +``` + + +## Logging Methods and Their Arguments + +Hive Logger provides convenient methods for each log level: `trace`, `debug`, `info`, `warn`, and `error`. + +All logging methods support flexible argument patterns for structured and formatted logging: + +### No Arguments + +Logs an empty message at the specified level. + +```ts +log.debug(); +``` + +```sh +2025-04-10T14:00:00.000Z DBG +``` + +### Attributes Only + +Logs structured attributes without a message. + +```ts +log.info({ hello: 'world' }); +``` + + +```sh +2025-04-10T14:00:00.000Z INF + hello: "world" +``` + + +### Message with Interpolation + +Logs a formatted message, similar to printf-style formatting. Read more about it in the [Message Formatting section](#message-formatting). + +```ts +log.warn('Hello %s!', 'World'); +``` + + +```sh +2025-04-10T14:00:00.000Z WRN Hello World! +``` + + +### Attributes and Message (with interpolation) + +Logs structured attributes and a formatted message. The attributes can be anything object-like, including classes. + +```ts +const err = new Error('Something went wrong!'); +log.error(err, 'Problem occurred at %s', new Date()); +``` + + +```sh +2025-04-10T14:00:00.000Z ERR Problem occurred at Thu Apr 10 2025 14:00:00 GMT+0200 (Central European Summer Time) + stack: "Error: Something went wrong! + at (/projects/example.js:2:1)" + message: "Something went wrong!" + name: "Error" + class: "Error" +``` + + +## Message Formatting + +The Hive Logger uses the [`quick-format-unescaped` library](https://github.com/pinojs/quick-format-unescaped) to format log messages that include interpolation (e.g., placeholders like %s, %d, etc.). + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger(); + +log.info('hello %s %j %d %o', 'world', { obj: true }, 4, { another: 'obj' }); +``` + +Outputs: + +```sh +2025-04-10T14:00:00.000Z INF hello world {"obj":true} 4 {"another":"obj"} +``` + +Available interpolation placeholders are: + +- `%s` - string +- `%d` and `%f` - number with(out) decimals +- `%i` - integer number +- `%o`,`%O` and `%j` - JSON stringified object +- `%%` - escaped percentage sign + +## Logging Levels + +The default logger uses the `info` log level which will make sure to log only `info`+ logs. Available log levels are: + +- false (disables logging altogether) +- `trace` +- `debug` +- `info` _default_ +- `warn` +- `error` + +### Lazy Arguments and Performance + +Hive Logger supports "lazy" attributes for log methods. If you pass a function as the attributes argument, it will only be evaluated if the log level is enabled and the log will actually be written. This avoids unnecessary computation for expensive attributes when the log would be ignored due to the current log level. + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger({ level: 'info' }); + +log.debug( + // This function will NOT be called, since 'debug' is below the current log level. + () => ({ expensive: computeExpensiveValue() }), + 'This will not be logged', +); + +log.info( + // This function WILL be called, since 'info' log level is set. + () => ({ expensive: computeExpensiveValue() }), + 'This will be logged', +); +``` + +### Change Logging Level on Creation + +When creating an instance of the logger, you can configure the logging level by configuring the `level` option. Like this: + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger({ level: 'debug' }); + +log.trace( + // you can suply "lazy" attributes which wont be evaluated unless the log level allows logging + () => ({ + wont: 'be evaluated', + some: expensiveOperation(), + }), + 'Wont be logged and attributes wont be evaluated', +); + +log.debug('Hello world!'); + +const child = log.child('[prefix] '); + +child.debug('Child loggers inherit the parent log level'); +``` + +Outputs the following to the console: + + +```sh +2025-04-10T14:00:00.000Z DBG Hello world! +2025-04-10T14:00:00.000Z DBG [prefix] Child loggers inherit the parent log level +``` + + +### Change Logging Level Dynamically + +Alternatively, you can change the logging level dynamically during runtime. There's two possible ways of doing that. + +#### Using `log.setLevel(level: LogLevel)` + +One way of doing it is by using the log's `setLevel` method. + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger({ level: 'debug' }); + +log.debug('Hello world!'); + +const child = log.child('[prefix] '); + +child.debug('Child loggers inherit the parent log level'); + +log.setLevel('trace'); + +log.trace(() => ({ hi: 'there' }), 'Now tracing is logged too!'); + +child.trace('Also on the child logger'); + +child.setLevel('info'); + +log.trace('Still logging!'); + +child.debug('Wont be logged because the child has a different log level now'); + +child.info('Hello child!'); +``` + +Outputs the following to the console: + + +```sh +2025-04-10T14:00:00.000Z DBG Hello world! +2025-04-10T14:00:00.000Z DBG [prefix] Child loggers inherit the parent log level +2025-04-10T14:00:00.000Z TRC Now tracing is logged too! + hi: "there" +2025-04-10T14:00:00.000Z TRC [prefix] Also on the child logger +2025-04-10T14:00:00.000Z TRC Still logging! +2025-04-10T14:00:00.000Z INF Hello child! +``` + + +#### Using `LoggerOptions.level` Function + +Another way of doing it is to pass a function to the `level` option when creating a logger. + +```ts +import { Logger } from '@graphql-hive/logger'; + +let isDebug = false; + +const log = new Logger({ + level: () => { + if (isDebug) { + return 'debug'; + } + return 'info'; + }, +}); + +log.debug('isDebug is false, so this wont be logged'); + +log.info('Hello world!'); + +const child = log.child('[scoped] '); + +child.debug( + 'Child loggers inherit the parent log level function, so this wont be logged either', +); + +// enable debug mode +isDebug = true; + +child.debug('Now debug is enabled and logged'); +``` + +Outputs the following: + + +```sh +2025-04-10T14:00:00.000Z INF Hello world! +2025-04-10T14:00:00.000Z DBG [scoped] Now debug is enabled and logged +``` + + +## Child Loggers + +Child loggers in Hive Logger allow you to create new logger instances that inherit configuration (such as log level, writers, and attributes) from their parent logger. This is useful for associating contextual information (like request IDs or component names) with all logs from a specific part of your application. + +When you create a child logger using the child method, you can: + +- Add a prefix to all log messages from the child logger. +- Add attributes that will be included in every log entry from the child logger. +- Inherit the log level and writers from the parent logger, unless explicitly changed on the child. + +This makes it easy to organize and structure logs in complex applications, ensuring that related logs carry consistent context. + +> [!IMPORTANT] +> In a child logger, attributes provided in individual log calls will overwrite any attributes inherited from the parent logger if they share the same keys. This allows you to override or add context-specific attributes for each log entry. + +For example, running this: + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger(); + +const child = log.child({ requestId: '123-456' }, '[child] '); + +child.info('Hello World!'); +child.info({ requestId: 'overwritten attribute' }); + +const nestedChild = child.child({ traceId: '789-012' }, '[nestedChild] '); + +nestedChild.info('Hello Deep Down!'); +``` + +Will output: + + +```sh +2025-04-10T14:00:00.000Z INF [child] Hello World! + requestId: "123-456" +2025-04-10T14:00:00.000Z INF [child] + requestId: "overwritten attribute" +2025-04-20T18:39:30.291Z INF [child] [nestedChild] Hello Deep Down! + requestId: "123-456" + traceId: "789-012" +``` + + +## Writers + +Logger writers are responsible for handling how and where log messages are output. In Hive Logger, writers are pluggable components that receive structured log data and determine its final destination and format. This allows you to easily customize logging behavior, such as printing logs to the console, writing them as JSON, storing them in memory for testing, or sending them to external systems. + +By default, Hive Logger provides several built-in writers, but you can also implement your own to suit your application's needs. The built-ins are: + +### `MemoryLogWriter` + +Writes the logs to memory allowing you to access the logs. Mostly useful for testing. + +```ts +import { Logger, MemoryLogWriter } from '@graphql-hive/logger'; + +const writer = new MemoryLogWriter(); + +const log = new Logger({ writers: [writer] }); + +log.info({ my: 'attrs' }, 'Hello World!'); + +console.log(writer.logs); +``` + +Outputs: + +```sh +[ { level: 'info', msg: 'Hello World!', attrs: { my: 'attrs' } } ] +``` + +### `ConsoleLogWriter` (default) + +The default log writer used by the Hive Logger. It outputs log messages to the console in a human-friendly, colorized format, making it easy to distinguish log levels and read structured attributes. Each log entry includes a timestamp, the log level (with color), the message, and any additional attributes (with colored keys), which are pretty-printed and formatted for clarity. + +The writer works in both Node.js and browser-like environments, automatically disabling colors if not supported. This makes `ConsoleLogWriter` ideal for all cases, providing clear and readable logs out of the box. + +```ts +import { ConsoleLogWriter, Logger } from '@graphql-hive/logger'; + +const writer = new ConsoleLogWriter({ + noColor: true, // defaults to env.NO_COLOR. read more: https://no-color.org/ + noTimestamp: true, +}); + +const log = new Logger({ writers: [writer] }); + +log.info({ my: 'attrs' }, 'Hello World!'); +``` + +Outputs: + + +```sh +INF Hello World! + my: "attrs" +``` + + +### `JSONLogWriter` + +> [!NOTE] +> Will be used then the `LOG_JSON=1` environment variable is provided. + +Built-in log writer that outputs each log entry as a structured JSON object. When used, it prints logs to the console in JSON format, including all provided attributes, the log level, message, and a timestamp. + +In the JSONLogWriter implementation, any attributes you provide with the keys `msg`, `timestamp`, or `level` will be overwritten in the final log output. This is because the writer explicitly sets these fields when constructing the log object. If you include these keys in your attributes, their values will be replaced by the logger's own values in the JSON output. + +If the `LOG_JSON_PRETTY=1` environment variable is provided, the output will be pretty-printed for readability; otherwise, it is compact. + +This writer's format is ideal for machine parsing, log aggregation, or integrating with external logging systems, especially useful for production environments or when logs need to be consumed by other tools. + +```ts +import { JSONLogWriter, Logger } from '@graphql-hive/logger'; + +const log = new Logger({ writers: [new JSONLogWriter()] }); + +log.info({ my: 'attrs' }, 'Hello World!'); +``` + +Outputs: + + +```sh +{"my":"attrs","level":"info","msg":"Hello World!","timestamp":"2025-04-10T14:00:00.000Z"} +``` + + +Or pretty printed: + + +```sh +$ LOG_JSON_PRETTY=1 node example.js + +{ + "my": "attrs", + "level": "info", + "msg": "Hello World!", + "timestamp": "2025-04-10T14:00:00.000Z" +} +``` + + +### Optional Writers + +Hive Logger includes some writers for common loggers of the JavaScript ecosystem with optional peer dependencies. + +#### `PinoLogWriter` + +Use the [Node.js `pino` logger library](https://github.com/pinojs/pino) for writing Hive Logger's logs. + +`pino` is an optional peer dependency, so you must install it first. + +```sh +npm i pino pino-pretty +``` + +```ts +import { Logger } from '@graphql-hive/logger'; +import { PinoLogWriter } from '@graphql-hive/logger/writers/pino'; +import pino from 'pino'; + +const pinoLogger = pino({ + transport: { + target: 'pino-pretty', + }, +}); + +const log = new Logger({ writers: [new PinoLogWriter(pinoLogger)] }); + +log.info({ some: 'attributes' }, 'hello world'); +``` + + +```sh +[14:00:00.000] INFO (20744): hello world + some: "attributes" +``` + + +#### `WinstonLogWriter` + +Use the [Node.js `winston` logger library](https://github.com/winstonjs/winston) for writing Hive Logger's logs. + +`winston` is an optional peer dependency, so you must install it first. + +```sh +npm i winston +``` + +```ts +import { Logger } from '@graphql-hive/logger'; +import { WinstonLogWriter } from '@graphql-hive/logger/writers/winston'; +import winston from 'winston'; + +const winstonLogger = winston.createLogger({ + transports: [new winston.transports.Console()], +}); + +const log = new Logger({ writers: [new WinstonLogWriter(winstonLogger)] }); + +log.info({ some: 'attributes' }, 'hello world'); +``` + +```sh +{"level":"info","message":"hello world","some":"attributes"} +``` + +> [!IMPORTANT] +> Winston logger does not have a "trace" log level. Hive Logger will instead use "verbose" when writing logs to Winston. + +### Custom Writers + +You can implement custom log writers for the Hive Logger by creating a class that implements the `LogWriter` interface. This interface requires a single `write` method, which receives the log level, attributes, and message. + +Your writer can perform any action, such as sending logs to a file, external service, or custom destination. + +Writers can be synchronous (returning `void`) or asynchronous (returning a `Promise`). If your writer performs asynchronous operations (like network requests or file writes), simply return a promise from the `write` method. + +```ts +import { + Attributes, + ConsoleLogWriter, + Logger, + LogLevel, + LogWriter, +} from '@graphql-hive/logger'; + +class HTTPLogWriter implements LogWriter { + async write(level: LogLevel, attrs: Attributes, msg: string) { + await fetch('https://my-log-service.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ level, attrs, msg }), + }); + } +} + +const log = new Logger({ + // send logs both to the HTTP loggging service and output them to the console + writers: [new HTTPLogWriter(), new ConsoleLogWriter()], +}); + +log.info('Hello World!'); + +await log.flush(); // make sure all async writes settle +``` + +#### Flushing and Non-Blocking Logging + +The logger does not block when you log asynchronously. Instead, it tracks all pending async writes internally. When you call `log.flush()` it waits for all pending writes to finish, ensuring no logs are lost on shutdown. During normal operation, logging remains fast and non-blocking, even if some writers are async. + +This design allows you to use async writers without impacting the performance of your application or blocking the main thread. + +##### Explicit Resource Management + +The Hive Logger also supports [Explicit Resource Management](https://github.com/tc39/proposal-explicit-resource-management). This allows you to ensure that all pending asynchronous log writes are properly flushed before your application exits or when the logger is no longer needed. + +You can use the logger with `await using` (in environments that support it) to wait for all log operations to complete. This is especially useful in serverless or short-lived environments where you want to guarantee that no logs are lost due to unfinished asynchronous operations. + +```ts +import { + Attributes, + ConsoleLogWriter, + Logger, + LogLevel, + LogWriter, +} from '@graphql-hive/logger'; + +class HTTPLogWriter implements LogWriter { + async write(level: LogLevel, attrs: Attributes, msg: string) { + await fetch('https://my-log-service.com', { + method: 'POST', + headers: { 'content-type': 'application/json' }, + body: JSON.stringify({ level, attrs, msg }), + }); + } +} + +{ + await using log = new Logger({ + // send logs both to the HTTP loggging service and output them to the console + writers: [new HTTPLogWriter(), new ConsoleLogWriter()], + }); + + log.info('Hello World!'); +} + +// logger went out of scope and all of the logs have been flushed +``` + +##### Handling Async Write Errors + +The Logger handles write errors for asynchronous writers by tracking all write promises. When `await log.flush()` is called (including during async disposal), it waits for all pending writes to settle. If any writes fail (i.e., their promises reject), their errors are collected and after all writes have settled, if there were any errors, an `AggregateError` is thrown containing all the individual write errors. + +```ts +import { Logger } from './Logger'; + +let i = 0; +const log = new Logger({ + writers: [ + { + async write() { + i++; + throw new Error('Write failed! #' + i); + }, + }, + ], +}); + +// no fail during logs +log.info('hello'); +log.info('world'); + +try { + await log.flush(); +} catch (e) { + // flush will fail with each individually failed writes + console.error(e); +} +``` + +Outputs: + +```sh +AggregateError: Failed to flush 2 writes + at async (/project/example.js:20:3) { + [errors]: [ + Error: Write failed! #1 + at Object.write (/project/example.js:9:15), + Error: Write failed! #2 + at Object.write (/project/example.js:9:15) + ] +} +``` + +## Advanced Serialization of Attributes + +Hive Logger uses advanced serialization to ensure that all attributes are logged safely and readably, even when they contain complex or circular data structures. This means you can log rich, nested objects or errors as attributes without worrying about serialization failures or unreadable logs. + +For example, the logger will serialize the error object, including its message and stack, in a safe and readable way. This advanced serialization is applied automatically to all attributes passed to log methods, child loggers, and writers. + +```ts +import { Logger } from '@graphql-hive/logger'; + +const log = new Logger(); + +class DatabaseError extends Error { + constructor(message: string) { + super(message); + this.name = 'DatabaseError'; + } +} +const dbErr = new DatabaseError('Connection failed'); +const userErr = new Error('Updating user failed', { cause: dbErr }); +const errs = new AggregateError([dbErr, userErr], 'Failed to update user'); + +log.error(errs); +``` + + +```sh +2025-04-10T14:00:00.000Z ERR + stack: "AggregateError: Failed to update user + at (/project/example.js:13:14) + at ModuleJob.run (node:internal/modules/esm/module_job:274:25) + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26) + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)" + message: "Failed to update user" + errors: [ + { + stack: "DatabaseError: Connection failed + at (/project/example.js:11:15) + at ModuleJob.run (node:internal/modules/esm/module_job:274:25) + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26) + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)" + message: "Database connection failed" + name: "DatabaseError" + class: "DatabaseError" + } + { + stack: "Error: Updating user failed + at (/project/example.js:12:17) + at ModuleJob.run (node:internal/modules/esm/module_job:274:25) + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26) + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)" + message: "Updating user failed" + cause: { + stack: "DatabaseError: Connection failed + at (/project/example.js:11:15) + at ModuleJob.run (node:internal/modules/esm/module_job:274:25) + at async onImport.tracePromise.__proto__ (node:internal/modules/esm/loader:644:26) + at async asyncRunEntryPointWithESMLoader (node:internal/modules/run_main:98:5)" + message: "Database connection failed" + name: "DatabaseError" + class: "DatabaseError" + } + name: "Error" + class: "Error" + } + ] + name: "AggregateError" + class: "AggregateError" +``` + diff --git a/packages/logger/package.json b/packages/logger/package.json new file mode 100644 index 000000000..e5bc51db8 --- /dev/null +++ b/packages/logger/package.json @@ -0,0 +1,119 @@ +{ + "name": "@graphql-hive/logger", + "version": "1.0.0", + "type": "module", + "repository": { + "type": "git", + "url": "git+https://github.com/graphql-hive/gateway.git", + "directory": "packages/logger" + }, + "author": { + "email": "contact@the-guild.dev", + "name": "The Guild", + "url": "https://the-guild.dev" + }, + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "exports": { + ".": { + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + }, + "import": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "./request": { + "require": { + "types": "./dist/request.d.cts", + "default": "./dist/request.cjs" + }, + "import": { + "types": "./dist/request.d.ts", + "default": "./dist/request.js" + } + }, + "./writers/json": { + "require": { + "types": "./dist/writers/json.d.cts", + "default": "./dist/writers/json.cjs" + }, + "import": { + "types": "./dist/writers/json.d.ts", + "default": "./dist/writers/json.js" + } + }, + "./writers/logtape": { + "require": { + "types": "./dist/writers/logtape.d.cts", + "default": "./dist/writers/logtape.cjs" + }, + "import": { + "types": "./dist/writers/logtape.d.ts", + "default": "./dist/writers/logtape.js" + } + }, + "./writers/pino": { + "require": { + "types": "./dist/writers/pino.d.cts", + "default": "./dist/writers/pino.cjs" + }, + "import": { + "types": "./dist/writers/pino.d.ts", + "default": "./dist/writers/pino.js" + } + }, + "./writers/winston": { + "require": { + "types": "./dist/writers/winston.d.cts", + "default": "./dist/writers/winston.cjs" + }, + "import": { + "types": "./dist/writers/winston.d.ts", + "default": "./dist/writers/winston.js" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist" + ], + "scripts": { + "build": "pkgroll --clean-dist", + "prepack": "yarn build" + }, + "peerDependencies": { + "@logtape/logtape": "^1.0.0", + "pino": "^9.6.0" + }, + "peerDependenciesMeta": { + "@logtape/logtape": { + "optional": true + }, + "pino": { + "optional": true + }, + "winston": { + "optional": true + } + }, + "devDependencies": { + "@logtape/logtape": "^1.0.0", + "@types/quick-format-unescaped": "^4.0.3", + "@whatwg-node/disposablestack": "^0.0.6", + "@whatwg-node/promise-helpers": "^1.3.2", + "fast-safe-stringify": "^2.1.1", + "pino": "^9.6.0", + "pkgroll": "2.11.2", + "quick-format-unescaped": "^4.0.4", + "winston": "^3.17.0" + }, + "sideEffects": false, + "dependencies.info": "all of the dependencies are in devDependencies which will bundle them into the package using pkgroll making pkgroll ultimately zero-dep with a smaller footprint because of tree-shaking" +} diff --git a/packages/logger/src/index.ts b/packages/logger/src/index.ts new file mode 100644 index 000000000..352b76440 --- /dev/null +++ b/packages/logger/src/index.ts @@ -0,0 +1,4 @@ +export * from './logger'; +export * from './writers'; +/** @deprecated Please migrate to using the './Logger' instead. */ +export * from './legacyLogger'; diff --git a/packages/logger/src/legacyLogger.ts b/packages/logger/src/legacyLogger.ts new file mode 100644 index 000000000..ed25e8948 --- /dev/null +++ b/packages/logger/src/legacyLogger.ts @@ -0,0 +1,98 @@ +import { Logger, LogLevel } from './logger'; +import { shouldLog } from './utils'; + +// type comes from "@graphql-mesh/types" package, we're copying them over just to avoid including the whole package +export type LazyLoggerMessage = (() => any | any[]) | any; + +/** @deprecated Please migrate to using the {@link Logger} instead.*/ +export class LegacyLogger { + #logger: Logger; + + constructor(logger: Logger) { + this.#logger = logger; + } + + static from(logger: Logger): LegacyLogger { + return new LegacyLogger(logger); + } + + #log(level: LogLevel, ...[maybeMsgOrArg, ...restArgs]: any[]) { + if (typeof maybeMsgOrArg === 'string') { + if (restArgs.length) { + this.#logger.log(level, restArgs, maybeMsgOrArg); + } else { + this.#logger.log(level, maybeMsgOrArg); + } + } else { + if (restArgs.length) { + this.#logger.log(level, [maybeMsgOrArg, ...restArgs]); + } else { + this.#logger.log(level, maybeMsgOrArg); + } + } + } + + log(...args: any[]) { + this.#log('info', ...args); + } + + warn(...args: any[]) { + this.#log('warn', ...args); + } + + info(...args: any[]) { + this.#log('info', ...args); + } + + error(...args: any[]) { + this.#log('error', ...args); + } + + debug(...lazyArgs: LazyLoggerMessage[]) { + if (!shouldLog(this.#logger.level, 'debug')) { + // we only return early here because only debug can have lazy logs + return; + } + this.#log('debug', ...handleLazyMessage(lazyArgs)); + } + + child(name: string | Record): LegacyLogger { + name = + stringifyName(name) + + // append space if object is strigified to space out the prefix + (typeof name === 'object' ? ' ' : ''); + if (this.#logger.prefix === name) { + return this; + } + return LegacyLogger.from(this.#logger.child(name)); + } + + addPrefix(prefix: string | Record): LegacyLogger { + prefix = stringifyName(prefix); + if (this.#logger.prefix?.includes(prefix)) { + // TODO: why do we do this? + return this; + } + return LegacyLogger.from(this.#logger.child(prefix)); + } +} + +function stringifyName(name: string | Record) { + if (typeof name === 'string' || typeof name === 'number') { + return `${name}`; + } + const names: string[] = []; + for (const [key, value] of Object.entries(name)) { + names.push(`${key}=${value}`); + } + return `${names.join(', ')}`; +} + +function handleLazyMessage(lazyArgs: LazyLoggerMessage[]) { + return lazyArgs.flat(Infinity).flatMap((arg) => { + if (typeof arg === 'function') { + return arg(); + } + return arg; + }); +} diff --git a/packages/logger/src/logger.ts b/packages/logger/src/logger.ts new file mode 100644 index 000000000..698a8fbc8 --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,307 @@ +import { DisposableSymbols } from '@whatwg-node/disposablestack'; +import { getEnvBool, getEnvStr } from '~internal/env'; +import fastSafeStringify from 'fast-safe-stringify'; +import format from 'quick-format-unescaped'; +import { + Attributes, + isPromise, + logLevel, + MaybeLazy, + parseAttrs, + shallowMergeAttributes, + shouldLog, +} from './utils'; +import { ConsoleLogWriter, JSONLogWriter, LogWriter } from './writers'; + +export type { AttributeValue, MaybeLazy } from './utils'; +export type { Attributes }; + +export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error'; + +export interface LoggerOptions { + /** + * The minimum log level to log. + * + * Providing `false` will disable all logging. + * + * Provided function will always be invoked to get the current log level. + * + * @default env.LOG_LEVEL || env.DEBUG ? 'debug' : 'info' + */ + level?: MaybeLazy; + /** A prefix to include in every log's message. */ + prefix?: string; + /** + * The attributes to include in all logs. Is mainly used to pass the parent + * attributes when creating {@link Logger.child child loggers}. + */ + attrs?: Attributes; + /** + * The log writers to use when writing logs. + * + * @default env.LOG_JSON ? [new JSONLogWriter()] : [new ConsoleLogWriter()] + */ + writers?: [LogWriter, ...LogWriter[]]; +} + +export class Logger implements AsyncDisposable { + #level: MaybeLazy; + #prefix: string | undefined; + #attrs: Attributes | undefined; + #writers: [LogWriter, ...LogWriter[]]; + #pendingWrites?: Set>; + + constructor(opts: LoggerOptions = {}) { + let logLevelEnv = getEnvStr('LOG_LEVEL'); + if (logLevelEnv && !(logLevelEnv in logLevel)) { + throw new Error( + `Invalid LOG_LEVEL environment variable "${logLevelEnv}". Must be one of: ${[...Object.keys(logLevel), 'false'].join(', ')}`, + ); + } + this.#level = + opts.level ?? + (logLevelEnv as LogLevel) ?? + (getEnvBool('DEBUG') ? 'debug' : 'info'); + this.#prefix = opts.prefix; + this.#attrs = opts.attrs; + this.#writers = + opts.writers ?? + (getEnvBool('LOG_JSON') + ? [new JSONLogWriter()] + : [new ConsoleLogWriter()]); + } + + /** The prefix that's prepended to each log message. */ + public get prefix() { + return this.#prefix; + } + + /** + * The attributes that are added to each log. If the log itself contains + * attributes with keys existing in {@link attrs}, the log's attributes will + * override. + */ + public get attrs() { + return this.#attrs; + } + + /** The current {@link LogLevel} of the logger. You can change the level using the {@link setLevel} method. */ + public get level() { + return typeof this.#level === 'function' ? this.#level() : this.#level; + } + + /** + * Sets the new {@link LogLevel} of the logger. All subsequent logs, and {@link child child loggers} whose + * level did not change, will respect the new level. + */ + public setLevel(level: MaybeLazy) { + this.#level = level; + } + + public write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + for (const w of this.#writers) { + const write$ = w.write(level, attrs, msg); + if (isPromise(write$)) { + this.#pendingWrites ??= new Set(); + this.#pendingWrites.add(write$); + write$ + .then(() => { + // we remove from pending writes only if the write was successful + this.#pendingWrites!.delete(write$); + }) + .catch((e) => { + // otherwise we keep in the pending write to throw on flush + console.error('Failed to write async log', e); + }); + } + } + } + + public flush() { + const writerFlushes = this.#writers.map((w) => w.flush).filter((f) => !!f); + if (this.#pendingWrites?.size || writerFlushes.length) { + const errs: unknown[] = []; + return Promise.allSettled([ + ...Array.from(this.#pendingWrites || []).map((w) => + w.catch((err) => errs.push(err)), + ), + ...Array.from(writerFlushes || []).map(async (f) => { + try { + await f(); + } catch (err) { + errs.push(err); + } + }), + ]).then(() => { + this.#pendingWrites?.clear(); + if (errs.length === 1) { + throw new Error('Failed to flush', { cause: errs[0] }); + } else if (errs.length) { + throw new AggregateError( + errs, + `Failed to flush with ${errs.length} errors`, + ); + } + }); + } + return; + } + + async [DisposableSymbols.asyncDispose]() { + return this.flush(); + } + + // + + public child(prefix: string): Logger; + public child(attrs: Attributes, prefix?: string): Logger; + public child(prefixOrAttrs: string | Attributes, prefix?: string): Logger { + if (typeof prefixOrAttrs === 'string') { + return new Logger({ + level: () => this.level, // inherits the parent level (yet can be changed on child only when using setLevel) + prefix: (this.#prefix || '') + prefixOrAttrs, + attrs: this.#attrs, + writers: this.#writers, + }); + } + return new Logger({ + level: () => this.level, // inherits the parent level (yet can be changed on child only when using setLevel) + prefix: (this.#prefix || '') + (prefix || '') || undefined, + attrs: shallowMergeAttributes(this.#attrs, prefixOrAttrs), + writers: this.#writers, + }); + } + + // + + public log(level: LogLevel): void; + public log(level: LogLevel, attrs: MaybeLazy): void; + public log(level: LogLevel, msg: string, ...interpol: unknown[]): void; + public log( + level: LogLevel, + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public log( + level: LogLevel, + maybeAttrsOrMsg?: MaybeLazy | string | null | undefined, + ...rest: unknown[] + ): void { + if (!shouldLog(this.#level, level)) { + return; + } + + let msg: string | undefined; + let attrs: MaybeLazy | undefined; + if (typeof maybeAttrsOrMsg === 'string') { + msg = maybeAttrsOrMsg; + } else if (maybeAttrsOrMsg) { + attrs = maybeAttrsOrMsg; + if (typeof rest[0] === 'string') { + // we shift because the "rest" becomes "interpol" + msg = rest.shift() as string; + } + } + + if (this.#prefix) { + msg = `${this.#prefix}${msg || ''}`.trim(); // we trim everything because maybe the "msg" is empty + } + + attrs = shallowMergeAttributes(parseAttrs(this.#attrs), parseAttrs(attrs)); + + msg = + msg && rest.length + ? format(msg, rest, { stringify: fastSafeStringify }) + : msg; + + this.write(level, attrs, msg); + if (getEnvBool('LOG_TRACE_LOGS')) { + console.trace('👆'); + } + } + + public trace(): void; + public trace(attrs: MaybeLazy): void; + public trace(msg: string, ...interpol: unknown[]): void; + public trace( + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public trace(...args: any): void { + this.log( + 'trace', + // @ts-expect-error + ...args, + ); + } + + public debug(): void; + public debug(attrs: MaybeLazy): void; + public debug(msg: string, ...interpol: unknown[]): void; + public debug( + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public debug(...args: any): void { + this.log( + 'debug', + // @ts-expect-error + ...args, + ); + } + + public info(): void; + public info(attrs: MaybeLazy): void; + public info(msg: string, ...interpol: unknown[]): void; + public info( + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public info(...args: any): void { + this.log( + 'info', + // @ts-expect-error + ...args, + ); + } + + public warn(): void; + public warn(attrs: MaybeLazy): void; + public warn(msg: string, ...interpol: unknown[]): void; + public warn( + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public warn(...args: any): void { + this.log( + 'warn', + // @ts-expect-error + ...args, + ); + } + + public error(): void; + public error(attrs: MaybeLazy): void; + public error(msg: string, ...interpol: unknown[]): void; + public error( + attrs: MaybeLazy, + msg: string, + ...interpol: unknown[] + ): void; + public error(...args: any): void { + this.log( + 'error', + // @ts-expect-error + ...args, + ); + } +} diff --git a/packages/logger/src/request.ts b/packages/logger/src/request.ts new file mode 100644 index 000000000..43239bf70 --- /dev/null +++ b/packages/logger/src/request.ts @@ -0,0 +1,20 @@ +import { Logger } from './logger'; + +export const requestIdByRequest = new WeakMap(); + +const loggerByRequest = new WeakMap(); + +/** + * Gets the {@link Logger} of for the {@link request}. + * + * If the request does not have a logger, the provided {@link log} + * will be associated to the {@link request} and returned. + */ +export function loggerForRequest(log: Logger, request: Request): Logger { + const reqLog = loggerByRequest.get(request); + if (reqLog) { + return reqLog; + } + loggerByRequest.set(request, log); + return log; +} diff --git a/packages/logger/src/utils.ts b/packages/logger/src/utils.ts new file mode 100644 index 000000000..c80c2d24e --- /dev/null +++ b/packages/logger/src/utils.ts @@ -0,0 +1,211 @@ +import { LogLevel } from './logger'; + +export type MaybeLazy = T | (() => T); + +export type AttributeValue = any; + +export type Attributes = + | AttributeValue[] + | { [key: string | number]: AttributeValue }; + +export const logLevel: { [level in LogLevel]: number } = { + trace: 0, + debug: 1, + info: 2, + warn: 3, + error: 4, +}; + +export function shouldLog( + setLevel: MaybeLazy, + loggingLevel: LogLevel, +): boolean { + setLevel = typeof setLevel === 'function' ? setLevel() : setLevel; + return ( + setLevel !== false && // logging is not disabled + logLevel[setLevel] <= logLevel[loggingLevel] // and set log level is less than or equal to logging level + ); +} + +export function logLevelToString(level: LogLevel): string { + switch (level) { + case 'trace': + return 'TRC'; + case 'debug': + return 'DBG'; + case 'info': + return 'INF'; + case 'warn': + return 'WRN'; + case 'error': + return 'ERR'; + default: + throw new Error(`Unknown log level "${level}"`); + } +} + +export function isPromise(val: unknown): val is Promise { + const obj = Object(val); + return ( + typeof obj.then === 'function' && + typeof obj.catch === 'function' && + typeof obj.finally === 'function' + ); +} + +/** Recursivelly unwrapps the lazy attributes and parses instances of classes. */ +export function parseAttrs( + attrs: MaybeLazy | undefined, + functionUnwrapDepth = 0, +): Attributes | undefined { + if (functionUnwrapDepth > 3) { + throw new Error('Too much recursion while unwrapping function attributes'); + } + + if (!attrs) { + return undefined; + } + + if (typeof attrs === 'function') { + return parseAttrs(attrs(), functionUnwrapDepth + 1); + } + + if (Array.isArray(attrs)) { + return attrs.map((val) => unwrapAttrVal(val)); + } + + if (isPlainObject(attrs)) { + const unwrapped: Attributes = {}; + for (const key of Object.keys(attrs)) { + const val = attrs[key as keyof typeof attrs]; + unwrapped[key] = unwrapAttrVal(val); + } + return unwrapped; + } + + return objectifyClass(attrs); +} + +function unwrapAttrVal( + attr: AttributeValue, + visited = new WeakSet(), +): AttributeValue { + if (!attr) { + return attr; + } + + if (isPrimitive(attr)) { + return attr; + } + + if (typeof attr === 'function') { + return `[Function: ${attr.name || '(anonymous)'}]`; + } + + if (visited.has(attr)) { + return '[Circular]'; + } + visited.add(attr); + + if (Array.isArray(attr)) { + return attr.map((val) => unwrapAttrVal(val)); + } + + if (isPlainObject(attr)) { + const unwrapped: { [key: string | number]: AttributeValue } = {}; + for (const key of Object.keys(attr)) { + const val = attr[key as keyof typeof attr]; + unwrapped[key] = unwrapAttrVal(val, visited); + } + return unwrapped; + } + + // very likely an instance of something, dont unwrap it + return objectifyClass(attr); +} + +function isPrimitive(val: unknown): val is string | number | boolean { + return val !== Object(val); +} + +const nodejsCustomInspectSy = Symbol.for('nodejs.util.inspect.custom'); + +function objectifyClass(val: unknown): Record { + if ( + // simply empty + !val || + // Object.create(null) + Object(val).__proto__ == null + ) { + return {}; + } + if ( + typeof val === 'object' && + 'toJSON' in val && + typeof val.toJSON === 'function' + ) { + // if the object has a toJSON method, use it - always + return val.toJSON(); + } + if ( + typeof val === 'object' && + nodejsCustomInspectSy in val && + typeof val[nodejsCustomInspectSy] === 'function' + ) { + // > Custom [util.inspect.custom](depth, opts, inspect) functions typically return a string but may return a value of any type that will be formatted accordingly by util.inspect(). + return { + [nodejsCustomInspectSy.toString()]: unwrapAttrVal( + val[nodejsCustomInspectSy](Infinity, {}), + ), + class: val.constructor.name, + }; + } + const props: Record = {}; + for (const propName of Object.getOwnPropertyNames(val)) { + props[propName] = unwrapAttrVal(val[propName as keyof typeof val]); + } + for (const protoPropName of Object.getOwnPropertyNames( + Object.getPrototypeOf(val), + )) { + const propVal = val[protoPropName as keyof typeof val]; + if (typeof propVal === 'function') { + continue; + } + props[protoPropName] = unwrapAttrVal(propVal); + } + return { + ...props, + class: val.constructor.name, + }; +} + +export function shallowMergeAttributes( + target: Attributes | undefined, + source: Attributes | undefined, +): Attributes | undefined { + switch (true) { + case Array.isArray(source) && Array.isArray(target): + // both are arrays + return [...target, ...source]; + case Array.isArray(source): + // only "source" is an array + return target ? [target, ...source] : source; + case Array.isArray(target): + // only "target" is an array + return source ? [...target, source] : target; + case !!(target || source): + // neither are arrays, but at least one is an object + return { ...target, ...source }; + default: + // neither are provided + return undefined; + } +} + +/** Checks whether the value is a plan object and not an instance of any other class. */ +function isPlainObject(val: unknown): val is Object { + return ( + Object(val).constructor === Object && + Object.getPrototypeOf(val) === Object.prototype + ); +} diff --git a/packages/logger/src/writers/common.ts b/packages/logger/src/writers/common.ts new file mode 100644 index 000000000..81c93eea4 --- /dev/null +++ b/packages/logger/src/writers/common.ts @@ -0,0 +1,15 @@ +import fastSafeStringify from 'fast-safe-stringify'; +import { Attributes, LogLevel } from '../logger'; + +export function jsonStringify(val: unknown, pretty?: boolean): string { + return fastSafeStringify(val, undefined, pretty ? 2 : undefined); +} + +export interface LogWriter { + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void | Promise; + flush?(): void | Promise; +} diff --git a/packages/logger/src/writers/console.ts b/packages/logger/src/writers/console.ts new file mode 100644 index 000000000..35e9d8eca --- /dev/null +++ b/packages/logger/src/writers/console.ts @@ -0,0 +1,158 @@ +import { createDeferredPromise } from '@whatwg-node/promise-helpers'; +import { getEnvBool } from '~internal/env'; +import { LogLevel } from '../logger'; +import { Attributes, logLevelToString } from '../utils'; +import { jsonStringify, LogWriter } from './common'; + +const asciMap = { + timestamp: '\x1b[90m', // bright black + trace: '\x1b[36m', // cyan + debug: '\x1b[90m', // bright black + info: '\x1b[32m', // green + warn: '\x1b[33m', // yellow + error: '\x1b[41;39m', // red; white + message: '\x1b[1m', // bold + key: '\x1b[35m', // magenta + reset: '\x1b[0m', // reset +}; + +export interface ConsoleLogWriterOptions { + /** @default globalThis.Console */ + console?: Pick; + /** + * Whether to disable colors in the console output. + * + * @default env.NO_COLOR || false + */ + noColor?: boolean; + /** + * Whether to include the timestamp at the beginning of the log message. + * + * @default false + */ + noTimestamp?: boolean; + /** + * Asynchronously write the logs to the {@link console}. Will not block the main thread, + * but has potential to spam the event loop with log promises. Use with caution. + * + * Note that all of the logs will be written in the order they were called, only not immedietely. + * + * The logs are queued in a macrotask, so they will not block the main thread and will have lower + * priority than microtasks (promises will have priority). + * + * @default false + */ + async?: boolean; +} + +export class ConsoleLogWriter implements LogWriter { + #console: NonNullable; + #noColor: boolean; + #noTimestamp: boolean; + #async: boolean; + constructor(opts: ConsoleLogWriterOptions = {}) { + const { + console = globalThis.console, + // no color if we're running in browser-like (edge) environments + noColor = typeof process === 'undefined' || + // or no color if https://no-color.org/ + getEnvBool('NO_COLOR'), + noTimestamp = false, + async = false, + } = opts; + this.#console = console; + this.#noColor = noColor; + this.#noTimestamp = noTimestamp; + this.#async = async; + } + color( + style: keyof typeof asciMap, + text: T, + ): T { + if (!text) { + return text; + } + if (this.#noColor) { + return text; + } + return (asciMap[style] + text + asciMap.reset) as T; + } + #writeToConsole( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ) { + this.#console[level === 'trace' ? 'debug' : level]( + [ + !this.#noTimestamp && this.color('timestamp', new Date().toISOString()), + this.color(level, logLevelToString(level)), + this.color('message', msg), + attrs && this.stringifyAttrs(attrs), + ] + .filter(Boolean) + .join(' '), + ); + } + + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void | Promise { + if (this.#async) { + const { promise, resolve } = createDeferredPromise(); + setTimeout(() => { + // queue a macrotask to avoid blocking the main thread and promises + this.#writeToConsole(level, attrs, msg); + resolve(); + }, 0); + return promise; + } + this.#writeToConsole(level, attrs, msg); + } + stringifyAttrs(attrs: Attributes): string { + let log = '\n'; + + for (const line of jsonStringify(attrs, true).split('\n')) { + // remove the first and last line the opening and closing brackets + if (line === '{' || line === '}' || line === '[' || line === ']') { + continue; + } + + let formattedLine = line; + + // remove the quotes from the keys and remove the opening bracket + // TODO: make sure keys with quotes are preserved + formattedLine = formattedLine.replace( + /"([^"]+)":/, + this.color('key', '$1:'), + ); + + // replace all escaped new lines with a new line and append the indentation of the line + let indentationSize = line.match(/^\s*/)?.[0]?.length || 0; + if (indentationSize) indentationSize++; + + // TODO: error stack traces will have 4 spaces of indentation, should we sanitize all 4 spaces / tabs to 2 space indentation? + formattedLine = formattedLine.replaceAll( + /\\n/g, + '\n' + [...Array(indentationSize)].join(' '), + ); + + // remove the ending comma + formattedLine = formattedLine.replace(/,$/, ''); + + // color the opening and closing brackets + formattedLine = formattedLine.replace( + /(\[|\{|\]|\})$/, + this.color('key', '$1'), + ); + + log += formattedLine + '\n'; + } + + // remove last new line + log = log.slice(0, -1); + + return log; + } +} diff --git a/packages/logger/src/writers/index.ts b/packages/logger/src/writers/index.ts new file mode 100644 index 000000000..346b943f9 --- /dev/null +++ b/packages/logger/src/writers/index.ts @@ -0,0 +1,4 @@ +export * from './common'; +export * from './console'; +export * from './json'; +export * from './memory'; diff --git a/packages/logger/src/writers/json.ts b/packages/logger/src/writers/json.ts new file mode 100644 index 000000000..d8c07eb48 --- /dev/null +++ b/packages/logger/src/writers/json.ts @@ -0,0 +1,24 @@ +import { getEnvBool } from '~internal/env'; +import { LogLevel } from '../logger'; +import { Attributes } from '../utils'; +import { jsonStringify, LogWriter } from './common'; + +export class JSONLogWriter implements LogWriter { + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + console.log( + jsonStringify( + { + ...attrs, + level, + ...(msg ? { msg } : {}), + timestamp: new Date().toISOString(), + }, + getEnvBool('LOG_JSON_PRETTY'), + ), + ); + } +} diff --git a/packages/logger/src/writers/logtape.ts b/packages/logger/src/writers/logtape.ts new file mode 100644 index 000000000..2fae2108c --- /dev/null +++ b/packages/logger/src/writers/logtape.ts @@ -0,0 +1,39 @@ +import { getLogger, Logger as LogTapeLogger } from '@logtape/logtape'; +import { LogLevel } from '../logger'; +import { Attributes } from '../utils'; +import { LogWriter } from './common'; + +export interface LogTapeLogWriterOptions { + category?: Parameters[0]; + getProperties?( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): Record; +} + +export class LogTapeLogWriter implements LogWriter { + #logTapeLogger: LogTapeLogger; + + constructor(public options: LogTapeLogWriterOptions = {}) { + this.#logTapeLogger = getLogger(this.options.category ?? ['hive-gateway']); + } + + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + const log = this.#logTapeLogger[level].bind(this.#logTapeLogger); + const properties = this.options.getProperties + ? this.options.getProperties(level, attrs, msg) + : attrs + ? { + // TODO: attrs can be an array too + ...attrs, + } + : undefined; + if (msg != null) log(msg, properties); + else if (properties) log(properties); + } +} diff --git a/packages/logger/src/writers/memory.ts b/packages/logger/src/writers/memory.ts new file mode 100644 index 000000000..5808b2d54 --- /dev/null +++ b/packages/logger/src/writers/memory.ts @@ -0,0 +1,18 @@ +import { LogLevel } from '../logger'; +import { Attributes } from '../utils'; +import { LogWriter } from './common'; + +export class MemoryLogWriter implements LogWriter { + public logs: { level: LogLevel; msg?: string; attrs?: unknown }[] = []; + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + this.logs.push({ + level, + ...(msg ? { msg } : {}), + ...(attrs ? { attrs } : {}), + }); + } +} diff --git a/packages/logger/src/writers/pino.ts b/packages/logger/src/writers/pino.ts new file mode 100644 index 000000000..84ea435bb --- /dev/null +++ b/packages/logger/src/writers/pino.ts @@ -0,0 +1,18 @@ +import type { BaseLogger as PinoLogger } from 'pino'; +import { LogLevel } from '../logger'; +import { Attributes } from '../utils'; +import { LogWriter } from './common'; + +export class PinoLogWriter implements LogWriter { + #pinoLogger: PinoLogger; + constructor(pinoLogger: PinoLogger) { + this.#pinoLogger = pinoLogger; + } + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + this.#pinoLogger[level](attrs, msg || undefined); + } +} diff --git a/packages/logger/src/writers/winston.ts b/packages/logger/src/writers/winston.ts new file mode 100644 index 000000000..6a5460847 --- /dev/null +++ b/packages/logger/src/writers/winston.ts @@ -0,0 +1,22 @@ +import type { Logger as WinstonLogger } from 'winston'; +import { LogLevel } from '../logger'; +import { Attributes } from '../utils'; +import { LogWriter } from './common'; + +export class WinstonLogWriter implements LogWriter { + #winstonLogger: WinstonLogger; + constructor(winstonLogger: WinstonLogger) { + this.#winstonLogger = winstonLogger; + } + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void { + if (msg) { + this.#winstonLogger[level === 'trace' ? 'verbose' : level](msg, attrs); + } else { + this.#winstonLogger[level === 'trace' ? 'verbose' : level](attrs); + } + } +} diff --git a/packages/logger/tests/__snapshots__/writers.test.ts.snap b/packages/logger/tests/__snapshots__/writers.test.ts.snap new file mode 100644 index 000000000..82b3e201c --- /dev/null +++ b/packages/logger/tests/__snapshots__/writers.test.ts.snap @@ -0,0 +1,32 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`ConsoleLogWriter should color levels and keys 1`] = ` +[ + ""\\u001b[36mTRC\\u001b[0m \\u001b[1mhi\\u001b[0m \\n \\u001b[35mhello:\\u001b[0m \\u001b[35m{\\u001b[0m\\n \\u001b[35mdear:\\u001b[0m \\"world\\"\\n \\u001b[35mtry:\\u001b[0m \\u001b[35m[\\u001b[0m\\n \\"num\\"\\n 1\\n 2\\n \\u001b[35m]\\u001b[0m\\n \\u001b[35m}\\u001b[0m"", + ""\\u001b[90mDBG\\u001b[0m \\u001b[1mhi\\u001b[0m \\n \\u001b[35mhello:\\u001b[0m \\u001b[35m{\\u001b[0m\\n \\u001b[35mdear:\\u001b[0m \\"world\\"\\n \\u001b[35mtry:\\u001b[0m \\u001b[35m[\\u001b[0m\\n \\"num\\"\\n 1\\n 2\\n \\u001b[35m]\\u001b[0m\\n \\u001b[35m}\\u001b[0m"", + ""\\u001b[32mINF\\u001b[0m \\u001b[1mhi\\u001b[0m \\n \\u001b[35mhello:\\u001b[0m \\u001b[35m{\\u001b[0m\\n \\u001b[35mdear:\\u001b[0m \\"world\\"\\n \\u001b[35mtry:\\u001b[0m \\u001b[35m[\\u001b[0m\\n \\"num\\"\\n 1\\n 2\\n \\u001b[35m]\\u001b[0m\\n \\u001b[35m}\\u001b[0m"", + ""\\u001b[33mWRN\\u001b[0m \\u001b[1mhi\\u001b[0m \\n \\u001b[35mhello:\\u001b[0m \\u001b[35m{\\u001b[0m\\n \\u001b[35mdear:\\u001b[0m \\"world\\"\\n \\u001b[35mtry:\\u001b[0m \\u001b[35m[\\u001b[0m\\n \\"num\\"\\n 1\\n 2\\n \\u001b[35m]\\u001b[0m\\n \\u001b[35m}\\u001b[0m"", + ""\\u001b[41;39mERR\\u001b[0m \\u001b[1mhi\\u001b[0m \\n \\u001b[35mhello:\\u001b[0m \\u001b[35m{\\u001b[0m\\n \\u001b[35mdear:\\u001b[0m \\"world\\"\\n \\u001b[35mtry:\\u001b[0m \\u001b[35m[\\u001b[0m\\n \\"num\\"\\n 1\\n 2\\n \\u001b[35m]\\u001b[0m\\n \\u001b[35m}\\u001b[0m"", +] +`; + +exports[`ConsoleLogWriter should flush async logs 1`] = ` +[ + ""TRC hi \\n hello: {\\n dear: \\"world\\"\\n try: [\\n \\"num\\"\\n 1\\n 2\\n ]\\n }"", + ""DBG hi \\n hello: {\\n dear: \\"world\\"\\n try: [\\n \\"num\\"\\n 1\\n 2\\n ]\\n }"", + ""INF hi \\n hello: {\\n dear: \\"world\\"\\n try: [\\n \\"num\\"\\n 1\\n 2\\n ]\\n }"", + ""WRN hi \\n hello: {\\n dear: \\"world\\"\\n try: [\\n \\"num\\"\\n 1\\n 2\\n ]\\n }"", + ""ERR hi \\n hello: {\\n dear: \\"world\\"\\n try: [\\n \\"num\\"\\n 1\\n 2\\n ]\\n }"", +] +`; + +exports[`ConsoleLogWriter should pretty print the attributes 1`] = ` +[ + ""TRC obj \\n a: 1\\n b: 2"", + ""DBG arr \\n \\"a\\"\\n \\"b\\"\\n \\"c\\""", + ""INF nested \\n a: {\\n b: {\\n c: {\\n d: 1\\n }\\n }\\n }"", + ""WRN arr objs \\n {\\n a: 1\\n }\\n {\\n b: 2\\n }"", + ""ERR multlinestring \\n str: \\"a\\n b\\n c\\"\\n err: {\\n message: \\"woah!\\"\\n }"", + ""INF graphql \\n query: \\"\\n {\\n hi(howMany: 1) {\\n hello\\n world\\n }\\n }\\n \\""", +] +`; diff --git a/packages/logger/tests/legacyLogger.test.ts b/packages/logger/tests/legacyLogger.test.ts new file mode 100644 index 000000000..574074833 --- /dev/null +++ b/packages/logger/tests/legacyLogger.test.ts @@ -0,0 +1,94 @@ +import { LegacyLogger } from '@graphql-hive/logger'; +import { Logger as MeshLogger } from '@graphql-mesh/types'; +import { expect, it } from 'vitest'; +import { Logger, LoggerOptions } from '../src/logger'; +import { MemoryLogWriter } from '../src/writers'; + +// a type test making sure the LegacyLogger is compatible with the MeshLogger +export const _: MeshLogger = new LegacyLogger(null as any); + +function createTLogger(opts?: Partial) { + const writer = new MemoryLogWriter(); + return [ + LegacyLogger.from( + new Logger({ ...opts, writers: opts?.writers ? opts.writers : [writer] }), + ), + writer, + ] as const; +} + +it('should correctly write legacy logger logs', () => { + const [log, writer] = createTLogger(); + + log.info('hello world'); + log.info({ hello: 'world' }); + log.info('hello', { wor: 'ld' }); + log.info('hello', [{ wor: 'ld' }]); + log.info('hello', { w: 'o' }, { rl: 'd' }); + log.info('hello', 'world'); + + log.child('child ').info('hello child'); + log.child({ chi: 'ld' }).info('hello chi ld'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "hello world", + }, + { + "attrs": { + "hello": "world", + }, + "level": "info", + }, + { + "attrs": [ + { + "wor": "ld", + }, + ], + "level": "info", + "msg": "hello", + }, + { + "attrs": [ + [ + { + "wor": "ld", + }, + ], + ], + "level": "info", + "msg": "hello", + }, + { + "attrs": [ + { + "w": "o", + }, + { + "rl": "d", + }, + ], + "level": "info", + "msg": "hello", + }, + { + "attrs": [ + "world", + ], + "level": "info", + "msg": "hello", + }, + { + "level": "info", + "msg": "child hello child", + }, + { + "level": "info", + "msg": "chi=ld hello chi ld", + }, + ] + `); +}); diff --git a/packages/logger/tests/logger.bench.ts b/packages/logger/tests/logger.bench.ts new file mode 100644 index 000000000..bd4a3eefa --- /dev/null +++ b/packages/logger/tests/logger.bench.ts @@ -0,0 +1,58 @@ +import { jsonStringify, Logger } from '@graphql-hive/logger'; +import { bench, describe } from 'vitest'; + +const voidlog = new Logger({ + writers: [ + { + write() { + // void + }, + }, + ], +}); + +describe.each([ + { name: 'string' as const, value: 'hello' }, + { name: 'integer' as const, value: 7 }, + { name: 'float' as const, value: 7.77 }, + { name: 'object' as const, value: { hello: 'world' } }, +])('log formatting of $name', ({ name, value }) => { + // we switch outside of the bench to avoid the overhead of the switch + switch (name) { + case 'string': + bench('template literals', () => { + voidlog.info(`hi there ${value}`); + }); + bench('interpolation', () => { + voidlog.info('hi there %s', value); + }); + break; + case 'integer': + bench('template literals', () => { + voidlog.info(`hi there ${value}`); + }); + bench('interpolation', () => { + voidlog.info('hi there %i', value); + }); + break; + case 'float': + bench('template literals', () => { + voidlog.info(`hi there ${value}`); + }); + bench('interpolation', () => { + voidlog.info('hi there %d', value); + }); + break; + case 'object': + bench('template literals native stringify', () => { + voidlog.info(`hi there ${JSON.stringify(value)}`); + }); + bench('template literals logger stringify', () => { + voidlog.info(`hi there ${jsonStringify(value)}`); + }); + bench('interpolation', () => { + voidlog.info('hi there %o', value); + }); + break; + } +}); diff --git a/packages/logger/tests/logger.test.ts b/packages/logger/tests/logger.test.ts new file mode 100644 index 000000000..febbec659 --- /dev/null +++ b/packages/logger/tests/logger.test.ts @@ -0,0 +1,932 @@ +import { setTimeout } from 'node:timers/promises'; +import { expect, it, vi } from 'vitest'; +import { Logger, LoggerOptions } from '../src/logger'; +import { MemoryLogWriter } from '../src/writers'; +import { stableError } from './utils'; + +function createTLogger(opts?: Partial) { + const writer = new MemoryLogWriter(); + return [ + new Logger({ ...opts, writers: opts?.writers ? opts.writers : [writer] }), + writer, + ] as const; +} + +it('should write logs with levels, message and attributes', () => { + const [log, writter] = createTLogger(); + + const err = stableError(new Error('Woah!')); + + log.log('info'); + log.log('info', { hello: 'world', err }, 'Hello, world!'); + log.log('info', '2nd Hello, world!'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + }, + { + "attrs": { + "err": { + "class": "Error", + "message": "Woah!", + "name": "Error", + "stack": "", + }, + "hello": "world", + }, + "level": "info", + "msg": "Hello, world!", + }, + { + "level": "info", + "msg": "2nd Hello, world!", + }, + ] + `); +}); + +it('should write logs only if level is higher than set', () => { + const [log, writter] = createTLogger({ + level: 'info', + }); + + log.trace('Trace'); + log.debug('Debug'); + log.info('Info'); + log.warn('Warn'); + log.error('Error'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "Info", + }, + { + "level": "warn", + "msg": "Warn", + }, + { + "level": "error", + "msg": "Error", + }, + ] + `); +}); + +it('should include attributes in child loggers', () => { + let [log, writter] = createTLogger(); + + log = log.child({ par: 'ent' }); + + log.info('hello'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "par": "ent", + }, + "level": "info", + "msg": "hello", + }, + ] + `); +}); + +it('should include prefix in child loggers', () => { + let [log, writter] = createTLogger(); + + log = log.child('prefix '); + + log.info('hello'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "prefix hello", + }, + ] + `); +}); + +it('should include attributes and prefix in child loggers', () => { + let [log, writter] = createTLogger(); + + log = log.child({ par: 'ent' }, 'prefix '); + + log.info('hello'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "par": "ent", + }, + "level": "info", + "msg": "prefix hello", + }, + ] + `); +}); + +it('should have child inherit parent log level', () => { + let [log, writter] = createTLogger({ level: 'warn' }); + + log = log.child({ par: 'ent' }); + + log.debug('no hello'); + log.info('still no hello'); + log.warn('hello'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "par": "ent", + }, + "level": "warn", + "msg": "hello", + }, + ] + `); +}); + +it('should include attributes and prefix in nested child loggers', () => { + let [log, writter] = createTLogger(); + + log = log.child({ par: 'ent' }, 'prefix '); + log = log.child({ par2: 'ent2' }, 'prefix2 '); + + log.info('hello'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "par": "ent", + "par2": "ent2", + }, + "level": "info", + "msg": "prefix prefix2 hello", + }, + ] + `); +}); + +it('should unwrap lazy attribute values', () => { + const [log, writter] = createTLogger(); + + log.info( + () => ({ + every: 'thing', + nested: { + lazy: () => 'nested lazy not unwrapped', + }, + }), + 'hello', + ); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "every": "thing", + "nested": { + "lazy": "[Function: lazy]", + }, + }, + "level": "info", + "msg": "hello", + }, + ] + `); +}); + +it('should not log lazy attributes returning nothing', () => { + const [log, writter] = createTLogger(); + + log.info(() => undefined, 'hello'); + log.info(() => null, 'wor'); + log.info(() => void 0, 'ld'); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "hello", + }, + { + "level": "info", + "msg": "wor", + }, + { + "level": "info", + "msg": "ld", + }, + ] + `); +}); + +it('should not unwrap lazy attribute values', () => { + const [log, writter] = createTLogger(); + + log.info( + { + lazy: () => 'lazy', + nested: { + lazy: () => 'nested lazy', + }, + arr: [() => '0', '1'], + }, + 'hello', + ); + + expect(writter.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "arr": [ + "[Function: (anonymous)]", + "1", + ], + "lazy": "[Function: lazy]", + "nested": { + "lazy": "[Function: lazy]", + }, + }, + "level": "info", + "msg": "hello", + }, + ] + `); +}); + +it('should not unwrap lazy attributes if level is not to be logged', () => { + const [log] = createTLogger({ + level: 'info', + }); + + const lazy = vi.fn(() => ({ la: 'zy' })); + log.debug(lazy, 'hello'); + + expect(lazy).not.toHaveBeenCalled(); +}); + +it('should wait for async writers on flush', async () => { + const logs: any[] = []; + const log = new Logger({ + writers: [ + { + async write(level, attrs, msg) { + await setTimeout(10); + logs.push({ level, attrs, msg }); + }, + }, + ], + }); + + log.info('hello'); + log.info('world'); + + // not flushed yet + expect(logs).toMatchInlineSnapshot(`[]`); + + await log.flush(); + + // flushed + expect(logs).toMatchInlineSnapshot(` + [ + { + "attrs": undefined, + "level": "info", + "msg": "hello", + }, + { + "attrs": undefined, + "level": "info", + "msg": "world", + }, + ] + `); +}); + +it('should wait for flushable writers on flush', async () => { + const logs: any[] = []; + const pendingWrites: Promise[] = []; + const log = new Logger({ + writers: [ + { + write(level, attrs, msg) { + pendingWrites.push( + (async () => { + await setTimeout(10); + logs.push({ level, attrs, msg }); + })(), + ); + }, + async flush() { + await Promise.all(pendingWrites); + }, + }, + ], + }); + + log.info('hello'); + log.info('world'); + + // not flushed yet + expect(logs).toMatchInlineSnapshot(`[]`); + + await log.flush(); + + // flushed + expect(logs).toMatchInlineSnapshot(` + [ + { + "attrs": undefined, + "level": "info", + "msg": "hello", + }, + { + "attrs": undefined, + "level": "info", + "msg": "world", + }, + ] + `); +}); + +it('should handle async write errors on flush', async () => { + const origConsoleError = console.error; + console.error = vi.fn(() => { + // failed writes will be logged to the error console + }); + using _ = { + [Symbol.dispose]() { + console.error = origConsoleError; + }, + }; + + let i = 0; + const log = new Logger({ + writers: [ + { + async write() { + i++; + throw new Error('Write failed! #' + i); + }, + }, + ], + }); + + // no fail + log.info('hello'); + log.info('world'); + + try { + await log.flush(); + throw new Error('should not have reached here'); + } catch (e) { + expect(e).toMatchInlineSnapshot( + `[AggregateError: Failed to flush with 2 errors]`, + ); + expect((e as AggregateError).errors).toMatchInlineSnapshot(` + [ + [Error: Write failed! #1], + [Error: Write failed! #2], + ] + `); + expect(console.error).toHaveBeenCalledTimes(2); + } +}); + +it('should handle writer flush errors on flush', async () => { + const log = new Logger({ + writers: [ + { + write() { + // noop + }, + flush() { + throw new Error('Whoops!'); + }, + }, + ], + }); + + // no fail + log.info('hello'); + log.info('world'); + + try { + await log.flush(); + throw new Error('should not have reached here'); + } catch (e) { + expect(e).toMatchInlineSnapshot(`[Error: Failed to flush]`); + expect((e as Error).cause).toMatchInlineSnapshot(`[Error: Whoops!]`); + } +}); + +it('should handle both async write and writer flush errors on flush', async () => { + const origConsoleError = console.error; + console.error = vi.fn(() => { + // failed writes will be logged to the error console + }); + using _ = { + [Symbol.dispose]() { + console.error = origConsoleError; + }, + }; + + let i = 0; + const log = new Logger({ + writers: [ + { + async write() { + i++; + throw new Error('Write failed! #' + i); + }, + flush() { + throw new Error('Whoops!'); + }, + }, + ], + }); + + // no fail + log.info('hello'); + log.info('world'); + + try { + await log.flush(); + throw new Error('should not have reached here'); + } catch (e) { + expect(e).toMatchInlineSnapshot( + `[AggregateError: Failed to flush with 3 errors]`, + ); + expect((e as AggregateError).errors).toMatchInlineSnapshot(` + [ + [Error: Whoops!], + [Error: Write failed! #1], + [Error: Write failed! #2], + ] + `); + expect(console.error).toHaveBeenCalledTimes(2); + } +}); + +it('should wait for async writers on async dispose', async () => { + const logs: any[] = []; + + { + await using log = new Logger({ + writers: [ + { + async write(level, attrs, msg) { + await setTimeout(10); + logs.push({ level, attrs, msg }); + }, + }, + ], + }); + + log.info('hello'); + log.info('world'); + + // not flushed yet + expect(logs).toMatchInlineSnapshot(`[]`); + } + + // flushed because scope ended and async dispose was called + expect(logs).toMatchInlineSnapshot(` + [ + { + "attrs": undefined, + "level": "info", + "msg": "hello", + }, + { + "attrs": undefined, + "level": "info", + "msg": "world", + }, + ] + `); +}); + +it('should log array attributes with object child attributes', () => { + let [log, writer] = createTLogger(); + + log = log.child({ hello: 'world' }); + log.info(['hello', 'world']); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": [ + { + "hello": "world", + }, + "hello", + "world", + ], + "level": "info", + }, + ] + `); +}); + +it('should log array child attributes with object attributes', () => { + let [log, writer] = createTLogger(); + + log = log.child(['hello', 'world']); + log.info({ hello: 'world' }); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": [ + "hello", + "world", + { + "hello": "world", + }, + ], + "level": "info", + }, + ] + `); +}); + +it('should log array child attributes with array attributes', () => { + let [log, writer] = createTLogger(); + + log = log.child(['hello', 'world']); + log.info(['more', 'life']); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": [ + "hello", + "world", + "more", + "life", + ], + "level": "info", + }, + ] + `); +}); + +it('should format string', () => { + const [log, writer] = createTLogger(); + + log.info('%o hello %s', { worldly: 1 }, 'world'); + log.info({ these: { are: 'attrs' } }, '%o hello %s', { worldly: 1 }, 'world'); + log.info('hello %s %j %d %o', 'world', { obj: true }, 4, { another: 'obj' }); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "{"worldly":1} hello world", + }, + { + "attrs": { + "these": { + "are": "attrs", + }, + }, + "level": "info", + "msg": "{"worldly":1} hello world", + }, + { + "level": "info", + "msg": "hello world {"obj":true} 4 {"another":"obj"}", + }, + ] + `); +}); + +it('should write logs with unexpected attributes', () => { + const [log, writer] = createTLogger(); + + const err = stableError(new Error('Woah!')); + + log.info(err); + + log.info([err, { denis: 'badurina' }, ['hello'], 'world']); + + class MyClass { + constructor(public someprop: string) {} + get getsomeprop() { + return this.someprop; + } + } + log.info(new MyClass('hey')); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "class": "Error", + "message": "Woah!", + "name": "Error", + "stack": "", + }, + "level": "info", + }, + { + "attrs": [ + { + "class": "Error", + "message": "Woah!", + "name": "Error", + "stack": "", + }, + { + "denis": "badurina", + }, + [ + "hello", + ], + "world", + ], + "level": "info", + }, + { + "attrs": { + "class": "MyClass", + "getsomeprop": "hey", + "someprop": "hey", + }, + "level": "info", + }, + ] + `); +}); + +it('should serialise using the toJSON method', () => { + const [log, writer] = createTLogger(); + + class ToJSON { + toJSON() { + return { hello: 'world' }; + } + } + + log.info(new ToJSON(), 'hello'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "hello": "world", + }, + "level": "info", + "msg": "hello", + }, + ] + `); +}); + +it('should serialise error causes', () => { + const [log, writer] = createTLogger(); + + const cause = stableError(new Error('Cause')); + + const err = stableError(new Error('Woah!', { cause })); + + log.info(err, 'hello'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "cause": { + "class": "Error", + "message": "Cause", + "name": "Error", + "stack": "", + }, + "class": "Error", + "message": "Woah!", + "name": "Error", + "stack": "", + }, + "level": "info", + "msg": "hello", + }, + ] + `); +}); + +it('should gracefully handle Object.create(null)', () => { + const [log, writer] = createTLogger(); + + class NullConst { + constructor() { + return Object.create(null); + } + } + class NullProp { + someprop = Object.create(null); + } + + log.info({ class: new NullConst() }, 'hello'); + log.info(new NullProp(), 'world'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "class": {}, + }, + "level": "info", + "msg": "hello", + }, + { + "attrs": { + "class": "NullProp", + "someprop": {}, + }, + "level": "info", + "msg": "world", + }, + ] + `); +}); + +it('should handle circular references', () => { + const [log, writer] = createTLogger(); + + const obj = { circ: null as any }; + const circ = { + hello: 'world', + obj, + }; + obj.circ = circ; + + log.info(circ, 'circular'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "hello": "world", + "obj": { + "circ": { + "hello": "world", + "obj": "[Circular]", + }, + }, + }, + "level": "info", + "msg": "circular", + }, + ] + `); +}); + +it('should serialise aggregate errors', () => { + const [log, writer] = createTLogger(); + + const err1 = stableError(new Error('Woah!')); + + const err2 = stableError(new Error('Woah2!')); + + const aggErr = stableError( + new AggregateError([err1, err2], 'Woah Aggregate!'), + ); + + log.info(aggErr, 'aggregate'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "class": "AggregateError", + "errors": [ + { + "class": "Error", + "message": "Woah!", + "name": "Error", + "stack": "", + }, + { + "class": "Error", + "message": "Woah2!", + "name": "Error", + "stack": "", + }, + ], + "message": "Woah Aggregate!", + "name": "AggregateError", + "stack": "", + }, + "level": "info", + "msg": "aggregate", + }, + ] + `); +}); + +it('should change log level', () => { + const [log, writer] = createTLogger(); + + log.info('hello'); + log.setLevel('warn'); + log.info('no hello'); + log.warn('yes hello'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "hello", + }, + { + "level": "warn", + "msg": "yes hello", + }, + ] + `); +}); + +it('should change root log level and propagate to child loggers', () => { + const [rootLog, writer] = createTLogger(); + + const childLog = rootLog.child('sub '); + + childLog.info('hello'); + rootLog.setLevel('warn'); + childLog.info('no hello'); + childLog.warn('yes hello'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "sub hello", + }, + { + "level": "warn", + "msg": "sub yes hello", + }, + ] + `); +}); + +it('should change child log level only on child', () => { + const [rootLog, writer] = createTLogger(); + + const childLog = rootLog.child('sub '); + + childLog.setLevel('warn'); + rootLog.info('yes hello'); // should still log because root didnt change + childLog.info('no hello'); + childLog.warn('yes hello'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "level": "info", + "msg": "yes hello", + }, + { + "level": "warn", + "msg": "sub yes hello", + }, + ] + `); +}); + +it('should log using nodejs.util.inspect.custom symbol', () => { + const [log, writer] = createTLogger(); + + class Inspect { + [Symbol.for('nodejs.util.inspect.custom')]() { + return 'Ok good'; + } + } + + log.info(new Inspect(), 'sy'); + + expect(writer.logs).toMatchInlineSnapshot(` + [ + { + "attrs": { + "Symbol(nodejs.util.inspect.custom)": "Ok good", + "class": "Inspect", + }, + "level": "info", + "msg": "sy", + }, + ] + `); +}); diff --git a/packages/logger/tests/utils.ts b/packages/logger/tests/utils.ts new file mode 100644 index 000000000..1c448beca --- /dev/null +++ b/packages/logger/tests/utils.ts @@ -0,0 +1,20 @@ +/** Stabilises the error for snapshot testing */ +export function stableError(err: T): T { + if (globalThis.Bun) { + // bun serialises errors differently from node + // we need to remove some properties to make the snapshots match + // @ts-expect-error + delete err.column; + // @ts-expect-error + delete err.line; + // @ts-expect-error + delete err.originalColumn; + // @ts-expect-error + delete err.originalLine; + // @ts-expect-error + delete err.sourceURL; + } + // we remove the stack to make the snapshot stable + err.stack = ''; + return err; +} diff --git a/packages/logger/tests/writers.test.ts b/packages/logger/tests/writers.test.ts new file mode 100644 index 000000000..b4f26c035 --- /dev/null +++ b/packages/logger/tests/writers.test.ts @@ -0,0 +1,85 @@ +import { describe, expect, it } from 'vitest'; +import { Logger } from '../src/logger'; +import { + ConsoleLogWriter, + ConsoleLogWriterOptions, + jsonStringify, +} from '../src/writers'; + +describe('ConsoleLogWriter', () => { + function createTConsoleLogger(opts?: Partial) { + const logs: string[] = []; + const writer = new ConsoleLogWriter({ + console: { + debug: (...args: unknown[]) => { + logs.push(args.map((arg) => jsonStringify(arg)).join(' ')); + }, + info: (...args: unknown[]) => { + logs.push(args.map((arg) => jsonStringify(arg)).join(' ')); + }, + warn: (...args: unknown[]) => { + logs.push(args.map((arg) => jsonStringify(arg)).join(' ')); + }, + error: (...args: unknown[]) => { + logs.push(args.map((arg) => jsonStringify(arg)).join(' ')); + }, + }, + noTimestamp: true, + noColor: true, + ...opts, + }); + return [new Logger({ level: 'trace', writers: [writer] }), logs] as const; + } + + it('should pretty print the attributes', () => { + const [log, logs] = createTConsoleLogger(); + + log.trace({ a: 1, b: 2 }, 'obj'); + log.debug(['a', 'b', 'c'], 'arr'); + log.info({ a: { b: { c: { d: 1 } } } }, 'nested'); + log.warn([{ a: 1 }, { b: 2 }], 'arr objs'); + log.error({ str: 'a\nb\nc', err: { message: 'woah!' } }, 'multlinestring'); + + log.info( + { + query: ` +{ + hi(howMany: 1) { + hello + world + } +} +`, + }, + 'graphql', + ); + + expect(logs).toMatchSnapshot(); + }); + + it('should color levels and keys', () => { + const [log, logs] = createTConsoleLogger({ noColor: false }); + + log.trace({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.debug({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.info({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.warn({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.error({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + + expect(logs).toMatchSnapshot(); + }); + + it('should flush async logs', async () => { + const [log, logs] = createTConsoleLogger({ noColor: true, async: true }); + + log.trace({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.debug({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.info({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.warn({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + log.error({ hello: { dear: 'world', try: ['num', 1, 2] } }, 'hi'); + + expect(logs).toHaveLength(0); + await log.flush(); + expect(logs).toMatchSnapshot(); + }); +}); diff --git a/packages/nestjs/package.json b/packages/nestjs/package.json index e838b9bc7..70f5937af 100644 --- a/packages/nestjs/package.json +++ b/packages/nestjs/package.json @@ -15,7 +15,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -46,6 +46,7 @@ }, "dependencies": { "@graphql-hive/gateway": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-mesh/types": "^0.104.7", "@graphql-tools/utils": "^10.9.1", "@whatwg-node/promise-helpers": "^1.3.0", diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index 6dcca5618..44e5be704 100644 --- a/packages/nestjs/src/index.ts +++ b/packages/nestjs/src/index.ts @@ -1,5 +1,6 @@ import { createGatewayRuntime, + createLoggerFromLogging, GatewayCLIBuiltinPluginConfig, GatewayConfigProxy, GatewayConfigSubgraph, @@ -11,10 +12,7 @@ import { PubSub, type GatewayRuntime, } from '@graphql-hive/gateway'; -import { - Logger as GatewayLogger, - type LazyLoggerMessage, -} from '@graphql-mesh/types'; +import { Logger as HiveLogger } from '@graphql-hive/logger'; import { asArray, type IResolvers, @@ -29,7 +27,6 @@ import { type SubscriptionConfig, } from '@nestjs/graphql'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; -import { isDebug } from '~internal/env'; import type { FastifyReply, FastifyRequest } from 'fastify'; import { lexicographicSortSchema } from 'graphql'; @@ -64,6 +61,7 @@ export class HiveGatewayDriver< private async ensureGatewayRuntime({ typeDefs, resolvers, + logging, ...options }: HiveGatewayDriverConfig) { if (this._gatewayRuntime) { @@ -79,14 +77,34 @@ export class HiveGatewayDriver< if (resolvers) { additionalResolvers.push(...asArray(resolvers)); } - const logger = new NestJSLoggerAdapter( - 'Hive Gateway', - {}, - new NestLogger('Hive Gateway'), - options.debug ?? isDebug(), - ); + + let log: HiveLogger; + if (logging != null) { + log = createLoggerFromLogging(logging); + } else { + const nestLog = new NestLogger('Hive Gateway'); + log = new HiveLogger({ + writers: [ + { + write(level, attrs, msg) { + switch (level) { + case 'trace': + nestLog.verbose(msg, attrs); + break; + case 'info': + nestLog.log(msg, attrs); + break; + default: + nestLog[level](msg, attrs); + } + }, + }, + ], + }); + } + const configCtx = { - logger, + log, cwd: process.cwd(), pubsub: options.pubsub || new PubSub(), }; @@ -97,7 +115,7 @@ export class HiveGatewayDriver< }); this._gatewayRuntime = createGatewayRuntime({ ...options, - logging: configCtx.logger, + logging: configCtx.log, cache, graphqlEndpoint: options.path, additionalTypeDefs, @@ -288,92 +306,3 @@ export class HiveGatewayDriver< }); } } - -class NestJSLoggerAdapter implements GatewayLogger { - constructor( - public name: string, - private meta: Record, - private logger: NestLogger, - private isDebug: boolean, - ) {} - private prepareMessage(args: LazyLoggerMessage[]) { - const obj = { - ...(this.meta || {}), - }; - const strs: string[] = []; - const flattenedArgs = args - .flatMap((arg) => (typeof arg === 'function' ? arg() : arg)) - .flat(Number.POSITIVE_INFINITY); - for (const arg of flattenedArgs) { - if (typeof arg === 'string' || typeof arg === 'number') { - strs.push(arg.toString()); - } else { - Object.assign(obj, arg); - } - } - return { obj, str: strs.join(', ') }; - } - log(...args: any[]) { - const { obj, str } = this.prepareMessage(args); - if (Object.keys(obj).length) { - this.logger.log(obj, str); - } else { - this.logger.log(str); - } - } - info(...args: any[]) { - const { obj, str } = this.prepareMessage(args); - if (Object.keys(obj).length) { - this.logger.log(obj, str); - } else { - this.logger.log(str); - } - } - error(...args: any[]) { - const { obj, str } = this.prepareMessage(args); - if (Object.keys(obj).length) { - this.logger.error(obj, str); - } else { - this.logger.error(str); - } - } - warn(...args: any[]) { - const { obj, str } = this.prepareMessage(args); - if (Object.keys(obj).length) { - this.logger.warn(obj, str); - } else { - this.logger.warn(str); - } - } - debug(...args: any[]) { - if (!this.isDebug) { - return; - } - const { obj, str } = this.prepareMessage(args); - if (Object.keys(obj).length) { - this.logger.debug(obj, str); - } else { - this.logger.debug(str); - } - } - child( - newNameOrMeta: string | Record, - ): NestJSLoggerAdapter { - const newName = - typeof newNameOrMeta === 'string' - ? this.name - ? `${this.name}, ${newNameOrMeta}` - : newNameOrMeta - : this.name; - const newMeta = - typeof newNameOrMeta === 'string' - ? this.meta - : { ...this.meta, ...newNameOrMeta }; - return new NestJSLoggerAdapter( - newName, - newMeta, - new NestLogger(newName), - this.isDebug, - ); - } -} diff --git a/packages/plugins/aws-sigv4/package.json b/packages/plugins/aws-sigv4/package.json index 9bad9ed4b..5906e958e 100644 --- a/packages/plugins/aws-sigv4/package.json +++ b/packages/plugins/aws-sigv4/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/plugins/aws-sigv4/src/plugin.ts b/packages/plugins/aws-sigv4/src/plugin.ts index 43be07557..46d8e68da 100644 --- a/packages/plugins/aws-sigv4/src/plugin.ts +++ b/packages/plugins/aws-sigv4/src/plugin.ts @@ -1,7 +1,6 @@ import { BinaryLike, createHash, createHmac, KeyObject } from 'node:crypto'; import { STS } from '@aws-sdk/client-sts'; import type { GatewayPlugin } from '@graphql-hive/gateway-runtime'; -import { subgraphNameByExecutionRequest } from '@graphql-mesh/fusion-runtime'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; import { getEnvStr } from '~internal/env'; import aws4, { type Request as AWS4Request } from 'aws4'; @@ -358,8 +357,7 @@ export function useAWSSigv4>( }, // Handle outgoing requests onFetch({ url, options, setURL, setOptions, executionRequest }) { - const subgraphName = (executionRequest && - subgraphNameByExecutionRequest.get(executionRequest))!; + const subgraphName = executionRequest?.subgraphName!; if (!isBufferOrString(options.body)) { return; } diff --git a/packages/plugins/deduplicate-request/package.json b/packages/plugins/deduplicate-request/package.json index 5675a57e9..2ceb1f8ab 100644 --- a/packages/plugins/deduplicate-request/package.json +++ b/packages/plugins/deduplicate-request/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/plugins/hmac-upstream-signature/package.json b/packages/plugins/hmac-upstream-signature/package.json index af59c957e..7d9c52a53 100644 --- a/packages/plugins/hmac-upstream-signature/package.json +++ b/packages/plugins/hmac-upstream-signature/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/plugins/hmac-upstream-signature/src/index.ts b/packages/plugins/hmac-upstream-signature/src/index.ts index 3bc4088b5..4bec1c30d 100644 --- a/packages/plugins/hmac-upstream-signature/src/index.ts +++ b/packages/plugins/hmac-upstream-signature/src/index.ts @@ -1,4 +1,6 @@ import type { GatewayPlugin } from '@graphql-hive/gateway-runtime'; +import type { Logger } from '@graphql-hive/logger'; +import { loggerForRequest } from '@graphql-hive/logger/request'; import type { OnSubgraphExecutePayload } from '@graphql-mesh/fusion-runtime'; import { serializeExecutionRequest } from '@graphql-tools/executor-common'; import type { ExecutionRequest } from '@graphql-tools/utils'; @@ -9,7 +11,6 @@ import { import type { FetchAPI, GraphQLParams, - YogaLogger, Plugin as YogaPlugin, } from 'graphql-yoga'; import jsonStableStringify from 'json-stable-stringify'; @@ -87,24 +88,22 @@ export function useHmacUpstreamSignature( let key$: MaybePromise; let fetchAPI: FetchAPI; let textEncoder: TextEncoder; - let yogaLogger: YogaLogger; return { onYogaInit({ yoga }) { fetchAPI = yoga.fetchAPI; - yogaLogger = yoga.logger; }, onSubgraphExecute({ subgraphName, subgraph, executionRequest, - setExecutionRequest, - logger = yogaLogger, + log: rootLog, }) { - logger?.debug(`running shouldSign for subgraph ${subgraphName}`); + const log = rootLog.child('[useHmacUpstreamSignature] '); + log.debug(`Running shouldSign for subgraph ${subgraphName}`); if (shouldSign({ subgraphName, subgraph, executionRequest })) { - logger?.debug( + log.debug( `shouldSign is true for subgraph ${subgraphName}, signing request`, ); textEncoder ||= new fetchAPI.TextEncoder(); @@ -129,23 +128,24 @@ export function useHmacUpstreamSignature( const extensionValue = fetchAPI.btoa( String.fromCharCode(...new Uint8Array(signature)), ); - logger?.debug( - `produced hmac signature for subgraph ${subgraphName}, signature: ${extensionValue}, signed payload: ${serializedExecutionRequest}`, + log.debug( + { + signature: extensionValue, + payload: serializedExecutionRequest, + }, + `Produced hmac signature for subgraph ${subgraphName}`, ); - setExecutionRequest({ - ...executionRequest, - extensions: { - ...executionRequest.extensions, - [extensionName]: extensionValue, - }, - }); + if (!executionRequest.extensions) { + executionRequest.extensions = {}; + } + executionRequest.extensions[extensionName] = extensionValue; }, ); }, ); } else { - logger?.debug( + log.debug( `shouldSign is false for subgraph ${subgraphName}, skipping hmac signature`, ); } @@ -154,6 +154,7 @@ export function useHmacUpstreamSignature( } export type HMACUpstreamSignatureValidationOptions = { + log: Logger; secret: string; extensionName?: string; serializeParams?: (params: GraphQLParams) => string; @@ -171,22 +172,17 @@ export function useHmacSignatureValidation( const extensionName = options.extensionName || DEFAULT_EXTENSION_NAME; let key$: MaybePromise; let textEncoder: TextEncoder; - let logger: YogaLogger; const paramsSerializer = options.serializeParams || defaultParamsSerializer; return { - onYogaInit({ yoga }) { - logger = yoga.logger; - }, - onParams({ params, fetchAPI }) { + onParams({ params, fetchAPI, request }) { + const log = loggerForRequest(options.log, request).child( + '[useHmacSignatureValidation] ', + ); textEncoder ||= new fetchAPI.TextEncoder(); const extension = params.extensions?.[extensionName]; if (!extension) { - logger.warn( - `Missing HMAC signature: extension ${extensionName} not found in request.`, - ); - throw new Error( `Missing HMAC signature: extension ${extensionName} not found in request.`, ); @@ -206,8 +202,9 @@ export function useHmacSignatureValidation( c.charCodeAt(0), ); const serializedParams = paramsSerializer(params); - logger.debug( - `HMAC signature will be calculate based on serialized params: ${serializedParams}`, + log.debug( + { serializedParams }, + 'HMAC signature will be calculate based on serialized params', ); return handleMaybePromise( @@ -220,10 +217,9 @@ export function useHmacSignatureValidation( ), (result) => { if (!result) { - logger.error( - `HMAC signature does not match the body content. short circuit request.`, + log.error( + 'HMAC signature does not match the body content. short circuit request.', ); - throw new Error( `Invalid HMAC signature: extension ${extensionName} does not match the body content.`, ); diff --git a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts index 2100ecfcc..a14d9c586 100644 --- a/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts +++ b/packages/plugins/hmac-upstream-signature/tests/hmac-upstream-signature.spec.ts @@ -4,6 +4,7 @@ import { GatewayPlugin, useCustomFetch, } from '@graphql-hive/gateway-runtime'; +import { Logger } from '@graphql-hive/logger'; import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; import { MeshFetch } from '@graphql-mesh/types'; import { GraphQLSchema, stripIgnoredCharacters } from 'graphql'; @@ -61,6 +62,7 @@ for (const [name, createConfig] of Object.entries(cases)) { ...createConfig(upstreamSchema), plugins: () => [ useHmacSignatureValidation({ + log: new Logger({ level: false }), secret: 'topSecret', }), useCustomFetch(upstream.fetch as MeshFetch), @@ -119,6 +121,7 @@ for (const [name, createConfig] of Object.entries(cases)) { schema: upstreamSchema, plugins: [ useHmacSignatureValidation({ + log: new Logger({ level: false }), secret: sharedSecret, }), ], diff --git a/packages/plugins/jwt-auth/package.json b/packages/plugins/jwt-auth/package.json index b3e249015..2b81bf1d2 100644 --- a/packages/plugins/jwt-auth/package.json +++ b/packages/plugins/jwt-auth/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/plugins/jwt-auth/src/index.ts b/packages/plugins/jwt-auth/src/index.ts index 001373555..351ebbb82 100644 --- a/packages/plugins/jwt-auth/src/index.ts +++ b/packages/plugins/jwt-auth/src/index.ts @@ -76,7 +76,7 @@ export function useJWT( executionRequest, subgraphName, setExecutionRequest, - logger, + log, }) { if (shouldForward && executionRequest.context?.jwt) { const jwtData: Partial = { @@ -86,9 +86,9 @@ export function useJWT( token: forwardToken ? executionRequest.context.jwt.token : undefined, }; - logger?.debug( - `Forwarding JWT payload to subgraph ${subgraphName}, payload: `, - jwtData.payload, + log.debug( + { payload: jwtData.payload }, + `[useJWT] Forwarding JWT payload to subgraph ${subgraphName}`, ); setExecutionRequest({ diff --git a/packages/plugins/opentelemetry/package.json b/packages/plugins/opentelemetry/package.json index 629821b32..efd9fbc15 100644 --- a/packages/plugins/opentelemetry/package.json +++ b/packages/plugins/opentelemetry/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -29,43 +29,61 @@ "default": "./dist/index.js" } }, + "./setup": { + "require": { + "types": "./dist/setup.d.cts", + "default": "./dist/setup.cjs" + }, + "import": { + "types": "./dist/setup.d.ts", + "default": "./dist/setup.js" + } + }, "./package.json": "./package.json" }, "files": [ "dist" ], "scripts": { - "build": "pkgroll --clean-dist", + "build": "pkgroll --clean-dist && tsx scripts/inject-version", "prepack": "yarn build" }, "peerDependencies": { "graphql": "^15.9.0 || ^16.9.0" }, "dependencies": { - "@azure/monitor-opentelemetry-exporter": "^1.0.0-beta.27", + "@graphql-hive/core": "^0.13.0", "@graphql-hive/gateway-runtime": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-mesh/cross-helpers": "^0.4.10", "@graphql-mesh/transport-common": "workspace:^", "@graphql-mesh/types": "^0.104.7", "@graphql-mesh/utils": "^0.104.7", "@graphql-tools/utils": "^10.9.1", "@opentelemetry/api": "^1.9.0", - "@opentelemetry/core": "^1.30.0", - "@opentelemetry/exporter-trace-otlp-grpc": "^0.57.0", - "@opentelemetry/exporter-trace-otlp-http": "^0.57.0", - "@opentelemetry/exporter-zipkin": "^1.29.0", - "@opentelemetry/instrumentation": "^0.57.0", - "@opentelemetry/resources": "^1.29.0", - "@opentelemetry/sdk-trace-base": "^1.29.0", - "@opentelemetry/sdk-trace-web": "^1.29.0", - "@opentelemetry/semantic-conventions": "^1.28.0", - "@whatwg-node/promise-helpers": "^1.3.0", + "@opentelemetry/api-logs": "^0.203.0", + "@opentelemetry/auto-instrumentations-node": "^0.62.1", + "@opentelemetry/context-async-hooks": "^2.0.1", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.203.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.203.0", + "@opentelemetry/instrumentation": "^0.203.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-logs": "^0.203.0", + "@opentelemetry/sdk-node": "^0.203.0", + "@opentelemetry/sdk-trace-base": "patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch", + "@opentelemetry/semantic-conventions": "^1.36.0", + "@whatwg-node/promise-helpers": "1.3.0", "tslib": "^2.8.1" }, "devDependencies": { + "@whatwg-node/server": "^0.10.0", "graphql": "^16.9.0", "graphql-yoga": "^5.15.1", - "pkgroll": "2.15.0" + "pkgroll": "2.15.0", + "rimraf": "^6.0.1", + "rollup": "^4.41.1", + "tsx": "^4.19.4" }, "sideEffects": false } diff --git a/packages/plugins/opentelemetry/scripts/inject-version.ts b/packages/plugins/opentelemetry/scripts/inject-version.ts new file mode 100644 index 000000000..34bcbdf7d --- /dev/null +++ b/packages/plugins/opentelemetry/scripts/inject-version.ts @@ -0,0 +1,35 @@ +import { readFile, writeFile } from 'node:fs/promises'; +import { resolve } from 'node:path'; +import { fileURLToPath, URL } from 'node:url'; +import pkg from '../package.json'; + +const version = process.argv[2] || pkg.version; + +console.log(`Injecting version ${version} to build and bundle`); + +const source = '// @inject-version globalThis.__OTEL_PLUGIN_VERSION__ here'; +const inject = `globalThis.__OTEL_PLUGIN_VERSION__ = '${version}';`; + +const __dirname = fileURLToPath(new URL('.', import.meta.url)); + +for (const file of [ + // build + resolve(__dirname, '../dist/setup.js'), + resolve(__dirname, '../dist/setup.cjs'), +]) { + try { + const content = await readFile(file, 'utf-8'); + if (content.includes(source)) { + await writeFile(file, content.replace(source, inject)); + } + console.info(`✅ Version injected to "${file}"`); + } catch (e) { + if (Object(e).code === 'ENOENT') { + console.warn( + `⚠️ File does not exist and cannot have the version injected "${file}"`, + ); + } else { + throw e; + } + } +} diff --git a/packages/plugins/opentelemetry/src/async-context-manager.ts b/packages/plugins/opentelemetry/src/async-context-manager.ts new file mode 100644 index 000000000..4e041bf6e --- /dev/null +++ b/packages/plugins/opentelemetry/src/async-context-manager.ts @@ -0,0 +1,6 @@ +import { + AsyncHooksContextManager, + AsyncLocalStorageContextManager, +} from '@opentelemetry/context-async-hooks'; + +export { AsyncHooksContextManager, AsyncLocalStorageContextManager }; diff --git a/packages/plugins/opentelemetry/src/attributes.ts b/packages/plugins/opentelemetry/src/attributes.ts index 09f23fa32..3916ed89f 100644 --- a/packages/plugins/opentelemetry/src/attributes.ts +++ b/packages/plugins/opentelemetry/src/attributes.ts @@ -1,5 +1,9 @@ -// HTTP/network attributes export { + // Basic attributes + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, + + // HTTP/network attributes SEMATTRS_HTTP_CLIENT_IP, SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, @@ -10,8 +14,6 @@ export { SEMATTRS_HTTP_URL, SEMATTRS_HTTP_USER_AGENT, SEMATTRS_NET_HOST_NAME, - ATTR_SERVICE_NAME as SEMRESATTRS_SERVICE_NAME, - ATTR_SERVICE_VERSION, } from '@opentelemetry/semantic-conventions'; // GraphQL-specific attributes @@ -19,8 +21,13 @@ export { export const SEMATTRS_GRAPHQL_DOCUMENT = 'graphql.document'; export const SEMATTRS_GRAPHQL_OPERATION_TYPE = 'graphql.operation.type'; export const SEMATTRS_GRAPHQL_OPERATION_NAME = 'graphql.operation.name'; -export const SEMATTRS_GRAPHQL_ERROR_COUNT = 'graphql.error.count'; +export const SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH = + 'hive.graphql.operation.hash'; +export const SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT = 'hive.graphql.error.count'; +export const SEMATTRS_HIVE_GRAPHQL_ERROR_CODES = 'hive.graphql.error.codes'; // Gateway-specific attributes -export const SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME = - 'gateway.upstream.subgraph.name'; +export const SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME = + 'hive.gateway.upstream.subgraph.name'; +export const SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES = + 'hive.gateway.operation.subgraph.names'; diff --git a/packages/plugins/opentelemetry/src/auto-instrumentations.ts b/packages/plugins/opentelemetry/src/auto-instrumentations.ts new file mode 100644 index 000000000..11be919b3 --- /dev/null +++ b/packages/plugins/opentelemetry/src/auto-instrumentations.ts @@ -0,0 +1,9 @@ +import { + getNodeAutoInstrumentations, + getResourceDetectors, + InstrumentationConfigMap, +} from '@opentelemetry/auto-instrumentations-node'; + +export type { InstrumentationConfigMap }; + +export { getNodeAutoInstrumentations, getResourceDetectors }; diff --git a/packages/plugins/opentelemetry/src/context.ts b/packages/plugins/opentelemetry/src/context.ts new file mode 100644 index 000000000..f4bae5c2b --- /dev/null +++ b/packages/plugins/opentelemetry/src/context.ts @@ -0,0 +1,42 @@ +import { trace, type Context } from '@opentelemetry/api'; + +type Node = { + ctx: Context; + previous?: Node; +}; + +export class OtelContextStack { + #root: Node; + #current: Node; + + constructor(root: Context) { + this.#root = { ctx: root }; + this.#current = this.#root; + } + + get current(): Context { + return this.#current.ctx; + } + + get root(): Context { + return this.#root.ctx; + } + + push = (ctx: Context) => { + this.#current = { ctx, previous: this.#current }; + }; + + pop = () => { + this.#current = this.#current.previous ?? this.#root; + }; + + toString() { + let node: Node | undefined = this.#current; + const names = []; + while (node != undefined) { + names.push((trace.getSpan(node.ctx) as unknown as { name: string }).name); + node = node.previous; + } + return names.join(' -> '); + } +} diff --git a/packages/plugins/opentelemetry/src/exporter-trace-otlp-grpc.ts b/packages/plugins/opentelemetry/src/exporter-trace-otlp-grpc.ts new file mode 100644 index 000000000..372deae69 --- /dev/null +++ b/packages/plugins/opentelemetry/src/exporter-trace-otlp-grpc.ts @@ -0,0 +1,3 @@ +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-grpc'; + +export { OTLPTraceExporter }; diff --git a/packages/plugins/opentelemetry/src/gobal.d.ts b/packages/plugins/opentelemetry/src/gobal.d.ts new file mode 100644 index 000000000..d15e15116 --- /dev/null +++ b/packages/plugins/opentelemetry/src/gobal.d.ts @@ -0,0 +1,6 @@ +export {}; + +declare global { + /** Gets injected during build by `scripts/inject-version.ts`. */ + var __OTEL_PLUGIN_VERSION__: string | undefined; +} diff --git a/packages/plugins/opentelemetry/src/hive-span-processor.ts b/packages/plugins/opentelemetry/src/hive-span-processor.ts new file mode 100644 index 000000000..5ea6a08ad --- /dev/null +++ b/packages/plugins/opentelemetry/src/hive-span-processor.ts @@ -0,0 +1,198 @@ +import { Context } from '@opentelemetry/api'; +import { hrTimeDuration } from '@opentelemetry/core'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { + BatchSpanProcessor, + BufferConfig, + Span, + SpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import type { SpanImpl } from '@opentelemetry/sdk-trace-base/build/src/Span'; +import { SEMATTRS_HTTP_METHOD } from '@opentelemetry/semantic-conventions'; +import { + SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, + SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, + SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, +} from './attributes'; + +export type HiveTracingSpanProcessorOptions = + | { + target: string; + accessToken: string; + endpoint: string; + batching?: BufferConfig; + processor?: never; + } + | { + processor: SpanProcessor; + }; + +type TraceState = { + traceId: string; + rootId: string; + operationRoots: Map; + subgraphExecutions: Map; + httpSpan: SpanImpl; +}; + +export class HiveTracingSpanProcessor implements SpanProcessor { + private traceStateById: Map = new Map(); + private processor: SpanProcessor; + + constructor(config: HiveTracingSpanProcessorOptions) { + if (config.processor) { + this.processor = config.processor; + } else { + this.processor = new BatchSpanProcessor( + new OTLPTraceExporter({ + url: config.endpoint, + headers: { + Authorization: `Bearer ${config.accessToken}`, + 'X-Hive-Target-Ref': config.target, + }, + }), + config.batching, + ); + } + } + + onStart(span: Span, parentContext: Context): void { + this.processor.onStart(span, parentContext); + const { spanId, traceId } = span.spanContext(); + const parentId = span.parentSpanContext?.spanId; + + if (isHttpSpan(span)) { + this.traceStateById.set(traceId, { + traceId, + rootId: spanId, + httpSpan: span as SpanImpl, + operationRoots: new Map(), + subgraphExecutions: new Map(), + }); + return; + } + + const traceState = this.traceStateById.get(traceId); + if (!traceState || !parentId) { + // This is not an HTTP trace, ignore it + return; + } + + if (span.name.startsWith('graphql.operation')) { + traceState?.operationRoots.set(spanId, span as SpanImpl); + return; + } + + const operationRoot = traceState.operationRoots.get(parentId); + if (operationRoot) { + // Set the root for children + traceState.operationRoots.set(spanId, operationRoot); + } + + if (span.name.startsWith('subgraph.execute')) { + traceState.subgraphExecutions.set(spanId, span as SpanImpl); + return; + } + + const subgraphExecution = traceState.subgraphExecutions.get(parentId); + if (subgraphExecution) { + // Set the root for children + traceState.subgraphExecutions.set(spanId, subgraphExecution); + } + } + + onEnd(span: Span): void { + const { traceId, spanId } = span.spanContext(); + const traceState = this.traceStateById.get(traceId); + + if (!traceState) { + // Skip, this is not an HTTP trace + return; + } + + if (traceState.rootId === spanId) { + // Clean up trace state early to avoid any memory leak in case of error thrown + this.traceStateById.delete(traceId); + + for (let operationSpan of new Set(traceState.operationRoots.values())) { + // @ts-expect-error set the start time to the HTTP start time, so that operation span replaces http span + operationSpan.startTime = span.startTime; + operationSpan.endTime = span.endTime; + // @ts-expect-error set the duration time + operationSpan._duration = hrTimeDuration( + operationSpan.startTime, + operationSpan.endTime, + ); + // @ts-expect-error Remove the parenting, so that this span appears as a root span for Hive + operationSpan.parentSpanContext = null; + + // Copy HTTP attributes + for (const attr in span.attributes) { + operationSpan.attributes[attr] ??= span.attributes[attr]; + } + + // Now that operation spans have been updated, we can report it + this.processor.onEnd(operationSpan); + } + + // This is the HTTP, don't report it, we report only the graphql operation + return; + } + + const operationSpan = traceState.operationRoots.get(spanId); + if (!operationSpan) { + // If the operation span is not found, it is probably not related to any request (init, schema loading...). + return; + } + + if (operationSpan === span) { + // It is an operation span, we don't want to report it yet, + // it has to be updated at HTTP end time. + return; + } + + if (span.name === 'graphql.execute') { + copyAttribute(span, operationSpan, SEMATTRS_HIVE_GRAPHQL_ERROR_CODES); + copyAttribute(span, operationSpan, SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT); + copyAttribute( + span, + operationSpan, + SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, + ); + } + + const subgraphExecution = traceState.subgraphExecutions.get(spanId); + if (span.name === 'http.fetch' && subgraphExecution) { + for (const attr in span.attributes) { + subgraphExecution.attributes[attr] ??= span.attributes[attr]; + } + } + + // Report all spans that belongs to an operation span + this.processor.onEnd(span); + } + + async forceFlush(): Promise { + return this.processor.forceFlush(); + } + + async shutdown(): Promise { + // Clean up resources when shutting down + await this.forceFlush(); + this.traceStateById.clear(); + return this.processor.shutdown(); + } +} + +function isHttpSpan(span: Span): boolean { + return !!span.attributes[SEMATTRS_HTTP_METHOD]; +} + +function copyAttribute( + source: Span, + target: Span, + sourceAttrName: string, + targetAttrName: string = sourceAttrName, +) { + target.attributes[targetAttrName] = source.attributes[sourceAttrName]; +} diff --git a/packages/plugins/opentelemetry/src/index.ts b/packages/plugins/opentelemetry/src/index.ts index 04669571b..667af3395 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -1,5 +1,18 @@ -export * from './processors'; -export { +import { DiagLogLevel } from '@opentelemetry/api'; +import { useOpenTelemetry, - type OpenTelemetryGatewayPluginOptions as OpenTelemetryMeshPluginOptions, + type OpenTelemetryGatewayPluginOptions, + type OpenTelemetryPlugin, + type OpenTelemetryPluginUtils, } from './plugin'; + +export * from './attributes'; + +export const OpenTelemetryDiagLogLevel = DiagLogLevel; + +export { + useOpenTelemetry, + OpenTelemetryPlugin, + OpenTelemetryGatewayPluginOptions, + OpenTelemetryPluginUtils, +}; diff --git a/packages/plugins/opentelemetry/src/log-writer.ts b/packages/plugins/opentelemetry/src/log-writer.ts new file mode 100644 index 000000000..f14be8cf2 --- /dev/null +++ b/packages/plugins/opentelemetry/src/log-writer.ts @@ -0,0 +1,151 @@ +import { Attributes, LogLevel, LogWriter } from '@graphql-hive/logger'; +import { Context, context, ROOT_CONTEXT } from '@opentelemetry/api'; +import { logs, SeverityNumber, type Logger } from '@opentelemetry/api-logs'; +import { Resource } from '@opentelemetry/resources'; +import { + BatchLogRecordProcessor, + ConsoleLogRecordExporter, + LoggerProvider, + LogRecordExporter, + LogRecordLimits, + LogRecordProcessor, + SimpleLogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { BufferConfig } from '@opentelemetry/sdk-trace-base'; +import { otelCtxForRequestId } from './plugin'; + +type ProcessorOptions = { + forceFlushTimeoutMillis?: number; + logRecordLimits?: LogRecordLimits; + resource?: Resource; + console?: boolean; +}; + +export type OpenTelemetryLogWriterSetupOptions = + | { + logger: Logger; + } + | { + provider: LoggerProvider; + } + | (ProcessorOptions & + ( + | { + processors: LogRecordProcessor[]; + exporter?: never; + } + | { + exporter: LogRecordExporter; + batching?: boolean | BufferConfig; + processors?: never; + } + | { + console: boolean; + processors?: never; + exporter?: never; + } + )); + +export type OpenTelemetryLogWriterOptions = + OpenTelemetryLogWriterSetupOptions & { + useContextManager?: boolean; + }; + +export class OpenTelemetryLogWriter implements LogWriter { + private logger: Logger; + private useContextManager: boolean; + + constructor(options: OpenTelemetryLogWriterOptions) { + this.useContextManager = options.useContextManager ?? true; + + if ('logger' in options) { + this.logger = options.logger; + return; + } + + if ('provider' in options) { + if ( + 'register' in options.provider && + typeof options.provider.register === 'function' + ) { + options.provider.register(); + } else { + logs.setGlobalLoggerProvider(options.provider); + } + } else { + const processors = options.processors ?? []; + + if (options.exporter) { + if (options.batching !== false) { + processors.push( + new BatchLogRecordProcessor( + options.exporter, + options.batching === true ? {} : options.batching, + ), + ); + } + processors.push(new SimpleLogRecordProcessor(options.exporter)); + } + + if (options.console) { + processors.push( + new SimpleLogRecordProcessor(new ConsoleLogRecordExporter()), + ); + } + + logs.setGlobalLoggerProvider( + new LoggerProvider({ + ...options, + processors, + }), + ); + } + + this.logger = logs.getLogger('gateway'); + } + + flush(): void | Promise { + const provider = logs.getLoggerProvider(); + if ('forceFlush' in provider && typeof provider.forceFlush === 'function') { + provider.forceFlush(); + } + } + + write( + level: LogLevel, + attrs: Attributes | null | undefined, + msg: string | null | undefined, + ): void | Promise { + const attributes = Array.isArray(attrs) + ? { ...attrs } + : (attrs ?? undefined); + + return this.logger.emit({ + body: msg, + attributes: attributes, + severityNumber: HIVE_LOG_LEVEL_NUMBERS[level], + severityText: level, + context: this.useContextManager + ? context.active() + : getContextForRequest(attributes), + }); + } +} + +export const HIVE_LOG_LEVEL_NUMBERS = { + trace: SeverityNumber.TRACE, + debug: SeverityNumber.DEBUG, + info: SeverityNumber.INFO, + warn: SeverityNumber.WARN, + error: SeverityNumber.ERROR, +}; + +export function getContextForRequest(attributes?: { + requestId?: string; +}): Context { + if (!attributes?.requestId) { + return ROOT_CONTEXT; + } + + return otelCtxForRequestId.get(attributes.requestId) ?? ROOT_CONTEXT; +} diff --git a/packages/plugins/opentelemetry/src/plugin-utils.ts b/packages/plugins/opentelemetry/src/plugin-utils.ts new file mode 100644 index 000000000..3dffe8f21 --- /dev/null +++ b/packages/plugins/opentelemetry/src/plugin-utils.ts @@ -0,0 +1,171 @@ +import type { ExecutionRequest } from '@graphql-tools/utils'; +import { GenericInstrumentation } from 'graphql-yoga'; + +export function withState< + P extends { instrumentation?: GenericInstrumentation }, + HttpState = object, + GraphqlState = object, + SubExecState = object, +>( + pluginFactory: ( + getState: ( + payload: SP, + ) => PayloadWithState['state'], + ) => PluginWithState, +): P { + const states: { + forRequest?: WeakMap>; + forOperation?: WeakMap>; + forSubgraphExecution?: WeakMap>; + } = {}; + + function getProp(scope: keyof typeof states, key: any): PropertyDescriptor { + return { + get() { + if (!states[scope]) states[scope] = new WeakMap(); + let value = states[scope].get(key as any); + if (!value) states[scope].set(key, (value = {})); + return value; + }, + enumerable: true, + }; + } + + function getState(payload: Payload) { + let { executionRequest, context, request } = payload; + const state = {}; + const defineState = (scope: keyof typeof states, key: any) => + Object.defineProperty(state, scope, getProp(scope, key)); + + if (executionRequest) { + defineState('forSubgraphExecution', executionRequest); + // ExecutionRequest can happen outside of any Graphql Operation for Gateway internal usage like Introspection queries. + // We check for `params` to be prensent, which means it's actually a GraphQL context. + if (executionRequest.context?.params) context = executionRequest.context; + } + if (context) { + defineState('forOperation', context); + if (context.request) request = context.request; + } + if (request) { + defineState('forRequest', request); + } + return state; + } + + function addStateGetters(src: any) { + const result: any = {}; + // Use the property descriptors to keep potential getters and setters, or not enumerable props + const properties = Object.entries(Object.getOwnPropertyDescriptors(src)); + for (const [hookName, descriptor] of properties) { + const hook = descriptor.value; + if (typeof hook !== 'function') { + descriptor.get &&= () => src[hookName]; + descriptor.set &&= (value) => { + src[hookName] = value; + }; + Object.defineProperty(result, hookName, descriptor); + } else { + result[hookName] = { + [hook.name](payload: any, ...args: any[]) { + return hook( + { + ...payload, + get state() { + return getState(payload); + }, + }, + ...args, + ); + }, + }[hook.name]; + } + } + return result; + } + + const plugin = pluginFactory(getState as any); + + const pluginWithState = addStateGetters(plugin); + pluginWithState.instrumentation = addStateGetters(plugin.instrumentation); + + return pluginWithState as P; +} + +export type HttpState = { + forRequest: Partial; +}; + +export type GraphQLState = { + forOperation: Partial; +}; + +export type GatewayState = { + forSubgraphExecution: Partial; +}; + +export function getMostSpecificState( + state: Partial & GraphQLState & GatewayState> = {}, +): Partial | undefined { + const { forOperation, forRequest, forSubgraphExecution } = state; + return forSubgraphExecution ?? forOperation ?? forRequest; +} + +type Payload = { + request?: Request; + context?: any; + executionRequest?: ExecutionRequest; +}; + +// Brace yourself! TS Wizardry is coming! + +type PayloadWithState = T extends { + executionRequest: any; +} + ? T & { + state: Partial & GraphQLState> & + GatewayState; + } + : T extends { + executionRequest?: any; + } + ? T & { + state: Partial< + HttpState & GraphQLState & GatewayState + >; + } + : T extends { context: any } + ? T & { state: HttpState & GraphQLState } + : T extends { request: any } + ? T & { state: HttpState } + : T extends { request?: any } + ? T & { state: Partial> } + : T; + +export type PluginWithState< + P, + Http = object, + GraphQL = object, + Gateway = object, +> = { + [K in keyof P]: K extends 'instrumentation' + ? P[K] extends infer Instrumentation | undefined + ? { + [I in keyof Instrumentation]: Instrumentation[I] extends + | ((payload: infer IP, ...args: infer Args) => infer IR) + | undefined + ? + | (( + payload: PayloadWithState, + ...args: Args + ) => IR) + | undefined + : Instrumentation[I]; + } + : P[K] + : P[K] extends ((payload: infer T) => infer R) | undefined + ? + | ((payload: PayloadWithState) => R) + | undefined + : P[K]; +}; diff --git a/packages/plugins/opentelemetry/src/plugin.ts b/packages/plugins/opentelemetry/src/plugin.ts index 1149ad7ca..9db2abd3b 100644 --- a/packages/plugins/opentelemetry/src/plugin.ts +++ b/packages/plugins/opentelemetry/src/plugin.ts @@ -1,145 +1,210 @@ import { - type OnExecuteEventPayload, - type OnParseEventPayload, - type OnValidateEventPayload, -} from '@envelop/types'; -import { type GatewayPlugin } from '@graphql-hive/gateway-runtime'; -import type { OnSubgraphExecutePayload } from '@graphql-mesh/fusion-runtime'; -import type { Logger, OnFetchHookPayload } from '@graphql-mesh/types'; -import { getHeadersObj } from '@graphql-mesh/utils'; + Attributes, + GatewayConfigContext, + getRetryInfo, + isRetryExecutionRequest, + Logger, + type GatewayPlugin, +} from '@graphql-hive/gateway-runtime'; import { - fakePromise, - isAsyncIterable, - MaybePromise, -} from '@graphql-tools/utils'; + loggerForRequest, + requestIdByRequest, +} from '@graphql-hive/logger/request'; +import { getHeadersObj } from '@graphql-mesh/utils'; +import { ExecutionRequest, fakePromise } from '@graphql-tools/utils'; import { context, diag, - DiagLogLevel, propagation, + ROOT_CONTEXT, trace, type Context, type TextMapGetter, type Tracer, } from '@opentelemetry/api'; import { setGlobalErrorHandler } from '@opentelemetry/core'; -import { Resource } from '@opentelemetry/resources'; -import { type SpanProcessor } from '@opentelemetry/sdk-trace-base'; -import { WebTracerProvider } from '@opentelemetry/sdk-trace-web'; -import { handleMaybePromise } from '@whatwg-node/promise-helpers'; -import type { OnRequestEventPayload } from '@whatwg-node/server'; -import { ATTR_SERVICE_VERSION, SEMRESATTRS_SERVICE_NAME } from './attributes'; +import { unfakePromise } from '@whatwg-node/promise-helpers'; +import { OtelContextStack } from './context'; import { - completeHttpSpan, + getMostSpecificState, + withState, + type GatewayState, + type GraphQLState, + type HttpState, +} from './plugin-utils'; +import { + createGraphqlContextBuildingSpan, createGraphQLExecuteSpan, createGraphQLParseSpan, + createGraphQLSpan, createGraphQLValidateSpan, createHttpSpan, - createSubgraphExecuteFetchSpan, + createSchemaLoadingSpan, + createSubgraphExecuteSpan, createUpstreamHttpFetchSpan, + OperationHashingFn, + recordCacheError, + recordCacheEvent, + registerException, + setExecutionAttributesOnOperationSpan, + setExecutionResultAttributes, + setGraphQLExecutionAttributes, + setGraphQLExecutionResultAttributes, + setGraphQLParseAttributes, + setGraphQLValidateAttributes, + setParamsAttributes, + setResponseAttributes, + setSchemaAttributes, + setUpstreamFetchAttributes, + setUpstreamFetchResponseAttributes, } from './spans'; +import { + diagLogLevelFromEnv, + isContextManagerCompatibleWithAsync, +} from './utils'; -type PrimitiveOrEvaluated = - | TExpectedResult - | ((input: TInput) => TExpectedResult); +const initializationTime = + 'performance' in globalThis ? performance.now() : undefined; -interface OpenTelemetryGatewayPluginOptionsWithoutInit { +type BooleanOrPredicate = + | boolean + | ((input: TInput) => boolean); + +export type OpenTelemetryGatewayPluginOptions = { /** - * Whether to initialize the OpenTelemetry SDK (default: true). + * Whether to rely on OTEL context api for span correlation. + * - `true`: the plugin will rely on OTEL context manager for span parenting. + * - `false`: the plugin will rely on request context for span parenting, + * which implies that parenting with user defined may be broken. + * + * By default, it is enabled if the registered Context Manager is compatible with async calls, + * or if it is possible to register an `AsyncLocalStorageContextManager`. + * + * Note: If `true`, an error is thrown if it fails to obtain an async calls compatible Context Manager. */ - initializeNodeSDK: false; -} - -interface OpenTelemetryGatewayPluginOptionsWithInit { + useContextManager?: boolean; /** - * Whether to initialize the OpenTelemetry SDK (default: true). + * Whether to inherit the context from the calling service (default: true). + * + * This process is done by extracting the context from the incoming request headers. If disabled, a new context and a trace-id will be created. + * + * See https://opentelemetry.io/docs/languages/js/propagation/ */ - initializeNodeSDK?: true; + inheritContext?: boolean; /** - * A list of OpenTelemetry exporters to use for exporting the spans. - * You can use exporters from `@opentelemetry/exporter-*` packages, or use the built-in utility functions. + * Whether to propagate the context to the outgoing requests (default: true). + * + * This process is done by injecting the context into the outgoing request headers. If disabled, the context will not be propagated. * - * Does not apply when `initializeNodeSDK` is `false`. + * See https://opentelemetry.io/docs/languages/js/propagation/ */ - exporters: MaybePromise[]; + propagateContext?: boolean; /** - * Service name to use for OpenTelemetry NodeSDK resource option (default: 'Gateway'). + * Configure Opentelemetry `diag` API to use Gateway's logger. * - * Does not apply when `initializeNodeSDK` is `false`. + * @default true + + * Note: Logger configuration respects OTEL environment variables standard. + * This means that the logger will be enabled only if `OTEL_LOG_LEVEL` variable is set. */ - serviceName?: string; -} + configureDiagLogger?: boolean; + /** + * The TraceProvider method to call on Gateway's disposal. By default, it tries to run `forceFlush` method on + * the registered trace provider if it exists. + * Set to `false` to disable this behavior. + * @default 'forceFlush' + */ + flushOnDispose?: string | false; + /** + * Function to be used to compute the hash of each operation (graphql.operation.hash attribute). + * Note: pass `null` to disable operation hashing + * + * @default `hashOperation` from @graphql-hive/core + */ + hashOperation?: OperationHashingFn | null; + /** + * Tracing configuration + */ + traces?: + | boolean + | { + /** + * Tracer instance to use for creating spans (default: a tracer with name 'gateway'). + */ + tracer?: Tracer; + /** + * Options to control which spans to create. + * By default, all spans are enabled. + * + * You may specify a boolean value to enable/disable all spans, or a function to dynamically enable/disable spans based on the input. + */ + spans?: { + /** + * Enable/disable HTTP request spans (default: true). + * + * Disabling the HTTP span will also disable all other child spans. + */ + http?: BooleanOrPredicate<{ request: Request }>; + /** + * Enable/disable GraphQL operation spans (default: true). + * + * Disabling the GraphQL operation spa will also disable all other child spans. + */ + graphql?: BooleanOrPredicate<{ context: unknown }>; // FIXME: better type for graphql context + /** + * Enable/disable GraphQL context building phase (default: true). + */ + graphqlContextBuilding?: BooleanOrPredicate<{ context: unknown }>; // FIXME: better type for graphql context + /** + * Enable/disable GraphQL parse spans (default: true). + */ + graphqlParse?: BooleanOrPredicate<{ context: unknown }>; // FIXME: better type for graphql context + /** + * Enable/disable GraphQL validate spans (default: true). + */ + graphqlValidate?: BooleanOrPredicate<{ context: unknown }>; + /** + * Enable/disable GraphQL execute spans (default: true). + * + * Disabling the GraphQL execute spans will also disable all other child spans. + */ + graphqlExecute?: BooleanOrPredicate<{ context: unknown }>; + /** + * Enable/disable subgraph execute spans (default: true). + * + * Disabling the subgraph execute spans will also disable all other child spans. + */ + subgraphExecute?: BooleanOrPredicate<{ + executionRequest: ExecutionRequest; + subgraphName: string; + }>; + /** + * Enable/disable upstream HTTP fetch calls spans (default: true). + */ + upstreamFetch?: BooleanOrPredicate<{ + executionRequest: ExecutionRequest | undefined; + }>; + /** + * Enable/disable schema loading spans (default: true if context manager available). + * + * Note: This span requires an Async compatible context manager + */ + schema?: boolean; + /** + * Enable/disable initialization span (default: true). + */ + initialization?: boolean; + }; + events?: { + /** + * Enable/Disable cache related span events (default: true). + */ + cache?: BooleanOrPredicate<{ key: string; action: 'read' | 'write' }>; + }; + }; +}; -type OpenTelemetryGatewayPluginOptionsInit = - | OpenTelemetryGatewayPluginOptionsWithInit - | OpenTelemetryGatewayPluginOptionsWithoutInit; - -export type OpenTelemetryGatewayPluginOptions = - OpenTelemetryGatewayPluginOptionsInit & { - /** - * Tracer instance to use for creating spans (default: a tracer with name 'gateway'). - */ - tracer?: Tracer; - /** - * Whether to inherit the context from the calling service (default: true). - * - * This process is done by extracting the context from the incoming request headers. If disabled, a new context and a trace-id will be created. - * - * See https://opentelemetry.io/docs/languages/js/propagation/ - */ - inheritContext?: boolean; - /** - * Whether to propagate the context to the outgoing requests (default: true). - * - * This process is done by injecting the context into the outgoing request headers. If disabled, the context will not be propagated. - * - * See https://opentelemetry.io/docs/languages/js/propagation/ - */ - propagateContext?: boolean; - /** - * Options to control which spans to create. - * By default, all spans are enabled. - * - * You may specify a boolean value to enable/disable all spans, or a function to dynamically enable/disable spans based on the input. - */ - spans?: { - /** - * Enable/disable HTTP request spans (default: true). - * - * Disabling the HTTP span will also disable all other child spans. - */ - http?: PrimitiveOrEvaluated>; - /** - * Enable/disable GraphQL parse spans (default: true). - */ - graphqlParse?: PrimitiveOrEvaluated>; - /** - * Enable/disable GraphQL validate spans (default: true). - */ - graphqlValidate?: PrimitiveOrEvaluated< - boolean, - OnValidateEventPayload - >; - /** - * Enable/disable GraphQL execute spans (default: true). - */ - graphqlExecute?: PrimitiveOrEvaluated< - boolean, - OnExecuteEventPayload - >; - /** - * Enable/disable subgraph execute spans (default: true). - */ - subgraphExecute?: PrimitiveOrEvaluated< - boolean, - OnSubgraphExecutePayload - >; - /** - * Enable/disable upstream HTTP fetch calls spans (default: true). - */ - upstreamFetch?: PrimitiveOrEvaluated>; - }; - }; +export const otelCtxForRequestId = new Map(); const HeadersTextMapGetter: TextMapGetter = { keys(carrier) { @@ -150,297 +215,764 @@ const HeadersTextMapGetter: TextMapGetter = { }, }; -export function useOpenTelemetry( - options: OpenTelemetryGatewayPluginOptions & { logger: Logger }, -): GatewayPlugin<{ - opentelemetry: { +export type ContextMatcher = { + request?: Request; + context?: any; + executionRequest?: ExecutionRequest; +}; + +export type OpenTelemetryPluginUtils = { + tracer?: Tracer; + getActiveContext: (payload: ContextMatcher) => Context; + getHttpContext: (request: Request) => Context | undefined; + getOperationContext: (context: any) => Context | undefined; + getExecutionRequestContext: ( + ExecutionRequest: ExecutionRequest, + ) => Context | undefined; +}; + +export type OpenTelemetryContextExtension = { + openTelemetry: { tracer: Tracer; - activeContext: () => Context; + getActiveContext: (payload?: ContextMatcher) => Context; + getHttpContext: (request?: Request) => Context | undefined; + getOperationContext: (context?: any) => Context | undefined; + getExecutionRequestContext: ( + ExecutionRequest: ExecutionRequest, + ) => Context | undefined; }; -}> { +}; + +type OtelState = { + otel: OtelContextStack; +}; + +type State = Partial< + HttpState & GraphQLState & GatewayState +>; + +export type OpenTelemetryPlugin = GatewayPlugin & + OpenTelemetryPluginUtils; + +export function useOpenTelemetry( + options: OpenTelemetryGatewayPluginOptions & + // We ask for a Partial context to still allow the usage as a Yoga plugin + Partial, +): OpenTelemetryPlugin { const inheritContext = options.inheritContext ?? true; const propagateContext = options.propagateContext ?? true; + let useContextManager: boolean; + const traces = typeof options.traces === 'object' ? options.traces : {}; + + let tracer: Tracer; + let initSpan: Context | null; + + // TODO: Make it const once Yoga has the Hive Logger + let pluginLogger: Logger | undefined = + options.log && options.log.child('[OpenTelemetry] '); - const requestContextMapping = new WeakMap(); - const contextMapping = new WeakMap(); - function getOTELContext( - context: any, - request?: Request, - ): Context | undefined { - let otelContext: Context | undefined; - if (request) { - otelContext = requestContextMapping.get(request); + function isParentEnabled(state: State): boolean { + const parentState = getMostSpecificState(state); + return !parentState || !!parentState.otel; + } + + function getContext(state?: State): Context { + const specificState = getMostSpecificState(state)?.otel; + + if (initSpan && !specificState) { + return initSpan; } - if (!otelContext && context?.request) { - otelContext = requestContextMapping.get(context.request); + + if (useContextManager) { + return context.active(); } - if (!otelContext && context) { - otelContext = contextMapping.get(context); + + return specificState?.current ?? ROOT_CONTEXT; + } + + let preparation$ = init(); + preparation$.then(() => { + preparation$ = fakePromise(); + }); + + async function init() { + if ( + options.useContextManager !== false && + !(await isContextManagerCompatibleWithAsync()) + ) { + useContextManager = false; + if (options.useContextManager === true) { + throw new Error( + '[OTEL] Context Manager usage is enabled, but the registered one is not compatible with async calls.' + + ' Please use another context manager, such as `AsyncLocalStorageContextManager`.', + ); + } + } else { + useContextManager = options.useContextManager ?? true; + } + + tracer = traces.tracer || trace.getTracer('gateway'); + + initSpan = trace.setSpan( + context.active(), + tracer.startSpan('gateway.initialization', { + startTime: initializationTime, + }), + ); + + if (!useContextManager) { + if (traces.spans?.schema) { + pluginLogger?.warn( + 'Schema loading spans are disabled because no context manager is available', + ); + } + + traces.spans = traces.spans ?? {}; + traces.spans.schema = false; } - return otelContext; } - let tracer: Tracer; - let spanProcessors: SpanProcessor[]; - let serviceName: string = 'Gateway'; - let provider: WebTracerProvider; + const plugin = withState< + OpenTelemetryPlugin, + OtelState, + OtelState & { skipExecuteSpan?: true; subgraphNames: string[] }, + OtelState + >((getState) => ({ + get tracer() { + return tracer; + }, + getActiveContext: ({ state }) => getContext(state), + getHttpContext: (request) => { + return getState({ request }).forRequest.otel?.root; + }, + getOperationContext: (context) => { + return getState({ context }).forOperation.otel?.root; + }, + getExecutionRequestContext: (executionRequest) => { + return getState({ executionRequest }).forSubgraphExecution.otel?.root; + }, + instrumentation: { + request({ state: { forRequest }, request }, wrapped) { + if (!shouldTrace(traces.spans?.http, { request })) { + return wrapped(); + } - let preparation$: Promise | undefined; + const url = getURL(request); + + return unfakePromise( + preparation$ + .then(() => { + const ctx = inheritContext + ? propagation.extract( + context.active(), + request.headers, + HeadersTextMapGetter, + ) + : context.active(); + + forRequest.otel = new OtelContextStack( + createHttpSpan({ ctx, request, tracer, url }).ctx, + ); + + if (useContextManager) { + wrapped = context.bind(forRequest.otel.current, wrapped); + } + + return wrapped(); + }) + .catch((error) => { + registerException(forRequest.otel?.current, error); + throw error; + }) + .finally(() => { + const ctx = forRequest.otel?.root; + ctx && trace.getSpan(ctx)?.end(); + }), + ); + }, - return { - onYogaInit({ yoga }) { - preparation$ = fakePromise(undefined).then(async () => { + operation( + { context: gqlCtx, state: { forOperation, ...parentState } }, + wrapped, + ) { + if ( + !isParentEnabled(parentState) || + !shouldTrace(traces.spans?.graphql, { context: gqlCtx }) + ) { + return wrapped(); + } + + return unfakePromise( + preparation$.then(() => { + const ctx = getContext(parentState); + forOperation.otel = new OtelContextStack( + createGraphQLSpan({ tracer, ctx }), + ); + + if (useContextManager) { + wrapped = context.bind(forOperation.otel.current, wrapped); + } + + return fakePromise() + .then(wrapped) + .catch((err) => { + registerException(forOperation.otel?.current, err); + throw err; + }) + .finally(() => trace.getSpan(forOperation.otel!.current)?.end()); + }), + ); + }, + + context({ state, context: gqlCtx }, wrapped) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlContextBuilding, { + context: gqlCtx, + }) + ) { + return wrapped(); + } + + const { forOperation } = state; + const ctx = getContext(state); + forOperation.otel!.push( + createGraphqlContextBuildingSpan({ ctx, tracer }), + ); + + if (useContextManager) { + wrapped = context.bind(forOperation.otel!.current, wrapped); + } + + try { + wrapped(); + } catch (err) { + registerException(forOperation.otel?.current, err); + throw err; + } finally { + trace.getSpan(forOperation.otel!.current)?.end(); + forOperation.otel!.pop(); + } + }, + + parse({ state, context: gqlCtx }, wrapped) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlParse, { context: gqlCtx }) + ) { + return wrapped(); + } + + const ctx = getContext(state); + const { forOperation } = state; + forOperation.otel!.push(createGraphQLParseSpan({ ctx, tracer })); + + if (useContextManager) { + wrapped = context.bind(forOperation.otel!.current, wrapped); + } + + try { + wrapped(); + } catch (err) { + registerException(forOperation.otel!.current, err); + throw err; + } finally { + trace.getSpan(forOperation.otel!.current)?.end(); + forOperation.otel!.pop(); + } + }, + + validate({ state, context: gqlCtx }, wrapped) { if ( - !( - 'initializeNodeSDK' in options && - options.initializeNodeSDK === false + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlValidate, { context: gqlCtx }) + ) { + return wrapped(); + } + + const { forOperation } = state; + forOperation.otel!.push( + createGraphQLValidateSpan({ + ctx: getContext(state), + tracer, + query: gqlCtx.params.query?.trim(), + operationName: gqlCtx.params.operationName, + }), + ); + + if (useContextManager) { + wrapped = context.bind(forOperation.otel!.current, wrapped); + } + + try { + wrapped(); + } catch (err) { + registerException(forOperation.otel?.current, err); + throw err; + } finally { + trace.getSpan(forOperation.otel!.current)?.end(); + forOperation.otel!.pop(); + } + }, + + execute({ state, context: gqlCtx }, wrapped) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlExecute, { context: gqlCtx }) + ) { + // Other parenting skipping are marked by the fact that `otel` is undefined in the state + // For execute, there is no specific state, so we keep track of it here. + state.forOperation.skipExecuteSpan = true; + return wrapped(); + } + + const ctx = getContext(state); + const { forOperation } = state; + forOperation.otel?.push(createGraphQLExecuteSpan({ ctx, tracer })); + + if (useContextManager) { + wrapped = context.bind(forOperation.otel!.current, wrapped); + } + + return unfakePromise( + fakePromise() + .then(wrapped) + .catch((err) => { + registerException(forOperation.otel!.current, err); + throw err; + }) + .finally(() => { + trace.getSpan(forOperation.otel!.current)?.end(); + forOperation.otel!.pop(); + }), + ); + }, + + subgraphExecute( + { + state: { forSubgraphExecution, ...parentState }, + executionRequest, + subgraphName, + }, + wrapped, + ) { + const isIntrospection = !executionRequest.context.params; + + if ( + !isParentEnabled(parentState) || + parentState.forOperation?.skipExecuteSpan || + !shouldTrace( + isIntrospection + ? traces.spans?.schema + : traces.spans?.subgraphExecute, + { + subgraphName, + executionRequest, + }, ) ) { - if (options.serviceName) { - serviceName = options.serviceName; - } - if (options.exporters) { - spanProcessors = await Promise.all(options.exporters); - } - const webProvider = new WebTracerProvider({ - resource: new Resource({ - [SEMRESATTRS_SERVICE_NAME]: serviceName, - [ATTR_SERVICE_VERSION]: yoga.version, + return wrapped(); + } + + // If a subgraph execution request doesn't belong to a graphql operation + // (such as Introspection requests in proxy mode), we don't want to use the active context, + // we want the span to be in it's own trace. + const parentContext = isIntrospection + ? context.active() + : getContext(parentState); + + forSubgraphExecution.otel = new OtelContextStack( + createSubgraphExecuteSpan({ + ctx: parentContext, + tracer, + executionRequest, + subgraphName, + }), + ); + + if (useContextManager) { + wrapped = context.bind(forSubgraphExecution.otel!.current, wrapped); + } + + return unfakePromise( + fakePromise() + .then(wrapped) + .catch((err) => { + registerException(forSubgraphExecution.otel!.current, err); + throw err; + }) + .finally(() => { + trace.getSpan(forSubgraphExecution.otel!.current)?.end(); + forSubgraphExecution.otel!.pop(); }), - spanProcessors, - }); - webProvider.register(); - provider = webProvider; + ); + }, + + fetch({ state, executionRequest }, wrapped) { + if (isRetryExecutionRequest(executionRequest)) { + // Retry plugin overrides the executionRequest, by "forking" it so that multiple attempts + // of the same execution request can be made. + // We need to attach the fetch span to the original execution request, because attempt + // execution requests doesn't create a new `subgraph.execute` span. + state = getState(getRetryInfo(executionRequest)); + } + + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.upstreamFetch, { executionRequest }) + ) { + return wrapped(); } - const pluginLogger = options.logger.child({ plugin: 'OpenTelemetry' }); - const diagLogger = pluginLogger.child('OtelDiag'); - diag.setLogger( + + return unfakePromise( + preparation$.then(() => { + const { forSubgraphExecution } = state; + const ctx = createUpstreamHttpFetchSpan({ + ctx: getContext(state), + tracer, + }); + + forSubgraphExecution?.otel!.push(ctx); + + if (useContextManager) { + wrapped = context.bind(ctx, wrapped); + } + + return fakePromise() + .then(wrapped) + .catch((err) => { + registerException(ctx, err); + throw err; + }) + .finally(() => { + trace.getSpan(ctx)?.end(); + forSubgraphExecution?.otel!.pop(); + }); + }), + ); + }, + + schema(_, wrapped) { + if (!shouldTrace(traces.spans?.schema, null)) { + return wrapped(); + } + + return unfakePromise( + preparation$.then(() => { + const ctx = createSchemaLoadingSpan({ + ctx: initSpan ?? ROOT_CONTEXT, + tracer, + }); + return fakePromise() + .then(() => context.with(ctx, wrapped)) + .catch((err) => { + trace.getSpan(ctx)?.recordException(err); + }) + .finally(() => { + trace.getSpan(ctx)?.end(); + }); + }), + ); + }, + }, + + onYogaInit({ yoga }) { + //TODO remove this when Yoga will also use the new Logger API + pluginLogger ??= new Logger({ + writers: [ { - error: (message, ...args) => diagLogger.error(message, ...args), - warn: (message, ...args) => diagLogger.warn(message, ...args), - info: (message, ...args) => diagLogger.info(message, ...args), - debug: (message, ...args) => diagLogger.debug(message, ...args), - verbose: (message, ...args) => diagLogger.debug(message, ...args), + write(level, attrs, msg) { + level = level === 'trace' ? 'debug' : level; + yoga.logger[level](msg, attrs); + }, }, - DiagLogLevel.VERBOSE, - ); - setGlobalErrorHandler((err) => - diagLogger.error('Uncaught OTEL internal error', err), + ], + }).child('[OpenTelemetry] '); + + if (options.configureDiagLogger !== false) { + const logLevel = diagLogLevelFromEnv(); // We enable the diag only if it is explicitly enabled, as NodeSDK does + if (logLevel) { + const diagLog = pluginLogger.child('[diag] ') as Logger & { + verbose: Logger['trace']; + }; + diagLog.verbose = diagLog.trace; + diag.setLogger(diagLog, logLevel); + setGlobalErrorHandler((err) => diagLog.error(err as Attributes)); + } + } + + pluginLogger.debug( + `context manager is ${useContextManager ? 'enabled' : 'disabled'}`, + ); + }, + + onRequest({ state, request }) { + try { + const requestId = requestIdByRequest.get(request); + if (requestId) { + if (options.log) { + loggerForRequest(options.log.child({ requestId }), request); + } + + // When running in a runtime without a context manager, we have to keep track of the + // span correlated to a log manually. For now, we just link all logs for a request to + // the HTTP root span + if (!useContextManager) { + otelCtxForRequestId.set(requestId, getContext(state)); + } + } + } catch (error) { + pluginLogger!.error( + { error }, + 'Error while setting up logger for request', ); - tracer = options.tracer || trace.getTracer('gateway'); - preparation$ = undefined; - }); + } }, - onContextBuilding({ extendContext, context }) { + + onEnveloped({ state, extendContext }) { extendContext({ - opentelemetry: { + openTelemetry: { tracer, - activeContext: () => - getOTELContext(context, context.request) ?? context['active'](), + getHttpContext: (request) => { + const { forRequest } = request ? getState({ request }) : state; + return forRequest.otel?.root; + }, + getOperationContext: (context) => { + const { forOperation } = context ? getState({ context }) : state; + return forOperation.otel?.root; + }, + getExecutionRequestContext: (executionRequest) => { + return getState({ executionRequest }).forSubgraphExecution.otel + ?.root; + }, + getActiveContext: (contextMatcher?: Parameters[0]) => + getContext(contextMatcher ? getState(contextMatcher) : state), }, }); }, - onRequest(onRequestPayload) { - return handleMaybePromise( - () => preparation$, - () => { - const shouldTraceHttp = - typeof options.spans?.http === 'function' - ? options.spans.http(onRequestPayload) - : (options.spans?.http ?? true); - - if (shouldTraceHttp) { - const { request, url } = onRequestPayload; - const otelContext = inheritContext - ? propagation.extract( - context.active(), - request.headers, - HeadersTextMapGetter, - ) - : context.active(); - - const httpSpan = createHttpSpan({ - request, - url, - tracer, - otelContext, - }); - const otelContextToSet = trace.setSpan(otelContext, httpSpan); - requestContextMapping.set(request, otelContextToSet); - contextMapping.set( - onRequestPayload.serverContext, - otelContextToSet, - ); + onCacheGet: (payload) => + shouldTrace(traces.events?.cache, { key: payload.key, action: 'read' }) + ? { + onCacheMiss: () => recordCacheEvent('miss', payload), + onCacheHit: () => recordCacheEvent('hit', payload), + onCacheGetError: ({ error }) => + recordCacheError('read', error, payload), } - }, - ); + : undefined, + + onCacheSet: (payload) => + shouldTrace(traces.events?.cache, { key: payload.key, action: 'write' }) + ? { + onCacheSetDone: () => recordCacheEvent('write', payload), + onCacheSetError: ({ error }) => + recordCacheError('write', error, payload), + } + : undefined, + + onResponse({ response, request, state }) { + try { + state.forRequest.otel && + setResponseAttributes(state.forRequest.otel.root, response); + + // Clean up Logging context tracking for runtimes without context manager + if (!useContextManager) { + const requestId = requestIdByRequest.get(request); + if (requestId) { + otelCtxForRequestId.delete(requestId); + } + } + } catch (error) { + pluginLogger!.error({ error }, 'Failed to end http span'); + } }, - onValidate(onValidatePayload) { - const shouldTraceValidate = - typeof options.spans?.graphqlValidate === 'function' - ? options.spans.graphqlValidate(onValidatePayload) - : (options.spans?.graphqlValidate ?? true); - - const { context } = onValidatePayload; - const otelContext = getOTELContext(context, context.request); - - if (shouldTraceValidate && otelContext) { - const { done } = createGraphQLValidateSpan({ - otelContext, - tracer, - query: context.params.query, - operationName: context.params.operationName, - }); - return ({ result }) => done(result); + onParams({ state, context: gqlCtx, params }) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphql, { context: gqlCtx }) + ) { + return; } - return void 0; + + const ctx = getContext(state); + setParamsAttributes({ ctx, params }); }, - onParse(onParsePayload) { - const shouldTracePrase = - typeof options.spans?.graphqlParse === 'function' - ? options.spans.graphqlParse(onParsePayload) - : (options.spans?.graphqlParse ?? true); - - const { context } = onParsePayload; - const otelContext = getOTELContext(context, context.request); - - if (shouldTracePrase && otelContext) { - const { done } = createGraphQLParseSpan({ - otelContext, - tracer, - query: context.params.query, - operationName: context.params.operationName, - }); - return ({ result }) => done(result); + onExecutionResult({ result, context: gqlCtx, state }) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphql, { context: gqlCtx }) + ) { + return; } - return void 0; + + setExecutionResultAttributes({ ctx: getContext(state), result }); }, - onExecute(onExecuteArgs) { - const shouldTraceExecute = - typeof options.spans?.graphqlExecute === 'function' - ? options.spans.graphqlExecute(onExecuteArgs) - : (options.spans?.graphqlExecute ?? true); - - const { args } = onExecuteArgs; - const otelContext = getOTELContext( - args.contextValue, - args.contextValue?.request, - ); - if (shouldTraceExecute && otelContext) { - const { done } = createGraphQLExecuteSpan({ - args, - otelContext, - tracer, + onParse({ state, context: gqlCtx }) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlParse, { context: gqlCtx }) + ) { + return; + } + + return ({ result }) => { + setGraphQLParseAttributes({ + ctx: getContext(state), + operationName: gqlCtx.params.operationName, + query: gqlCtx.params.query?.trim(), + result, }); + }; + }, - return { - onExecuteDone: ({ result }) => { - if (!isAsyncIterable(result)) { - done(result); - } - }, - }; + onValidate({ state, context: gqlCtx }) { + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.graphqlValidate, { context: gqlCtx }) + ) { + return; } - return void 0; + + return ({ result }) => { + setGraphQLValidateAttributes({ ctx: getContext(state), result }); + }; }, - onSubgraphExecute(onSubgraphPayload) { - const shouldTraceSubgraphExecute = - typeof options.spans?.subgraphExecute === 'function' - ? options.spans.subgraphExecute(onSubgraphPayload) - : (options.spans?.subgraphExecute ?? true); - - const otelContext = getOTELContext( - onSubgraphPayload.executionRequest?.context, - onSubgraphPayload.executionRequest?.context?.request, - ); - if (shouldTraceSubgraphExecute && otelContext) { - const { subgraphName, executionRequest } = onSubgraphPayload; - const { done } = createSubgraphExecuteFetchSpan({ - otelContext, - tracer, - executionRequest, - subgraphName, - }); + onExecute({ state, args }) { + if (!isParentEnabled(state)) { + return; + } - return done; + setExecutionAttributesOnOperationSpan({ + ctx: state.forOperation.otel!.root, + args, + hashOperationFn: options.hashOperation, + }); + + if (state.forOperation.skipExecuteSpan) { + return; } - return void 0; - }, - onFetch(onFetchPayload) { - const shouldTraceFetch = - typeof options.spans?.upstreamFetch === 'function' - ? options.spans.upstreamFetch(onFetchPayload) - : (options.spans?.upstreamFetch ?? true); - - const { - context, - options: fetchOptions, - url, - setOptions, - executionRequest, - } = onFetchPayload; - const otelContext = getOTELContext(context, context?.request); - if (shouldTraceFetch && otelContext) { - if (propagateContext) { - const reqHeaders = getHeadersObj(fetchOptions.headers || {}); - propagation.inject(otelContext, reqHeaders); + const ctx = getContext(state); + setGraphQLExecutionAttributes({ ctx, args }); - setOptions({ - ...fetchOptions, - headers: reqHeaders, + state.forOperation.subgraphNames = []; + + return { + onExecuteDone({ result }) { + setGraphQLExecutionResultAttributes({ + ctx, + result, + subgraphNames: state.forOperation.subgraphNames, }); - } + }, + }; + }, - const { done } = createUpstreamHttpFetchSpan({ - otelContext, - tracer, - url, - fetchOptions, - executionRequest, - }); + onSubgraphExecute({ subgraphName, state }) { + // Keep track of the list of subgraphs that has been hit for this operation + // This list will be added as attribute on onExecuteDone hook + state.forOperation?.subgraphNames?.push(subgraphName); + }, + + onFetch(payload) { + const { url, setFetchFn, fetchFn, executionRequest } = payload; + let { state } = payload; - return (fetchDonePayload) => done(fetchDonePayload.response); + if (executionRequest && isRetryExecutionRequest(executionRequest)) { + // Retry plugin overrides the executionRequest, by "forking" it so that multiple attempts + // of the same execution request can be made. + // We need to attach the fetch span to the original execution request, because attempt + // execution requests doesn't create a new `subgraph.execute` span. + state = getState(getRetryInfo(executionRequest)); } - return void 0; - }, - onResponse({ request, response, serverContext }) { - const otelContext = getOTELContext(serverContext, request); - if (!otelContext) { + + // We want to always propagate context, even if we are not tracing the fetch phase. + if (propagateContext) { + setFetchFn((url, options, ...args) => { + const reqHeaders = getHeadersObj(options?.headers || {}); + propagation.inject(getContext(state), reqHeaders); + return fetchFn(url, { ...options, headers: reqHeaders }, ...args); + }); + } + + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.upstreamFetch, { executionRequest }) + ) { return; } - const rootSpan = trace.getSpan(otelContext); + const ctx = getContext(state); - if (rootSpan) { - completeHttpSpan(rootSpan, response); - } + setUpstreamFetchAttributes({ + ctx, + url, + options: payload.options, + executionRequest, + }); - requestContextMapping.delete(request); + return ({ response }) => { + setUpstreamFetchResponseAttributes({ ctx, response }); + }; }, - async onDispose() { - if (spanProcessors) { - await Promise.all( - spanProcessors.map((processor) => processor.forceFlush()), - ); - } - await provider?.forceFlush?.(); - if (spanProcessors) { - spanProcessors.forEach((processor) => processor.shutdown()); + onSchemaChange(payload) { + setSchemaAttributes(payload); + + if (initSpan) { + trace.getSpan(initSpan)?.end(); + initSpan = null; } + }, - await provider?.shutdown?.(); + onDispose() { + if (options.flushOnDispose !== false) { + const flushMethod = options.flushOnDispose ?? 'forceFlush'; - diag.disable(); - trace.disable(); - context.disable(); - propagation.disable(); + const provider = trace.getTracerProvider() as Record; + if ( + flushMethod in provider && + typeof provider[flushMethod] === 'function' + ) { + return provider[flushMethod](); + } + } }, - }; + })); + + if (options.openTelemetry) { + if (options.openTelemetry.register) { + options.openTelemetry?.register?.(plugin); + } else { + options.log?.warn('An OpenTelemetry plugin is already registered'); + } + } + + return plugin; +} + +function shouldTrace( + value: BooleanOrPredicate | null | undefined, + args: Args, +): boolean { + if (value == null) { + return true; + } + if (typeof value === 'function') { + return value(args); + } + return value; +} + +function getURL(request: Request) { + if ('parsedUrl' in request) { + // It is a `whatwg-node/fetch` request which already contains a parsed URL object + return request.parsedUrl as URL; + } + + return new URL(request.url, 'http://localhost'); // to be iso with whatwg-node/server behavior } diff --git a/packages/plugins/opentelemetry/src/processors.ts b/packages/plugins/opentelemetry/src/processors.ts deleted file mode 100644 index b98c96874..000000000 --- a/packages/plugins/opentelemetry/src/processors.ts +++ /dev/null @@ -1,127 +0,0 @@ -import type { AzureMonitorExporterOptions } from '@azure/monitor-opentelemetry-exporter'; -import { OTLPTraceExporter as OtlpHttpExporter } from '@opentelemetry/exporter-trace-otlp-http'; -import { - ZipkinExporter, - type ExporterConfig as ZipkinExporterConfig, -} from '@opentelemetry/exporter-zipkin'; -import { type OTLPExporterNodeConfigBase } from '@opentelemetry/otlp-exporter-base'; -import { type OTLPGRPCExporterConfigNode } from '@opentelemetry/otlp-grpc-exporter-base'; -import { - BatchSpanProcessor, - ConsoleSpanExporter, - SimpleSpanProcessor, - type BufferConfig, - type SpanExporter, - type SpanProcessor, -} from '@opentelemetry/sdk-trace-base'; -import { handleMaybePromise, MaybePromise } from '@whatwg-node/promise-helpers'; - -export type BatchingConfig = boolean | BufferConfig; - -function resolveBatchingConfig( - exporter: SpanExporter, - batchingConfig?: BatchingConfig, -): SpanProcessor { - const value = batchingConfig ?? true; - - if (value === true) { - return new BatchSpanProcessor(exporter); - } else if (value === false) { - return new SimpleSpanProcessor(exporter); - } else { - return new BatchSpanProcessor(exporter, value); - } -} - -export function createStdoutExporter( - batchingConfig?: BatchingConfig, -): SpanProcessor { - return resolveBatchingConfig(new ConsoleSpanExporter(), batchingConfig); -} - -export function createZipkinExporter( - config: ZipkinExporterConfig, - batchingConfig?: BatchingConfig, -): SpanProcessor { - return resolveBatchingConfig(new ZipkinExporter(config), batchingConfig); -} - -export function createOtlpHttpExporter( - config: OTLPExporterNodeConfigBase, - batchingConfig?: BatchingConfig, -): SpanProcessor { - return resolveBatchingConfig(new OtlpHttpExporter(config), batchingConfig); -} - -interface SpanExporterCtor { - new (config: TConfig): SpanExporter; -} - -function loadExporterLazily< - TConfig, - TSpanExporterCtor extends SpanExporterCtor, ->( - exporterName: string, - exporterModuleName: string, - exportNameInModule: string, -): MaybePromise { - try { - return handleMaybePromise( - () => import(exporterModuleName), - (mod) => { - const ExportCtor = - mod?.default?.[exportNameInModule] || mod?.[exportNameInModule]; - if (!ExportCtor) { - throw new Error( - `${exporterName} exporter is not available in the current environment`, - ); - } - return ExportCtor; - }, - ); - } catch (err) { - throw new Error( - `${exporterName} exporter is not available in the current environment`, - ); - } -} - -export function createOtlpGrpcExporter( - config: OTLPGRPCExporterConfigNode, - batchingConfig?: BatchingConfig, -): MaybePromise { - return handleMaybePromise( - () => - loadExporterLazily( - 'OTLP gRPC', - '@opentelemetry/exporter-trace-otlp-grpc', - 'OTLPTraceExporter', - ), - (OTLPTraceExporter) => { - return resolveBatchingConfig( - new OTLPTraceExporter(config), - batchingConfig, - ); - }, - ); -} - -export function createAzureMonitorExporter( - config: AzureMonitorExporterOptions, - batchingConfig?: BatchingConfig, -): MaybePromise { - return handleMaybePromise( - () => - loadExporterLazily( - 'Azure Monitor', - '@azure/monitor-opentelemetry-exporter', - 'AzureMonitorTraceExporter', - ), - (AzureMonitorTraceExporter) => { - return resolveBatchingConfig( - new AzureMonitorTraceExporter(config), - batchingConfig, - ); - }, - ); -} diff --git a/packages/plugins/opentelemetry/src/sdk-node.ts b/packages/plugins/opentelemetry/src/sdk-node.ts new file mode 100644 index 000000000..2dc67d534 --- /dev/null +++ b/packages/plugins/opentelemetry/src/sdk-node.ts @@ -0,0 +1,31 @@ +import sdkDefault, { + api, + contextBase, + core, + LoggerProviderConfig, + logs, + MeterProviderConfig, + metrics, + node, + NodeSDK, + NodeSDKConfiguration, + resources, + tracing, +} from '@opentelemetry/sdk-node'; + +export type { LoggerProviderConfig, MeterProviderConfig, NodeSDKConfiguration }; + +export { + api, + contextBase, + core, + logs, + metrics, + node, + NodeSDK, + resources, + sdkDefault, + tracing, +}; + +export default sdkDefault; diff --git a/packages/plugins/opentelemetry/src/setup.ts b/packages/plugins/opentelemetry/src/setup.ts new file mode 100644 index 000000000..cbe5e724f --- /dev/null +++ b/packages/plugins/opentelemetry/src/setup.ts @@ -0,0 +1,289 @@ +import { Attributes, Logger } from '@graphql-hive/logger'; +import { + context, + ContextManager, + propagation, + TextMapPropagator, + trace, + TracerProvider, +} from '@opentelemetry/api'; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from '@opentelemetry/core'; +import { Resource, resourceFromAttributes } from '@opentelemetry/resources'; +import { + AlwaysOnSampler, + BasicTracerProvider, + BatchSpanProcessor, + ConsoleSpanExporter, + GeneralLimits, + ParentBasedSampler, + SimpleSpanProcessor, + SpanLimits, + TraceIdRatioBasedSampler, + type BufferConfig, + type Sampler, + type SpanExporter, + type SpanProcessor, +} from '@opentelemetry/sdk-trace-base'; +import { + ATTR_SERVICE_NAME, + ATTR_SERVICE_VERSION, +} from '@opentelemetry/semantic-conventions'; +import { + HiveTracingSpanProcessor, + HiveTracingSpanProcessorOptions, +} from './hive-span-processor'; +import { getEnvVar } from './utils'; + +export * from './attributes'; +export * from './log-writer'; +export * from './hive-span-processor'; +export { getEnvVar }; + +// @inject-version globalThis.__OTEL_PLUGIN_VERSION__ here + +type TracingOptions = { + traces?: + | { tracerProvider: TracerProvider } + | (TracerOptions & + ( + | { + // Processors + processors: SpanProcessor[]; + tracerProvider?: never; + exporter?: never; + } + | { + // Exporter + exporter: SpanExporter; + batching?: BatchingConfig | boolean; + tracerProvider?: never; + processors?: never; + } + | { + // Console only + tracerProvider?: never; + processors?: never; + exporter?: never; + } + )); +}; + +type TracerOptions = { + console?: boolean; + spanLimits?: SpanLimits; +}; + +type SamplingOptions = + | { + sampler: Sampler; + samplingRate?: never; + } + | { + sampler?: never; + samplingRate?: number; + }; + +type OpentelemetrySetupOptions = TracingOptions & + SamplingOptions & { + resource?: Resource | { serviceName: string; serviceVersion: string }; + contextManager: ContextManager | null; + propagators?: TextMapPropagator[]; + generalLimits?: GeneralLimits; + log?: Logger; + }; + +export function openTelemetrySetup(options: OpentelemetrySetupOptions) { + const log = options.log?.child('[OpenTelemetry] '); + + if (getEnvVar('OTEL_SDK_DISABLED', false) === 'true') { + log?.warn( + 'OpenTelemetry integration is disabled because `OTEL_SDK_DISABLED` environment variable is set to `true`', + ); + return; + } + + const logAttributes: Attributes = { registrationResults: {} }; + let logMessage = 'OpenTelemetry integration is enabled'; + + if (options.traces) { + if (options.traces.tracerProvider) { + if ( + 'register' in options.traces.tracerProvider && + typeof options.traces.tracerProvider.register === 'function' + ) { + logAttributes['registrationResults'].tracer = + options.traces.tracerProvider.register(); + } else { + logAttributes['registrationResults'].tracer = + trace.setGlobalTracerProvider(options.traces.tracerProvider); + } + logMessage += ' and provided TracerProvider has been registered'; + } else { + let spanProcessors = options.traces.processors ?? []; + + if (options.traces.exporter) { + spanProcessors.push( + resolveBatchingConfig( + options.traces.exporter, + options.traces.batching, + ), + ); + logMessage += ' and exporter have been registered'; + logAttributes['batching'] = options.traces.batching ?? true; + } + + if (options.traces.console) { + spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter())); + logMessage += ' in addition to an stdout debug exporter'; + logAttributes['console'] = true; + } + + const baseResource = resourceFromAttributes({ + [ATTR_SERVICE_NAME]: + options.resource && 'serviceName' in options.resource + ? options.resource?.serviceName + : getEnvVar( + 'OTEL_SERVICE_NAME', + '@graphql-mesh/plugin-opentelemetry', + ), + [ATTR_SERVICE_VERSION]: + options.resource && 'serviceVersion' in options.resource + ? options.resource?.serviceVersion + : getEnvVar( + 'OTEL_SERVICE_VERSION', + globalThis.__OTEL_PLUGIN_VERSION__, + ), + ['hive.gateway.version']: globalThis.__VERSION__, + ['hive.otel.version']: globalThis.__OTEL_PLUGIN_VERSION__, + }); + + const resource = + options.resource && !('serviceName' in options.resource) + ? baseResource.merge(options.resource) + : baseResource; + + logAttributes['resource'] = resource.attributes; + logAttributes['sampling'] = options.sampler + ? 'custom' + : options.samplingRate; + + logAttributes['registrationResults'].tracerProvider = + trace.setGlobalTracerProvider( + new BasicTracerProvider({ + resource, + sampler: + options.sampler ?? + (options.samplingRate + ? new ParentBasedSampler({ + root: new TraceIdRatioBasedSampler(options.samplingRate), + }) + : new AlwaysOnSampler()), + spanProcessors, + generalLimits: options.generalLimits, + spanLimits: options.traces.spanLimits, + }), + ); + } + } + + if (options.contextManager !== null) { + logAttributes['registrationResults'].contextManager = + context.setGlobalContextManager(options.contextManager); + } + + if (!options.propagators || options.propagators.length !== 0) { + const propagators = options.propagators ?? [ + new W3CBaggagePropagator(), + new W3CTraceContextPropagator(), + ]; + + logAttributes['registrationResults'].propagators = + propagation.setGlobalPropagator( + propagators.length === 1 + ? propagators[0]! + : new CompositePropagator({ propagators }), + ); + } + + log?.info(logAttributes, logMessage); +} + +export type HiveTracingOptions = { target?: string } & ( + | { + accessToken?: string; + batching?: BufferConfig; + processor?: never; + endpoint?: string; + } + | { + processor: SpanProcessor; + } +); + +export function hiveTracingSetup( + config: HiveTracingOptions & { + contextManager: ContextManager | null; + log?: Logger; + }, +) { + const log = config.log?.child('[OpenTelemetry] '); + config.target ??= getEnvVar('HIVE_TARGET', undefined); + + if (!config.target) { + throw new Error( + 'You must specify the Hive Registry `target`. Either provide `target` option or `HIVE_TARGET` environment variable.', + ); + } + + const logAttributes: Attributes = { target: config.target }; + + if (!config.processor) { + config.accessToken ??= + getEnvVar('HIVE_TRACING_ACCESS_TOKEN', undefined) ?? + getEnvVar('HIVE_ACCESS_TOKEN', undefined); + + if (!config.accessToken) { + throw new Error( + 'You must specify the Hive Registry `accessToken`. Either provide `accessToken` option or `HIVE_ACCESS_TOKEN`/`HIVE_TRACE_ACCESS_TOKEN` environment variable.', + ); + } + + logAttributes['endpoint'] = config.endpoint; + logAttributes['batching'] = config.batching; + } + + openTelemetrySetup({ + contextManager: config.contextManager, + resource: resourceFromAttributes({ + 'hive.target_id': config.target, + }), + traces: { + processors: [ + new HiveTracingSpanProcessor(config as HiveTracingSpanProcessorOptions), + ], + }, + }); + + log?.info(logAttributes, 'Hive Tracing integration has been enabled'); +} + +export type BatchingConfig = boolean | BufferConfig; + +function resolveBatchingConfig( + exporter: SpanExporter, + batchingConfig?: BatchingConfig, +): SpanProcessor { + const value = batchingConfig ?? true; + + if (value === true) { + return new BatchSpanProcessor(exporter); + } else if (value === false) { + return new SimpleSpanProcessor(exporter); + } else { + return new BatchSpanProcessor(exporter, value); + } +} diff --git a/packages/plugins/opentelemetry/src/spans.ts b/packages/plugins/opentelemetry/src/spans.ts index 11fc2afad..4d6a489aa 100644 --- a/packages/plugins/opentelemetry/src/spans.ts +++ b/packages/plugins/opentelemetry/src/spans.ts @@ -1,23 +1,47 @@ +import { hashOperation } from '@graphql-hive/core'; +import { OnCacheGetHookEventPayload } from '@graphql-hive/gateway-runtime'; import { defaultPrintFn } from '@graphql-mesh/transport-common'; import { getOperationASTFromDocument, + isAsyncIterable, type ExecutionRequest, type ExecutionResult, } from '@graphql-tools/utils'; import { + context, + ROOT_CONTEXT, SpanKind, SpanStatusCode, + trace, type Context, - type Span, type Tracer, } from '@opentelemetry/api'; -import type { ExecutionArgs } from 'graphql'; import { - SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME, + SEMATTRS_EXCEPTION_MESSAGE, + SEMATTRS_EXCEPTION_STACKTRACE, + SEMATTRS_EXCEPTION_TYPE, +} from '@opentelemetry/semantic-conventions'; +import { + DocumentNode, + GraphQLSchema, + printSchema, + TypeInfo, + type ExecutionArgs, +} from 'graphql'; +import type { GraphQLParams } from 'graphql-yoga'; +import { + getRetryInfo, + isRetryExecutionRequest, +} from '../../../runtime/src/plugins/useUpstreamRetry'; +import { SEMATTRS_GRAPHQL_DOCUMENT, - SEMATTRS_GRAPHQL_ERROR_COUNT, SEMATTRS_GRAPHQL_OPERATION_NAME, SEMATTRS_GRAPHQL_OPERATION_TYPE, + SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, + SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME, + SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, + SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, + SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH, SEMATTRS_HTTP_CLIENT_IP, SEMATTRS_HTTP_HOST, SEMATTRS_HTTP_METHOD, @@ -30,90 +54,203 @@ import { } from './attributes'; export function createHttpSpan(input: { + ctx: Context; tracer: Tracer; request: Request; url: URL; - otelContext: Context; -}): Span { - const { url, request, tracer, otelContext } = input; - const path = url.pathname; - const userAgent = request.headers.get('user-agent'); - const ips = request.headers.get('x-forwarded-for'); - const method = request.method || 'GET'; - const host = url.host || request.headers.get('host'); - const hostname = url.hostname || host || 'localhost'; - const rootSpanName = `${method} ${path}`; - - return tracer.startSpan( - rootSpanName, +}): { ctx: Context } { + const { url, request, tracer } = input; + + const span = tracer.startSpan( + `${request.method || 'GET'} ${url.pathname}`, { attributes: { - [SEMATTRS_HTTP_METHOD]: method, + [SEMATTRS_HTTP_METHOD]: request.method || 'GET', [SEMATTRS_HTTP_URL]: request.url, - [SEMATTRS_HTTP_ROUTE]: path, + [SEMATTRS_HTTP_ROUTE]: url.pathname, [SEMATTRS_HTTP_SCHEME]: url.protocol, - [SEMATTRS_NET_HOST_NAME]: hostname, - [SEMATTRS_HTTP_HOST]: host || undefined, - [SEMATTRS_HTTP_CLIENT_IP]: ips?.split(',')[0], - [SEMATTRS_HTTP_USER_AGENT]: userAgent || undefined, + [SEMATTRS_NET_HOST_NAME]: + url.hostname || + url.host || + request.headers.get('host') || + 'localhost', + [SEMATTRS_HTTP_HOST]: + url.host || request.headers.get('host') || undefined, + [SEMATTRS_HTTP_CLIENT_IP]: request.headers + .get('x-forwarded-for') + ?.split(',')[0], + [SEMATTRS_HTTP_USER_AGENT]: + request.headers.get('user-agent') || undefined, + 'hive.client.name': + request.headers.get('x-graphql-client-name') || undefined, + 'hive.client.version': + request.headers.get('x-graphql-client-version') || undefined, }, kind: SpanKind.SERVER, }, - otelContext, + input.ctx, ); + + return { + ctx: trace.setSpan(input.ctx, span), + }; } -export function completeHttpSpan(span: Span, response: Response) { - span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status); - span.setStatus({ - code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR, - message: response.ok ? undefined : response.statusText, +export function setResponseAttributes(ctx: Context, response: Response) { + const span = trace.getSpan(ctx); + if (span) { + span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status); + span.setAttribute( + 'gateway.cache.response_cache', + response.status === 304 && response.headers.get('ETag') ? 'hit' : 'miss', + ); + span.setStatus({ + code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR, + message: response.ok ? undefined : response.statusText, + }); + } +} + +export function createGraphQLSpan(input: { + ctx: Context; + tracer: Tracer; +}): Context { + const span = input.tracer.startSpan( + `graphql.operation`, + { kind: SpanKind.INTERNAL }, + input.ctx, + ); + + return trace.setSpan(input.ctx, span); +} + +export function setParamsAttributes(input: { + ctx: Context; + params: GraphQLParams; +}) { + const { ctx, params } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } + + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, params.query ?? ''); + if (params.operationName) { + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, params.operationName); + } +} + +export type OperationHashingFn = (input: { + document: DocumentNode; + operationName?: string | null; + variableValues?: Record | null; + schema: GraphQLSchema; +}) => string | null; + +const typeInfos = new WeakMap(); +export const defaultOperationHashingFn: OperationHashingFn = (input) => { + if (!typeInfos.has(input.schema)) { + typeInfos.set(input.schema, new TypeInfo(input.schema)); + } + const typeInfo = typeInfos.get(input.schema); + + return hashOperation({ + documentNode: input.document, + operationName: input.operationName ?? null, + schema: input.schema, + variables: null, // Unstable feature, not using it for now + typeInfo, }); - span.end(); +}; + +export function setExecutionAttributesOnOperationSpan(input: { + ctx: Context; + args: ExecutionArgs; + hashOperationFn?: OperationHashingFn | null; +}) { + const { hashOperationFn = defaultOperationHashingFn, args, ctx } = input; + const span = trace.getSpan(ctx); + if (span) { + const operation = getOperationASTFromDocument( + args.document, + args.operationName || undefined, + ); + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation); + + const document = defaultPrintFn(args.document); + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, document); + + const hash = hashOperationFn?.({ ...args }); + if (hash) { + span.setAttribute(SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH, hash); + } + + const operationName = operation.name?.value; + if (operationName) { + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName); + span.updateName(`graphql.operation ${operationName}`); + } + } +} + +export function createGraphqlContextBuildingSpan(input: { + ctx: Context; + tracer: Tracer; +}): Context { + const span = input.tracer.startSpan( + 'graphql.context', + { kind: SpanKind.INTERNAL }, + input.ctx, + ); + + return trace.setSpan(input.ctx, span); } export function createGraphQLParseSpan(input: { - otelContext: Context; + ctx: Context; tracer: Tracer; - query?: string; - operationName?: string; -}) { - const parseSpan = input.tracer.startSpan( +}): Context { + const span = input.tracer.startSpan( 'graphql.parse', { - attributes: { - [SEMATTRS_GRAPHQL_DOCUMENT]: input.query, - [SEMATTRS_GRAPHQL_OPERATION_NAME]: input.operationName, - }, kind: SpanKind.INTERNAL, }, - input.otelContext, + input.ctx, ); - return { - parseSpan, - done: (result: any | Error | null) => { - if (result instanceof Error) { - parseSpan.setAttribute(SEMATTRS_GRAPHQL_ERROR_COUNT, 1); - parseSpan.recordException(result); - parseSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: result.message, - }); - } + return trace.setSpan(input.ctx, span); +} - parseSpan.end(); - }, - }; +export function setGraphQLParseAttributes(input: { + ctx: Context; + query?: string; + operationName?: string; + result: unknown; +}) { + const span = trace.getSpan(input.ctx); + if (!span) { + return; + } + + if (input.query) { + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, input.query); + } + if (input.operationName) { + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, input.operationName); + } + + if (input.result instanceof Error) { + span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, 1); + } } export function createGraphQLValidateSpan(input: { - otelContext: Context; + ctx: Context; tracer: Tracer; query?: string; operationName?: string; -}) { - const validateSpan = input.tracer.startSpan( +}): Context { + const span = input.tracer.startSpan( 'graphql.validate', { attributes: { @@ -122,167 +259,290 @@ export function createGraphQLValidateSpan(input: { }, kind: SpanKind.INTERNAL, }, - input.otelContext, + input.ctx, ); + return trace.setSpan(input.ctx, span); +} - return { - validateSpan, - done: (result: any[] | readonly Error[]) => { - if (result instanceof Error) { - validateSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: result.message, - }); - } else if (Array.isArray(result) && result.length > 0) { - validateSpan.setAttribute(SEMATTRS_GRAPHQL_ERROR_COUNT, result.length); - validateSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: result.map((e) => e.message).join(', '), - }); - - for (const error in result) { - validateSpan.recordException(error); - } - } +export function setGraphQLValidateAttributes(input: { + ctx: Context; + result: any[] | readonly Error[]; +}) { + const { result, ctx } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } - validateSpan.end(); - }, - }; + if (result instanceof Error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: result.message, + }); + } else if (Array.isArray(result) && result.length > 0) { + span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, result.length); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: result.map((e) => e.message).join(', '), + }); + + for (const error in result) { + span.recordException(error); + } + } } export function createGraphQLExecuteSpan(input: { - args: ExecutionArgs; - otelContext: Context; + ctx: Context; tracer: Tracer; +}): Context { + const span = input.tracer.startSpan( + 'graphql.execute', + { kind: SpanKind.INTERNAL }, + input.ctx, + ); + + return trace.setSpan(input.ctx, span); +} + +export function setGraphQLExecutionAttributes(input: { + ctx: Context; + args: ExecutionArgs; }) { + const { ctx, args } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } + const operation = getOperationASTFromDocument( - input.args.document, - input.args.operationName || undefined, - ); - const executeSpan = input.tracer.startSpan( - 'graphql.execute', - { - attributes: { - [SEMATTRS_GRAPHQL_OPERATION_TYPE]: operation.operation, - [SEMATTRS_GRAPHQL_OPERATION_NAME]: - input.args.operationName || undefined, - [SEMATTRS_GRAPHQL_DOCUMENT]: defaultPrintFn(input.args.document), - }, - kind: SpanKind.INTERNAL, - }, - input.otelContext, + args.document, + args.operationName || undefined, ); + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation); - return { - executeSpan, - done: (result: ExecutionResult) => { - if (result.errors && result.errors.length > 0) { - executeSpan.setAttribute( - SEMATTRS_GRAPHQL_ERROR_COUNT, - result.errors.length, - ); - executeSpan.setStatus({ - code: SpanStatusCode.ERROR, - message: result.errors.map((e) => e.message).join(', '), - }); - - for (const error in result.errors) { - executeSpan.recordException(error); - } - } + const operationName = operation.name?.value; + if (operationName) { + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName); + } - executeSpan.end(); - }, - }; + const document = defaultPrintFn(input.args.document); + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, document); } -export const subgraphExecReqSpanMap = new WeakMap(); +export function setGraphQLExecutionResultAttributes(input: { + ctx: Context; + result: ExecutionResult | AsyncIterableIterator; + subgraphNames?: string[]; +}) { + const { ctx, result } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } + + if (input.subgraphNames?.length) { + span.setAttribute( + SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, + input.subgraphNames, + ); + } + + if ( + !isAsyncIterable(result) && // FIXME: Handle async iterable too + result.errors && + result.errors.length > 0 + ) { + span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, result.errors.length); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: result.errors.map((e) => e.message).join(', '), + }); + + const codes: string[] = []; + for (const error of result.errors) { + span.recordException(error); + if (error.extensions['code']) { + codes.push(`${error.extensions['code']}`); // Ensure string using string interpolation + } + } + + if (codes.length > 0) { + span.setAttribute(SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, codes); + } + } +} -export function createSubgraphExecuteFetchSpan(input: { - otelContext: Context; +export function createSubgraphExecuteSpan(input: { + ctx: Context; tracer: Tracer; executionRequest: ExecutionRequest; subgraphName: string; -}) { - const subgraphExecuteSpan = input.tracer.startSpan( +}): Context { + const operation = getOperationASTFromDocument( + input.executionRequest.document, + input.executionRequest.operationName, + ); + + const span = input.tracer.startSpan( `subgraph.execute (${input.subgraphName})`, { attributes: { - [SEMATTRS_GRAPHQL_OPERATION_NAME]: input.executionRequest.operationName, + [SEMATTRS_GRAPHQL_OPERATION_NAME]: operation.name?.value, [SEMATTRS_GRAPHQL_DOCUMENT]: defaultPrintFn( input.executionRequest.document, ), - [SEMATTRS_GRAPHQL_OPERATION_TYPE]: getOperationASTFromDocument( - input.executionRequest.document, - input.executionRequest.operationName, - )?.operation, - [SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME]: input.subgraphName, + [SEMATTRS_GRAPHQL_OPERATION_TYPE]: operation.operation, + [SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME]: input.subgraphName, }, kind: SpanKind.CLIENT, }, - input.otelContext, + input.ctx, ); - subgraphExecReqSpanMap.set(input.executionRequest, subgraphExecuteSpan); - - return { - done() { - subgraphExecuteSpan.end(); - }, - }; + return trace.setSpan(input.ctx, span); } export function createUpstreamHttpFetchSpan(input: { - otelContext: Context; + ctx: Context; tracer: Tracer; +}): Context { + const span = input.tracer.startSpan( + 'http.fetch', + { + attributes: {}, + kind: SpanKind.CLIENT, + }, + input.ctx, + ); + return trace.setSpan(input.ctx, span); +} + +export function setUpstreamFetchAttributes(input: { + ctx: Context; url: string; - fetchOptions: RequestInit; + options: RequestInit; executionRequest?: ExecutionRequest; }) { + const { ctx, url, options: fetchOptions } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } + const urlObj = new URL(input.url); + span.setAttribute(SEMATTRS_HTTP_METHOD, fetchOptions.method ?? 'GET'); + span.setAttribute(SEMATTRS_HTTP_URL, url); + span.setAttribute(SEMATTRS_NET_HOST_NAME, urlObj.hostname); + span.setAttribute(SEMATTRS_HTTP_HOST, urlObj.host); + span.setAttribute(SEMATTRS_HTTP_ROUTE, urlObj.pathname); + span.setAttribute(SEMATTRS_HTTP_SCHEME, urlObj.protocol); + if ( + input.executionRequest && + isRetryExecutionRequest(input.executionRequest) + ) { + const { attempt } = getRetryInfo(input.executionRequest); + if (attempt > 0) { + // The resend attribute should only be present on second and subsequent retry attempt + // https://opentelemetry.io/docs/specs/semconv/http/http-spans/#http-request-retries-and-redirects + span.setAttribute('http.request.resend_count', attempt); + } + } +} - const attributes = { - [SEMATTRS_HTTP_METHOD]: input.fetchOptions.method, - [SEMATTRS_HTTP_URL]: input.url, - [SEMATTRS_NET_HOST_NAME]: urlObj.hostname, - [SEMATTRS_HTTP_HOST]: urlObj.host, - [SEMATTRS_HTTP_ROUTE]: urlObj.pathname, - [SEMATTRS_HTTP_SCHEME]: urlObj.protocol, - }; +export function setUpstreamFetchResponseAttributes(input: { + ctx: Context; + response: Response; +}) { + const { ctx, response } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } - let fetchSpan: Span | undefined; - let isOrigSpan: boolean; + span.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status); + span.setStatus({ + code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR, + message: response.ok ? undefined : response.statusText, + }); +} - if (input.executionRequest) { - fetchSpan = subgraphExecReqSpanMap.get(input.executionRequest); - if (fetchSpan) { - isOrigSpan = false; - fetchSpan.setAttributes(attributes); - } - } +export function recordCacheEvent( + event: string, + payload: OnCacheGetHookEventPayload, +) { + trace.getActiveSpan()?.addEvent('gateway.cache.' + event, { + 'gateway.cache.key': payload.key, + 'gateway.cache.ttl': payload.ttl, + }); +} - if (!fetchSpan) { - fetchSpan = input.tracer.startSpan( - 'http.fetch', - { - attributes, - kind: SpanKind.CLIENT, - }, - input.otelContext, +export function recordCacheError( + action: 'read' | 'write', + error: Error, + payload: OnCacheGetHookEventPayload, +) { + trace.getActiveSpan()?.addEvent('gateway.cache.error', { + 'gateway.cache.key': payload.key, + 'gateway.cache.ttl': payload.ttl, + 'gateway.cache.action': action, + [SEMATTRS_EXCEPTION_TYPE]: + 'code' in error ? (error.code as string) : error.message, + [SEMATTRS_EXCEPTION_MESSAGE]: error.message, + [SEMATTRS_EXCEPTION_STACKTRACE]: error.stack, + }); +} + +const responseCacheSymbol = Symbol.for('servedFromResponseCache'); +export function setExecutionResultAttributes(input: { + ctx: Context; + result?: any; // We don't need a proper type here because we rely on Symbol mark from response cache plugin +}) { + const span = trace.getSpan(input.ctx); + if (input.result && span) { + span.setAttribute( + 'gateway.cache.response_cache', + input.result[responseCacheSymbol] ? 'hit' : 'miss', ); - isOrigSpan = true; } +} - return { - done: (response: Response) => { - fetchSpan.setAttribute(SEMATTRS_HTTP_STATUS_CODE, response.status); - fetchSpan.setStatus({ - code: response.ok ? SpanStatusCode.OK : SpanStatusCode.ERROR, - message: response.ok ? undefined : response.statusText, - }); - if (isOrigSpan) { - fetchSpan.end(); - } - }, - }; +export function createSchemaLoadingSpan(inputs: { + tracer: Tracer; + ctx: Context; +}) { + const span = inputs.tracer.startSpan( + 'gateway.schema', + { attributes: { 'gateway.schema.changed': false } }, + inputs.ctx, + ); + const currentContext = context.active(); + + // If the current span is not the provided span, add a link to the current span + if (currentContext !== inputs.ctx) { + const currentSpan = trace.getActiveSpan(); + currentSpan?.addLink({ context: span.spanContext() }); + } + + return trace.setSpan(ROOT_CONTEXT, span); +} + +export function setSchemaAttributes(inputs: { schema: GraphQLSchema }) { + const span = trace.getActiveSpan(); + if (!span) { + return; + } + span.setAttribute('gateway.schema.changed', true); + span.setAttribute('graphql.schema', printSchema(inputs.schema)); +} + +export function registerException(ctx: Context | undefined, error: any) { + const span = ctx && trace.getSpan(ctx); + if (!span) { + return; + } + + const message = error?.message?.toString() ?? error?.toString(); + span.setStatus({ code: SpanStatusCode.ERROR, message }); + span.recordException(error); } diff --git a/packages/plugins/opentelemetry/src/utils.ts b/packages/plugins/opentelemetry/src/utils.ts new file mode 100644 index 000000000..254edc0a2 --- /dev/null +++ b/packages/plugins/opentelemetry/src/utils.ts @@ -0,0 +1,66 @@ +import { context, diag, DiagLogLevel } from '@opentelemetry/api'; + +export async function tryContextManagerSetup( + useContextManager: true | undefined, +): Promise { + if (await isContextManagerCompatibleWithAsync()) { + return true; + } + + if (useContextManager) { + throw new Error( + '[OTEL] A Context Manager is already registered, but is not compatible with async calls.' + + ' Please use another context manager, such as `AsyncLocalStorageContextManager`.', + ); + } + + return true; +} + +export function isContextManagerCompatibleWithAsync(): Promise { + const symbol = Symbol(); + const root = context.active(); + return context.with(root.setValue(symbol, true), () => { + return new Promise((resolve) => { + // Use timeout to ensure that we yield to the event loop. + // Some runtimes are optimized and doesn't yield for straight forward async functions + // without actual async work. + setTimeout(() => { + resolve((context.active().getValue(symbol) as boolean) || false); + }); + }); + }); +} + +export const getEnvVar = + 'process' in globalThis + ? (name: string, defaultValue: T): string | T => + process.env[name] || defaultValue + : (_name: string, defaultValue: T): string | T => defaultValue; + +const logLevelMap: Record = { + ALL: DiagLogLevel.ALL, + VERBOSE: DiagLogLevel.VERBOSE, + DEBUG: DiagLogLevel.DEBUG, + INFO: DiagLogLevel.INFO, + WARN: DiagLogLevel.WARN, + ERROR: DiagLogLevel.ERROR, + NONE: DiagLogLevel.NONE, +}; + +export function diagLogLevelFromEnv(): DiagLogLevel | undefined { + const value = getEnvVar('OTEL_LOG_LEVEL', null); + + if (value == null) { + return undefined; + } + + const resolvedLogLevel = logLevelMap[value.toUpperCase()]; + if (resolvedLogLevel == null) { + diag.warn( + `Unknown log level "${value}", expected one of ${Object.keys(logLevelMap)}, using default`, + ); + return DiagLogLevel.INFO; + } + return resolvedLogLevel; +} diff --git a/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts b/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts index afe32f5a5..91ee708f5 100644 --- a/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts +++ b/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts @@ -1,131 +1,1234 @@ -import { createSchema, createYoga } from 'graphql-yoga'; -import { beforeAll, beforeEach, describe, expect, it, vi } from 'vitest'; - -let mockModule = vi.mock; -if (globalThis.Bun) { - mockModule = require('bun:test').mock.module; -} -const mockRegisterProvider = vi.fn(); -describe('useOpenTelemetry', () => { - mockModule('@opentelemetry/sdk-trace-web', () => ({ - WebTracerProvider: vi.fn(() => ({ register: mockRegisterProvider })), - })); +import { Logger } from '@graphql-hive/logger'; +import { loggerForRequest } from '@graphql-hive/logger/request'; +import { + hiveTracingSetup, + HiveTracingSpanProcessor, + OpenTelemetryLogWriter, + openTelemetrySetup, + SEMATTRS_GRAPHQL_DOCUMENT, + SEMATTRS_GRAPHQL_OPERATION_NAME, + SEMATTRS_GRAPHQL_OPERATION_TYPE, + SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES, + SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME, + SEMATTRS_HIVE_GRAPHQL_ERROR_CODES, + SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT, + SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH, + SEMATTRS_HTTP_HOST, + SEMATTRS_HTTP_METHOD, + SEMATTRS_HTTP_ROUTE, + SEMATTRS_HTTP_SCHEME, + SEMATTRS_HTTP_STATUS_CODE, + SEMATTRS_HTTP_URL, + SEMATTRS_NET_HOST_NAME, +} from '@graphql-mesh/plugin-opentelemetry/setup'; +import { + ROOT_CONTEXT, + SpanStatusCode, + TextMapPropagator, + TracerProvider, +} from '@opentelemetry/api'; +import { SeverityNumber } from '@opentelemetry/api-logs'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { + CompositePropagator, + W3CBaggagePropagator, + W3CTraceContextPropagator, +} from '@opentelemetry/core'; +import { OTLPTraceExporter } from '@opentelemetry/exporter-trace-otlp-http'; +import { resourceFromAttributes } from '@opentelemetry/resources'; +import { + LoggerProvider, + LogRecordExporter, + LogRecordProcessor, +} from '@opentelemetry/sdk-logs'; +import { + AlwaysOffSampler, + AlwaysOnSampler, + BasicTracerProvider, + BatchSpanProcessor, + ConsoleSpanExporter, + ParentBasedSampler, + SimpleSpanProcessor, + SpanProcessor, + TraceIdRatioBasedSampler, +} from '@opentelemetry/sdk-trace-base'; +import { beforeEach, describe, expect, it, MockedFunction, vi } from 'vitest'; +import type { + ContextMatcher, + OpenTelemetryContextExtension, +} from '../src/plugin'; +import { + buildTestGateway, + disableAll, + getContextManager, + getPropagator, + getResource, + getSampler, + getSpanProcessors, + getTracerProvider, + MockLogRecordExporter, + setupOtelForTests, + spanExporter, +} from './utils'; - let gw: typeof import('../../../runtime/src'); - beforeAll(async () => { - gw = await import('../../../runtime/src'); - }); +describe('useOpenTelemetry', () => { beforeEach(() => { vi.clearAllMocks(); + spanExporter.reset(); }); - describe('when not passing a custom provider', () => { - it('initializes and starts a new provider', async () => { - const { useOpenTelemetry } = await import('../src'); - await using upstream = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello: String - } - `, - resolvers: { - Query: { - hello: () => 'World', - }, - }, - }), - logging: false, + + describe('setup', () => { + beforeEach(() => { + // Unregister all global OTEL apis, so that each tests can check for different setups + disableAll(); + }); + + it('should setup OTEL with sain default', () => { + openTelemetrySetup({ + contextManager: new AsyncLocalStorageContextManager(), + traces: { + exporter: new OTLPTraceExporter(), + }, }); - await using gateway = gw.createGatewayRuntime({ - proxy: { - endpoint: 'https://example.com/graphql', + // Check context manager + expect(getTracerProvider()).toBeInstanceOf(BasicTracerProvider); + expect(getContextManager()).toBeInstanceOf( + AsyncLocalStorageContextManager, + ); + + // Check processor. Should be a batched HTTP OTLP exporter + const processors = getSpanProcessors(); + expect(processors).toHaveLength(1); + expect(processors![0]).toBeInstanceOf(BatchSpanProcessor); + const processor = processors![0] as BatchSpanProcessor; + // @ts-ignore access private field + const exporter = processor._exporter as OTLPTraceExporter; + expect(exporter).toBeInstanceOf(OTLPTraceExporter); + + // Check Sampler + expect(getSampler()).toBeInstanceOf(AlwaysOnSampler); + + // Check Propagators + const propagator = getPropagator(); + expect(propagator).toBeInstanceOf(CompositePropagator); + // @ts-expect-error Access of private field + const propagators = propagator._propagators as TextMapPropagator[]; + expect(propagators).toContainEqual(expect.any(W3CBaggagePropagator)); + expect(propagators).toContainEqual(expect.any(W3CTraceContextPropagator)); + + const resource = getResource(); + expect(resource?.attributes).toMatchObject({ + 'service.name': '@graphql-mesh/plugin-opentelemetry', + }); + }); + + it('should register a custom TracerProvider', () => { + const tracerProvider: TracerProvider & { register: () => void } = { + register: vi.fn(), + getTracer: vi.fn(), + }; + + openTelemetrySetup({ + contextManager: null, + traces: { + tracerProvider, }, - plugins: (ctx) => [ - gw.useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, - ), - useOpenTelemetry({ - exporters: [], - ...ctx, - }), - ], - logging: false, }); - const response = await gateway.fetch('http://localhost:4000/graphql', { - method: 'POST', - headers: { - 'content-type': 'application/json', + expect(tracerProvider.register).toHaveBeenCalled(); + }); + + it('should not register a contextManager when passed null', () => { + const before = getContextManager(); + + openTelemetrySetup({ + contextManager: null, + }); + + expect(getContextManager()).toBe(before); + }); + + it('should register a console exporter', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + console: true, }, - body: JSON.stringify({ - query: /* GraphQL */ ` - query { - hello - } - `, + }); + + const processors = getSpanProcessors(); + expect(processors).toHaveLength(1); + // @ts-ignore access of private field + const exporter = processors![0]!._exporter; + expect(exporter).toBeInstanceOf(ConsoleSpanExporter); + }); + + it('should register a console exporter even if an exporter is given', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + exporter: new OTLPTraceExporter(), + console: true, + }, + }); + + const processors = getSpanProcessors(); + expect(processors).toHaveLength(2); + // @ts-ignore access of private field + const exporter = processors![1]!._exporter; + expect(exporter).toBeInstanceOf(ConsoleSpanExporter); + }); + + it('should register a console exporter even if a list of processors is given', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + processors: [new SimpleSpanProcessor(new OTLPTraceExporter())], + console: true, + }, + }); + + const processors = getSpanProcessors(); + expect(processors).toHaveLength(2); + // @ts-ignore access of private field + const exporter = processors![1]!._exporter; + expect(exporter).toBeInstanceOf(ConsoleSpanExporter); + }); + + it('should register a custom resource', () => { + openTelemetrySetup({ + resource: resourceFromAttributes({ + 'service.name': 'test-name', + 'service.version': 'test-version', + 'custom-attribute': 'test-value', }), + traces: { + console: true, + }, + contextManager: null, + }); + + expect(getResource()?.attributes).toMatchObject({ + 'service.name': 'test-name', + 'service.version': 'test-version', + 'custom-attribute': 'test-value', + }); + }); + + it.skipIf(!vi.stubEnv)( + 'should get service name and version from env var', + () => { + vi.stubEnv('OTEL_SERVICE_NAME', 'test-name'); + vi.stubEnv('OTEL_SERVICE_VERSION', 'test-version'); + + openTelemetrySetup({ + traces: { console: true }, + contextManager: null, + }); + + expect(getResource()?.attributes).toMatchObject({ + 'service.name': 'test-name', + 'service.version': 'test-version', + }); + + vi.unstubAllEnvs(); + }, + ); + + it('should allow to register a custom sampler', () => { + openTelemetrySetup({ + traces: { + console: true, + }, + contextManager: null, + sampler: new AlwaysOffSampler(), + }); + + expect(getSampler()).toBeInstanceOf(AlwaysOffSampler); + }); + + it('should allow to configure a rate sampling strategy', () => { + openTelemetrySetup({ + contextManager: null, + traces: { console: true }, + samplingRate: 0.1, }); - expect(response.status).toBe(200); - const body = await response.json(); - expect(body.data?.hello).toBe('World'); - expect(mockRegisterProvider).toHaveBeenCalledTimes(1); + const sampler = getSampler(); + expect(sampler).toBeInstanceOf(ParentBasedSampler); + + // @ts-ignore access private field + const rootSampler = sampler._root; + expect(rootSampler).toBeInstanceOf(TraceIdRatioBasedSampler); + + // @ts-ignore access private field + const rate = rootSampler._ratio; + expect(rate).toBe(0.1); + }); + + it('should allow to disable batching', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + exporter: new OTLPTraceExporter(), + batching: false, + }, + }); + + const [processor] = getSpanProcessors()!; + expect(processor).toBeInstanceOf(SimpleSpanProcessor); + }); + + it('should allow to configure batching', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + exporter: new OTLPTraceExporter(), + batching: { + maxExportBatchSize: 1, + maxQueueSize: 2, + scheduledDelayMillis: 3, + exportTimeoutMillis: 4, + }, + }, + }); + + const [processor] = getSpanProcessors()!; + expect(processor).toBeInstanceOf(BatchSpanProcessor); + expect(processor).toMatchObject({ + _maxExportBatchSize: 1, + _maxQueueSize: 2, + _scheduledDelayMillis: 3, + _exportTimeoutMillis: 4, + }); + }); + + it('should allow to manually define processor', () => { + const processor = {} as SpanProcessor; + openTelemetrySetup({ + contextManager: null, + traces: { + processors: [processor], + }, + }); + + const processors = getSpanProcessors(); + expect(processors).toHaveLength(1); + expect(getSpanProcessors()![0]).toBe(processor); + }); + + it('should allow to customize propagators', () => { + const propagator = {} as TextMapPropagator; + openTelemetrySetup({ + contextManager: null, + propagators: [propagator], + }); + + expect(getPropagator()).toBe(propagator); + }); + + it('should allow to customize propagators', () => { + const before = getPropagator(); + + openTelemetrySetup({ + contextManager: null, + propagators: [], + }); + + expect(getPropagator()).toBe(before); + }); + + it('should allow to customize limits', () => { + openTelemetrySetup({ + contextManager: null, + traces: { + console: true, + spanLimits: { + attributeCountLimit: 1, + attributePerEventCountLimit: 2, + attributePerLinkCountLimit: 3, + attributeValueLengthLimit: 4, + eventCountLimit: 5, + linkCountLimit: 6, + }, + }, + generalLimits: { + attributeCountLimit: 7, + attributeValueLengthLimit: 8, + }, + }); + + // @ts-ignore access private field + const registeredConfig = getTracerProvider()._config; + expect(registeredConfig).toMatchObject({ + spanLimits: { + attributeCountLimit: 1, + attributePerEventCountLimit: 2, + attributePerLinkCountLimit: 3, + attributeValueLengthLimit: 4, + eventCountLimit: 5, + linkCountLimit: 6, + }, + generalLimits: { + attributeCountLimit: 7, + attributeValueLengthLimit: 8, + }, + }); + }); + + it('should setup Hive Tracing', () => { + hiveTracingSetup({ + contextManager: new AsyncLocalStorageContextManager(), + target: 'target', + accessToken: 'access-token', + }); + + const processors = getSpanProcessors(); + expect(processors).toHaveLength(1); + expect(processors![0]).toBeInstanceOf(HiveTracingSpanProcessor); + const processor = processors![0] as HiveTracingSpanProcessor; + // @ts-expect-error Access of private field + const subProcessor = processor.processor as BatchSpanProcessor; + expect(subProcessor).toBeInstanceOf(BatchSpanProcessor); + // @ts-expect-error Access of private field + const exporter = subProcessor._exporter as OTLPTraceExporter; + expect(exporter).toBeInstanceOf(OTLPTraceExporter); + // @ts-expect-error Access of private field + expect(exporter._delegate._transport._transport._parameters.url).toBe( + 'http://localhost:4318/v1/traces', + ); }); }); - describe('when passing a custom provider', () => { - it('does not initialize a new provider and does not start the provided provider instance', async () => { - const { useOpenTelemetry } = await import('../src'); - await using upstream = createYoga({ - schema: createSchema({ - typeDefs: /* GraphQL */ ` - type Query { - hello: String + describe('tracing', () => { + beforeEach(() => { + // Register testing OTEL api with a custom Span processor and an Async Context Manager + disableAll(); + setupOtelForTests(); + }); + + describe.each([ + { name: 'with context manager', useContextManager: undefined }, + { name: 'without context manager', useContextManager: false as const }, + ])('$name', ({ useContextManager }) => { + const buildTestGatewayForCtx: typeof buildTestGateway = (options) => + buildTestGateway({ + ...options, + options: { useContextManager, ...options?.options }, + }); + + const expected = { + http: { + root: 'POST /graphql', + children: ['graphql.operation'], + }, + graphql: { + root: 'graphql.operation', + children: [ + 'graphql.parse', + 'graphql.validate', + 'graphql.context', + 'graphql.execute', + ], + }, + execute: { + root: 'graphql.execute', + children: ['subgraph.execute (upstream)'], + }, + subgraphExecute: { + root: 'subgraph.execute (upstream)', + children: ['http.fetch'], + }, + }; + + const allExpectedSpans: string[] = [ + expected.http.root, + ...Object.values(expected).flatMap(({ children }) => children), + ]; + + describe('span parenting', () => { + it('should register a complete span tree $name', async () => { + await using gateway = await buildTestGatewayForCtx(); + await gateway.query(); + + for (const { root, children } of Object.values(expected)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + + it('should allow to report custom spans', async () => { + const expectedCustomSpans = { + http: { root: 'POST /graphql', children: ['custom.request'] }, + graphql: { + root: 'graphql.operation', + children: ['custom.operation'], + }, + parse: { root: 'graphql.parse', children: ['custom.parse'] }, + validate: { + root: 'graphql.validate', + children: ['custom.validate'], + }, + context: { root: 'graphql.context', children: ['custom.context'] }, + execute: { root: 'graphql.execute', children: ['custom.execute'] }, + subgraphExecute: { + root: 'subgraph.execute (upstream)', + children: ['custom.subgraph'], + }, + fetch: { root: 'http.fetch', children: ['custom.fetch'] }, + }; + + await using gateway = await buildTestGatewayForCtx({ + plugins: ({ openTelemetry }) => { + const createSpan = (name: string) => (matcher: ContextMatcher) => + openTelemetry.tracer + ?.startSpan(name, {}, openTelemetry.getActiveContext(matcher)) + .end(); + + return [ + { + onRequest: createSpan('custom.request'), + onParams: createSpan('custom.operation'), + onParse: createSpan('custom.parse'), + onValidate: createSpan('custom.validate'), + onContextBuilding: createSpan('custom.context'), + onExecute: createSpan('custom.execute'), + onSubgraphExecute: createSpan('custom.subgraph'), + onFetch: createSpan('custom.fetch'), + }, + ]; + }, + }); + await gateway.query(); + + for (const { root, children } of Object.values(expectedCustomSpans)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + it('should allow to report custom spans using graphql context', async () => { + const expectedCustomSpans = { + parse: { root: 'graphql.parse', children: ['custom.parse'] }, + validate: { + root: 'graphql.validate', + children: ['custom.validate'], + }, + context: { root: 'graphql.context', children: ['custom.context'] }, + execute: { root: 'graphql.execute', children: ['custom.execute'] }, + }; + + await using gateway = await buildTestGatewayForCtx({ + plugins: () => { + const createSpan = (name: string) => (payload: any) => { + try { + const { context: gqlCtx, executionRequest } = payload; + const ctx: OpenTelemetryContextExtension = + gqlCtx ?? executionRequest?.context; + return ctx.openTelemetry.tracer + .startSpan(name, {}, ctx.openTelemetry.getActiveContext()) + .end(); + } catch (err) { + console.error(err); + } + }; + + return [ + { + onParse: createSpan('custom.parse'), + onValidate: createSpan('custom.validate'), + onContextBuilding: createSpan('custom.context'), + onExecute: createSpan('custom.execute'), + }, + ]; + }, + }); + await gateway.query(); + + for (const { root, children } of Object.values(expectedCustomSpans)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + + it('should report retries of execution requests', async () => { + let attempts = 0; + await using gateway = await buildTestGatewayForCtx({ + gatewayOptions: { + logging: true, + upstreamRetry: { + maxRetries: 2, + retryDelay: 1, + retryDelayFactor: 1, + }, + }, + fetch: + (upstreamFetch) => + (...args) => { + attempts = (attempts + 1) % 3; + return attempts === 0 + ? upstreamFetch(...args) + : new Response('', { status: 500 }); + }, + }); + await gateway.query(); + const rootSpan = spanExporter.assertRoot('POST /graphql'); + const subgraphSpan = rootSpan + .expectChild('graphql.operation') + .expectChild('graphql.execute') + .expectChild('subgraph.execute (upstream)'); + + for (let i = 0; i < 3; i++) { + const span = subgraphSpan.children[i]?.span; + expect(span).toBeDefined(); + expect(span!.name).toBe('http.fetch'); + if (i > 0) { + expect(span!.attributes).toMatchObject({ + 'http.request.resend_count': i, + }); } - `, - resolvers: { - Query: { - hello: () => 'World', + expect(span?.status.code).toBe( + i < 2 ? SpanStatusCode.ERROR : SpanStatusCode.OK, + ); + } + }); + }); + + describe('span configuration', () => { + it('should not trace http requests if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { http: false, schema: false }, + }, }, - }, - }), - logging: false, + }); + await gateway.query(); + + allExpectedSpans.forEach(spanExporter.assertNoSpanWithName); + }); + + it('should not trace graphql operation if disable', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { graphql: false, schema: false }, + }, + }, + }); + await gateway.query(); + + const httpSpan = spanExporter.assertRoot(expected.http.root); + expected.http.children + .filter((name) => name != expected.graphql.root) + .forEach(httpSpan.expectChild); + + [ + expected.graphql.root, + ...expected.graphql.children, + ...expected.execute.children, + ...expected.subgraphExecute.children, + ].forEach(spanExporter.assertNoSpanWithName); + }); + + it('should not trace parse if disable', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { graphqlParse: false }, + }, + }, + }); + await gateway.query(); + + spanExporter.assertNoSpanWithName('graphql.parse'); + + allExpectedSpans + .filter((name) => name != 'graphql.parse') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace validate if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { graphqlValidate: false }, + }, + }, + }); + await gateway.query(); + + spanExporter.assertNoSpanWithName('graphql.validate'); + + allExpectedSpans + .filter((name) => name != 'graphql.validate') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace context building if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { graphqlContextBuilding: false }, + }, + }, + }); + await gateway.query(); + + spanExporter.assertNoSpanWithName('graphql.context'); + + allExpectedSpans + .filter((name) => name != 'graphql.context') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace execute if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { graphqlExecute: false, schema: false }, + }, + }, + }); + await gateway.query(); + + [ + expected.execute.root, + ...expected.execute.children, + ...expected.subgraphExecute.children, + ].forEach(spanExporter.assertNoSpanWithName); + + [ + expected.http.root, + ...expected.http.children, + ...expected.graphql.children, + ] + .filter((name) => name != 'graphql.execute') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace subgraph execute if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { subgraphExecute: false, schema: false }, + }, + }, + }); + await gateway.query(); + + [ + expected.subgraphExecute.root, + ...expected.subgraphExecute.children, + ].forEach(spanExporter.assertNoSpanWithName); + + [ + expected.http.root, + ...expected.http.children, + ...expected.graphql.children, + ...expected.execute.children, + ] + .filter((name) => name !== 'subgraph.execute (upstream)') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace fetch if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + options: { + traces: { + spans: { upstreamFetch: false }, + }, + }, + }); + await gateway.query(); + + spanExporter.assertNoSpanWithName('http.fetch'); + + allExpectedSpans + .filter((name) => name !== 'http.fetch') + .forEach(spanExporter.assertSpanWithName); + }); + + it('should not trace fetch if disabled', async () => { + await using gateway = await buildTestGatewayForCtx({ + plugins: ({ fetch }) => { + return [ + { + onPluginInit() { + fetch('http://foo.bar', {}); + }, + }, + ]; + }, + }); + await gateway.query(); + + const initSpan = spanExporter.assertRoot('gateway.initialization'); + initSpan.expectChild('http.fetch'); + }); }); + }); + it('should allow to create custom spans without explicit context passing', async () => { + const expectedCustomSpans = { + http: { root: 'POST /graphql', children: ['custom.request'] }, + graphql: { + root: 'graphql.operation', + children: ['custom.operation'], + }, + parse: { root: 'graphql.parse', children: ['custom.parse'] }, + validate: { + root: 'graphql.validate', + children: ['custom.validate'], + }, + context: { root: 'graphql.context', children: ['custom.context'] }, + execute: { root: 'graphql.execute', children: ['custom.execute'] }, + subgraphExecute: { + root: 'subgraph.execute (upstream)', + children: ['custom.subgraph'], + }, + fetch: { root: 'http.fetch', children: ['custom.fetch'] }, + }; + + await using gateway = await buildTestGateway({ + plugins: ({ openTelemetry }) => { + const createSpan = (name: string) => () => + openTelemetry.tracer?.startSpan(name).end(); - await using gateway = gw.createGatewayRuntime({ - proxy: { - endpoint: 'https://example.com/graphql', + return [ + { + onRequest: createSpan('custom.request'), + onParams: createSpan('custom.operation'), + onParse: createSpan('custom.parse'), + onValidate: createSpan('custom.validate'), + onContextBuilding: createSpan('custom.context'), + onExecute: createSpan('custom.execute'), + onSubgraphExecute: createSpan('custom.subgraph'), + onFetch: createSpan('custom.fetch'), + }, + ]; }, - plugins: (ctx) => [ - gw.useCustomFetch( - // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch - upstream.fetch, + }); + await gateway.query(); + + for (const { root, children } of Object.values(expectedCustomSpans)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + + it('should have a response cache attribute', async () => { + function checkCacheAttributes(attrs: { + http: 'hit' | 'miss'; + operation?: 'hit' | 'miss'; + }) { + const { span: httpSpan } = spanExporter.assertRoot('POST /graphql'); + const operationSpan = spanExporter.spans.find(({ name }) => + name.startsWith('graphql.operation'), + ); + + expect(httpSpan.attributes['gateway.cache.response_cache']).toBe( + attrs.http, + ); + if (attrs.operation) { + expect(operationSpan).toBeDefined(); + expect( + operationSpan!.attributes['gateway.cache.response_cache'], + ).toBe(attrs.operation); + } + } + await using gateway = await buildTestGateway({ + gatewayOptions: { + cache: await import('@graphql-mesh/cache-localforage').then( + ({ default: Cache }) => new Cache(), ), - useOpenTelemetry({ initializeNodeSDK: false, ...ctx }), - ], - logging: false, + responseCaching: { + session: () => '1', + }, + }, }); + await gateway.query(); - const response = await gateway.fetch('http://localhost:4000/graphql', { + checkCacheAttributes({ http: 'miss', operation: 'miss' }); + + spanExporter.reset(); + await gateway.query(); + + checkCacheAttributes({ http: 'miss', operation: 'hit' }); + + spanExporter.reset(); + const response = await gateway.fetch('http://gateway/graphql', { method: 'POST', headers: { 'content-type': 'application/json', + 'If-None-Match': + 'c2f6fb105ef60ccc99dd6725b55939742e69437d4f85d52bf4664af3799c49fa', + 'If-Modified-Since': new Date(), }, - body: JSON.stringify({ - query: /* GraphQL */ ` - query { - hello + }); + expect(response.status).toBe(304); + + checkCacheAttributes({ http: 'hit' }); // There is no graphql operation span when cached by HTTP + }); + + it('should register schema loading span', async () => { + await using gateway = await buildTestGateway({ + options: { traces: { spans: { http: false, schema: true } } }, + }); + await gateway.query(); + + const schemaSpan = spanExporter.assertRoot('gateway.schema'); + + const descendants = schemaSpan.descendants.map(({ name }) => name); + + expect(descendants).toEqual([ + 'gateway.schema', + 'subgraph.execute (upstream)', + 'http.fetch', + ]); + }); + }); + + describe('logging', () => { + beforeEach(() => { + disableAll(); + }); + + describe('setup', () => { + beforeEach(() => { + disableAll(); + }); + + it('should allow to use a given logger', () => { + const logger = { emit: vi.fn() }; + const log = new Logger({ + writers: [ + new OpenTelemetryLogWriter({ + logger, + }), + ], + }); + + log.info({ foo: 'bar' }, 'test'); + + expect(logger.emit).toHaveBeenCalledWith({ + severityText: 'info', + severityNumber: SeverityNumber.INFO, + body: 'test', + attributes: { foo: 'bar' }, + context: ROOT_CONTEXT, + }); + }); + + it('should allow to use a provider', () => { + const processor: LogRecordProcessor = { + onEmit: vi.fn(), + forceFlush: vi.fn(), + shutdown: vi.fn(), + }; + const provider = new LoggerProvider({ processors: [processor] }); + const log = new Logger({ + writers: [ + new OpenTelemetryLogWriter({ + provider, + }), + ], + }); + + log.info({ foo: 'bar' }, 'test'); + + expect(processor.onEmit).toHaveBeenCalled(); + expect( + (processor.onEmit as MockedFunction) + .mock.calls[0]![0], + ).toMatchObject({ + severityText: 'info', + severityNumber: SeverityNumber.INFO, + body: 'test', + attributes: { foo: 'bar' }, + }); + }); + + it('should allow to provide a processor', () => { + const processor: LogRecordProcessor = { + onEmit: vi.fn(), + forceFlush: vi.fn(), + shutdown: vi.fn(), + }; + const log = new Logger({ + writers: [ + new OpenTelemetryLogWriter({ + processors: [processor], + }), + ], + }); + + log.info({ foo: 'bar' }, 'test'); + + expect(processor.onEmit).toHaveBeenCalled(); + expect( + (processor.onEmit as MockedFunction) + .mock.calls[0]![0], + ).toMatchObject({ + severityText: 'info', + severityNumber: SeverityNumber.INFO, + body: 'test', + attributes: { foo: 'bar' }, + }); + }); + + it('should allow to use an exporter', () => { + const exportFn = vi.fn(); + const exporter: LogRecordExporter = { + export: exportFn, + shutdown: vi.fn(), + }; + + const log = new Logger({ + writers: [ + new OpenTelemetryLogWriter({ + exporter, + batching: false, + }), + ], + }); + + log.info({ foo: 'bar' }, 'test'); + + expect(exportFn).toHaveBeenCalled(); + expect(exportFn.mock.calls[0]![0]).toMatchObject([ + { + severityText: 'info', + severityNumber: SeverityNumber.INFO, + body: 'test', + attributes: { foo: 'bar' }, + }, + ]); + }); + }); + + describe('logs correlation with span', () => { + const hooks = [ + 'onRequest', + 'onParams', + 'onParse', + 'onContextBuilding', + 'onValidate', + 'onExecute', + 'onSubgraphExecute', + 'onFetch', + ]; + const exporter = new MockLogRecordExporter(); + + const buildTestGatewayForLogs = ({ + useContextManager, + }: { useContextManager?: boolean } = {}) => + buildTestGateway({ + options: { useContextManager }, + gatewayOptions: { + logging: new Logger({ + writers: [ + new OpenTelemetryLogWriter({ + useContextManager, + exporter, + batching: false, + }), + ], + }), + }, + plugins: (ctx) => { + const createHook = (name: string): any => ({ + [name]: (payload: any) => { + let log = ctx.log; + const context = + payload.context ?? payload.executionRequest?.context; + const request = payload.request ?? context?.request; + if (request) { + log = loggerForRequest(log, request) ?? log; + } + if (payload.context) { + log = payload.context.log ?? log; + } + + const phase = + (name as string).charAt(2).toLowerCase() + + (name as string).substring(3); + log.info({ phase }, name); + }, + }); + + let plugin = {}; + for (let hook of hooks) { + plugin = { ...plugin, ...createHook(hook) }; } - `, - }), + + return [plugin]; + }, + }); + + beforeEach(() => { + disableAll(); + exporter.reset(); + }); + + it('should correlate logs with spans with context manager', async () => { + setupOtelForTests(); + + await using gateway = await buildTestGatewayForLogs(); + await gateway.query(); + + const expectedLogs = { + 'POST /graphql': 'onRequest', + 'graphql.operation': 'onParams', + 'graphql.parse': 'onParse', + 'graphql.validate': 'onValidate', + 'graphql.context': 'onContextBuilding', + 'graphql.execute': 'onExecute', + 'subgraph.execute (upstream)': 'onSubgraphExecute', + 'http.fetch': 'onFetch', + }; + + Object.entries(expectedLogs).forEach(([spanName, hook]) => { + const span = spanExporter.assertSpanWithName(spanName); + const logs = exporter.getLogsForSpan(span.id); + const phase = hook.charAt(2).toLowerCase() + hook.substring(3); + // @ts-expect-error Access to private field. public `body` seems to not be readable (returns undefined) + const log = logs.find((log) => log._body === hook); + if (!log) { + console.error( + `${hook} log not found. Logs for ${spanName} were`, + logs, + ); + throw new Error(`${hook} log not found`); + } + + expect(log.attributes).toMatchObject({ phase }); + }); + }); + + it('should correlate logs with root http span without a context manager', async () => { + setupOtelForTests({ contextManager: false }); + + await using gateway = await buildTestGatewayForLogs({ + useContextManager: false, + }); + await gateway.query(); + + const httpSpan = spanExporter.assertRoot('POST /graphql'); + const logs = exporter.getLogsForSpan(httpSpan.span.id); + for (let hook of hooks) { + const phase = hook.charAt(2).toLowerCase() + hook.substring(3); + // @ts-expect-error access to private field + const log = logs.find(({ _body: body }) => body === hook); + if (!log) { + console.error( + `missing log for ${hook}. Logs were:`, + exporter.records, + ); + throw new Error(`missing log for ${hook}`); + } + expect(log).toBeDefined(); + expect(log?.attributes).toMatchObject({ phase }); + } + }); + }); + }); + + describe('hive tracing', () => { + beforeEach(() => { + // Register testing OTEL api with a custom Span processor and an Async Context Manager + disableAll(); + hiveTracingSetup({ + target: 'test-target', + contextManager: new AsyncLocalStorageContextManager(), + processor: new SimpleSpanProcessor(spanExporter), + }); + }); + + it('should not report http spans', async () => { + await using gateway = await buildTestGateway(); + await gateway.query(); + + spanExporter.assertNoSpanWithName('POST /graphql'); + }); + + it('should have all attributes required by Hive Tracing', async () => { + await using gateway = await buildTestGateway({ + fetch: (upstreamFetch) => { + let calls = 0; + return (...args) => { + calls++; + if (calls > 1) + return Promise.resolve(new Response(null, { status: 500 })); + else return upstreamFetch(...args); + }; + }, + }); + await gateway.query({ + shouldReturnErrors: true, + body: { query: 'query testOperation { hello }' }, + headers: { + 'x-graphql-client-name': 'test-client-name', + 'x-graphql-client-version': 'test-client-version', + }, + }); + + const operationSpan = spanExporter.assertRoot( + 'graphql.operation testOperation', + ); + + expect(operationSpan.span.resource.attributes).toMatchObject({ + 'hive.target_id': 'test-target', }); - expect(response.status).toBe(200); - const body = await response.json(); - expect(body.data?.hello).toBe('World'); - expect(mockRegisterProvider).not.toHaveBeenCalled(); + // Root span + expect(operationSpan.span.attributes).toMatchObject({ + // HTTP Attributes + [SEMATTRS_HTTP_METHOD]: 'POST', + [SEMATTRS_HTTP_URL]: 'http://localhost:4000/graphql', + [SEMATTRS_HTTP_ROUTE]: '/graphql', + [SEMATTRS_HTTP_SCHEME]: 'http:', + [SEMATTRS_NET_HOST_NAME]: 'localhost', + [SEMATTRS_HTTP_HOST]: 'localhost:4000', + [SEMATTRS_HTTP_STATUS_CODE]: 500, + + // Hive specific + ['hive.client.name']: 'test-client-name', + ['hive.client.version']: 'test-client-version', + + // Operation Attributes + [SEMATTRS_GRAPHQL_DOCUMENT]: 'query testOperation{hello}', + [SEMATTRS_GRAPHQL_OPERATION_NAME]: 'testOperation', + [SEMATTRS_GRAPHQL_OPERATION_TYPE]: 'query', + [SEMATTRS_HIVE_GRAPHQL_OPERATION_HASH]: + 'd40f732de805d03db6284b9b8c6c6f0b', + [SEMATTRS_HIVE_GRAPHQL_ERROR_COUNT]: 1, + [SEMATTRS_HIVE_GRAPHQL_ERROR_CODES]: ['DOWNSTREAM_SERVICE_ERROR'], + + // Execution Attributes + [SEMATTRS_HIVE_GATEWAY_OPERATION_SUBGRAPH_NAMES]: ['upstream'], + }); + + // Subgraph Execution Span + expect( + spanExporter.assertRoot('subgraph.execute (upstream)').span.attributes, + ).toMatchObject({ + // HTTP Attributes + [SEMATTRS_HTTP_METHOD]: 'POST', + [SEMATTRS_HTTP_URL]: 'https://example.com/graphql', + [SEMATTRS_HTTP_ROUTE]: '/graphql', + [SEMATTRS_HTTP_SCHEME]: 'https:', + [SEMATTRS_NET_HOST_NAME]: 'example.com', + [SEMATTRS_HTTP_HOST]: 'example.com', + [SEMATTRS_HTTP_STATUS_CODE]: 500, + + // Operation Attributes + [SEMATTRS_GRAPHQL_DOCUMENT]: 'query testOperation{hello}', + [SEMATTRS_GRAPHQL_OPERATION_TYPE]: 'query', + [SEMATTRS_GRAPHQL_OPERATION_NAME]: 'testOperation', + + // Federation attributes + [SEMATTRS_HIVE_GATEWAY_UPSTREAM_SUBGRAPH_NAME]: 'upstream', + }); }); }); }); diff --git a/packages/plugins/opentelemetry/tests/utils.ts b/packages/plugins/opentelemetry/tests/utils.ts new file mode 100644 index 000000000..209c618b7 --- /dev/null +++ b/packages/plugins/opentelemetry/tests/utils.ts @@ -0,0 +1,347 @@ +import { + GatewayConfigContext, + GatewayConfigProxy, + GatewayPlugin, +} from '@graphql-hive/gateway'; +import { MeshFetch } from '@graphql-mesh/types'; +import { + context, + diag, + metrics, + propagation, + ProxyTracerProvider, + trace, + TraceState, + type TextMapPropagator, +} from '@opentelemetry/api'; +import { logs } from '@opentelemetry/api-logs'; +import { AsyncLocalStorageContextManager } from '@opentelemetry/context-async-hooks'; +import { ExportResultCode, type ExportResult } from '@opentelemetry/core'; +import { LogRecordExporter, ReadableLogRecord } from '@opentelemetry/sdk-logs'; +import { + BasicTracerProvider, + SimpleSpanProcessor, + type ReadableSpan, + type SpanExporter, + type TracerConfig, +} from '@opentelemetry/sdk-trace-base'; +import { AsyncDisposableStack } from '@whatwg-node/disposablestack'; +import { createSchema, createYoga, type GraphQLParams } from 'graphql-yoga'; +import { expect } from 'vitest'; +import type { OpenTelemetryGatewayPluginOptions } from '../src/plugin'; + +export async function buildTestGateway( + options: { + gatewayOptions?: Omit; + options?: OpenTelemetryGatewayPluginOptions; + plugins?: ( + ctx: GatewayConfigContext, + ) => GatewayPlugin[]; + fetch?: (upstreamFetch: MeshFetch) => MeshFetch; + } = {}, +) { + const gw = await import('../../../runtime/src'); + const { useOpenTelemetry } = await import('../src'); + const stack = new AsyncDisposableStack(); + + const upstream = stack.use( + createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => 'World', + }, + }, + }), + logging: false, + }), + ); + + let otelPlugin: ReturnType; + + const gateway = stack.use( + gw.createGatewayRuntime({ + proxy: { + endpoint: 'https://example.com/graphql', + }, + maskedErrors: false, + plugins: (ctx) => { + otelPlugin = useOpenTelemetry({ + ...ctx, + ...options.options, + }); + return [ + gw.useCustomFetch( + // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch + options.fetch ? options.fetch(upstream.fetch) : upstream.fetch, + ), + otelPlugin, + ...(options.plugins?.(ctx) ?? []), + ]; + }, + logging: true, + ...options.gatewayOptions, + }), + ); + + return { + otelPlugin: otelPlugin!, + query: async ({ + shouldReturnErrors, + body = { + query: /* GraphQL */ ` + query { + hello + } + `, + }, + headers, + }: { + body?: GraphQLParams; + shouldReturnErrors?: boolean; + headers?: Record; + } = {}) => { + const response = await gateway.fetch('http://localhost:4000/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + ...headers, + }, + body: JSON.stringify(body), + }); + + const result = await response.json(); + if (shouldReturnErrors) { + expect(result.errors).toBeDefined(); + } else { + if (result.errors) { + console.error(result.errors); + } + expect(result.errors).not.toBeDefined(); + } + return result; + }, + fetch: gateway.fetch, + [Symbol.asyncDispose]: () => { + diag.disable(); + return stack.disposeAsync(); + }, + }; +} + +export class MockSpanExporter implements SpanExporter { + spans: Span[] = []; + + export( + spans: ReadableSpan[], + resultCallback: (result: ExportResult) => void, + ): void { + this.spans.push( + ...spans.map((span) => ({ + ...span, + traceId: span.spanContext().traceId, + traceState: span.spanContext().traceState, + id: span.spanContext().spanId, + })), + ); + setTimeout(() => resultCallback({ code: ExportResultCode.SUCCESS }), 0); + } + shutdown() { + this.reset(); + return Promise.resolve(); + } + forceFlush() { + this.reset(); + return Promise.resolve(); + } + reset() { + this.spans = []; + } + + buildSpanNode = (span: Span): TraceTreeNode => + new TraceTreeNode( + span, + this.spans + .filter( + ({ parentSpanContext }) => parentSpanContext?.spanId === span.id, + ) + .map(this.buildSpanNode), + ); + + assertRoot(rootName: string): TraceTreeNode { + const root = this.spans.find(({ name }) => name === rootName); + if (!root) { + console.error( + `failed to find "${rootName}". Spans are: `, + this.toString(), + ); + throw new Error( + `No root span found with name '${rootName}'. Span names are: ${this.toString()}`, + ); + } + return this.buildSpanNode(root); + } + + assertNoSpanWithName = (name: string) => { + expect(this.spans.map(({ name }) => name)).not.toContain(name); + }; + + assertSpanWithName = (name: string) => { + const span = this.spans.find((span) => span.name === name); + expect(span).toBeDefined(); + return span!; + }; + + toString() { + return this.spans.map((span) => span.name); + } +} + +export class TraceTreeNode { + constructor( + public span: Span, + public children: TraceTreeNode[], + ) {} + + expectChild = (name: string): TraceTreeNode => { + const child = this.children.find((child) => child.span.name === name); + if (!child) { + console.error(`No child span with name "${name}" in:\n`, this.toString()); + throw new Error( + `No child span found with name '${name}'. Children names are: ${this.children.map((child) => `\n\t- ${child.span.name}`)}`, + ); + } + return child; + }; + + get length() { + return this.children.length; + } + + get descendants(): Span[] { + return [this.span, ...this.children.flatMap((c) => c.descendants)]; + } + + toString(prefix = '') { + return `${prefix}-- ${this.span.name}\n${this.children.map((c): string => c.toString(prefix + ' |')).join('')}`; + } +} + +export type Span = ReadableSpan & { + traceId: string; + traceState?: TraceState; + id: string; +}; + +export const spanExporter = new MockSpanExporter(); +const traceProvider = new BasicTracerProvider({ + spanProcessors: [new SimpleSpanProcessor(spanExporter)], +}); + +export function setupOtelForTests({ + contextManager, +}: { + contextManager?: boolean; +} = {}) { + trace.setGlobalTracerProvider(traceProvider); + if (contextManager !== false) { + context.setGlobalContextManager(new AsyncLocalStorageContextManager()); + } +} + +export const getContextManager = () => { + // @ts-expect-error Access to private method for test purpose + return context._getContextManager() as Context; +}; + +export const getTracerProvider = () => { + return (trace.getTracerProvider() as ProxyTracerProvider).getDelegate(); +}; + +export const getPropagator = () => { + // @ts-expect-error Access to private method for test purpose + return propagation._getGlobalPropagator() as TextMapPropagator; +}; + +export const getTracerProviderConfig = () => { + return ( + // @ts-expect-error Access to private method for test purpose + (getTracerProvider() as BasicTracerProvider)._config as TracerConfig + ); +}; + +export const getSampler = () => { + return getTracerProviderConfig().sampler; +}; + +export const getSpanProcessors = () => { + return getTracerProviderConfig().spanProcessors; +}; + +export const getResource = () => { + return getTracerProviderConfig().resource; +}; + +export const getLimits = () => { + const { spanLimits, generalLimits } = getTracerProviderConfig(); + return { spanLimits, generalLimits }; +}; + +export const disableAll = () => { + trace.disable(); + context.disable(); + propagation.disable(); + metrics.disable(); + diag.disable(); + logs.disable(); +}; + +export class MockLogRecordExporter implements LogRecordExporter { + records: LogRecord[] = []; + + export( + logs: ReadableLogRecord[], + resultCallback: (result: ExportResult) => void, + ): void { + this.records.push( + ...logs.map((record) => ({ + ...record, + traceId: record.spanContext?.traceId, + spanId: record.spanContext?.spanId, + })), + ); + resultCallback({ code: ExportResultCode.SUCCESS }); + } + + shutdown(): Promise { + this.reset(); + return Promise.resolve(); + } + + forceFlush(): Promise { + this.reset(); + return Promise.resolve(); + } + + reset() { + this.records = []; + } + + getLogsForSpan(spanId: string) { + return this.records.filter((record) => record.spanId === spanId); + } + + getLogsForTrace(traceId: string) { + return this.records.filter((record) => record.traceId === traceId); + } +} + +export type LogRecord = ReadableLogRecord & { + traceId?: string; + spanId?: string; +}; diff --git a/packages/plugins/opentelemetry/tests/yoga.spec.ts b/packages/plugins/opentelemetry/tests/yoga.spec.ts new file mode 100644 index 000000000..257fa6d2e --- /dev/null +++ b/packages/plugins/opentelemetry/tests/yoga.spec.ts @@ -0,0 +1,147 @@ +import { Logger } from '@graphql-hive/logger'; +import { useOpenTelemetry } from '@graphql-mesh/plugin-opentelemetry'; +import { createSchema, createYoga, Plugin as YogaPlugin } from 'graphql-yoga'; +import { beforeAll, beforeEach, describe, expect, it } from 'vitest'; +import { ContextMatcher } from '../src/plugin'; +import { disableAll, setupOtelForTests, spanExporter } from './utils'; + +describe('useOpenTelemetry', () => { + beforeAll(() => { + disableAll(); + setupOtelForTests(); + }); + + beforeEach(() => { + spanExporter.reset(); + }); + + describe('usage with Yoga', () => { + describe.each([ + { name: 'with context manager', contextManager: undefined }, + { name: 'without context manager', contextManager: false as const }, + ])('$name', ({ contextManager }) => { + function buildTest( + options: { + plugins?: ( + otelPlugin: ReturnType, + ) => YogaPlugin[]; + } = {}, + ) { + const otelPlugin = useOpenTelemetry({ + log: new Logger({ level: false }), + useContextManager: contextManager, + }); + + const yoga = createYoga({ + schema: createSchema({ + typeDefs: /* GraphQL */ ` + type Query { + hello: String + } + `, + resolvers: { + Query: { + hello: () => 'World', + }, + }, + }), + logging: false, + maskedErrors: false, + plugins: [otelPlugin, ...(options.plugins?.(otelPlugin) ?? [])], + }); + + return { + query: async () => { + const response = await yoga.fetch('http://yoga/graphql', { + method: 'POST', + headers: { + 'content-type': 'application/json', + }, + body: JSON.stringify({ query: '{ hello }' }), + }); + expect(response.status).toBe(200); + const result = await response.json(); + if (result.errors) { + console.error('Graphql Errors:', result.errors); + } + expect(result.errors).not.toBeDefined(); + }, + [Symbol.asyncDispose]: async () => { + await yoga.dispose(); + }, + }; + } + + const expected = { + http: { + root: 'POST /graphql', + children: ['graphql.operation'], + }, + graphql: { + root: 'graphql.operation', + children: [ + 'graphql.parse', + 'graphql.validate', + 'graphql.context', + 'graphql.execute', + ], + }, + }; + + describe('span parenting', () => { + it('should register a complete span tree $name', async () => { + await using gateway = buildTest(); + await gateway.query(); + + for (const { root, children } of Object.values(expected)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + + it('should allow to report custom spans', async () => { + const expectedCustomSpans = { + http: { root: 'POST /graphql', children: ['custom.request'] }, + graphql: { + root: 'graphql.operation', + children: ['custom.operation'], + }, + parse: { root: 'graphql.parse', children: ['custom.parse'] }, + validate: { + root: 'graphql.validate', + children: ['custom.validate'], + }, + context: { root: 'graphql.context', children: ['custom.context'] }, + execute: { root: 'graphql.execute', children: ['custom.execute'] }, + }; + + await using yoga = buildTest({ + plugins: (openTelemetry) => { + const createSpan = (name: string) => (matcher: ContextMatcher) => + openTelemetry.tracer + ?.startSpan(name, {}, openTelemetry.getActiveContext(matcher)) + .end(); + + return [ + { + onRequest: createSpan('custom.request'), + onParams: createSpan('custom.operation'), + onParse: createSpan('custom.parse'), + onValidate: createSpan('custom.validate'), + onContextBuilding: createSpan('custom.context'), + onExecute: createSpan('custom.execute'), + }, + ]; + }, + }); + await yoga.query(); + + for (const { root, children } of Object.values(expectedCustomSpans)) { + const spanTree = spanExporter.assertRoot(root); + children.forEach(spanTree.expectChild); + } + }); + }); + }); + }); +}); diff --git a/packages/plugins/prometheus/package.json b/packages/plugins/prometheus/package.json index 2227a3e79..d31193df2 100644 --- a/packages/plugins/prometheus/package.json +++ b/packages/plugins/prometheus/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -44,6 +44,7 @@ }, "dependencies": { "@graphql-hive/gateway-runtime": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-mesh/cross-helpers": "^0.4.10", "@graphql-mesh/types": "^0.104.7", "@graphql-mesh/utils": "^0.104.7", diff --git a/packages/plugins/prometheus/src/index.ts b/packages/plugins/prometheus/src/index.ts index 00e2aa24b..3918ac141 100644 --- a/packages/plugins/prometheus/src/index.ts +++ b/packages/plugins/prometheus/src/index.ts @@ -1,12 +1,11 @@ -import { type GatewayPlugin } from '@graphql-hive/gateway-runtime'; +import type { GatewayPlugin, OnFetchHook } from '@graphql-hive/gateway-runtime'; +import type { Logger } from '@graphql-hive/logger'; import type { OnSubgraphExecuteHook } from '@graphql-mesh/fusion-runtime'; import type { TransportEntry } from '@graphql-mesh/transport-common'; import type { ImportFn, - Logger, MeshFetchRequestInit, MeshPlugin, - OnFetchHook, } from '@graphql-mesh/types'; import { defaultImportFn, @@ -140,12 +139,11 @@ type MeshMetricsConfig = { */ fetchResponseHeaders?: boolean | string[]; }; - /** * The logger instance used by the plugin to log messages. * This should be the logger instance provided by Mesh in the plugins context. */ - logger: Logger; + log: Logger; }; export type PrometheusPluginOptions = PrometheusTracingPluginConfig & @@ -369,7 +367,7 @@ export default function useMeshPrometheus( } function registryFromYamlConfig( - config: YamlConfig & { logger: Logger }, + config: YamlConfig & { log: Logger }, ): Registry { if (!config.registry) { throw new Error('Registry not defined in the YAML config'); @@ -393,7 +391,9 @@ function registryFromYamlConfig( registry$ .then(() => registryProxy.revoke()) - .catch((e) => config.logger.error(e)); + .catch((e) => + config.log.error(e, '[usePrometheus] Failed to load Prometheus registry'), + ); return registryProxy.proxy; } diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index 0e02e764c..3d39226ec 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/runtime/assets/icon-256x256.png b/packages/runtime/assets/icon-256x256.png new file mode 100644 index 000000000..2e07863e8 Binary files /dev/null and b/packages/runtime/assets/icon-256x256.png differ diff --git a/packages/runtime/assets/icon.png b/packages/runtime/assets/icon.png new file mode 100644 index 000000000..bc4a29d9c Binary files /dev/null and b/packages/runtime/assets/icon.png differ diff --git a/packages/runtime/assets/logo.svg b/packages/runtime/assets/logo.svg new file mode 100644 index 000000000..cd353e2b3 --- /dev/null +++ b/packages/runtime/assets/logo.svg @@ -0,0 +1,11 @@ + + + + + + + + + diff --git a/packages/runtime/package.json b/packages/runtime/package.json index a8d59cbc3..f527cd8c3 100644 --- a/packages/runtime/package.json +++ b/packages/runtime/package.json @@ -15,7 +15,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -37,8 +37,9 @@ ], "scripts": { "build": "pkgroll --clean-dist", - "generate-landing-page": "tsx scripts/generate-landing-page-html.ts", - "prepack": "yarn generate-landing-page && yarn build" + "prepack": "yarn render-landing-page && yarn build", + "render-landing-page": "tsx scripts/render-landing-page.tsx", + "serve-landing-page": "tsx watch --include scripts/LandingPage.css scripts/serve-landing-page.tsx" }, "peerDependencies": { "graphql": "^15.9.0 || ^16.9.0" @@ -47,8 +48,9 @@ "@envelop/core": "^5.3.0", "@envelop/disable-introspection": "^8.0.0", "@envelop/generic-auth": "^9.0.0", + "@envelop/instrumentation": "^1.0.0", "@graphql-hive/core": "^0.13.0", - "@graphql-hive/logger-json": "workspace:^", + "@graphql-hive/logger": "workspace:^", "@graphql-hive/pubsub": "workspace:^", "@graphql-hive/signal": "workspace:^", "@graphql-hive/yoga": "^0.42.2", @@ -71,6 +73,7 @@ "@graphql-yoga/plugin-csrf-prevention": "^3.15.1", "@graphql-yoga/plugin-defer-stream": "^3.15.1", "@graphql-yoga/plugin-persisted-operations": "^3.15.1", + "@opentelemetry/api": "^1.9.0", "@types/node": "^22.15.30", "@whatwg-node/disposablestack": "^0.0.6", "@whatwg-node/promise-helpers": "^1.3.0", @@ -86,12 +89,16 @@ "@graphql-mesh/transport-rest": "^0.9.8", "@omnigraph/openapi": "^0.109.11", "@types/html-minifier-terser": "^7.0.2", + "@types/react": "^19", + "@types/react-dom": "^19", "@whatwg-node/fetch": "^0.10.9", "fets": "^0.8.4", "graphql": "^16.9.0", "graphql-sse": "^2.5.3", "html-minifier-terser": "7.2.0", "pkgroll": "2.15.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "tsx": "4.20.3" }, "sideEffects": false diff --git a/packages/runtime/scripts/LandingPage.css b/packages/runtime/scripts/LandingPage.css new file mode 100644 index 000000000..041a8f6ae --- /dev/null +++ b/packages/runtime/scripts/LandingPage.css @@ -0,0 +1,184 @@ +:root { + --background-color: #a5c4cb; + --background-light-color: #adcad0; + --primary-color: #00342c; + --accent-color: #e1ff00; + --primary-light-color: #245850; + --primary-light-hover-color: #3b736a; +} + +* { + box-sizing: border-box; +} + +body, +html { + padding: 50px 25px; + margin: 0; + background-color: var(--background-color); + color: var(--primary-color); + font-family: + -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, + sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol'; +} +main { + z-index: 1; + position: relative; +} +footer { + margin-top: 50px; + text-align: center; +} +footer { + display: flex; + justify-content: center; + align-items: center; +} +.the-guild { + display: flex; + margin-left: 5px; + color: black; + width: 3.2rem; +} +.the-guild svg.logo { + width: 2em; +} +.the-guild svg.name { + margin-left: 3px; + width: 2.5em; +} +.watermark { + z-index: 0; + position: absolute; + overflow: hidden; + top: 0; + left: 0; + width: 100%; + height: 100%; +} +a { + /* color: var(--accent-color); */ + color: var(--primary-color); + font-weight: bold; +} +code { + background-color: white; + color: var(--primary-color); + padding: 4px; + border-radius: 4px; + font-family: monospace; +} + +pre { + max-width: 100%; +} +pre code { + border-radius: 16px; +} + +.hero { + display: flex; + flex-direction: column; + align-items: center; +} +.hero .logo { + font-size: 2rem; + display: flex; + flex-wrap: wrap; + justify-content: center; + gap: 10px; +} +.hero .logo svg { + min-width: 50px; + max-width: 50px; +} +.hero .logo h1 { + margin: 0; + text-align: center; +} +.hero .description { + color: #245850; + max-width: 600px; + text-align: center; +} +.hero .links { + display: flex; + gap: 10px; + flex-wrap: wrap; + justify-content: center; +} + +.content { + position: relative; + margin: 50px auto; + display: flex; + flex-direction: column; + max-width: 800px; + align-items: center; +} +.content p { + text-align: center; +} + +.shell { + background-color: var(--background-light-color); + border-radius: 16px; + display: flex; + padding: 6px 16px; +} +.shell .dollar { + margin-top: 11px; /* Align dollar sign with the command text */ + margin-right: 10px; + color: var(--accent-color); +} +.shell .command { + user-select: all; + /* color: white; */ +} + +.curl code { + padding-left: 30px !important; + background-color: var(--primary-light-color); + color: white; +} + +.four-oh-four { + position: relative; + display: flex; + flex-direction: column; + align-items: center; + border-radius: 16px; + margin: 0 auto; + padding: 20px 30px; + max-width: 800px; + opacity: 0.4; + transition: opacity 0.3s ease-in-out; +} +.four-oh-four:hover { + opacity: 1; +} +.four-oh-four p { + text-align: center; +} + +a.button { + display: inline-block; + padding: 12px 24px; + font-size: 1.2rem; + transition: background-color 0.1s ease-in-out; + color: white; + background-color: var(--primary-light-color); + text-decoration: none; + border-radius: 8px; + border: 1px solid var(--primary-light-color); +} +a.button:hover { + background-color: var(--primary-light-hover-color); +} +a.button.accent { + color: black; + background-color: var(--accent-color); +} +a.button.accent:hover { + background-color: white; +} diff --git a/packages/runtime/scripts/LandingPage.tsx b/packages/runtime/scripts/LandingPage.tsx new file mode 100644 index 000000000..ac35d42f2 --- /dev/null +++ b/packages/runtime/scripts/LandingPage.tsx @@ -0,0 +1,206 @@ +import fs from 'node:fs'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { SVGProps } from 'react'; + +const __dirname = path.dirname(fileURLToPath(import.meta.url)); + +const styles = fs.readFileSync( + path.join(__dirname, 'LandingPage.css'), + 'utf-8', +); + +export const iconBase64 = Buffer.from( + fs.readFileSync(path.join(__dirname, '..', 'assets', 'icon-256x256.png')), +).toString('base64'); + +export interface LandingPageProps { + productName?: string; + productDescription?: string; + productLink?: string; + productPackageName?: string; + graphiqlPathname?: string; + graphqlUrl?: string; + requestPathname?: string; +} + +export function LandingPage(props: LandingPageProps) { + const { + productName = '__PRODUCT_NAME__', + productDescription = '__PRODUCT_DESCRIPTION__', + productLink = '__PRODUCT_LINK__', + productPackageName = '__PRODUCT_PACKAGE_NAME__', + graphiqlPathname = '__GRAPHIQL_PATHNAME__', + graphqlUrl = '__GRAPHQL_URL__', + requestPathname = '__REQUEST_PATHNAME__', + } = props; + return ( + + + + {`Welcome to ${productName}`} + + + + + + + + +

__PRODUCT_DESCRIPTION__

__SUBGRAPH_HTML__

ℹ️ Not the Page You Expected To See?

This page is shown be default whenever a 404 is hit.
You can disable this by behavior via the landingPage option.

import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';\n\nexport const gatewayConfig = defineConfig({\n  landingPage: false,\n});\n

If you expected this page to be the GraphQL route, you need to configure Hive Gateway.
Currently, the GraphQL route is configured to be on __GRAPHIQL_LINK__.

import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';\n\nexport const gatewayConfig = defineConfig({\n  graphqlEndpoint: '__REQUEST_PATH__',\n});\n
" \ No newline at end of file diff --git a/packages/runtime/src/landing-page.generated.ts b/packages/runtime/src/landing-page.generated.ts new file mode 100644 index 000000000..be3acd29a --- /dev/null +++ b/packages/runtime/src/landing-page.generated.ts @@ -0,0 +1,3 @@ +export const iconBase64 = "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"; +export const logoSvg = " "; +export const html = "Welcome to __PRODUCT_NAME__

__PRODUCT_DESCRIPTION__


You can interact with this endpoint by sending a POST request

$
curl --url '__GRAPHQL_URL__' \\\n  --header 'content-type: application/json' \\\n  --data '{"query":"{ __typename }"}'

Not the Page You Expected To See?

This page is shown be default whenever a 404 is hit.
You can disable this by behavior via the landingPage option.

import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';\n\nexport const gatewayConfig = defineConfig({\n  landingPage: false,\n});

If you expected this page to be the GraphQL route, you need to configure Hive Gateway.
Currently, the GraphQL route is configured to be on __GRAPHIQL_PATHNAME__.

import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';\n\nexport const gatewayConfig = defineConfig({\n  graphqlEndpoint: '__REQUEST_PATHNAME__',\n});\n
Developed with ❤️ by
"; diff --git a/packages/runtime/src/landing-page.html b/packages/runtime/src/landing-page.html deleted file mode 100644 index 65e54eb5c..000000000 --- a/packages/runtime/src/landing-page.html +++ /dev/null @@ -1,204 +0,0 @@ - - - - - Welcome to __PRODUCT_NAME__ - - - - - - - - - - - - -
-
- -

__PRODUCT_DESCRIPTION__

- -
-
__SUBGRAPH_HTML__
-
-

ℹ️ Not the Page You Expected To See?

-

- This page is shown be default whenever a 404 is hit.
You can - disable this by behavior via the - landingPage - option. -

-
import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';
-
-export const gatewayConfig = defineConfig({
-  landingPage: false,
-});
-
-

- If you expected - this - page to be the GraphQL route, you need to configure Hive Gateway.
Currently, - the GraphQL route is configured to be on - __GRAPHIQL_LINK__. -

-
import { defineConfig } from '__PRODUCT_PACKAGE_NAME__';
-
-export const gatewayConfig = defineConfig({
-  graphqlEndpoint: '__REQUEST_PATH__',
-});
-
-
-
- - diff --git a/packages/runtime/src/plugins/useCacheDebug.ts b/packages/runtime/src/plugins/useCacheDebug.ts index 480bf4da0..304ba8a74 100644 --- a/packages/runtime/src/plugins/useCacheDebug.ts +++ b/packages/runtime/src/plugins/useCacheDebug.ts @@ -1,48 +1,52 @@ -import { Logger } from '@graphql-mesh/types'; +import type { Logger } from '@graphql-hive/logger'; import { GatewayPlugin } from '../types'; -export function useCacheDebug>(opts: { - logger: Logger; +export function useCacheDebug>({ + log: rootLog, +}: { + log: Logger; }): GatewayPlugin { return { + onContextBuilding({ context }) { + // onContextBuilding might not execute at all so we use the root log + rootLog = context.log; + }, onCacheGet({ key }) { + const log = rootLog.child({ key }, '[useCacheDebug] '); + log.debug('Get'); return { onCacheGetError({ error }) { - const cacheGetErrorLogger = opts.logger.child('cache-get-error'); - cacheGetErrorLogger.error({ key, error }); + log.error({ key, error }, 'Error'); }, onCacheHit({ value }) { - const cacheHitLogger = opts.logger.child('cache-hit'); - cacheHitLogger.debug({ key, value }); + log.debug({ key, value }, 'Hit'); }, onCacheMiss() { - const cacheMissLogger = opts.logger.child('cache-miss'); - cacheMissLogger.debug({ key }); + log.debug({ key }, 'Miss'); }, }; }, onCacheSet({ key, value, ttl }) { + const log = rootLog.child({ key, value, ttl }, '[useCacheDebug] '); + log.debug('Set'); return { onCacheSetError({ error }) { - const cacheSetErrorLogger = opts.logger.child('cache-set-error'); - cacheSetErrorLogger.error({ key, value, ttl, error }); + log.error({ error }, 'Error'); }, onCacheSetDone() { - const cacheSetDoneLogger = opts.logger.child('cache-set-done'); - cacheSetDoneLogger.debug({ key, value, ttl }); + log.debug('Done'); }, }; }, onCacheDelete({ key }) { + const log = rootLog.child({ key }, '[useCacheDebug] '); + log.debug('Delete'); return { onCacheDeleteError({ error }) { - const cacheDeleteErrorLogger = - opts.logger.child('cache-delete-error'); - cacheDeleteErrorLogger.error({ key, error }); + log.error({ error }, 'Error'); }, onCacheDeleteDone() { - const cacheDeleteDoneLogger = opts.logger.child('cache-delete-done'); - cacheDeleteDoneLogger.debug({ key }); + log.debug('Done'); }, }; }, diff --git a/packages/runtime/src/plugins/useContentEncoding.ts b/packages/runtime/src/plugins/useContentEncoding.ts index b8bd8689c..bf21fc7e3 100644 --- a/packages/runtime/src/plugins/useContentEncoding.ts +++ b/packages/runtime/src/plugins/useContentEncoding.ts @@ -11,7 +11,6 @@ export function useContentEncoding>({ subgraphs, }: UseContentEncodingOpts = {}): GatewayPlugin { if (!subgraphs?.length) { - // @ts-expect-error - Return types are not compatible return useOrigContentEncoding(); } const compressionAlgorithm: CompressionFormat = 'gzip'; @@ -22,10 +21,7 @@ export function useContentEncoding>({ fetchAPI = yoga.fetchAPI; }, onPluginInit({ addPlugin }) { - addPlugin( - // @ts-expect-error - Plugin types do not match - useOrigContentEncoding(), - ); + addPlugin(useOrigContentEncoding()); }, onSubgraphExecute({ subgraphName, executionRequest }) { if (subgraphs.includes(subgraphName) || subgraphs.includes('*')) { diff --git a/packages/runtime/src/plugins/useCustomAgent.ts b/packages/runtime/src/plugins/useCustomAgent.ts index 8211b3dc6..3f61a2cb5 100644 --- a/packages/runtime/src/plugins/useCustomAgent.ts +++ b/packages/runtime/src/plugins/useCustomAgent.ts @@ -2,8 +2,11 @@ import type { Agent as HttpAgent } from 'node:http'; // eslint-disable-next-line import/no-nodejs-modules import type { Agent as HttpsAgent } from 'node:https'; -import type { OnFetchHookPayload } from '@graphql-mesh/types'; -import type { GatewayContext, GatewayPlugin } from '../types'; +import type { + GatewayContext, + GatewayPlugin, + OnFetchHookPayload, +} from '../types'; export type AgentFactory = ( payload: OnFetchHookPayload< diff --git a/packages/runtime/src/plugins/useDelegationPlanDebug.ts b/packages/runtime/src/plugins/useDelegationPlanDebug.ts index eb792a9fa..9acc94281 100644 --- a/packages/runtime/src/plugins/useDelegationPlanDebug.ts +++ b/packages/runtime/src/plugins/useDelegationPlanDebug.ts @@ -1,4 +1,3 @@ -import type { Logger } from '@graphql-mesh/types'; import { pathToArray } from '@graphql-tools/utils'; import { print } from 'graphql'; import { FetchAPI } from 'graphql-yoga'; @@ -6,7 +5,7 @@ import type { GatewayContext, GatewayPlugin } from '../types'; export function useDelegationPlanDebug< TContext extends Record, ->(opts: { logger: Logger }): GatewayPlugin { +>(): GatewayPlugin { let fetchAPI: FetchAPI; const stageExecuteLogById = new WeakMap>(); return { @@ -18,15 +17,15 @@ export function useDelegationPlanDebug< variables, fragments, fieldNodes, + context, info, - logger = opts.logger, }) { const planId = fetchAPI.crypto.randomUUID(); - const planLogger = logger.child({ planId, typeName }); - const delegationPlanStartLogger = planLogger.child( - 'delegation-plan-start', + const log = context.log.child( + { planId, typeName }, + '[useDelegationPlanDebug] ', ); - delegationPlanStartLogger.debug(() => { + log.debug(() => { const logObj: Record = {}; if (variables && Object.keys(variables).length) { logObj['variables'] = variables; @@ -48,19 +47,21 @@ export function useDelegationPlanDebug< logObj['path'] = pathToArray(info.path).join(' | '); } return logObj; - }); + }, 'Start'); return ({ delegationPlan }) => { - const delegationPlanDoneLogger = logger.child('delegation-plan-done'); - delegationPlanDoneLogger.debug(() => - delegationPlan.map((plan) => { - const planObj: Record = {}; - for (const [subschema, selectionSet] of plan) { - if (subschema.name) { - planObj[subschema.name] = print(selectionSet); + log.debug( + () => ({ + delegationPlan: delegationPlan.map((plan) => { + const planObj: Record = {}; + for (const [subschema, selectionSet] of plan) { + if (subschema.name) { + planObj[subschema.name] = print(selectionSet); + } } - } - return planObj; + return planObj; + }), }), + 'Done', ); }; }, @@ -72,19 +73,18 @@ export function useDelegationPlanDebug< selectionSet, key, typeName, - logger = opts.logger, }) { let contextLog = stageExecuteLogById.get(context); if (!contextLog) { contextLog = new Set(); stageExecuteLogById.set(context, contextLog); } - const log = { + const logAttr = { key: JSON.stringify(key), object: JSON.stringify(object), selectionSet: print(selectionSet), }; - const logStr = JSON.stringify(log); + const logStr = JSON.stringify(logAttr); if (contextLog.has(logStr)) { return; } @@ -94,18 +94,16 @@ export function useDelegationPlanDebug< subgraph, typeName, }; - const delegationStageLogger = logger.child(logMeta); - delegationStageLogger.debug('delegation-plan-start', () => { - return { + const log = context.log.child(logMeta, '[useDelegationPlanDebug] '); + log.debug( + () => ({ ...log, path: pathToArray(info.path).join(' | '), - }; - }); + }), + 'Stage start', + ); return ({ result }) => { - const delegationStageExecuteDoneLogger = logger.child( - 'delegation-stage-execute-done', - ); - delegationStageExecuteDoneLogger.debug(() => result); + log.debug(() => result, 'Stage done'); }; }, }; diff --git a/packages/runtime/src/plugins/useDemandControl.ts b/packages/runtime/src/plugins/useDemandControl.ts index 77e17cc8c..649f03ff4 100644 --- a/packages/runtime/src/plugins/useDemandControl.ts +++ b/packages/runtime/src/plugins/useDemandControl.ts @@ -69,8 +69,7 @@ export function useDemandControl>({ }); const costByContextMap = new WeakMap(); return { - onSubgraphExecute({ subgraph, executionRequest, logger }) { - const demandControlLogger = logger?.child('demand-control'); + onSubgraphExecute({ subgraph, executionRequest, log }) { let costByContext = executionRequest.context ? costByContextMap.get(executionRequest.context) || 0 : 0; @@ -83,10 +82,13 @@ export function useDemandControl>({ if (executionRequest.context) { costByContextMap.set(executionRequest.context, costByContext); } - demandControlLogger?.debug({ - operationCost, - totalCost: costByContext, - }); + log.debug( + { + operationCost, + totalCost: costByContext, + }, + '[useDemandControl]', + ); if (maxCost != null && costByContext > maxCost) { throw createGraphQLError( `Operation estimated cost ${costByContext} exceeded configured maximum ${maxCost}`, diff --git a/packages/runtime/src/plugins/useFetchDebug.ts b/packages/runtime/src/plugins/useFetchDebug.ts index d6a9eb207..3e87798da 100644 --- a/packages/runtime/src/plugins/useFetchDebug.ts +++ b/packages/runtime/src/plugins/useFetchDebug.ts @@ -1,38 +1,36 @@ -import type { Logger } from '@graphql-mesh/types'; import { FetchAPI } from 'graphql-yoga'; import type { GatewayPlugin } from '../types'; -export function useFetchDebug>(opts: { - logger: Logger; -}): GatewayPlugin { +export function useFetchDebug< + TContext extends Record, +>(): GatewayPlugin { let fetchAPI: FetchAPI; return { onYogaInit({ yoga }) { fetchAPI = yoga.fetchAPI; }, - onFetch({ url, options, logger = opts.logger }) { + onFetch({ url, options, context }) { const fetchId = fetchAPI.crypto.randomUUID(); - const fetchLogger = logger.child({ - fetchId, - }); - const httpFetchRequestLogger = fetchLogger.child('http-fetch-request'); - httpFetchRequestLogger.debug(() => ({ - url, - ...(options || {}), - body: options?.body, - headers: options?.headers, - signal: options?.signal?.aborted ? options?.signal?.reason : false, - })); + const log = context.log.child({ fetchId }, '[useFetchDebug] '); + log.debug( + () => ({ + url, + body: options?.body?.toString(), + headers: options?.headers, + signal: options?.signal?.aborted ? options?.signal?.reason : false, + }), + 'Request', + ); const start = performance.now(); return function onFetchDone({ response }) { - const httpFetchResponseLogger = fetchLogger.child( - 'http-fetch-response', + log.debug( + () => ({ + status: response.status, + headers: Object.fromEntries(response.headers.entries()), + duration: performance.now() - start, + }), + 'Response', ); - httpFetchResponseLogger.debug(() => ({ - status: response.status, - headers: Object.fromEntries(response.headers.entries()), - duration: performance.now() - start, - })); }; }, }; diff --git a/packages/runtime/src/plugins/useHiveConsole.ts b/packages/runtime/src/plugins/useHiveConsole.ts index 1c4e80d94..826f7d610 100644 --- a/packages/runtime/src/plugins/useHiveConsole.ts +++ b/packages/runtime/src/plugins/useHiveConsole.ts @@ -1,6 +1,6 @@ import type { HivePluginOptions } from '@graphql-hive/core'; +import { LegacyLogger, type Logger } from '@graphql-hive/logger'; import { useHive } from '@graphql-hive/yoga'; -import type { Logger } from '@graphql-mesh/types'; import { isDebug } from '~internal/env'; import { GatewayPlugin } from '../types'; @@ -33,13 +33,13 @@ export default function useHiveConsole< enabled, token, ...options -}: HiveConsolePluginOptions & { logger: Logger }): GatewayPlugin< +}: HiveConsolePluginOptions & { log: Logger }): GatewayPlugin< TPluginContext, TContext > { const agent: HiveConsolePluginOptions['agent'] = { name: 'hive-gateway', - logger: options.logger, + logger: LegacyLogger.from(options.log), ...options.agent, }; diff --git a/packages/runtime/src/plugins/usePropagateHeaders.ts b/packages/runtime/src/plugins/usePropagateHeaders.ts index f678f3bd4..b0e34e4e6 100644 --- a/packages/runtime/src/plugins/usePropagateHeaders.ts +++ b/packages/runtime/src/plugins/usePropagateHeaders.ts @@ -1,7 +1,5 @@ -import { subgraphNameByExecutionRequest } from '@graphql-mesh/fusion-runtime'; -import type { OnFetchHookDone } from '@graphql-mesh/types'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; -import type { GatewayPlugin } from '../types'; +import type { GatewayPlugin, OnFetchHookDone } from '../types'; interface FromClientToSubgraphsPayload { request: Request; @@ -34,10 +32,12 @@ export function usePropagateHeaders>( const resHeadersByRequest = new WeakMap>(); return { onFetch({ executionRequest, context, options, setOptions }) { - const request = context?.request || executionRequest?.context?.request; + const request = + 'request' in context + ? context?.request || executionRequest?.context?.request + : undefined; if (request) { - const subgraphName = (executionRequest && - subgraphNameByExecutionRequest.get(executionRequest))!; + const subgraphName = executionRequest?.subgraphName!; return handleMaybePromise( () => handleMaybePromise( diff --git a/packages/runtime/src/plugins/useRequestId.ts b/packages/runtime/src/plugins/useRequestId.ts index 46468012b..57ca584c2 100644 --- a/packages/runtime/src/plugins/useRequestId.ts +++ b/packages/runtime/src/plugins/useRequestId.ts @@ -1,4 +1,8 @@ -import { requestIdByRequest } from '@graphql-mesh/utils'; +import { LegacyLogger } from '@graphql-hive/logger'; +import { + loggerForRequest, + requestIdByRequest, +} from '@graphql-hive/logger/request'; import { FetchAPI } from '@whatwg-node/server'; import type { GatewayContext, GatewayPlugin } from '../types'; @@ -48,17 +52,26 @@ export function useRequestId>( }); requestIdByRequest.set(request, requestId); }, - onContextBuilding({ context }) { - if (context?.request) { - const requestId = requestIdByRequest.get(context.request); - if (requestId && context.logger) { - // @ts-expect-error - Logger is somehow read-only - context.logger = context.logger.child({ requestId }); - } + onContextBuilding({ context, extendContext }) { + // the request ID wont always be available because there's no request in websockets + const requestId = requestIdByRequest.get(context.request); + let log = context.log; + if (requestId) { + log = loggerForRequest( + context.log.child({ requestId }), + context.request, + ); } + extendContext( + // @ts-expect-error TODO: typescript is acting up here + { + log, + logger: LegacyLogger.from(log), + }, + ); }, onFetch({ context, options, setOptions }) { - if (context?.request) { + if ('request' in context) { const requestId = requestIdByRequest.get(context.request); if (requestId) { setOptions({ diff --git a/packages/runtime/src/plugins/useRetryOnSchemaReload.ts b/packages/runtime/src/plugins/useRetryOnSchemaReload.ts index f0f5060a1..8dc8c8910 100644 --- a/packages/runtime/src/plugins/useRetryOnSchemaReload.ts +++ b/packages/runtime/src/plugins/useRetryOnSchemaReload.ts @@ -1,5 +1,5 @@ -import { Logger } from '@graphql-mesh/types'; -import { requestIdByRequest } from '@graphql-mesh/utils'; +import type { Logger } from '@graphql-hive/logger'; +import { loggerForRequest } from '@graphql-hive/logger/request'; import type { MaybeAsyncIterable } from '@graphql-tools/utils'; import { handleMaybePromise, @@ -12,9 +12,9 @@ import type { GatewayPlugin } from '../types'; type ExecHandler = () => MaybePromise>; export function useRetryOnSchemaReload>({ - logger, + log: rootLog, }: { - logger: Logger; + log: Logger; }): GatewayPlugin { const execHandlerByContext = new WeakMap<{}, ExecHandler>(); function handleOnExecute(args: ExecutionArgs) { @@ -26,6 +26,7 @@ export function useRetryOnSchemaReload>({ } } } + const logForRequest = new WeakMap(); function handleExecutionResult({ context, result, @@ -35,20 +36,22 @@ export function useRetryOnSchemaReload>({ context: {}; result?: ExecutionResult; setResult: (result: MaybeAsyncIterable) => void; - request: Request; + // request wont be available over websockets + request: Request | undefined; }) { const execHandler = execHandlerByContext.get(context); if ( execHandler && result?.errors?.some((e) => e.extensions?.['code'] === 'SCHEMA_RELOAD') ) { - let requestLogger = logger; - const requestId = requestIdByRequest.get(request); - if (requestId) { - requestLogger = logger.child({ requestId }); - } - requestLogger.info( - 'The operation has been aborted after the supergraph schema reloaded, retrying the operation...', + const log = request + ? loggerForRequest( + logForRequest.get(request)!, // must exist at this point + request, + ) + : rootLog; + log.info( + '[useRetryOnSchemaReload] The operation has been aborted after the supergraph schema reloaded, retrying the operation...', ); if (execHandler) { return handleMaybePromise(execHandler, (newResult) => @@ -67,10 +70,20 @@ export function useRetryOnSchemaReload>({ }), ); }, - onExecute({ args }) { + onExecute({ args, context }) { + // we set the logger here because it most likely contains important attributes (like the request-id) + if (context.request) { + // the request wont be available over websockets + logForRequest.set(context.request, context.log); + } handleOnExecute(args); }, - onSubscribe({ args }) { + onSubscribe({ args, context }) { + // we set the logger here because it most likely contains important attributes (like the request-id) + if (context.request) { + // the request wont be available over websockets + logForRequest.set(context.request, context.log); + } handleOnExecute(args); }, onExecutionResult({ request, context, result, setResult }) { diff --git a/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts b/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts index d009a1e2b..192af09e9 100644 --- a/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts +++ b/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts @@ -1,24 +1,19 @@ import { defaultPrintFn } from '@graphql-mesh/transport-common'; -import type { Logger } from '@graphql-mesh/types'; -import { FetchAPI, isAsyncIterable } from 'graphql-yoga'; +import { isAsyncIterable } from 'graphql-yoga'; import type { GatewayPlugin } from '../types'; export function useSubgraphExecuteDebug< TContext extends Record, ->(opts: { logger: Logger }): GatewayPlugin { - let fetchAPI: FetchAPI; +>(): GatewayPlugin { return { - onYogaInit({ yoga }) { - fetchAPI = yoga.fetchAPI; - }, - onSubgraphExecute({ executionRequest, logger = opts.logger }) { - const subgraphExecuteHookLogger = logger.child({ - subgraphExecuteId: fetchAPI.crypto.randomUUID(), - }); - const subgraphExecuteStartLogger = subgraphExecuteHookLogger.child( - 'subgraph-execute-start', + onSubgraphExecute({ executionRequest }) { + let log = executionRequest.context?.log.child( + '[useSubgraphExecuteDebug] ', ); - subgraphExecuteStartLogger.debug(() => { + if (!log) { + throw new Error('Logger is not available in the execution context'); + } + log.debug(() => { const logData: Record = {}; if (executionRequest.document) { logData['query'] = defaultPrintFn(executionRequest.document); @@ -30,28 +25,25 @@ export function useSubgraphExecuteDebug< logData['variables'] = executionRequest.variables; } return logData; - }); + }, 'Start'); const start = performance.now(); return function onSubgraphExecuteDone({ result }) { - const subgraphExecuteEndLogger = subgraphExecuteHookLogger.child( - 'subgraph-execute-end', - ); if (isAsyncIterable(result)) { return { onNext({ result }) { - const subgraphExecuteNextLogger = subgraphExecuteHookLogger.child( - 'subgraph-execute-next', - ); - subgraphExecuteNextLogger.debug(result); + log.debug(result, 'Next'); }, onEnd() { - subgraphExecuteEndLogger.debug(() => ({ - duration: performance.now() - start, - })); + log.debug( + () => ({ + duration: performance.now() - start, + }), + 'End', + ); }, }; } - subgraphExecuteEndLogger.debug(result); + log.debug(result, 'Done'); return void 0; }; }, diff --git a/packages/runtime/src/plugins/useUpstreamCancel.ts b/packages/runtime/src/plugins/useUpstreamCancel.ts index a744a2a7e..04ed514fc 100644 --- a/packages/runtime/src/plugins/useUpstreamCancel.ts +++ b/packages/runtime/src/plugins/useUpstreamCancel.ts @@ -6,7 +6,7 @@ export function useUpstreamCancel(): GatewayPlugin { return { onFetch({ context, options, executionRequest, info }) { const signals: AbortSignal[] = []; - if (context?.request?.signal) { + if ('request' in context && context.request.signal) { signals.push(context.request.signal); } const execRequestSignal = diff --git a/packages/runtime/src/plugins/useUpstreamRetry.ts b/packages/runtime/src/plugins/useUpstreamRetry.ts index b6f91aa3d..7ef434d45 100644 --- a/packages/runtime/src/plugins/useUpstreamRetry.ts +++ b/packages/runtime/src/plugins/useUpstreamRetry.ts @@ -8,6 +8,17 @@ import { import { handleMaybePromise, MaybePromise } from '@whatwg-node/promise-helpers'; import { GatewayPlugin } from '../types'; +export const RETRY_SYMBOL = Symbol.for('@hive-gateway/runtime/upstreamRetry'); + +type RetryExecutionRequest = ExecutionRequest & { + [RETRY_SYMBOL]: RetryInfo; +}; + +type RetryInfo = { + attempt: number; + executionRequest: ExecutionRequest; +}; + export interface UpstreamRetryOptions { /** * The maximum number of retries to attempt. @@ -104,6 +115,13 @@ export function useUpstreamRetry>( } const requestTime = Date.now(); attemptsLeft--; + + // @ts-expect-error we rather mutatate the executionRequest because we strict compare it + executionRequest[RETRY_SYMBOL] = { + attempt: maxRetries - attemptsLeft, + executionRequest, + }; + return handleMaybePromise( () => executor(executionRequest), (currRes) => { @@ -164,11 +182,8 @@ export function useUpstreamRetry>( } } }, - onFetch({ info, executionRequest }) { - // if there's no execution request, it's a subgraph request + onFetch({ executionRequest }) { // TODO: Also consider what happens when there are multiple fetch calls for a single subgraph request - // @ts-expect-error - we know that it might have executionRequest property - executionRequest ||= info?.rootValue?.executionRequest; if (executionRequest) { return function onFetchDone({ response }) { executionRequestResponseMap.set(executionRequest, response); @@ -184,3 +199,15 @@ export function useUpstreamRetry>( }, }; } + +export function isRetryExecutionRequest( + executionRequest?: ExecutionRequest, +): executionRequest is RetryExecutionRequest { + return !!(executionRequest as any)?.[RETRY_SYMBOL]; +} + +export function getRetryInfo( + executionRequest: RetryExecutionRequest, +): RetryInfo { + return executionRequest[RETRY_SYMBOL]; +} diff --git a/packages/runtime/src/plugins/useUpstreamTimeout.ts b/packages/runtime/src/plugins/useUpstreamTimeout.ts index 9d42727d8..14d3a4bd3 100644 --- a/packages/runtime/src/plugins/useUpstreamTimeout.ts +++ b/packages/runtime/src/plugins/useUpstreamTimeout.ts @@ -1,5 +1,4 @@ import { abortSignalAny } from '@graphql-hive/signal'; -import { subgraphNameByExecutionRequest } from '@graphql-mesh/fusion-runtime'; import { UpstreamErrorExtensions } from '@graphql-mesh/transport-common'; import { getHeadersObj } from '@graphql-mesh/utils'; import { @@ -54,11 +53,6 @@ export function useUpstreamTimeout>( timeoutSignal, ); } - const signals: AbortSignal[] = []; - signals.push(timeoutSignal); - if (executionRequest.signal) { - signals.push(executionRequest.signal); - } const timeoutDeferred = createDeferred(); function rejectDeferred() { timeoutDeferred.reject(timeoutSignal?.reason); @@ -66,10 +60,17 @@ export function useUpstreamTimeout>( timeoutSignal.addEventListener('abort', rejectDeferred, { once: true, }); - const combinedSignal = abortSignalAny(signals); + const signals: AbortSignal[] = []; + signals.push(timeoutSignal); + if (executionRequest.signal) { + signals.push(executionRequest.signal); + } + // we want to create a new executionrequest and not mutate the existing one becaus, when using + // this with useUpstreamRetry, the same executionRequest will be used for each retry and we need + // to timeoutSignalsByExecutionRequest.set(...) again above const res$ = executor({ ...executionRequest, - signal: combinedSignal, + signal: abortSignalAny(signals), }); if (!isPromise(res$)) { return res$; @@ -125,9 +126,7 @@ export function useUpstreamTimeout>( return undefined; }, onFetch({ url, executionRequest, options, setOptions }) { - const subgraphName = - executionRequest && - subgraphNameByExecutionRequest.get(executionRequest); + const subgraphName = executionRequest?.subgraphName; if ( !executionRequest || !timeoutSignalsByExecutionRequest.has(executionRequest) diff --git a/packages/runtime/src/plugins/useWebhooks.ts b/packages/runtime/src/plugins/useWebhooks.ts index 8f557e5ec..1fddbf0bd 100644 --- a/packages/runtime/src/plugins/useWebhooks.ts +++ b/packages/runtime/src/plugins/useWebhooks.ts @@ -1,16 +1,18 @@ +import type { Logger } from '@graphql-hive/logger'; import { HivePubSub } from '@graphql-hive/pubsub'; -import type { Logger } from '@graphql-mesh/types'; import { handleMaybePromise } from '@whatwg-node/promise-helpers'; -import type { Plugin } from 'graphql-yoga'; +import { GatewayPlugin } from '../types'; export interface GatewayWebhooksPluginOptions { + log: Logger; pubsub?: HivePubSub; - logger: Logger; } + export function useWebhooks({ + log: rootLog, pubsub, - logger, -}: GatewayWebhooksPluginOptions): Plugin { +}: GatewayWebhooksPluginOptions): GatewayPlugin { + const log = rootLog.child('[useWebhooks] '); if (!pubsub) { throw new Error(`You must provide a pubsub instance to webhooks feature! Example: @@ -32,13 +34,13 @@ export function useWebhooks({ const expectedEventName = `webhook:${requestMethod}:${pathname}`; for (const eventName of eventNames) { if (eventName === expectedEventName) { - logger?.debug(() => `Received webhook request for ${pathname}`); + log.debug({ pathname }, 'Received webhook request'); return handleMaybePromise( () => request.text(), function handleWebhookPayload(webhookPayload) { - logger?.debug( - () => - `Emitted webhook request for ${pathname}: ${webhookPayload}`, + log.debug( + { pathname, payload: webhookPayload }, + 'Emitted webhook request', ); webhookPayload = request.headers.get('content-type') === 'application/json' diff --git a/packages/runtime/src/productLogo.ts b/packages/runtime/src/productLogo.ts deleted file mode 100644 index 6002254b2..000000000 --- a/packages/runtime/src/productLogo.ts +++ /dev/null @@ -1,16 +0,0 @@ -export const defaultProductLogo = /* HTML */ ` - - - - - - - -`; diff --git a/packages/runtime/src/types.ts b/packages/runtime/src/types.ts index 89a990c31..3dd6a5e69 100644 --- a/packages/runtime/src/types.ts +++ b/packages/runtime/src/types.ts @@ -1,5 +1,6 @@ import type { Plugin as EnvelopPlugin } from '@envelop/core'; import type { useGenericAuth } from '@envelop/generic-auth'; +import type { Logger, LogLevel } from '@graphql-hive/logger'; import { HivePubSub } from '@graphql-hive/pubsub'; import type { Instrumentation as GatewayRuntimeInstrumentation, @@ -8,16 +9,18 @@ import type { UnifiedGraphPlugin, } from '@graphql-mesh/fusion-runtime'; import type { HMACUpstreamSignatureOptions } from '@graphql-mesh/hmac-upstream-signature'; +import { OpenTelemetryPluginUtils } from '@graphql-mesh/plugin-opentelemetry'; import type { ResponseCacheConfig } from '@graphql-mesh/plugin-response-cache'; import type { KeyValueCache, - Logger, + Logger as LegacyLogger, MeshFetch, - OnFetchHook, + MeshFetchRequestInit, } from '@graphql-mesh/types'; -import type { FetchInstrumentation, LogLevel } from '@graphql-mesh/utils'; +import type { FetchInstrumentation } from '@graphql-mesh/utils'; import type { HTTPExecutorOptions } from '@graphql-tools/executor-http'; import type { + ExecutionRequest, IResolvers, MaybePromise, TypeSource, @@ -35,6 +38,8 @@ import type { Plugin as YogaPlugin, YogaServerOptions, } from 'graphql-yoga'; +import { GraphQLResolveInfo } from 'graphql/type'; +import { OpenTelemetryContextExtension } from '../../plugins/opentelemetry/src/plugin'; import type { UnifiedGraphConfig } from './handleUnifiedGraphConfig'; import type { UseContentEncodingOpts } from './plugins/useContentEncoding'; import type { AgentFactory } from './plugins/useCustomAgent'; @@ -61,9 +66,9 @@ export interface GatewayConfigContext { */ fetch: MeshFetch; /** - * The logger to use throught Mesh and it's plugins. + * The logger to use throught Hive and its plugins. */ - logger: Logger; + log: Logger; /** * Current working directory. */ @@ -76,10 +81,17 @@ export interface GatewayConfigContext { * Cache Storage */ cache?: KeyValueCache; + /** + * OpenTelemetry API to get access to OTEL Tracer and Hive Gateway internal OTEL Contexts + */ + openTelemetry: OpenTelemetryPluginUtils & { + register?: (plugin: OpenTelemetryPluginUtils) => void; + }; } export interface GatewayContext - extends GatewayConfigContext, + extends Omit, + OpenTelemetryContextExtension, YogaInitialContext { /** * Environment agnostic HTTP headers provided with the request. @@ -96,7 +108,7 @@ export type GatewayPlugin< TContext extends Record = Record, > = YogaPlugin & GatewayContext & TContext> & UnifiedGraphPlugin & GatewayContext & TContext> & { - onFetch?: OnFetchHook & GatewayContext & TContext>; + onFetch?: OnFetchHook & TContext>; onCacheGet?: OnCacheGetHook; onCacheSet?: OnCacheSetHook; onCacheDelete?: OnCacheDeleteHook; @@ -112,6 +124,40 @@ export type GatewayPlugin< >; }; +export interface OnFetchHookPayload { + url: string; + setURL(url: URL | string): void; + options: MeshFetchRequestInit; + setOptions(options: MeshFetchRequestInit): void; + /** + * The context is not available in cases where "fetch" is done in + * order to pull a supergraph or do some internal work. + * + * The logger will be available in all cases. + */ + context: (GatewayContext & TContext) | { log: Logger }; + /** @deprecated Please use `log` from the {@link context} instead. */ + logger: LegacyLogger; + info: GraphQLResolveInfo; + fetchFn: MeshFetch; + setFetchFn: (fetchFn: MeshFetch) => void; + executionRequest?: ExecutionRequest; + endResponse: (response$: MaybePromise) => void; +} + +export interface OnFetchHookDonePayload { + response: Response; + setResponse: (response: Response) => void; +} + +export type OnFetchHookDone = ( + payload: OnFetchHookDonePayload, +) => MaybePromise; + +export type OnFetchHook = ( + payload: OnFetchHookPayload, +) => MaybePromise; + export type OnCacheGetHook = ( payload: OnCacheGetHookEventPayload, ) => MaybePromise; @@ -485,9 +531,10 @@ interface GatewayConfigBase> { * Enable, disable or implement a custom logger for logging. * * @default true + * * @see https://the-guild.dev/graphql/hive/docs/gateway/logging-and-error-handling */ - logging?: boolean | Logger | LogLevel | keyof typeof LogLevel | undefined; + logging?: boolean | Logger | LogLevel | undefined; /** * Endpoint of the GraphQL API. */ @@ -580,6 +627,15 @@ interface GatewayConfigBase> { */ deferStream?: boolean; + /** + * GraphQL Multipart Request support. + * + * @see https://github.com/jaydenseric/graphql-multipart-request-spec + * + * @default false + */ + multipart?: boolean; + /** * Enable execution cancellation * diff --git a/packages/runtime/src/utils.ts b/packages/runtime/src/utils.ts index 2fe7e284e..4f520c569 100644 --- a/packages/runtime/src/utils.ts +++ b/packages/runtime/src/utils.ts @@ -1,3 +1,4 @@ +import { OpenTelemetryPluginUtils } from '@graphql-mesh/plugin-opentelemetry'; import { KeyValueCache } from '@graphql-mesh/types'; import type { ExecutionArgs } from '@graphql-tools/executor'; import { @@ -5,9 +6,11 @@ import { getDirectiveExtensions, memoize1, } from '@graphql-tools/utils'; +import { context } from '@opentelemetry/api'; import { handleMaybePromise, iterateAsync } from '@whatwg-node/promise-helpers'; import type { GraphQLSchema, SelectionSetNode } from 'graphql'; import { + GatewayConfigContext, OnCacheDeleteHook, OnCacheDeleteHookResult, OnCacheGetHook, @@ -331,3 +334,41 @@ export function getDirectiveNameForFederationDirective({ } return normalizedDirectiveName; } + +/** + * Creates the OpenTelemetry API for the config context. + * It has a default implementation that can be replaced by calling `register`. + * The OpenTelemetry plugin uses `register` to replace the default on gateway initialization. + */ +export function createOpenTelemetryAPI(): GatewayConfigContext['openTelemetry'] { + let delegate: OpenTelemetryPluginUtils = { + // In case no OpenTelemetry plugin is registered, we just rely on standard context management + getActiveContext: () => context.active(), + // undefined to indicate the OpenTelemetry is either not setup or not initialized yet. + tracer: undefined, + getHttpContext: () => undefined, + getExecutionRequestContext: () => undefined, + getOperationContext: () => undefined, + }; + + let register: GatewayConfigContext['openTelemetry']['register'] = ( + plugin, + ) => { + delegate = plugin; + // Disallow the registration of another delegate. Multiple OpenTelemetry plugin should not + // exits in the same Gateway instance. + register = undefined; + }; + + return { + get tracer() { + return delegate.tracer; + }, + getActiveContext: (...args) => delegate.getActiveContext(...args), + getHttpContext: (...args) => delegate.getHttpContext(...args), + getOperationContext: (...args) => delegate.getOperationContext(...args), + getExecutionRequestContext: (...args) => + delegate.getExecutionRequestContext(...args), + register, + }; +} diff --git a/packages/runtime/src/wrapFetchWithHooks.ts b/packages/runtime/src/wrapFetchWithHooks.ts new file mode 100644 index 000000000..a8f27249f --- /dev/null +++ b/packages/runtime/src/wrapFetchWithHooks.ts @@ -0,0 +1,105 @@ +import { getInstrumented } from '@envelop/instrumentation'; +import { LegacyLogger, type Logger } from '@graphql-hive/logger'; +import type { MeshFetch } from '@graphql-mesh/types'; +import type { ExecutionRequest, MaybePromise } from '@graphql-tools/utils'; +import { handleMaybePromise, iterateAsync } from '@whatwg-node/promise-helpers'; +import { OnFetchHook, OnFetchHookDone } from './types'; + +export type FetchInstrumentation = { + fetch?: ( + payload: { executionRequest?: ExecutionRequest }, + wrapped: () => MaybePromise, + ) => MaybePromise; +}; + +export function wrapFetchWithHooks( + onFetchHooks: OnFetchHook[], + log: Logger, + instrumentation?: () => FetchInstrumentation | undefined, +): MeshFetch { + let wrappedFetchFn = function wrappedFetchFn(url, options, context, info) { + let fetchFn: MeshFetch; + let response$: MaybePromise; + const onFetchDoneHooks: OnFetchHookDone[] = []; + return handleMaybePromise( + () => + iterateAsync( + onFetchHooks, + (onFetch, endEarly) => + onFetch({ + fetchFn, + setFetchFn(newFetchFn) { + fetchFn = newFetchFn; + }, + url, + setURL(newUrl) { + url = String(newUrl); + }, + // @ts-expect-error TODO: why? + options, + setOptions(newOptions) { + options = newOptions; + }, + context: { log, ...context }, + logger: LegacyLogger.from(log), + // @ts-expect-error TODO: why? + info, + get executionRequest() { + return ( + info?.executionRequest || + // @ts-expect-error might be in the root value, see packages/fusion-runtime/src/utils.ts + info?.rootValue?.executionRequest + ); + }, + endResponse(newResponse) { + response$ = newResponse; + endEarly(); + }, + }), + onFetchDoneHooks, + ), + function handleIterationResult() { + if (response$) { + return response$; + } + return handleMaybePromise( + () => fetchFn(url, options, context, info), + function (response: Response) { + return handleMaybePromise( + () => + iterateAsync(onFetchDoneHooks, (onFetchDone) => + onFetchDone({ + response, + setResponse(newResponse) { + response = newResponse; + }, + }), + ), + function handleOnFetchDone() { + return response; + }, + ); + }, + ); + }, + ); + } as MeshFetch; + + if (instrumentation) { + const originalWrappedFetch = wrappedFetchFn; + wrappedFetchFn = function wrappedFetchFn(url, options, context, info) { + const fetchInstrument = instrumentation()?.fetch; + const instrumentedFetch = fetchInstrument + ? getInstrumented({ + get executionRequest() { + return info?.executionRequest; + }, + }).asyncFn(fetchInstrument, originalWrappedFetch) + : originalWrappedFetch; + + return instrumentedFetch(url, options, context, info); + }; + } + + return wrappedFetchFn; +} diff --git a/packages/runtime/tests/contentEncoding.test.ts b/packages/runtime/tests/contentEncoding.test.ts index 84443e376..317455fb1 100644 --- a/packages/runtime/tests/contentEncoding.test.ts +++ b/packages/runtime/tests/contentEncoding.test.ts @@ -1,5 +1,4 @@ import { getUnifiedGraphGracefully } from '@graphql-mesh/fusion-composition'; -import type { OnFetchHookDonePayload } from '@graphql-mesh/types'; import { getSupportedEncodings, useContentEncoding } from '@whatwg-node/server'; import { createSchema, @@ -8,7 +7,11 @@ import { type YogaInitialContext, } from 'graphql-yoga'; import { afterEach, describe, expect, it, vi } from 'vitest'; -import { createGatewayRuntime, useCustomFetch } from '../src/index'; +import { + createGatewayRuntime, + OnFetchHookDonePayload, + useCustomFetch, +} from '../src/index'; describe('contentEncoding', () => { const fooResolver = vi.fn((_, __, _context: YogaInitialContext) => { diff --git a/packages/runtime/tests/graphos.test.ts b/packages/runtime/tests/graphos.test.ts index b1135db8e..58f065376 100644 --- a/packages/runtime/tests/graphos.test.ts +++ b/packages/runtime/tests/graphos.test.ts @@ -1,12 +1,14 @@ import { setTimeout } from 'timers/promises'; import { - JSONLogger, type GatewayConfigContext, type GatewayGraphOSManagedFederationOptions, } from '@graphql-hive/gateway-runtime'; +import { LegacyLogger, Logger } from '@graphql-hive/logger'; +import { TransportContext } from '@graphql-mesh/transport-common'; import { Response } from '@whatwg-node/fetch'; import { beforeEach, describe, expect, it, vi } from 'vitest'; import { createGraphOSFetcher } from '../src/fetchers/graphos'; +import { createOpenTelemetryAPI } from '../src/utils'; describe('GraphOS', () => { describe('supergraph fetching', () => { @@ -20,7 +22,7 @@ describe('GraphOS', () => { it('should fetch the supergraph SDL', async () => { const { unifiedGraphFetcher } = createTestFetcher({ fetch: mockSDL }); - const result = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); expect(await result).toBe(supergraphSdl); }); @@ -37,7 +39,7 @@ describe('GraphOS', () => { }, }); - const result = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result = Promise.resolve().then(() => unifiedGraphFetcher()); for (let i = 0; i < 3; i++) { await advanceTimersByTimeAsync(1_000); } @@ -52,7 +54,7 @@ describe('GraphOS', () => { ); const result = Promise.resolve() - .then(() => unifiedGraphFetcher({})) + .then(() => unifiedGraphFetcher()) .catch((err) => err); for (let i = 0; i < 3; i++) { await advanceTimersByTimeAsync(1_000); @@ -68,7 +70,7 @@ describe('GraphOS', () => { ); const result = Promise.resolve() - .then(() => unifiedGraphFetcher({})) + .then(() => unifiedGraphFetcher()) .catch(() => {}); await advanceTimersByTimeAsync(25); expect(mockFetchError).toHaveBeenCalledTimes(1); @@ -84,12 +86,12 @@ describe('GraphOS', () => { it('should respect min-delay between polls', async () => { const { unifiedGraphFetcher } = createTestFetcher({ fetch: mockSDL }); - Promise.resolve().then(() => unifiedGraphFetcher({})); + Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(25); expect(mockSDL).toHaveBeenCalledTimes(1); await advanceTimersByTimeAsync(20); expect(mockSDL).toHaveBeenCalledTimes(1); - Promise.resolve().then(() => unifiedGraphFetcher({})); + Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(50); expect(mockSDL).toHaveBeenCalledTimes(1); await advanceTimersByTimeAsync(50); @@ -107,19 +109,19 @@ describe('GraphOS', () => { return mockSDL(); }, }); - const result1 = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result1 = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); - const result2 = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result2 = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); expect(await result1).toBe(await result2); }, 30_000); it('should not wait if min delay is superior to polling interval', async () => { const { unifiedGraphFetcher } = createTestFetcher({ fetch: mockSDL }); - const result = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); await result; - const result2 = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result2 = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); expect(await result).toBe(await result2); }); @@ -146,9 +148,9 @@ describe('GraphOS', () => { }, }); - const result = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); - const result2 = Promise.resolve().then(() => unifiedGraphFetcher({})); + const result2 = Promise.resolve().then(() => unifiedGraphFetcher()); await advanceTimersByTimeAsync(1_000); expect(await result).toBe(await result2); }); @@ -161,21 +163,12 @@ function createTestFetcher( }, opts?: Partial, ) { - return createGraphOSFetcher({ + const log = new Logger({ level: process.env['DEBUG'] ? 'debug' : false }); + const fetcher = createGraphOSFetcher({ configContext: { - logger: process.env['DEBUG'] - ? new JSONLogger() - : { - child() { - return this; - }, - info: () => {}, - debug: () => {}, - error: () => {}, - warn: () => {}, - log: () => {}, - }, + log, cwd: process.cwd(), + openTelemetry: createOpenTelemetryAPI(), ...configContext, }, graphosOpts: { @@ -186,6 +179,15 @@ function createTestFetcher( }, pollingInterval: 0.000000001, }); + return { + unifiedGraphFetcher: (transportContext: Partial = {}) => { + return fetcher.unifiedGraphFetcher({ + log, + logger: LegacyLogger.from(log), + ...transportContext, + }); + }, + }; } let supergraphSdl = 'TEST SDL'; diff --git a/packages/runtime/tests/hive.spec.ts b/packages/runtime/tests/hive.spec.ts index a9c1591ce..cbda3f7ca 100644 --- a/packages/runtime/tests/hive.spec.ts +++ b/packages/runtime/tests/hive.spec.ts @@ -94,18 +94,6 @@ describe('Hive CDN', () => { const resJson: ExecutionResult = await res.json(); const clientSchema = buildClientSchema(resJson.data!); expect(printSchema(clientSchema)).toMatchSnapshot('hive-cdn'); - - // Landing page - const landingPageRes = await gateway.fetch('http://localhost:4000', { - method: 'GET', - headers: { - accept: 'text/html', - }, - }); - const landingPage = await landingPageRes.text(); - expect(landingPage).toContain('Hive CDN'); - expect(landingPage).toContain('upstream'); - expect(landingPage).toContain('http://upstream/graphql'); }); it('uses Hive CDN instead of introspection for Proxy mode', async () => { const upstreamSchema = createUpstreamSchema(); diff --git a/packages/runtime/tests/wrapFetchWithHooks.test.ts b/packages/runtime/tests/wrapFetchWithHooks.test.ts new file mode 100644 index 000000000..145b5a880 --- /dev/null +++ b/packages/runtime/tests/wrapFetchWithHooks.test.ts @@ -0,0 +1,39 @@ +import { Logger } from '@graphql-hive/logger'; +import { createServerAdapter, Response } from '@whatwg-node/server'; +import type { GraphQLResolveInfo } from 'graphql'; +import { expect, it } from 'vitest'; +import { + wrapFetchWithHooks, + type FetchInstrumentation, +} from '../src/wrapFetchWithHooks'; + +it('should wrap fetch instrumentation', async () => { + await using adapter = createServerAdapter(() => + Response.json({ hello: 'world' }), + ); + let receivedExecutionRequest; + const fetchInstrumentation: FetchInstrumentation = { + fetch: async ({ executionRequest }, wrapped) => { + receivedExecutionRequest = executionRequest; + await wrapped(); + }, + }; + const wrappedFetch = wrapFetchWithHooks( + [ + ({ setFetchFn }) => { + setFetchFn( + // @ts-expect-error TODO: MeshFetch is not compatible with @whatwg-node/server fetch + adapter.fetch, + ); + }, + ], + new Logger({ level: false }), + () => fetchInstrumentation, + ); + const executionRequest = {}; + const res = await wrappedFetch('http://localhost:4000', {}, {}, { + executionRequest, + } as GraphQLResolveInfo); + expect(await res.json()).toEqual({ hello: 'world' }); + expect(receivedExecutionRequest).toBe(executionRequest); +}); diff --git a/packages/signal/package.json b/packages/signal/package.json index fbe68a591..0a3b7c858 100644 --- a/packages/signal/package.json +++ b/packages/signal/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/stitch/package.json b/packages/stitch/package.json index 4a06949b4..45c4c451f 100644 --- a/packages/stitch/package.json +++ b/packages/stitch/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -49,6 +49,7 @@ "tslib": "^2.8.1" }, "devDependencies": { + "@graphql-tools/mock": "^9.0.23", "dataloader": "^2.2.3", "graphql": "^16.9.0", "pkgroll": "2.15.0" diff --git a/packages/stitching-directives/package.json b/packages/stitching-directives/package.json index c7d997cde..e9e2c3a66 100644 --- a/packages/stitching-directives/package.json +++ b/packages/stitching-directives/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/transports/common/package.json b/packages/transports/common/package.json index 0695ee4da..0aa714ca2 100644 --- a/packages/transports/common/package.json +++ b/packages/transports/common/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -43,6 +43,7 @@ }, "dependencies": { "@envelop/core": "^5.3.0", + "@graphql-hive/logger": "workspace:^", "@graphql-hive/pubsub": "workspace:^", "@graphql-hive/signal": "workspace:^", "@graphql-mesh/types": "^0.104.7", diff --git a/packages/transports/common/src/types.ts b/packages/transports/common/src/types.ts index 529ca9828..c304d7179 100644 --- a/packages/transports/common/src/types.ts +++ b/packages/transports/common/src/types.ts @@ -1,5 +1,10 @@ +import type { Logger } from '@graphql-hive/logger'; import { HivePubSub } from '@graphql-hive/pubsub'; -import type { KeyValueCache, Logger, MeshFetch } from '@graphql-mesh/types'; +import type { + KeyValueCache, + Logger as LegacyLogger, + MeshFetch, +} from '@graphql-mesh/types'; import type { Executor, MaybePromise } from '@graphql-tools/utils'; import type { GraphQLError, GraphQLSchema } from 'graphql'; @@ -20,10 +25,14 @@ export interface TransportEntry< } export interface TransportContext { + log: Logger; + /** @deprecated Please migrate to using the {@link log}. */ + logger: LegacyLogger; + /** The fetch API to use. */ fetch?: MeshFetch; - pubsub?: HivePubSub; - logger?: Logger; + /** Will be empty when run on serverless. */ cwd?: string; + pubsub?: HivePubSub; cache?: KeyValueCache; } diff --git a/packages/transports/http-callback/package.json b/packages/transports/http-callback/package.json index 592d01698..c267cb9b3 100644 --- a/packages/transports/http-callback/package.json +++ b/packages/transports/http-callback/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/transports/http-callback/src/index.ts b/packages/transports/http-callback/src/index.ts index 1dfee881a..06884254b 100644 --- a/packages/transports/http-callback/src/index.ts +++ b/packages/transports/http-callback/src/index.ts @@ -76,7 +76,7 @@ export default { transportEntry, fetch, pubsub, - logger, + log: rootLog, }): DisposableExecutor { let headersInConfig: Record | undefined; if (typeof transportEntry.headers === 'string') { @@ -112,7 +112,7 @@ export default { executionRequest: ExecutionRequest, ) { const subscriptionId = crypto.randomUUID(); - const subscriptionLogger = logger?.child({ + const log = rootLog.child({ executor: 'http-callback', subscription: subscriptionId, }); @@ -144,8 +144,9 @@ export default { stopSubscription(createTimeoutError()); }, heartbeatIntervalMs), ); - subscriptionLogger?.debug( - `Subscribing to ${transportEntry.location} with callbackUrl: ${callbackUrl}`, + log.debug( + { location: transportEntry.location, callbackUrl }, + 'Subscribing using callback', ); let pushFn: Push = () => { throw new Error( @@ -213,7 +214,7 @@ export default { } return; } - logger?.debug(`Subscription request received`, resJson); + log.debug(resJson, 'Subscription request received'); if (resJson.errors) { if (resJson.errors.length === 1 && resJson.errors[0]) { const error = resJson.errors[0]; @@ -235,7 +236,7 @@ export default { }, ), (e) => { - logger?.debug(`Subscription request failed`, e); + log.error(e, 'Subscription request failed'); stopSubscription(e); }, ); @@ -257,13 +258,13 @@ export default { pushFn = push; stopSubscription = stop; stopFnSet.add(stop); - logger?.debug(`Listening to ${subscriptionCallbackPath}`); + log.debug(`Listening to ${subscriptionCallbackPath}`); const subId = pubsub.subscribe( `webhook:post:${subscriptionCallbackPath}`, (message: HTTPCallbackMessage) => { - logger?.debug( - `Received message from ${subscriptionCallbackPath}`, + log.debug( message, + `Received message from ${subscriptionCallbackPath}`, ); if (message.verifier !== verifier) { return; diff --git a/packages/transports/http/package.json b/packages/transports/http/package.json index 241883635..18d679e5b 100644 --- a/packages/transports/http/package.json +++ b/packages/transports/http/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/transports/http/tests/http.spec.ts b/packages/transports/http/tests/http.spec.ts index a2af02670..d636eaad1 100644 --- a/packages/transports/http/tests/http.spec.ts +++ b/packages/transports/http/tests/http.spec.ts @@ -1,3 +1,4 @@ +import { LegacyLogger, Logger } from '@graphql-hive/logger'; import { TransportEntry } from '@graphql-mesh/transport-common'; import type { MeshFetch } from '@graphql-mesh/types'; import { buildSchema, OperationTypeNode, parse } from 'graphql'; @@ -5,6 +6,9 @@ import { describe, expect, it, vi } from 'vitest'; import httpTransport from '../src'; describe('HTTP Transport', () => { + const log = new Logger({ level: false }); + const logger = new LegacyLogger(log); + const subgraphName = 'test'; it('interpolate the strings in headers', async () => { const fetch = vi.fn(async () => @@ -17,6 +21,8 @@ describe('HTTP Transport', () => { const expectedToken = 'wowmuchsecret'; const getTransportExecutor = (transportEntry: TransportEntry) => httpTransport.getSubgraphExecutor({ + log, + logger, subgraphName, transportEntry, fetch, @@ -56,6 +62,8 @@ describe('HTTP Transport', () => { const getTransportExecutor = (transportEntry: TransportEntry) => httpTransport.getSubgraphExecutor({ + log, + logger, subgraphName, transportEntry, fetch, diff --git a/packages/transports/ws/package.json b/packages/transports/ws/package.json index b66cfb589..8248da45f 100644 --- a/packages/transports/ws/package.json +++ b/packages/transports/ws/package.json @@ -14,7 +14,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", diff --git a/packages/transports/ws/src/index.ts b/packages/transports/ws/src/index.ts index 4c444ea45..3741df09e 100644 --- a/packages/transports/ws/src/index.ts +++ b/packages/transports/ws/src/index.ts @@ -36,7 +36,7 @@ export interface WSTransportOptions { export default { getSubgraphExecutor( - { transportEntry, logger }, + { transportEntry, log: rootLog }, /** * Do not use this option unless you know what you are doing. * @internal @@ -83,12 +83,12 @@ export default { let wsExecutor = wsExecutorMap.get(hash); if (!wsExecutor) { - const executorLogger = logger?.child({ + const log = rootLog.child({ executor: 'GraphQL WS', wsUrl, connectionParams, headers, - } as Record); + }); wsExecutor = buildGraphQLWSExecutor({ headers, url: wsUrl, @@ -98,30 +98,30 @@ export default { connectionParams, on: { connecting(isRetry) { - executorLogger?.debug('connecting', { isRetry }); + log.debug({ isRetry }, 'connecting'); }, opened(socket) { - executorLogger?.debug('opened', { socket }); + log.debug({ socket }, 'opened'); }, connected(socket, payload) { - executorLogger?.debug('connected', { socket, payload }); + log.debug({ socket, payload }, 'connected'); }, ping(received, payload) { - executorLogger?.debug('ping', { received, payload }); + log.debug({ received, payload }, 'ping'); }, pong(received, payload) { - executorLogger?.debug('pong', { received, payload }); + log.debug({ received, payload }, 'pong'); }, message(message) { - executorLogger?.debug('message', { message }); + log.debug({ message }, 'message'); }, closed(event) { - executorLogger?.debug('closed', { event }); + log.debug({ event }, 'closed'); // no subscriptions and the lazy close timeout has passed - remove the client wsExecutorMap.delete(hash); }, error(error) { - executorLogger?.debug('error', { error }); + log.debug({ error }, 'error'); }, }, onClient, diff --git a/packages/transports/ws/tests/ws.spec.ts b/packages/transports/ws/tests/ws.spec.ts index 0e71544ec..e9d7f0c68 100644 --- a/packages/transports/ws/tests/ws.spec.ts +++ b/packages/transports/ws/tests/ws.spec.ts @@ -1,4 +1,4 @@ -import { JSONLogger } from '@graphql-hive/logger-json'; +import { Logger } from '@graphql-hive/logger'; import type { TransportEntry, TransportGetSubgraphExecutorOptions, @@ -79,7 +79,7 @@ async function createTServer( ...transportEntry, options, }, - logger: new JSONLogger(), + log: new Logger({ level: false }), } as unknown as TransportGetSubgraphExecutorOptions, onClient, ); diff --git a/packages/wrap/package.json b/packages/wrap/package.json index 1fa0ec6fa..6a56ecbec 100644 --- a/packages/wrap/package.json +++ b/packages/wrap/package.json @@ -10,7 +10,7 @@ }, "license": "MIT", "engines": { - "node": ">=18.0.0" + "node": ">=20.0.0" }, "main": "./dist/index.js", "types": "./dist/index.d.ts", @@ -45,6 +45,7 @@ "tslib": "^2.8.1" }, "devDependencies": { + "@graphql-tools/mock": "^9.0.23", "graphql": "^16.9.0", "pkgroll": "2.15.0" }, diff --git a/packages/wrap/tests/requests.test.ts b/packages/wrap/tests/requests.test.ts index 844506303..a3c43b29c 100644 --- a/packages/wrap/tests/requests.test.ts +++ b/packages/wrap/tests/requests.test.ts @@ -28,6 +28,7 @@ describe('requests', () => { test('should create requests', () => { const request = removeLocations( createRequest({ + subgraphName: undefined, targetOperation: 'query' as OperationTypeNode, targetFieldName: 'version', selectionSet: parseSelectionSet(`{ diff --git a/tsconfig.json b/tsconfig.json index 1dd0fa0cd..1b9f15f8b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -7,6 +7,8 @@ "module": "esnext", "moduleResolution": "bundler", "target": "esnext", + // for landing page HTML generation + "jsx": "react-jsx", // TODO: set to true once dependencies (like yoga and whatwg server) add `| undefined` in addition to `?` "exactOptionalPropertyTypes": false, // packages @@ -46,6 +48,9 @@ "@graphql-mesh/plugin-opentelemetry": [ "./packages/plugins/opentelemetry/src/index.ts" ], + "@graphql-mesh/plugin-opentelemetry/setup": [ + "./packages/plugins/opentelemetry/src/setup.ts" + ], "@graphql-mesh/plugin-prometheus": [ "./packages/plugins/prometheus/src/index.ts" ], @@ -61,11 +66,14 @@ ], "@graphql-tools/wrap": ["./packages/wrap/src/index.ts"], "@graphql-tools/executor-*": ["./packages/executors/*/src/index.ts"], - "@graphql-hive/logger-json": ["./packages/logger-json/src/index.ts"], - "@graphql-hive/logger-winston": [ - "./packages/logger-winston/src/index.ts" + "@graphql-hive/logger": ["./packages/logger/src/index.ts"], + "@graphql-hive/logger/request": ["./packages/logger/src/request.ts"], + "@graphql-hive/logger/writers/pino": [ + "./packages/logger/src/writers/pino.ts" + ], + "@graphql-hive/logger/writers/winston": [ + "./packages/logger/src/writers/winston.ts" ], - "@graphql-hive/logger-pino": ["./packages/logger-pino/src/index.ts"], "@graphql-hive/plugin-aws-sigv4": [ "./packages/plugins/aws-sigv4/src/index.ts" ], diff --git a/yarn.lock b/yarn.lock index 3fcc8f4f0..59d0e9d61 100644 --- a/yarn.lock +++ b/yarn.lock @@ -941,7 +941,7 @@ __metadata: languageName: node linkType: hard -"@aws-sdk/types@npm:3.840.0, @aws-sdk/types@npm:^3.222.0": +"@aws-sdk/types@npm:3.840.0": version: 3.840.0 resolution: "@aws-sdk/types@npm:3.840.0" dependencies: @@ -951,6 +951,16 @@ __metadata: languageName: node linkType: hard +"@aws-sdk/types@npm:^3.222.0": + version: 3.821.0 + resolution: "@aws-sdk/types@npm:3.821.0" + dependencies: + "@smithy/types": "npm:^4.3.1" + tslib: "npm:^2.6.2" + checksum: 10c0/6202b2c0db1dd5ee78e6dc45c51f8b19deff0ee400dd5a7a15d089cc5493a2db6a6e0553ff32742e8bc810d428b36599534e14c1b466695550aef1b1d87f043d + languageName: node + linkType: hard + "@aws-sdk/util-endpoints@npm:3.848.0": version: 3.848.0 resolution: "@aws-sdk/util-endpoints@npm:3.848.0" @@ -1013,122 +1023,6 @@ __metadata: languageName: node linkType: hard -"@azure/abort-controller@npm:^2.0.0": - version: 2.1.2 - resolution: "@azure/abort-controller@npm:2.1.2" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10c0/3771b6820e33ebb56e79c7c68e2288296b8c2529556fbd29cf4cf2fbff7776e7ce1120072972d8df9f1bf50e2c3224d71a7565362b589595563f710b8c3d7b79 - languageName: node - linkType: hard - -"@azure/core-auth@npm:^1.4.0, @azure/core-auth@npm:^1.8.0, @azure/core-auth@npm:^1.9.0": - version: 1.9.0 - resolution: "@azure/core-auth@npm:1.9.0" - dependencies: - "@azure/abort-controller": "npm:^2.0.0" - "@azure/core-util": "npm:^1.11.0" - tslib: "npm:^2.6.2" - checksum: 10c0/b7d8f33b81a8c9a76531acacc7af63d888429f0d763bb1ab8e28e91ddbf1626fc19cf8ca74f79c39b0a3e5acb315bdc4c4276fb979816f315712ea1bd611273d - languageName: node - linkType: hard - -"@azure/core-client@npm:^1.9.2": - version: 1.9.4 - resolution: "@azure/core-client@npm:1.9.4" - dependencies: - "@azure/abort-controller": "npm:^2.0.0" - "@azure/core-auth": "npm:^1.4.0" - "@azure/core-rest-pipeline": "npm:^1.20.0" - "@azure/core-tracing": "npm:^1.0.0" - "@azure/core-util": "npm:^1.6.1" - "@azure/logger": "npm:^1.0.0" - tslib: "npm:^2.6.2" - checksum: 10c0/c38c494c0bf085a89720d97c5bfc098cd1a2bbc5b9c41f8c32ecf2f3b81b476af1afe2f0d996d7e23900581415306e59280d507410bf0aa80804e0e411b8a2be - languageName: node - linkType: hard - -"@azure/core-rest-pipeline@npm:^1.19.0": - version: 1.19.1 - resolution: "@azure/core-rest-pipeline@npm:1.19.1" - dependencies: - "@azure/abort-controller": "npm:^2.0.0" - "@azure/core-auth": "npm:^1.8.0" - "@azure/core-tracing": "npm:^1.0.1" - "@azure/core-util": "npm:^1.11.0" - "@azure/logger": "npm:^1.0.0" - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.0" - tslib: "npm:^2.6.2" - checksum: 10c0/23f2cb9d08e9535bcdca3123aa89bcfe8c5b9d37d6e9c0f87fa8009a089989bebfa11f5bc6fd96ceb6acee210b00b754ec7e02f3f7ee8916e8593e9ef0d610b8 - languageName: node - linkType: hard - -"@azure/core-rest-pipeline@npm:^1.20.0": - version: 1.20.0 - resolution: "@azure/core-rest-pipeline@npm:1.20.0" - dependencies: - "@azure/abort-controller": "npm:^2.0.0" - "@azure/core-auth": "npm:^1.8.0" - "@azure/core-tracing": "npm:^1.0.1" - "@azure/core-util": "npm:^1.11.0" - "@azure/logger": "npm:^1.0.0" - "@typespec/ts-http-runtime": "npm:^0.2.2" - tslib: "npm:^2.6.2" - checksum: 10c0/d82094805fad3ef7b5c2646c21e3fc62ff9534806bfc9840a9439244800cdb3ea7e539f40f5077e7c4db2656f882d8a56ea736e81167ee39dd9f426919fe72f2 - languageName: node - linkType: hard - -"@azure/core-tracing@npm:^1.0.0, @azure/core-tracing@npm:^1.0.1": - version: 1.2.0 - resolution: "@azure/core-tracing@npm:1.2.0" - dependencies: - tslib: "npm:^2.6.2" - checksum: 10c0/7cd114b3c11730a1b8b71d89b64f9d033dfe0710f2364ef65645683381e2701173c08ff8625a0b0bc65bb3c3e0de46c80fdb2735e37652425489b65a283f043d - languageName: node - linkType: hard - -"@azure/core-util@npm:^1.11.0, @azure/core-util@npm:^1.6.1": - version: 1.12.0 - resolution: "@azure/core-util@npm:1.12.0" - dependencies: - "@azure/abort-controller": "npm:^2.0.0" - "@typespec/ts-http-runtime": "npm:^0.2.2" - tslib: "npm:^2.6.2" - checksum: 10c0/9335e619078781a14c616840125deaaaef89198d9c99e6c9cd4452e9b18d4f95823246da4b8277ba186647e05ebcd88b419372616d1acad020c0172340ae02e5 - languageName: node - linkType: hard - -"@azure/logger@npm:^1.0.0": - version: 1.2.0 - resolution: "@azure/logger@npm:1.2.0" - dependencies: - "@typespec/ts-http-runtime": "npm:^0.2.2" - tslib: "npm:^2.6.2" - checksum: 10c0/a04693423143204d25e82bde1c57ddb53456fd00be0dbd91d1b078b0e17a1d564ababfd92467d9e51e201a0a756c6611918d7207b17bc4feb417885b80770ecd - languageName: node - linkType: hard - -"@azure/monitor-opentelemetry-exporter@npm:^1.0.0-beta.27": - version: 1.0.0-beta.31 - resolution: "@azure/monitor-opentelemetry-exporter@npm:1.0.0-beta.31" - dependencies: - "@azure/core-auth": "npm:^1.9.0" - "@azure/core-client": "npm:^1.9.2" - "@azure/core-rest-pipeline": "npm:^1.19.0" - "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/api-logs": "npm:^0.57.2" - "@opentelemetry/core": "npm:^1.30.1" - "@opentelemetry/resources": "npm:^1.30.1" - "@opentelemetry/sdk-logs": "npm:^0.57.2" - "@opentelemetry/sdk-metrics": "npm:^1.30.1" - "@opentelemetry/sdk-trace-base": "npm:^1.30.1" - "@opentelemetry/semantic-conventions": "npm:^1.30.0" - tslib: "npm:^2.8.1" - checksum: 10c0/8c00d851518d57f695e78e42542a2f1c971015d2e6a05045742d83c1c806119179d2b44ee05bdd12e1e36be016064e00f728a1c3ddb11a18408b2f18be177064 - languageName: node - linkType: hard - "@babel/code-frame@npm:^7.0.0, @babel/code-frame@npm:^7.12.13, @babel/code-frame@npm:^7.27.1": version: 7.27.1 resolution: "@babel/code-frame@npm:7.27.1" @@ -1147,7 +1041,7 @@ __metadata: languageName: node linkType: hard -"@babel/core@npm:7.28.0, @babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.7, @babel/core@npm:^7.26.10": +"@babel/core@npm:7.28.0": version: 7.28.0 resolution: "@babel/core@npm:7.28.0" dependencies: @@ -1170,6 +1064,29 @@ __metadata: languageName: node linkType: hard +"@babel/core@npm:^7.11.6, @babel/core@npm:^7.12.3, @babel/core@npm:^7.23.9, @babel/core@npm:^7.24.7, @babel/core@npm:^7.26.10": + version: 7.27.4 + resolution: "@babel/core@npm:7.27.4" + dependencies: + "@ampproject/remapping": "npm:^2.2.0" + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.27.3" + "@babel/helper-compilation-targets": "npm:^7.27.2" + "@babel/helper-module-transforms": "npm:^7.27.3" + "@babel/helpers": "npm:^7.27.4" + "@babel/parser": "npm:^7.27.4" + "@babel/template": "npm:^7.27.2" + "@babel/traverse": "npm:^7.27.4" + "@babel/types": "npm:^7.27.3" + convert-source-map: "npm:^2.0.0" + debug: "npm:^4.1.0" + gensync: "npm:^1.0.0-beta.2" + json5: "npm:^2.2.3" + semver: "npm:^6.3.1" + checksum: 10c0/d2d17b106a8d91d3eda754bb3f26b53a12eb7646df73c2b2d2e9b08d90529186bc69e3823f70a96ec6e5719dc2372fb54e14ad499da47ceeb172d2f7008787b5 + languageName: node + linkType: hard + "@babel/generator@npm:^7.16.0, @babel/generator@npm:^7.26.2, @babel/generator@npm:^7.28.0, @babel/generator@npm:^7.7.2": version: 7.28.0 resolution: "@babel/generator@npm:7.28.0" @@ -1183,6 +1100,19 @@ __metadata: languageName: node linkType: hard +"@babel/generator@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/generator@npm:7.27.3" + dependencies: + "@babel/parser": "npm:^7.27.3" + "@babel/types": "npm:^7.27.3" + "@jridgewell/gen-mapping": "npm:^0.3.5" + "@jridgewell/trace-mapping": "npm:^0.3.25" + jsesc: "npm:^3.0.2" + checksum: 10c0/341622e17c61d008fc746b655ab95ef7febb543df8efb4148f57cf06e60ade1abe091ed7d6811df17b064d04d64f69bb7f35ab0654137116d55c54a73145a61a + languageName: node + linkType: hard + "@babel/helper-annotate-as-pure@npm:^7.27.1, @babel/helper-annotate-as-pure@npm:^7.27.3": version: 7.27.3 resolution: "@babel/helper-annotate-as-pure@npm:7.27.3" @@ -1374,6 +1304,16 @@ __metadata: languageName: node linkType: hard +"@babel/helpers@npm:^7.27.4": + version: 7.27.4 + resolution: "@babel/helpers@npm:7.27.4" + dependencies: + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.27.3" + checksum: 10c0/3463551420926b3f403c1a30d66ac67bba5c4f73539a8ccb71544da129c4709ac37c57fac740ed8a261b3e6bbbf353b05e03b36ea1a6bf1081604b2a94ca43c1 + languageName: node + linkType: hard + "@babel/helpers@npm:^7.27.6": version: 7.27.6 resolution: "@babel/helpers@npm:7.27.6" @@ -1395,6 +1335,17 @@ __metadata: languageName: node linkType: hard +"@babel/parser@npm:^7.27.3, @babel/parser@npm:^7.27.4": + version: 7.27.4 + resolution: "@babel/parser@npm:7.27.4" + dependencies: + "@babel/types": "npm:^7.27.3" + bin: + parser: ./bin/babel-parser.js + checksum: 10c0/d1bf17e7508585235e2a76594ba81828e48851877112bb8abbecd7161a31fb66654e993e458ddaedb18a3d5fa31970e5f3feca5ae2900f51e6d8d3d35da70dbf + languageName: node + linkType: hard + "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:^7.27.1": version: 7.27.1 resolution: "@babel/plugin-bugfix-firefox-class-in-computed-class-key@npm:7.27.1" @@ -2519,7 +2470,42 @@ __metadata: languageName: node linkType: hard -"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.27.3, @babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": +"@babel/traverse@npm:^7.27.4": + version: 7.27.4 + resolution: "@babel/traverse@npm:7.27.4" + dependencies: + "@babel/code-frame": "npm:^7.27.1" + "@babel/generator": "npm:^7.27.3" + "@babel/parser": "npm:^7.27.4" + "@babel/template": "npm:^7.27.2" + "@babel/types": "npm:^7.27.3" + debug: "npm:^4.3.1" + globals: "npm:^11.1.0" + checksum: 10c0/6de8aa2a0637a6ee6d205bf48b9e923928a02415771fdec60085ed754dcdf605e450bb3315c2552fa51c31a4662275b45d5ae4ad527ce55a7db9acebdbbbb8ed + languageName: node + linkType: hard + +"@babel/types@npm:^7.0.0, @babel/types@npm:^7.20.7, @babel/types@npm:^7.26.0, @babel/types@npm:^7.26.10, @babel/types@npm:^7.27.1, @babel/types@npm:^7.3.3, @babel/types@npm:^7.4.4": + version: 7.27.1 + resolution: "@babel/types@npm:7.27.1" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10c0/ed736f14db2fdf0d36c539c8e06b6bb5e8f9649a12b5c0e1c516fed827f27ef35085abe08bf4d1302a4e20c9a254e762eed453bce659786d4a6e01ba26a91377 + languageName: node + linkType: hard + +"@babel/types@npm:^7.27.3": + version: 7.27.3 + resolution: "@babel/types@npm:7.27.3" + dependencies: + "@babel/helper-string-parser": "npm:^7.27.1" + "@babel/helper-validator-identifier": "npm:^7.27.1" + checksum: 10c0/bafdfc98e722a6b91a783b6f24388f478fd775f0c0652e92220e08be2cc33e02d42088542f1953ac5e5ece2ac052172b3dadedf12bec9aae57899e92fb9a9757 + languageName: node + linkType: hard + +"@babel/types@npm:^7.27.6, @babel/types@npm:^7.28.0": version: 7.28.1 resolution: "@babel/types@npm:7.28.1" dependencies: @@ -3147,6 +3133,7 @@ __metadata: "@apollo/server": "npm:^4.12.2" "@apollo/subgraph": "npm:^2.11.2" "@graphql-hive/gateway": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/compose-cli": "npm:^1.4.12" "@graphql-mesh/hmac-upstream-signature": "workspace:^" "@graphql-mesh/plugin-jwt-auth": "workspace:^" @@ -3201,6 +3188,12 @@ __metadata: languageName: unknown linkType: soft +"@e2e/load-on-init@workspace:e2e/load-on-init": + version: 0.0.0-use.local + resolution: "@e2e/load-on-init@workspace:e2e/load-on-init" + languageName: unknown + linkType: soft + "@e2e/location-string-interpolation@workspace:e2e/location-string-interpolation": version: 0.0.0-use.local resolution: "@e2e/location-string-interpolation@workspace:e2e/location-string-interpolation" @@ -4112,8 +4105,9 @@ __metadata: "@envelop/core": "npm:^5.3.0" "@envelop/disable-introspection": "npm:^8.0.0" "@envelop/generic-auth": "npm:^9.0.0" + "@envelop/instrumentation": "npm:^1.0.0" "@graphql-hive/core": "npm:^0.13.0" - "@graphql-hive/logger-json": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-hive/pubsub": "workspace:^" "@graphql-hive/signal": "workspace:^" "@graphql-hive/yoga": "npm:^0.42.2" @@ -4139,8 +4133,11 @@ __metadata: "@graphql-yoga/plugin-defer-stream": "npm:^3.15.1" "@graphql-yoga/plugin-persisted-operations": "npm:^3.15.1" "@omnigraph/openapi": "npm:^0.109.11" + "@opentelemetry/api": "npm:^1.9.0" "@types/html-minifier-terser": "npm:^7.0.2" "@types/node": "npm:^22.15.30" + "@types/react": "npm:^19" + "@types/react-dom": "npm:^19" "@whatwg-node/disposablestack": "npm:^0.0.6" "@whatwg-node/fetch": "npm:^0.10.9" "@whatwg-node/promise-helpers": "npm:^1.3.0" @@ -4153,6 +4150,8 @@ __metadata: graphql-yoga: "npm:^5.15.1" html-minifier-terser: "npm:7.2.0" pkgroll: "npm:2.15.0" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" tslib: "npm:^2.8.1" tsx: "npm:4.20.3" peerDependencies: @@ -4171,6 +4170,7 @@ __metadata: "@escape.tech/graphql-armor-max-tokens": "npm:^2.5.0" "@graphql-hive/gateway-runtime": "workspace:^" "@graphql-hive/importer": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-hive/plugin-aws-sigv4": "workspace:^" "@graphql-hive/plugin-deduplicate-request": "workspace:^" "@graphql-hive/pubsub": "workspace:^" @@ -4183,7 +4183,6 @@ __metadata: "@graphql-mesh/plugin-http-cache": "npm:^0.105.8" "@graphql-mesh/plugin-jit": "npm:^0.2.7" "@graphql-mesh/plugin-jwt-auth": "workspace:^" - "@graphql-mesh/plugin-mock": "npm:^0.105.8" "@graphql-mesh/plugin-opentelemetry": "workspace:^" "@graphql-mesh/plugin-prometheus": "workspace:^" "@graphql-mesh/plugin-rate-limit": "npm:^0.104.7" @@ -4201,9 +4200,22 @@ __metadata: "@graphql-tools/load": "npm:^8.1.2" "@graphql-tools/utils": "npm:^10.9.1" "@graphql-yoga/render-graphiql": "npm:^5.15.1" + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/context-async-hooks": "npm:^2.0.1" + "@opentelemetry/context-zone": "npm:^2.0.1" + "@opentelemetry/core": "npm:^2.0.1" + "@opentelemetry/exporter-jaeger": "npm:^2.0.1" + "@opentelemetry/exporter-zipkin": "npm:^2.0.1" + "@opentelemetry/propagator-b3": "npm:^2.0.1" + "@opentelemetry/propagator-jaeger": "npm:^2.0.1" + "@opentelemetry/sampler-jaeger-remote": "npm:^0.203.0" + "@opentelemetry/sdk-logs": "npm:^0.203.0" + "@opentelemetry/sdk-metrics": "npm:^2.0.1" + "@opentelemetry/sdk-trace-base": "patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch" "@rollup/plugin-commonjs": "npm:^28.0.0" "@rollup/plugin-json": "npm:^6.1.0" - "@rollup/plugin-node-resolve": "npm:^16.0.0" + "@rollup/plugin-node-resolve": "patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch" "@rollup/plugin-sucrase": "npm:^5.0.2" "@tsconfig/node18": "npm:^18.2.4" "@types/adm-zip": "npm:^0.5.5" @@ -4244,52 +4256,29 @@ __metadata: languageName: unknown linkType: soft -"@graphql-hive/logger-json@workspace:^, @graphql-hive/logger-json@workspace:packages/logger-json": - version: 0.0.0-use.local - resolution: "@graphql-hive/logger-json@workspace:packages/logger-json" - dependencies: - "@graphql-mesh/cross-helpers": "npm:^0.4.10" - "@graphql-mesh/types": "npm:^0.104.7" - "@graphql-mesh/utils": "npm:^0.104.7" - cross-inspect: "npm:^1.0.1" - graphql: "npm:^16.9.0" - pkgroll: "npm:2.15.0" - tslib: "npm:^2.8.1" - peerDependencies: - graphql: ^15.9.0 || ^16.9.0 - languageName: unknown - linkType: soft - -"@graphql-hive/logger-pino@workspace:packages/logger-pino": +"@graphql-hive/logger@workspace:^, @graphql-hive/logger@workspace:packages/logger": version: 0.0.0-use.local - resolution: "@graphql-hive/logger-pino@workspace:packages/logger-pino" + resolution: "@graphql-hive/logger@workspace:packages/logger" dependencies: - "@graphql-mesh/types": "npm:^0.104.7" - "@graphql-mesh/utils": "npm:^0.104.7" - "@whatwg-node/disposablestack": "npm:^0.0.6" - graphql: "npm:16.11.0" - pino: "npm:^9.7.0" - pkgroll: "npm:2.15.0" - tslib: "npm:^2.8.1" - peerDependencies: - graphql: ^15.9.0 || ^16.9.0 - pino: ^9.7.0 - languageName: unknown - linkType: soft - -"@graphql-hive/logger-winston@workspace:packages/logger-winston": - version: 0.0.0-use.local - resolution: "@graphql-hive/logger-winston@workspace:packages/logger-winston" - dependencies: - "@graphql-mesh/types": "npm:^0.104.7" + "@logtape/logtape": "npm:^1.0.0" + "@types/quick-format-unescaped": "npm:^4.0.3" "@whatwg-node/disposablestack": "npm:^0.0.6" - graphql: "npm:16.11.0" - pkgroll: "npm:2.15.0" - tslib: "npm:^2.8.1" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + fast-safe-stringify: "npm:^2.1.1" + pino: "npm:^9.6.0" + pkgroll: "npm:2.11.2" + quick-format-unescaped: "npm:^4.0.4" winston: "npm:^3.17.0" peerDependencies: - graphql: ^15.9.0 || ^16.9.0 - winston: ^3.17.0 + "@logtape/logtape": ^1.0.0 + pino: ^9.6.0 + peerDependenciesMeta: + "@logtape/logtape": + optional: true + pino: + optional: true + winston: + optional: true languageName: unknown linkType: soft @@ -4298,6 +4287,7 @@ __metadata: resolution: "@graphql-hive/nestjs@workspace:packages/nestjs" dependencies: "@graphql-hive/gateway": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/types": "npm:^0.104.7" "@graphql-tools/utils": "npm:^10.9.1" "@nestjs/common": "npm:11.1.5" @@ -4564,6 +4554,7 @@ __metadata: dependencies: "@envelop/core": "npm:^5.3.0" "@envelop/instrumentation": "npm:^1.0.0" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/cross-helpers": "npm:^0.4.10" "@graphql-mesh/transport-common": "workspace:^" "@graphql-mesh/types": "npm:^0.104.7" @@ -4693,52 +4684,40 @@ __metadata: languageName: node linkType: hard -"@graphql-mesh/plugin-mock@npm:^0.105.8": - version: 0.105.8 - resolution: "@graphql-mesh/plugin-mock@npm:0.105.8" - dependencies: - "@graphql-mesh/cross-helpers": "npm:^0.4.10" - "@graphql-mesh/string-interpolation": "npm:^0.5.8" - "@graphql-mesh/types": "npm:^0.104.7" - "@graphql-mesh/utils": "npm:^0.104.7" - "@graphql-tools/mock": "npm:^9.0.3" - "@graphql-tools/schema": "npm:^10.0.5" - "@graphql-tools/utils": "npm:^10.8.0" - faker: "npm:5.5.3" - graphql-scalars: "npm:^1.22.4" - tslib: "npm:^2.4.0" - peerDependencies: - graphql: "*" - checksum: 10c0/d50246cbbe84a1e2d281d2dc0504b55899496f5c484fa858cd40854f6765e6e8f5db4b92e2a58bf79154169da835eafd49665a4a44ba5aac769162a797720cb6 - languageName: node - linkType: hard - "@graphql-mesh/plugin-opentelemetry@workspace:^, @graphql-mesh/plugin-opentelemetry@workspace:packages/plugins/opentelemetry": version: 0.0.0-use.local resolution: "@graphql-mesh/plugin-opentelemetry@workspace:packages/plugins/opentelemetry" dependencies: - "@azure/monitor-opentelemetry-exporter": "npm:^1.0.0-beta.27" + "@graphql-hive/core": "npm:^0.13.0" "@graphql-hive/gateway-runtime": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/cross-helpers": "npm:^0.4.10" "@graphql-mesh/transport-common": "workspace:^" "@graphql-mesh/types": "npm:^0.104.7" "@graphql-mesh/utils": "npm:^0.104.7" "@graphql-tools/utils": "npm:^10.9.1" "@opentelemetry/api": "npm:^1.9.0" - "@opentelemetry/core": "npm:^1.30.0" - "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.57.0" - "@opentelemetry/exporter-trace-otlp-http": "npm:^0.57.0" - "@opentelemetry/exporter-zipkin": "npm:^1.29.0" - "@opentelemetry/instrumentation": "npm:^0.57.0" - "@opentelemetry/resources": "npm:^1.29.0" - "@opentelemetry/sdk-trace-base": "npm:^1.29.0" - "@opentelemetry/sdk-trace-web": "npm:^1.29.0" - "@opentelemetry/semantic-conventions": "npm:^1.28.0" - "@whatwg-node/promise-helpers": "npm:^1.3.0" + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/auto-instrumentations-node": "npm:^0.62.1" + "@opentelemetry/context-async-hooks": "npm:^2.0.1" + "@opentelemetry/core": "npm:^2.0.1" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.203.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.203.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/resources": "npm:^2.0.1" + "@opentelemetry/sdk-logs": "npm:^0.203.0" + "@opentelemetry/sdk-node": "npm:^0.203.0" + "@opentelemetry/sdk-trace-base": "patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch" + "@opentelemetry/semantic-conventions": "npm:^1.36.0" + "@whatwg-node/promise-helpers": "npm:1.3.0" + "@whatwg-node/server": "npm:^0.10.0" graphql: "npm:^16.9.0" graphql-yoga: "npm:^5.15.1" pkgroll: "npm:2.15.0" + rimraf: "npm:^6.0.1" + rollup: "npm:^4.41.1" tslib: "npm:^2.8.1" + tsx: "npm:^4.19.4" peerDependencies: graphql: ^15.9.0 || ^16.9.0 languageName: unknown @@ -4749,6 +4728,7 @@ __metadata: resolution: "@graphql-mesh/plugin-prometheus@workspace:packages/plugins/prometheus" dependencies: "@graphql-hive/gateway-runtime": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/cross-helpers": "npm:^0.4.10" "@graphql-mesh/types": "npm:^0.104.7" "@graphql-mesh/utils": "npm:^0.104.7" @@ -4857,6 +4837,7 @@ __metadata: resolution: "@graphql-mesh/transport-common@workspace:packages/transports/common" dependencies: "@envelop/core": "npm:^5.3.0" + "@graphql-hive/logger": "workspace:^" "@graphql-hive/pubsub": "workspace:^" "@graphql-hive/signal": "workspace:^" "@graphql-mesh/cross-helpers": "npm:^0.4.10" @@ -5187,10 +5168,10 @@ __metadata: linkType: soft "@graphql-tools/executor@npm:^1.3.2, @graphql-tools/executor@npm:^1.3.6, @graphql-tools/executor@npm:^1.4.0": - version: 1.4.8 - resolution: "@graphql-tools/executor@npm:1.4.8" + version: 1.4.7 + resolution: "@graphql-tools/executor@npm:1.4.7" dependencies: - "@graphql-tools/utils": "npm:^10.9.0" + "@graphql-tools/utils": "npm:^10.8.6" "@graphql-typed-document-node/core": "npm:^3.2.0" "@repeaterjs/repeater": "npm:^3.0.4" "@whatwg-node/disposablestack": "npm:^0.0.6" @@ -5198,7 +5179,7 @@ __metadata: tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/a0c5ad381ee585df76a6d0a28fb99f881328fb9449c2a0f030a4a0d17e664d4be67a1451072a761aecc9a23c8f9d2e98fb670633c22e6cd8186de841b68965b4 + checksum: 10c0/aea551a5e7e926078e9ff78fb2a3e6660367e5d12e0cb9971f69881ff40ad423e634b2f511b790766e7c58eaad27e6016af41caaf392e5c14a3662c8637b0b97 languageName: node linkType: hard @@ -5342,8 +5323,8 @@ __metadata: linkType: hard "@graphql-tools/load@npm:^8.0.1": - version: 8.0.19 - resolution: "@graphql-tools/load@npm:8.0.19" + version: 8.1.0 + resolution: "@graphql-tools/load@npm:8.1.0" dependencies: "@graphql-tools/schema": "npm:^10.0.23" "@graphql-tools/utils": "npm:^10.8.6" @@ -5351,7 +5332,7 @@ __metadata: tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/c83dbd3a6cff3784ce3d2e2c4b2381f9af7469d0ff474d175d180fa9b7d14a25e61d1d75134f8049501523ac1e79261e4d07c6d96c0afab1e5023391df522cc3 + checksum: 10c0/4653e50ba45cc940400853fcff7bb77f63b133018f114ebc355e4f3033d28c788579277d9be45223de89fe8a25509fe9f99bfcc74508439065577be65fa34eac languageName: node linkType: hard @@ -5418,9 +5399,9 @@ __metadata: languageName: node linkType: hard -"@graphql-tools/mock@npm:^9.0.3": - version: 9.0.22 - resolution: "@graphql-tools/mock@npm:9.0.22" +"@graphql-tools/mock@npm:^9.0.23": + version: 9.0.23 + resolution: "@graphql-tools/mock@npm:9.0.23" dependencies: "@graphql-tools/schema": "npm:^10.0.23" "@graphql-tools/utils": "npm:^10.8.6" @@ -5428,7 +5409,7 @@ __metadata: tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/3e79c7e8ef2dcf787c65dda66c6dadd81252e7a05c201c394d4286d86b928ffe20c2153db746b47dc340c5c9805b3528a28441ac26bfbc0188ca2469652fcbdc + checksum: 10c0/f6d2642bc5e67800a8ca8572685f144af2e483cd7ceb9f8777fed5e948cd60a47b3513727d07a1b8cd030dbf6c7d50ca004a26fb486e3a0cc912de61b3a6d9fb languageName: node linkType: hard @@ -5493,6 +5474,7 @@ __metadata: "@graphql-tools/delegate": "workspace:^" "@graphql-tools/executor": "npm:^1.4.9" "@graphql-tools/merge": "npm:^9.1.1" + "@graphql-tools/mock": "npm:^9.0.23" "@graphql-tools/schema": "npm:^10.0.25" "@graphql-tools/utils": "npm:^10.9.1" "@graphql-tools/wrap": "workspace:^" @@ -5521,39 +5503,9 @@ __metadata: languageName: unknown linkType: soft -"@graphql-tools/utils@npm:10.8.6": - version: 10.8.6 - resolution: "@graphql-tools/utils@npm:10.8.6" - dependencies: - "@graphql-typed-document-node/core": "npm:^3.1.1" - "@whatwg-node/promise-helpers": "npm:^1.0.0" - cross-inspect: "npm:1.0.1" - dset: "npm:^3.1.4" - tslib: "npm:^2.4.0" - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/17f727eb85415c15c5920ab9ef4648e0d205e1ca8b7d8539ac84f55da04ed60464313792456ebbde30bb883c0abde8df81919fd22f2ed5096b873920e84bef4b - languageName: node - linkType: hard - -"@graphql-tools/utils@npm:^10.0.0, @graphql-tools/utils@npm:^10.0.3, @graphql-tools/utils@npm:^10.5.1, @graphql-tools/utils@npm:^10.5.4, @graphql-tools/utils@npm:^10.6.1, @graphql-tools/utils@npm:^10.6.2, @graphql-tools/utils@npm:^10.6.4, @graphql-tools/utils@npm:^10.8.0, @graphql-tools/utils@npm:^10.8.6, @graphql-tools/utils@npm:^10.9.0": - version: 10.9.0 - resolution: "@graphql-tools/utils@npm:10.9.0" - dependencies: - "@graphql-typed-document-node/core": "npm:^3.1.1" - "@whatwg-node/promise-helpers": "npm:^1.0.0" - cross-inspect: "npm:1.0.1" - dset: "npm:^3.1.4" - tslib: "npm:^2.4.0" - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/fd8e82ff87c3debd78f774327160da81381c9b6746022b7e1e20861690c11c71022a6924d61da38460414046416a48eb8355556fd36dfc726ed160501ba009f7 - languageName: node - linkType: hard - -"@graphql-tools/utils@npm:^10.9.1": - version: 10.9.1 - resolution: "@graphql-tools/utils@npm:10.9.1" +"@graphql-tools/utils@npm:10.9.0-alpha-20250710200000-fde1c74a0c2fa4f651cbeed5b2091aeda7afb162": + version: 10.9.0-alpha-20250710200000-fde1c74a0c2fa4f651cbeed5b2091aeda7afb162 + resolution: "@graphql-tools/utils@npm:10.9.0-alpha-20250710200000-fde1c74a0c2fa4f651cbeed5b2091aeda7afb162" dependencies: "@graphql-typed-document-node/core": "npm:^3.1.1" "@whatwg-node/promise-helpers": "npm:^1.0.0" @@ -5562,30 +5514,7 @@ __metadata: tslib: "npm:^2.4.0" peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/97199f52d0235124d4371f7f54cc0df5ce9df6d8aae716ac05d8ebeda4b5ee3faf1fca94d5d1c521a565e152f8e02a1abfb9c2629ffe805c14468aec0c3d41cf - languageName: node - linkType: hard - -"@graphql-tools/utils@npm:^8.5.2": - version: 8.13.1 - resolution: "@graphql-tools/utils@npm:8.13.1" - dependencies: - tslib: "npm:^2.4.0" - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/f9bab1370aa91e706abec4c8ea980e15293cb78bd4effba53ad2365dc39d81148db7667b3ef89b35f0a0b0ad58081ffdac4264b7125c69fa8393590ae5025745 - languageName: node - linkType: hard - -"@graphql-tools/utils@npm:^9.2.1": - version: 9.2.1 - resolution: "@graphql-tools/utils@npm:9.2.1" - dependencies: - "@graphql-typed-document-node/core": "npm:^3.1.1" - tslib: "npm:^2.4.0" - peerDependencies: - graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 || ^17.0.0 - checksum: 10c0/37a7bd7e14d28ff1bacc007dca84bc6cef2d7d7af9a547b5dbe52fcd134afddd6d4a7b2148cfbaff5ddba91a868453d597da77bd0457fb0be15928f916901606 + checksum: 10c0/550a50fcfa9142dc7e40a96f69526be56efdb8ac4ba64b5dfe52400eeaaa40ea0e3724ead6c3248f811ac2cc8ceee59e9e0635349844242c1118e89d2b3033db languageName: node linkType: hard @@ -5594,6 +5523,7 @@ __metadata: resolution: "@graphql-tools/wrap@workspace:packages/wrap" dependencies: "@graphql-tools/delegate": "workspace:^" + "@graphql-tools/mock": "npm:^9.0.23" "@graphql-tools/schema": "npm:^10.0.25" "@graphql-tools/utils": "npm:^10.9.1" "@whatwg-node/promise-helpers": "npm:^1.3.0" @@ -5692,7 +5622,7 @@ __metadata: languageName: node linkType: hard -"@graphql-yoga/plugin-persisted-operations@npm:^3.15.1, @graphql-yoga/plugin-persisted-operations@npm:^3.9.0": +"@graphql-yoga/plugin-persisted-operations@npm:^3.15.1": version: 3.15.1 resolution: "@graphql-yoga/plugin-persisted-operations@npm:3.15.1" dependencies: @@ -5704,6 +5634,18 @@ __metadata: languageName: node linkType: hard +"@graphql-yoga/plugin-persisted-operations@npm:^3.9.0": + version: 3.13.6 + resolution: "@graphql-yoga/plugin-persisted-operations@npm:3.13.6" + dependencies: + "@whatwg-node/promise-helpers": "npm:^1.2.4" + peerDependencies: + graphql: ^15.2.0 || ^16.0.0 + graphql-yoga: ^5.13.5 + checksum: 10c0/586170d8dfcaee285e3a85009ea6507e945c7c06cf17cc77293b3ce9d83162eec02192058b2e7d53937f36e4f835c47b0f396962f9028374699f9ea2673161e3 + languageName: node + linkType: hard + "@graphql-yoga/plugin-prometheus@npm:^6.10.1": version: 6.10.1 resolution: "@graphql-yoga/plugin-prometheus@npm:6.10.1" @@ -6531,6 +6473,13 @@ __metadata: languageName: node linkType: hard +"@logtape/logtape@npm:^1.0.0": + version: 1.0.0 + resolution: "@logtape/logtape@npm:1.0.0" + checksum: 10c0/c1f70c0b61109c67980595d0794205652e5cc6d1d5d297712902f4cf8e0992a69033d8099b5a7bb2f11d1d4b6d045f712d6aa78c95e737f3324ed09af62adfb5 + languageName: node + linkType: hard + "@lukeed/csprng@npm:^1.0.0": version: 1.1.0 resolution: "@lukeed/csprng@npm:1.1.0" @@ -6983,21 +6932,12 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api-logs@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/api-logs@npm:0.56.0" - dependencies: - "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/af78b5534fd8f93edc23811349c88acf9e7cc2c7d94f58a2b58f70016f97aaa80878096c46283fdb53fb7375df83f1a048ac8d5f52b3dc1c98a2184c3a5d50ff - languageName: node - linkType: hard - -"@opentelemetry/api-logs@npm:0.57.2, @opentelemetry/api-logs@npm:^0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/api-logs@npm:0.57.2" +"@opentelemetry/api-logs@npm:0.203.0, @opentelemetry/api-logs@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/api-logs@npm:0.203.0" dependencies: "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/1e514d3fd4ca68e7e8b008794a95ee0562a5d9e1d3ebb02647b245afaa6c2d72cc14e99e3ea47a1d1007f8a965c62bfb6170e1aa26756230bea063cfde2898bf + checksum: 10c0/e7a0a0ff46aaeb62192a99f45ef4889222e4fea09be25cab6fea811afc2df95c02ea050b2c98dfc0fc5a6ec6a623d87096af2751fdf91ddbb3afcab61b5325da languageName: node linkType: hard @@ -7008,304 +6948,1127 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/core@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/core@npm:1.29.0" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.28.0" +"@opentelemetry/auto-instrumentations-node@npm:^0.62.1": + version: 0.62.1 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.62.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/instrumentation-amqplib": "npm:^0.50.0" + "@opentelemetry/instrumentation-aws-lambda": "npm:^0.54.0" + "@opentelemetry/instrumentation-aws-sdk": "npm:^0.57.0" + "@opentelemetry/instrumentation-bunyan": "npm:^0.49.0" + "@opentelemetry/instrumentation-cassandra-driver": "npm:^0.49.0" + "@opentelemetry/instrumentation-connect": "npm:^0.47.0" + "@opentelemetry/instrumentation-cucumber": "npm:^0.18.1" + "@opentelemetry/instrumentation-dataloader": "npm:^0.21.1" + "@opentelemetry/instrumentation-dns": "npm:^0.47.0" + "@opentelemetry/instrumentation-express": "npm:^0.52.0" + "@opentelemetry/instrumentation-fastify": "npm:^0.48.0" + "@opentelemetry/instrumentation-fs": "npm:^0.23.0" + "@opentelemetry/instrumentation-generic-pool": "npm:^0.47.0" + "@opentelemetry/instrumentation-graphql": "npm:^0.51.0" + "@opentelemetry/instrumentation-grpc": "npm:^0.203.0" + "@opentelemetry/instrumentation-hapi": "npm:^0.50.0" + "@opentelemetry/instrumentation-http": "npm:^0.203.0" + "@opentelemetry/instrumentation-ioredis": "npm:^0.51.0" + "@opentelemetry/instrumentation-kafkajs": "npm:^0.13.0" + "@opentelemetry/instrumentation-knex": "npm:^0.48.0" + "@opentelemetry/instrumentation-koa": "npm:^0.51.0" + "@opentelemetry/instrumentation-lru-memoizer": "npm:^0.48.0" + "@opentelemetry/instrumentation-memcached": "npm:^0.47.0" + "@opentelemetry/instrumentation-mongodb": "npm:^0.56.0" + "@opentelemetry/instrumentation-mongoose": "npm:^0.50.0" + "@opentelemetry/instrumentation-mysql": "npm:^0.49.0" + "@opentelemetry/instrumentation-mysql2": "npm:^0.50.0" + "@opentelemetry/instrumentation-nestjs-core": "npm:^0.49.0" + "@opentelemetry/instrumentation-net": "npm:^0.47.0" + "@opentelemetry/instrumentation-oracledb": "npm:^0.29.0" + "@opentelemetry/instrumentation-pg": "npm:^0.56.0" + "@opentelemetry/instrumentation-pino": "npm:^0.50.0" + "@opentelemetry/instrumentation-redis": "npm:^0.51.0" + "@opentelemetry/instrumentation-restify": "npm:^0.49.0" + "@opentelemetry/instrumentation-router": "npm:^0.48.0" + "@opentelemetry/instrumentation-runtime-node": "npm:^0.17.1" + "@opentelemetry/instrumentation-socket.io": "npm:^0.50.0" + "@opentelemetry/instrumentation-tedious": "npm:^0.22.0" + "@opentelemetry/instrumentation-undici": "npm:^0.14.0" + "@opentelemetry/instrumentation-winston": "npm:^0.48.1" + "@opentelemetry/resource-detector-alibaba-cloud": "npm:^0.31.3" + "@opentelemetry/resource-detector-aws": "npm:^2.3.0" + "@opentelemetry/resource-detector-azure": "npm:^0.10.0" + "@opentelemetry/resource-detector-container": "npm:^0.7.3" + "@opentelemetry/resource-detector-gcp": "npm:^0.37.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/sdk-node": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.4.1 + "@opentelemetry/core": ^2.0.0 + checksum: 10c0/95782f56264b2733403b31cfc8c4203c38727a5da5544d1da0379633f276b3aa76d1169bb4c1b0fbe9831fdfd3b34e5aa3cf0c303b84fee08c3eea9d1b3a4902 + languageName: node + linkType: hard + +"@opentelemetry/context-async-hooks@npm:2.0.1, @opentelemetry/context-async-hooks@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/context-async-hooks@npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/393fa276262ecc0e7bd7db5f507a2118df0725afab0cea1cb071b8d0ec879c08d9d163a83bb13f77a6bd0ad0cb66094856eb19caf225da32d3b1767156105d26 + checksum: 10c0/75b06f33b9c3dccb8d9802c14badcc3b9a497b21c77bf0344fc6231041ea1bf6a2bcc195cc27fafd5914bffcc7fa160b9f4480c06a37e86e876c98bf1a533a0d languageName: node linkType: hard -"@opentelemetry/core@npm:1.30.1, @opentelemetry/core@npm:^1.30.0, @opentelemetry/core@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/core@npm:1.30.1" - dependencies: - "@opentelemetry/semantic-conventions": "npm:1.28.0" +"@opentelemetry/context-zone-peer-dep@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/context-zone-peer-dep@npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/4c25ba50a6137c2ba9ca563fb269378f3c9ca6fd1b3f15dbb6eff78eebf5656f281997cbb7be8e51c01649fd6ad091083fcd8a42dd9b5dfac907dc06d7cfa092 + zone.js: ^0.10.2 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0 + checksum: 10c0/3a0f432d0a6982a29932ee048458e9a374c6475cbaebb8763c20d07c786367b5912c5a68a569a1db38d9c777b8feeb675f8764408f9e0b855020722e0be2b078 languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-grpc@npm:^0.57.0": - version: 0.57.2 - resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.57.2" +"@opentelemetry/context-zone@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/context-zone@npm:2.0.1" dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/otlp-exporter-base": "npm:0.57.2" - "@opentelemetry/otlp-grpc-exporter-base": "npm:0.57.2" - "@opentelemetry/otlp-transformer": "npm:0.57.2" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/sdk-trace-base": "npm:1.30.1" - peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ac88a7978f231db1d327af0c26807f01230807d4f80a04119cc487aaafed287b9383e0511895d438b76ec35d66e0243ed5ad3eecdcf797caab5f2624c4e28bb4 + "@opentelemetry/context-zone-peer-dep": "npm:2.0.1" + zone.js: "npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0" + checksum: 10c0/fc67bac8475518c2ab2bb53b74825e9946cbd1792e05d2ecd5be508abe20647b69282d7169e7a095b63e9dae7955c2244392a864f5f1c39953823fb423b7b5a6 languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.56.0" +"@opentelemetry/core@npm:2.0.1, @opentelemetry/core@npm:^2.0.0, @opentelemetry/core@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/core@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-exporter-base": "npm:0.56.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" - "@opentelemetry/resources": "npm:1.29.0" - "@opentelemetry/sdk-trace-base": "npm:1.29.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ^1.3.0 - checksum: 10c0/2e606f25312d435e48a3e221cb26ac791f789ba68fa441dae83c0f8dd4bda22964a5cf75d39b548ae482a641f552332948010f782dfc6fe1641157a8b4ef02b0 + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 languageName: node linkType: hard -"@opentelemetry/exporter-trace-otlp-http@patch:@opentelemetry/exporter-trace-otlp-http@npm:0.56.0#~/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch": - version: 0.56.0 - resolution: "@opentelemetry/exporter-trace-otlp-http@patch:@opentelemetry/exporter-trace-otlp-http@npm%3A0.56.0#~/.yarn/patches/@opentelemetry-exporter-trace-otlp-http-npm-0.56.0-dddd282e41.patch::version=0.56.0&hash=fcdbb1" +"@opentelemetry/exporter-jaeger@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/exporter-jaeger@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-exporter-base": "npm:0.56.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" - "@opentelemetry/resources": "npm:1.29.0" - "@opentelemetry/sdk-trace-base": "npm:1.29.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + jaeger-client: "npm:^3.15.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/5120cd843865fe2f2d10e782b0df5f08e7d14f747c943920405a9237c706597863c66fd0a6ca1b83790ebdd2fd0bac6e009ce0f56102cc84eebd0543ee88cdb6 + languageName: node + linkType: hard + +"@opentelemetry/exporter-logs-otlp-grpc@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/sdk-logs": "npm:0.203.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/53dc519e923bc738273a61a352988b0bfc3dee2b8138be1d0521401556fb47791e1d5eb2a90f5e94567399d1e7f484839772b7be4212b840aa014172bb9c0d1e + checksum: 10c0/61c88ecc21063d348c885b6b21bd074f0e2a931321f17bffea4dc29c7dacccd199c3d8b317a64e3c8ebb8e00a528fa04382e288e3cd7bf459774c2b22d0b2592 languageName: node linkType: hard -"@opentelemetry/exporter-zipkin@npm:^1.29.0": - version: 1.30.1 - resolution: "@opentelemetry/exporter-zipkin@npm:1.30.1" +"@opentelemetry/exporter-logs-otlp-http@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-http@npm:0.203.0" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/sdk-trace-base": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/sdk-logs": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/724109336a3fd46cddddc7fce8cec6887cecce775241a3821336a68b7b643d35b8a9830658922b5ccf3df51387bd91bfcc63afc71e15dbd403d0541561477f8f + languageName: node + linkType: hard + +"@opentelemetry/exporter-logs-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-logs-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8c85d6b07469c12c8b5e39bdd419e18224a696d2092d2a5f3df82e64b477b21c8032132eb50ad87369e9f114edb582aed4f56a457ac519411d034198916dfba0 + languageName: node + linkType: hard + +"@opentelemetry/exporter-metrics-otlp-grpc@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-grpc@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e708f099247189cbc14900c23ee32c7552b9a4356211678c3d2ed307139590cb3c5c01026304067c775fe827ccbe043e49161ad6879599a6f9305c223a85ed4e + languageName: node + linkType: hard + +"@opentelemetry/exporter-metrics-otlp-http@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/6fd10835fa998023095b57f74d550f976fe3396cfdc1004fb6f678bd9fadc86d58f428d0c0f8618d1a96f20f3fbb8c6e6e4862e4833b22bcf70dc0b7920cb049 + languageName: node + linkType: hard + +"@opentelemetry/exporter-metrics-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/0947c0b70f8644f7eabbaffb9c671752628eca3f92f372308ee51083870566c70cf8d99ca38e9b9381e6c4f78fdd8e897b1af541727faccd8ce15adb83a31aed + languageName: node + linkType: hard + +"@opentelemetry/exporter-prometheus@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-prometheus@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e931de4a7bf3c4c6896245a242997097430f5ea4cda6f80f5fce0f987d29db2c3d0ab9dc43a2e3b30b1fa92ce7300cbef15c8ac4c70d82b3b42ea86a977aa469 + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-grpc@npm:0.203.0, @opentelemetry/exporter-trace-otlp-grpc@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/67f6c63c2fcb38a63db9708cd0a420c1090717f5945767fa779a7c324adbc610f3aec44259eb7b76fbf98ee684049a42a492a54df5d37db31c4d7e460596623e + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-http@npm:0.203.0, @opentelemetry/exporter-trace-otlp-http@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/21a65ebc40dcab05cf11178e5037f96847ce344c4a855aac46dcab3f74982016318ee75fafdfeeb42f10b92a0a781b7cd8b2b5b036cbe53c14714fd13940142e + languageName: node + linkType: hard + +"@opentelemetry/exporter-trace-otlp-proto@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e7b6e159950b457970b1020f67acb1b5b8bc205c1cf099202a8d50985d674d92857633d407f81a5289235e3fccaa369900e537f312dfee47814b150d41a264b9 + languageName: node + linkType: hard + +"@opentelemetry/exporter-zipkin@npm:2.0.1, @opentelemetry/exporter-zipkin@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/exporter-zipkin@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ^1.0.0 - checksum: 10c0/e2918387c0ddc1835779f5551f5f04b3c1331643f05e3cf6019abf169de035c55be8112a30d5ee0dfef6938cbe260e84c426d5a46bce6f9eb6b2c9b916a425f6 + checksum: 10c0/10e0ad1dfb967c951d26bc64647e2f7d0705fdcf82449473308f277e1866552a07d7636bcf198e21662ada93df2366c4f24aec2d329d18e59f3d09ddcf65969d languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:^0.57.0": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation@npm:0.57.2" +"@opentelemetry/instrumentation-amqplib@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.50.0" dependencies: - "@opentelemetry/api-logs": "npm:0.57.2" - "@types/shimmer": "npm:^1.2.0" - import-in-the-middle: "npm:^1.8.1" - require-in-the-middle: "npm:^7.1.1" - semver: "npm:^7.5.2" - shimmer: "npm:^1.2.1" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/79ca65b66357665d19f89da7027da25ea1c6b55ecdacb0a99534923743c80deb9282870db563de8ae284b13e7e0aab8413efa1937f199deeaef069e07c7e4875 + checksum: 10c0/bf25dbbe38a56d35a66d03f6a49a949970a3dd47c2bad2ccaf68382ecffce8c1ca0e5e07db6fa2cf4c1c8567537daa74c4ae24d67b66f4628c3557456d9515ba languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/otlp-exporter-base@npm:0.56.0" +"@opentelemetry/instrumentation-aws-lambda@npm:^0.54.0": + version: 0.54.0 + resolution: "@opentelemetry/instrumentation-aws-lambda@npm:0.54.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/aws-lambda": "npm:8.10.150" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/45f07ffd52c65792dbc3a2d106fc4b4d9dd486b306929226ad9041d5c5dd05699c1f614610088e8ade9258ad75ff24a5e3c0d9ccdf453c52e1146e5d5550515f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-aws-sdk@npm:^0.57.0": + version: 0.57.0 + resolution: "@opentelemetry/instrumentation-aws-sdk@npm:0.57.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/propagation-utils": "npm:^0.31.3" + "@opentelemetry/semantic-conventions": "npm:^1.34.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/74dbcc236f634e1018169ecdfef316b9ab164c573a5577b1c5b04843d5b3cab25dfa4feb4d85f256abd174a1226b6f664648f51c0e37c49f6b0ab0ca45818386 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-bunyan@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-bunyan@npm:0.49.0" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@types/bunyan": "npm:1.8.11" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a68805951a8becb483448536cddc10ea25e3210e7ba5ba319d5192238bce7f626bad5725f048cba245c89365ae71805c55d1fd9a3ac8a900318770c14e87e7a5 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-cassandra-driver@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-cassandra-driver@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/beb08cb450eed076896bd892c1a11b93aa6155bd89e79211d5c421afd534a67ca2cec1c35f13025f4b3ea166095b15329bbb350a5ed62a78a08aaf5351a29c13 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-connect@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-connect@npm:0.47.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/connect": "npm:3.4.38" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/720016e6d5dab7ef6a484129537c2bf81ecae1fd5214632cf91fafe499f24935058bf98b3ca5b9f6798a265ed7208a4f248e95c406997706ce2001417703ce38 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-cucumber@npm:^0.18.1": + version: 0.18.1 + resolution: "@opentelemetry/instrumentation-cucumber@npm:0.18.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/85d72b038055ee03f1b40a72dea25e7bd6f54b72eaf37a6279a88ce98f9fae111c81623808fb6a78b325bff0303362a2f82cc41257753810619c6e0600ee2e63 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-dataloader@npm:^0.21.1": + version: 0.21.1 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.21.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a9377dee842e48da4490fa1d35c064229e8b33f988be48aeb9d50a4dc000910e43b93f8f371ae8bddc2fc40a0f3c408f675af202c64c2ce0bd61791e216897e1 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-dns@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-dns@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/abff7a6a72aa86b1745660279ec24c74694f87887063aae6ca8fbea04082e69cd33b2aa80aa0a2127938d79d753775f371edede05d682b1a61a0459523e1026f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-express@npm:^0.52.0": + version: 0.52.0 + resolution: "@opentelemetry/instrumentation-express@npm:0.52.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fb0e2122ae5c2003b107ff198c2efa7d261644d4c67a176d5cc53774f63745b465b8c427fda3426e417bf130b3db6591d11a7a1df9cd716999b432279acd158a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fastify@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-fastify@npm:0.48.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/67f730bc65e647ad6a7a1f4d2790043992ad31ef872fdb3df6b30ed65ccd05d253334428cc95efad0fcf97ceca091b785c8ba625fc1e911458bad65e579c882f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-fs@npm:^0.23.0": + version: 0.23.0 + resolution: "@opentelemetry/instrumentation-fs@npm:0.23.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2d64470b58dd47b4f56b5304b43a3dcb72f9ca7fa509951f1b6bd045664a765f00bf761b8377171ae81fb7ab250790cb7e7ef69b23dcfead0ab51cbcc1b69e3e + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-generic-pool@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/97deb1e1cc95ad0afdfb50ccff5a526f062b40bc77d05a527d47ec561d711886eb34f7321181405d570511e8ea92c3da806bc8f56b4922f7f7b0c8cefaa7fb66 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-graphql@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.51.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bbbbd1c42104b8d349ba075ec2642d87903bd4ae74a831e725785279183267f7a73560bbe4911781c12d2768670f12fc6c3ab0b5d184a3bfc1da7c3f3328fd5a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-grpc@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation-grpc@npm:0.203.0" + dependencies: + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/371259fa14d7d85ae179eda4346855994e2667e3ac6aee6b141eebc9cc138d3a22b92be0da9b73a8408b80e2417377d730a7b03189569fdc5f3d9aad32959976 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-hapi@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.50.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/aed37f4ef5934ca9f79fa596b92526c8e94e45fb0b10b8c59b6feffee1e21c558cb94cb4f635241614ff5db2c11e42830190682e7c575408270645fd3215ac1a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-http@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation-http@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + forwarded-parse: "npm:2.1.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/424eeb5b162b480a8a6157ca147ecd074de3e6d31298fed115e4d6f47ca3f65ba0a79a43f3a998ebd9f0f6e96da1092500408590150c308c5ef91c0b760ae467 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-ioredis@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.51.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/redis-common": "npm:^0.38.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/fd07ce6c07081c8323595f7784052efdb83b3f45eda46c11eb4a14f72742280592c8d1b4dab2e8d3c645d0dfbbdcbb67d867ba8d29838a630eab4fd822d27b8b + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-kafkajs@npm:^0.13.0": + version: 0.13.0 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.13.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/34b7d6ab7cde07657639dc9c5dc4f781f06ca199ffdab0ba93ab8008f63bb604a35c4ba49fdf5aa46c27ad06d7eafde2bf0bd071a60d1052ece63ac272e4135b + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-knex@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/540374797ac131000487dae0598aca44aa34760ec8c2f33809352dab08042d1c5993c7da0b7f6082f334601695f8456a5a745b42aa24368f5eeb38c38051a4ab + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-koa@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-koa@npm:0.51.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/aef15da0ae4fcc4aef59129983c143dca4e1a36d6d57dfce09bbae42d514b52a9536e7b117a8f7f6d8d006d08afdc677528653a31cea406b0ea6632a9d222c9a + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-lru-memoizer@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.48.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/1c7062bf9edf2a012826341a41fe7bd03b03835a3dc5be5a43a85f5d5ad5d474a3f9241e12906cf9454df6e10f8039d97b21645afdc5f1fb3f55b371a1ed9bd6 + checksum: 10c0/6ab7810c5602a389c0d15c2fb1f132b069e1f60ddb6ddd406a1ce5bad6f96ebe0046f1cb55cc1e4b868c7dbde45a79e8940b0128f5117a85a8fcc0a1ab78ae4f languageName: node linkType: hard -"@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm:0.56.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch": +"@opentelemetry/instrumentation-memcached@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-memcached@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/memcached": "npm:^2.2.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a788cf9bb55cafa93292e0cc1b445938517265d78c2b84cdbbfd395cafb5ca75a726b1ad5af278edd3ab59b82f57181d82c2b259b85d149e81a0226c4b39cd76 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongodb@npm:^0.56.0": version: 0.56.0 - resolution: "@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm%3A0.56.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.56.0-ba3dc5f5c5.patch::version=0.56.0&hash=5c9582" + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.56.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/2c388896ad67fab82944ffd42f4349fb488e6065a2168270a06efd469359bcc775e87e2c2448e45fa4ecfd45d8bd7bc15ee272684308d76c63483dd5c90a945f + checksum: 10c0/87b4a438c4ddb9d075986ca338ba05f8f31df13b8da3254f07d9461cb7e4ff511bdeefd527833427b1a505c2d4f26a3e3ee90795c2731405d944c50323152824 languageName: node linkType: hard -"@opentelemetry/otlp-grpc-exporter-base@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.57.2" +"@opentelemetry/instrumentation-mongoose@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.50.0" dependencies: - "@grpc/grpc-js": "npm:^1.7.1" - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/otlp-exporter-base": "npm:0.57.2" - "@opentelemetry/otlp-transformer": "npm:0.57.2" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2dd4cf1d39e3abba77eeb8540bd4fa110ddd8394f164e30fb46daaac25e65d3a021325fa096124410585cccee142c8deb99131345add6ff23617ad8d6c874b10 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql2@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/09b44e155178918567493cc00b7c9afb2eb680666205cd020cf01e670bbd4e25547b911b77c97df51ffbef277af09c66646baaff1c08020f82edf37705afcf56 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/mysql": "npm:2.15.27" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f8853919f055dbdbde80cc71a214d5f5c98e7eff6749fdcd68678387124e1de6f6ba592a2461c799e364dbdeda5a2080d712d85c104d1241c59e5effcb0da3bc + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-nestjs-core@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f44d448bb6a2d6a78af0490be6f6aad80d113fdd9a01fb32a712ad110dbbf956409da24b2df60f042a24ea5485054934f0dfeca0b2014e9070fb0eaa72daf380 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-net@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-net@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/93189616053f9ceb0a96183ef18e281d604644d42947a6faabec92046386f7a8a0b31a25220c6138171808cd51d0b150c43117fc02bd4f456da8f10ef4e138e0 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-oracledb@npm:^0.29.0": + version: 0.29.0 + resolution: "@opentelemetry/instrumentation-oracledb@npm:0.29.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/oracledb": "npm:6.5.2" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/4a5ced2720061a6833b8210e0c606211393da98e0701087dc803c333e04a186550224408c7d82960c6cc07c96b1ab9c16b918cafdc9b77c27bc2f3781b6de6b1 + checksum: 10c0/8f6708067dc7e3ccb6b35e9552ccdf2e942f102560bfcd072a5a3bf80fdad3db9fc326abe4fd8b8b681e1558a0f59f8a3d8685de0a0a2b8aec198c471004db56 languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.56.0": +"@opentelemetry/instrumentation-pg@npm:^0.56.0": version: 0.56.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.56.0" - dependencies: - "@opentelemetry/api-logs": "npm:0.56.0" - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" - "@opentelemetry/sdk-logs": "npm:0.56.0" - "@opentelemetry/sdk-metrics": "npm:1.29.0" - "@opentelemetry/sdk-trace-base": "npm:1.29.0" - protobufjs: "npm:^7.3.0" + resolution: "@opentelemetry/instrumentation-pg@npm:0.56.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.34.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + "@types/pg": "npm:8.15.4" + "@types/pg-pool": "npm:2.0.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/52c9af5da33e6755ea6b938ce64fdad57a1d55ccc931f09a7b512f1a51e09ad363cdecafe9d822c6ff955d152132ad3c4e18241aa599f291d41e10b93c1723aa + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pino@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-pino@npm:0.50.0" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/06dc7e509c8fddbb25f6aaa1ac84324c6116a230abec3b6a340fd483f4778b840b4305de3238dbe8ad77446832e68967496a72947877e68c6d6c07e7447cd1dc + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-redis@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-redis@npm:0.51.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/redis-common": "npm:^0.38.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/1e2564fb5b407cb8015397c2ff2ab4560f8b7d3d8d9eba00e57ece04226a93cbfe8f029342009d73a438e51f486d3b91aaaaa51b0a5e2444ce6c1b690c1f5099 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-restify@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-restify@npm:0.49.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/25b30b061e6c47f27eebc48786ee656854c3d93a64a9dc70e07846a3e08580e0bfa4c7ef089ced59089ab1b002b3cbae8b2c751a8c9536bb9973d44e1b9cbd0f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-router@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-router@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/4373c80432bfc3a48b45603604c8d9f81f8e30f9f3d821a166d96a86de1e3186db1d96de4a85ba93d8fdc7ad6f8857481d3abe224228b2c5fac3bc16987c23d6 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-runtime-node@npm:^0.17.1": + version: 0.17.1 + resolution: "@opentelemetry/instrumentation-runtime-node@npm:0.17.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/96e7b1191a15d986f89f98323cf20016b9d07e298ca5173750bab2ca144734827c5a4b7e7791c08a82f2b6cb690b9d68766f0023cb92ca2a9b8834b0b1797a6d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-socket.io@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-socket.io@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e462419086b0bed49d3b6b8b3f25b43116e8aa752d13c3bfa1ed6a42cd1dfda41cd8835fae7b9a23e26494e612ca04d5d5d3172bc942b558d66da0c4bf1be95d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-tedious@npm:^0.22.0": + version: 0.22.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.22.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.203.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/tedious": "npm:^4.0.14" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/bbd6f78591ba918d274e6990a6b1c46d814e60a0da1cb213190f868f4fe8e06fcabcded1e1e674afd70e9f60f670c601a842fc4a7b82c634eb19313cd7d37106 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-undici@npm:^0.14.0": + version: 0.14.0 + resolution: "@opentelemetry/instrumentation-undici@npm:0.14.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.7.0 + checksum: 10c0/0689e4836b774405e9415a4186ffa342e41bb7df7f518c1872847c64b8895e49865b6fc00a8525479f862dc97ea57684b7c0c2809cec422c8d8d3b946707f0ed + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-winston@npm:^0.48.1": + version: 0.48.1 + resolution: "@opentelemetry/instrumentation-winston@npm:0.48.1" + dependencies: + "@opentelemetry/api-logs": "npm:^0.203.0" + "@opentelemetry/instrumentation": "npm:^0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/25392239f8155232d0dabfd8d182181ddc8b6d6bfeb0105e0ed2cc389aac4080f4dba1efdabb4b48a95ac64313fae13141e0f5dc3a3552907707a4ddb75c81a4 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation@npm:0.203.0, @opentelemetry/instrumentation@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/instrumentation@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + import-in-the-middle: "npm:^1.8.1" + require-in-the-middle: "npm:^7.1.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/b9de27ea7b42c54b1d0dab15dac62d4fc71c781bb6a48e90fa4ce8ce97be1b78e1fa9f05f58c39f68ca0e4a5590b8538d04209482f6b0632958926f7e80a28c1 + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.203.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/ad5b771b06b192f06f332f60701d1ad208df88a05975b16e1cdd1dff8e1cb66e775b3e9de513c2f5d48f390f25ca35411ead08ce4849c8203b86a264d34561d3 + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm%3A0.203.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch": + version: 0.203.0 + resolution: "@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm%3A0.203.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.203.0-183dcac0e6.patch::version=0.203.0&hash=301c6a" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-transformer": "npm:0.203.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/c90d352cdf5fa221d9ed185aace3d722a74629670a479153f0315f4824ac6efb9a85a2eb29c9eb6cda23cc912f2c483c3d5ecb100a23606019452836a26a6f88 + languageName: node + linkType: hard + +"@opentelemetry/otlp-grpc-exporter-base@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.203.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.203.0" + "@opentelemetry/otlp-transformer": "npm:0.203.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/898bae14a8f84a2f5a8d3a3b89966a5d3af1102b273dac748e404651ad113876b095faf1528055fce65de99dc33bc764cd650f21019ada0f85926f7606dc377e + checksum: 10c0/8e0936a7bc5359fe2f15d6302e7ba2cafe93e3dc95c0d316a95b78d8638822939612f07cb27c60544eefbabae2c8ed9b83d246695e92af30b0896a7c3888db32 languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/otlp-transformer@npm:0.57.2" +"@opentelemetry/otlp-transformer@npm:0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.203.0" dependencies: - "@opentelemetry/api-logs": "npm:0.57.2" - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/sdk-logs": "npm:0.57.2" - "@opentelemetry/sdk-metrics": "npm:1.30.1" - "@opentelemetry/sdk-trace-base": "npm:1.30.1" + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" protobufjs: "npm:^7.3.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/094979421768c5ac0672d1ce62bbc710a8cc836eb24e1cdfe5fb2c5c55908d19cf35fd6810cd266e7444d5677087846d5a8959df5886dfe1774199a3ae1d50a4 + checksum: 10c0/3f7b4bfe4bcab4db434ff2c4e59b53de53642d379b80056610456d8e9ae0cbab0f8b69f088078637b7b5ceffd0ac2fda68469c5f295b1c0ac625f522f640338c languageName: node linkType: hard -"@opentelemetry/resources@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/resources@npm:1.29.0" +"@opentelemetry/propagation-utils@npm:^0.31.3": + version: 0.31.3 + resolution: "@opentelemetry/propagation-utils@npm:0.31.3" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/7122b444883c0bb851860f3aaf9d7ca0ea1d2caa34e3d72ddfe0b3bd5d4063c787ede23eff4729c37145294373698ef68a13fa8881cd1c1bd35fb6d46a47753a + languageName: node + linkType: hard + +"@opentelemetry/propagator-b3@npm:2.0.1, @opentelemetry/propagator-b3@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/propagator-b3@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/10a91597b2ae92eeeeee9645c8147056b930739023bde4f18190317f7e8a05acd9e440b29d04be3580f7af4ffe5ff629d970264278f86574c429685f4804a006 + checksum: 10c0/79dbfaaa867f4f71a22ab640848f797ef9789fd94fc824ca4e38f298968a3f559a895fc228a17f09b1e06ec88cbf0b1f3cadc480ea76848504c7364693fd30ca languageName: node linkType: hard -"@opentelemetry/resources@patch:@opentelemetry/resources@npm:1.29.0#~/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch": - version: 1.29.0 - resolution: "@opentelemetry/resources@patch:@opentelemetry/resources@npm%3A1.29.0#~/.yarn/patches/@opentelemetry-resources-npm-1.29.0-112f89f0c5.patch::version=1.29.0&hash=7be975" +"@opentelemetry/propagator-jaeger@npm:2.0.1, @opentelemetry/propagator-jaeger@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/propagator-jaeger@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/d691090002ca32813eb99ba0748ea1d5ef1957283326a874ee298ad634eee2bb8bb4ec26fa72605d70a4aa972e35389d5ca6b12851d8bef492574fa08c6ce147 + checksum: 10c0/e21df109b831a7efffe54459bb5da35be05eeb72581017f0ce40dee2ab98b3e8063602894a477f6c593ad1bd3a1ead36adfceee21eb2472ca88050d49f056154 languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/sdk-logs@npm:0.56.0" +"@opentelemetry/redis-common@npm:^0.38.0": + version: 0.38.0 + resolution: "@opentelemetry/redis-common@npm:0.38.0" + checksum: 10c0/fb0553b6115bc395b4ae8dcfc5888a38bda0abaf8107533268c65f60be499b79a3fe25a8a793905e39f78b05e7273ee7c39d1e4a65eaca39523fd261143383b5 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.31.3": + version: 0.31.3 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.31.3" dependencies: - "@opentelemetry/api-logs": "npm:0.56.0" - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/abd5584c8d98a71bfefdca4b864f69714d0e638c5ad9fe4819744b938aea362c9602b884da1da24ae394bccbe044246463a208e775b3ac4718eadaff7fac295d + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/352fa3cf0799ae4a02313f0cae4e1ebc6d0ea3d0e644a943f7d738f535bf14a3b8396b2f94796db839286487c655e136e7735cb240b1e1516d1cbd9d6b4bc25f languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.57.2, @opentelemetry/sdk-logs@npm:^0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/sdk-logs@npm:0.57.2" +"@opentelemetry/resource-detector-aws@npm:^2.3.0": + version: 2.3.0 + resolution: "@opentelemetry/resource-detector-aws@npm:2.3.0" dependencies: - "@opentelemetry/api-logs": "npm:0.57.2" - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/dda61cf656a93d2f5ef1ca0495db59bfa33efc8ca7ee11018850a9ff78ee0459fb0393e70be7ae5d3cd084e0652d36fbf5778c7b3e9028c6668f9bf0d6c9473e + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/e1ccf47dc8af25b861f95bd1d10d138512779174d4d4edc87df32db32bb619c8dfd0bc17bfe559e4ee219d2f370517f2fa48cd9168aa4c8c11be8cc6cacc37a8 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-azure@npm:^0.10.0": + version: 0.10.0 + resolution: "@opentelemetry/resource-detector-azure@npm:0.10.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/5617dc6f5ab6476c57747a4882b05f2e086326e2ae6c86e2d06fc5e26250b1022e1c5b7968cb0bb11da98e28846220eadc9e42882e018e20c64291f278a65f9b + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-container@npm:^0.7.3": + version: 0.7.3 + resolution: "@opentelemetry/resource-detector-container@npm:0.7.3" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/c2e0298eb65505ce774fa5c2f7ac205b57058b4f537fada10845627f010e6714522b8903c76104bc3b863efab0cc80c28e6e5d18506bd1a8790af5dee4440a6f + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-gcp@npm:^0.37.0": + version: 0.37.0 + resolution: "@opentelemetry/resource-detector-gcp@npm:0.37.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + gcp-metadata: "npm:^6.0.0" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/83f96e1ed5122154bb7d4bc7cc9707c093e775cb317cf2f3c77745987f1ab6e9618f6f0c8735dbf06d317f24f8a19bd5f33b76219d015f07783c15a47e00b2c8 languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/sdk-metrics@npm:1.29.0" +"@opentelemetry/resources@npm:2.0.1, @opentelemetry/resources@npm:^2.0.0, @opentelemetry/resources@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/resources@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/4fca3b43fc9e9d139e87e18abf91069ba09c110bd4aa093e97ae02cd9af2cc82560f38dd4e3a6c76b054e1f8cbe5801faada2d24d7673a0ab15bf69923d6202c + checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 + languageName: node + linkType: hard + +"@opentelemetry/sampler-jaeger-remote@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/sampler-jaeger-remote@npm:0.203.0" + dependencies: + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/9424b451b620d810992615c9a05d1b06c7d738966c1c99d7bafa7ef67e336d31d0cf99a04de82b3bd284bfc875c6475149cbe41b212bceca8a1934ccdf27c956 languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:1.30.1, @opentelemetry/sdk-metrics@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/sdk-metrics@npm:1.30.1" +"@opentelemetry/sdk-logs@npm:0.203.0, @opentelemetry/sdk-logs@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/sdk-logs@npm:0.203.0" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.4.0 <1.10.0" + checksum: 10c0/02dd9d9969628f05f71ae1d149f1aa6d1fee2dad607923a68a1cfc923e94b046dcc0e18e85e865324e3bda0cee7a5a0ba9fa0d57e4e95fa672be103e2ce60270 + languageName: node + linkType: hard + +"@opentelemetry/sdk-metrics@npm:2.0.1, @opentelemetry/sdk-metrics@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-metrics@npm:2.0.1" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.9.0 <1.10.0" + checksum: 10c0/fcf7ae23d459e5da7cb6fe150064b6dc4e11e47925b08980c3b357bd5534ad388898bbacd0ff8befef6801f43b35142dc7123f028ffde2d0fe2bd72177d07639 + languageName: node + linkType: hard + +"@opentelemetry/sdk-node@npm:^0.203.0": + version: 0.203.0 + resolution: "@opentelemetry/sdk-node@npm:0.203.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.203.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-logs-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-logs-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-logs-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-metrics-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-prometheus": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:0.203.0" + "@opentelemetry/exporter-trace-otlp-proto": "npm:0.203.0" + "@opentelemetry/exporter-zipkin": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.203.0" + "@opentelemetry/propagator-b3": "npm:2.0.1" + "@opentelemetry/propagator-jaeger": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.203.0" + "@opentelemetry/sdk-metrics": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + "@opentelemetry/sdk-trace-node": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/7e60178e61eaf49db5d74f6c3701706762d71d370044253c72bb5668dba3a435030ed6847605ee55d0e1b8908ad123a2517b5f00415a2fb3d98468a0a318e3c0 + checksum: 10c0/2c846f40908afad73d7898516c37e35ab34a63c2147f0241d39c7442207f42de7bd6d451afbccf7a102022a5132465b98ffcdd1fbc77050b49909c501b42cbd8 languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/sdk-trace-base@npm:1.29.0" +"@opentelemetry/sdk-trace-base@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-base@npm:2.0.1" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/870f29d3d72f4d6cbcaada328b544aa111527d72f0818f89bc5661b0427a37618db939cc65e834c8c73bad744665f9ac6dc0ec48276b113b5d4a0913c2b8fece + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b languageName: node linkType: hard -"@opentelemetry/sdk-trace-base@npm:1.30.1, @opentelemetry/sdk-trace-base@npm:^1.29.0, @opentelemetry/sdk-trace-base@npm:^1.30.1": - version: 1.30.1 - resolution: "@opentelemetry/sdk-trace-base@npm:1.30.1" +"@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@npm%3A2.0.1#~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch::version=2.0.1&hash=212481": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@npm%3A2.0.1#~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch::version=2.0.1&hash=212481" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/77019dc3efaeceb41b4c54dd83b92f0ccd81ecceca544cbbe8e0aee4b2c8727724bdb9dcecfe00622c16d60946ae4beb69a5c0e7d85c4bc7ef425bd84f8b970c + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/1eeddf77153d7f374e57c1033e0971a7aa27e489e17d5679368ccd39b71eb4c467516859e93b627f9bbe3b7af839db44c4de727d1c17bffd3775dd288e64974d languageName: node linkType: hard -"@opentelemetry/sdk-trace-web@npm:^1.29.0": - version: 1.30.1 - resolution: "@opentelemetry/sdk-trace-web@npm:1.30.1" +"@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-base@patch:@opentelemetry/sdk-trace-base@patch%3A@opentelemetry/sdk-trace-base@npm%253A2.0.1%23~/.yarn/patches/@opentelemetry-sdk-trace-base-npm-2.0.1-ebe4f8e34e.patch%3A%3Aversion=2.0.1&hash=212481#~/.yarn/patches/@opentelemetry-sdk-trace-base-patch-0b7dbf6a30.patch::version=2.0.1&hash=9e89a6" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/sdk-trace-base": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/8dd2901b5eef68a5896da0ad11f04c8990ce4ef2dcbec27bbc02d7e193097c270ba5f4c9ca363ea10fb53ca7cc515f18d9dc383a69a17720cd0590474c0ffdaf + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/8dd7c5266da16f3eebe37a2d158c17394b8abfb078b7276600e158d2af280778b77eadc980b91a8ac8b5a3d30e8ebe3bf07308c8e8a859254e0a2c9d61d7d2cf languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:1.28.0": - version: 1.28.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.28.0" - checksum: 10c0/deb8a0f744198071e70fea27143cf7c9f7ecb7e4d7b619488c917834ea09b31543c1c2bcea4ec5f3cf68797f0ef3549609c14e859013d9376400ac1499c2b9cb +"@opentelemetry/sdk-trace-node@npm:2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/sdk-trace-node@npm:2.0.1" + dependencies: + "@opentelemetry/context-async-hooks": "npm:2.0.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/b237efc219dc10c33746c05461c8c8741edbe7558eaf7f2dab01a3e75af4788bfd0633a049cd5dc7ecf015a2de7aa948c3989c0131d1f140109fb5e7b0313d7a languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:^1.28.0, @opentelemetry/semantic-conventions@npm:^1.30.0": +"@opentelemetry/semantic-conventions@npm:^1.27.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.33.1, @opentelemetry/semantic-conventions@npm:^1.34.0": version: 1.34.0 resolution: "@opentelemetry/semantic-conventions@npm:1.34.0" checksum: 10c0/a51a32a5cf5c803bd2125a680d0abacbff632f3b255d0fe52379dac191114a0e8d72a34f9c46c5483ccfe91c4061c309f3cf61a19d11347e2a69779e82cfefd0 languageName: node linkType: hard +"@opentelemetry/semantic-conventions@npm:^1.29.0": + version: 1.30.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.30.0" + checksum: 10c0/0bf99552e3b4b7e8b7eb504b678d52f59c6f259df88e740a2011a0d858e523d36fee86047ae1b7f45849c77f00f970c3059ba58e0a06a7d47d6f01dbe8c455bd + languageName: node + linkType: hard + +"@opentelemetry/semantic-conventions@npm:^1.36.0": + version: 1.36.0 + resolution: "@opentelemetry/semantic-conventions@npm:1.36.0" + checksum: 10c0/edc8a6fe3ec4fc0c67ba3a92b86fb3dcc78fe1eb4f19838d8013c3232b9868540a034dd25cfe0afdd5eae752c5f0e9f42272ff46da144a2d5b35c644478e1c62 + languageName: node + linkType: hard + +"@opentelemetry/sql-common@npm:^0.41.0": + version: 0.41.0 + resolution: "@opentelemetry/sql-common@npm:0.41.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + peerDependencies: + "@opentelemetry/api": ^1.1.0 + checksum: 10c0/3611fa2766be809d3f19f1c032373349962c3203cbc77839ea59d78a4ce2bf6954da9b163efdde386f3f4a8ec77c50c985f9e5b8b3df6e7fccd2990cf861bca4 + languageName: node + linkType: hard + "@oven/bun-darwin-aarch64@npm:1.2.18": version: 1.2.18 resolution: "@oven/bun-darwin-aarch64@npm:1.2.18" @@ -7612,9 +8375,9 @@ __metadata: languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:^15.2.3": - version: 15.3.1 - resolution: "@rollup/plugin-node-resolve@npm:15.3.1" +"@rollup/plugin-node-resolve@npm:16.0.1": + version: 16.0.1 + resolution: "@rollup/plugin-node-resolve@npm:16.0.1" dependencies: "@rollup/pluginutils": "npm:^5.0.1" "@types/resolve": "npm:1.20.2" @@ -7626,13 +8389,13 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10c0/ecf3abe890fc98ad665fdbfb1ea245253e0d1f2bc6d9f4e8f496f212c76a2ce7cd4b9bc0abd21e6bcaa16f72d1c67cc6b322ea12a6ec68e8a8834df8242a5ecd + checksum: 10c0/54d33282321492fafec29b49c66dd1efd90c72a24f9d1569dcb57a72ab8de8a782810f39fdb917b96ec6a598c18f3416588b419bf7af331793a010de1fe28c60 languageName: node linkType: hard -"@rollup/plugin-node-resolve@npm:^16.0.0": +"@rollup/plugin-node-resolve@patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch": version: 16.0.1 - resolution: "@rollup/plugin-node-resolve@npm:16.0.1" + resolution: "@rollup/plugin-node-resolve@patch:@rollup/plugin-node-resolve@npm%3A16.0.1#~/.yarn/patches/@rollup-plugin-node-resolve-npm-16.0.1-2936474bab.patch::version=16.0.1&hash=c5de40" dependencies: "@rollup/pluginutils": "npm:^5.0.1" "@types/resolve": "npm:1.20.2" @@ -7644,7 +8407,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10c0/54d33282321492fafec29b49c66dd1efd90c72a24f9d1569dcb57a72ab8de8a782810f39fdb917b96ec6a598c18f3416588b419bf7af331793a010de1fe28c60 + checksum: 10c0/1baa7e86d03bdd55e2cc0109b103436a962262b71fc313181b1b0fd4a9318f3aeb77e329062f3353bc0214f9ca51d1bac6e45b5678041bd1279ee482a051fd21 languageName: node linkType: hard @@ -7694,9 +8457,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.37.0" +"@rollup/rollup-android-arm-eabi@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-android-arm-eabi@npm:4.41.1" conditions: os=android & cpu=arm languageName: node linkType: hard @@ -7708,9 +8471,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-android-arm64@npm:4.37.0" +"@rollup/rollup-android-arm64@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-android-arm64@npm:4.41.1" conditions: os=android & cpu=arm64 languageName: node linkType: hard @@ -7722,9 +8485,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.37.0" +"@rollup/rollup-darwin-arm64@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-darwin-arm64@npm:4.41.1" conditions: os=darwin & cpu=arm64 languageName: node linkType: hard @@ -7736,9 +8499,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.37.0" +"@rollup/rollup-darwin-x64@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-darwin-x64@npm:4.41.1" conditions: os=darwin & cpu=x64 languageName: node linkType: hard @@ -7750,9 +8513,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.37.0" +"@rollup/rollup-freebsd-arm64@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-freebsd-arm64@npm:4.41.1" conditions: os=freebsd & cpu=arm64 languageName: node linkType: hard @@ -7764,9 +8527,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.37.0" +"@rollup/rollup-freebsd-x64@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-freebsd-x64@npm:4.41.1" conditions: os=freebsd & cpu=x64 languageName: node linkType: hard @@ -7778,9 +8541,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.37.0" +"@rollup/rollup-linux-arm-gnueabihf@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.41.1" conditions: os=linux & cpu=arm & libc=glibc languageName: node linkType: hard @@ -7792,9 +8555,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.37.0" +"@rollup/rollup-linux-arm-musleabihf@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.41.1" conditions: os=linux & cpu=arm & libc=musl languageName: node linkType: hard @@ -7806,9 +8569,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.37.0" +"@rollup/rollup-linux-arm64-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.41.1" conditions: os=linux & cpu=arm64 & libc=glibc languageName: node linkType: hard @@ -7820,9 +8583,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.37.0" +"@rollup/rollup-linux-arm64-musl@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-arm64-musl@npm:4.41.1" conditions: os=linux & cpu=arm64 & libc=musl languageName: node linkType: hard @@ -7834,9 +8597,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.37.0" +"@rollup/rollup-linux-loongarch64-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.41.1" conditions: os=linux & cpu=loong64 & libc=glibc languageName: node linkType: hard @@ -7848,9 +8611,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.37.0" +"@rollup/rollup-linux-powerpc64le-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.41.1" conditions: os=linux & cpu=ppc64 & libc=glibc languageName: node linkType: hard @@ -7862,9 +8625,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.37.0" +"@rollup/rollup-linux-riscv64-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.41.1" conditions: os=linux & cpu=riscv64 & libc=glibc languageName: node linkType: hard @@ -7876,9 +8639,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.37.0" +"@rollup/rollup-linux-riscv64-musl@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.41.1" conditions: os=linux & cpu=riscv64 & libc=musl languageName: node linkType: hard @@ -7890,9 +8653,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.37.0" +"@rollup/rollup-linux-s390x-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.41.1" conditions: os=linux & cpu=s390x & libc=glibc languageName: node linkType: hard @@ -7904,9 +8667,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.37.0" +"@rollup/rollup-linux-x64-gnu@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-x64-gnu@npm:4.41.1" conditions: os=linux & cpu=x64 & libc=glibc languageName: node linkType: hard @@ -7918,9 +8681,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.37.0" +"@rollup/rollup-linux-x64-musl@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-linux-x64-musl@npm:4.41.1" conditions: os=linux & cpu=x64 & libc=musl languageName: node linkType: hard @@ -7932,9 +8695,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.37.0" +"@rollup/rollup-win32-arm64-msvc@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.41.1" conditions: os=win32 & cpu=arm64 languageName: node linkType: hard @@ -7946,9 +8709,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.37.0" +"@rollup/rollup-win32-ia32-msvc@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.41.1" conditions: os=win32 & cpu=ia32 languageName: node linkType: hard @@ -7960,9 +8723,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.37.0": - version: 4.37.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.37.0" +"@rollup/rollup-win32-x64-msvc@npm:4.41.1": + version: 4.41.1 + resolution: "@rollup/rollup-win32-x64-msvc@npm:4.41.1" conditions: os=win32 & cpu=x64 languageName: node linkType: hard @@ -8738,6 +9501,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:8.10.150": + version: 8.10.150 + resolution: "@types/aws-lambda@npm:8.10.150" + checksum: 10c0/7f2719a7e114461e03fdde82aa4c64a9a7b18adc8fd4b57cd9d929c1c77668f9a09b09c54941e9a0bee1aa6c9434db4baf84ba257d332e863e3cd0c427ae2aee + languageName: node + linkType: hard + "@types/aws4@npm:^1.11.6": version: 1.11.6 resolution: "@types/aws4@npm:1.11.6" @@ -8807,6 +9577,15 @@ __metadata: languageName: node linkType: hard +"@types/bunyan@npm:1.8.11": + version: 1.8.11 + resolution: "@types/bunyan@npm:1.8.11" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/07d762499307a1c3f04f56f2c62417b909f86f6090cee29b73a00dde323a4463cfd2e78888598cb1cd3b1eb88e6c47ef2a58e17f119dae27ff04cd361c0a1d4c + languageName: node + linkType: hard + "@types/cacheable-request@npm:^6.0.1": version: 6.0.3 resolution: "@types/cacheable-request@npm:6.0.3" @@ -8828,7 +9607,7 @@ __metadata: languageName: node linkType: hard -"@types/connect@npm:*": +"@types/connect@npm:*, @types/connect@npm:3.4.38": version: 3.4.38 resolution: "@types/connect@npm:3.4.38" dependencies: @@ -8895,10 +9674,10 @@ __metadata: languageName: node linkType: hard -"@types/estree@npm:1.0.6": - version: 1.0.6 - resolution: "@types/estree@npm:1.0.6" - checksum: 10c0/cdfd751f6f9065442cd40957c07fd80361c962869aa853c1c2fd03e101af8b9389d8ff4955a43a6fcfa223dd387a089937f95be0f3eec21ca527039fd2d9859a +"@types/estree@npm:1.0.7": + version: 1.0.7 + resolution: "@types/estree@npm:1.0.7" + checksum: 10c0/be815254316882f7c40847336cd484c3bc1c3e34f710d197160d455dc9d6d050ffbf4c3bc76585dba86f737f020ab20bdb137ebe0e9116b0c86c7c0342221b8c languageName: node linkType: hard @@ -9075,6 +9854,15 @@ __metadata: languageName: node linkType: hard +"@types/memcached@npm:^2.2.6": + version: 2.2.10 + resolution: "@types/memcached@npm:2.2.10" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/0c5214a73c9abb3d1bbf91d2890d38476961ae8aa387f71235519be65a537c654ca0380a468cf3ab49d3b9409c441580d081f16f14ed6aea3339144aee0f16fb + languageName: node + linkType: hard + "@types/methods@npm:^1.1.4": version: 1.1.4 resolution: "@types/methods@npm:1.1.4" @@ -9096,6 +9884,15 @@ __metadata: languageName: node linkType: hard +"@types/mysql@npm:2.15.27": + version: 2.15.27 + resolution: "@types/mysql@npm:2.15.27" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d34064de1697e9e29dbad313df759a8c7aff9d9d1918c9b666b1ebc894b9a0c1c6f4ae779453fdcd20b892fa60a8e55640138c292c6c2a28d2f758eaeb539ce3 + languageName: node + linkType: hard + "@types/node-fetch@npm:^2.6.1, @types/node-fetch@npm:^2.6.2": version: 2.6.12 resolution: "@types/node-fetch@npm:2.6.12" @@ -9140,6 +9937,35 @@ __metadata: languageName: node linkType: hard +"@types/oracledb@npm:6.5.2": + version: 6.5.2 + resolution: "@types/oracledb@npm:6.5.2" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/16e6d2e4247222dddf7be01273946b7f6a686327ce440be861671a2a0b98fe1a0d42df849d039a3f58aa1014f1c9d803f3c9793531a476077d762423ac911e65 + languageName: node + linkType: hard + +"@types/pg-pool@npm:2.0.6": + version: 2.0.6 + resolution: "@types/pg-pool@npm:2.0.6" + dependencies: + "@types/pg": "npm:*" + checksum: 10c0/41965d4d0b677c54ce45d36add760e496d356b78019cb062d124af40287cf6b0fd4d86e3b0085f443856c185983a60c8b0795ff76d15683e2a93c62f5ac0125f + languageName: node + linkType: hard + +"@types/pg@npm:*, @types/pg@npm:8.15.4": + version: 8.15.4 + resolution: "@types/pg@npm:8.15.4" + dependencies: + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^2.2.0" + checksum: 10c0/7f9295cb2d934681bba84f7caad529c3b100d87e83ad0732c7fe496f4f79e42a795097321db54e010fcff22cb5e410cf683b4c9941907ee4564c822242816e91 + languageName: node + linkType: hard + "@types/qs@npm:*": version: 6.9.18 resolution: "@types/qs@npm:6.9.18" @@ -9147,6 +9973,13 @@ __metadata: languageName: node linkType: hard +"@types/quick-format-unescaped@npm:^4.0.3": + version: 4.0.3 + resolution: "@types/quick-format-unescaped@npm:4.0.3" + checksum: 10c0/e95ba1dfa68f9d1dee785905c3c648b9fe1514c5261160e566d9a19731013d138269937632dea79a73859988a90621c9653504170c6a7415cacb426343f2a11a + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7" @@ -9154,6 +9987,24 @@ __metadata: languageName: node linkType: hard +"@types/react-dom@npm:^19": + version: 19.1.6 + resolution: "@types/react-dom@npm:19.1.6" + peerDependencies: + "@types/react": ^19.0.0 + checksum: 10c0/7ba74eee2919e3f225e898b65fdaa16e54952aaf9e3472a080ddc82ca54585e46e60b3c52018d21d4b7053f09d27b8293e9f468b85f9932ff452cd290cc131e8 + languageName: node + linkType: hard + +"@types/react@npm:^19": + version: 19.1.8 + resolution: "@types/react@npm:19.1.8" + dependencies: + csstype: "npm:^3.0.2" + checksum: 10c0/4908772be6dc941df276931efeb0e781777fa76e4d5d12ff9f75eb2dcc2db3065e0100efde16fde562c5bafa310cc8f50c1ee40a22640459e066e72cd342143e + languageName: node + linkType: hard + "@types/resolve@npm:1.20.2": version: 1.20.2 resolution: "@types/resolve@npm:1.20.2" @@ -9198,13 +10049,6 @@ __metadata: languageName: node linkType: hard -"@types/shimmer@npm:^1.2.0": - version: 1.2.0 - resolution: "@types/shimmer@npm:1.2.0" - checksum: 10c0/6f7bfe1b55601cfc3ae713fc74a03341f3834253b8b91cb2add926d5949e4a63f7e666f59c2a6e40a883a5f9e2f3e3af10f9d3aed9b60fced0bda87659e58d8d - languageName: node - linkType: hard - "@types/ssh2@npm:*": version: 1.15.5 resolution: "@types/ssh2@npm:1.15.5" @@ -9243,6 +10087,15 @@ __metadata: languageName: node linkType: hard +"@types/tedious@npm:^4.0.14": + version: 4.0.14 + resolution: "@types/tedious@npm:4.0.14" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/d2914f8e9b5b998e4275ec5f0130cba1c2fb47e75616b5c125a65ef6c1db2f1dc3f978c7900693856a15d72bbb4f4e94f805537a4ecb6dc126c64415d31c0590 + languageName: node + linkType: hard + "@types/treeify@npm:^1.0.0": version: 1.0.3 resolution: "@types/treeify@npm:1.0.3" @@ -9442,17 +10295,6 @@ __metadata: languageName: node linkType: hard -"@typespec/ts-http-runtime@npm:^0.2.2": - version: 0.2.2 - resolution: "@typespec/ts-http-runtime@npm:0.2.2" - dependencies: - http-proxy-agent: "npm:^7.0.0" - https-proxy-agent: "npm:^7.0.0" - tslib: "npm:^2.6.2" - checksum: 10c0/0f7d633f9885bd7fb065b205887eb6a85639c37ef2d4b50a1b55cee3ef7ad270dcf4757db0882a39157624e8888c6f1f29aaf4c163403c91e75a4b646d362c49 - languageName: node - linkType: hard - "@upstash/redis@npm:^1.34.3": version: 1.34.9 resolution: "@upstash/redis@npm:1.34.9" @@ -9616,6 +10458,15 @@ __metadata: languageName: node linkType: hard +"@whatwg-node/promise-helpers@npm:1.3.0": + version: 1.3.0 + resolution: "@whatwg-node/promise-helpers@npm:1.3.0" + dependencies: + tslib: "npm:^2.6.3" + checksum: 10c0/79b3d4fd264a7ce82fe977690191bade5c6da50f085c63bf1b548c2066c7bb9ccb9088ad064fbb0ff8da9bea7b336a527973556d8475642bac639e75bedec7ea + languageName: node + linkType: hard + "@whatwg-node/promise-helpers@npm:^1.0.0, @whatwg-node/promise-helpers@npm:^1.2.0, @whatwg-node/promise-helpers@npm:^1.2.1, @whatwg-node/promise-helpers@npm:^1.2.4, @whatwg-node/promise-helpers@npm:^1.2.5, @whatwg-node/promise-helpers@npm:^1.3.0, @whatwg-node/promise-helpers@npm:^1.3.1, @whatwg-node/promise-helpers@npm:^1.3.2": version: 1.3.2 resolution: "@whatwg-node/promise-helpers@npm:1.3.2" @@ -9636,7 +10487,20 @@ __metadata: languageName: node linkType: hard -"@whatwg-node/server@npm:^0.10.0, @whatwg-node/server@npm:^0.10.11, @whatwg-node/server@npm:^0.10.5": +"@whatwg-node/server@npm:^0.10.0, @whatwg-node/server@npm:^0.10.5": + version: 0.10.9 + resolution: "@whatwg-node/server@npm:0.10.9" + dependencies: + "@envelop/instrumentation": "npm:^1.0.0" + "@whatwg-node/disposablestack": "npm:^0.0.6" + "@whatwg-node/fetch": "npm:^0.10.8" + "@whatwg-node/promise-helpers": "npm:^1.3.2" + tslib: "npm:^2.6.3" + checksum: 10c0/aac2a59d7b96904d5b78b888d9bea706b7b38ba8ed30b62e8ffb8dea65454c0baaf3f8a52c768fbfe3b8a2bab803e136bbf0248f31308ee195e0f4aba2f5a2e7 + languageName: node + linkType: hard + +"@whatwg-node/server@npm:^0.10.11": version: 0.10.11 resolution: "@whatwg-node/server@npm:0.10.11" dependencies: @@ -10431,6 +11295,20 @@ __metadata: languageName: node linkType: hard +"ansi-color@npm:0.2.1": + version: 0.2.1 + resolution: "ansi-color@npm:0.2.1" + checksum: 10c0/0ccfb57aadd3a955d1bbdc8392fb24e7e13b247812955b0182a3bea3f733d757e49f0b8faa00a6d2c619613f2d2a3136096f91e1fb4d688c39cf24b4f776a0d3 + languageName: node + linkType: hard + +"ansi-color@patch:ansi-color@npm%3A0.2.1#~/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch": + version: 0.2.1 + resolution: "ansi-color@patch:ansi-color@npm%3A0.2.1#~/.yarn/patches/ansi-color-npm-0.2.1-f7243d10a4.patch::version=0.2.1&hash=de9957" + checksum: 10c0/b23a96f3c448e8dc1db58d4a5c2f21608a39d3a886784642395496828ece114d54e5690320ef89b8faa0d9dc824c7dbd9f40beec958336e2aab6709c1c984388 + languageName: node + linkType: hard + "ansi-colors@npm:^4.1.1, ansi-colors@npm:^4.1.3": version: 4.1.3 resolution: "ansi-colors@npm:4.1.3" @@ -11028,6 +11906,13 @@ __metadata: languageName: node linkType: hard +"bignumber.js@npm:^9.0.0": + version: 9.3.0 + resolution: "bignumber.js@npm:9.3.0" + checksum: 10c0/f54a79cd6fc98552ac0510c1cd9381650870ae443bdb20ba9b98e3548188d941506ac3c22a9f9c69b2cc60da9be5700e87d3f54d2825310a8b2ae999dfd6d99d + languageName: node + linkType: hard + "bintrees@npm:1.0.2": version: 1.0.2 resolution: "bintrees@npm:1.0.2" @@ -11187,6 +12072,18 @@ __metadata: languageName: node linkType: hard +"bufrw@npm:^1.3.0": + version: 1.4.0 + resolution: "bufrw@npm:1.4.0" + dependencies: + ansi-color: "npm:^0.2.1" + error: "npm:^7.0.0" + hexer: "npm:^1.5.0" + xtend: "npm:^4.0.0" + checksum: 10c0/f4bfd7e13796f1f562753661be8b72c7eaac38e77b06edd5f238f8567d3d973c7f2686b1098bd5914fb88dc7141252d8361d0507e818af321fab13ef93ad3239 + languageName: node + linkType: hard + "buildcheck@npm:~0.0.6": version: 0.0.6 resolution: "buildcheck@npm:0.0.6" @@ -12097,6 +12994,13 @@ __metadata: languageName: node linkType: hard +"csstype@npm:^3.0.2": + version: 3.1.3 + resolution: "csstype@npm:3.1.3" + checksum: 10c0/80c089d6f7e0c5b2bd83cf0539ab41474198579584fa10d86d0cafe0642202343cbc119e076a0b1aece191989477081415d66c9fefbf3c957fc2fc4b7009f248 + languageName: node + linkType: hard + "data-uri-to-buffer@npm:^6.0.2": version: 6.0.2 resolution: "data-uri-to-buffer@npm:6.0.2" @@ -12174,27 +13078,27 @@ __metadata: languageName: node linkType: hard -"debug@npm:4, debug@npm:4.4.0, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.4.0": - version: 4.4.0 - resolution: "debug@npm:4.4.0" +"debug@npm:4, debug@npm:4.4.1, debug@npm:^4.1.0, debug@npm:^4.1.1, debug@npm:^4.3.1, debug@npm:^4.3.2, debug@npm:^4.3.3, debug@npm:^4.3.4, debug@npm:^4.3.5, debug@npm:^4.3.6, debug@npm:^4.3.7, debug@npm:^4.4.0, debug@npm:^4.4.1": + version: 4.4.1 + resolution: "debug@npm:4.4.1" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de + checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 languageName: node linkType: hard -"debug@npm:4.4.1, debug@npm:^4.3.7, debug@npm:^4.4.1": - version: 4.4.1 - resolution: "debug@npm:4.4.1" +"debug@npm:4.4.0": + version: 4.4.0 + resolution: "debug@npm:4.4.0" dependencies: ms: "npm:^2.1.3" peerDependenciesMeta: supports-color: optional: true - checksum: 10c0/d2b44bc1afd912b49bb7ebb0d50a860dc93a4dd7d946e8de94abc957bb63726b7dd5aa48c18c2386c379ec024c46692e15ed3ed97d481729f929201e671fcd55 + checksum: 10c0/db94f1a182bf886f57b4755f85b3a74c39b5114b9377b7ab375dc2cfa3454f09490cc6c30f829df3fc8042bc8b8995f6567ce5cd96f3bc3688bd24027197d9de languageName: node linkType: hard @@ -12746,6 +13650,25 @@ __metadata: languageName: node linkType: hard +"error@npm:7.0.2": + version: 7.0.2 + resolution: "error@npm:7.0.2" + dependencies: + string-template: "npm:~0.2.1" + xtend: "npm:~4.0.0" + checksum: 10c0/abee95f258f34490278bfb5f5852420e23f9d7dd7754215144391a731c2e7f68ccb5367497ca7cc20459d1eb7ae5d119d6c82f620a9340150034ddd2e3603178 + languageName: node + linkType: hard + +"error@npm:^7.0.0": + version: 7.2.1 + resolution: "error@npm:7.2.1" + dependencies: + string-template: "npm:~0.2.1" + checksum: 10c0/91ce301017292eab20b59e27a0bc322a8f45fcf48d992761530d20c5f9c5699a2ae1822fc94298d4815fd35c2595e89139a7c6fdd3bbe9e93871e3b412186567 + languageName: node + linkType: hard + "es-abstract@npm:^1.23.2, es-abstract@npm:^1.23.5, es-abstract@npm:^1.23.9, es-abstract@npm:^1.24.0": version: 1.24.0 resolution: "es-abstract@npm:1.24.0" @@ -13454,6 +14377,13 @@ __metadata: languageName: node linkType: hard +"extend@npm:^3.0.2": + version: 3.0.2 + resolution: "extend@npm:3.0.2" + checksum: 10c0/73bf6e27406e80aa3e85b0d1c4fd987261e628064e170ca781125c0b635a3dabad5e05adbf07595ea0cf1e6c5396cacb214af933da7cbaf24fe75ff14818e8f9 + languageName: node + linkType: hard + "extendable-error@npm:^0.1.5": version: 0.1.7 resolution: "extendable-error@npm:0.1.7" @@ -13498,13 +14428,6 @@ __metadata: languageName: node linkType: hard -"faker@npm:5.5.3": - version: 5.5.3 - resolution: "faker@npm:5.5.3" - checksum: 10c0/55ee2fb6425df717253f237b4e952c94efe33da23a5826ca41c6ecb31ccfb49d06c5de64b82b6994ca9c76c05eab4dbf779cea047455fff6e62cc9d585c7d460 - languageName: node - linkType: hard - "fast-copy@npm:^3.0.2": version: 3.0.2 resolution: "fast-copy@npm:3.0.2" @@ -13983,6 +14906,13 @@ __metadata: languageName: node linkType: hard +"forwarded-parse@npm:2.1.2": + version: 2.1.2 + resolution: "forwarded-parse@npm:2.1.2" + checksum: 10c0/0c6b4c631775f272b4475e935108635495e8a5b261d1b4a5caef31c47c5a0b04134adc564e655aadfef366a02647fa3ae90a1d3ac19929f3ade47f9bed53036a + languageName: node + linkType: hard + "forwarded@npm:0.2.0": version: 0.2.0 resolution: "forwarded@npm:0.2.0" @@ -14132,6 +15062,7 @@ __metadata: "@babel/plugin-proposal-explicit-resource-management": "npm:7.27.4" "@babel/plugin-transform-class-properties": "npm:7.27.1" "@babel/plugin-transform-class-static-block": "npm:7.27.1" + "@babel/plugin-transform-private-methods": "npm:^7.27.1" "@babel/preset-env": "npm:7.28.0" "@babel/preset-typescript": "npm:7.27.1" "@changesets/changelog-github": "npm:^0.5.0" @@ -14162,6 +15093,30 @@ __metadata: languageName: unknown linkType: soft +"gaxios@npm:^6.1.1": + version: 6.7.1 + resolution: "gaxios@npm:6.7.1" + dependencies: + extend: "npm:^3.0.2" + https-proxy-agent: "npm:^7.0.1" + is-stream: "npm:^2.0.0" + node-fetch: "npm:^2.6.9" + uuid: "npm:^9.0.1" + checksum: 10c0/53e92088470661c5bc493a1de29d05aff58b1f0009ec5e7903f730f892c3642a93e264e61904383741ccbab1ce6e519f12a985bba91e13527678b32ee6d7d3fd + languageName: node + linkType: hard + +"gcp-metadata@npm:^6.0.0": + version: 6.1.1 + resolution: "gcp-metadata@npm:6.1.1" + dependencies: + gaxios: "npm:^6.1.1" + google-logging-utils: "npm:^0.0.2" + json-bigint: "npm:^1.0.0" + checksum: 10c0/71f6ad4800aa622c246ceec3955014c0c78cdcfe025971f9558b9379f4019f5e65772763428ee8c3244fa81b8631977316eaa71a823493f82e5c44d7259ffac8 + languageName: node + linkType: hard + "generate-function@npm:^2.3.1": version: 2.3.1 resolution: "generate-function@npm:2.3.1" @@ -14394,6 +15349,13 @@ __metadata: languageName: node linkType: hard +"globals@npm:^11.1.0": + version: 11.12.0 + resolution: "globals@npm:11.12.0" + checksum: 10c0/758f9f258e7b19226bd8d4af5d3b0dcf7038780fb23d82e6f98932c44e239f884847f1766e8fa9cc5635ccb3204f7fa7314d4408dd4002a5e8ea827b4018f0a1 + languageName: node + linkType: hard + "globals@npm:^14.0.0": version: 14.0.0 resolution: "globals@npm:14.0.0" @@ -14446,6 +15408,13 @@ __metadata: languageName: node linkType: hard +"google-logging-utils@npm:^0.0.2": + version: 0.0.2 + resolution: "google-logging-utils@npm:0.0.2" + checksum: 10c0/9a4bbd470dd101c77405e450fffca8592d1d7114f245a121288d04a957aca08c9dea2dd1a871effe71e41540d1bb0494731a0b0f6fea4358e77f06645e4268c1 + languageName: node + linkType: hard + "gopd@npm:^1.0.1, gopd@npm:^1.2.0": version: 1.2.0 resolution: "gopd@npm:1.2.0" @@ -14758,6 +15727,20 @@ __metadata: languageName: node linkType: hard +"hexer@npm:^1.5.0": + version: 1.5.0 + resolution: "hexer@npm:1.5.0" + dependencies: + ansi-color: "npm:^0.2.1" + minimist: "npm:^1.1.0" + process: "npm:^0.10.0" + xtend: "npm:^4.0.0" + bin: + hexer: ./cli.js + checksum: 10c0/43b00fad220a98ed98dad3a3d0e7297bd3d0ce66f0d935a2927e07c0d29f5b8de92f9c0fa32c641daa4291ccb19385fa1bd0853d298983ec9b3ec88e7686ee5c + languageName: node + linkType: hard + "hotscript@npm:^1.0.11": version: 1.0.13 resolution: "hotscript@npm:1.0.13" @@ -14857,7 +15840,7 @@ __metadata: languageName: node linkType: hard -"https-proxy-agent@npm:^7.0.0, https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": +"https-proxy-agent@npm:^7.0.1, https-proxy-agent@npm:^7.0.6": version: 7.0.6 resolution: "https-proxy-agent@npm:7.0.6" dependencies: @@ -15682,6 +16665,19 @@ __metadata: languageName: node linkType: hard +"jaeger-client@npm:^3.15.0": + version: 3.19.0 + resolution: "jaeger-client@npm:3.19.0" + dependencies: + node-int64: "npm:^0.4.0" + opentracing: "npm:^0.14.4" + thriftrw: "npm:^3.5.0" + uuid: "npm:^8.3.2" + xorshift: "npm:^1.1.1" + checksum: 10c0/04f5683461212de49e4d5b6ca6b214276a797e361fba852231bc5e7fdcee76b053a6e618e5490106d7f7254917d928dda0880f7cd71e35dece7e09bbdcdd0927 + languageName: node + linkType: hard + "jake@npm:^10.8.5": version: 10.9.2 resolution: "jake@npm:10.9.2" @@ -16290,6 +17286,15 @@ __metadata: languageName: node linkType: hard +"json-bigint@npm:^1.0.0": + version: 1.0.0 + resolution: "json-bigint@npm:1.0.0" + dependencies: + bignumber.js: "npm:^9.0.0" + checksum: 10c0/e3f34e43be3284b573ea150a3890c92f06d54d8ded72894556357946aeed9877fd795f62f37fe16509af189fd314ab1104d0fd0f163746ad231b9f378f5b33f4 + languageName: node + linkType: hard + "json-buffer@npm:3.0.1": version: 3.0.1 resolution: "json-buffer@npm:3.0.1" @@ -16821,6 +17826,13 @@ __metadata: languageName: node linkType: hard +"long@npm:^2.4.0": + version: 2.4.0 + resolution: "long@npm:2.4.0" + checksum: 10c0/2560d6a299f2177b94ce300cc21fc15dbf72d9899ddac7bd10251468e1b4eabbce48463f651fea15cc4acf2c4775e3507767c58df73945d5d6ae4e12ac058796 + languageName: node + linkType: hard + "long@npm:^4.0.0": version: 4.0.0 resolution: "long@npm:4.0.0" @@ -17253,7 +18265,7 @@ __metadata: languageName: node linkType: hard -"minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.6, minimist@npm:^1.2.8": +"minimist@npm:^1.1.0, minimist@npm:^1.2.0, minimist@npm:^1.2.3, minimist@npm:^1.2.6, minimist@npm:^1.2.8": version: 1.2.8 resolution: "minimist@npm:1.2.8" checksum: 10c0/19d3fcdca050087b84c2029841a093691a91259a47def2f18222f41e7645a0b7c44ef4b40e88a1e58a40c84d2ef0ee6047c55594d298146d0eb3f6b737c20ce6 @@ -17583,7 +18595,7 @@ __metadata: languageName: node linkType: hard -"node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.7": +"node-fetch@npm:^2.5.0, node-fetch@npm:^2.6.7, node-fetch@npm:^2.6.9": version: 2.7.0 resolution: "node-fetch@npm:2.7.0" dependencies: @@ -17861,6 +18873,13 @@ __metadata: languageName: node linkType: hard +"opentracing@npm:^0.14.4": + version: 0.14.7 + resolution: "opentracing@npm:0.14.7" + checksum: 10c0/a7be8d697b1997548233423f5f4c196e285af8e864a24d7704fc6029beb73cd1f987651ca814e207629c6bc624cb03297a86601c0dc51cdca9a07a20f97b71ea + languageName: node + linkType: hard + "optionator@npm:^0.9.3": version: 0.9.4 resolution: "optionator@npm:0.9.4" @@ -18264,6 +19283,33 @@ __metadata: languageName: node linkType: hard +"pg-int8@npm:1.0.1": + version: 1.0.1 + resolution: "pg-int8@npm:1.0.1" + checksum: 10c0/be6a02d851fc2a4ae3e9de81710d861de3ba35ac927268973eb3cb618873a05b9424656df464dd43bd7dc3fc5295c3f5b3c8349494f87c7af50ec59ef14e0b98 + languageName: node + linkType: hard + +"pg-protocol@npm:*": + version: 1.10.0 + resolution: "pg-protocol@npm:1.10.0" + checksum: 10c0/7d0d64fe9df50262d907fd476454e1e36f41f5f66044c3ba6aa773fb8add1d350a9c162306e5c33e99bdfbdcc1140dd4ca74f66eda41d0aaceb5853244dcdb65 + languageName: node + linkType: hard + +"pg-types@npm:^2.2.0": + version: 2.2.0 + resolution: "pg-types@npm:2.2.0" + dependencies: + pg-int8: "npm:1.0.1" + postgres-array: "npm:~2.0.0" + postgres-bytea: "npm:~1.0.0" + postgres-date: "npm:~1.0.4" + postgres-interval: "npm:^1.1.0" + checksum: 10c0/ab3f8069a323f601cd2d2279ca8c425447dab3f9b61d933b0601d7ffc00d6200df25e26a4290b2b0783b59278198f7dd2ed03e94c4875797919605116a577c65 + languageName: node + linkType: hard + "picocolors@npm:^1.0.1, picocolors@npm:^1.1.0, picocolors@npm:^1.1.1": version: 1.1.1 resolution: "picocolors@npm:1.1.1" @@ -18340,16 +19386,16 @@ __metadata: languageName: node linkType: hard -"pino@npm:^9.0.0, pino@npm:^9.7.0": - version: 9.7.0 - resolution: "pino@npm:9.7.0" +"pino@npm:^9.0.0, pino@npm:^9.6.0": + version: 9.6.0 + resolution: "pino@npm:9.6.0" dependencies: atomic-sleep: "npm:^1.0.0" fast-redact: "npm:^3.1.1" on-exit-leak-free: "npm:^2.1.0" pino-abstract-transport: "npm:^2.0.0" pino-std-serializers: "npm:^7.0.0" - process-warning: "npm:^5.0.0" + process-warning: "npm:^4.0.0" quick-format-unescaped: "npm:^4.0.3" real-require: "npm:^0.2.0" safe-stable-stringify: "npm:^2.3.1" @@ -18357,7 +19403,7 @@ __metadata: thread-stream: "npm:^3.0.0" bin: pino: bin.js - checksum: 10c0/c7f8a83a9a9d728b4eff6d0f4b9367f031c91bcaa5806fbf0eedcc8e77faba593d59baf11a8fba0dd1c778bb17ca7ed01418ac1df4ec129faeedd4f3ecaff66f + checksum: 10c0/bcd1e9d9b301bea13b95689ca9ad7105ae9451928fb6c0b67b3e58c5fe37cea1d40665f3d6641e3da00be0bbc17b89031e67abbc8ea6aac6164f399309fd78e7 languageName: node linkType: hard @@ -18461,6 +19507,36 @@ __metadata: languageName: node linkType: hard +"postgres-array@npm:~2.0.0": + version: 2.0.0 + resolution: "postgres-array@npm:2.0.0" + checksum: 10c0/cbd56207e4141d7fbf08c86f2aebf21fa7064943d3f808ec85f442ff94b48d891e7a144cc02665fb2de5dbcb9b8e3183a2ac749959e794b4a4cfd379d7a21d08 + languageName: node + linkType: hard + +"postgres-bytea@npm:~1.0.0": + version: 1.0.0 + resolution: "postgres-bytea@npm:1.0.0" + checksum: 10c0/febf2364b8a8953695cac159eeb94542ead5886792a9627b97e33f6b5bb6e263bc0706ab47ec221516e79fbd6b2452d668841830fb3b49ec6c0fc29be61892ce + languageName: node + linkType: hard + +"postgres-date@npm:~1.0.4": + version: 1.0.7 + resolution: "postgres-date@npm:1.0.7" + checksum: 10c0/0ff91fccc64003e10b767fcfeefb5eaffbc522c93aa65d5051c49b3c4ce6cb93ab091a7d22877a90ad60b8874202c6f1d0f935f38a7235ed3b258efd54b97ca9 + languageName: node + linkType: hard + +"postgres-interval@npm:^1.1.0": + version: 1.2.0 + resolution: "postgres-interval@npm:1.2.0" + dependencies: + xtend: "npm:^4.0.0" + checksum: 10c0/c1734c3cb79e7f22579af0b268a463b1fa1d084e742a02a7a290c4f041e349456f3bee3b4ee0bb3f226828597f7b76deb615c1b857db9a742c45520100456272 + languageName: node + linkType: hard + "postject@npm:^1.0.0-alpha.6": version: 1.0.0-alpha.6 resolution: "postject@npm:1.0.0-alpha.6" @@ -18572,6 +19648,13 @@ __metadata: languageName: node linkType: hard +"process@npm:^0.10.0": + version: 0.10.1 + resolution: "process@npm:0.10.1" + checksum: 10c0/2608c672bb59fbd5e87f0c4df512540995af7e047c4eed024e1a4a1c9b144ce394840ce15069a97b82afef7aa58c6e2e8e959c1b711fe3a1f7bfb6529b03084c + languageName: node + linkType: hard + "progress@npm:^2.0.3": version: 2.0.3 resolution: "progress@npm:2.0.3" @@ -18763,7 +19846,7 @@ __metadata: languageName: node linkType: hard -"quick-format-unescaped@npm:^4.0.3": +"quick-format-unescaped@npm:^4.0.3, quick-format-unescaped@npm:^4.0.4": version: 4.0.4 resolution: "quick-format-unescaped@npm:4.0.4" checksum: 10c0/fe5acc6f775b172ca5b4373df26f7e4fd347975578199e7d74b2ae4077f0af05baa27d231de1e80e8f72d88275ccc6028568a7a8c9ee5e7368ace0e18eff93a4 @@ -18845,6 +19928,17 @@ __metadata: languageName: node linkType: hard +"react-dom@npm:^19.1.0": + version: 19.1.0 + resolution: "react-dom@npm:19.1.0" + dependencies: + scheduler: "npm:^0.26.0" + peerDependencies: + react: ^19.1.0 + checksum: 10c0/3e26e89bb6c67c9a6aa86cb888c7a7f8258f2e347a6d2a15299c17eb16e04c19194e3452bc3255bd34000a61e45e2cb51e46292392340432f133e5a5d2dfb5fc + languageName: node + linkType: hard + "react-is@npm:^18.0.0": version: 18.3.1 resolution: "react-is@npm:18.3.1" @@ -18875,6 +19969,13 @@ __metadata: languageName: node linkType: hard +"react@npm:^19.1.0": + version: 19.1.0 + resolution: "react@npm:19.1.0" + checksum: 10c0/530fb9a62237d54137a13d2cfb67a7db6a2156faed43eecc423f4713d9b20c6f2728b026b45e28fcd72e8eadb9e9ed4b089e99f5e295d2f0ad3134251bdd3698 + languageName: node + linkType: hard + "read-yaml-file@npm:^1.1.0": version: 1.1.0 resolution: "read-yaml-file@npm:1.1.0" @@ -19233,7 +20334,7 @@ __metadata: languageName: node linkType: hard -"rollup@npm:4.44.0, rollup@npm:^4.34.9": +"rollup@npm:4.44.0": version: 4.44.0 resolution: "rollup@npm:4.44.0" dependencies: @@ -19308,31 +20409,31 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.18.1": - version: 4.37.0 - resolution: "rollup@npm:4.37.0" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.37.0" - "@rollup/rollup-android-arm64": "npm:4.37.0" - "@rollup/rollup-darwin-arm64": "npm:4.37.0" - "@rollup/rollup-darwin-x64": "npm:4.37.0" - "@rollup/rollup-freebsd-arm64": "npm:4.37.0" - "@rollup/rollup-freebsd-x64": "npm:4.37.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.37.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.37.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.37.0" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.37.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.37.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.37.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.37.0" - "@rollup/rollup-linux-x64-musl": "npm:4.37.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.37.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.37.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.37.0" - "@types/estree": "npm:1.0.6" +"rollup@npm:^4.18.1, rollup@npm:^4.34.9, rollup@npm:^4.41.1": + version: 4.41.1 + resolution: "rollup@npm:4.41.1" + dependencies: + "@rollup/rollup-android-arm-eabi": "npm:4.41.1" + "@rollup/rollup-android-arm64": "npm:4.41.1" + "@rollup/rollup-darwin-arm64": "npm:4.41.1" + "@rollup/rollup-darwin-x64": "npm:4.41.1" + "@rollup/rollup-freebsd-arm64": "npm:4.41.1" + "@rollup/rollup-freebsd-x64": "npm:4.41.1" + "@rollup/rollup-linux-arm-gnueabihf": "npm:4.41.1" + "@rollup/rollup-linux-arm-musleabihf": "npm:4.41.1" + "@rollup/rollup-linux-arm64-gnu": "npm:4.41.1" + "@rollup/rollup-linux-arm64-musl": "npm:4.41.1" + "@rollup/rollup-linux-loongarch64-gnu": "npm:4.41.1" + "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.41.1" + "@rollup/rollup-linux-riscv64-gnu": "npm:4.41.1" + "@rollup/rollup-linux-riscv64-musl": "npm:4.41.1" + "@rollup/rollup-linux-s390x-gnu": "npm:4.41.1" + "@rollup/rollup-linux-x64-gnu": "npm:4.41.1" + "@rollup/rollup-linux-x64-musl": "npm:4.41.1" + "@rollup/rollup-win32-arm64-msvc": "npm:4.41.1" + "@rollup/rollup-win32-ia32-msvc": "npm:4.41.1" + "@rollup/rollup-win32-x64-msvc": "npm:4.41.1" + "@types/estree": "npm:1.0.7" fsevents: "npm:~2.3.2" dependenciesMeta: "@rollup/rollup-android-arm-eabi": @@ -19379,7 +20480,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/2e00382e08938636edfe0a7547ea2eaa027205dc0b6ff85d8b82be0fbe55a4ef88a1995fee2a5059e33dbccf12d1376c236825353afb89c96298cc95c5160a46 + checksum: 10c0/c4d5f2257320b50dc0e035e31d8d2f78d36b7015aef2f87cc984c0a1c97ffebf14337dddeb488b4b11ae798fea6486189b77e7cf677617dcf611d97db41ebfda languageName: node linkType: hard @@ -19495,6 +20596,13 @@ __metadata: languageName: node linkType: hard +"scheduler@npm:^0.26.0": + version: 0.26.0 + resolution: "scheduler@npm:0.26.0" + checksum: 10c0/5b8d5bfddaae3513410eda54f2268e98a376a429931921a81b5c3a2873aab7ca4d775a8caac5498f8cbc7d0daeab947cf923dbd8e215d61671f9f4e392d34356 + languageName: node + linkType: hard + "secure-json-parse@npm:^4.0.0": version: 4.0.0 resolution: "secure-json-parse@npm:4.0.0" @@ -19777,13 +20885,6 @@ __metadata: languageName: node linkType: hard -"shimmer@npm:^1.2.1": - version: 1.2.1 - resolution: "shimmer@npm:1.2.1" - checksum: 10c0/ae8b27c389db2a00acfc8da90240f11577685a8f3e40008f826a3bea8b4f3b3ecd305c26be024b4a0fd3b123d132c1569d6e238097960a9a543b6c60760fb46a - languageName: node - linkType: hard - "side-channel-list@npm:^1.0.0": version: 1.0.0 resolution: "side-channel-list@npm:1.0.0" @@ -20225,6 +21326,13 @@ __metadata: languageName: node linkType: hard +"string-template@npm:~0.2.1": + version: 0.2.1 + resolution: "string-template@npm:0.2.1" + checksum: 10c0/5dc9bd8741e50aaf1ebb616c64fdada32301dc52718692a7a13088285b96fecd1010ab612b348ef29c08dff4df4f96c8e80689ca855a578d01cc182e48199182 + languageName: node + linkType: hard + "string-width-cjs@npm:string-width@^4.2.0, string-width@npm:^4.0.0, string-width@npm:^4.1.0, string-width@npm:^4.2.0, string-width@npm:^4.2.2, string-width@npm:^4.2.3": version: 4.2.3 resolution: "string-width@npm:4.2.3" @@ -20672,6 +21780,19 @@ __metadata: languageName: node linkType: hard +"thriftrw@npm:^3.5.0": + version: 3.12.0 + resolution: "thriftrw@npm:3.12.0" + dependencies: + bufrw: "npm:^1.3.0" + error: "npm:7.0.2" + long: "npm:^2.4.0" + bin: + thrift2json: ./thrift2json.js + checksum: 10c0/3f7f4184eb3d722c8f07be7a27f960f9ebe4c014532cb77d625b69f2246e5c3f9f1e7be2dbbf438372e6d6827b32ba55f658c80addcb9b511e907017205f9213 + languageName: node + linkType: hard + "through@npm:2, through@npm:^2.3.8, through@npm:~2.3, through@npm:~2.3.1": version: 2.3.8 resolution: "through@npm:2.3.8" @@ -21549,6 +22670,15 @@ __metadata: languageName: node linkType: hard +"uuid@npm:^8.3.2": + version: 8.3.2 + resolution: "uuid@npm:8.3.2" + bin: + uuid: dist/bin/uuid + checksum: 10c0/bcbb807a917d374a49f475fae2e87fdca7da5e5530820ef53f65ba1d12131bd81a92ecf259cc7ce317cbe0f289e7d79fdfebcef9bfa3087c8c8a2fa304c9be54 + languageName: node + linkType: hard + "uuid@npm:^9.0.0, uuid@npm:^9.0.1": version: 9.0.1 resolution: "uuid@npm:9.0.1" @@ -22120,7 +23250,14 @@ __metadata: languageName: node linkType: hard -"xtend@npm:^4.0.2": +"xorshift@npm:^1.1.1": + version: 1.2.0 + resolution: "xorshift@npm:1.2.0" + checksum: 10c0/e805cdda3ca16ea48d3e1dbcb6f55f7c135bcf5219ae842bdea814486e4e6788cefc6703a04140b93be631370c1a7403c51ab0a6554ec68d66e84b601e34a722 + languageName: node + linkType: hard + +"xtend@npm:^4.0.0, xtend@npm:^4.0.2, xtend@npm:~4.0.0": version: 4.0.2 resolution: "xtend@npm:4.0.2" checksum: 10c0/366ae4783eec6100f8a02dff02ac907bf29f9a00b82ac0264b4d8b832ead18306797e283cf19de776538babfdcb2101375ec5646b59f08c52128ac4ab812ed0e @@ -22265,3 +23402,10 @@ __metadata: checksum: 10c0/00d76093a999e377e4ffd037fa7185e861c35917e8c4272f514115c206a0654995168f57fb71708b11e0a9243206d988b7f63b543404e1796402e50d346a6bd7 languageName: node linkType: hard + +"zone.js@npm:^0.11.0 || ^0.12.0 || ^0.13.0 || ^0.14.0 || ^0.15.0": + version: 0.15.1 + resolution: "zone.js@npm:0.15.1" + checksum: 10c0/4eca000f90dbea1c34f62e88ce56910dace8cdecbe14747b214f2af37aa511264c7bd50faf3e9b00612e95dc4567da156a9690fef1983bfcf74604a630d190ef + languageName: node + linkType: hard