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-1335-dependencies.md b/.changeset/@graphql-hive_gateway-1335-dependencies.md new file mode 100644 index 000000000..530da306b --- /dev/null +++ b/.changeset/@graphql-hive_gateway-1335-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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.202.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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sampler-jaeger-remote/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`) +- 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.6` ↗︎](https://www.npmjs.com/package/@graphql-mesh/plugin-mock/v/0.105.6) (from `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..abfa62910 --- /dev/null +++ b/.changeset/@graphql-hive_gateway-956-dependencies.md @@ -0,0 +1,19 @@ +--- +'@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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.202.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-logs@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/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`) +- Removed dependency [`@graphql-mesh/plugin-mock@^0.105.6` ↗︎](https://www.npmjs.com/package/@graphql-mesh/plugin-mock/v/0.105.6) (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-1335-dependencies.md b/.changeset/@graphql-hive_gateway-runtime-1335-dependencies.md new file mode 100644 index 000000000..102b3e27b --- /dev/null +++ b/.changeset/@graphql-hive_gateway-runtime-1335-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-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-1335-dependencies.md b/.changeset/@graphql-hive_nestjs-1335-dependencies.md new file mode 100644 index 000000000..0af93d273 --- /dev/null +++ b/.changeset/@graphql-hive_nestjs-1335-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-1335-dependencies.md b/.changeset/@graphql-mesh_fusion-runtime-1335-dependencies.md new file mode 100644 index 000000000..d381bdd14 --- /dev/null +++ b/.changeset/@graphql-mesh_fusion-runtime-1335-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..62f8db249 --- /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-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c` ↗︎](https://www.npmjs.com/package/@graphql-hive/core/v/0.13.0) (to `dependencies`) diff --git a/.changeset/@graphql-mesh_plugin-opentelemetry-1335-dependencies.md b/.changeset/@graphql-mesh_plugin-opentelemetry-1335-dependencies.md new file mode 100644 index 000000000..11f34a7b2 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-1335-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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-grpc/v/0.202.0) (from `^0.57.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.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/instrumentation@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/instrumentation/v/0.202.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.34.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/semantic-conventions/v/1.34.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-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c` ↗︎](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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/api-logs/v/0.202.0) (to `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/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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.202.0) (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 [`@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-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..b44729e9c --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-opentelemetry-956-dependencies.md @@ -0,0 +1,23 @@ +--- +'@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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/exporter-trace-otlp-grpc/v/0.202.0) (from `^0.57.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.57.0`, in `dependencies`) +- Updated dependency [`@opentelemetry/instrumentation@^0.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/instrumentation/v/0.202.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.34.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/semantic-conventions/v/1.34.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/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/auto-instrumentations-node@^0.60.1` ↗︎](https://www.npmjs.com/package/@opentelemetry/auto-instrumentations-node/v/0.60.1) (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.202.0` ↗︎](https://www.npmjs.com/package/@opentelemetry/sdk-logs/v/0.202.0) (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 [`@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-1335-dependencies.md b/.changeset/@graphql-mesh_plugin-prometheus-1335-dependencies.md new file mode 100644 index 000000000..02e9f8bf5 --- /dev/null +++ b/.changeset/@graphql-mesh_plugin-prometheus-1335-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-1335-dependencies.md b/.changeset/@graphql-mesh_transport-common-1335-dependencies.md new file mode 100644 index 000000000..2096b7a2b --- /dev/null +++ b/.changeset/@graphql-mesh_transport-common-1335-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/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/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/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/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 5e8481f32..ce3469252 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 @@ -59,7 +58,6 @@ jobs: fail-fast: false matrix: node-version: - - 18 - 20 - 22 - 23 @@ -87,10 +85,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 d2c6c10b1..b5c0065f9 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,4 +12,5 @@ __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 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.202.0-f6f29c2eeb.patch b/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch new file mode 100644 index 000000000..bfdae5bdd --- /dev/null +++ b/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch @@ -0,0 +1,29 @@ +diff --git a/build/esnext/transport/http-exporter-transport.js b/build/esnext/transport/http-exporter-transport.js +index 7977489487a2236fbd0e4c2273ef53fd3c7b93a8..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-var-requires +- } = 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/@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 3d46565a5..46ee8f1f4 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 @@ -18,4 +18,17 @@ Here we collect reasons and write explanations about why some resolutions or pat ### @memlab/core 1. Define package.json#export for `@memlab/core/Types` -1. Define package.json#export for `@memlab/core/Utils` +2. Define package.json#export for `@memlab/core/Utils` + +### @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/hmac-auth-https/package.json b/e2e/hmac-auth-https/package.json index 46b343942..58afde843 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.10", "@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 1a386084e..be3ff0e98 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,15 +35,30 @@ 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) => { describe(`exporter > ${OTLP_EXPORTER_TYPE}`, () => { @@ -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' }), @@ -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' }), @@ -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 5e1fba2f1..38634cfd1 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 b65b10100..cd21fa49b 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 e9d14b849..3af65e6eb 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 9bf8159a0..1b3661ee1 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.15.4", + "@graphql-hive/logger": "^1.0.0", "@graphql-mesh/compose-cli": "^1.4.10", "@graphql-mesh/hmac-upstream-signature": "^1.2.28", "@graphql-mesh/plugin-jwt-auth": "^1.5.6", @@ -2184,6 +2185,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.5", "resolved": "https://registry.npmjs.org/@graphql-hive/logger-json/-/logger-json-0.0.5.tgz", @@ -2397,17 +2408,17 @@ } }, "node_modules/@graphql-mesh/compose-cli": { - "version": "1.4.10", - "resolved": "https://registry.npmjs.org/@graphql-mesh/compose-cli/-/compose-cli-1.4.10.tgz", - "integrity": "sha512-i15PRiAXhUDt0ebEBRv+jgTJZQlvFXvSCZNVmiSWvH42dpEZcdhS+UWjZeIt7ScU7ZqwuZC2EvLijkHe3DdQNA==", + "version": "1.4.11", + "resolved": "https://registry.npmjs.org/@graphql-mesh/compose-cli/-/compose-cli-1.4.11.tgz", + "integrity": "sha512-mT6Q/VIzkuFAe19i6CjJSWH+piQDL7oCtjSr5FXSQBxUDjYzSTLI4vz72oN4vVwoNU9pIjIE1QwpvY+hlPBo7g==", "license": "MIT", "dependencies": { "@commander-js/extra-typings": "^13.0.0", - "@graphql-mesh/fusion-composition": "^0.8.9", - "@graphql-mesh/include": "^0.3.5", + "@graphql-mesh/fusion-composition": "^0.8.10", + "@graphql-mesh/include": "^0.3.6", "@graphql-mesh/string-interpolation": "^0.5.8", - "@graphql-mesh/types": "^0.104.5", - "@graphql-mesh/utils": "^0.104.5", + "@graphql-mesh/types": "^0.104.6", + "@graphql-mesh/utils": "^0.104.6", "@graphql-tools/code-file-loader": "^8.1.7", "@graphql-tools/graphql-file-loader": "^8.0.5", "@graphql-tools/load": "^8.0.1", @@ -2462,12 +2473,12 @@ } }, "node_modules/@graphql-mesh/fusion-composition": { - "version": "0.8.9", - "resolved": "https://registry.npmjs.org/@graphql-mesh/fusion-composition/-/fusion-composition-0.8.9.tgz", - "integrity": "sha512-O8qyhcGD8MK5W/KeKDnhXKZ1/7nQl6OSDvMfHItJpAn3UpDensJeHrSpv2tGVmygJyLZdJEp6rhDQOqraJRxAg==", + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/@graphql-mesh/fusion-composition/-/fusion-composition-0.8.10.tgz", + "integrity": "sha512-5ry9YNOa8rBCCs1V3YFWTzZi+BJ0is8QknEmJE34EuKHZgYEfNdYbWYkcyt7dKcDKHcjA89FQ0Lyd6yfjAooqA==", "license": "MIT", "dependencies": { - "@graphql-mesh/utils": "^0.104.5", + "@graphql-mesh/utils": "^0.104.6", "@graphql-tools/schema": "^10.0.5", "@graphql-tools/stitching-directives": "^3.1.9", "@graphql-tools/utils": "^10.8.0", @@ -2559,12 +2570,12 @@ } }, "node_modules/@graphql-mesh/include": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@graphql-mesh/include/-/include-0.3.5.tgz", - "integrity": "sha512-mHN67/M++JVy2POiZDNHIS0ffD0NQYEeyojDuSTl0lqjOw9srpni7TGX4x5OSO+5IcoBrrORySNj33JumSApEw==", + "version": "0.3.6", + "resolved": "https://registry.npmjs.org/@graphql-mesh/include/-/include-0.3.6.tgz", + "integrity": "sha512-fEnrpxLeFVk1eslhIXhRsEpeqMa2AfJTZMZyDveOFiCMrQuxyhvKS2lDHkFYriGcsWPZzk14bylAeNbTyCOG7w==", "license": "MIT", "dependencies": { - "@graphql-mesh/utils": "^0.104.5", + "@graphql-mesh/utils": "^0.104.6", "dotenv": "^16.3.1", "get-tsconfig": "^4.7.6", "jiti": "^2.0.0", @@ -2800,6 +2811,21 @@ "graphql": "*" } }, + "node_modules/@graphql-mesh/plugin-snapshot/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/@graphql-mesh/store": { "version": "0.104.6", "resolved": "https://registry.npmjs.org/@graphql-mesh/store/-/store-0.104.6.tgz", @@ -3800,6 +3826,27 @@ "integrity": "sha512-Sx1pU8EM64o2BrqNpEO1CNLtKQwyhuXuqyfH7oGKCk+1a33d2r5saW8zNwm3j6BTExtjrv2BxTgzzkMwts6vGg==", "license": "MIT" }, + "node_modules/@isaacs/balanced-match": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@isaacs/balanced-match/-/balanced-match-4.0.1.tgz", + "integrity": "sha512-yzMTt9lEb8Gv7zRioUilSglI0c0smZ9k5D65677DLWLtWJaXIS3CqcGyUFByYKlnUj6TkjLVs54fBl6+TiGQDQ==", + "license": "MIT", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@isaacs/brace-expansion": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@isaacs/brace-expansion/-/brace-expansion-5.0.0.tgz", + "integrity": "sha512-ZT55BDLV0yv0RBm2czMiZ+SqCGO7AvmOM3G/w2xhVPH+te0aKgFjmBvGlL1dH+ql2tgGO3MVrbb3jCKyvpgnxA==", + "license": "MIT", + "dependencies": { + "@isaacs/balanced-match": "^4.0.1" + }, + "engines": { + "node": "20 || >=22" + } + }, "node_modules/@isaacs/cliui": { "version": "8.0.2", "resolved": "https://registry.npmjs.org/@isaacs/cliui/-/cliui-8.0.2.tgz", @@ -4457,9 +4504,9 @@ } }, "node_modules/@smithy/core": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.6.0.tgz", - "integrity": "sha512-Pgvfb+TQ4wUNLyHzvgCP4aYZMh16y7GcfF59oirRHcgGgkH1e/s9C0nv/v3WP+Quymyr5je71HeFQCwh+44XLg==", + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.7.0.tgz", + "integrity": "sha512-7ov8hu/4j0uPZv8b27oeOFtIBtlFmM3ibrPv/Omx1uUdoXvcpJ00U+H/OWWC/keAguLlcqwtyL2/jTlSnApgNQ==", "license": "Apache-2.0", "dependencies": { "@smithy/middleware-serde": "^4.0.8", @@ -4468,7 +4515,7 @@ "@smithy/util-base64": "^4.0.0", "@smithy/util-body-length-browser": "^4.0.0", "@smithy/util-middleware": "^4.0.4", - "@smithy/util-stream": "^4.2.2", + "@smithy/util-stream": "^4.2.3", "@smithy/util-utf8": "^4.0.0", "tslib": "^2.6.2" }, @@ -4493,9 +4540,9 @@ } }, "node_modules/@smithy/fetch-http-handler": { - "version": "5.0.4", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.0.4.tgz", - "integrity": "sha512-AMtBR5pHppYMVD7z7G+OlHHAcgAN7v0kVKEpHuTO4Gb199Gowh0taYi9oDStFeUhetkeP55JLSVlTW1n9rFtUw==", + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.1.0.tgz", + "integrity": "sha512-mADw7MS0bYe2OGKkHYMaqarOXuDwRbO6ArD91XhHcl2ynjGCFF+hvqf0LyQcYxkA1zaWjefSkU7Ne9mqgApSgQ==", "license": "Apache-2.0", "dependencies": { "@smithy/protocol-http": "^5.1.2", @@ -4563,12 +4610,12 @@ } }, "node_modules/@smithy/middleware-endpoint": { - "version": "4.1.13", - "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.13.tgz", - "integrity": "sha512-xg3EHV/Q5ZdAO5b0UiIMj3RIOCobuS40pBBODguUDVdko6YK6QIzCVRrHTogVuEKglBWqWenRnZ71iZnLL3ZAQ==", + "version": "4.1.14", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.1.14.tgz", + "integrity": "sha512-+BGLpK5D93gCcSEceaaYhUD/+OCGXM1IDaq/jKUQ+ujB0PTWlWN85noodKw/IPFZhIKFCNEe19PGd/reUMeLSQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.6.0", + "@smithy/core": "^3.7.0", "@smithy/middleware-serde": "^4.0.8", "@smithy/node-config-provider": "^4.1.3", "@smithy/shared-ini-file-loader": "^4.0.4", @@ -4582,15 +4629,15 @@ } }, "node_modules/@smithy/middleware-retry": { - "version": "4.1.14", - "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.14.tgz", - "integrity": "sha512-eoXaLlDGpKvdmvt+YBfRXE7HmIEtFF+DJCbTPwuLunP0YUnrydl+C4tS+vEM0+nyxXrX3PSUFqC+lP1+EHB1Tw==", + "version": "4.1.15", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.1.15.tgz", + "integrity": "sha512-iKYUJpiyTQ33U2KlOZeUb0GwtzWR3C0soYcKuCnTmJrvt6XwTPQZhMfsjJZNw7PpQ3TU4Ati1qLSrkSJxnnSMQ==", "license": "Apache-2.0", "dependencies": { "@smithy/node-config-provider": "^4.1.3", "@smithy/protocol-http": "^5.1.2", "@smithy/service-error-classification": "^4.0.6", - "@smithy/smithy-client": "^4.4.5", + "@smithy/smithy-client": "^4.4.6", "@smithy/types": "^4.3.1", "@smithy/util-middleware": "^4.0.4", "@smithy/util-retry": "^4.0.6", @@ -4644,9 +4691,9 @@ } }, "node_modules/@smithy/node-http-handler": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.0.6.tgz", - "integrity": "sha512-NqbmSz7AW2rvw4kXhKGrYTiJVDHnMsFnX4i+/FzcZAfbOBauPYs2ekuECkSbtqaxETLLTu9Rl/ex6+I2BKErPA==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.1.0.tgz", + "integrity": "sha512-vqfSiHz2v8b3TTTrdXi03vNz1KLYYS3bhHCDv36FYDqxT7jvTll1mMnCrkD+gOvgwybuunh/2VmvOMqwBegxEg==", "license": "Apache-2.0", "dependencies": { "@smithy/abort-controller": "^4.0.4", @@ -4757,17 +4804,17 @@ } }, "node_modules/@smithy/smithy-client": { - "version": "4.4.5", - "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.5.tgz", - "integrity": "sha512-+lynZjGuUFJaMdDYSTMnP/uPBBXXukVfrJlP+1U/Dp5SFTEI++w6NMga8DjOENxecOF71V9Z2DllaVDYRnGlkg==", + "version": "4.4.6", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.4.6.tgz", + "integrity": "sha512-3wfhywdzB/CFszP6moa5L3lf5/zSfQoH0kvVSdkyK2az5qZet0sn2PAHjcTDiq296Y4RP5yxF7B6S6+3oeBUCQ==", "license": "Apache-2.0", "dependencies": { - "@smithy/core": "^3.6.0", - "@smithy/middleware-endpoint": "^4.1.13", + "@smithy/core": "^3.7.0", + "@smithy/middleware-endpoint": "^4.1.14", "@smithy/middleware-stack": "^4.0.4", "@smithy/protocol-http": "^5.1.2", "@smithy/types": "^4.3.1", - "@smithy/util-stream": "^4.2.2", + "@smithy/util-stream": "^4.2.3", "tslib": "^2.6.2" }, "engines": { @@ -4864,13 +4911,13 @@ } }, "node_modules/@smithy/util-defaults-mode-browser": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.21.tgz", - "integrity": "sha512-wM0jhTytgXu3wzJoIqpbBAG5U6BwiubZ6QKzSbP7/VbmF1v96xlAbX2Am/mz0Zep0NLvLh84JT0tuZnk3wmYQA==", + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.0.22.tgz", + "integrity": "sha512-hjElSW18Wq3fUAWVk6nbk7pGrV7ZT14DL1IUobmqhV3lxcsIenr5FUsDe2jlTVaS8OYBI3x+Og9URv5YcKb5QA==", "license": "Apache-2.0", "dependencies": { "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.5", + "@smithy/smithy-client": "^4.4.6", "@smithy/types": "^4.3.1", "bowser": "^2.11.0", "tslib": "^2.6.2" @@ -4880,16 +4927,16 @@ } }, "node_modules/@smithy/util-defaults-mode-node": { - "version": "4.0.21", - "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.21.tgz", - "integrity": "sha512-/F34zkoU0GzpUgLJydHY8Rxu9lBn8xQC/s/0M0U9lLBkYbA1htaAFjWYJzpzsbXPuri5D1H8gjp2jBum05qBrA==", + "version": "4.0.22", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.0.22.tgz", + "integrity": "sha512-7B8mfQBtwwr2aNRRmU39k/bsRtv9B6/1mTMrGmmdJFKmLAH+KgIiOuhaqfKOBGh9sZ/VkZxbvm94rI4MMYpFjQ==", "license": "Apache-2.0", "dependencies": { "@smithy/config-resolver": "^4.1.4", "@smithy/credential-provider-imds": "^4.0.6", "@smithy/node-config-provider": "^4.1.3", "@smithy/property-provider": "^4.0.4", - "@smithy/smithy-client": "^4.4.5", + "@smithy/smithy-client": "^4.4.6", "@smithy/types": "^4.3.1", "tslib": "^2.6.2" }, @@ -4951,13 +4998,13 @@ } }, "node_modules/@smithy/util-stream": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.2.tgz", - "integrity": "sha512-aI+GLi7MJoVxg24/3J1ipwLoYzgkB4kUfogZfnslcYlynj3xsQ0e7vk4TnTro9hhsS5PvX1mwmkRqqHQjwcU7w==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.2.3.tgz", + "integrity": "sha512-cQn412DWHHFNKrQfbHY8vSFI3nTROY1aIKji9N0tpp8gUABRilr7wdf8fqBbSlXresobM+tQFNk6I+0LXK/YZg==", "license": "Apache-2.0", "dependencies": { - "@smithy/fetch-http-handler": "^5.0.4", - "@smithy/node-http-handler": "^4.0.6", + "@smithy/fetch-http-handler": "^5.1.0", + "@smithy/node-http-handler": "^4.1.0", "@smithy/types": "^4.3.1", "@smithy/util-base64": "^4.0.0", "@smithy/util-buffer-from": "^4.0.0", @@ -4995,13 +5042,13 @@ } }, "node_modules/@theguild/federation-composition": { - "version": "0.18.4", - "resolved": "https://registry.npmjs.org/@theguild/federation-composition/-/federation-composition-0.18.4.tgz", - "integrity": "sha512-2hdoeVSVTxp4TkZAy9HJnGWmKMyjoTx20tsWKz8eZ+B6pMA6XqCGLahrr8NPyj1jx+1vyczKAdGNopgvzuDHWw==", + "version": "0.18.5", + "resolved": "https://registry.npmjs.org/@theguild/federation-composition/-/federation-composition-0.18.5.tgz", + "integrity": "sha512-3tL689XKObclVOXgLHrAwkDOlo3gTGxhJhV19xb4AHZx5xWLON4RFMtOnphUeOqcfF1IoIu2x1scDH0NEVE5bw==", "license": "MIT", "dependencies": { "constant-case": "^3.0.4", - "debug": "4.4.0", + "debug": "4.4.1", "json5": "^2.2.3", "lodash.sortby": "^4.7.0" }, @@ -5012,23 +5059,6 @@ "graphql": "^16.0.0" } }, - "node_modules/@theguild/federation-composition/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -5545,9 +5575,9 @@ "license": "MIT" }, "node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" @@ -7660,12 +7690,12 @@ } }, "node_modules/minimatch": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", - "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "version": "10.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", + "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", "license": "ISC", "dependencies": { - "brace-expansion": "^2.0.1" + "@isaacs/brace-expansion": "^5.0.0" }, "engines": { "node": "20 || >=22" diff --git a/examples/hmac-auth-https/package.json b/examples/hmac-auth-https/package.json index 491089d07..e5a2a4312 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.15.4", + "@graphql-hive/logger": "^1.0.0", "@graphql-mesh/compose-cli": "^1.4.10", "@graphql-mesh/hmac-upstream-signature": "^1.2.28", "@graphql-mesh/plugin-jwt-auth": "^1.5.6", 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 709e0319c..5dcc2df8d 100644 --- a/internal/e2e/src/tenv.ts +++ b/internal/e2e/src/tenv.ts @@ -424,8 +424,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 ef951dbbb..13628faa9 100644 --- a/package.json +++ b/package.json @@ -12,6 +12,7 @@ ], "packageManager": "yarn@4.9.2", "scripts": { + "start": "yarn workspace @graphql-hive/gateway start", "bench": "vitest bench --project bench", "build": "yarn workspaces foreach -A -p run build", "bundle": "yarn workspaces foreach -A -p run bundle", @@ -22,7 +23,7 @@ "format": "yarn check:format --write", "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,10 @@ "@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", + "@opentelemetry/otlp-exporter-base@npm:0.202.0": "patch:@opentelemetry/otlp-exporter-base@npm%3A0.202.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.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", "cookie": "^1.0.0", "cross-spawn": "7.0.6", "esbuild": "0.25.5", @@ -79,4 +81,4 @@ "tsx": "patch:tsx@npm%3A4.20.3#~/.yarn/patches/tsx-npm-4.20.3-7de67a623f.patch", "vite": "6.3.5" } -} +} \ No newline at end of file diff --git a/packages/batch-delegate/package.json b/packages/batch-delegate/package.json index 89095f8a6..2dfac57b2 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 7c10a2e03..53e274e4a 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/delegate/package.json b/packages/delegate/package.json index 7e0eb77b4..76a19837b 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/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/executors/common/package.json b/packages/executors/common/package.json index 6179da86a..9de877fe0 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 49770c7fe..0d99b9e21 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 9c2e27496..75355021f 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 5120af8f8..bb6efbb89 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 730f8f23c..f7dac9e93 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.5", 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..73fc867ce 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,6 +164,8 @@ function getTransportExecutor({ }, getDisposeReason, ...transportContext, + log, + logger, }); }, ); @@ -186,7 +182,7 @@ export const subgraphNameByExecutionRequest = new WeakMap< */ export function getOnSubgraphExecute({ onSubgraphExecuteHooks, - transportContext = {}, + transportContext, transportEntryMap, getSubgraphSchema, transportExecutorStack, @@ -214,18 +210,19 @@ export function getOnSubgraphExecute({ 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 +253,7 @@ export function getOnSubgraphExecute({ transportEntryMap, transportContext, getSubgraphSchema, + instrumentation, }); // Caches the executor for future use subgraphExecutorMap.set(subgraphName, executor); @@ -266,20 +264,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 +283,7 @@ export interface WrapExecuteWithHooksOptions { transportEntryMap?: Record; getSubgraphSchema: (subgraphName: string) => GraphQLSchema; transportContext?: TransportContext; + instrumentation: () => Instrumentation | undefined; } declare module 'graphql' { @@ -310,28 +303,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 +342,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 +365,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 +405,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 +425,14 @@ export function wrapExecutorWithHooks({ ); }, ); + } + + return function instrumentedExecutor(executionRequest: ExecutionRequest) { + const subgraphInstrument = instrumentation()?.subgraphExecute; + return getInstrumented({ executionRequest, subgraphName }).asyncFn( + subgraphInstrument, + executorWithHooks, + )(executionRequest); }; } @@ -468,8 +454,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 +495,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 +531,7 @@ export interface OnDelegationStageExecutePayload { typeName: string; - requestId?: string; - logger?: Logger; + log: Logger; } export type OnDelegationStageExecuteDoneHook = ( @@ -595,19 +578,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 +601,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 6f6d4638c..f407ac7e2 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.6", "@graphql-mesh/plugin-jit": "^0.2.5", "@graphql-mesh/plugin-jwt-auth": "workspace:^", - "@graphql-mesh/plugin-mock": "^0.105.6", "@graphql-mesh/plugin-opentelemetry": "workspace:^", "@graphql-mesh/plugin-prometheus": "workspace:^", "@graphql-mesh/plugin-rate-limit": "^0.104.5", @@ -82,6 +82,19 @@ "@graphql-tools/load": "^8.0.14", "@graphql-tools/utils": "^10.8.1", "@graphql-yoga/render-graphiql": "^5.15.1", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/api-logs": "^0.202.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.202.0", + "@opentelemetry/sdk-logs": "^0.202.0", + "@opentelemetry/sdk-metrics": "^2.0.1", + "@opentelemetry/sdk-trace-base": "^2.0.1", "commander": "^13.1.0", "dotenv": "^17.2.0", "graphql-ws": "^6.0.4", @@ -95,7 +108,7 @@ "@graphql-tools/executor": "^1.4.7", "@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 45e8f1b88..a65992305 100644 --- a/packages/gateway/src/cli.ts +++ b/packages/gateway/src/cli.ts @@ -15,16 +15,16 @@ 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 parseDuration from 'parse-duration'; -import { getDefaultLogger } from '../../runtime/src/getDefaultLogger'; import { addCommands } from './commands/index'; import { createDefaultConfigPaths } from './config'; import { getMaxConcurrency } from './getMaxConcurrency'; @@ -105,7 +105,7 @@ export interface GatewayCLIProxyConfig } export type KeyValueCacheFactoryFn = (ctx: { - logger: Logger; + log: Logger; pubsub: HivePubSub; cwd: string; }) => KeyValueCache; @@ -128,7 +128,10 @@ export interface GatewayCLIBuiltinPluginConfig { * * @see https://graphql-hive.com/docs/gateway/monitoring-tracing */ - openTelemetry?: Exclude; + openTelemetry?: Exclude< + OpenTelemetryGatewayPluginOptions, + GatewayConfigContext + >; /** * Configure Rate Limiting * @@ -221,7 +224,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; @@ -252,9 +255,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: process.env['NODE_ENV'] === 'production' ? maxFork : 1, + fork: 1, host: platform().toLowerCase() === 'win32' || // is WSL? @@ -274,21 +276,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( @@ -340,31 +343,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"] Hive registry token for usage metrics reporting', ).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( @@ -400,9 +444,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 6c17f1364..288a6d5ba 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, @@ -28,7 +29,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) => @@ -49,11 +50,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, @@ -65,6 +72,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, @@ -75,15 +95,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); } @@ -93,10 +112,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); } @@ -123,7 +142,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, @@ -138,11 +157,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, @@ -153,11 +172,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, @@ -171,20 +193,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, @@ -224,7 +249,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); } @@ -270,12 +295,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); } @@ -285,11 +310,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}`); }); @@ -301,7 +329,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'); @@ -314,10 +345,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', + ); }); } } @@ -352,21 +389,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 74edf43d8..eda7b6020 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 f20b3a230..000000000 --- a/packages/logger-json/CHANGELOG.md +++ /dev/null @@ -1,52 +0,0 @@ -# @graphql-hive/logger-json - -## 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 69713d53d..000000000 --- a/packages/logger-json/package.json +++ /dev/null @@ -1,56 +0,0 @@ -{ - "name": "@graphql-hive/logger-json", - "version": "0.0.5", - "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.5", - "@graphql-mesh/utils": "^0.104.5", - "cross-inspect": "^1.0.1", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "^16.9.0", - "pkgroll": "2.14.3" - }, - "sideEffects": false -} diff --git a/packages/logger-json/src/index.ts b/packages/logger-json/src/index.ts deleted file mode 100644 index 712d7d7e1..000000000 --- a/packages/logger-json/src/index.ts +++ /dev/null @@ -1,185 +0,0 @@ -import { process } from '@graphql-mesh/cross-helpers'; -import type { LazyLoggerMessage, Logger } from '@graphql-mesh/types'; -import { LogLevel } from '@graphql-mesh/utils'; -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 = [process.env['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 88ecb90e6..000000000 --- a/packages/logger-pino/CHANGELOG.md +++ /dev/null @@ -1,37 +0,0 @@ -# @graphql-hive/logger-pino - -## 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 8a8ec7461..000000000 --- a/packages/logger-pino/package.json +++ /dev/null @@ -1,58 +0,0 @@ -{ - "name": "@graphql-hive/logger-pino", - "version": "1.0.2", - "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.5", - "@graphql-mesh/utils": "^0.104.5", - "@whatwg-node/disposablestack": "^0.0.6", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "16.11.0", - "pino": "^9.7.0", - "pkgroll": "2.14.3" - }, - "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 5fe13b275..000000000 --- a/packages/logger-winston/CHANGELOG.md +++ /dev/null @@ -1,55 +0,0 @@ -# @graphql-hive/logger-winston - -## 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 57d761b93..000000000 --- a/packages/logger-winston/package.json +++ /dev/null @@ -1,57 +0,0 @@ -{ - "name": "@graphql-hive/logger-winston", - "version": "1.0.3", - "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.5", - "@whatwg-node/disposablestack": "^0.0.6", - "tslib": "^2.8.1" - }, - "devDependencies": { - "graphql": "16.11.0", - "pkgroll": "2.14.3", - "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..5bb6355cb --- /dev/null +++ b/packages/logger/src/logger.ts @@ -0,0 +1,308 @@ +import { DisposableSymbols } from '@whatwg-node/disposablestack'; +import fastSafeStringify from 'fast-safe-stringify'; +import format from 'quick-format-unescaped'; +import { + Attributes, + getEnv, + isPromise, + logLevel, + MaybeLazy, + parseAttrs, + shallowMergeAttributes, + shouldLog, + truthyEnv, +} 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 = getEnv('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) ?? + (truthyEnv('DEBUG') ? 'debug' : 'info'); + this.#prefix = opts.prefix; + this.#attrs = opts.attrs; + this.#writers = + opts.writers ?? + (truthyEnv('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 (truthyEnv('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..fbfaae167 --- /dev/null +++ b/packages/logger/src/utils.ts @@ -0,0 +1,229 @@ +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 getEnv(key: string): string | undefined { + return ( + globalThis.process?.env?.[key] || + // @ts-expect-error can exist in wrangler and maybe other runtimes + globalThis.env?.[key] || + // @ts-expect-error can exist in deno + globalThis.Deno?.env?.get(key) || + // @ts-expect-error could be + globalThis[key] + ); +} + +export function truthyEnv(key: string): boolean { + return ['1', 't', 'true', 'y', 'yes'].includes( + getEnv(key)?.toLowerCase() || '', + ); +} + +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..777d48d79 --- /dev/null +++ b/packages/logger/src/writers/console.ts @@ -0,0 +1,157 @@ +import { createDeferredPromise } from '@whatwg-node/promise-helpers'; +import { LogLevel } from '../logger'; +import { Attributes, logLevelToString, truthyEnv } 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/ + truthyEnv('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..2f5cd6fa9 --- /dev/null +++ b/packages/logger/src/writers/json.ts @@ -0,0 +1,23 @@ +import { LogLevel } from '../logger'; +import { Attributes, truthyEnv } 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(), + }, + truthyEnv('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 390010499..1837050e8 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.5", "@graphql-tools/utils": "^10.8.1", "@whatwg-node/promise-helpers": "^1.3.0", diff --git a/packages/nestjs/src/index.ts b/packages/nestjs/src/index.ts index e4f14f710..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, @@ -63,6 +61,7 @@ export class HiveGatewayDriver< private async ensureGatewayRuntime({ typeDefs, resolvers, + logging, ...options }: HiveGatewayDriverConfig) { if (this._gatewayRuntime) { @@ -78,14 +77,34 @@ export class HiveGatewayDriver< if (resolvers) { additionalResolvers.push(...asArray(resolvers)); } - const logger = new NestJSLoggerAdapter( - 'Hive Gateway', - {}, - new NestLogger('Hive Gateway'), - options.debug ?? truthy(process.env['DEBUG']), - ); + + 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(), }; @@ -96,7 +115,7 @@ export class HiveGatewayDriver< }); this._gatewayRuntime = createGatewayRuntime({ ...options, - logging: configCtx.logger, + logging: configCtx.log, cache, graphqlEndpoint: options.path, additionalTypeDefs, @@ -287,100 +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, - ); - } -} - -function truthy(val: unknown) { - return ( - val === true || - val === 1 || - ['1', 't', 'true', 'y', 'yes'].includes(String(val)) - ); -} diff --git a/packages/plugins/aws-sigv4/package.json b/packages/plugins/aws-sigv4/package.json index e7de7b780..6c700974e 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/deduplicate-request/package.json b/packages/plugins/deduplicate-request/package.json index 175f97b2d..7273f6d87 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 ad79ec909..f2bbf140a 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 99308a1d7..1c819502b 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 9ce7d75b3..d995c5076 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-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c", "@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.5", "@graphql-mesh/utils": "^0.104.5", "@graphql-tools/utils": "^10.8.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.202.0", + "@opentelemetry/auto-instrumentations-node": "^0.60.1", + "@opentelemetry/context-async-hooks": "^2.0.1", + "@opentelemetry/core": "^2.0.1", + "@opentelemetry/exporter-trace-otlp-grpc": "^0.202.0", + "@opentelemetry/exporter-trace-otlp-http": "^0.202.0", + "@opentelemetry/instrumentation": "^0.202.0", + "@opentelemetry/resources": "^2.0.1", + "@opentelemetry/sdk-logs": "^0.202.0", + "@opentelemetry/sdk-node": "^0.202.0", + "@opentelemetry/sdk-trace-base": "^2.0.1", + "@opentelemetry/semantic-conventions": "^1.34.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.14.3" + "pkgroll": "2.14.3", + "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..65b63ecf5 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,14 @@ 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'; -// Gateway-specific attributes +// GraphQL-specific-non-standard attributes +export const SEMATTRS_GRAPHQL_OPERATION_HASH = 'hive.graphql.operation.hash'; +export const SEMATTRS_GRAPHQL_ERROR_COUNT = 'hive.graphql.error.count'; +export const SEMATTRS_GRAPHQL_ERROR_CODES = 'hive.graphql.error.codes'; + +// Hive Gateway-specific attributes export const SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME = - 'gateway.upstream.subgraph.name'; + 'hive.gateway.upstream.subgraph.name'; +export const SEMATTRS_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..be19b8c20 --- /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_GATEWAY_OPERATION_SUBGRAPH_NAMES, + SEMATTRS_GRAPHQL_ERROR_CODES, + SEMATTRS_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_GRAPHQL_ERROR_CODES); + copyAttribute(span, operationSpan, SEMATTRS_GRAPHQL_ERROR_COUNT); + copyAttribute( + span, + operationSpan, + SEMATTRS_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..ebbe4c6cf 100644 --- a/packages/plugins/opentelemetry/src/index.ts +++ b/packages/plugins/opentelemetry/src/index.ts @@ -1,5 +1,16 @@ -export * from './processors'; -export { +import { DiagLogLevel } from '@opentelemetry/api'; +import { useOpenTelemetry, - type OpenTelemetryGatewayPluginOptions as OpenTelemetryMeshPluginOptions, + type OpenTelemetryGatewayPluginOptions, + type OpenTelemetryPlugin, } from './plugin'; + +export * from './attributes'; + +export const OpenTelemetryDiagLogLevel = DiagLogLevel; + +export { + useOpenTelemetry, + OpenTelemetryPlugin, + OpenTelemetryGatewayPluginOptions, +}; 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..77817b544 --- /dev/null +++ b/packages/plugins/opentelemetry/src/plugin-utils.ts @@ -0,0 +1,164 @@ +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 = {}; + for (const [hookName, hook] of Object.entries(src) as any) { + if (typeof hook !== 'function') { + result[hookName] = hook; + } else { + result[hookName] = { + [hook.name](payload: any, ...args: any[]) { + return hook( + { + ...payload, + get state() { + return getState(payload); + }, + }, + ...args, + ); + }, + }[hook.name]; + } + } + return result; + } + + const { instrumentation, ...hooks } = pluginFactory(getState as any); + + const pluginWithState = addStateGetters(hooks); + pluginWithState.instrumentation = addStateGetters(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..2bf14df45 100644 --- a/packages/plugins/opentelemetry/src/plugin.ts +++ b/packages/plugins/opentelemetry/src/plugin.ts @@ -1,145 +1,209 @@ 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, + 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 +214,715 @@ const HeadersTextMapGetter: TextMapGetter = { }, }; -export function useOpenTelemetry( - options: OpenTelemetryGatewayPluginOptions & { logger: Logger }, -): GatewayPlugin<{ - opentelemetry: { +export type OpenTelemetryContextExtension = { + openTelemetry: { tracer: Tracer; activeContext: () => Context; }; -}> { +}; + +type OtelState = { + otel: OtelContextStack; +}; + +type State = Partial< + HttpState & GraphQLState & GatewayState +>; + +export type OpenTelemetryPlugin = + GatewayPlugin & { + getOtelContext: (payload: { + request?: Request; + context?: any; + executionRequest?: ExecutionRequest; + }) => Context; + getTracer(): Tracer; + }; + +export function useOpenTelemetry( + options: OpenTelemetryGatewayPluginOptions & { + log: Logger; + }, +): 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 pluginLogger: Logger; + let initSpan: Context | null; + + 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; + } - 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); + if (useContextManager) { + return context.active(); } - if (!otelContext && context?.request) { - otelContext = requestContextMapping.get(context.request); + + 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; } - if (!otelContext && context) { - otelContext = contextMapping.get(context); + + 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; + return withState< + OpenTelemetryPlugin, + OtelState, + OtelState & { skipExecuteSpan?: true; subgraphNames: string[] }, + OtelState + >((getState) => ({ + getTracer: () => tracer, + getOtelContext: ({ state }) => getContext(state), + 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 ( - !( - 'initializeNodeSDK' in options && - options.initializeNodeSDK === false + !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 ( + !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)); } - const pluginLogger = options.logger.child({ plugin: 'OpenTelemetry' }); - const diagLogger = pluginLogger.child('OtelDiag'); - diag.setLogger( - { - 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), - }, - DiagLogLevel.VERBOSE, + + if ( + !isParentEnabled(state) || + !shouldTrace(traces.spans?.upstreamFetch, { executionRequest }) + ) { + return wrapped(); + } + + 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(); + }); + }), ); - setGlobalErrorHandler((err) => - diagLogger.error('Uncaught OTEL internal error', err), + }, + + 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(); + }); + }), ); - tracer = options.tracer || trace.getTracer('gateway'); - preparation$ = undefined; - }); + }, }, - onContextBuilding({ extendContext, context }) { + + onYogaInit({ yoga }) { + const log = + options.log ?? + //TODO remove this when Yoga will also use the new Logger API + new Logger({ + writers: [ + { + write(level, attrs, msg) { + level = level === 'trace' ? 'debug' : level; + yoga.logger[level](msg, attrs); + }, + }, + ], + }); + + pluginLogger = log.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) { + 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', + ); + } + }, + + onEnveloped({ state, extendContext }) { extendContext({ - opentelemetry: { + openTelemetry: { tracer, - activeContext: () => - getOTELContext(context, context.request) ?? context['active'](), + activeContext: () => getContext(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; + } + + setExecutionAttributesOnOperationSpan({ + ctx: state.forOperation.otel!.root, + args, + hashOperationFn: options.hashOperation, + }); - return done; + 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](); + } + } }, - }; + })); +} + +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..bc4e7bd8c --- /dev/null +++ b/packages/plugins/opentelemetry/src/setup.ts @@ -0,0 +1,246 @@ +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; + }; + +export function openTelemetrySetup(options: OpentelemetrySetupOptions) { + if (getEnvVar('OTEL_SDK_DISABLED', 'false') === 'true') { + return; + } + + if (options.traces) { + if (options.traces.tracerProvider) { + if ( + 'register' in options.traces.tracerProvider && + typeof options.traces.tracerProvider.register === 'function' + ) { + options.traces.tracerProvider.register(); + } else { + trace.setGlobalTracerProvider(options.traces.tracerProvider); + } + } else { + let spanProcessors = options.traces.processors ?? []; + + if (options.traces.exporter) { + spanProcessors.push( + resolveBatchingConfig( + options.traces.exporter, + options.traces.batching, + ), + ); + } + + if (options.traces.console) { + spanProcessors.push(new SimpleSpanProcessor(new ConsoleSpanExporter())); + } + + 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__, + ), + }); + + trace.setGlobalTracerProvider( + new BasicTracerProvider({ + resource: + options.resource && !('serviceName' in options.resource) + ? baseResource.merge(options.resource) + : baseResource, + 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) { + context.setGlobalContextManager(options.contextManager); + } + + if (!options.propagators || options.propagators.length !== 0) { + const propagators = options.propagators ?? [ + new W3CBaggagePropagator(), + new W3CTraceContextPropagator(), + ]; + + propagation.setGlobalPropagator( + propagators.length === 1 + ? propagators[0]! + : new CompositePropagator({ propagators }), + ); + } +} + +export type HiveTracingOptions = { target?: string } & ( + | { + accessToken?: string; + batching?: BufferConfig; + processor?: never; + } + | { + processor: SpanProcessor; + } +); + +export function hiveTracingSetup( + config: HiveTracingOptions & { contextManager: ContextManager | null }, +) { + 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.', + ); + } + + 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.', + ); + } + } + + openTelemetrySetup({ + contextManager: config.contextManager, + resource: resourceFromAttributes({ + 'hive.target_id': config.target, + }), + traces: { + processors: [ + new HiveTracingSpanProcessor(config as HiveTracingSpanProcessorOptions), + ], + }, + }); +} + +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..d01f5cce0 100644 --- a/packages/plugins/opentelemetry/src/spans.ts +++ b/packages/plugins/opentelemetry/src/spans.ts @@ -1,21 +1,45 @@ +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_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_GATEWAY_OPERATION_SUBGRAPH_NAMES, SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME, SEMATTRS_GRAPHQL_DOCUMENT, + SEMATTRS_GRAPHQL_ERROR_CODES, SEMATTRS_GRAPHQL_ERROR_COUNT, + SEMATTRS_GRAPHQL_OPERATION_HASH, SEMATTRS_GRAPHQL_OPERATION_NAME, SEMATTRS_GRAPHQL_OPERATION_TYPE, SEMATTRS_HTTP_CLIENT_IP, @@ -30,90 +54,198 @@ 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 ?? ''); + 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, + // NOTE: we should make this configurable at some point. + variables: null, + 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, + ); + const operationName = operation.name?.value; + const document = defaultPrintFn(args.document); + + const hash = hashOperationFn?.({ ...args }); + if (hash) { + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_HASH, hash); + } + + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation); + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, operationName ?? ''); + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, document); + 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, - }); - } - - parseSpan.end(); - }, - }; + return trace.setSpan(input.ctx, span); +} + +export function setGraphQLParseAttributes(input: { + ctx: Context; + query?: string; + operationName?: string; + result: unknown; +}) { + const span = trace.getSpan(input.ctx); + if (!span) { + return; + } + + span.setAttribute(SEMATTRS_GRAPHQL_DOCUMENT, input.query ?? ''); + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_NAME, input.operationName ?? ''); + + if (input.result instanceof Error) { + span.setAttribute(SEMATTRS_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 +254,286 @@ 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); - } - } - - validateSpan.end(); - }, - }; +export function setGraphQLValidateAttributes(input: { + ctx: Context; + result: any[] | readonly Error[]; +}) { + const { result, ctx } = input; + const span = trace.getSpan(ctx); + if (!span) { + return; + } + + if (result instanceof Error) { + span.setStatus({ + code: SpanStatusCode.ERROR, + message: result.message, + }); + } else if (Array.isArray(result) && result.length > 0) { + span.setAttribute(SEMATTRS_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, ); - 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); - } - } - - executeSpan.end(); - }, - }; + span.setAttribute(SEMATTRS_GRAPHQL_OPERATION_TYPE, operation.operation); + span.setAttribute( + SEMATTRS_GRAPHQL_OPERATION_NAME, + operation.name?.value ?? '', + ); + span.setAttribute( + SEMATTRS_GRAPHQL_DOCUMENT, + defaultPrintFn(input.args.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) { + span.setAttribute( + SEMATTRS_GATEWAY_OPERATION_SUBGRAPH_NAMES, + input.subgraphNames, + ); + } + + if ( + !isAsyncIterable(result) && // FIXME: Handle async iterable too + result.errors && + result.errors.length > 0 + ) { + span.setAttribute(SEMATTRS_GRAPHQL_ERROR_COUNT, result.errors.length); + span.setStatus({ + code: SpanStatusCode.ERROR, + message: result.errors.map((e) => e.message).join(', '), + }); -export function createSubgraphExecuteFetchSpan(input: { - otelContext: Context; + const codes: string[] = []; + for (const error of result.errors) { + span.recordException(error); + codes.push(`${error.extensions['code']}`); // Ensure string using string interpolation + } + span.setAttribute(SEMATTRS_GRAPHQL_ERROR_CODES, codes); + } +} + +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_GRAPHQL_OPERATION_TYPE]: operation.operation, [SEMATTRS_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..f9bce85ed 100644 --- a/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts +++ b/packages/plugins/opentelemetry/tests/useOpenTelemetry.spec.ts @@ -1,131 +1,1232 @@ -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_GATEWAY_OPERATION_SUBGRAPH_NAMES, + SEMATTRS_GATEWAY_UPSTREAM_SUBGRAPH_NAME, + SEMATTRS_GRAPHQL_DOCUMENT, + SEMATTRS_GRAPHQL_ERROR_CODES, + SEMATTRS_GRAPHQL_ERROR_COUNT, + SEMATTRS_GRAPHQL_OPERATION_HASH, + SEMATTRS_GRAPHQL_OPERATION_NAME, + SEMATTRS_GRAPHQL_OPERATION_TYPE, + 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 { 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 Anonymous'], + }, + graphql: { + root: 'graphql.operation Anonymous', + 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 Anonymous', + 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: (otelPlugin) => { + const createSpan = + (name: string) => + ( + matcher: Parameters<(typeof otelPlugin)['getOtelContext']>[0], + ) => + otelPlugin + .getTracer() + .startSpan(name, {}, otelPlugin.getOtelContext(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) => + ({ context: gqlCtx, executionRequest }: any) => { + const ctx: OpenTelemetryContextExtension = + gqlCtx ?? executionRequest?.context; + return ctx.openTelemetry.tracer + .startSpan(name, {}, ctx.openTelemetry.activeContext()) + .end(); + }; + + 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 Anonymous') + .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 Anonymous', + 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: (otelPlugin) => { + const createSpan = (name: string) => () => + otelPlugin.getTracer().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 Anonymous': '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_GRAPHQL_OPERATION_HASH]: 'd40f732de805d03db6284b9b8c6c6f0b', + [SEMATTRS_GRAPHQL_ERROR_COUNT]: 1, + [SEMATTRS_GRAPHQL_ERROR_CODES]: ['DOWNSTREAM_SERVICE_ERROR'], + + // Execution Attributes + [SEMATTRS_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_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..b6ac0b85b --- /dev/null +++ b/packages/plugins/opentelemetry/tests/utils.ts @@ -0,0 +1,351 @@ +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, + OpenTelemetryPlugin, +} from '../src/plugin'; + +export async function buildTestGateway( + options: { + gatewayOptions?: Omit; + options?: OpenTelemetryGatewayPluginOptions; + plugins?: ( + otelPlugin: OpenTelemetryPlugin, + 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?.(otelPlugin, ctx) ?? []), + ]; + }, + logging: false, + ...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..1724dc524 --- /dev/null +++ b/packages/plugins/opentelemetry/tests/yoga.spec.ts @@ -0,0 +1,146 @@ +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 { 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); + }, + [Symbol.asyncDispose]: async () => { + await yoga.dispose(); + }, + }; + } + + const expected = { + http: { + root: 'POST /graphql', + children: ['graphql.operation Anonymous'], + }, + graphql: { + root: 'graphql.operation Anonymous', + 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 Anonymous', + 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: (otelPlugin) => { + const createSpan = + (name: string) => + ( + matcher: Parameters<(typeof otelPlugin)['getOtelContext']>[0], + ) => + otelPlugin + .getTracer() + .startSpan(name, {}, otelPlugin.getOtelContext(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 874c3c781..33b851bf8 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.5", "@graphql-mesh/utils": "^0.104.5", 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 2106be23a..e2d1cce23 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 5efed7971..739e19185 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", @@ -85,12 +87,16 @@ "@graphql-mesh/transport-rest": "^0.9.6", "@omnigraph/openapi": "^0.109.11", "@types/html-minifier-terser": "^7.0.2", + "@types/react": "^19", + "@types/react-dom": "^19", "@whatwg-node/fetch": "^0.10.8", "fets": "^0.8.4", "graphql": "^16.9.0", "graphql-sse": "^2.5.3", "html-minifier-terser": "7.2.0", "pkgroll": "2.14.3", + "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 e55adf7e5..c7782fb34 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 4f34a7c9a..6e5a0336b 100644 --- a/packages/runtime/src/plugins/useHiveConsole.ts +++ b/packages/runtime/src/plugins/useHiveConsole.ts @@ -1,7 +1,7 @@ import type { HivePluginOptions } from '@graphql-hive/core'; +import { LegacyLogger, type Logger } from '@graphql-hive/logger'; import { useHive } from '@graphql-hive/yoga'; import { process } from '@graphql-mesh/cross-helpers'; -import type { Logger } from '@graphql-mesh/types'; import { GatewayPlugin } from '../types'; export interface HiveConsolePluginOptions @@ -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..ae9dc5f1c 100644 --- a/packages/runtime/src/plugins/usePropagateHeaders.ts +++ b/packages/runtime/src/plugins/usePropagateHeaders.ts @@ -1,7 +1,6 @@ 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,7 +33,10 @@ 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))!; 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..bb9870a3a 100644 --- a/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts +++ b/packages/runtime/src/plugins/useSubgraphExecuteDebug.ts @@ -1,24 +1,27 @@ import { defaultPrintFn } from '@graphql-mesh/transport-common'; -import type { Logger } from '@graphql-mesh/types'; import { FetchAPI, isAsyncIterable } from 'graphql-yoga'; import type { GatewayPlugin } from '../types'; export function useSubgraphExecuteDebug< TContext extends Record, ->(opts: { logger: Logger }): GatewayPlugin { +>(): GatewayPlugin { let fetchAPI: FetchAPI; return { onYogaInit({ yoga }) { fetchAPI = yoga.fetchAPI; }, - onSubgraphExecute({ executionRequest, logger = opts.logger }) { - const subgraphExecuteHookLogger = logger.child({ + onSubgraphExecute({ executionRequest }) { + let log = executionRequest.context?.log.child({ subgraphExecuteId: fetchAPI.crypto.randomUUID(), }); - const subgraphExecuteStartLogger = subgraphExecuteHookLogger.child( - 'subgraph-execute-start', - ); - subgraphExecuteStartLogger.debug(() => { + if (!log) { + throw new Error('Logger is not available in the execution context'); + } + // we intentionally mutate the executionRequest context here + // to avoid losing the context ref and improve perf + executionRequest.context!.log = log; + log = log.child('[useSubgraphExecuteDebug] '); + log.debug(() => { const logData: Record = {}; if (executionRequest.document) { logData['query'] = defaultPrintFn(executionRequest.document); @@ -30,28 +33,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..3eb286818 100644 --- a/packages/runtime/src/plugins/useUpstreamTimeout.ts +++ b/packages/runtime/src/plugins/useUpstreamTimeout.ts @@ -54,11 +54,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 +61,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$; 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 ebda1cc81..432d92fe3 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, @@ -11,13 +12,14 @@ import type { HMACUpstreamSignatureOptions } from '@graphql-mesh/hmac-upstream-s 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 +37,7 @@ import type { Plugin as YogaPlugin, YogaServerOptions, } from 'graphql-yoga'; +import { GraphQLResolveInfo } from 'graphql/type'; import type { UnifiedGraphConfig } from './handleUnifiedGraphConfig'; import type { UseContentEncodingOpts } from './plugins/useContentEncoding'; import type { AgentFactory } from './plugins/useCustomAgent'; @@ -61,9 +64,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. */ @@ -96,7 +99,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 +115,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; @@ -481,9 +518,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. */ @@ -576,6 +614,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/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..2ed5143b3 100644 --- a/packages/runtime/tests/graphos.test.ts +++ b/packages/runtime/tests/graphos.test.ts @@ -1,9 +1,10 @@ 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'; @@ -20,7 +21,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 +38,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 +53,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 +69,7 @@ describe('GraphOS', () => { ); const result = Promise.resolve() - .then(() => unifiedGraphFetcher({})) + .then(() => unifiedGraphFetcher()) .catch(() => {}); await advanceTimersByTimeAsync(25); expect(mockFetchError).toHaveBeenCalledTimes(1); @@ -84,12 +85,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 +108,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 +147,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,20 +162,10 @@ 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(), ...configContext, }, @@ -186,6 +177,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 ad66c6a45..6fd5f39d0 100644 --- a/packages/runtime/tests/hive.spec.ts +++ b/packages/runtime/tests/hive.spec.ts @@ -97,18 +97,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 5cf4820a2..4ff26db9c 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 6605cc341..0cf6a73fd 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.14.3" diff --git a/packages/stitching-directives/package.json b/packages/stitching-directives/package.json index c8a280a23..cc6c11b93 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 c4ea62a98..6a1eb92c2 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.5", 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 175363b89..e21d6906a 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 614670d67..8c006dbbf 100644 --- a/packages/transports/http-callback/src/index.ts +++ b/packages/transports/http-callback/src/index.ts @@ -73,7 +73,7 @@ export default { transportEntry, fetch, pubsub, - logger, + log: rootLog, }): DisposableExecutor { let headersInConfig: Record | undefined; if (typeof transportEntry.headers === 'string') { @@ -106,7 +106,7 @@ export default { executionRequest: ExecutionRequest, ) { const subscriptionId = crypto.randomUUID(); - const subscriptionLogger = logger?.child({ + const log = rootLog.child({ executor: 'http-callback', subscription: subscriptionId, }); @@ -138,8 +138,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( @@ -202,7 +203,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]; @@ -224,7 +225,7 @@ export default { }, ), (e) => { - logger?.debug(`Subscription request failed`, e); + log.error(e, 'Subscription request failed'); stopSubscription(e); }, ); @@ -246,13 +247,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 1557af656..82c30990e 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/ws/package.json b/packages/transports/ws/package.json index 1b4068413..9841aabfd 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 2f937b45a..22a42fe48 100644 --- a/packages/transports/ws/src/index.ts +++ b/packages/transports/ws/src/index.ts @@ -33,7 +33,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 @@ -76,12 +76,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, @@ -91,30 +91,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 18494aa2e..1a4d3b828 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.14.3" }, diff --git a/tsconfig.json b/tsconfig.json index 583838cbd..f6b7d5897 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/vitest.projects.ts b/vitest.projects.ts new file mode 100644 index 000000000..33180dd9d --- /dev/null +++ b/vitest.projects.ts @@ -0,0 +1,62 @@ +import { defineWorkspace } from 'vitest/config'; +import { timeout as testTimeout } from './internal/e2e/src/timeout'; +import { boolEnv, isCI, isNotPlatform } from './internal/testing/src/env'; + +export default defineWorkspace([ + { + extends: './vitest.config.ts', + test: { + name: 'unit', + include: ['**/*.(test|spec).ts'], + }, + }, + { + extends: './vitest.config.ts', + test: { + name: 'e2e', + include: [ + '**/*.e2e.ts', + ...(isCI() && isNotPlatform('linux') + ? [ + // TODO: containers are not starting on non-linux environments + '!**/e2e/auto-type-merging', + '!**/e2e/neo4j-example', + '!**/e2e/soap-demo', + '!**/e2e/mysql-employees', + '!**/e2e/opentelemetry', + '!**/e2e/graphos-polling', + ] + : []), + ], + hookTimeout: testTimeout, + testTimeout, + retry: boolEnv('CI') ? 3 : 0, + }, + }, + { + extends: './vitest.config.ts', + test: { + name: 'bench', + hookTimeout: testTimeout, + testTimeout, + benchmark: { + include: [ + 'bench/**/*.bench.ts', + 'e2e/**/*.bench.ts', + '**/tests/**/*.bench.ts', + ], + reporters: ['verbose'], + outputJson: 'bench/results.json', + }, + }, + }, + { + extends: './vitest.config.ts', + test: { + name: 'memtest', + include: ['**/*.memtest.ts'], + hookTimeout: testTimeout, + testTimeout, + }, + }, +]); diff --git a/yarn.lock b/yarn.lock index 053eaf902..926f43c41 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1023,106 +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, @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" @@ -2610,7 +2510,17 @@ __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.3.3, @babel/types@npm:^7.4.4": +"@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: @@ -3239,6 +3149,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.10" "@graphql-mesh/hmac-upstream-signature": "workspace:^" "@graphql-mesh/plugin-jwt-auth": "workspace:^" @@ -3293,6 +3204,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/manual-transport-def@workspace:e2e/manual-transport-def": version: 0.0.0-use.local resolution: "@e2e/manual-transport-def@workspace:e2e/manual-transport-def" @@ -4189,6 +4106,22 @@ __metadata: languageName: node linkType: hard +"@graphql-hive/core@npm:0.13.0-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c": + version: 0.13.0-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c + resolution: "@graphql-hive/core@npm:0.13.0-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c" + dependencies: + "@graphql-tools/utils": "npm:^10.0.0" + "@whatwg-node/fetch": "npm:^0.10.6" + async-retry: "npm:^1.3.3" + js-md5: "npm:0.8.3" + lodash.sortby: "npm:^4.7.0" + tiny-lru: "npm:^8.0.2" + peerDependencies: + graphql: ^0.13.0 || ^14.0.0 || ^15.0.0 || ^16.0.0 + checksum: 10c0/c5c5a1b3d5d5c63274e81503d148cdba6549eb6cbef21ccd00cf801c71611b9e74193e6aa553e17a08d28ffeefc3299f416431d99799f3d51dc35a0e559aefe7 + languageName: node + linkType: hard + "@graphql-hive/gateway-runtime@workspace:*, @graphql-hive/gateway-runtime@workspace:^, @graphql-hive/gateway-runtime@workspace:packages/runtime": version: 0.0.0-use.local resolution: "@graphql-hive/gateway-runtime@workspace:packages/runtime" @@ -4196,8 +4129,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" @@ -4225,6 +4159,8 @@ __metadata: "@omnigraph/openapi": "npm:^0.109.11" "@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.8" "@whatwg-node/promise-helpers": "npm:^1.3.0" @@ -4237,6 +4173,8 @@ __metadata: graphql-yoga: "npm:^5.15.1" html-minifier-terser: "npm:7.2.0" pkgroll: "npm:2.14.3" + react: "npm:^19.1.0" + react-dom: "npm:^19.1.0" tslib: "npm:^2.8.1" tsx: "npm:4.20.3" peerDependencies: @@ -4255,6 +4193,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:^" @@ -4267,7 +4206,6 @@ __metadata: "@graphql-mesh/plugin-http-cache": "npm:^0.105.6" "@graphql-mesh/plugin-jit": "npm:^0.2.5" "@graphql-mesh/plugin-jwt-auth": "workspace:^" - "@graphql-mesh/plugin-mock": "npm:^0.105.6" "@graphql-mesh/plugin-opentelemetry": "workspace:^" "@graphql-mesh/plugin-prometheus": "workspace:^" "@graphql-mesh/plugin-rate-limit": "npm:^0.104.5" @@ -4285,9 +4223,22 @@ __metadata: "@graphql-tools/load": "npm:^8.0.14" "@graphql-tools/utils": "npm:^10.8.1" "@graphql-yoga/render-graphiql": "npm:^5.15.1" + "@opentelemetry/api": "npm:^1.9.0" + "@opentelemetry/api-logs": "npm:^0.202.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.202.0" + "@opentelemetry/sdk-logs": "npm:^0.202.0" + "@opentelemetry/sdk-metrics": "npm:^2.0.1" + "@opentelemetry/sdk-trace-base": "npm:^2.0.1" "@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" @@ -4328,52 +4279,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.5" - "@graphql-mesh/utils": "npm:^0.104.5" - cross-inspect: "npm:^1.0.1" - graphql: "npm:^16.9.0" - pkgroll: "npm:2.14.3" - 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": - version: 0.0.0-use.local - resolution: "@graphql-hive/logger-pino@workspace:packages/logger-pino" - dependencies: - "@graphql-mesh/types": "npm:^0.104.5" - "@graphql-mesh/utils": "npm:^0.104.5" - "@whatwg-node/disposablestack": "npm:^0.0.6" - graphql: "npm:16.11.0" - pino: "npm:^9.7.0" - pkgroll: "npm:2.14.3" - 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": +"@graphql-hive/logger@workspace:^, @graphql-hive/logger@workspace:packages/logger": version: 0.0.0-use.local - resolution: "@graphql-hive/logger-winston@workspace:packages/logger-winston" + resolution: "@graphql-hive/logger@workspace:packages/logger" dependencies: - "@graphql-mesh/types": "npm:^0.104.5" + "@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.14.3" - 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 @@ -4382,6 +4310,7 @@ __metadata: resolution: "@graphql-hive/nestjs@workspace:packages/nestjs" dependencies: "@graphql-hive/gateway": "workspace:^" + "@graphql-hive/logger": "workspace:^" "@graphql-mesh/types": "npm:^0.104.5" "@graphql-tools/utils": "npm:^10.8.1" "@nestjs/common": "npm:11.1.3" @@ -4627,6 +4556,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.5" @@ -4756,52 +4686,40 @@ __metadata: languageName: node linkType: hard -"@graphql-mesh/plugin-mock@npm:^0.105.6": - version: 0.105.7 - resolution: "@graphql-mesh/plugin-mock@npm:0.105.7" - dependencies: - "@graphql-mesh/cross-helpers": "npm:^0.4.10" - "@graphql-mesh/string-interpolation": "npm:^0.5.8" - "@graphql-mesh/types": "npm:^0.104.6" - "@graphql-mesh/utils": "npm:^0.104.6" - "@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/51703a20d7b82d64ce06cd7c223c007f82355e29c719ffe8c5af8f433bbce08b5f48856b6980ff51dc982a3dff2018c6f76eb4d9029d97dae2581cb6c013acff - 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-alpha-20250707104821-802de2e836e8fc0fb59533a3c4c63cf53e9fc33c" "@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.5" "@graphql-mesh/utils": "npm:^0.104.5" "@graphql-tools/utils": "npm:^10.8.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.202.0" + "@opentelemetry/auto-instrumentations-node": "npm:^0.60.1" + "@opentelemetry/context-async-hooks": "npm:^2.0.1" + "@opentelemetry/core": "npm:^2.0.1" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:^0.202.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:^0.202.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/resources": "npm:^2.0.1" + "@opentelemetry/sdk-logs": "npm:^0.202.0" + "@opentelemetry/sdk-node": "npm:^0.202.0" + "@opentelemetry/sdk-trace-base": "npm:^2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.34.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.14.3" + 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 @@ -4812,6 +4730,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.5" "@graphql-mesh/utils": "npm:^0.104.5" @@ -4920,6 +4839,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" @@ -5341,9 +5261,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" @@ -5351,7 +5271,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 @@ -5390,6 +5310,7 @@ __metadata: "@graphql-tools/delegate": "workspace:^" "@graphql-tools/executor": "npm:^1.4.7" "@graphql-tools/merge": "npm:^9.0.12" + "@graphql-tools/mock": "npm:^9.0.23" "@graphql-tools/schema": "npm:^10.0.11" "@graphql-tools/utils": "npm:^10.8.1" "@graphql-tools/wrap": "workspace:^" @@ -5461,6 +5382,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.11" "@graphql-tools/utils": "npm:^10.8.1" "@whatwg-node/promise-helpers": "npm:^1.3.0" @@ -6404,6 +6326,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" @@ -6856,21 +6785,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.202.0, @opentelemetry/api-logs@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/api-logs@npm:0.202.0" dependencies: "@opentelemetry/api": "npm:^1.3.0" - checksum: 10c0/1e514d3fd4ca68e7e8b008794a95ee0562a5d9e1d3ebb02647b245afaa6c2d72cc14e99e3ea47a1d1007f8a965c62bfb6170e1aa26756230bea063cfde2898bf + checksum: 10c0/2b6fd961c9303c5581367e7bef16a0cc7e7fc9a22ffae417ac7812151238760a9c0db20aad2c222c492cd8afc8353a611076bcac9272168d04b64e6386e71a66 languageName: node linkType: hard @@ -6881,366 +6801,1170 @@ __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.60.1": + version: 0.60.1 + resolution: "@opentelemetry/auto-instrumentations-node@npm:0.60.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/instrumentation-amqplib": "npm:^0.49.0" + "@opentelemetry/instrumentation-aws-lambda": "npm:^0.53.0" + "@opentelemetry/instrumentation-aws-sdk": "npm:^0.54.0" + "@opentelemetry/instrumentation-bunyan": "npm:^0.48.0" + "@opentelemetry/instrumentation-cassandra-driver": "npm:^0.48.0" + "@opentelemetry/instrumentation-connect": "npm:^0.46.0" + "@opentelemetry/instrumentation-cucumber": "npm:^0.17.0" + "@opentelemetry/instrumentation-dataloader": "npm:^0.19.0" + "@opentelemetry/instrumentation-dns": "npm:^0.46.0" + "@opentelemetry/instrumentation-express": "npm:^0.51.0" + "@opentelemetry/instrumentation-fastify": "npm:^0.47.0" + "@opentelemetry/instrumentation-fs": "npm:^0.22.0" + "@opentelemetry/instrumentation-generic-pool": "npm:^0.46.0" + "@opentelemetry/instrumentation-graphql": "npm:^0.50.0" + "@opentelemetry/instrumentation-grpc": "npm:^0.202.0" + "@opentelemetry/instrumentation-hapi": "npm:^0.49.0" + "@opentelemetry/instrumentation-http": "npm:^0.202.0" + "@opentelemetry/instrumentation-ioredis": "npm:^0.50.0" + "@opentelemetry/instrumentation-kafkajs": "npm:^0.11.0" + "@opentelemetry/instrumentation-knex": "npm:^0.47.0" + "@opentelemetry/instrumentation-koa": "npm:^0.50.1" + "@opentelemetry/instrumentation-lru-memoizer": "npm:^0.47.0" + "@opentelemetry/instrumentation-memcached": "npm:^0.46.0" + "@opentelemetry/instrumentation-mongodb": "npm:^0.55.1" + "@opentelemetry/instrumentation-mongoose": "npm:^0.49.0" + "@opentelemetry/instrumentation-mysql": "npm:^0.48.0" + "@opentelemetry/instrumentation-mysql2": "npm:^0.48.0" + "@opentelemetry/instrumentation-nestjs-core": "npm:^0.48.0" + "@opentelemetry/instrumentation-net": "npm:^0.46.1" + "@opentelemetry/instrumentation-oracledb": "npm:^0.28.0" + "@opentelemetry/instrumentation-pg": "npm:^0.54.0" + "@opentelemetry/instrumentation-pino": "npm:^0.49.0" + "@opentelemetry/instrumentation-redis": "npm:^0.49.1" + "@opentelemetry/instrumentation-redis-4": "npm:^0.49.0" + "@opentelemetry/instrumentation-restify": "npm:^0.48.1" + "@opentelemetry/instrumentation-router": "npm:^0.47.0" + "@opentelemetry/instrumentation-runtime-node": "npm:^0.16.0" + "@opentelemetry/instrumentation-socket.io": "npm:^0.49.0" + "@opentelemetry/instrumentation-tedious": "npm:^0.21.0" + "@opentelemetry/instrumentation-undici": "npm:^0.13.1" + "@opentelemetry/instrumentation-winston": "npm:^0.47.0" + "@opentelemetry/resource-detector-alibaba-cloud": "npm:^0.31.2" + "@opentelemetry/resource-detector-aws": "npm:^2.2.0" + "@opentelemetry/resource-detector-azure": "npm:^0.9.0" + "@opentelemetry/resource-detector-container": "npm:^0.7.2" + "@opentelemetry/resource-detector-gcp": "npm:^0.36.0" + "@opentelemetry/resources": "npm:^2.0.0" + "@opentelemetry/sdk-node": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.4.1 + "@opentelemetry/core": ^2.0.0 + checksum: 10c0/5417d264f2f4ff4eac25010e5a66fd1ee8cbef03da938352b5031f1fd2e2ab0390cbf237c19cd679efdf2e182f94234451bae8150eb316b965ec083de460d0f5 + 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/75b06f33b9c3dccb8d9802c14badcc3b9a497b21c77bf0344fc6231041ea1bf6a2bcc195cc27fafd5914bffcc7fa160b9f4480c06a37e86e876c98bf1a533a0d + languageName: node + linkType: hard + +"@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/393fa276262ecc0e7bd7db5f507a2118df0725afab0cea1cb071b8d0ec879c08d9d163a83bb13f77a6bd0ad0cb66094856eb19caf225da32d3b1767156105d26 + 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/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" +"@opentelemetry/context-zone@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/context-zone@npm:2.0.1" + dependencies: + "@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/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/semantic-conventions": "npm:1.28.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" peerDependencies: "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/4c25ba50a6137c2ba9ca563fb269378f3c9ca6fd1b3f15dbb6eff78eebf5656f281997cbb7be8e51c01649fd6ad091083fcd8a42dd9b5dfac907dc06d7cfa092 + checksum: 10c0/d587b1289559757d80da98039f9f57612f84f72ec608cd665dc467c7c6c5ce3a987dfcc2c63b521c7c86ce984a2552b3ead15a0dc458de1cf6bde5cdfe4ca9d8 + languageName: node + linkType: hard + +"@opentelemetry/exporter-jaeger@npm:^2.0.1": + version: 2.0.1 + resolution: "@opentelemetry/exporter-jaeger@npm:2.0.1" + dependencies: + "@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-trace-otlp-grpc@npm:^0.57.0": - version: 0.57.2 - resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.57.2" +"@opentelemetry/exporter-logs-otlp-grpc@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-logs-otlp-grpc@npm:0.202.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-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" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/sdk-logs": "npm:0.202.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/ac88a7978f231db1d327af0c26807f01230807d4f80a04119cc487aaafed287b9383e0511895d438b76ec35d66e0243ed5ad3eecdcf797caab5f2624c4e28bb4 + checksum: 10c0/b6a19d50807965f4767d777e82ef8ec8cbb910c18a5435918eabc7bee6c56c4363388a79820dac302217e5d0c59ba4317b59b6a2297e94248d8caca56f447398 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/exporter-logs-otlp-http@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-logs-otlp-http@npm:0.202.0" 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/api-logs": "npm:0.202.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/sdk-logs": "npm:0.202.0" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/2e606f25312d435e48a3e221cb26ac791f789ba68fa441dae83c0f8dd4bda22964a5cf75d39b548ae482a641f552332948010f782dfc6fe1641157a8b4ef02b0 + checksum: 10c0/4e891dc369de7f8d04b1a804efabf96871c7a7bebb03a1856628c786677716a8ebcb6515e0971058d84965130e16a5202e9ed2512e5ff4f114d92d18d93ef2fd 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-logs-otlp-proto@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-logs-otlp-proto@npm:0.202.0" 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/api-logs": "npm:0.202.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.202.0" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/53dc519e923bc738273a61a352988b0bfc3dee2b8138be1d0521401556fb47791e1d5eb2a90f5e94567399d1e7f484839772b7be4212b840aa014172bb9c0d1e + checksum: 10c0/cc1724d344dfce533e99286cab5345545a11b7519e73a6d78f6cb5a782831da194140b3dc8e8cb4f8d372d2f7df531329223ed7f35169ded76b08d72f8ec045c 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-metrics-otlp-grpc@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-metrics-otlp-grpc@npm:0.202.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" + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.202.0" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" peerDependencies: - "@opentelemetry/api": ^1.0.0 - checksum: 10c0/e2918387c0ddc1835779f5551f5f04b3c1331643f05e3cf6019abf169de035c55be8112a30d5ee0dfef6938cbe260e84c426d5a46bce6f9eb6b2c9b916a425f6 + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/a310f4e72b0d6535cc07bafc6286489b61e8d1b919e6484d704aef01969378e42cac96e7b3cc9aabfe5e08d9303dca76d221624f9002f9614d3871f3effbe90d languageName: node linkType: hard -"@opentelemetry/instrumentation@npm:^0.57.0": - version: 0.57.2 - resolution: "@opentelemetry/instrumentation@npm:0.57.2" +"@opentelemetry/exporter-metrics-otlp-http@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-metrics-otlp-http@npm:0.202.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.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/79ca65b66357665d19f89da7027da25ea1c6b55ecdacb0a99534923743c80deb9282870db563de8ae284b13e7e0aab8413efa1937f199deeaef069e07c7e4875 + checksum: 10c0/a1f4d2284d2fc72205a24b0d4a5720788fe1df7b4af56c4fc222eccd36cb3ebd50aadbc85c52a21708dd9ce8cd7b506fcd2f22434f653d68927bc7ce7a4b96c9 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/exporter-metrics-otlp-proto@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-metrics-otlp-proto@npm:0.202.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.202.0" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-metrics": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/1c7062bf9edf2a012826341a41fe7bd03b03835a3dc5be5a43a85f5d5ad5d474a3f9241e12906cf9454df6e10f8039d97b21645afdc5f1fb3f55b371a1ed9bd6 + checksum: 10c0/7be21fe5b1b6685caad5e1cd178ad9a30add2a90ed910c33e050b06285bc709f709c4b9db34ad842d2c6df9a68f9f48531a9d45b7982733d14b278b08daabf37 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": - 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" +"@opentelemetry/exporter-prometheus@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-prometheus@npm:0.202.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/otlp-transformer": "npm:0.56.0" + "@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/2c388896ad67fab82944ffd42f4349fb488e6065a2168270a06efd469359bcc775e87e2c2448e45fa4ecfd45d8bd7bc15ee272684308d76c63483dd5c90a945f + checksum: 10c0/6115cb6ae9634294a4f4422d92710dd01d9e049da5a0587fa5d42b5d49991111c0a95075ec8804a7aa7dd42e2c2580e425d63bb31a952af4478175b8b4e92576 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/exporter-trace-otlp-grpc@npm:0.202.0, @opentelemetry/exporter-trace-otlp-grpc@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-trace-otlp-grpc@npm:0.202.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.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-grpc-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/4a5ced2720061a6833b8210e0c606211393da98e0701087dc803c333e04a186550224408c7d82960c6cc07c96b1ab9c16b918cafdc9b77c27bc2f3781b6de6b1 + checksum: 10c0/b997c0ee2704ffddffd0a2f9204ea9f4963cecd8d39027082e0c105b6fc8b8e3679bbff835f81f4b5997b0846e57ce4a6d2a3fb241b03a3a37a8c90a53e9440b languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/otlp-transformer@npm:0.56.0" +"@opentelemetry/exporter-trace-otlp-http@npm:0.202.0, @opentelemetry/exporter-trace-otlp-http@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-trace-otlp-http@npm:0.202.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" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/898bae14a8f84a2f5a8d3a3b89966a5d3af1102b273dac748e404651ad113876b095faf1528055fce65de99dc33bc764cd650f21019ada0f85926f7606dc377e + checksum: 10c0/9cc7c2d65699f8634a0b39f0a9ea89ef4114562f57f0fb08a986cb92a2ba1fed9085ecabf3d3cafe95d311579a61fba4cf0a95cddc09df3990bc35cf5226098b languageName: node linkType: hard -"@opentelemetry/otlp-transformer@npm:0.57.2": - version: 0.57.2 - resolution: "@opentelemetry/otlp-transformer@npm:0.57.2" +"@opentelemetry/exporter-trace-otlp-proto@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/exporter-trace-otlp-proto@npm:0.202.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" - protobufjs: "npm:^7.3.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-trace-base": "npm:2.0.1" peerDependencies: "@opentelemetry/api": ^1.3.0 - checksum: 10c0/094979421768c5ac0672d1ce62bbc710a8cc836eb24e1cdfe5fb2c5c55908d19cf35fd6810cd266e7444d5677087846d5a8959df5886dfe1774199a3ae1d50a4 + checksum: 10c0/c65405d18e6f04f48beac58c72a23eca47814c741ee8b997f2413adc8ee6c1fd76efe920349c0180cf4a0ee0f00599c6f217d364be19eef9d98ee84b7feb867e languageName: node linkType: hard -"@opentelemetry/resources@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/resources@npm:1.29.0" +"@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:1.29.0" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@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 <1.10.0" - checksum: 10c0/10a91597b2ae92eeeeee9645c8147056b930739023bde4f18190317f7e8a05acd9e440b29d04be3580f7af4ffe5ff629d970264278f86574c429685f4804a006 + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/10e0ad1dfb967c951d26bc64647e2f7d0705fdcf82449473308f277e1866552a07d7636bcf198e21662ada93df2366c4f24aec2d329d18e59f3d09ddcf65969d 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/instrumentation-amqplib@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-amqplib@npm:0.49.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/d691090002ca32813eb99ba0748ea1d5ef1957283326a874ee298ad634eee2bb8bb4ec26fa72605d70a4aa972e35389d5ca6b12851d8bef492574fa08c6ce147 + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/5c60b11065709ee4b88d70c6934e12c4da5aa6a18001cd4134b69279509ba7bc9fd409ff9167104019b72761924da8ba8aade2c52e78ecc3a37cc1d2fdb48d25 languageName: node linkType: hard -"@opentelemetry/sdk-logs@npm:0.56.0": - version: 0.56.0 - resolution: "@opentelemetry/sdk-logs@npm:0.56.0" +"@opentelemetry/instrumentation-aws-lambda@npm:^0.53.0": + version: 0.53.0 + resolution: "@opentelemetry/instrumentation-aws-lambda@npm:0.53.0" dependencies: - "@opentelemetry/api-logs": "npm:0.56.0" - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/aws-lambda": "npm:8.10.147" peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/abd5584c8d98a71bfefdca4b864f69714d0e638c5ad9fe4819744b938aea362c9602b884da1da24ae394bccbe044246463a208e775b3ac4718eadaff7fac295d + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/57ba724a66fadbcd70095563cf1c4890fa2d29092b8b396bcd7d431b5b6c31075e077f4834c3a862b15a1d746b742f9503f786bff41363123bdd55b251084027 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/instrumentation-aws-sdk@npm:^0.54.0": + version: 0.54.0 + resolution: "@opentelemetry/instrumentation-aws-sdk@npm:0.54.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/instrumentation": "npm:^0.202.0" + "@opentelemetry/propagation-utils": "npm:^0.31.2" + "@opentelemetry/semantic-conventions": "npm:^1.31.0" peerDependencies: - "@opentelemetry/api": ">=1.4.0 <1.10.0" - checksum: 10c0/dda61cf656a93d2f5ef1ca0495db59bfa33efc8ca7ee11018850a9ff78ee0459fb0393e70be7ae5d3cd084e0652d36fbf5778c7b3e9028c6668f9bf0d6c9473e + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/860ab7f74c7a5d87d8977d068ce6057624705f7f7bb0e62c2aa04319b58ed9850026cfec2a1f314eac3eba8575bb86a8800d819bac3fae7c31220444c0d1b0bd languageName: node linkType: hard -"@opentelemetry/sdk-metrics@npm:1.29.0": - version: 1.29.0 - resolution: "@opentelemetry/sdk-metrics@npm:1.29.0" +"@opentelemetry/instrumentation-bunyan@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-bunyan@npm:0.48.0" dependencies: - "@opentelemetry/core": "npm:1.29.0" - "@opentelemetry/resources": "npm:1.29.0" + "@opentelemetry/api-logs": "npm:^0.202.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@types/bunyan": "npm:1.8.11" peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/4fca3b43fc9e9d139e87e18abf91069ba09c110bd4aa093e97ae02cd9af2cc82560f38dd4e3a6c76b054e1f8cbe5801faada2d24d7673a0ab15bf69923d6202c + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f7e568dec0fda807e028c1a34f534dad12898b445226a7ac4bbb19eb8f5e3b92157a2a3eafec3df19066a11687f47c4521186b162975a45ab6c125f83e64ee85 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/instrumentation-cassandra-driver@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-cassandra-driver@npm:0.48.0" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: - "@opentelemetry/api": ">=1.3.0 <1.10.0" - checksum: 10c0/7e60178e61eaf49db5d74f6c3701706762d71d370044253c72bb5668dba3a435030ed6847605ee55d0e1b8908ad123a2517b5f00415a2fb3d98468a0a318e3c0 + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/d61d95d6c695a2ec39fcd3fcc1b8aa2a12524537c85b5d3d2dcbc2d9c16cac0a103772fa9a68f4c1fd4fe2c842b64ede86a72ae2643a127382e404ac235d807f 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/instrumentation-connect@npm:^0.46.0": + version: 0.46.0 + resolution: "@opentelemetry/instrumentation-connect@npm:0.46.0" 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.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/connect": "npm:3.4.38" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/870f29d3d72f4d6cbcaada328b544aa111527d72f0818f89bc5661b0427a37618db939cc65e834c8c73bad744665f9ac6dc0ec48276b113b5d4a0913c2b8fece + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/59c0dd3232664c2ab55fa4da6dd0c845c007d3afa8f0ad9acf4eb21c1a4be0d5f5bce0e245646cedc2c0874c9a19a3e67989d6d521889d6c103e902cb17ea390 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/instrumentation-cucumber@npm:^0.17.0": + version: 0.17.0 + resolution: "@opentelemetry/instrumentation-cucumber@npm:0.17.0" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/resources": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/77019dc3efaeceb41b4c54dd83b92f0ccd81ecceca544cbbe8e0aee4b2c8727724bdb9dcecfe00622c16d60946ae4beb69a5c0e7d85c4bc7ef425bd84f8b970c + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/b066b511ba47ed678b2264ff7e9080fc1bff1cb6751fc1a7e25c5ad79f3f4876128b4f92e22874449fa77d3bc9b1b212a8c8725373bc213b0912a52392ab3c05 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/instrumentation-dataloader@npm:^0.19.0": + version: 0.19.0 + resolution: "@opentelemetry/instrumentation-dataloader@npm:0.19.0" dependencies: - "@opentelemetry/core": "npm:1.30.1" - "@opentelemetry/sdk-trace-base": "npm:1.30.1" - "@opentelemetry/semantic-conventions": "npm:1.28.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" peerDependencies: - "@opentelemetry/api": ">=1.0.0 <1.10.0" - checksum: 10c0/8dd2901b5eef68a5896da0ad11f04c8990ce4ef2dcbec27bbc02d7e193097c270ba5f4c9ca363ea10fb53ca7cc515f18d9dc383a69a17720cd0590474c0ffdaf + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f2b1f9b7fcc24b1270580044c194355b89aeb16c5cc6ddae1d6087d35bc50496dfe9eeeac70d1668a5dd5f152217c70dc049d98a01607fdc5356696b6e3f832c 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/instrumentation-dns@npm:^0.46.0": + version: 0.46.0 + resolution: "@opentelemetry/instrumentation-dns@npm:0.46.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/212404c6f9951130abc7cb3a6d2557941f61c0e88627756e1f4fbc3fdb21394dacf6acefbef800c45693e0cc207cd3f4b6068ea3568de45405286ce3d3973414 languageName: node linkType: hard -"@opentelemetry/semantic-conventions@npm:^1.28.0, @opentelemetry/semantic-conventions@npm:^1.30.0": - version: 1.34.0 - resolution: "@opentelemetry/semantic-conventions@npm:1.34.0" - checksum: 10c0/a51a32a5cf5c803bd2125a680d0abacbff632f3b255d0fe52379dac191114a0e8d72a34f9c46c5483ccfe91c4061c309f3cf61a19d11347e2a69779e82cfefd0 +"@opentelemetry/instrumentation-express@npm:^0.51.0": + version: 0.51.0 + resolution: "@opentelemetry/instrumentation-express@npm:0.51.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2befd056cf0f9b3c19d8a990eef74fb2e9f32c191a7d0cbd36b0ce6a1322faf2ff99fe7da94964ceecddd3653072e88902403097b6744e0a9589673c383d8b26 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" - conditions: os=darwin & cpu=arm64 +"@opentelemetry/instrumentation-fastify@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-fastify@npm:0.47.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/8b4d2d6fff7b1888933598efbb9e8fc4fcc8a9f6191493387975d7b1cf06254c680de28910ab10141ba07e183bb941a11c407fca02718e442d2c80fd05a772ce languageName: node linkType: hard -"@oven/bun-darwin-x64-baseline@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-darwin-x64-baseline@npm:1.2.18" - conditions: os=darwin & cpu=x64 +"@opentelemetry/instrumentation-fs@npm:^0.22.0": + version: 0.22.0 + resolution: "@opentelemetry/instrumentation-fs@npm:0.22.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/ec97db2c182e2f205cf56f3e214fef291ad7e5ffea6f08ec38c0c68cffa4b8d9db886e299cb13c140381a5ee6d37497d45f875cbf424462fe8da3f581c6b2f90 languageName: node linkType: hard -"@oven/bun-darwin-x64@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-darwin-x64@npm:1.2.18" - conditions: os=darwin & cpu=x64 +"@opentelemetry/instrumentation-generic-pool@npm:^0.46.0": + version: 0.46.0 + resolution: "@opentelemetry/instrumentation-generic-pool@npm:0.46.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/cd60f28323d00f45727748f4052d69327d1b595d89a30c2fe001ba4b375d8cb63667ea48cbef0a3ec4dc67b20f82bb110bd0d98b7210e1cc95f9d22306ca591e languageName: node linkType: hard -"@oven/bun-linux-aarch64-musl@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-aarch64-musl@npm:1.2.18" - conditions: os=linux & cpu=aarch64 +"@opentelemetry/instrumentation-graphql@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-graphql@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/3db096518d7b0a480a8bcfc6178247c1bee0ba7f22e1f083176ae2f50f0466311863e2a5cd8af32815da4da512170b51f3e3b41f0f70506868aa02288b8cc0db languageName: node linkType: hard -"@oven/bun-linux-aarch64@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-aarch64@npm:1.2.18" - conditions: os=linux & cpu=arm64 +"@opentelemetry/instrumentation-grpc@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-grpc@npm:0.202.0" + dependencies: + "@opentelemetry/instrumentation": "npm:0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/53461cb750b18469ed5c121d5093038550a84c35e4a56677b2da7ea513f52fe8cb0fe870e6cb1fd83d516384cd33bd60327318ae71922284d5dd72d9487fd889 languageName: node linkType: hard -"@oven/bun-linux-x64-baseline@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-x64-baseline@npm:1.2.18" - conditions: os=linux & cpu=x64 +"@opentelemetry/instrumentation-hapi@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-hapi@npm:0.49.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/867de41fd1fb8bf2af1fd90e2ee105aa0da65db4ead02e21fb519b09e0218fa07c617245af538689abb742a091e4ca74aa65220e4e4a06e4bf72c00b1e70bdb0 languageName: node linkType: hard -"@oven/bun-linux-x64-musl-baseline@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-x64-musl-baseline@npm:1.2.18" - conditions: os=linux & cpu=x64 +"@opentelemetry/instrumentation-http@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation-http@npm:0.202.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + forwarded-parse: "npm:2.1.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/ba221223573bdfa7011292cd373bbaf68e1236887510bb051a8a61d7e72c0aed95d59f0031169d539289032f9ad5f6e50a7e0e57236f66f10e9f1ce2f83020f3 languageName: node linkType: hard -"@oven/bun-linux-x64-musl@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-x64-musl@npm:1.2.18" - conditions: os=linux & cpu=x64 +"@opentelemetry/instrumentation-ioredis@npm:^0.50.0": + version: 0.50.0 + resolution: "@opentelemetry/instrumentation-ioredis@npm:0.50.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/redis-common": "npm:^0.37.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/4400a774ea243ee5bdf9f04d892c9d8fa826bfa11fce360e2adac4683b46272bc17c2987521b928778d943f5617af915f5790281ecc0d94bdbf69146835e4978 languageName: node linkType: hard -"@oven/bun-linux-x64@npm:1.2.18": - version: 1.2.18 - resolution: "@oven/bun-linux-x64@npm:1.2.18" - conditions: os=linux & cpu=x64 - languageName: node - linkType: hard +"@opentelemetry/instrumentation-kafkajs@npm:^0.11.0": + version: 0.11.0 + resolution: "@opentelemetry/instrumentation-kafkajs@npm:0.11.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/d2fcce3ff69b22b000054f8bdd89cbf05bd212a4e3c41f88ae1cdc39c31499e044b43184697a993e28cbd0f70b41241e9619660b48415c984405e4639c53e641 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-knex@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-knex@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.33.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/c031b3d18df48f103b706dd178b70cd20addd9d6b3c801fa7c0cfa216d82ccdfd3db009533734c356003f730207076755f8472fa43f27df0df6e99a244367418 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-koa@npm:^0.50.1": + version: 0.50.1 + resolution: "@opentelemetry/instrumentation-koa@npm:0.50.1" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/5c17e9f892d652f9d3b014c1b72dab9e4ed79c605c27284d7523d7f5fb328744baff6eefe9c39361cda351ac80ac32c3520faf18530c18bdf2b18d72f80cb490 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-lru-memoizer@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-lru-memoizer@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/785e957fdda1b4c754f2344557a7da2490a09520e17e739c2402c1a3d551e3715da76351035173f62362ff35dde5eab09b103508b049018ffb106460e0f37b63 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-memcached@npm:^0.46.0": + version: 0.46.0 + resolution: "@opentelemetry/instrumentation-memcached@npm:0.46.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/memcached": "npm:^2.2.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/4309cfcbfa83848cf203ea2eddcfea5ad85b4c4e33c9150e4ccb8f951b8f12f30032879ddd3474934ea772ddb3f31fa86f0c541afa47862a9dc4502cb631c723 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongodb@npm:^0.55.1": + version: 0.55.1 + resolution: "@opentelemetry/instrumentation-mongodb@npm:0.55.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/e5880c15518f9ffee86dc3051b09be08889c8b5e423e2e2bd2cc13e72921b1052356b35767926af5aedbc73ca93848ec5484e6998223afed514c95f2cf84860c + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mongoose@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-mongoose@npm:0.49.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/53164c59e51eba2340015e521c819e602e9ba0a87bb49b2ad2106ea3c3bce0b4a18e0bc124e4a942ce640a790e622e725e9a4dae3277cd9a2b465768ad2eeb89 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql2@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-mysql2@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/0f68ff719695c7a2055b3419053309493a278b09e00709ed016920b1b99bc4f4991a62434643d93ad1da20c5c062c5e0d45c1e3a55e411ef106c43629ac3b275 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-mysql@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-mysql@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/mysql": "npm:2.15.26" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/b807d6f35fa9a1cb16e8fa1857d6a3946fb2453c091979ea5f7f98cc66602655d2c6f9cd59e004b38557f3c1de8d3660d2119547c3e022b9ed3147840f586149 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-nestjs-core@npm:^0.48.0": + version: 0.48.0 + resolution: "@opentelemetry/instrumentation-nestjs-core@npm:0.48.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.30.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/77f742ed1d3827e99608f9ad17409ad64f3e52b8b2903b2fcbc9e4c7ba61a76dfed2e6b53f842fa0e5373147a1d6e4f92d5708ba49bec31c3f9bc7bf76cd4d07 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-net@npm:^0.46.1": + version: 0.46.1 + resolution: "@opentelemetry/instrumentation-net@npm:0.46.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/1bb22530c8a01d572cc6a874f9f0bde3b93b2082d5900464ff01f15bbbf8031c42fdbba8a16068c408a31d56de9b160abe48ca9002b8a550f7f46ead892ac34d + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-oracledb@npm:^0.28.0": + version: 0.28.0 + resolution: "@opentelemetry/instrumentation-oracledb@npm:0.28.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/oracledb": "npm:6.5.2" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/3eb4f369239919e1c93aadd7f4ed708425e9680cc6265c6f37a3f071ef9db74a9f11b85a6692d19e42a84cf65fde6638c72320f3332ef637d7315ba3591b6112 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pg@npm:^0.54.0": + version: 0.54.0 + resolution: "@opentelemetry/instrumentation-pg@npm:0.54.0" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@opentelemetry/sql-common": "npm:^0.41.0" + "@types/pg": "npm:8.15.1" + "@types/pg-pool": "npm:2.0.6" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/2fc36b58ecb507617deb96bb572e869f13020d90c7985fab696c76589999eb9b5ee4ec89d4cb2c66c8549ef13ead7659b3d37bfbb2da75926e4c81a7f4429a16 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-pino@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-pino@npm:0.49.0" + dependencies: + "@opentelemetry/api-logs": "npm:^0.202.0" + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/9ee664bfd308ad62f139f5edd8c0a789de8061d23b5958f3e0e622f0ce87b3de129b56ba03c40436e82f89c3c65ff032be825b67cad173e6c257f77b5300d1e9 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-redis-4@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-redis-4@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/redis-common": "npm:^0.37.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f16e2d49a18d537c5b226151b6b1e46951c1433bf6b307ca7ae5ce09a25738af40448d2289125001c667d047f9e3ef5c7bf0b87e609a7af7e92e5eb42e729206 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-redis@npm:^0.49.1": + version: 0.49.1 + resolution: "@opentelemetry/instrumentation-redis@npm:0.49.1" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/redis-common": "npm:^0.37.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/97a463db65bcc37d12781e4691d0febee6e50f486b40a304ba4fc67ed8f92d3d5ef191766071142d789ff995e33de8a87f2214b162bdc98eee28e80bf8ed9257 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-restify@npm:^0.48.1": + version: 0.48.1 + resolution: "@opentelemetry/instrumentation-restify@npm:0.48.1" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/82861ecc695b0f2fa3e2544a2bfefe1808a24ac75e9d1631b12822266ed95193d06028a5134633e8cc13593025908cd5324d7d2ac636409f98338c382cef4dc4 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-router@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-router@npm:0.47.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f3259d50d5acc029440ccc310a1a0f404e73fe0cbd0338c113e1140ec27197696e85dd1163da87f1451c2da010dbe43f43b9c86a11343c66dbfa464fe18bafb2 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-runtime-node@npm:^0.16.0": + version: 0.16.0 + resolution: "@opentelemetry/instrumentation-runtime-node@npm:0.16.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/1ad58386693f363c2807daf074188f3ce36e37babb657477a667af2a4e8552e9965e0d78fc1e5435acd7d0e2be6c12994fa74ea0462af300b7b51832796155b3 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-socket.io@npm:^0.49.0": + version: 0.49.0 + resolution: "@opentelemetry/instrumentation-socket.io@npm:0.49.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/527491f9c971c7aa3ce078cc10337c03d651539832d7f8cf9c3cae1ef57c46dc1827bd75857828ef2aecee8902f44eea09ae224f214c7557cc2b5fd175569dbe + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-tedious@npm:^0.21.0": + version: 0.21.0 + resolution: "@opentelemetry/instrumentation-tedious@npm:0.21.0" + dependencies: + "@opentelemetry/instrumentation": "npm:^0.202.0" + "@opentelemetry/semantic-conventions": "npm:^1.27.0" + "@types/tedious": "npm:^4.0.14" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/727ce27d4a9db08eb17fc4de9fd1e1a57fcfdecee30e4132313c077e1ea68993048dee9c0c0dea031e68cf34778742232a0a809a6f79da0dc0bd14e36976ea0f + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-undici@npm:^0.13.1": + version: 0.13.1 + resolution: "@opentelemetry/instrumentation-undici@npm:0.13.1" + dependencies: + "@opentelemetry/core": "npm:^2.0.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.7.0 + checksum: 10c0/9e02b509eb0654b0bbc68c4cf6f0e9503fa259104ea7f71e54446bc68e180a09783209871d771f393152671a9db4d2b85a7ed4996070c3897a00f36ea58b9bd4 + languageName: node + linkType: hard + +"@opentelemetry/instrumentation-winston@npm:^0.47.0": + version: 0.47.0 + resolution: "@opentelemetry/instrumentation-winston@npm:0.47.0" + dependencies: + "@opentelemetry/api-logs": "npm:^0.202.0" + "@opentelemetry/instrumentation": "npm:^0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/55b7c78bd71e38dfccfe23976b855f0dcafba218958f987b087f576076e1966ae4df3be436de7818359987409b8ebee7fb7758a09ab0f0fdbd6ed563b2359aad + languageName: node + linkType: hard + +"@opentelemetry/instrumentation@npm:0.202.0, @opentelemetry/instrumentation@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/instrumentation@npm:0.202.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.202.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/3edac99d3093841bcd6008c19353f989042231485bcfe93c069e6ac41f35ab11800998275c0e3a85873656a21af982b8fd8721da974288a4ac653ecae8407641 + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-exporter-base@npm:0.202.0" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/800047f125988a55da6e7cb732a5a000e39fa707f4f40e86976a50b72e93ccee9cb6d75a62ea9ca4e494233fc21812fb5cffd0de581fe9df478ea68f005230ca + languageName: node + linkType: hard + +"@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm%3A0.202.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch": + version: 0.202.0 + resolution: "@opentelemetry/otlp-exporter-base@patch:@opentelemetry/otlp-exporter-base@npm%3A0.202.0#~/.yarn/patches/@opentelemetry-otlp-exporter-base-npm-0.202.0-f6f29c2eeb.patch::version=0.202.0&hash=e59ded" + dependencies: + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/72130f5a45d0a0c9d3b1b0f1943827672519cfdbf354076bda9fdd92ed808c94c9dae5a73fcf9e881c5c833a92cbcad1ce63063e1df9b09a90943951b7d253de + languageName: node + linkType: hard + +"@opentelemetry/otlp-grpc-exporter-base@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-grpc-exporter-base@npm:0.202.0" + dependencies: + "@grpc/grpc-js": "npm:^1.7.1" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/otlp-exporter-base": "npm:0.202.0" + "@opentelemetry/otlp-transformer": "npm:0.202.0" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/f04e0a5d3935f4aaafceae5795dd7e9ba3e5df3f327b76d483c7855f68ae675f4455e90bae47b895bc58b840684e130220ddd3475fb78b4ec80ef45cb7b70311 + languageName: node + linkType: hard + +"@opentelemetry/otlp-transformer@npm:0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/otlp-transformer@npm:0.202.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.202.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/sdk-logs": "npm:0.202.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/33cd518ba39904d9a6baf2486efc61f979d8e27a2f0418b6f908f54c9b93412dac1bca1571e1c7f8ec8a08c3c74ab5bc11de8aa74c1a99f80dcab4ef5c7b28c8 + languageName: node + linkType: hard + +"@opentelemetry/propagation-utils@npm:^0.31.2": + version: 0.31.2 + resolution: "@opentelemetry/propagation-utils@npm:0.31.2" + peerDependencies: + "@opentelemetry/api": ^1.0.0 + checksum: 10c0/c184fdb11199759575c9afff4babeabf36de4be4748cf8649828aefdfa4de6a8df11c302605896005c34130b2272c233e58e176ff2bf63db1cb5482e4f881b12 + 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:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/79dbfaaa867f4f71a22ab640848f797ef9789fd94fc824ca4e38f298968a3f559a895fc228a17f09b1e06ec88cbf0b1f3cadc480ea76848504c7364693fd30ca + languageName: node + linkType: hard + +"@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:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.0.0 <1.10.0" + checksum: 10c0/e21df109b831a7efffe54459bb5da35be05eeb72581017f0ce40dee2ab98b3e8063602894a477f6c593ad1bd3a1ead36adfceee21eb2472ca88050d49f056154 + languageName: node + linkType: hard + +"@opentelemetry/redis-common@npm:^0.37.0": + version: 0.37.0 + resolution: "@opentelemetry/redis-common@npm:0.37.0" + checksum: 10c0/8e754164c800c509504f07bb38b8bbf2890eafde65bb5a5ffb8227e1b234cf84bc294671416f27504d90bfd22f334f671db2d7cd146f852f0b6dc7c8d36798c0 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-alibaba-cloud@npm:^0.31.2": + version: 0.31.2 + resolution: "@opentelemetry/resource-detector-alibaba-cloud@npm:0.31.2" + 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/46974743ec5f731057b5e524ce01fcbaaa34775ece76bb7eedca79ce7dae5a87737208f58712bca2cf67772640a711b80a13a08bf186cb38baf61c9a3b6cf9c9 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-aws@npm:^2.2.0": + version: 2.2.0 + resolution: "@opentelemetry/resource-detector-aws@npm:2.2.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/67a21804281b3106a75e029aa1016d8db993eeb857024b1a65841d2c6ae8692b733fe6a75f16131fe5d75b08a517ba5eac0f07d026c068b09ec1c29bbd9c36b7 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-azure@npm:^0.9.0": + version: 0.9.0 + resolution: "@opentelemetry/resource-detector-azure@npm:0.9.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/3966148393536e5af66a58f0fde4d6ae8cb64bb013ed18eaad73b68845e20e0cfbe812cf2b9ac7d01d5206857ba869fca54abd1e6f86745a3951038addd3a928 + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-container@npm:^0.7.2": + version: 0.7.2 + resolution: "@opentelemetry/resource-detector-container@npm:0.7.2" + 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/298d2c3955f7f6f604ccfd1ad34fde2c4f50b1cdf1e916033c114c8001c02bc3746791395c6c1aedc024aec45973b0ff95518e743ff041af21d1b0c81d3ffc8a + languageName: node + linkType: hard + +"@opentelemetry/resource-detector-gcp@npm:^0.36.0": + version: 0.36.0 + resolution: "@opentelemetry/resource-detector-gcp@npm:0.36.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/57c9c1fea896667253ef95034f25a60afd59afa3d376137288b2df70b23e68ef97cb4bd347fff79710226e1b40fee0a3d8c8c96dc58bd73c5c60c2d43d7462fc + languageName: node + linkType: hard + +"@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:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/96532b7553b26607a7a892d72f6b03ad12bd542dc23c95135a8ae40362da9c883c21a4cff3d2296d9e0e9bd899a5977e325ed52d83142621a8ffe81d08d99341 + languageName: node + linkType: hard + +"@opentelemetry/sampler-jaeger-remote@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/sampler-jaeger-remote@npm:0.202.0" + dependencies: + "@opentelemetry/sdk-trace-base": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ^1.3.0 + checksum: 10c0/dc1fa837c989dedeb2ff2ffc9b123ee6d5be9afaf9276781decb3f4c66fb7c75aa9dbd19c23a3a6c2ebbf684da1152a5edb8241ec08536ab6551ff08c97b4b27 + languageName: node + linkType: hard + +"@opentelemetry/sdk-logs@npm:0.202.0, @opentelemetry/sdk-logs@npm:^0.202.0": + version: 0.202.0 + resolution: "@opentelemetry/sdk-logs@npm:0.202.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.202.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + peerDependencies: + "@opentelemetry/api": ">=1.4.0 <1.10.0" + checksum: 10c0/02659eb5da445f7740eafd79dfebefd38a4265751b7b9cb78c7231dcb2e3b2b9d073dff16abbb0da7cff0837be880dc5b581118601c7630f976a4ccf9db62ab2 + 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.202.0": + version: 0.202.0 + resolution: "@opentelemetry/sdk-node@npm:0.202.0" + dependencies: + "@opentelemetry/api-logs": "npm:0.202.0" + "@opentelemetry/core": "npm:2.0.1" + "@opentelemetry/exporter-logs-otlp-grpc": "npm:0.202.0" + "@opentelemetry/exporter-logs-otlp-http": "npm:0.202.0" + "@opentelemetry/exporter-logs-otlp-proto": "npm:0.202.0" + "@opentelemetry/exporter-metrics-otlp-grpc": "npm:0.202.0" + "@opentelemetry/exporter-metrics-otlp-http": "npm:0.202.0" + "@opentelemetry/exporter-metrics-otlp-proto": "npm:0.202.0" + "@opentelemetry/exporter-prometheus": "npm:0.202.0" + "@opentelemetry/exporter-trace-otlp-grpc": "npm:0.202.0" + "@opentelemetry/exporter-trace-otlp-http": "npm:0.202.0" + "@opentelemetry/exporter-trace-otlp-proto": "npm:0.202.0" + "@opentelemetry/exporter-zipkin": "npm:2.0.1" + "@opentelemetry/instrumentation": "npm:0.202.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.202.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/8df1556294a0a23ec1608f8eb51837820f86ec09e70e3df656fa5e7a176d736c0b13dce14f990ac7037cb14a90012eb3bdb85c54001a905d40368201ab7813b1 + languageName: node + linkType: hard + +"@opentelemetry/sdk-trace-base@npm:2.0.1, @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:2.0.1" + "@opentelemetry/resources": "npm:2.0.1" + "@opentelemetry/semantic-conventions": "npm:^1.29.0" + peerDependencies: + "@opentelemetry/api": ">=1.3.0 <1.10.0" + checksum: 10c0/4e3c733296012b758d007e9c0d8a5b175edbe9a680c73ec75303476e7982b73ad4209f1a2791c1a94c428e5a53eba6c2a72faa430c70336005aa58744d6cb37b + languageName: node + linkType: hard + +"@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.27.0, @opentelemetry/semantic-conventions@npm:^1.30.0, @opentelemetry/semantic-conventions@npm:^1.31.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/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" + conditions: os=darwin & cpu=arm64 + languageName: node + linkType: hard + +"@oven/bun-darwin-x64-baseline@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-darwin-x64-baseline@npm:1.2.18" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oven/bun-darwin-x64@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-darwin-x64@npm:1.2.18" + conditions: os=darwin & cpu=x64 + languageName: node + linkType: hard + +"@oven/bun-linux-aarch64-musl@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-aarch64-musl@npm:1.2.18" + conditions: os=linux & cpu=aarch64 + languageName: node + linkType: hard + +"@oven/bun-linux-aarch64@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-aarch64@npm:1.2.18" + conditions: os=linux & cpu=arm64 + languageName: node + linkType: hard + +"@oven/bun-linux-x64-baseline@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-x64-baseline@npm:1.2.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@oven/bun-linux-x64-musl-baseline@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-x64-musl-baseline@npm:1.2.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@oven/bun-linux-x64-musl@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-x64-musl@npm:1.2.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard + +"@oven/bun-linux-x64@npm:1.2.18": + version: 1.2.18 + resolution: "@oven/bun-linux-x64@npm:1.2.18" + conditions: os=linux & cpu=x64 + languageName: node + linkType: hard "@oven/bun-windows-x64-baseline@npm:1.2.18": version: 1.2.18 @@ -7485,9 +8209,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" @@ -7499,13 +8223,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" @@ -7517,7 +8241,7 @@ __metadata: peerDependenciesMeta: rollup: optional: true - checksum: 10c0/54d33282321492fafec29b49c66dd1efd90c72a24f9d1569dcb57a72ab8de8a782810f39fdb917b96ec6a598c18f3416588b419bf7af331793a010de1fe28c60 + checksum: 10c0/1baa7e86d03bdd55e2cc0109b103436a962262b71fc313181b1b0fd4a9318f3aeb77e329062f3353bc0214f9ca51d1bac6e45b5678041bd1279ee482a051fd21 languageName: node linkType: hard @@ -7567,9 +8291,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm-eabi@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-android-arm-eabi@npm:4.41.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 @@ -7581,9 +8305,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-android-arm64@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-android-arm64@npm:4.41.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 @@ -7595,9 +8319,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-arm64@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-darwin-arm64@npm:4.41.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 @@ -7609,9 +8333,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-darwin-x64@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-darwin-x64@npm:4.41.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 @@ -7623,9 +8347,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-arm64@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-freebsd-arm64@npm:4.41.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 @@ -7637,9 +8361,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-freebsd-x64@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-freebsd-x64@npm:4.41.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 @@ -7651,9 +8375,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-gnueabihf@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-arm-gnueabihf@npm:4.41.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 @@ -7665,9 +8389,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm-musleabihf@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-arm-musleabihf@npm:4.41.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 @@ -7679,9 +8403,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-arm64-gnu@npm:4.41.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 @@ -7693,9 +8417,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-arm64-musl@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-arm64-musl@npm:4.41.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 @@ -7707,9 +8431,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-loongarch64-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-loongarch64-gnu@npm:4.41.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 @@ -7721,9 +8445,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-powerpc64le-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-powerpc64le-gnu@npm:4.41.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 @@ -7735,9 +8459,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-riscv64-gnu@npm:4.41.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 @@ -7749,9 +8473,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-riscv64-musl@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-riscv64-musl@npm:4.41.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 @@ -7763,9 +8487,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-s390x-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-s390x-gnu@npm:4.41.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 @@ -7777,9 +8501,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-gnu@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-x64-gnu@npm:4.41.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 @@ -7791,9 +8515,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-linux-x64-musl@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-linux-x64-musl@npm:4.41.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 @@ -7805,9 +8529,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-arm64-msvc@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-win32-arm64-msvc@npm:4.41.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 @@ -7819,9 +8543,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-ia32-msvc@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-win32-ia32-msvc@npm:4.41.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 @@ -7833,9 +8557,9 @@ __metadata: languageName: node linkType: hard -"@rollup/rollup-win32-x64-msvc@npm:4.41.0": - version: 4.41.0 - resolution: "@rollup/rollup-win32-x64-msvc@npm:4.41.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 @@ -8590,6 +9314,13 @@ __metadata: languageName: node linkType: hard +"@types/aws-lambda@npm:8.10.147": + version: 8.10.147 + resolution: "@types/aws-lambda@npm:8.10.147" + checksum: 10c0/c77bcb18a935fb26f5b1164aaadf46b3d11d6c001a95c6e9f2ff72f7d9ed4e7f28075a3abf9f9585cc75510acbc29c7a6441e66727902eae1bd39ac8dc28351e + languageName: node + linkType: hard + "@types/aws4@npm:^1.11.6": version: 1.11.6 resolution: "@types/aws4@npm:1.11.6" @@ -8659,6 +9390,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" @@ -8680,7 +9420,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: @@ -8927,6 +9667,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" @@ -8948,6 +9697,15 @@ __metadata: languageName: node linkType: hard +"@types/mysql@npm:2.15.26": + version: 2.15.26 + resolution: "@types/mysql@npm:2.15.26" + dependencies: + "@types/node": "npm:*" + checksum: 10c0/3cf279e7db05d56c0544532a4380b9079f579092379a04c8138bd5cf88dda5b31208ac2d23ce7dbf4e3a3f43aaeed44e72f9f19f726518f308efe95a7435619a + 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" @@ -8971,24 +9729,64 @@ __metadata: version: 22.15.32 resolution: "@types/node@npm:22.15.32" dependencies: - undici-types: "npm:~6.21.0" - checksum: 10c0/63a2fa52adf1134d1b3bee8b1862d4b8e4550fffc190551068d3d41a41d9e5c0c8f1cb81faa18767b260637360f662115c26c5e4e7718868ead40c4a57cbc0e3 + undici-types: "npm:~6.21.0" + checksum: 10c0/63a2fa52adf1134d1b3bee8b1862d4b8e4550fffc190551068d3d41a41d9e5c0c8f1cb81faa18767b260637360f662115c26c5e4e7718868ead40c4a57cbc0e3 + languageName: node + linkType: hard + +"@types/node@npm:^12.7.1": + version: 12.20.55 + resolution: "@types/node@npm:12.20.55" + checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 + languageName: node + linkType: hard + +"@types/node@npm:^18.11.18, @types/node@npm:^18.17.15": + version: 18.19.112 + resolution: "@types/node@npm:18.19.112" + dependencies: + undici-types: "npm:~5.26.4" + checksum: 10c0/e3421fb3c755337a0477014b235026d914cac8266b67de5867d778b2d4ce2ed0c8052c922e61810f2364d6b29ee24c6ea18671c5636076371b82b0d3e480fbef + 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/node@npm:^12.7.1": - version: 12.20.55 - resolution: "@types/node@npm:12.20.55" - checksum: 10c0/3b190bb0410047d489c49bbaab592d2e6630de6a50f00ba3d7d513d59401d279972a8f5a598b5bb8ddc1702f8a2f4ec57a65d93852f9c329639738e7053637d1 +"@types/pg@npm:*": + 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/node@npm:^18.11.18, @types/node@npm:^18.17.15": - version: 18.19.112 - resolution: "@types/node@npm:18.19.112" +"@types/pg@npm:8.15.1": + version: 8.15.1 + resolution: "@types/pg@npm:8.15.1" dependencies: - undici-types: "npm:~5.26.4" - checksum: 10c0/e3421fb3c755337a0477014b235026d914cac8266b67de5867d778b2d4ce2ed0c8052c922e61810f2364d6b29ee24c6ea18671c5636076371b82b0d3e480fbef + "@types/node": "npm:*" + pg-protocol: "npm:*" + pg-types: "npm:^4.0.1" + checksum: 10c0/68ab3b2ae56a9ef891fcefe814b4a4eb6da4f4f3860b56d2821de8fe024eb093895ffe152414c708d889f7110cd5b3839392d2955b07d38f111df8e6186c7e8c languageName: node linkType: hard @@ -8999,6 +9797,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" @@ -9006,6 +9811,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" @@ -9050,13 +9873,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" @@ -9095,6 +9911,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" @@ -9293,17 +10118,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" @@ -9467,6 +10281,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" @@ -9487,7 +10310,20 @@ __metadata: languageName: node linkType: hard -"@whatwg-node/server@npm:^0.10.0, @whatwg-node/server@npm:^0.10.10, @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.10": version: 0.10.10 resolution: "@whatwg-node/server@npm:0.10.10" dependencies: @@ -10282,6 +11118,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" @@ -10879,6 +11729,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" @@ -11062,6 +11919,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" @@ -11979,6 +12848,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" @@ -12635,6 +13511,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": version: 1.23.9 resolution: "es-abstract@npm:1.23.9" @@ -13420,6 +14315,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" @@ -13464,13 +14366,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" @@ -13936,6 +14831,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" @@ -14085,6 +14987,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" @@ -14115,6 +15018,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" @@ -14221,7 +15148,7 @@ __metadata: languageName: node linkType: hard -"get-tsconfig@npm:^4.10.1, get-tsconfig@npm:^4.7.5, get-tsconfig@npm:^4.7.6, get-tsconfig@npm:^4.8.1": +"get-tsconfig@npm:^4.10.1": version: 4.10.1 resolution: "get-tsconfig@npm:4.10.1" dependencies: @@ -14230,6 +15157,15 @@ __metadata: languageName: node linkType: hard +"get-tsconfig@npm:^4.7.5, get-tsconfig@npm:^4.7.6, get-tsconfig@npm:^4.8.1": + version: 4.10.0 + resolution: "get-tsconfig@npm:4.10.0" + dependencies: + resolve-pkg-maps: "npm:^1.0.0" + checksum: 10c0/c9b5572c5118923c491c04285c73bd55b19e214992af957c502a3be0fc0043bb421386ffd45ca3433c0a7fba81221ca300479e8393960acf15d0ed4563f38a86 + languageName: node + linkType: hard + "get-uri@npm:^6.0.1": version: 6.0.4 resolution: "get-uri@npm:6.0.4" @@ -14413,6 +15349,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" @@ -14748,6 +15691,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" @@ -14847,7 +15804,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: @@ -15681,6 +16638,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" @@ -16289,6 +17259,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" @@ -16820,6 +17799,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" @@ -17261,7 +18247,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 @@ -17591,7 +18577,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: @@ -17800,6 +18786,13 @@ __metadata: languageName: node linkType: hard +"obuf@npm:~1.1.2": + version: 1.1.2 + resolution: "obuf@npm:1.1.2" + checksum: 10c0/520aaac7ea701618eacf000fc96ae458e20e13b0569845800fc582f81b386731ab22d55354b4915d58171db00e79cfcd09c1638c02f89577ef092b38c65b7d81 + languageName: node + linkType: hard + "ohash@npm:^2.0.11": version: 2.0.11 resolution: "ohash@npm:2.0.11" @@ -17869,6 +18862,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" @@ -18272,6 +19272,55 @@ __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-numeric@npm:1.0.2": + version: 1.0.2 + resolution: "pg-numeric@npm:1.0.2" + checksum: 10c0/43dd9884e7b52c79ddc28d2d282d7475fce8bba13452d33c04ceb2e0a65f561edf6699694e8e1c832ff9093770496363183c950dd29608e1bdd98f344b25bca9 + 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 + +"pg-types@npm:^4.0.1": + version: 4.0.2 + resolution: "pg-types@npm:4.0.2" + dependencies: + pg-int8: "npm:1.0.1" + pg-numeric: "npm:1.0.2" + postgres-array: "npm:~3.0.1" + postgres-bytea: "npm:~3.0.0" + postgres-date: "npm:~2.1.0" + postgres-interval: "npm:^3.0.0" + postgres-range: "npm:^1.1.1" + checksum: 10c0/780fccda2f3fa2a34e85a72e8e7dadb7d88fbe71ce88f126cb3313f333ad836d02488ec4ff3d94d0c1e5846f735d6e6c6281f8059e6b8919d2180429acaec3e2 + 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" @@ -18348,16 +19397,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" @@ -18365,7 +19414,7 @@ __metadata: thread-stream: "npm:^3.0.0" bin: pino: bin.js - checksum: 10c0/c7f8a83a9a9d728b4eff6d0f4b9367f031c91bcaa5806fbf0eedcc8e77faba593d59baf11a8fba0dd1c778bb17ca7ed01418ac1df4ec129faeedd4f3ecaff66f + checksum: 10c0/bcd1e9d9b301bea13b95689ca9ad7105ae9451928fb6c0b67b3e58c5fe37cea1d40665f3d6641e3da00be0bbc17b89031e67abbc8ea6aac6164f399309fd78e7 languageName: node linkType: hard @@ -18469,6 +19518,73 @@ __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-array@npm:~3.0.1": + version: 3.0.4 + resolution: "postgres-array@npm:3.0.4" + checksum: 10c0/47f3e648da512bacdd6a5ed55cf770605ec271330789faeece0fd13805a49f376d6e5c9e0e353377be11a9545e727dceaa2473566c505432bf06366ccd04c6b2 + 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-bytea@npm:~3.0.0": + version: 3.0.0 + resolution: "postgres-bytea@npm:3.0.0" + dependencies: + obuf: "npm:~1.1.2" + checksum: 10c0/41c79cc48aa730c5ba3eda6ab989a940034f07a1f57b8f2777dce56f1b8cca16c5870582932b5b10cc605048aef9b6157e06253c871b4717cafc6d00f55376aa + 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-date@npm:~2.1.0": + version: 2.1.0 + resolution: "postgres-date@npm:2.1.0" + checksum: 10c0/00a7472c10788f6b0d08d24108bf1eb80858de1bd6317740198a564918ea4a69b80c98148167b92ae688abd606483020d0de0dd3a36f3ea9a3e26bbeef3464f4 + 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 + +"postgres-interval@npm:^3.0.0": + version: 3.0.0 + resolution: "postgres-interval@npm:3.0.0" + checksum: 10c0/8b570b30ea37c685e26d136d34460f246f98935a1533defc4b53bb05ee23ae3dc7475b718ec7ea607a57894d8c6b4f1adf67ca9cc83a75bdacffd427d5c68de8 + languageName: node + linkType: hard + +"postgres-range@npm:^1.1.1": + version: 1.1.4 + resolution: "postgres-range@npm:1.1.4" + checksum: 10c0/254494ef81df208e0adeae6b66ce394aba37914ea14c7ece55a45fb6691b7db04bee74c825380a47c887a9f87158fd3d86f758f9cc60b76d3a38ce5aca7912e8 + 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" @@ -18580,6 +19696,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" @@ -18771,7 +19894,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 @@ -18853,6 +19976,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" @@ -18883,6 +20017,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" @@ -19309,30 +20450,30 @@ __metadata: languageName: node linkType: hard -"rollup@npm:^4.18.1, rollup@npm:^4.34.9": - version: 4.41.0 - resolution: "rollup@npm:4.41.0" - dependencies: - "@rollup/rollup-android-arm-eabi": "npm:4.41.0" - "@rollup/rollup-android-arm64": "npm:4.41.0" - "@rollup/rollup-darwin-arm64": "npm:4.41.0" - "@rollup/rollup-darwin-x64": "npm:4.41.0" - "@rollup/rollup-freebsd-arm64": "npm:4.41.0" - "@rollup/rollup-freebsd-x64": "npm:4.41.0" - "@rollup/rollup-linux-arm-gnueabihf": "npm:4.41.0" - "@rollup/rollup-linux-arm-musleabihf": "npm:4.41.0" - "@rollup/rollup-linux-arm64-gnu": "npm:4.41.0" - "@rollup/rollup-linux-arm64-musl": "npm:4.41.0" - "@rollup/rollup-linux-loongarch64-gnu": "npm:4.41.0" - "@rollup/rollup-linux-powerpc64le-gnu": "npm:4.41.0" - "@rollup/rollup-linux-riscv64-gnu": "npm:4.41.0" - "@rollup/rollup-linux-riscv64-musl": "npm:4.41.0" - "@rollup/rollup-linux-s390x-gnu": "npm:4.41.0" - "@rollup/rollup-linux-x64-gnu": "npm:4.41.0" - "@rollup/rollup-linux-x64-musl": "npm:4.41.0" - "@rollup/rollup-win32-arm64-msvc": "npm:4.41.0" - "@rollup/rollup-win32-ia32-msvc": "npm:4.41.0" - "@rollup/rollup-win32-x64-msvc": "npm:4.41.0" +"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: @@ -19380,7 +20521,7 @@ __metadata: optional: true bin: rollup: dist/bin/rollup - checksum: 10c0/4c3d361f400317cb18f5e3912553a514bb1dcc7ce0c92ff66b9786b598632eb1d56f7bb1cc11ed16a4ccdbe6808ae851bc6bde5d2305cc587450c6212e69617f + checksum: 10c0/c4d5f2257320b50dc0e035e31d8d2f78d36b7015aef2f87cc984c0a1c97ffebf14337dddeb488b4b11ae798fea6486189b77e7cf677617dcf611d97db41ebfda languageName: node linkType: hard @@ -19496,6 +20637,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:^2.4.0": version: 2.7.0 resolution: "secure-json-parse@npm:2.7.0" @@ -19785,13 +20933,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" @@ -20233,6 +21374,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" @@ -20666,6 +21814,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" @@ -21544,6 +22705,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" @@ -22130,7 +23300,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 @@ -22275,3 +23452,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