diff --git a/.github/workflows/release-preview.yaml b/.github/workflows/release-preview.yaml new file mode 100644 index 0000000000..118a9023d4 --- /dev/null +++ b/.github/workflows/release-preview.yaml @@ -0,0 +1,44 @@ +name: Release Preview + +on: + push: + tags-ignore: + - '**' + branches: + - 'main' + paths-ignore: + - 'docs*/**' + - '*.md' + +concurrency: + group: ${{github.workflow}}-${{github.head_ref}} + cancel-in-progress: false + +env: + CI: true + +jobs: + version: + timeout-minutes: 10 + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + with: + fetch-depth: 0 + + - uses: ./.github/actions/node + with: + working-directory: ${{ env.WORKING_DIRECTORY }} + repo-token: ${{ secrets.GITHUB_TOKEN }} + + - name: Install + run: | + pnpm add -g @lerna-lite/cli@2.5.0 @lerna-lite/publish@2.5.0 @lerna-lite/version@2.5.0 @commitlint/config-conventional@17.6.6 + + - name: Generate next release (dry-run) + run: pnpm run release-preview --yes + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Show CHANGELOG.md + run: echo -e "\`\`\`diff\n$(git --no-pager diff './packages/*/CHANGELOG.md')\n\`\`\`" >> $GITHUB_STEP_SUMMARY diff --git a/Makefile b/Makefile index 8745bbe9d1..597aeb6ba4 100644 --- a/Makefile +++ b/Makefile @@ -63,7 +63,7 @@ start-router: (cd router && make dev) dc-dev: - docker compose --file docker-compose.yml up --remove-orphans --detach + docker compose --file docker-compose.yml up --remove-orphans --detach --build dc-stack: docker compose --file docker-compose.cosmo.yml up --remove-orphans --detach diff --git a/README.md b/README.md index d36d85c6a6..2315ecbc10 100644 --- a/README.md +++ b/README.md @@ -122,12 +122,12 @@ We manage multiple compose files: ## On-Premise Cosmo was designed to be deployed on-premise e.g. Kubernetes. We provide a helm chart to deploy the platform on any Kubernetes like AKS, GKE, AKS or Minikube. You can find the helm chart in the [helm](./helm) directory. -If you need help with the deployment, please contact us at [Sales](https://wundergraph.com/contact/sales). +If you need help with the deployment, please contact us [here](https://form.typeform.com/to/oC6XATf4). ## Managed Service If you don't want to manage the platform yourself, you can use our managed service [WunderGraph Cosmo Cloud](https://cosmo.wundergraph.com). It is a fully managed platform that don't make you worry about infrastructure, so you can focus on building. -The managed service is currently in private beta. If you want to participate, please contact us at [Sales](https://wundergraph.com/contact/sales). +The managed service is currently in private beta. If you want to participate, please contact us [here](https://form.typeform.com/to/oC6XATf4). After contacting us, we will hook you up with a free trial and help you to get started. ## License diff --git a/controlplane/package.json b/controlplane/package.json index 3ed95c9c9a..34b21adee8 100644 --- a/controlplane/package.json +++ b/controlplane/package.json @@ -17,7 +17,7 @@ "db:custom": "drizzle-kit generate:pg --custom --config=drizzle.config.ts", "db:check": "drizzle-kit check:pg --config=drizzle.config.ts", "db:drop": "drizzle-kit drop --config=drizzle.config.ts", - "start": "node dist/index.js", + "start": "NODE_ENV=production node dist/index.js", "test": "pnpm lint && tsc -p tsconfig.test.json && vitest run", "lint": "eslint --cache --ext .ts,.mjs,.cjs . && prettier -c src", "lint:fix": "eslint --cache --fix --ext .ts,.mjs,.cjs . && prettier --write -c src", @@ -35,7 +35,7 @@ "@bufbuild/connect-fastify": "^0.13.0", "@bufbuild/connect-node": "^0.13.0", "@fastify/cors": "^8.3.0", - "@graphql-inspector/core": "^5.0.0", + "@graphql-inspector/core": "^5.0.1", "@keycloak/keycloak-admin-client": "^22.0.1", "@wundergraph/composition": "workspace:*", "@wundergraph/cosmo-connect": "workspace:*", @@ -44,19 +44,20 @@ "cookie": "^0.5.0", "dotenv": "^16.3.1", "drizzle-orm": "^0.28.5", - "fastify": "^4.21.0", + "fastify": "^4.22.0", "fastify-graceful-shutdown": "^3.5.1", "fastify-plugin": "^4.5.1", + "postgres": "^3.3.5", "graphql": "^16.7.1", "jose": "^4.14.4", "nuid": "^1.1.6", - "pino": "^8.14.1", - "postgres": "^3.3.5", + "pg-boss": "^9.0.3", + "pino": "^8.15.0", "rxjs": "^7.8.1", "stream-json": "^1.8.0", "tiny-lru": "^11.0.1", "uid": "^2.0.2", - "zod": "^3.21.4" + "zod": "^3.22.2" }, "devDependencies": { "@bufbuild/protobuf": "^1.3.0", diff --git a/controlplane/src/core/build-server.ts b/controlplane/src/core/build-server.ts index f92d4621c9..67e65fcff1 100644 --- a/controlplane/src/core/build-server.ts +++ b/controlplane/src/core/build-server.ts @@ -3,7 +3,7 @@ import { fastifyConnectPlugin } from '@bufbuild/connect-fastify'; import { cors } from '@bufbuild/connect'; import fastifyCors from '@fastify/cors'; import { PinoLoggerOptions } from 'fastify/types/logger.js'; -import pino from 'pino'; +import { pino } from 'pino'; import { compressionBrotli, compressionGzip } from '@bufbuild/connect-node'; import fastifyGracefulShutdown from 'fastify-graceful-shutdown'; import routes from './routes.js'; @@ -74,8 +74,12 @@ export default async function build(opts: BuildConfig) { ...opts.logger, }; + const log = pino(opts.production ? opts.logger : { ...developmentLoggerOpts, ...opts.logger }); + const fastify = Fastify({ - logger: opts.production ? opts.logger : { ...developmentLoggerOpts, ...opts.logger }, + logger: log, + // The maximum amount of time in *milliseconds* in which a plugin can load + pluginTimeout: 10_000, // 10s }); /** @@ -86,10 +90,22 @@ export default async function build(opts: BuildConfig) { await fastify.register(fastifyDatabase, { databaseConnectionUrl: opts.database.url, + gracefulTimeoutSec: 15, ssl: opts.database.ssl, debugSQL: opts.debugSQL, }); + // await fastify.register(fastifyPgBoss, { + // databaseConnectionUrl: opts.database.url, + // }); + + // PgBoss Workers + + // Example + // const tw = new TrafficAnalyzerWorker(fastify.pgboss); + // await tw.register({ graphId: 'test' }); + // await tw.subscribe(); + await fastify.register(fastifyCors, { // Produce an error if allowedOrigins is undefined origin: opts.allowedOrigins || [], @@ -105,10 +121,10 @@ export default async function build(opts: BuildConfig) { if (opts.clickhouseDsn) { await fastify.register(fastifyClickHouse, { dsn: opts.clickhouseDsn, - logger: fastify.log, + logger: log, }); } else { - fastify.log.warn('ClickHouse connection not configured'); + log.warn('ClickHouse connection not configured'); } const authUtils = new AuthUtils(fastify.db, { @@ -170,7 +186,7 @@ export default async function build(opts: BuildConfig) { await fastify.register(fastifyConnectPlugin, { routes: routes({ db: fastify.db, - logger: fastify.log as pino.Logger, + logger: log, jwtSecret: opts.auth.secret, keycloakRealm: opts.keycloak.realm, chClient: fastify.ch, @@ -186,7 +202,9 @@ export default async function build(opts: BuildConfig) { acceptCompression: [compressionBrotli, compressionGzip], }); - await fastify.register(fastifyGracefulShutdown, {}); + await fastify.register(fastifyGracefulShutdown, { + timeout: 60_000, + }); return fastify; } diff --git a/controlplane/src/core/plugins/clickhouse.ts b/controlplane/src/core/plugins/clickhouse.ts index 892e407e1b..82e9016e68 100644 --- a/controlplane/src/core/plugins/clickhouse.ts +++ b/controlplane/src/core/plugins/clickhouse.ts @@ -14,7 +14,7 @@ export interface ChPluginOptions { logger: BaseLogger; } -export default fp(async function (fastify, opts) { +export default fp(async function ClickHousePlugin(fastify, opts) { const connection = new ClickHouseClient({ dsn: opts.dsn, logger: opts.logger, diff --git a/controlplane/src/core/plugins/database.ts b/controlplane/src/core/plugins/database.ts index dcfb3fe9df..c5ad1f1b11 100644 --- a/controlplane/src/core/plugins/database.ts +++ b/controlplane/src/core/plugins/database.ts @@ -17,6 +17,7 @@ declare module 'fastify' { export interface DbPluginOptions { databaseConnectionUrl: string; debugSQL?: boolean; + gracefulTimeoutSec?: number; ssl?: { // Necessary only if the server uses a self-signed certificate. caPath?: string; @@ -40,13 +41,14 @@ export default fp(async function (fastify, opts) { // Necessary only if the server uses a self-signed certificate. if (opts.ssl.caPath) { - sslOptions.key = await readFile(opts.ssl.caPath, 'utf8'); + sslOptions.ca = await readFile(opts.ssl.caPath, 'utf8'); } // Necessary only if the server requires client certificate authentication. if (opts.ssl.certPath) { sslOptions.cert = await readFile(opts.ssl.certPath, 'utf8'); } + if (opts.ssl.keyPath) { sslOptions.key = await readFile(opts.ssl.keyPath, 'utf8'); } @@ -73,10 +75,12 @@ export default fp(async function (fastify, opts) { } }); fastify.addHook('onClose', () => { - fastify.log.debug('Closing database connection'); + fastify.log.debug('Closing database connection ...'); + queryConnection.end({ - timeout: 5, // in seconds + timeout: opts.gracefulTimeoutSec ?? 5, }); + fastify.log.debug('Database connection closed'); }); diff --git a/controlplane/src/core/plugins/health.ts b/controlplane/src/core/plugins/health.ts index a1a1f5c679..f631b93217 100644 --- a/controlplane/src/core/plugins/health.ts +++ b/controlplane/src/core/plugins/health.ts @@ -1,7 +1,7 @@ import fp from 'fastify-plugin'; import { FastifyPluginCallback } from 'fastify'; -const plugin: FastifyPluginCallback = function Health(fastify, opts, done) { +const plugin: FastifyPluginCallback = function HealthPlugin(fastify, opts, done) { let shutdown = false; fastify.addHook('onClose', (instance, done) => { diff --git a/controlplane/src/core/plugins/pgboss.ts b/controlplane/src/core/plugins/pgboss.ts new file mode 100644 index 0000000000..efa6f8dfb4 --- /dev/null +++ b/controlplane/src/core/plugins/pgboss.ts @@ -0,0 +1,103 @@ +import tls from 'node:tls'; +import { readFile } from 'node:fs/promises'; +import fp from 'fastify-plugin'; +import PgBoss from 'pg-boss'; +import './pgboss.js'; + +declare module 'fastify' { + interface FastifyInstance { + pgboss: PgBoss; + } +} + +export interface PgBossOptions { + databaseConnectionUrl: string; + ssl?: { + // Necessary only if the server uses a self-signed certificate. + caPath?: string; + // Necessary only if the server requires client certificate authentication. + keyPath?: string; + certPath?: string; + }; +} + +export default fp(async function PgBossPlugin(fastify, opts) { + const config: PgBoss.ConstructorOptions = { + connectionString: opts.databaseConnectionUrl, + application_name: 'controlplane', + // How many days a job may be in created or retry state before it's archived. Must be >=1 + retentionDays: 30, + // How many minutes a job may be in active state before it is failed because of expiration. Must be >=1 + expireInMinutes: 15, + // Specifies how long in seconds completed jobs get archived (12 hours). + archiveCompletedAfterSeconds: 12 * 60 * 60, + // Specifies how long in seconds failed jobs get archived (12 hours). + archiveFailedAfterSeconds: 12 * 60 * 60, + // When jobs in the archive table become eligible for deletion. + deleteAfterDays: 30, + // How often maintenance operations are run against the job and archive tables. + maintenanceIntervalMinutes: 1, + }; + + if (opts.ssl) { + const sslOptions: tls.ConnectionOptions = { + rejectUnauthorized: false, + }; + + // Necessary only if the server uses a self-signed certificate. + if (opts.ssl.caPath) { + sslOptions.ca = await readFile(opts.ssl.caPath, 'utf8'); + } + + // Necessary only if the server requires client certificate authentication. + if (opts.ssl.certPath) { + sslOptions.cert = await readFile(opts.ssl.certPath, 'utf8'); + } + + if (opts.ssl.keyPath) { + sslOptions.key = await readFile(opts.ssl.keyPath, 'utf8'); + } + + config.ssl = sslOptions; + } + + const boss = new PgBoss(config); + + boss.on('error', (error) => fastify.log.error(error, 'PgBoss error')); + + await boss.start(); + + boss.on('wip', (data) => { + const progress = data.filter((worker) => worker.state === 'active').length; + const failed = data.filter((worker) => worker.state === 'failed').length; + // @ts-ignore https://github.com/timgit/pg-boss/issues/422 + const stopping = data.filter((worker) => worker.state === 'stopping').length; + + fastify.log.debug({ progress, stopping, failed }, `PgBoss Worker report`); + }); + + fastify.addHook('onClose', async () => { + const destroy = process.env.NODE_ENV !== 'production'; + fastify.log.info({ gracePeriod: '30s', destroy }, 'Shutting down PgBoss ...'); + + const stopOptions: PgBoss.StopOptions = { + timeout: 30_000, + graceful: true, + destroy, + }; + await boss.stop(stopOptions); + + // Wait until pgBoss has gracefully stopped. + // https://github.com/timgit/pg-boss/issues/421 + await new Promise((resolve) => { + boss.once('stopped', () => { + fastify.log.info('PgBoss stopped'); + resolve(undefined); + }); + }); + + fastify.log.info('PgBoss shutdown complete'); + }); + + fastify.decorate('pgboss', boss); +}); diff --git a/controlplane/src/core/workers/TrafficAnalyzerWorker.ts b/controlplane/src/core/workers/TrafficAnalyzerWorker.ts new file mode 100644 index 0000000000..56c0e75848 --- /dev/null +++ b/controlplane/src/core/workers/TrafficAnalyzerWorker.ts @@ -0,0 +1,46 @@ +import PgBoss from 'pg-boss'; + +interface RegisterTrafficAnalyzerOptions { + graphId: string; +} + +interface TrafficAnalyzerJob { + graphId: string; + type: 'traffic/analyzer'; +} + +export default class TrafficAnalyzerWorker { + constructor(private boss: PgBoss) {} + + /** + * Register a new graph to be analyzed. This method is idempotent. + * @param opts + */ + public async register(opts: RegisterTrafficAnalyzerOptions): Promise { + const queue = `traffic/analyzer/graph/${opts.graphId}`; + // Create a cron job that runs every 5 minute. + await this.boss.schedule(queue, '*/5 * * * *', { type: 'traffic', graphId: opts.graphId }); + } + + /** + * Subscribe to the traffic analyzer queue with a max concurrency of 100 + * and a team size of 10.sc + */ + public async subscribe(): Promise { + await this.boss.work( + `traffic/analyzer/graph/*`, + { teamSize: 10, teamConcurrency: 100 }, + (job) => this.handler(job), + ); + } + + /** + * Handle a traffic analyzer job. + * @param event + */ + // eslint-disable-next-line require-await + public async handler(event: PgBoss.Job): Promise { + // TODO: Implement me! + console.log(event); + } +} diff --git a/docker-compose.yml b/docker-compose.yml index ee407400ca..7087f86a70 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -97,3 +97,4 @@ networks: volumes: postgres: clickhouse: + prometheus: diff --git a/helm/cosmo/charts/controlplane/values.yaml b/helm/cosmo/charts/controlplane/values.yaml index c73d940868..b6450c9b68 100644 --- a/helm/cosmo/charts/controlplane/values.yaml +++ b/helm/cosmo/charts/controlplane/values.yaml @@ -107,7 +107,7 @@ podDisruptionBudget: {} priorityClassName: "" # -- Sets the [termination grace period](https://kubernetes.io/docs/concepts/containers/container-lifecycle-hooks/#hook-handler-execution) for Deployment pods -terminationGracePeriodSeconds: 10 +terminationGracePeriodSeconds: 60 probes: # -- Configure readiness probe diff --git a/helm/cosmo/charts/router/templates/deployment.yaml b/helm/cosmo/charts/router/templates/deployment.yaml index a55999ea29..0d435a91d9 100644 --- a/helm/cosmo/charts/router/templates/deployment.yaml +++ b/helm/cosmo/charts/router/templates/deployment.yaml @@ -55,8 +55,6 @@ spec: containerPort: {{ .Values.service.port }} protocol: TCP env: - - name: GIN_MODE - value: release - name: LOG_LEVEL valueFrom: configMapKeyRef: diff --git a/lerna.json b/lerna.json index 810da9b54b..26cef75560 100644 --- a/lerna.json +++ b/lerna.json @@ -11,7 +11,8 @@ "syncWorkspaceLock": true, "changelogIncludeCommitsClientLogin": " (@%l)", "skipBumpOnlyRelease": true, - "message": "chore(release): Publish [skip ci]" + "message": "chore(release): Publish [skip ci]", + "changelogHeaderMessage": "Images can be found [here](https://github.com/orgs/wundergraph/packages?repo_name=cosmo)" } }, "packages": [ diff --git a/otelcollector/otel-config.yaml b/otelcollector/otel-config.yaml index b8877f4ffa..aa0c5bb812 100644 --- a/otelcollector/otel-config.yaml +++ b/otelcollector/otel-config.yaml @@ -51,9 +51,11 @@ extensions: service: extensions: [health_check, jwt] pipelines: - # metrics: - # receivers: [otlp] - # exporters: [clickhouse] + metrics: + receivers: [otlp] + # Order is important here. Otherwise, the attributes processor will not be able to read the attributes from the auth context. + processors: [attributes/from_auth_context, memory_limiter, batch] + exporters: [clickhouse] traces: receivers: [otlp] # Order is important here. Otherwise, the attributes processor will not be able to read the attributes from the auth context. diff --git a/package.json b/package.json index 4f72a3cc4a..8d0998c1ee 100644 --- a/package.json +++ b/package.json @@ -24,7 +24,7 @@ "husky": "husky install", "lint:fix": "pnpm run -r --parallel lint:fix", "clean": "del-cli '**/node_modules/' '**/**/dist/' '**/**/gen/' '**/**/.next' '**/**/tsconfig.tsbuildinfo' '**/**/.eslintcache'", - "release-preview": "lerna publish --dry-run", + "release-preview": "lerna publish --ignore-scripts --dry-run", "release": "lerna publish -y" }, "files": [ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e81aeb0758..e9061d4dc5 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -182,8 +182,8 @@ importers: specifier: ^8.3.0 version: 8.3.0 '@graphql-inspector/core': - specifier: ^5.0.0 - version: 5.0.0(graphql@16.7.1) + specifier: ^5.0.1 + version: 5.0.1(graphql@16.7.1) '@keycloak/keycloak-admin-client': specifier: ^22.0.1 version: 22.0.1 @@ -207,10 +207,10 @@ importers: version: 16.3.1 drizzle-orm: specifier: ^0.28.5 - version: 0.28.5(postgres@3.3.5) + version: 0.28.5(pg@8.11.3)(postgres@3.3.5) fastify: - specifier: ^4.21.0 - version: 4.21.0 + specifier: ^4.22.0 + version: 4.22.0 fastify-graceful-shutdown: specifier: ^3.5.1 version: 3.5.1 @@ -226,9 +226,12 @@ importers: nuid: specifier: ^1.1.6 version: 1.1.6 + pg-boss: + specifier: ^9.0.3 + version: 9.0.3 pino: - specifier: ^8.14.1 - version: 8.14.1 + specifier: ^8.15.0 + version: 8.15.0 postgres: specifier: ^3.3.5 version: 3.3.5 @@ -245,8 +248,8 @@ importers: specifier: ^2.0.2 version: 2.0.2 zod: - specifier: ^3.21.4 - version: 3.21.4 + specifier: ^3.22.2 + version: 3.22.2 devDependencies: '@bufbuild/protobuf': specifier: ^1.3.0 @@ -743,7 +746,7 @@ packages: '@bufbuild/connect': 0.13.0(@bufbuild/protobuf@1.3.0) '@bufbuild/connect-node': 0.13.0(@bufbuild/protobuf@1.3.0) '@bufbuild/protobuf': 1.3.0 - fastify: 4.21.0 + fastify: 4.22.0 transitivePeerDependencies: - supports-color dev: false @@ -765,7 +768,7 @@ packages: '@bufbuild/connect': '>=0.12.0' '@bufbuild/protobuf': '>=1.3.0' '@tanstack/react-query': ^4.20.4 - react: ^18.2.0 + react: 18.2.0 react-dom: ^18.2.0 dependencies: '@bufbuild/connect': 0.13.0(@bufbuild/protobuf@1.3.0) @@ -1548,7 +1551,7 @@ packages: /@floating-ui/react-dom@2.0.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-rZtAmSht4Lry6gdhAJDrCp/6rKN7++JnL1/Anbr/DdeyYXQPxvg/ivrbYvJulbRf4vL8b212suwMM2lxbv+RQA==} peerDependencies: - react: '>=16.8.0' + react: 18.2.0 react-dom: '>=16.8.0' dependencies: '@floating-ui/dom': 1.4.5 @@ -1560,7 +1563,7 @@ packages: resolution: {integrity: sha512-dgvcoQPJDl6h7DoACMw1MaH1KqUTOWY2ny9t+zjBnW/ZhXzQfoni1H3JmLndbfiXq/XPvtRNGGfd2hfqk8KVLg==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@graphiql/react': 0.17.6(@codemirror/language@6.0.0)(@types/node@20.3.1)(@types/react@18.2.14)(graphql@15.8.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0) @@ -1580,7 +1583,7 @@ packages: resolution: {integrity: sha512-3k1paSRbRwVNxr2U80xnRhkws8tSErWlETJvEQBmqRcWbt0+WmwFJorkLnG1n3Wj0Ho6k4a2BAiTfJ6F4SPrLg==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@graphiql/toolkit': 0.8.4(@types/node@20.3.1)(graphql@15.8.0) @@ -1624,8 +1627,8 @@ packages: - '@types/node' dev: false - /@graphql-inspector/core@5.0.0(graphql@16.7.1): - resolution: {integrity: sha512-Pj5F03EdU8K3yfd5uo0BoxG1HSiZhqG5MvAdFDiA60vHfkD/t0UOXveR6vQnbThNrSzJY8vSt0NTwlSURw0kZQ==} + /@graphql-inspector/core@5.0.1(graphql@16.7.1): + resolution: {integrity: sha512-1CWfFYucnRdULGiN1NDSinlNlpucBT+0x4i4AIthKe5n5jD9RIVyJtkA8zBbujUFrP++YE3l+TQifwbN1yTQsw==} engines: {node: '>=16.0.0'} peerDependencies: graphql: ^14.0.0 || ^15.0.0 || ^16.0.0 @@ -1633,7 +1636,7 @@ packages: dependency-graph: 0.11.0 graphql: 16.7.1 object-inspect: 1.12.3 - tslib: 2.5.3 + tslib: 2.6.0 dev: false /@graphql-tools/merge@8.3.1(graphql@16.7.1): @@ -1724,7 +1727,7 @@ packages: resolution: {integrity: sha512-OTO0XtoRQ6JPB1cKNFYBZv2Q0JMqMGNhYP1CjPvcJvjz8YGokz8oAj89HIYZGN0gZzn/4kk9iUpmMF4Q21Gsqw==} engines: {node: '>=10'} peerDependencies: - react: ^16 || ^17 || ^18 + react: 18.2.0 react-dom: ^16 || ^17 || ^18 dependencies: client-only: 0.0.1 @@ -1735,7 +1738,7 @@ packages: /@heroicons/react@2.0.18(react@18.2.0): resolution: {integrity: sha512-7TyMjRrZZMBPa+/5Y8lN0iyvUU/01PeMGX2+RE7cQWpEUIcb4QotzUObFkJDejj/HUH4qjP/eQ0gzzKs2f+6Yw==} peerDependencies: - react: '>= 16' + react: 18.2.0 dependencies: react: 18.2.0 dev: false @@ -2040,7 +2043,7 @@ packages: engines: {node: '>=14.7.0'} peerDependencies: '@types/react': '*' - react: '*' + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2058,7 +2061,7 @@ packages: peerDependencies: '@markdoc/markdoc': '*' next: '*' - react: '*' + react: 18.2.0 dependencies: '@markdoc/markdoc': 0.3.1(@types/react@18.2.14)(react@18.2.0) js-yaml: 4.1.0 @@ -2500,7 +2503,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2529,7 +2532,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2550,7 +2553,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2578,7 +2581,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2606,7 +2609,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2628,7 +2631,7 @@ packages: /@radix-ui/react-compose-refs@1.0.0(react@18.2.0): resolution: {integrity: sha512-0KaSv6sx787/hK3eF53iOkiSLwAGlFMx5lotrqD2pTjB18KbybKoEIgkNZTKC60YECDQTKGTRcDBILwZVqVKvA==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -2638,7 +2641,7 @@ packages: resolution: {integrity: sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2651,7 +2654,7 @@ packages: /@radix-ui/react-context@1.0.0(react@18.2.0): resolution: {integrity: sha512-1pVM9RfOQ+n/N5PJK33kRSKsr1glNxomxONs5c49MliinBY6Yw2Q995qfBUUo0/Mbg05B/sGA0gkgPI7kmSHBg==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -2661,7 +2664,7 @@ packages: resolution: {integrity: sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2674,7 +2677,7 @@ packages: /@radix-ui/react-dialog@1.0.0(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-Yn9YU+QlHYLWwV1XfKiqnGVpWYWk6MeBVM6x/bcoyPvxgjQGoeT35482viLPctTMWoMw0PoHgqfSox7Ig+957Q==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -2703,7 +2706,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2736,7 +2739,7 @@ packages: resolution: {integrity: sha512-RXcvnXgyvYvBEOhCBuddKecVkoMiI10Jcm5cTI7abJRAHYfFxeu+FBQs/DvdxSYucxR5mna0dNsL6QFlds5TMA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2749,7 +2752,7 @@ packages: /@radix-ui/react-dismissable-layer@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-n7kDRfx+LB1zLueRDvZ1Pd0bxdJWDUZNQ/GWoxDn2prnuJKRdxsjulejX/ePkOsLi2tTm6P24mDqlMSgQpsT6g==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -2767,7 +2770,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2792,7 +2795,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2817,7 +2820,7 @@ packages: /@radix-ui/react-focus-guards@1.0.0(react@18.2.0): resolution: {integrity: sha512-UagjDk4ijOAnGu4WMUPj9ahi7/zJJqNZ9ZAiGPp7waUWJO0O1aWXi/udPphI0IUjvrhBsZJGSN66dR2dsueLWQ==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -2827,7 +2830,7 @@ packages: resolution: {integrity: sha512-Rect2dWbQ8waGzhMavsIbmSVCgYxkXLxxR3ZvCX79JOglzdEy4JXMb98lq4hPxUbLr77nP0UOGf4rcMU+s1pUA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2840,7 +2843,7 @@ packages: /@radix-ui/react-focus-scope@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-C4SWtsULLGf/2L4oGeIHlvWQx7Rf+7cX/vKOAD2dXW0A1b5QXwi3wWeaEgW+wn+SEVrraMUk05vLU9fZZz5HbQ==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -2856,7 +2859,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2877,7 +2880,7 @@ packages: /@radix-ui/react-icons@1.3.0(react@18.2.0): resolution: {integrity: sha512-jQxj/0LKgp+j9BiTXz3O3sgs26RNet2iLWmsPyRz2SIcR4q/4SbazXfnYwbAr+vLYKSfc7qxzyGQA1HLlYiuNw==} peerDependencies: - react: ^16.x || ^17.x || ^18.x + react: 18.2.0 dependencies: react: 18.2.0 dev: false @@ -2885,7 +2888,7 @@ packages: /@radix-ui/react-id@1.0.0(react@18.2.0): resolution: {integrity: sha512-Q6iAB/U7Tq3NTolBBQbHTgclPmGWE3OlktGGqrClPozSw4vkQ1DfQAOtzgRPecKsMdJINE05iaoDUG8tRzCBjw==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 '@radix-ui/react-use-layout-effect': 1.0.0(react@18.2.0) @@ -2896,7 +2899,7 @@ packages: resolution: {integrity: sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -2912,7 +2915,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2933,7 +2936,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -2971,7 +2974,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3006,7 +3009,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3034,7 +3037,7 @@ packages: /@radix-ui/react-portal@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-a8qyFO/Xb99d8wQdu4o7qnigNjTPG123uADNecz0eX4usnQEj7o+cG4ZX4zkqq98NYekT7UoEQIjxBNWIFuqTA==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -3048,7 +3051,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3067,7 +3070,7 @@ packages: /@radix-ui/react-presence@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-A+6XEvN01NfVWiKu38ybawfHsBjWum42MRPnEuqPsBZ4eV7e/7K321B5VgYMPv3Xx5An6o1/l9ZuDBgmcmWK3w==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -3082,7 +3085,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3102,7 +3105,7 @@ packages: /@radix-ui/react-primitive@1.0.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EyXe6mnRlHZ8b6f4ilTDrXmkLShICIuOTTj0GX4w1rp+wSxf3+TD05u1UOITC8VsJ2a9nwHvdXtOXEOl0Cw/zQ==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 dependencies: '@babel/runtime': 7.22.6 @@ -3116,7 +3119,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3137,7 +3140,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3166,7 +3169,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3207,7 +3210,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3226,7 +3229,7 @@ packages: /@radix-ui/react-slot@1.0.0(react@18.2.0): resolution: {integrity: sha512-3mrKauI/tWXo1Ll+gN5dHcxDPdm/Df1ufcDLCecn+pnCIVcdWE7CujXo8QaXOWRJyZyQWWbpB8eFwHzWXlv5mQ==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 '@radix-ui/react-compose-refs': 1.0.0(react@18.2.0) @@ -3237,7 +3240,7 @@ packages: resolution: {integrity: sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3253,7 +3256,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3281,7 +3284,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3313,7 +3316,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3343,7 +3346,7 @@ packages: /@radix-ui/react-use-callback-ref@1.0.0(react@18.2.0): resolution: {integrity: sha512-GZtyzoHz95Rhs6S63D2t/eqvdFCm7I+yHMLVQheKM7nBD8mbZIt+ct1jz4536MDnaOGKIxynJ8eHTkVGVVkoTg==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -3353,7 +3356,7 @@ packages: resolution: {integrity: sha512-D94LjX4Sp0xJFVaoQOd3OO9k7tpBYNOXdVhkltUbGv2Qb9OXdrg/CpsjlZv7ia14Sylv398LswWBVVu5nqKzAQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3366,7 +3369,7 @@ packages: /@radix-ui/react-use-controllable-state@1.0.0(react@18.2.0): resolution: {integrity: sha512-FohDoZvk3mEXh9AWAVyRTYR4Sq7/gavuofglmiXB2g1aKyboUD4YtgWxKj8O5n+Uak52gXQ4wKz5IFST4vtJHg==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) @@ -3377,7 +3380,7 @@ packages: resolution: {integrity: sha512-Svl5GY5FQeN758fWKrjM6Qb7asvXeiZltlT4U2gVfl8Gx5UAv2sMR0LWo8yhsIZh2oQ0eFdZ59aoOOMV7b47VA==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3391,7 +3394,7 @@ packages: /@radix-ui/react-use-escape-keydown@1.0.0(react@18.2.0): resolution: {integrity: sha512-JwfBCUIfhXRxKExgIqGa4CQsiMemo1Xt0W/B4ei3fpzpvPENKpMKQ8mZSB6Acj3ebrAEgi2xiQvcI1PAAodvyg==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 '@radix-ui/react-use-callback-ref': 1.0.0(react@18.2.0) @@ -3402,7 +3405,7 @@ packages: resolution: {integrity: sha512-vyL82j40hcFicA+M4Ex7hVkB9vHgSse1ZWomAqV2Je3RleKGO5iM8KMOEtfoSB0PnIelMd2lATjTGMYqN5ylTg==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3416,7 +3419,7 @@ packages: /@radix-ui/react-use-layout-effect@1.0.0(react@18.2.0): resolution: {integrity: sha512-6Tpkq+R6LOlmQb1R5NNETLG0B4YP0wc+klfXafpUCj6JGyaUc8il7/kUZ7m59rGbXGczE9Bs+iz2qloqsZBduQ==} peerDependencies: - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -3426,7 +3429,7 @@ packages: resolution: {integrity: sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3440,7 +3443,7 @@ packages: resolution: {integrity: sha512-cV5La9DPwiQ7S0gf/0qiD6YgNqM5Fk97Kdrlc5yBcrF3jyEZQwm7vYFqMo4IfeHgJXsRaMvLABFtd0OVEmZhDw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3454,7 +3457,7 @@ packages: resolution: {integrity: sha512-Cq5DLuSiuYVKNU8orzJMbl15TXilTnJKUCltMVQg53BQOF1/C5toAaGrowkgksdBQ9H+SRL23g0HDmg9tvmxXw==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3469,7 +3472,7 @@ packages: resolution: {integrity: sha512-ibay+VqrgcaI6veAojjofPATwledXiSmX+C0KrBk/xgpX9rBzPV3OsfwlhQdUOFbh+LKQorLYT+xTXW9V8yd0g==} peerDependencies: '@types/react': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -3485,7 +3488,7 @@ packages: peerDependencies: '@types/react': '*' '@types/react-dom': '*' - react: ^16.8 || ^17.0 || ^18.0 + react: 18.2.0 react-dom: ^16.8 || ^17.0 || ^18.0 peerDependenciesMeta: '@types/react': @@ -3510,7 +3513,7 @@ packages: /@reach/auto-id@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-ud8iPwF52RVzEmkHq1twuqGuPA+moreumUHdtgvU3sr3/15BNhwp3KyDLrKKSz0LP1r3V4pSdyF9MbYM8BoSjA==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3522,7 +3525,7 @@ packages: /@reach/combobox@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-2mYvU5agOBCQBMdlM4cri+P1BbNwp05P1OuDyc33xJSNiBG7BMy4+ZSHJ0X4fyle6rHwSgCAOCLOeWV1XUYjoQ==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3540,7 +3543,7 @@ packages: /@reach/descendants@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-c7lUaBfjgcmKFZiAWqhG+VnXDMEhPkI4kAav/82XKZD6NVvFjsQOTH+v3tUkskrAPV44Yuch0mFW/u5Ntifr7Q==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3552,7 +3555,7 @@ packages: /@reach/dialog@0.17.0(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AnfKXugqDTGbeG3c8xDcrQDE4h9b/vnc27Sa118oQSquz52fneUeX9MeFb5ZEiBJK8T5NJpv7QUTBIKnFCAH5A==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3570,7 +3573,7 @@ packages: /@reach/dropdown@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-qBTIGInhxtPHtdj4Pl2XZgZMz3e37liydh0xR3qc48syu7g71sL4nqyKjOzThykyfhA3Pb3/wFgsFJKGTSdaig==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3585,7 +3588,7 @@ packages: /@reach/listbox@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AMnH1P6/3VKy2V/nPb4Es441arYR+t4YRdh9jdcFVrCOD6y7CQrlmxsYjeg9Ocdz08XpdoEBHM3PKLJqNAUr7A==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3601,7 +3604,7 @@ packages: /@reach/machine@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-9EHnuPgXzkbRENvRUzJvVvYt+C2jp7PGN0xon7ffmKoK8rTO6eA/bb7P0xgloyDDQtu88TBUXKzW0uASqhTXGA==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3614,7 +3617,7 @@ packages: /@reach/menu-button@0.17.0(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0): resolution: {integrity: sha512-YyuYVyMZKamPtivoEI6D0UEILYH3qZtg4kJzEAuzPmoR/aHN66NZO75Fx0gtjG1S6fZfbiARaCOZJC0VEiDOtQ==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x react-is: ^16.8.0 || 17.x dependencies: @@ -3636,7 +3639,7 @@ packages: /@reach/popover@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-yYbBF4fMz4Ml4LB3agobZjcZ/oPtPsNv70ZAd7lEC2h7cvhF453pA+zOBGYTPGupKaeBvgAnrMjj7RnxDU5hoQ==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/portal': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3651,7 +3654,7 @@ packages: /@reach/portal@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+IxsgVycOj+WOeNPL2NdgooUdHPSY285wCtj/iWID6akyr4FgGUK7sMhRM9aGFyrGpx2vzr+eggbUmAVZwOz+A==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/utils': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3664,7 +3667,7 @@ packages: /@reach/rect@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-3YB7KA5cLjbLc20bmPkJ06DIfXSK06Cb5BbD2dHgKXjUkT9WjZaLYIbYCO8dVjwcyO3GCNfOmPxy62VsPmZwYA==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/observe-rect': 1.2.0 @@ -3679,7 +3682,7 @@ packages: /@reach/tooltip@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HP8Blordzqb/Cxg+jnhGmWQfKgypamcYLBPlcx6jconyV5iLJ5m93qipr1giK7MqKT2wlsKWy44ZcOrJ+Wrf8w==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: '@reach/auto-id': 0.17.0(react-dom@18.2.0)(react@18.2.0) @@ -3697,7 +3700,7 @@ packages: /@reach/utils@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-M5y8fCBbrWeIsxedgcSw6oDlAMQDkl5uv3VnMVJ7guwpf4E48Xlh1v66z/1BgN/WYe2y8mB/ilFD2nysEfdGeA==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: react: 18.2.0 @@ -3709,7 +3712,7 @@ packages: /@reach/visually-hidden@0.17.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-T6xF3Nv8vVnjVkGU6cm0+kWtvliLqPAo8PcZ+WxkKacZsaHTjaZb4v1PaCcyQHmuTNT/vtTVNOJLG0SjQOIb7g==} peerDependencies: - react: ^16.8.0 || 17.x + react: 18.2.0 react-dom: ^16.8.0 || 17.x dependencies: prop-types: 15.8.1 @@ -3721,7 +3724,7 @@ packages: /@reactflow/background@11.2.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-gOtae79Zx1zOs9tD4tmKfuiQQOyG0O81BWBCHqlAQgemKlYExElFKOC67lgTDZ4GGFK+4sXrgrWQ5h14hzaFgg==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/core': 11.7.2(react-dom@18.2.0)(react@18.2.0) @@ -3736,7 +3739,7 @@ packages: /@reactflow/controls@11.1.13(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-rA74+mbt2bnz9Fba6JL1JsKHNNEK6Nl70+ssfOLKMpRFIg512IroayBWufgPJB82X9dgMIzZfx/UcEFFUFJQ8Q==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/core': 11.7.2(react-dom@18.2.0)(react@18.2.0) @@ -3751,7 +3754,7 @@ packages: /@reactflow/core@11.7.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-AhszxILp1RNk3SwrnC/eYh1XsOil5yzthYG5k+oTpvLH5H3BtIWIVkeLEJwmL611lPKL3JuScfjliUxBm9128w==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@types/d3': 7.4.0 @@ -3772,7 +3775,7 @@ packages: /@reactflow/minimap@11.5.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-564gP/GMZKaJjVRGIrVLv2gIjgc89+qvNwuZzHnQLXjBw5+nS/QkW57Qx/M33MxVAaM+Z5rJ8gKknMSnxekwvQ==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/core': 11.7.2(react-dom@18.2.0)(react@18.2.0) @@ -3791,7 +3794,7 @@ packages: /@reactflow/node-resizer@2.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-DVL8nnWsltP8/iANadAcTaDB4wsEkx2mOLlBEPNE3yc5loSm3u9l5m4enXRcBym61MiMuTtDPzZMyYYQUjuYIg==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/core': 11.7.2(react-dom@18.2.0)(react@18.2.0) @@ -3808,7 +3811,7 @@ packages: /@reactflow/node-toolbar@1.2.1(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-EcMUMzJGAACYQTiUaBm3zxeiiKCPwU2MaoDeZdnEMbvq+2SfohKOur6JklANjv30kL+Pf3xj8QopEtyKTS4XrA==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/core': 11.7.2(react-dom@18.2.0)(react@18.2.0) @@ -3905,7 +3908,7 @@ packages: /@tanstack/react-query@4.33.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-97nGbmDK0/m0B86BdiXzx3EW9RcDYKpnyL2+WwyuLHEgpfThYAnXFaMMmnTDuAO4bQJXEhflumIEUfKmP7ESGA==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 react-native: '*' peerDependenciesMeta: @@ -3923,7 +3926,7 @@ packages: resolution: {integrity: sha512-Ng9rdm3JPoSCi6cVZvANsYnF+UoGVRxflMb270tVj0+LjeT/ZtZ9ckxF6oLPLcKesza6VKBqtdF9mQ+vaz24Aw==} engines: {node: '>=12'} peerDependencies: - react: '>=16' + react: 18.2.0 react-dom: '>=16' dependencies: '@tanstack/table-core': 8.9.3 @@ -4651,7 +4654,6 @@ packages: dependencies: clean-stack: 2.2.0 indent-string: 4.0.0 - dev: true /aggregate-error@4.0.1: resolution: {integrity: sha512-0poP0T7el6Vq3rstR8Mn4V/IQrpBLO6POkUSrN7RhyY+GF/InCFShQzsQ39T25gkHhLgSLByyAz+Kjb+c2L98w==} @@ -5016,6 +5018,11 @@ packages: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} dev: true + /buffer-writer@2.0.0: + resolution: {integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==} + engines: {node: '>=4'} + dev: false + /buffer@5.7.1: resolution: {integrity: sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==} dependencies: @@ -5256,7 +5263,6 @@ packages: /clean-stack@2.2.0: resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==} engines: {node: '>=6'} - dev: true /clean-stack@4.2.0: resolution: {integrity: sha512-LYv6XPxoyODi36Dp976riBtSY27VmFo+MKqEU9QCCWyTrdEPDog+RWA7xQWHi6Vbp61j5c4cdzzX1NidnwtUWg==} @@ -5360,7 +5366,7 @@ packages: /cmdk@0.2.0(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-JQpKvEOb86SnvMZbYaFKYhvzFntWBeSZdyii0rZPhKJj9uwJBxu4DaVYDrRN7r3mPop56oPhRw+JYWTKs66TYw==} peerDependencies: - react: ^18.0.0 + react: 18.2.0 react-dom: ^18.0.0 dependencies: '@radix-ui/react-dialog': 1.0.0(@types/react@18.2.14)(react-dom@18.2.0)(react@18.2.0) @@ -5646,6 +5652,13 @@ packages: /create-require@1.1.1: resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + /cron-parser@4.9.0: + resolution: {integrity: sha512-p0SaNjrHOnQeR8/VnfGbmg9te2kfyYSQ7Sc/j/6DtPL3JQvKxmjO9TSjNFpujqV3vEYYBvNNvXSxzyksBWAx1Q==} + engines: {node: '>=12.0.0'} + dependencies: + luxon: 3.4.2 + dev: false + /cross-env@7.0.3: resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} @@ -5970,6 +5983,11 @@ packages: slash: 4.0.0 dev: true + /delay@5.0.0: + resolution: {integrity: sha512-ReEBKkIfe4ya47wlPYf/gu5ib6yUG0/Aez0JQZQz94kiWtRQvZIQbTiehsnwHvLSWJnQdhVeqYue7Id1dKr0qw==} + engines: {node: '>=10'} + dev: false + /delayed-stream@1.0.0: resolution: {integrity: sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==} engines: {node: '>=0.4.0'} @@ -6088,12 +6106,12 @@ packages: hanji: 0.0.5 json-diff: 0.9.0 minimatch: 7.4.6 - zod: 3.21.4 + zod: 3.22.2 transitivePeerDependencies: - supports-color dev: true - /drizzle-orm@0.28.5(postgres@3.3.5): + /drizzle-orm@0.28.5(pg@8.11.3)(postgres@3.3.5): resolution: {integrity: sha512-6r6Iw4c38NAmW6TiKH3TUpGUQ1YdlEoLJOQptn8XPx3Z63+vFNKfAiANqrIiYZiMjKR9+NYAL219nFrmo1duXA==} peerDependencies: '@aws-sdk/client-rds-data': '>=3' @@ -6155,6 +6173,7 @@ packages: sqlite3: optional: true dependencies: + pg: 8.11.3 postgres: 3.3.5 dev: false @@ -7217,8 +7236,8 @@ packages: resolution: {integrity: sha512-stRHYGeuqpEZTL1Ef0Ovr2ltazUT9g844X5z/zEBFLG8RYlpDiOCIG+ATvYEp+/zmc7sN29mcIMp8gvYplYPIQ==} dev: false - /fastify@4.21.0: - resolution: {integrity: sha512-tsu4bcwE4HetxqW8prA5fbC9bKHMYDp7jGEDWyzK1l90a3uOaLoIcQbdGcWeODNLVJviQnzh1wvIjTZE3MJFEg==} + /fastify@4.22.0: + resolution: {integrity: sha512-HLoBmetdQ6zaJohKW6jzUww8NnwHzkbIbUEyAzM+Nnf7cZVSXRuUV+6b2/xLmu6GGkruIFJ/bIQoKWYRx4wnAQ==} dependencies: '@fastify/ajv-compiler': 3.5.0 '@fastify/error': 3.3.0 @@ -7229,7 +7248,7 @@ packages: fast-json-stringify: 5.7.0 find-my-way: 7.6.2 light-my-request: 5.10.0 - pino: 8.14.1 + pino: 8.15.0 process-warning: 2.2.0 proxy-addr: 2.0.7 rfdc: 1.3.0 @@ -7709,7 +7728,7 @@ packages: resolution: {integrity: sha512-fZC/wsuatqiQDO2otchxriFO0LaWIo/ovF/CQJ1yOudmY0P7pzDiP+l9CEHUiWbizk3e99x6DQG4XG1VxA+d6A==} peerDependencies: graphql: ^0.6.0 || ^0.7.0 || ^0.8.0-b || ^0.9.0 || ^0.10.0 || ^0.11.0 || ^0.12.0 || ^0.13.0 || ^14.0.0 || ^15.0.0 - react: ^15.6.0 || ^16.0.0 + react: 18.2.0 react-dom: ^15.6.0 || ^16.0.0 dependencies: graphql: 15.8.0 @@ -7721,7 +7740,7 @@ packages: resolution: {integrity: sha512-Fm3fVI65EPyXy+PdbeQUyODTwl2NhpZ47msGnGwpDvdEzYdgF7pPrxL96xCfF31KIauS4+ceEJ+ZwEe5iLWiQw==} peerDependencies: graphql: ^15.5.0 || ^16.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.8.0 || ^17.0.0 || ^18.0.0 dependencies: '@graphiql/react': 0.17.6(@codemirror/language@6.0.0)(@types/node@20.3.1)(@types/react@18.2.14)(graphql@15.8.0)(react-dom@18.2.0)(react-is@17.0.2)(react@18.2.0) @@ -8011,7 +8030,6 @@ packages: /indent-string@4.0.0: resolution: {integrity: sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==} engines: {node: '>=8'} - dev: true /indent-string@5.0.0: resolution: {integrity: sha512-m6FAo/spmsW2Ab2fU35JTYwtOKa2yAwXSwgjSv1TJzh4Mh7mC3lzAOVLBprb72XsTrgkEIsl7YrFNAiDiRhIGg==} @@ -8655,6 +8673,10 @@ packages: resolution: {integrity: sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==} dev: true + /lodash.debounce@4.0.8: + resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==} + dev: false + /lodash.isfunction@3.0.9: resolution: {integrity: sha512-AirXNj15uRIMMPihnkInB4i3NHeb4iBtNg9WRWuK2o31S+ePwwNmDPaTL3o7dTJ+VXNZim7rFs4rxN4YU1oUJw==} dev: true @@ -8766,6 +8788,11 @@ packages: es5-ext: 0.10.62 dev: true + /luxon@3.4.2: + resolution: {integrity: sha512-uBoAVCVcajsrqy3pv7eo5jEUz1oeLmCcnMv8n4AJpT5hbpN9lUssAXibNElpbLce3Mhm9dyBzwYLs9zctM/0tA==} + engines: {node: '>=12'} + dev: false + /magic-string@0.30.1: resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} engines: {node: '>=12'} @@ -9121,7 +9148,7 @@ packages: resolution: {integrity: sha512-B+AKNfYNIzh0vqQQKqQItTS8evEouKD7H5Hj3kmuPERwddR2TxvDSFZuTj6T7Jfn1oyeUyJMydPl1Bkxkh0W7A==} peerDependencies: next: '*' - react: '*' + react: 18.2.0 react-dom: '*' dependencies: next: 13.4.19(react-dom@18.2.0)(react@18.2.0) @@ -9139,7 +9166,7 @@ packages: hasBin: true peerDependencies: '@opentelemetry/api': ^1.1.0 - react: ^18.2.0 + react: 18.2.0 react-dom: ^18.2.0 sass: ^1.3.0 peerDependenciesMeta: @@ -9579,7 +9606,6 @@ packages: engines: {node: '>=10'} dependencies: aggregate-error: 3.1.0 - dev: true /p-map@5.5.0: resolution: {integrity: sha512-VFqfGDHlx87K66yZrNdI4YGtD70IRyd+zSvgks6mzHPRNkoKy+9EKP4SFC77/vTTQYmRmti7dvqC+m5jBrBAcg==} @@ -9626,6 +9652,10 @@ packages: engines: {node: '>=6'} dev: true + /packet-reader@1.0.0: + resolution: {integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==} + dev: false + /pacote@15.2.0: resolution: {integrity: sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==} engines: {node: ^14.17.0 || ^16.13.0 || >=18.0.0} @@ -9780,6 +9810,85 @@ packages: through: 2.3.8 dev: true + /pg-boss@9.0.3: + resolution: {integrity: sha512-cUWUiv3sr563yNy0nCZ25Tv5U0m59Y9MhX/flm0vTR012yeVCrqpfboaZP4xFOQPdWipMJpuu4g94HR0SncTgw==} + engines: {node: '>=16'} + dependencies: + cron-parser: 4.9.0 + delay: 5.0.0 + lodash.debounce: 4.0.8 + p-map: 4.0.0 + pg: 8.11.3 + serialize-error: 8.1.0 + uuid: 9.0.0 + transitivePeerDependencies: + - pg-native + dev: false + + /pg-cloudflare@1.1.1: + resolution: {integrity: sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==} + requiresBuild: true + dev: false + optional: true + + /pg-connection-string@2.6.2: + resolution: {integrity: sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==} + dev: false + + /pg-int8@1.0.1: + resolution: {integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==} + engines: {node: '>=4.0.0'} + dev: false + + /pg-pool@3.6.1(pg@8.11.3): + resolution: {integrity: sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==} + peerDependencies: + pg: '>=8.0' + dependencies: + pg: 8.11.3 + dev: false + + /pg-protocol@1.6.0: + resolution: {integrity: sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==} + dev: false + + /pg-types@2.2.0: + resolution: {integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==} + engines: {node: '>=4'} + dependencies: + pg-int8: 1.0.1 + postgres-array: 2.0.0 + postgres-bytea: 1.0.0 + postgres-date: 1.0.7 + postgres-interval: 1.2.0 + dev: false + + /pg@8.11.3: + resolution: {integrity: sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==} + engines: {node: '>= 8.0.0'} + peerDependencies: + pg-native: '>=3.0.1' + peerDependenciesMeta: + pg-native: + optional: true + dependencies: + buffer-writer: 2.0.0 + packet-reader: 1.0.0 + pg-connection-string: 2.6.2 + pg-pool: 3.6.1(pg@8.11.3) + pg-protocol: 1.6.0 + pg-types: 2.2.0 + pgpass: 1.0.5 + optionalDependencies: + pg-cloudflare: 1.1.1 + dev: false + + /pgpass@1.0.5: + resolution: {integrity: sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==} + dependencies: + split2: 4.2.0 + dev: false + /picocolors@1.0.0: resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} @@ -9854,6 +9963,23 @@ packages: thread-stream: 2.3.0 dev: false + /pino@8.15.0: + resolution: {integrity: sha512-olUADJByk4twxccmAxb1RiGKOSvddHugCV3wkqjyv+3Sooa2KLrmXrKEWOKi0XPCLasRR5jBXxioE1jxUa4KzQ==} + hasBin: true + dependencies: + atomic-sleep: 1.0.0 + fast-redact: 3.2.0 + on-exit-leak-free: 2.1.0 + pino-abstract-transport: 1.0.0 + pino-std-serializers: 6.2.2 + process-warning: 2.2.0 + quick-format-unescaped: 4.0.4 + real-require: 0.2.0 + safe-stable-stringify: 2.4.3 + sonic-boom: 3.3.0 + thread-stream: 2.3.0 + dev: false + /pirates@4.0.6: resolution: {integrity: sha512-saLsH7WeYYPiD25LDuLRRY/i+6HaPYr6G1OUlN39otzkSTxKnubR9RTxS3/Kk50s1g2JTgFwWQDQyplC5/SHZg==} engines: {node: '>= 6'} @@ -9962,6 +10088,28 @@ packages: picocolors: 1.0.0 source-map-js: 1.0.2 + /postgres-array@2.0.0: + resolution: {integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==} + engines: {node: '>=4'} + dev: false + + /postgres-bytea@1.0.0: + resolution: {integrity: sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-date@1.0.7: + resolution: {integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==} + engines: {node: '>=0.10.0'} + dev: false + + /postgres-interval@1.2.0: + resolution: {integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==} + engines: {node: '>=0.10.0'} + dependencies: + xtend: 4.0.2 + dev: false + /postgres@3.3.5: resolution: {integrity: sha512-+JD93VELV9gHkqpV5gdL5/70HdGtEw4/XE1S4BC8f1mcPmdib3K5XsKVbnR1XcAyC41zOnifJ+9YRKxdIsXiUw==} dev: false @@ -10042,7 +10190,7 @@ packages: /prism-react-renderer@2.0.6(react@18.2.0): resolution: {integrity: sha512-ERzmAI5UvrcTw5ivfEG20/dYClAsC84eSED5p9X3oKpm0xPV4A5clFK1mp7lPIdKmbLnQYsPTGiOI7WS6gWigw==} peerDependencies: - react: '>=16.0.0' + react: 18.2.0 dependencies: '@types/prismjs': 1.26.0 clsx: 1.2.1 @@ -10162,7 +10310,7 @@ packages: /react-clientside-effect@1.2.6(react@18.2.0): resolution: {integrity: sha512-XGGGRQAKY+q25Lz9a/4EPqom7WRjz3z9R2k4jhVKA/puQFH/5Nt27vFZYql4m4NVNdUvX8PS3O7r/Zzm7cjUlg==} peerDependencies: - react: ^15.3.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 react: 18.2.0 @@ -10172,7 +10320,7 @@ packages: resolution: {integrity: sha512-+9t0HyClbCqw1IhYbpWecjsiaftCeRN5cdhsi9v06YdimwyMR2yYHWcgVn3URwtN/txhqKpEZB6UX1fHpvK76w==} peerDependencies: date-fns: 2.0.0-alpha.7 || >=2.0.0 - react: ^0.14 || ^15.0.0-rc || >=15.0 + react: 18.2.0 dependencies: classnames: 2.3.2 date-fns: 2.30.0 @@ -10186,7 +10334,7 @@ packages: resolution: {integrity: sha512-QIC3uOuyGGbtypbd5QEggsCSqVaPNu8kzUWquZ7JjW9fuWB9yv7WyixKmnaFelTLXFdq7h7zU6n/aBleBqe/dA==} peerDependencies: date-fns: ^2.28.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 dependencies: date-fns: 2.30.0 react: 18.2.0 @@ -10195,7 +10343,7 @@ packages: /react-dom@18.2.0(react@18.2.0): resolution: {integrity: sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==} peerDependencies: - react: ^18.2.0 + react: 18.2.0 dependencies: loose-envify: 1.4.0 react: 18.2.0 @@ -10205,7 +10353,7 @@ packages: resolution: {integrity: sha512-h6vrdgUbsH2HeD5I7I3Cx1PPrmwGuKYICS+kB9m+32X/9xHRrAbxgvaBpG7BFBN9h3tO+C3qX1QAVESmi4CiIA==} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10224,7 +10372,7 @@ packages: resolution: {integrity: sha512-6dWoFJwycbuFfw/iKMcl+RdAOAOHDiF11KWYhNDRN/OkUt+Di5qsZHwA0OwsVnu9y135gkHpTw9DJA+WzCeR9w==} engines: {node: '>=12.22.0'} peerDependencies: - react: ^16.8.0 || ^17 || ^18 + react: 18.2.0 dependencies: react: 18.2.0 dev: false @@ -10232,7 +10380,7 @@ packages: /react-icons@4.10.1(react@18.2.0): resolution: {integrity: sha512-/ngzDP/77tlCfqthiiGNZeYFACw85fUjZtLbedmJ5DTlNDIwETxhwBzdOJ21zj4iJdvc0J3y7yOsX3PpxAJzrw==} peerDependencies: - react: '*' + react: 18.2.0 dependencies: react: 18.2.0 dev: false @@ -10254,7 +10402,7 @@ packages: /react-list@0.8.17(react@18.2.0): resolution: {integrity: sha512-pgmzGi0G5uGrdHzMhgO7KR1wx5ZXVvI3SsJUmkblSAKtewIhMwbQiMuQiTE83ozo04BQJbe0r3WIWzSO0dR1xg==} peerDependencies: - react: 0.14 || 15 - 18 + react: 18.2.0 dependencies: prop-types: 15.8.1 react: 18.2.0 @@ -10263,7 +10411,7 @@ packages: /react-move-hook@0.1.2(react@18.2.0): resolution: {integrity: sha512-kl3CCIbvu8BgqKZ1FGNyfUtS0x2cAMf1DMrooBFZ5hbtVPD6z9Q855LhzbgRnsPBa830ryIh+RFJegmkentv+Q==} peerDependencies: - react: '>=16.8' + react: 18.2.0 dependencies: react: 18.2.0 dev: false @@ -10273,7 +10421,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10289,7 +10437,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10308,7 +10456,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10327,7 +10475,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10344,7 +10492,7 @@ packages: /react-resize-detector@8.1.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-S7szxlaIuiy5UqLhLL1KY3aoyGHbZzsTpYal9eYMwCyKqoqoVLCmIgAgNyIM1FhnP2KyBygASJxdhejrzjMb+w==} peerDependencies: - react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: lodash: 4.17.21 @@ -10356,7 +10504,7 @@ packages: resolution: {integrity: sha512-yl4y3XiMorss7ayF5QnBiSprig0+qFHui8uh7Hgg46QX5O+aRMRKlfGGNGLHno35JkQSvSYY8eCWkBfHfrSHfg==} peerDependencies: prop-types: ^15.6.0 - react: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^15.0.0 || ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: fast-equals: 5.0.1 @@ -10371,7 +10519,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -10386,7 +10534,7 @@ packages: /react-transition-group@2.9.0(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-+HzNTCHpeQyl4MJ/bdE0u6XRMe9+XG/+aL4mCxVN4DnPBQ0/5bfHWPDuOZUzYdMj94daZaZdCCc1Dzt9R/xSSg==} peerDependencies: - react: '>=15.0.0' + react: 18.2.0 react-dom: '>=15.0.0' dependencies: dom-helpers: 3.4.0 @@ -10406,7 +10554,7 @@ packages: /reactflow@11.7.2(react-dom@18.2.0)(react@18.2.0): resolution: {integrity: sha512-HBudD8BwZacOMqX8fbkiXbeBQs3nRezWVLCDurfc+tTeHsA7988uyaIOhrnKgYCcKtlpJaspsnxDZk+5JmmHxA==} peerDependencies: - react: '>=17' + react: 18.2.0 react-dom: '>=17' dependencies: '@reactflow/background': 11.2.2(react-dom@18.2.0)(react@18.2.0) @@ -10556,7 +10704,7 @@ packages: engines: {node: '>=12'} peerDependencies: prop-types: ^15.6.0 - react: ^16.0.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 react-dom: ^16.0.0 || ^17.0.0 || ^18.0.0 dependencies: classnames: 2.3.2 @@ -10824,6 +10972,13 @@ packages: upper-case-first: 2.0.2 dev: false + /serialize-error@8.1.0: + resolution: {integrity: sha512-3NnuWfM6vBYoy5gZFvHiYsVbafvI9vZv/+jlIigFn4oP4zjNPK3LhcY0xSCgeb1a5L8jO71Mit9LlNoi2UfDDQ==} + engines: {node: '>=10'} + dependencies: + type-fest: 0.20.2 + dev: false + /set-blocking@2.0.0: resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==} dev: true @@ -11224,7 +11379,7 @@ packages: peerDependencies: '@babel/core': '*' babel-plugin-macros: '*' - react: '>= 16.8.0 || 17.x.x || ^18.0.0-0' + react: 18.2.0 peerDependenciesMeta: '@babel/core': optional: true @@ -11525,8 +11680,8 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib@2.5.3: - resolution: {integrity: sha512-mSxlJJwl3BMEQCUNnxXBU9jP4JBktcEGhURcPR6VQVlnP0FdDEsIaz0C35dXNGLyRfrATNofF0F5p2KPxQgB+w==} + /tslib@2.6.0: + resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} dev: false /tslib@2.6.1: @@ -11593,7 +11748,6 @@ packages: /type-fest@0.20.2: resolution: {integrity: sha512-Ne+eE4r0/iWnpAxD852z3A+N0Bt5RN//NjJwRd2VFHEmrywxf5vsZlh4R6lixl6B+wz/8d+maTSAkN1FIkI3LQ==} engines: {node: '>=10'} - dev: true /type-fest@0.21.3: resolution: {integrity: sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==} @@ -11793,7 +11947,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.8.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -11807,7 +11961,7 @@ packages: resolution: {integrity: sha512-kbeNVZ9Zkc0RFGpfMN3MNfaKNvcLNyxOAAd9O4CBZ+kCBXXscn9s/4I+8ytUER4RDpEYs5+O6Rs4PqiZ+rHr5Q==} engines: {node: '>=10', npm: '>=6'} peerDependencies: - react: '>=16.13' + react: 18.2.0 dependencies: '@babel/runtime': 7.22.6 dequal: 2.0.3 @@ -11819,7 +11973,7 @@ packages: engines: {node: '>=10'} peerDependencies: '@types/react': ^16.9.0 || ^17.0.0 || ^18.0.0 - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 peerDependenciesMeta: '@types/react': optional: true @@ -11833,7 +11987,7 @@ packages: /use-sync-external-store@1.2.0(react@18.2.0): resolution: {integrity: sha512-eEgnFxGQ1Ife9bzYs6VLi8/4X6CObHMw9Qr9tPY43iKwsPw8xE8+EFsf/2cFZ5S3esXgpWgtSCtLNS41F+sKPA==} peerDependencies: - react: ^16.8.0 || ^17.0.0 || ^18.0.0 + react: 18.2.0 dependencies: react: 18.2.0 @@ -11843,7 +11997,6 @@ packages: /uuid@9.0.0: resolution: {integrity: sha512-MXcSTerfPa4uqyzStbRoTgt5XIe3x5+42+q1sDuy3R5MDk66URdLMOZe5aPX/SQd+kuYAh0FdP/pO28IkQyTeg==} hasBin: true - dev: true /v8-compile-cache-lib@3.0.1: resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} @@ -12247,13 +12400,17 @@ packages: /zod@3.21.4: resolution: {integrity: sha512-m46AKbrzKVzOzs/DZgVnG5H55N1sv1M8qZU3A8RIKbs3mrACDNeIOeilDymVb2HdmP8uwshOCF4uJ8uM9rCqJw==} + dev: false + + /zod@3.22.2: + resolution: {integrity: sha512-wvWkphh5WQsJbVk1tbx1l1Ly4yg+XecD+Mq280uBGt9wa5BKSWf4Mhp6GmrkPixhMxmabYY7RbzlwVP32pbGCg==} /zustand@4.3.9(react@18.2.0): resolution: {integrity: sha512-Tat5r8jOMG1Vcsj8uldMyqYKC5IZvQif8zetmLHs9WoZlntTHmIoNM8TpLRY31ExncuUvUOXehd0kvahkuHjDw==} engines: {node: '>=12.7.0'} peerDependencies: immer: '>=9.0' - react: '>=16.8' + react: 18.2.0 peerDependenciesMeta: immer: optional: true diff --git a/router/go.mod b/router/go.mod index 58a5cbef7c..ccd9ba40ca 100644 --- a/router/go.mod +++ b/router/go.mod @@ -7,25 +7,25 @@ require ( github.com/buger/jsonparser v1.1.1 github.com/cespare/xxhash v1.1.0 github.com/dgraph-io/ristretto v0.1.1 - github.com/gin-contrib/requestid v0.0.6 - github.com/gin-contrib/zap v0.1.0 - github.com/gin-gonic/gin v1.9.1 + github.com/go-chi/chi v1.5.4 github.com/go-playground/validator/v10 v10.14.1 - github.com/gorilla/mux v1.8.0 github.com/hashicorp/go-multierror v1.1.1 github.com/joho/godotenv v1.5.1 github.com/kelseyhightower/envconfig v1.4.0 github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 - github.com/rs/cors v1.9.0 - github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950 + github.com/prometheus/client_golang v1.15.1 github.com/stretchr/testify v1.8.4 github.com/tidwall/gjson v1.14.4 github.com/tidwall/sjson v1.2.5 github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230822083323-a115bc0c7af6 go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 go.opentelemetry.io/otel v1.16.0 + go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0 go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 + go.opentelemetry.io/otel/exporters/prometheus v0.39.0 + go.opentelemetry.io/otel/metric v1.16.0 go.opentelemetry.io/otel/sdk v1.16.0 + go.opentelemetry.io/otel/sdk/metric v0.39.0 go.opentelemetry.io/otel/trace v1.16.0 go.uber.org/zap v1.24.0 golang.org/x/sync v0.1.0 @@ -34,49 +34,45 @@ require ( ) require ( + github.com/beorn7/perks v1.0.1 // indirect github.com/bytedance/sonic v1.9.2 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/evanphx/json-patch v0.5.2 // indirect github.com/felixge/httpsnoop v1.0.3 // indirect github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect + github.com/gin-gonic/gin v1.9.1 // indirect github.com/go-logr/logr v1.2.4 // indirect github.com/go-logr/stdr v1.2.2 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect github.com/golang/glog v1.1.1 // indirect github.com/golang/protobuf v1.5.3 // indirect - github.com/google/uuid v1.3.0 // indirect github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/golang-lru v0.5.4 // indirect github.com/jensneuse/abstractlogger v0.0.4 // indirect github.com/jensneuse/byte-template v0.0.0-20200214152254-4f3cf06e5c68 // indirect - github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/compress v1.15.15 // indirect github.com/klauspost/cpuid/v2 v2.2.5 // indirect github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/matttproud/golang_protobuf_extensions v1.0.4 // indirect github.com/pelletier/go-toml/v2 v2.0.9 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.4.0 // indirect + github.com/prometheus/common v0.42.0 // indirect + github.com/prometheus/procfs v0.9.0 // indirect github.com/r3labs/sse/v2 v2.8.1 // indirect github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 // indirect github.com/sirupsen/logrus v1.8.1 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.1 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 // indirect + go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 // indirect - go.opentelemetry.io/otel/metric v1.16.0 // indirect go.opentelemetry.io/proto/otlp v0.19.0 // indirect go.uber.org/atomic v1.9.0 // indirect go.uber.org/multierr v1.8.0 // indirect diff --git a/router/go.sum b/router/go.sum index d4b895cf3c..7314a682e2 100644 --- a/router/go.sum +++ b/router/go.sum @@ -39,7 +39,8 @@ github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAE github.com/agnivade/levenshtein v1.1.1 h1:QY8M92nrzkmr798gCo3kmMyqXFzdQVpxLlGPRBij0P8= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= -github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= +github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= +github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/bufbuild/connect-go v1.9.0 h1:JIgAeNuFpo+SUPfU19Yt5TcWlznsN5Bv10/gI/6Pjoc= github.com/bufbuild/connect-go v1.9.0/go.mod h1:CAIePUgkDR5pAFaylSMtNK45ANQjp9JvpluG20rhpV8= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= @@ -69,7 +70,6 @@ github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= @@ -95,21 +95,17 @@ github.com/felixge/httpsnoop v1.0.3/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSw github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/gin-contrib/requestid v0.0.6 h1:mGcxTnHQ45F6QU5HQRgQUDsAfHprD3P7g2uZ4cSZo9o= -github.com/gin-contrib/requestid v0.0.6/go.mod h1:9i4vKATX/CdggbkY252dPVasgVucy/ggBeELXuQztm4= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-contrib/zap v0.1.0 h1:RMSFFJo34XZogV62OgOzvrlaMNmXrNxmJ3bFmMwl6Cc= -github.com/gin-contrib/zap v0.1.0/go.mod h1:hvnZaPs478H1PGvRP8w89ZZbyJUiyip4ddiI/53WG3o= github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-chi/chi v1.5.4 h1:QHdzF2szwjqVV4wmByUnTcsbIg7UGaQ0tPF2t5GcAIs= +github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.3/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= @@ -117,15 +113,12 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/locales v0.14.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.14.1 h1:9c50NUPC30zyuKprjL3vNZ0m5oG+jU0zvx4AqHGnv4k= github.com/go-playground/validator/v10 v10.14.1/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= @@ -134,9 +127,7 @@ github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/gobwas/ws v1.0.4 h1:5eXU1CZhpQdq5kXbKb+sECH5Ia5KiO6CYzIzdlVx6Bs= -github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= github.com/golang/glog v1.1.1 h1:jxpi2eWoU84wbX9iIEyAeeoac3FLuifZpY9tcNUD9kw= @@ -181,7 +172,6 @@ github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/ github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= @@ -195,12 +185,8 @@ github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= @@ -226,7 +212,6 @@ github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0= github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= github.com/kelseyhightower/envconfig v1.4.0 h1:Im6hONhd3pLkfDFsbRgu68RDNkGF1r3dvMUtDTo2cv8= @@ -240,15 +225,11 @@ github.com/klauspost/cpuid/v2 v2.2.5 h1:0E5MSMDEoAulmXNFquVs//DdoomxaoTY1kUhbc/q github.com/klauspost/cpuid/v2 v2.2.5/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= github.com/logrusorgru/aurora/v3 v3.0.0 h1:R6zcoZZbvVcGMvDCKo45A9U/lzYyzl5NfYIvznmDfE4= @@ -256,37 +237,35 @@ github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91 h1:JnZSkFP1/GL github.com/mattbaird/jsonpatch v0.0.0-20230413205102-771768614e91/go.mod h1:M1qoD/MqPgTZIk0EWKB38wE28ACRfVcn+cU08jyArI0= github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/matttproud/golang_protobuf_extensions v1.0.4 h1:mmDVorXM7PCGKw94cs5zkfA9PSy5pEvNWRP0ET0TIVo= +github.com/matttproud/golang_protobuf_extensions v1.0.4/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/prometheus/client_golang v1.15.1 h1:8tXpTmJbyH5lydzFPoxSIJ0J46jdh3tylbvM1xCv0LI= +github.com/prometheus/client_golang v1.15.1/go.mod h1:e9yaBhRPU2pPNsZwE+JdQl0KEt1N9XgF6zxWmaC0xOk= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.4.0 h1:5lQXD3cAg1OXBf4Wq03gTrXHeaV0TQvGfUooCfx1yqY= +github.com/prometheus/client_model v0.4.0/go.mod h1:oMQmHW1/JoDwqLtg57MGgP/Fb1CJEYF2imWWhWtMkYU= +github.com/prometheus/common v0.42.0 h1:EKsfXEYo4JpWMHH5cg+KOUWeuJSov1Id8zGR8eeI1YM= +github.com/prometheus/common v0.42.0/go.mod h1:xBwqVerjNdUDjgODMpudtOMwlOwf2SaTr1yjz4b7Zbc= +github.com/prometheus/procfs v0.9.0 h1:wzCHvIvM5SxWqYvwgVL7yJY8Lz3PKn49KQtpgMYJfhI= +github.com/prometheus/procfs v0.9.0/go.mod h1:+pB4zwohETzFnmlpe6yd2lSc+0/46IYZRB/chUwxUZY= github.com/r3labs/sse/v2 v2.8.1 h1:lZH+W4XOLIq88U5MIHOsLec7+R62uhz3bIi2yn0Sg8o= github.com/r3labs/sse/v2 v2.8.1/go.mod h1:Igau6Whc+F17QUgML1fYe1VPZzTV6EMCnYktEmkNJ7I= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= -github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE= -github.com/rs/cors v1.9.0 h1:l9HGsTsHJcvW14Nk7J9KFz8bzeAWXn3CG6bgt7LsrAE= -github.com/rs/cors v1.9.0/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950 h1:AqLt1PEuscqbMJkmkfOw1xLlDH0VIQzrDEuOGggv0a4= -github.com/rs/cors/wrapper/gin v0.0.0-20230526135330-e90f16747950/go.mod h1:gmu40DuK3SLdKUzGOUofS3UDZwyeOUy6ZjPPuaALatw= +github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0 h1:uIkTLo0AGRc8l7h5l9r+GcYi9qfVPt6lD4/bhmzfiKo= github.com/santhosh-tekuri/jsonschema/v5 v5.3.0/go.mod h1:FKdcjfQW6rpZSnxxUvEA5H/cDPdvJ/SZJQLWWXWGrZ0= github.com/sebdah/goldie/v2 v2.5.3 h1:9ES/mNN+HNUbNWpVAlrzuZ7jE+Nrczbj8uFRjM7624Y= @@ -304,7 +283,6 @@ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXf github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= @@ -324,21 +302,16 @@ github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY= github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/vektah/gqlparser/v2 v2.5.1 h1:ZGu+bquAY23jsxDRcYpWjttRZrUz07LbiY77gUOHcr4= -github.com/wundergraph/graphql-go-tools-private/v2 v2.0.0-20230817140237-3b7eeaa55339 h1:8SqW9yboYUtV6dXesM+ySG/QQilq0uq/40eatnW3+zk= -github.com/wundergraph/graphql-go-tools-private/v2 v2.0.0-20230817140237-3b7eeaa55339/go.mod h1:jOEQFeTIDSAEWA//qrpSNjGYcCjMylvc/R/W8eM7+gY= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230822083323-a115bc0c7af6 h1:LRQ/s+rdZhVBpOJ2wlIk7vq60VAlanbdzRGZP5ViIeE= github.com/wundergraph/graphql-go-tools/v2 v2.0.0-rc.2.0.20230822083323-a115bc0c7af6/go.mod h1:jOEQFeTIDSAEWA//qrpSNjGYcCjMylvc/R/W8eM7+gY= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -346,20 +319,26 @@ go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0 h1:pginetY7+onl4qN1vl0xW/V/v6OBZ0vVdH+esuJgvmM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.42.0/go.mod h1:XiYsayHc36K3EByOO6nbAXnAWbrUxdjUROCEeeROOH8= -go.opentelemetry.io/otel v1.10.0/go.mod h1:NbvWjCthWHKBEUMpf0/v8ZRZlni86PpGFEMA9pnQSnQ= go.opentelemetry.io/otel v1.16.0 h1:Z7GVAX/UkAXPKsy94IU+i6thsQS4nb7LviLpnaNeW8s= go.opentelemetry.io/otel v1.16.0/go.mod h1:vl0h9NUa1D5s1nv3A5vZOYWn8av4K8Ml6JDeHrT/bx4= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0 h1:t4ZwRPU+emrcvM2e9DHd0Fsf0JTPVcbfa/BhTDF03d0= go.opentelemetry.io/otel/exporters/otlp/internal/retry v1.16.0/go.mod h1:vLarbg68dH2Wa77g71zmKQqlQ8+8Rq3GRG31uc0WcWI= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0 h1:f6BwB2OACc3FCbYVznctQ9V6KK7Vq6CjmYXJ7DeSs4E= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric v0.39.0/go.mod h1:UqL5mZ3qs6XYhDnZaW1Ps4upD+PX6LipH40AoeuIlwU= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0 h1:IZXpCEtI7BbX01DRQEWTGDkvjMB6hEhiEZXS+eg2YqY= +go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp v0.39.0/go.mod h1:xY111jIZtWb+pUUgT4UiiSonAaY2cD2Ts5zvuKLki3o= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0 h1:cbsD4cUcviQGXdw8+bo5x2wazq10SKz8hEbtCRPcU78= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.16.0/go.mod h1:JgXSGah17croqhJfhByOLVY719k1emAXC8MVhCIJlRs= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0 h1:iqjq9LAB8aK++sKVcELezzn655JnBNdsDhghU4G/So8= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.16.0/go.mod h1:hGXzO5bhhSHZnKvrDaXB82Y9DRFour0Nz/KrBh7reWw= +go.opentelemetry.io/otel/exporters/prometheus v0.39.0 h1:whAaiHxOatgtKd+w0dOi//1KUxj3KoPINZdtDaDj3IA= +go.opentelemetry.io/otel/exporters/prometheus v0.39.0/go.mod h1:4jo5Q4CROlCpSPsXLhymi+LYrDXd2ObU5wbKayfZs7Y= go.opentelemetry.io/otel/metric v1.16.0 h1:RbrpwVG1Hfv85LgnZ7+txXioPDoh6EdbZHo26Q3hqOo= go.opentelemetry.io/otel/metric v1.16.0/go.mod h1:QE47cpOmkwipPiefDwo2wDzwJrlfxxNYodqc4xnGCo4= go.opentelemetry.io/otel/sdk v1.16.0 h1:Z1Ok1YsijYL0CSJpHt4cS3wDDh7p572grzNrBMiMWgE= go.opentelemetry.io/otel/sdk v1.16.0/go.mod h1:tMsIuKXuuIWPBAOrH+eHtvhTL+SntFtXF9QD68aP6p4= -go.opentelemetry.io/otel/trace v1.10.0/go.mod h1:Sij3YYczqAdz+EhmGhE6TpTxUO5/F/AzrK+kxfGqySM= +go.opentelemetry.io/otel/sdk/metric v0.39.0 h1:Kun8i1eYf48kHH83RucG93ffz0zGV1sh46FAScOTuDI= +go.opentelemetry.io/otel/sdk/metric v0.39.0/go.mod h1:piDIRgjcK7u0HCL5pCA4e74qpK/jk3NiUoAHATVAmiI= go.opentelemetry.io/otel/trace v1.16.0 h1:8JRpaObFoW0pxuVPapkgH8UhHQj+bJW8jJsCZEu5MQs= go.opentelemetry.io/otel/trace v1.16.0/go.mod h1:Yt9vYq1SdNz3xdjZZK7wcXv1qv2pwLkqr2QVwea0ef0= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= @@ -370,14 +349,11 @@ go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.9.0 h1:ECmE8Bn/WFTYwEW/bpKD3M8VtR/zQVbavAoalC1PYyE= go.uber.org/atomic v1.9.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.1.11 h1:wy28qYRKZgnJTxGxvye5/wgWr1EKjmUDGYox5mGlRlI= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/multierr v1.8.0 h1:dg6GjLku4EH+249NNmoIciG9N/jURbDG+pFlTkhzIC8= go.uber.org/multierr v1.8.0/go.mod h1:7EAYxJLBy9rStEaz58O2t4Uvip6FSURkq8/ppBp95ak= go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= -go.uber.org/zap v1.23.0/go.mod h1:D+nX8jyLsMHMYrln8A0rJjFt/T/9/bGgIhAqxv5URuY= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= @@ -388,7 +364,6 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= @@ -423,7 +398,6 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -451,7 +425,6 @@ golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/ golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.12.0 h1:cfawfvKITfUsFCeJIHJrbSxpeu/E81khclypR0GVT50= golang.org/x/net v0.12.0/go.mod h1:zEVYFnQC7m/vmpQFELhcD1EWkZlX69l4oqgmer6hfKA= @@ -469,7 +442,6 @@ golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= @@ -503,12 +475,8 @@ golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20221010170243-090e33056c14/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0 h1:SqMFp9UcQJZa+pmYuAKjd9xq1f0j5rLcDIk0mj4qAsA= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= @@ -518,7 +486,6 @@ golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.11.0 h1:LAntKIrcmeSKERyiOh0XMV39LXS8IE9UL2yP7+f5ij4= golang.org/x/text v0.11.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= @@ -566,7 +533,6 @@ golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -659,7 +625,6 @@ google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlba google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.31.0 h1:g0LDEJHgrBl9N9r17Ru3sqWhkIx2NB67okBHPwC7hs8= google.golang.org/protobuf v1.31.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/cenkalti/backoff.v1 v1.1.0 h1:Arh75ttbsvlpVA7WtVpH4u9h6Zl46xuptxqLxPiSo4Y= @@ -667,12 +632,10 @@ gopkg.in/cenkalti/backoff.v1 v1.1.0/go.mod h1:J6Vskwqd+OMVJl8C33mmtxTBs2gyzfv7UD gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/router/main.go b/router/main.go index c04b29e33b..e2ef265992 100644 --- a/router/main.go +++ b/router/main.go @@ -2,9 +2,10 @@ package main import ( "context" - "github.com/rs/cors" "github.com/wundergraph/cosmo/router/pkg/app" "github.com/wundergraph/cosmo/router/pkg/controlplane" + "github.com/wundergraph/cosmo/router/pkg/handler/cors" + "github.com/wundergraph/cosmo/router/pkg/metric" "github.com/wundergraph/cosmo/router/pkg/trace" "log" "os" @@ -56,17 +57,17 @@ func main() { app.WithConfigFetcher(cp), app.WithIntrospection(cfg.IntrospectionEnabled), app.WithPlayground(cfg.PlaygroundEnabled), - app.WithProduction(cfg.Production), app.WithGraphApiToken(cfg.GraphApiToken), app.WithGracePeriod(time.Duration(cfg.GracePeriodSeconds)*time.Second), - app.WithCors(&cors.Options{ - AllowedOrigins: cfg.CORSAllowedOrigins, - AllowedMethods: cfg.CORSAllowedMethods, + app.WithCors(&cors.Config{ + AllowOrigins: cfg.CORSAllowedOrigins, + AllowMethods: cfg.CORSAllowedMethods, AllowCredentials: cfg.CORSAllowCredentials, - AllowedHeaders: cfg.CORSAllowedHeaders, - MaxAge: cfg.CORSMaxAgeMinutes, + AllowHeaders: cfg.CORSAllowedHeaders, + MaxAge: time.Duration(cfg.CORSMaxAgeMinutes) * time.Minute, }), app.WithTracing(&trace.Config{ + Enabled: cfg.OTELTracingEnabled, Name: cfg.OTELServiceName, Endpoint: cfg.OTELCollectorEndpoint, Sampler: cfg.OTELSampler, @@ -75,6 +76,18 @@ func main() { OtlpHeaders: cfg.OTELCollectorHeaders, OtlpHttpPath: "/v1/traces", }), + app.WithMetrics(&metric.Config{ + Enabled: cfg.OTELMetricsEnabled, + Name: cfg.OTELServiceName, + Endpoint: cfg.OTELCollectorEndpoint, + OtlpHeaders: cfg.OTELCollectorHeaders, + Prometheus: metric.Prometheus{ + Enabled: cfg.PrometheusEnabled, + ListenAddr: cfg.PrometheusHttpAddr, + Path: cfg.PrometheusHttpPath, + }, + OtlpHttpPath: "/v1/metrics", + }), ) if err != nil { diff --git a/router/pkg/app/app.go b/router/pkg/app/app.go index 1562485ae8..dcb9562ac2 100644 --- a/router/pkg/app/app.go +++ b/router/pkg/app/app.go @@ -5,20 +5,28 @@ import ( "errors" "fmt" "github.com/dgraph-io/ristretto" - "github.com/gin-contrib/requestid" - ginzap "github.com/gin-contrib/zap" - "github.com/gin-gonic/gin" - cors "github.com/rs/cors/wrapper/gin" + "github.com/go-chi/chi" + "github.com/go-chi/chi/middleware" + "github.com/prometheus/client_golang/prometheus/promhttp" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/pkg/controlplane" "github.com/wundergraph/cosmo/router/pkg/graphiql" "github.com/wundergraph/cosmo/router/pkg/graphql" - "github.com/wundergraph/cosmo/router/pkg/handlers" + "github.com/wundergraph/cosmo/router/pkg/handler/cors" + "github.com/wundergraph/cosmo/router/pkg/handler/health" + "github.com/wundergraph/cosmo/router/pkg/handler/recovery" + "github.com/wundergraph/cosmo/router/pkg/handler/requestlogger" + "github.com/wundergraph/cosmo/router/pkg/metric" + "github.com/wundergraph/cosmo/router/pkg/otel" + "github.com/wundergraph/cosmo/router/pkg/planner" + "github.com/wundergraph/cosmo/router/pkg/stringsx" "github.com/wundergraph/cosmo/router/pkg/trace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" sdktrace "go.opentelemetry.io/otel/sdk/trace" - trace2 "go.opentelemetry.io/otel/trace" + oteltrace "go.opentelemetry.io/otel/trace" "go.uber.org/zap" + "go.uber.org/zap/zapcore" "golang.org/x/sync/errgroup" "net" "net/http" @@ -37,8 +45,10 @@ type ( transport *http.Transport logger *zap.Logger traceConfig *trace.Config + metricConfig *metric.Config tracerProvider *sdktrace.TracerProvider - corsOptions *cors.Options + meterProvider *sdkmetric.MeterProvider + corsOptions *cors.Config configFetcher *controlplane.ConfigFetcher gracePeriod time.Duration addr string @@ -49,6 +59,7 @@ type ( production bool federatedGraphName string graphApiToken string + prometheusServer *http.Server } // Router is the main router instance. @@ -79,6 +90,10 @@ func New(opts ...Option) (*App, error) { r.traceConfig = trace.DefaultConfig() } + if r.metricConfig == nil { + r.metricConfig = metric.DefaultConfig() + } + if r.corsOptions == nil { r.corsOptions = CorsDefaultOptions() } @@ -93,9 +108,8 @@ func New(opts ...Option) (*App, error) { defaultMethods := []string{ "HEAD", "GET", "POST", } - - r.corsOptions.AllowedHeaders = append(r.corsOptions.AllowedOrigins, defaultHeaders...) - r.corsOptions.AllowedMethods = append(r.corsOptions.AllowedMethods, defaultMethods...) + r.corsOptions.AllowHeaders = stringsx.RemoveDuplicates(append(r.corsOptions.AllowHeaders, defaultHeaders...)) + r.corsOptions.AllowMethods = stringsx.RemoveDuplicates(append(r.corsOptions.AllowMethods, defaultMethods...)) r.baseURL = fmt.Sprintf("http://%s", r.addr) @@ -132,91 +146,106 @@ func New(opts ...Option) (*App, error) { } r.traceConfig.OtlpHeaders["Authorization"] = fmt.Sprintf("Bearer %s", r.graphApiToken) - tp, err := trace.StartAgent(r.logger, r.traceConfig) - if err != nil { - return nil, fmt.Errorf("failed to start trace agent: %w", err) + if r.metricConfig.OtlpHeaders == nil { + r.metricConfig.OtlpHeaders = make(map[string]string) } - r.tracerProvider = tp - - r.logger.Info("Collector agent started", zap.String("url", r.traceConfig.Endpoint+r.traceConfig.OtlpHttpPath)) + r.metricConfig.OtlpHeaders["Authorization"] = fmt.Sprintf("Bearer %s", r.graphApiToken) return r, nil } -// swapServer swaps the active server with a new server instance. +// startServer starts a new server. It swaps the active server with a new server instance when the config has changed. // This method is not safe for concurrent use. -func (s *App) swapServer(ctx context.Context, cfg *nodev1.RouterConfig) error { +func (a *App) startServer(ctx context.Context, cfg *nodev1.RouterConfig) error { // Rebuild server with new router config // In case of an error, we return early and keep the old server running - newServer, err := s.newRouter(ctx, cfg) + newServer, err := a.newRouter(ctx, cfg) if err != nil { - s.logger.Error("Failed to newRouter server", zap.Error(err)) + a.logger.Error("Failed to newRouter server", zap.Error(err)) return err } // Gracefully shutdown server // Wait grace period before forcefully shutting down the server - prevServer := s.activeServer + prevServer := a.activeServer if prevServer != nil { - s.logger.Info("Gracefully shutting down server ...", + a.logger.Info("Gracefully shutting down server ...", zap.String("version", prevServer.routerConfig.GetVersion()), - zap.String("gracePeriod", s.gracePeriod.String()), + zap.String("gracePeriod", a.gracePeriod.String()), ) if prevServer.gracePeriod > 0 { - ctxWithTimer, cancel := context.WithTimeout(context.Background(), s.gracePeriod) + ctxWithTimer, cancel := context.WithTimeout(context.Background(), a.gracePeriod) ctx = ctxWithTimer defer cancel() } if err := prevServer.Shutdown(ctx); err != nil { - s.logger.Error("Could not shutdown server", zap.Error(err)) + a.logger.Error("Could not shutdown server", zap.Error(err)) } } // Swap active server - s.activeServer = newServer + a.activeServer = newServer // Start new server go func() { if prevServer != nil { - s.logger.Info("Starting server with new config", + a.logger.Info("Starting server with new config", zap.String("version", cfg.GetVersion()), ) } else { - s.logger.Info("Server listening", - zap.String("listen_addr", s.addr), - zap.Bool("playground", s.playground), - zap.Bool("introspection", s.introspection), + a.logger.Info("Server listening", + zap.String("listen_addr", a.addr), + zap.Bool("playground", a.playground), + zap.Bool("introspection", a.introspection), zap.String("version", cfg.GetVersion()), ) - if s.playground && s.introspection { - s.logger.Info("Playground available at", zap.String("url", s.baseURL+"/graphql")) + if a.playground && a.introspection { + a.logger.Info("Playground available at", zap.String("url", a.baseURL+"/graphql")) } } // This is a blocking call - if err := s.activeServer.listenAndServe(); err != nil { - s.logger.Error("Failed to start new server", zap.Error(err)) + if err := a.activeServer.listenAndServe(); err != nil { + a.logger.Error("Failed to start new server", zap.Error(err)) } - s.logger.Info("Server stopped", zap.String("version", s.activeServer.routerConfig.GetVersion())) + a.logger.Info("Server stopped", zap.String("version", a.activeServer.routerConfig.GetVersion())) }() return nil } // Start starts the server. It blocks until the context is cancelled or when the initial config could not be fetched. -func (s *App) Start(ctx context.Context) error { +func (a *App) Start(ctx context.Context) error { eg, ctx := errgroup.WithContext(ctx) + tp, err := trace.StartAgent(a.logger, a.traceConfig) + if err != nil { + return fmt.Errorf("failed to start trace agent: %w", err) + } + a.tracerProvider = tp + + mp, err := metric.StartAgent(a.logger, a.metricConfig) + if err != nil { + return fmt.Errorf("failed to start trace agent: %w", err) + } + a.meterProvider = mp + + if a.metricConfig.Prometheus.Enabled { + eg.Go(func() error { + return a.startPrometheus() + }) + } + var initCh = make(chan *nodev1.RouterConfig, 1) eg.Go(func() error { @@ -225,11 +254,11 @@ func (s *App) Start(ctx context.Context) error { case <-ctx.Done(): // context cancelled return nil case cfg := <-initCh: // initial config - if err := s.swapServer(ctx, cfg); err != nil { + if err := a.startServer(ctx, cfg); err != nil { return fmt.Errorf("failed to handle initial config: %w", err) } - case cfg := <-s.configFetcher.Subscribe(ctx): // new config - if err := s.swapServer(ctx, cfg); err != nil { + case cfg := <-a.configFetcher.Subscribe(ctx): // new config + if err := a.startServer(ctx, cfg); err != nil { continue } } @@ -238,7 +267,7 @@ func (s *App) Start(ctx context.Context) error { // Get initial router config - initialCfg, err := s.configFetcher.GetRouterConfig(ctx) + initialCfg, err := a.configFetcher.GetRouterConfig(ctx) if err != nil { return fmt.Errorf("failed to get initial router config: %w", err) } @@ -248,26 +277,54 @@ func (s *App) Start(ctx context.Context) error { return eg.Wait() } +func (a *App) startPrometheus() error { + r := chi.NewRouter() + r.Handle(a.metricConfig.Prometheus.Path, promhttp.Handler()) + + svr := &http.Server{ + Addr: a.metricConfig.Prometheus.ListenAddr, + ReadTimeout: 1 * time.Minute, + WriteTimeout: 2 * time.Minute, + ReadHeaderTimeout: 20 * time.Second, + ErrorLog: zap.NewStdLog(a.logger), + Handler: r, + } + + a.prometheusServer = svr + + a.logger.Info("Serve Prometheus metrics", zap.String("addr", svr.Addr), zap.String("endpoint", a.metricConfig.Prometheus.Path)) + + if err := svr.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + return err + } + + return nil +} + // newRouter creates a new server instance. // All stateful data is copied from the app over to the new router instance. -func (s *App) newRouter(ctx context.Context, routerConfig *nodev1.RouterConfig) (*Router, error) { - if s.production { - gin.SetMode(gin.ReleaseMode) - } +func (a *App) newRouter(ctx context.Context, routerConfig *nodev1.RouterConfig) (*Router, error) { + recoveryHandler := recovery.New(recovery.WithLogger(a.logger), recovery.WithPrintStack()) + requestLogger := requestlogger.New( + a.logger, + requestlogger.WithDefaultOptions(), + requestlogger.WithContext(func(r *http.Request) []zapcore.Field { + return []zapcore.Field{ + zap.String("configVersion", routerConfig.GetVersion()), + zap.String("requestID", middleware.GetReqID(r.Context())), + zap.String("federatedGraphName", a.federatedGraphName), + } + }), + ) - router := gin.New() - router.Use(ginzap.GinzapWithConfig(s.logger, &ginzap.Config{ - TimeFormat: time.RFC3339, - UTC: true, - TraceID: true, - SkipPaths: []string{"/health"}, - })) - router.Use(ginzap.RecoveryWithZap(s.logger, true)) - router.Use(requestid.New()) - router.Use(cors.New(*s.corsOptions)) + router := chi.NewRouter() + router.Use(middleware.RequestID) + router.Use(middleware.RealIP) + router.Use(requestLogger) + router.Use(cors.New(*a.corsOptions)) - healthHandler := handlers.NewHealthHandler() - router.GET("/health", healthHandler.Handler) + // Health check + router.Get("/health", health.New()) // when an execution plan was generated, which can be quite expensive, we want to cache it // this means that we can hash the input and cache the generated plan @@ -281,79 +338,111 @@ func (s *App) newRouter(ctx context.Context, routerConfig *nodev1.RouterConfig) BufferItems: 64, // number of keys per Get buffer. }) if err != nil { - return nil, fmt.Errorf("failed to create plan cache: %w", err) + return nil, fmt.Errorf("failed to create planner cache: %w", err) } - hb := graphql.NewGraphQLHandlerBuilder( - graphql.WithIntrospection(), - graphql.WithPlanCache(planCache), - graphql.WithLogger(s.logger), - graphql.WithBaseURL(s.baseURL), - graphql.WithTransport(s.transport), + pb := planner.NewPlanner( + planner.WithIntrospection(), + planner.WithLogger(a.logger), + planner.WithBaseURL(a.baseURL), + planner.WithTransport(a.transport), ) - graphqlHandler, err := hb.Build(ctx, routerConfig) + plan, err := pb.Build(ctx, routerConfig) + if err != nil { + return nil, fmt.Errorf("failed to build plan configuration: %w", err) + } + + graphqlHandler := graphql.NewHandler(graphql.HandlerOptions{ + PlanConfig: plan.PlanConfig, + Definition: plan.Definition, + Resolver: plan.Resolver, + Pool: plan.Pool, + Cache: planCache, + Log: a.logger, + }) + + graphqlPreHandler := graphql.NewPreHandler(&graphql.PreHandlerOptions{ + Logger: a.logger, + Pool: plan.Pool, + RenameTypeNames: plan.RenameTypeNames, + PlanConfig: plan.PlanConfig, + }) + + metricHandler, err := metric.NewMetricHandler( + a.meterProvider, + otel.WgRouterGraphName.String(a.federatedGraphName), + otel.WgRouterConfigVersion.String(routerConfig.GetVersion()), + ) if err != nil { - s.logger.Error("Failed to newRouter initial handler", zap.Error(err)) - return nil, err + return nil, fmt.Errorf("failed to create metric handler: %w", err) } - router.POST(s.graphqlPath, graphqlHandler.Handler) + // Serve GraphQL. Metrics are collected after the request is handled and considered as a GraphQL request. + router.Post(a.graphqlPath, graphqlPreHandler.Handler(metricHandler.Handler(graphqlHandler))) - s.logger.Debug("Registered GraphQLHandler", + a.logger.Debug("GraphQLHandler registered", zap.String("method", http.MethodPost), - zap.String("path", s.graphqlPath), + zap.String("path", a.graphqlPath), ) - if s.playground { - graphqlPlaygroundHandler := &graphql.GraphQLPlaygroundHandler{ - Log: s.logger, + if a.playground { + graphqlPlaygroundHandler := graphiql.NewPlayground(&graphiql.PlaygroundOptions{ + Log: a.logger, Html: graphiql.GetGraphiqlPlaygroundHTML(), - NodeUrl: s.baseURL, - } - router.GET(s.graphqlPath, graphqlPlaygroundHandler.Handler) - s.logger.Debug("Registered GraphQLPlaygroundHandler", + NodeUrl: a.baseURL, + }) + router.Get(a.graphqlPath, graphqlPlaygroundHandler) + a.logger.Debug("PlaygroundHandler registered", zap.String("method", http.MethodGet), - zap.String("path", s.graphqlPath), + zap.String("path", a.graphqlPath), ) } server := &http.Server{ - Addr: s.addr, + Addr: a.addr, // https://ieftimov.com/posts/make-resilient-golang-net-http-servers-using-timeouts-deadlines-context-cancellation/ ReadTimeout: 1 * time.Minute, WriteTimeout: 2 * time.Minute, ReadHeaderTimeout: 20 * time.Second, // TODO: Move to middleware after release https://github.com/open-telemetry/opentelemetry-go-contrib/compare/v1.17.0...HEAD Handler: trace.WrapHandler( - router, - trace.RouterServerAttribute, + recoveryHandler(router), + otel.RouterServerAttribute, otelhttp.WithSpanOptions( - trace2.WithAttributes( - trace.WgRouterGraphName.String(s.federatedGraphName), - trace.WgRouterVersion.String(routerConfig.GetVersion()), + oteltrace.WithAttributes( + otel.WgRouterGraphName.String(a.federatedGraphName), + otel.WgRouterConfigVersion.String(routerConfig.GetVersion()), ), ), + // Disable built-in metrics + otelhttp.WithMeterProvider(sdkmetric.NewMeterProvider()), ), - ErrorLog: zap.NewStdLog(s.logger), + ErrorLog: zap.NewStdLog(a.logger), } svr := &Router{ routerConfig: routerConfig, server: server, Options: Options{ - graphqlPath: s.graphqlPath, - transport: s.transport, - logger: s.logger, - traceConfig: s.traceConfig, - corsOptions: s.corsOptions, - configFetcher: s.configFetcher, - addr: s.addr, - baseURL: s.baseURL, - playground: s.playground, - introspection: s.introspection, - production: s.production, - gracePeriod: s.gracePeriod, + transport: a.transport, + logger: a.logger, + traceConfig: a.traceConfig, + metricConfig: a.metricConfig, + tracerProvider: a.tracerProvider, + meterProvider: a.meterProvider, + corsOptions: a.corsOptions, + configFetcher: a.configFetcher, + gracePeriod: a.gracePeriod, + addr: a.addr, + baseURL: a.baseURL, + graphqlPath: a.graphqlPath, + playground: a.playground, + introspection: a.introspection, + production: a.production, + federatedGraphName: a.federatedGraphName, + graphApiToken: a.graphApiToken, + prometheusServer: a.prometheusServer, }, } @@ -361,9 +450,9 @@ func (s *App) newRouter(ctx context.Context, routerConfig *nodev1.RouterConfig) } // listenAndServe starts the server and blocks until the server is shutdown. -func (s *Router) listenAndServe() error { +func (r *Router) listenAndServe() error { - if err := s.server.ListenAndServe(); err != nil && err != http.ErrServerClosed { + if err := r.server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { return err } @@ -371,19 +460,25 @@ func (s *Router) listenAndServe() error { } // Shutdown gracefully shuts down the router. -func (s *App) Shutdown(ctx context.Context) (err error) { +func (a *App) Shutdown(ctx context.Context) (err error) { + + if a.activeServer != nil { + if err := a.activeServer.Shutdown(ctx); err != nil { + err = errors.Join(err, fmt.Errorf("failed to shutdown primary server: %w", err)) + } + } - if s.activeServer != nil { - if err := s.activeServer.Shutdown(ctx); err != nil { - err = errors.Join(err, fmt.Errorf("failed to shutdown server: %w", err)) + if a.prometheusServer != nil { + if err := a.prometheusServer.Shutdown(ctx); err != nil { + err = errors.Join(err, fmt.Errorf("failed to shutdown prometheus server: %w", err)) } } - if s.tracerProvider != nil { - if err := s.tracerProvider.ForceFlush(ctx); err != nil { + if a.tracerProvider != nil { + if err := a.tracerProvider.ForceFlush(ctx); err != nil { err = errors.Join(err, fmt.Errorf("failed to force flush tracer: %w", err)) } - if err := s.tracerProvider.Shutdown(ctx); err != nil { + if err := a.tracerProvider.Shutdown(ctx); err != nil { err = errors.Join(err, fmt.Errorf("failed to shutdown tracer: %w", err)) } } @@ -392,9 +487,9 @@ func (s *App) Shutdown(ctx context.Context) (err error) { } // Shutdown gracefully shuts down the server. -func (s *Router) Shutdown(ctx context.Context) (err error) { - if s.server != nil { - if err := s.server.Shutdown(ctx); err != nil { +func (r *Router) Shutdown(ctx context.Context) (err error) { + if r.server != nil { + if err := r.server.Shutdown(ctx); err != nil { return err } } @@ -432,7 +527,7 @@ func WithTracing(cfg *trace.Config) Option { } } -func WithCors(corsOpts *cors.Options) Option { +func WithCors(corsOpts *cors.Config) Option { return func(s *App) { s.corsOptions = corsOpts } @@ -444,12 +539,6 @@ func WithGraphQLPath(path string) Option { } } -func WithProduction(enable bool) Option { - return func(s *App) { - s.production = enable - } -} - func WithConfigFetcher(cf *controlplane.ConfigFetcher) Option { return func(s *App) { s.configFetcher = cf @@ -462,6 +551,12 @@ func WithGracePeriod(timeout time.Duration) Option { } } +func WithMetrics(cfg *metric.Config) Option { + return func(s *App) { + s.metricConfig = cfg + } +} + func WithFederatedGraphName(name string) Option { return func(s *App) { s.federatedGraphName = name @@ -469,15 +564,15 @@ func WithFederatedGraphName(name string) Option { } // CorsDefaultOptions returns the default CORS options for the rs/cors package. -func CorsDefaultOptions() *cors.Options { - return &cors.Options{ - AllowedOrigins: []string{"*"}, - AllowedMethods: []string{ +func CorsDefaultOptions() *cors.Config { + return &cors.Config{ + AllowOrigins: []string{"*"}, + AllowMethods: []string{ http.MethodHead, http.MethodGet, http.MethodPost, }, - AllowedHeaders: []string{}, + AllowHeaders: []string{}, AllowCredentials: false, } } diff --git a/router/pkg/config/config.go b/router/pkg/config/config.go index d18ec97824..986c7176a8 100644 --- a/router/pkg/config/config.go +++ b/router/pkg/config/config.go @@ -26,11 +26,16 @@ type Config struct { FederatedGraphName string `envconfig:"FEDERATED_GRAPH_NAME" validate:"required"` ControlplaneURL string `validate:"required" envconfig:"CONTROLPLANE_URL" validate:"uri"` ListenAddr string `default:"localhost:3002" envconfig:"LISTEN_ADDR"` + OTELTracingEnabled bool `default:"true" envconfig:"OTEL_TRACING_ENABLED"` OTELCollectorEndpoint string `validate:"required" envconfig:"OTEL_COLLECTOR_ENDPOINT" validate:"uri"` OTELCollectorHeaders map[string]string `default:"" envconfig:"OTEL_COLLECTOR_HEADERS"` OTELSampler float64 `default:"1" envconfig:"OTEL_SAMPLER"` OTELBatchTimeoutSeconds int `default:"5" envconfig:"OTEL_BATCH_TIMEOUT_SECONDS"` - OTELServiceName string `default:"wundergraph-cosmo-router" envconfig:"OTEL_SERVICE_NAME"` + OTELServiceName string `default:"cosmo-router" envconfig:"OTEL_SERVICE_NAME"` + OTELMetricsEnabled bool `default:"true" envconfig:"OTEL_METRICS_ENABLED"` + PrometheusEnabled bool `default:"true" envconfig:"PROMETHEUS_ENABLED"` + PrometheusHttpPath string `default:"/metrics" envconfig:"PROMETHEUS_HTTP_PATH"` + PrometheusHttpAddr string `default:"127.0.0.1:8088" envconfig:"PROMETHEUS_HTTP_ADDR"` CORSAllowedOrigins []string `default:"*" envconfig:"CORS_ALLOWED_ORIGINS"` CORSAllowedMethods []string `default:"HEAD,GET,POST" envconfig:"CORS_ALLOWED_METHODS"` CORSAllowCredentials bool `default:"false" envconfig:"CORS_ALLOW_CREDENTIALS"` @@ -40,7 +45,6 @@ type Config struct { IntrospectionEnabled bool `default:"true" envconfig:"INTROSPECTION_ENABLED"` LogLevel string `default:"info" envconfig:"LOG_LEVEL" validate:"oneof=debug info warning error fatal panic"` JSONLog bool `default:"true" envconfig:"JSON_LOG"` - Production bool `default:"false" envconfig:"PRODUCTION"` ShutdownDelaySeconds int `default:"15" envconfig:"SHUTDOWN_DELAY_SECONDS"` GracePeriodSeconds int `default:"0" envconfig:"GRACE_PERIOD_SECONDS"` PollIntervalSeconds int `default:"10" envconfig:"POLL_INTERVAL_SECONDS"` diff --git a/router/pkg/contextx/context.go b/router/pkg/contextx/context.go deleted file mode 100644 index 35703df38f..0000000000 --- a/router/pkg/contextx/context.go +++ /dev/null @@ -1,35 +0,0 @@ -package contextx - -import ( - "context" -) - -type graphqlOperationCtxKey string - -type GraphQLOperation struct { - // Name is the name of the operation - Name string - // Type is the type of the operation (query, mutation, subscription) - Type string - // Content is the content of the operation - Content string - // Hash is the hash of the operation. Only available if the operation was parsed successfully. - Hash uint64 -} - -const key = graphqlOperationCtxKey("graphqlOperation") - -func AddGraphQLOperationToContext(ctx context.Context, operation *GraphQLOperation) context.Context { - return context.WithValue(ctx, key, operation) -} - -func GetGraphQLOperationFromContext(ctx context.Context) *GraphQLOperation { - if ctx == nil { - return nil - } - op := ctx.Value(key) - if op == nil { - return nil - } - return op.(*GraphQLOperation) -} diff --git a/router/pkg/contextx/operation.go b/router/pkg/contextx/operation.go new file mode 100644 index 0000000000..8e8f5502e2 --- /dev/null +++ b/router/pkg/contextx/operation.go @@ -0,0 +1,36 @@ +package contextx + +import ( + "context" + "github.com/wundergraph/cosmo/router/pkg/pool" +) + +type operationCtxKey string + +type OperationContext struct { + // Name is the name of the operation + Name string + // Type is the type of the operation (query, mutation, subscription) + Type string + // Content is the content of the operation + Content string + // Plan is the execution plan of the operation + Plan *pool.Shared +} + +const key = operationCtxKey("graphql.operation") + +func WithOperationContext(ctx context.Context, operation *OperationContext) context.Context { + return context.WithValue(ctx, key, operation) +} + +func GetOperationContext(ctx context.Context) *OperationContext { + if ctx == nil { + return nil + } + op := ctx.Value(key) + if op == nil { + return nil + } + return op.(*OperationContext) +} diff --git a/router/pkg/factoryresolver/transport.go b/router/pkg/factoryresolver/transport.go index b556686d3d..4bbe75fd51 100644 --- a/router/pkg/factoryresolver/transport.go +++ b/router/pkg/factoryresolver/transport.go @@ -1,6 +1,7 @@ package factoryresolver import ( + "github.com/wundergraph/cosmo/router/pkg/otel" "github.com/wundergraph/cosmo/router/pkg/trace" "go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp" otrace "go.opentelemetry.io/otel/trace" @@ -20,7 +21,7 @@ func New() *TransportFactory { func (t TransportFactory) RoundTripper(transport *http.Transport, enableStreamingMode bool) http.RoundTripper { return trace.NewTransport( transport, - otelhttp.WithSpanOptions(otrace.WithAttributes(trace.EngineTransportAttribute)), + otelhttp.WithSpanOptions(otrace.WithAttributes(otel.EngineTransportAttribute)), ) } diff --git a/router/pkg/graphiql/playgroundhandler.go b/router/pkg/graphiql/playgroundhandler.go new file mode 100644 index 0000000000..2516bffc6d --- /dev/null +++ b/router/pkg/graphiql/playgroundhandler.go @@ -0,0 +1,29 @@ +package graphiql + +import ( + "go.uber.org/zap" + "net/http" + "strconv" + "strings" +) + +type PlaygroundOptions struct { + Log *zap.Logger + Html string + NodeUrl string +} + +func NewPlayground(opts *PlaygroundOptions) http.HandlerFunc { + fn := func(w http.ResponseWriter, r *http.Request) { + tpl := strings.Replace(opts.Html, "{{apiURL}}", opts.NodeUrl, -1) + resp := []byte(tpl) + + w.Header().Set("Content-Type", "text/html; charset=utf-8") + w.Header().Set("Content-Length", strconv.Itoa(len(resp))) + + w.WriteHeader(http.StatusOK) + _, _ = w.Write(resp) + } + + return fn +} diff --git a/router/pkg/graphiql/playgroundhandler_test.go b/router/pkg/graphiql/playgroundhandler_test.go new file mode 100644 index 0000000000..fb2600c02f --- /dev/null +++ b/router/pkg/graphiql/playgroundhandler_test.go @@ -0,0 +1,25 @@ +package graphiql + +import ( + "github.com/stretchr/testify/assert" + "github.com/wundergraph/cosmo/router/pkg/test" + "go.uber.org/zap" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + handler := NewPlayground(&PlaygroundOptions{ + Log: zap.NewNop(), + Html: "test {{apiURL}}", + NodeUrl: "http://localhost:8080", + }) + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, test.NewRequest(http.MethodGet, "/html")) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "text/html; charset=utf-8", rec.Header().Get("Content-Type")) + assert.Equal(t, "test http://localhost:8080", rec.Body.String()) +} diff --git a/router/pkg/graphql/graphql.go b/router/pkg/graphql/graphql.go deleted file mode 100644 index 97839d65d1..0000000000 --- a/router/pkg/graphql/graphql.go +++ /dev/null @@ -1,433 +0,0 @@ -package graphql - -import ( - "bytes" - "context" - "errors" - "fmt" - "github.com/gin-contrib/requestid" - "github.com/gin-gonic/gin" - "github.com/wundergraph/cosmo/router/pkg/contextx" - "github.com/wundergraph/cosmo/router/pkg/flushwriter" - ctrace "github.com/wundergraph/cosmo/router/pkg/trace" - "go.opentelemetry.io/otel/trace" - "io" - "net/http" - "strconv" - "strings" - "sync" - "time" - - "github.com/buger/jsonparser" - "github.com/dgraph-io/ristretto" - "github.com/hashicorp/go-multierror" - "github.com/tidwall/gjson" - "github.com/tidwall/sjson" - "github.com/wundergraph/cosmo/router/pkg/internal/unsafebytes" - "github.com/wundergraph/cosmo/router/pkg/logging" - "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" - "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" - "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" - "github.com/wundergraph/graphql-go-tools/v2/pkg/graphql" - "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" - "go.uber.org/zap" - "golang.org/x/sync/singleflight" - - "github.com/wundergraph/cosmo/router/pkg/pool" -) - -const ( - ErrMsgOperationParseFailed = "failed to parse operation: %w" - ErrMsgOperationValidationFailed = "operation validation failed: %s" -) - -var ( - couldNotResolveResponseErr = errors.New("could not resolve response") - internalServerErrorErr = errors.New("internal server error") -) - -type planWithExtractedVariables struct { - preparedPlan plan.Plan - variables []byte -} - -func MergeJsonRightIntoLeft(left, right []byte) []byte { - if len(left) == 0 { - return right - } - if len(right) == 0 { - return left - } - result := gjson.ParseBytes(right) - result.ForEach(func(key, value gjson.Result) bool { - left, _ = sjson.SetRawBytes(left, key.Str, unsafebytes.StringToBytes(value.Raw)) - return true - }) - return left -} - -type HandlerOptions struct { - PlanConfig plan.Configuration - Definition *ast.Document - Resolver *resolve.Resolver - RenameTypeNames []resolve.RenameTypeName - Pool *pool.Pool - Cache *ristretto.Cache - Log *zap.Logger -} - -func NewGraphQLHandler(opts HandlerOptions) *GraphQLHandler { - graphQLHandler := &GraphQLHandler{ - planConfig: opts.PlanConfig, - definition: opts.Definition, - resolver: opts.Resolver, - log: opts.Log, - pool: opts.Pool, - sf: &singleflight.Group{}, - prepared: map[uint64]planWithExtractedVariables{}, - preparedMux: &sync.RWMutex{}, - renameTypeNames: opts.RenameTypeNames, - planCache: opts.Cache, - } - return graphQLHandler -} - -type GraphQLPlaygroundHandler struct { - Log *zap.Logger - Html string - NodeUrl string -} - -func (h *GraphQLPlaygroundHandler) Handler(c *gin.Context) { - tpl := strings.Replace(h.Html, "{{apiURL}}", h.NodeUrl, -1) - resp := []byte(tpl) - - c.Header("Content-Type", "text/Html; charset=utf-8") - c.Header("Content-Length", strconv.Itoa(len(resp))) - - c.Status(http.StatusOK) - _, _ = c.Writer.Write(resp) -} - -type GraphQLHandler struct { - planConfig plan.Configuration - definition *ast.Document - resolver *resolve.Resolver - log *zap.Logger - pool *pool.Pool - sf *singleflight.Group - - prepared map[uint64]planWithExtractedVariables - preparedMux *sync.RWMutex - - renameTypeNames []resolve.RenameTypeName - - planCache *ristretto.Cache -} - -func (h *GraphQLHandler) Handler(c *gin.Context) { - - var ( - preparedPlan planWithExtractedVariables - ) - - requestLogger := h.log.With(logging.WithRequestID(requestid.Get(c))) - - buf := pool.GetBytesBuffer() - defer pool.PutBytesBuffer(buf) - _, err := io.Copy(buf, c.Request.Body) - if err != nil { - requestLogger.Error("failed to read request body", zap.Error(err)) - c.Status(http.StatusBadRequest) - h.writeRequestErrors(graphql.RequestErrorsFromError(errors.New("bad request")), c.Writer, requestLogger) - c.String(http.StatusInternalServerError, "unexpected error") - return - } - - body := buf.Bytes() - - requestQuery, _ := jsonparser.GetString(body, "query") - requestOperationName, _ := jsonparser.GetString(body, "operationName") - requestVariables, _, _, _ := jsonparser.Get(body, "variables") - requestOperationType := "" - - shared := h.pool.GetSharedFromRequest(c.Request, h.planConfig, pool.Config{ - RenameTypeNames: h.renameTypeNames, - }) - defer h.pool.PutShared(shared) - shared.Ctx.Variables = requestVariables - shared.Doc.Input.ResetInputString(requestQuery) - shared.Parser.Parse(shared.Doc, shared.Report) - - // If the operationName is not set, we try to get it from the named operation in the document - if requestOperationName == "" { - if len(shared.Doc.OperationDefinitions) == 1 { - requestOperationName = shared.Doc.Input.ByteSlice(shared.Doc.OperationDefinitions[0].Name).String() - } - } - - // If multiple operations are defined, but no operationName is set, we return an error - if len(shared.Doc.OperationDefinitions) > 1 && requestOperationName == "" { - requestLogger.Error("operation name is required when multiple operations are defined") - c.String(http.StatusBadRequest, "operation name is required when multiple operations are defined") - return - } - - // Extract the operation type from the first operation that matches the operationName - for _, op := range shared.Doc.OperationDefinitions { - if shared.Doc.Input.ByteSlice(op.Name).String() == requestOperationName { - switch op.OperationType { - case ast.OperationTypeQuery: - requestOperationType = "query" - case ast.OperationTypeMutation: - requestOperationType = "mutation" - case ast.OperationTypeSubscription: - requestOperationType = "subscription" - } - break - } - } - - // Add the operation to the context, so we can access it later in custom transports etc. - shared.Ctx = shared.Ctx.WithContext(contextx.AddGraphQLOperationToContext(c.Request.Context(), &contextx.GraphQLOperation{ - Name: requestOperationName, - Type: requestOperationType, - Content: requestQuery, - })) - - // Add the operation to the trace span - span := trace.SpanFromContext(c.Request.Context()) - span.SetAttributes(ctrace.WgOperationName.String(requestOperationName)) - span.SetAttributes(ctrace.WgOperationType.String(requestOperationType)) - span.SetAttributes(ctrace.WgOperationContent.String(requestQuery)) - - // Add client info to trace span - clientName := ctrace.GetClientInfo(c, "graphql-client-name", "apollographql-client-name", "unknown") - clientVersion := ctrace.GetClientInfo(c, "graphql-client-version", "apollographql-client-version", "missing") - span.SetAttributes(ctrace.WgClientName.String(clientName)) - span.SetAttributes(ctrace.WgClientVersion.String(clientVersion)) - - if shared.Report.HasErrors() { - h.logInternalErrors(shared.Report, requestLogger) - c.Status(http.StatusBadRequest) - h.writeRequestErrorsFromReport(shared.Report, c.Writer, requestLogger) - return - } - - requestOperationNameBytes := []byte(requestOperationName) - - if requestOperationName == "" { - shared.Normalizer.NormalizeOperation(shared.Doc, h.definition, shared.Report) - } else { - shared.Normalizer.NormalizeNamedOperation(shared.Doc, h.definition, requestOperationNameBytes, shared.Report) - } - - if shared.Report.HasErrors() { - h.logInternalErrors(shared.Report, requestLogger) - c.Status(http.StatusBadRequest) - h.writeRequestErrorsFromReport(shared.Report, c.Writer, requestLogger) - return - } - - // add the operation name to the hash - // this is important for multi operation documents to have a different hash for each operation - // otherwise, the prepared plan cache would return the same plan for all operations - _, err = shared.Hash.Write(requestOperationNameBytes) - if err != nil { - requestLogger.Error("hash write failed", zap.Error(err)) - c.String(http.StatusInternalServerError, "unexpected error") - return - } - - // create a hash of the query to use as a key for the prepared plan cache - // in this hash, we include the printed operation - // and the extracted variables (see below) - err = shared.Printer.Print(shared.Doc, h.definition, shared.Hash) - if err != nil { - requestLogger.Error("unable to print document", zap.Error(err)) - h.respondWithInternalServerError(c.Writer, requestLogger) - return - } - - // add the extracted variables to the hash - _, err = shared.Hash.Write(shared.Doc.Input.Variables) - if err != nil { - requestLogger.Error("hash write failed", zap.Error(err)) - h.respondWithInternalServerError(c.Writer, requestLogger) - return - } - operationID := shared.Hash.Sum64() // generate the operation ID - shared.Hash.Reset() - - span.SetAttributes(ctrace.WgOperationHash.Int64(int64(operationID))) - - // try to get a prepared plan for this operation ID from the cache - cachedPlan, ok := h.planCache.Get(operationID) - if ok && cachedPlan != nil { - // re-use a prepared plan - preparedPlan = cachedPlan.(planWithExtractedVariables) - } else { - // prepare a new plan using single flight - // this ensures that we only prepare the plan once for this operation ID - sharedPreparedPlan, err, _ := h.sf.Do(strconv.FormatUint(operationID, 10), func() (interface{}, error) { - prepared, err := h.preparePlan(requestOperationNameBytes, shared) - if err != nil { - return nil, err - } - // cache the prepared plan for 1 hour - h.planCache.SetWithTTL(operationID, prepared, 1, time.Hour) - return prepared, nil - }) - if err != nil { - if shared.Report.HasErrors() { - c.Status(http.StatusBadRequest) - h.writeRequestErrorsFromReport(shared.Report, c.Writer, requestLogger) - } else { - requestLogger.Error("prepare plan failed", zap.Error(err)) - h.respondWithInternalServerError(c.Writer, requestLogger) - } - return - } - - if sharedPreparedPlan == nil { - requestLogger.Error("prepare plan is nil", zap.Error(err)) - c.String(http.StatusInternalServerError, "unexpected error") - return - } - preparedPlan = sharedPreparedPlan.(planWithExtractedVariables) - } - - if len(preparedPlan.variables) != 0 { - shared.Ctx.Variables = MergeJsonRightIntoLeft(shared.Ctx.Variables, preparedPlan.variables) - } - - switch p := preparedPlan.preparedPlan.(type) { - case *plan.SynchronousResponsePlan: - c.Header("Content-Type", "application/json") - - executionBuf := pool.GetBytesBuffer() - defer pool.PutBytesBuffer(executionBuf) - - err := h.resolver.ResolveGraphQLResponse(shared.Ctx, p.Response, nil, executionBuf) - if err != nil { - if errors.Is(err, context.Canceled) { - return - } - - c.Status(http.StatusInternalServerError) - h.writeRequestErrors(graphql.RequestErrorsFromError(couldNotResolveResponseErr), c.Writer, requestLogger) - requestLogger.Error("unable to resolve GraphQL response", zap.Error(err)) - return - } - _, err = executionBuf.WriteTo(c.Writer) - if err != nil { - requestLogger.Error("respond to client", zap.Error(err)) - return - } - case *plan.SubscriptionResponsePlan: - var ( - flushWriter *flushwriter.HttpFlushWriter - ok bool - ) - shared.Ctx, flushWriter, ok = flushwriter.GetFlushWriter(shared.Ctx, shared.Ctx.Variables, c.Request, c.Writer) - if !ok { - requestLogger.Error("connection not flushable") - c.String(http.StatusInternalServerError, "unexpected error") - return - } - - err := h.resolver.ResolveGraphQLSubscription(shared.Ctx, p.Response, flushWriter) - if err != nil { - if errors.Is(err, context.Canceled) { - return - } - - c.Status(http.StatusInternalServerError) - h.writeRequestErrors(graphql.RequestErrorsFromError(couldNotResolveResponseErr), c.Writer, requestLogger) - requestLogger.Error("unable to resolve subscription response", zap.Error(err)) - return - } - case *plan.StreamingResponsePlan: - c.String(http.StatusInternalServerError, "not implemented") - } -} - -func (h *GraphQLHandler) logInternalErrors(report *operationreport.Report, requestLogger *zap.Logger) { - var internalErr error - for _, err := range report.InternalErrors { - internalErr = multierror.Append(internalErr, err) - } - - if internalErr != nil { - requestLogger.Error("internal error", zap.Error(internalErr)) - } -} - -func (h *GraphQLHandler) writeRequestErrorsFromReport(report *operationreport.Report, w http.ResponseWriter, requestLogger *zap.Logger) { - requestErrors := graphql.RequestErrorsFromOperationReport(*report) - h.writeRequestErrors(requestErrors, w, requestLogger) - - // log internal errors - h.logInternalErrors(report, requestLogger) - - // write internal server error if there are no external errors but there are internal errors - if len(report.ExternalErrors) == 0 && len(report.InternalErrors) > 0 { - h.writeRequestErrors(graphql.RequestErrorsFromError(internalServerErrorErr), w, requestLogger) - } -} - -func (h *GraphQLHandler) writeRequestErrors(requestErrors graphql.RequestErrors, w http.ResponseWriter, requestLogger *zap.Logger) { - if requestErrors != nil { - if _, err := requestErrors.WriteResponse(w); err != nil { - requestLogger.Error("error writing response", zap.Error(err)) - } - } -} - -func (h *GraphQLHandler) respondWithInternalServerError(w http.ResponseWriter, requestLogger *zap.Logger) { - w.WriteHeader(http.StatusInternalServerError) - h.writeRequestErrors(graphql.RequestErrorsFromError(internalServerErrorErr), w, requestLogger) -} - -func (h *GraphQLHandler) preparePlan(requestOperationName []byte, shared *pool.Shared) (planWithExtractedVariables, error) { - // copy the extracted variables from the shared document - // this is necessary because the shared document is reused across requests - variables := make([]byte, len(shared.Doc.Input.Variables)) - copy(variables, shared.Doc.Input.Variables) - - // print the shared document into a buffer and reparse it - // this is necessary because the shared document will be re-used across requests - // as the plan is cached, and will have references to the document, it cannot be re-used - buf := &bytes.Buffer{} - err := shared.Printer.Print(shared.Doc, h.definition, buf) - if err != nil { - return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) - } - - // parse the document again into a non-shared document, which will be used for planning - // this will be cached, so it's insignificant that reparsing causes overhead - doc, report := astparser.ParseGraphqlDocumentBytes(buf.Bytes()) - if report.HasErrors() { - return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) - } - - // validate the document before planning - state := shared.Validation.Validate(&doc, h.definition, shared.Report) - if state != astvalidation.Valid { - return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationValidationFailed, state.String()) - } - - // create and postprocess the plan - preparedPlan := shared.Planner.Plan(&doc, h.definition, unsafebytes.BytesToString(requestOperationName), shared.Report) - if shared.Report.HasErrors() { - return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) - } - shared.Postprocess.Process(preparedPlan) - - return planWithExtractedVariables{ - preparedPlan: preparedPlan, - variables: variables, - }, nil -} diff --git a/router/pkg/graphql/handler.go b/router/pkg/graphql/handler.go new file mode 100644 index 0000000000..bab4132972 --- /dev/null +++ b/router/pkg/graphql/handler.go @@ -0,0 +1,329 @@ +package graphql + +import ( + "bytes" + "context" + "errors" + "fmt" + "github.com/go-chi/chi/middleware" + "github.com/wundergraph/cosmo/router/pkg/contextx" + "github.com/wundergraph/cosmo/router/pkg/flushwriter" + "github.com/wundergraph/cosmo/router/pkg/logging" + "github.com/wundergraph/cosmo/router/pkg/otel" + "go.opentelemetry.io/otel/trace" + "net/http" + "strconv" + "sync" + "time" + + "github.com/dgraph-io/ristretto" + "github.com/hashicorp/go-multierror" + "github.com/tidwall/gjson" + "github.com/tidwall/sjson" + "github.com/wundergraph/cosmo/router/pkg/internal/unsafebytes" + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" + "github.com/wundergraph/graphql-go-tools/v2/pkg/astvalidation" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/graphql" + "github.com/wundergraph/graphql-go-tools/v2/pkg/operationreport" + "go.uber.org/zap" + "golang.org/x/sync/singleflight" + + "github.com/wundergraph/cosmo/router/pkg/pool" +) + +const ( + ErrMsgOperationParseFailed = "failed to parse operation: %w" + ErrMsgOperationValidationFailed = "operation validation failed: %s" +) + +var ( + couldNotResolveResponseErr = errors.New("could not resolve response") + internalServerErrorErr = errors.New("internal server error") +) + +type planWithExtractedVariables struct { + preparedPlan plan.Plan + variables []byte +} + +func MergeJsonRightIntoLeft(left, right []byte) []byte { + if len(left) == 0 { + return right + } + if len(right) == 0 { + return left + } + result := gjson.ParseBytes(right) + result.ForEach(func(key, value gjson.Result) bool { + left, _ = sjson.SetRawBytes(left, key.Str, unsafebytes.StringToBytes(value.Raw)) + return true + }) + return left +} + +type HandlerOptions struct { + PlanConfig plan.Configuration + Definition *ast.Document + Resolver *resolve.Resolver + Pool *pool.Pool + Cache *ristretto.Cache + Log *zap.Logger +} + +func NewHandler(opts HandlerOptions) *GraphQLHandler { + graphQLHandler := &GraphQLHandler{ + planConfig: opts.PlanConfig, + definition: opts.Definition, + resolver: opts.Resolver, + log: opts.Log, + pool: opts.Pool, + sf: &singleflight.Group{}, + prepared: map[uint64]planWithExtractedVariables{}, + preparedMux: &sync.RWMutex{}, + planCache: opts.Cache, + } + return graphQLHandler +} + +type GraphQLHandler struct { + planConfig plan.Configuration + definition *ast.Document + resolver *resolve.Resolver + log *zap.Logger + pool *pool.Pool + sf *singleflight.Group + + prepared map[uint64]planWithExtractedVariables + preparedMux *sync.RWMutex + + planCache *ristretto.Cache +} + +func (h *GraphQLHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + + var ( + preparedPlan planWithExtractedVariables + ) + + requestLogger := h.log.With(logging.WithRequestID(middleware.GetReqID(r.Context()))) + operationContext := contextx.GetOperationContext(r.Context()) + + requestOperationNameBytes := []byte(operationContext.Name) + + if operationContext.Name == "" { + operationContext.Plan.Normalizer.NormalizeOperation(operationContext.Plan.Doc, h.definition, operationContext.Plan.Report) + } else { + operationContext.Plan.Normalizer.NormalizeNamedOperation(operationContext.Plan.Doc, h.definition, requestOperationNameBytes, operationContext.Plan.Report) + } + + if operationContext.Plan.Report.HasErrors() { + logInternalErrors(operationContext.Plan.Report, requestLogger) + w.WriteHeader(http.StatusBadRequest) + writeRequestErrorsFromReport(operationContext.Plan.Report, w, requestLogger) + return + } + + // add the operation name to the hash + // this is important for multi operation documents to have a different hash for each operation + // otherwise, the prepared plan cache would return the same plan for all operations + _, err := operationContext.Plan.Hash.Write(requestOperationNameBytes) + if err != nil { + requestLogger.Error("hash write failed", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) + return + } + + // create a hash of the query to use as a key for the prepared plan cache + // in this hash, we include the printed operation + // and the extracted variables (see below) + err = operationContext.Plan.Printer.Print(operationContext.Plan.Doc, h.definition, operationContext.Plan.Hash) + if err != nil { + requestLogger.Error("unable to print document", zap.Error(err)) + h.respondWithInternalServerError(w, requestLogger) + return + } + + // add the extracted variables to the hash + _, err = operationContext.Plan.Hash.Write(operationContext.Plan.Doc.Input.Variables) + if err != nil { + requestLogger.Error("hash write failed", zap.Error(err)) + h.respondWithInternalServerError(w, requestLogger) + return + } + operationID := operationContext.Plan.Hash.Sum64() // generate the operation ID + operationContext.Plan.Hash.Reset() + + span := trace.SpanFromContext(r.Context()) + span.SetAttributes(otel.WgOperationHash.Int64(int64(operationID))) + + // try to get a prepared plan for this operation ID from the cache + cachedPlan, ok := h.planCache.Get(operationID) + if ok && cachedPlan != nil { + // re-use a prepared plan + preparedPlan = cachedPlan.(planWithExtractedVariables) + } else { + // prepare a new plan using single flight + // this ensures that we only prepare the plan once for this operation ID + sharedPreparedPlan, err, _ := h.sf.Do(strconv.FormatUint(operationID, 10), func() (interface{}, error) { + prepared, err := h.preparePlan(requestOperationNameBytes, operationContext.Plan) + if err != nil { + return nil, err + } + // cache the prepared plan for 1 hour + h.planCache.SetWithTTL(operationID, prepared, 1, time.Hour) + return prepared, nil + }) + if err != nil { + if operationContext.Plan.Report.HasErrors() { + w.WriteHeader(http.StatusBadRequest) + writeRequestErrorsFromReport(operationContext.Plan.Report, w, requestLogger) + } else { + requestLogger.Error("prepare plan failed", zap.Error(err)) + h.respondWithInternalServerError(w, requestLogger) + } + return + } + + if sharedPreparedPlan == nil { + requestLogger.Error("prepare plan is nil", zap.Error(err)) + w.WriteHeader(http.StatusInternalServerError) + return + } + preparedPlan = sharedPreparedPlan.(planWithExtractedVariables) + } + + if len(preparedPlan.variables) != 0 { + operationContext.Plan.Ctx.Variables = MergeJsonRightIntoLeft(operationContext.Plan.Ctx.Variables, preparedPlan.variables) + } + + switch p := preparedPlan.preparedPlan.(type) { + case *plan.SynchronousResponsePlan: + w.Header().Set("Content-Type", "application/json") + + executionBuf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(executionBuf) + + err := h.resolver.ResolveGraphQLResponse(operationContext.Plan.Ctx, p.Response, nil, executionBuf) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + + w.WriteHeader(http.StatusInternalServerError) + writeRequestErrors(graphql.RequestErrorsFromError(couldNotResolveResponseErr), w, requestLogger) + requestLogger.Error("unable to resolve GraphQL response", zap.Error(err)) + return + } + _, err = executionBuf.WriteTo(w) + if err != nil { + requestLogger.Error("respond to client", zap.Error(err)) + return + } + case *plan.SubscriptionResponsePlan: + var ( + flushWriter *flushwriter.HttpFlushWriter + ok bool + ) + operationContext.Plan.Ctx, flushWriter, ok = flushwriter.GetFlushWriter(operationContext.Plan.Ctx, operationContext.Plan.Ctx.Variables, r, w) + if !ok { + requestLogger.Error("connection not flushable") + w.WriteHeader(http.StatusInternalServerError) + return + } + + err := h.resolver.ResolveGraphQLSubscription(operationContext.Plan.Ctx, p.Response, flushWriter) + if err != nil { + if errors.Is(err, context.Canceled) { + return + } + + w.WriteHeader(http.StatusInternalServerError) + writeRequestErrors(graphql.RequestErrorsFromError(couldNotResolveResponseErr), w, requestLogger) + requestLogger.Error("unable to resolve subscription response", zap.Error(err)) + return + } + case *plan.StreamingResponsePlan: + w.WriteHeader(http.StatusInternalServerError) + } +} + +func (h *GraphQLHandler) respondWithInternalServerError(w http.ResponseWriter, requestLogger *zap.Logger) { + w.WriteHeader(http.StatusInternalServerError) + writeRequestErrors(graphql.RequestErrorsFromError(internalServerErrorErr), w, requestLogger) +} + +func (h *GraphQLHandler) preparePlan(requestOperationName []byte, shared *pool.Shared) (planWithExtractedVariables, error) { + // copy the extracted variables from the shared document + // this is necessary because the shared document is reused across requests + variables := make([]byte, len(shared.Doc.Input.Variables)) + copy(variables, shared.Doc.Input.Variables) + + // print the shared document into a buffer and reparse it + // this is necessary because the shared document will be re-used across requests + // as the plan is cached, and will have references to the document, it cannot be re-used + buf := &bytes.Buffer{} + err := shared.Printer.Print(shared.Doc, h.definition, buf) + if err != nil { + return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) + } + + // parse the document again into a non-shared document, which will be used for planning + // this will be cached, so it's insignificant that reparsing causes overhead + doc, report := astparser.ParseGraphqlDocumentBytes(buf.Bytes()) + if report.HasErrors() { + return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) + } + + // validate the document before planning + state := shared.Validation.Validate(&doc, h.definition, shared.Report) + if state != astvalidation.Valid { + return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationValidationFailed, state.String()) + } + + // create and postprocess the plan + preparedPlan := shared.Planner.Plan(&doc, h.definition, unsafebytes.BytesToString(requestOperationName), shared.Report) + if shared.Report.HasErrors() { + return planWithExtractedVariables{}, fmt.Errorf(ErrMsgOperationParseFailed, err) + } + shared.Postprocess.Process(preparedPlan) + + return planWithExtractedVariables{ + preparedPlan: preparedPlan, + variables: variables, + }, nil +} + +func logInternalErrors(report *operationreport.Report, requestLogger *zap.Logger) { + var internalErr error + for _, err := range report.InternalErrors { + internalErr = multierror.Append(internalErr, err) + } + + if internalErr != nil { + requestLogger.Error("internal error", zap.Error(internalErr)) + } +} + +func writeRequestErrorsFromReport(report *operationreport.Report, w http.ResponseWriter, requestLogger *zap.Logger) { + requestErrors := graphql.RequestErrorsFromOperationReport(*report) + writeRequestErrors(requestErrors, w, requestLogger) + + // log internal errors + logInternalErrors(report, requestLogger) + + // write internal server error if there are no external errors but there are internal errors + if len(report.ExternalErrors) == 0 && len(report.InternalErrors) > 0 { + writeRequestErrors(graphql.RequestErrorsFromError(internalServerErrorErr), w, requestLogger) + } +} + +func writeRequestErrors(requestErrors graphql.RequestErrors, w http.ResponseWriter, requestLogger *zap.Logger) { + if requestErrors != nil { + if _, err := requestErrors.WriteResponse(w); err != nil { + requestLogger.Error("error writing response", zap.Error(err)) + } + } +} diff --git a/router/pkg/graphql/prehandler.go b/router/pkg/graphql/prehandler.go new file mode 100644 index 0000000000..e6f9eaa9ca --- /dev/null +++ b/router/pkg/graphql/prehandler.go @@ -0,0 +1,142 @@ +package graphql + +import ( + "errors" + "github.com/buger/jsonparser" + "github.com/go-chi/chi/middleware" + "github.com/wundergraph/cosmo/router/pkg/contextx" + "github.com/wundergraph/cosmo/router/pkg/logging" + "github.com/wundergraph/cosmo/router/pkg/otel" + "github.com/wundergraph/cosmo/router/pkg/pool" + ctrace "github.com/wundergraph/cosmo/router/pkg/trace" + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/plan" + "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/resolve" + "github.com/wundergraph/graphql-go-tools/v2/pkg/graphql" + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "io" + "net/http" +) + +type PreHandlerOptions struct { + Logger *zap.Logger + Pool *pool.Pool + RenameTypeNames []resolve.RenameTypeName + PlanConfig plan.Configuration +} + +type PreHandler struct { + log *zap.Logger + pool *pool.Pool + renameTypeNames []resolve.RenameTypeName + planConfig plan.Configuration +} + +func NewPreHandler(opts *PreHandlerOptions) *PreHandler { + return &PreHandler{ + log: opts.Logger, + pool: opts.Pool, + renameTypeNames: opts.RenameTypeNames, + planConfig: opts.PlanConfig, + } +} + +func (h *PreHandler) Handler(handler http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + requestLogger := h.log.With(logging.WithRequestID(middleware.GetReqID(r.Context()))) + + buf := pool.GetBytesBuffer() + defer pool.PutBytesBuffer(buf) + _, err := io.Copy(buf, r.Body) + if err != nil { + requestLogger.Error("failed to read request body", zap.Error(err)) + writeRequestErrors(graphql.RequestErrorsFromError(errors.New("bad request")), w, requestLogger) + w.WriteHeader(http.StatusInternalServerError) + return + } + + body := buf.Bytes() + + requestQuery, _ := jsonparser.GetString(body, "query") + requestOperationName, _ := jsonparser.GetString(body, "operationName") + requestVariables, _, _, _ := jsonparser.Get(body, "variables") + requestOperationType := "" + + shared := h.pool.GetSharedFromRequest(r, h.planConfig, pool.Config{ + RenameTypeNames: h.renameTypeNames, + }) + defer h.pool.PutShared(shared) + shared.Ctx.Variables = requestVariables + shared.Doc.Input.ResetInputString(requestQuery) + shared.Parser.Parse(shared.Doc, shared.Report) + + // If the operationName is not set, we try to get it from the named operation in the document + if requestOperationName == "" { + if len(shared.Doc.OperationDefinitions) == 1 { + requestOperationName = shared.Doc.Input.ByteSlice(shared.Doc.OperationDefinitions[0].Name).String() + } + } + + // If multiple operations are defined, but no operationName is set, we return an error + if len(shared.Doc.OperationDefinitions) > 1 && requestOperationName == "" { + requestLogger.Error("operation name is required when multiple operations are defined") + w.WriteHeader(http.StatusBadRequest) + w.Write([]byte("operation name is required when multiple operations are defined")) + return + } + + // Extract the operation type from the first operation that matches the operationName + for _, op := range shared.Doc.OperationDefinitions { + if shared.Doc.Input.ByteSlice(op.Name).String() == requestOperationName { + switch op.OperationType { + case ast.OperationTypeQuery: + requestOperationType = "query" + case ast.OperationTypeMutation: + requestOperationType = "mutation" + case ast.OperationTypeSubscription: + requestOperationType = "subscription" + } + break + } + } + + ctxWithOperation := contextx.WithOperationContext(r.Context(), &contextx.OperationContext{ + Name: requestOperationName, + Type: requestOperationType, + Content: requestQuery, + Plan: shared, + }) + + // Make it available in the request context as well for metrics etc. + r = r.WithContext(ctxWithOperation) + + // Add the operation to the trace span + span := trace.SpanFromContext(r.Context()) + + // Set the span name to the operation name after we figured it out + span.SetName(ctrace.SpanNameFormatter(requestOperationName, r)) + + span.SetAttributes(otel.WgOperationName.String(requestOperationName)) + span.SetAttributes(otel.WgOperationType.String(requestOperationType)) + span.SetAttributes(otel.WgOperationContent.String(requestQuery)) + + // Add client info to trace span + clientName := ctrace.GetClientInfo(r.Header, "graphql-client-name", "apollographql-client-name", "unknown") + clientVersion := ctrace.GetClientInfo(r.Header, "graphql-client-version", "apollographql-client-version", "missing") + span.SetAttributes(otel.WgClientName.String(clientName)) + span.SetAttributes(otel.WgClientVersion.String(clientVersion)) + + if shared.Report.HasErrors() { + logInternalErrors(shared.Report, requestLogger) + w.WriteHeader(http.StatusBadRequest) + writeRequestErrorsFromReport(shared.Report, w, requestLogger) + return + } + + // Add the operation to the context, so we can access it later in custom transports etc. + shared.Ctx = shared.Ctx.WithContext(ctxWithOperation) + + handler.ServeHTTP(w, r) + } +} diff --git a/router/pkg/handler/cors/config.go b/router/pkg/handler/cors/config.go new file mode 100644 index 0000000000..0cc82fc232 --- /dev/null +++ b/router/pkg/handler/cors/config.go @@ -0,0 +1,142 @@ +package cors + +import ( + "net/http" + "strings" +) + +type cors struct { + allowAllOrigins bool + allowCredentials bool + allowOriginFunc func(string) bool + allowOrigins []string + normalHeaders http.Header + preflightHeaders http.Header + wildcardOrigins [][]string + handler http.Handler +} + +var ( + DefaultSchemas = []string{ + "http://", + "https://", + } + ExtensionSchemas = []string{ + "chrome-extension://", + "safari-extension://", + "moz-extension://", + "ms-browser-extension://", + } + FileSchemas = []string{ + "file://", + } + WebSocketSchemas = []string{ + "ws://", + "wss://", + } +) + +func newCors(handler http.Handler, config Config) *cors { + if err := config.Validate(); err != nil { + panic(err.Error()) + } + + for _, origin := range config.AllowOrigins { + if origin == "*" { + config.AllowAllOrigins = true + } + } + + return &cors{ + allowOriginFunc: config.AllowOriginFunc, + allowAllOrigins: config.AllowAllOrigins, + allowCredentials: config.AllowCredentials, + allowOrigins: normalize(config.AllowOrigins), + normalHeaders: generateNormalHeaders(config), + preflightHeaders: generatePreflightHeaders(config), + wildcardOrigins: config.parseWildcardRules(), + handler: handler, + } +} + +func (cors *cors) ServeHTTP(w http.ResponseWriter, r *http.Request) { + origin := r.Header.Get("Origin") + if len(origin) == 0 { + // request is not a CORS request + cors.handler.ServeHTTP(w, r) + return + } + host := r.Host + + if origin == "http://"+host || origin == "https://"+host { + // request is not a CORS request but have origin header. + // for example, use fetch api + cors.handler.ServeHTTP(w, r) + return + } + + if !cors.validateOrigin(origin) { + w.WriteHeader(http.StatusForbidden) + return + } + + if r.Method == "OPTIONS" { + cors.handlePreflight(w) + defer w.WriteHeader(http.StatusNoContent) // Using 204 is better than 200 when the request status is OPTIONS + } else { + cors.handleNormal(w) + cors.handler.ServeHTTP(w, r) + } + + if !cors.allowAllOrigins { + w.Header().Set("Access-Control-Allow-Origin", origin) + } +} + +func (cors *cors) validateWildcardOrigin(origin string) bool { + for _, w := range cors.wildcardOrigins { + if w[0] == "*" && strings.HasSuffix(origin, w[1]) { + return true + } + if w[1] == "*" && strings.HasPrefix(origin, w[0]) { + return true + } + if strings.HasPrefix(origin, w[0]) && strings.HasSuffix(origin, w[1]) { + return true + } + } + + return false +} + +func (cors *cors) validateOrigin(origin string) bool { + if cors.allowAllOrigins { + return true + } + for _, value := range cors.allowOrigins { + if value == origin { + return true + } + } + if len(cors.wildcardOrigins) > 0 && cors.validateWildcardOrigin(origin) { + return true + } + if cors.allowOriginFunc != nil { + return cors.allowOriginFunc(origin) + } + return false +} + +func (cors *cors) handlePreflight(w http.ResponseWriter) { + header := w.Header() + for key, value := range cors.preflightHeaders { + header[key] = value + } +} + +func (cors *cors) handleNormal(w http.ResponseWriter) { + header := w.Header() + for key, value := range cors.normalHeaders { + header[key] = value + } +} diff --git a/router/pkg/handler/cors/cors.go b/router/pkg/handler/cors/cors.go new file mode 100644 index 0000000000..646f9222be --- /dev/null +++ b/router/pkg/handler/cors/cors.go @@ -0,0 +1,166 @@ +package cors + +import ( + "errors" + "net/http" + "strings" + "time" +) + +// Config represents all available options for the middleware. +type Config struct { + AllowAllOrigins bool + + // AllowOrigins is a list of origins a cross-domain request can be executed from. + // If the special "*" value is present in the list, all origins will be allowed. + // Default value is [] + AllowOrigins []string + + // AllowOriginFunc is a custom function to validate the origin. It take the origin + // as argument and returns true if allowed or false otherwise. If this option is + // set, the content of AllowOrigins is ignored. + AllowOriginFunc func(origin string) bool + + // AllowMethods is a list of methods the client is allowed to use with + // cross-domain requests. Default value is simple methods (GET, POST, PUT, PATCH, DELETE, HEAD, and OPTIONS) + AllowMethods []string + + // AllowHeaders is list of non simple headers the client is allowed to use with + // cross-domain requests. + AllowHeaders []string + + // AllowCredentials indicates whether the request can include user credentials like + // cookies, HTTP authentication or client side SSL certificates. + AllowCredentials bool + + // ExposeHeaders indicates which headers are safe to expose to the API of a CORS + // API specification + ExposeHeaders []string + + // MaxAge indicates how long (with second-precision) the results of a preflight request + // can be cached + MaxAge time.Duration + + // Allows to add origins like http://some-domain/*, https://api.* or http://some.*.subdomain.com + AllowWildcard bool + + // Allows usage of popular browser extensions schemas + AllowBrowserExtensions bool + + // Allows usage of WebSocket protocol + AllowWebSockets bool + + // Allows usage of file:// schema (dangerous!) use it only when you 100% sure it's needed + AllowFiles bool +} + +// AddAllowMethods is allowed to add custom methods +func (c *Config) AddAllowMethods(methods ...string) { + c.AllowMethods = append(c.AllowMethods, methods...) +} + +// AddAllowHeaders is allowed to add custom headers +func (c *Config) AddAllowHeaders(headers ...string) { + c.AllowHeaders = append(c.AllowHeaders, headers...) +} + +// AddExposeHeaders is allowed to add custom expose headers +func (c *Config) AddExposeHeaders(headers ...string) { + c.ExposeHeaders = append(c.ExposeHeaders, headers...) +} + +func (c Config) getAllowedSchemas() []string { + allowedSchemas := DefaultSchemas + if c.AllowBrowserExtensions { + allowedSchemas = append(allowedSchemas, ExtensionSchemas...) + } + if c.AllowWebSockets { + allowedSchemas = append(allowedSchemas, WebSocketSchemas...) + } + if c.AllowFiles { + allowedSchemas = append(allowedSchemas, FileSchemas...) + } + return allowedSchemas +} + +func (c Config) validateAllowedSchemas(origin string) bool { + allowedSchemas := c.getAllowedSchemas() + for _, schema := range allowedSchemas { + if strings.HasPrefix(origin, schema) { + return true + } + } + return false +} + +// Validate is check configuration of user defined. +func (c Config) Validate() error { + if c.AllowAllOrigins && (c.AllowOriginFunc != nil || len(c.AllowOrigins) > 0) { + return errors.New("conflict settings: all origins are allowed. AllowOriginFunc or AllowOrigins is not needed") + } + if !c.AllowAllOrigins && c.AllowOriginFunc == nil && len(c.AllowOrigins) == 0 { + return errors.New("conflict settings: all origins disabled") + } + for _, origin := range c.AllowOrigins { + if !strings.Contains(origin, "*") && !c.validateAllowedSchemas(origin) { + return errors.New("bad origin: origins must contain '*' or include " + strings.Join(c.getAllowedSchemas(), ",")) + } + } + return nil +} + +func (c Config) parseWildcardRules() [][]string { + var wRules [][]string + + if !c.AllowWildcard { + return wRules + } + + for _, o := range c.AllowOrigins { + if !strings.Contains(o, "*") { + continue + } + + if c := strings.Count(o, "*"); c > 1 { + panic(errors.New("only one * is allowed").Error()) + } + + i := strings.Index(o, "*") + if i == 0 { + wRules = append(wRules, []string{"*", o[1:]}) + continue + } + if i == (len(o) - 1) { + wRules = append(wRules, []string{o[:i-1], "*"}) + continue + } + + wRules = append(wRules, []string{o[:i], o[i+1:]}) + } + + return wRules +} + +// DefaultConfig returns a generic default configuration mapped to localhost. +func DefaultConfig() Config { + return Config{ + AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "HEAD", "OPTIONS"}, + AllowHeaders: []string{"Origin", "Content-Length", "Content-Type"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + } +} + +// Default returns the location middleware with default configuration. +func Default() func(h http.Handler) http.Handler { + config := DefaultConfig() + config.AllowAllOrigins = true + return New(config) +} + +// New returns the location middleware with user-defined custom configuration. +func New(config Config) func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + return newCors(h, config) + } +} diff --git a/router/pkg/handler/cors/cors_test.go b/router/pkg/handler/cors/cors_test.go new file mode 100644 index 0000000000..cfe692c57f --- /dev/null +++ b/router/pkg/handler/cors/cors_test.go @@ -0,0 +1,410 @@ +package cors + +import ( + "context" + "github.com/go-chi/chi" + "net/http" + "net/http/httptest" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func newTestRouter(config Config) *chi.Mux { + router := chi.NewRouter() + router.Use(New(config)) + router.Get("/", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + writer.Write([]byte("get")) + }) + router.Post("/", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + writer.Write([]byte("post")) + }) + router.Patch("/", func(writer http.ResponseWriter, request *http.Request) { + writer.WriteHeader(http.StatusOK) + writer.Write([]byte("patch")) + }) + return router +} + +func performRequest(r http.Handler, method, origin string) *httptest.ResponseRecorder { + return performRequestWithHeaders(r, method, origin, http.Header{}) +} + +func performRequestWithHeaders(r http.Handler, method, origin string, header http.Header) *httptest.ResponseRecorder { + req, _ := http.NewRequestWithContext(context.Background(), method, "/", nil) + // From go/net/http/request.go: + // For incoming requests, the Host header is promoted to the + // Request.Host field and removed from the Header map. + req.Host = header.Get("Host") + header.Del("Host") + if len(origin) > 0 { + header.Set("Origin", origin) + } + req.Header = header + w := httptest.NewRecorder() + r.ServeHTTP(w, req) + return w +} + +func TestConfigAddAllow(t *testing.T) { + config := Config{} + config.AddAllowMethods("POST") + config.AddAllowMethods("GET", "PUT") + config.AddExposeHeaders() + + config.AddAllowHeaders("Some", " cool") + config.AddAllowHeaders("header") + config.AddExposeHeaders() + + config.AddExposeHeaders() + config.AddExposeHeaders("exposed", "header") + config.AddExposeHeaders("hey") + + assert.Equal(t, config.AllowMethods, []string{"POST", "GET", "PUT"}) + assert.Equal(t, config.AllowHeaders, []string{"Some", " cool", "header"}) + assert.Equal(t, config.ExposeHeaders, []string{"exposed", "header", "hey"}) +} + +func TestBadConfig(t *testing.T) { + assert.Panics(t, func() { New(Config{})(nil) }) + assert.Panics(t, func() { + New(Config{ + AllowAllOrigins: true, + AllowOrigins: []string{"http://google.com"}, + })(nil) + }) + assert.Panics(t, func() { + New(Config{ + AllowAllOrigins: true, + AllowOriginFunc: func(origin string) bool { return false }, + })(nil) + }) + assert.Panics(t, func() { + New(Config{ + AllowOrigins: []string{"google.com"}, + })(nil) + }) +} + +func TestNormalize(t *testing.T) { + values := normalize([]string{ + "http-Access ", "Post", "POST", " poSt ", + "HTTP-Access", "", + }) + assert.Equal(t, values, []string{"http-access", "post", ""}) + + values = normalize(nil) + assert.Nil(t, values) + + values = normalize([]string{}) + assert.Equal(t, values, []string{}) +} + +func TestConvert(t *testing.T) { + methods := []string{"Get", "GET", "get"} + headers := []string{"X-CSRF-TOKEN", "X-CSRF-Token", "x-csrf-token"} + + assert.Equal(t, []string{"GET", "GET", "GET"}, convert(methods, strings.ToUpper)) + assert.Equal(t, []string{"X-Csrf-Token", "X-Csrf-Token", "X-Csrf-Token"}, convert(headers, http.CanonicalHeaderKey)) +} + +func TestGenerateNormalHeaders_AllowAllOrigins(t *testing.T) { + header := generateNormalHeaders(Config{ + AllowAllOrigins: false, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 1) + + header = generateNormalHeaders(Config{ + AllowAllOrigins: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "*") + assert.Equal(t, header.Get("Vary"), "") + assert.Len(t, header, 1) +} + +func TestGenerateNormalHeaders_AllowCredentials(t *testing.T) { + header := generateNormalHeaders(Config{ + AllowCredentials: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Credentials"), "true") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGenerateNormalHeaders_ExposedHeaders(t *testing.T) { + header := generateNormalHeaders(Config{ + ExposeHeaders: []string{"X-user", "xPassword"}, + }) + assert.Equal(t, header.Get("Access-Control-Expose-Headers"), "X-User,Xpassword") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowAllOrigins: false, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 1) + + header = generateNormalHeaders(Config{ + AllowAllOrigins: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Origin"), "*") + assert.Equal(t, header.Get("Vary"), "") + assert.Len(t, header, 1) +} + +func TestGeneratePreflightHeaders_AllowCredentials(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowCredentials: true, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Credentials"), "true") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_AllowMethods(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowMethods: []string{"GET ", "post", "PUT", " put "}, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Methods"), "GET,POST,PUT") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_AllowHeaders(t *testing.T) { + header := generatePreflightHeaders(Config{ + AllowHeaders: []string{"X-user", "Content-Type"}, + }) + assert.Equal(t, header.Get("Access-Control-Allow-Headers"), "X-User,Content-Type") + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestGeneratePreflightHeaders_MaxAge(t *testing.T) { + header := generatePreflightHeaders(Config{ + MaxAge: 12 * time.Hour, + }) + assert.Equal(t, header.Get("Access-Control-Max-Age"), "43200") // 12*60*60 + assert.Equal(t, header.Get("Vary"), "Origin") + assert.Len(t, header, 2) +} + +func TestValidateOrigin(t *testing.T) { + cors := newCors(nil, Config{ + AllowAllOrigins: true, + }) + assert.True(t, cors.validateOrigin("http://google.com")) + assert.True(t, cors.validateOrigin("https://google.com")) + assert.True(t, cors.validateOrigin("example.com")) + assert.True(t, cors.validateOrigin("chrome-extension://random-extension-id")) + + cors = newCors(nil, Config{ + AllowOrigins: []string{"https://google.com", "https://github.com"}, + AllowOriginFunc: func(origin string) bool { + return (origin == "http://news.ycombinator.com") + }, + AllowBrowserExtensions: true, + }) + assert.False(t, cors.validateOrigin("http://google.com")) + assert.True(t, cors.validateOrigin("https://google.com")) + assert.True(t, cors.validateOrigin("https://github.com")) + assert.True(t, cors.validateOrigin("http://news.ycombinator.com")) + assert.False(t, cors.validateOrigin("http://example.com")) + assert.False(t, cors.validateOrigin("google.com")) + assert.False(t, cors.validateOrigin("chrome-extension://random-extension-id")) + + cors = newCors(nil, Config{ + AllowOrigins: []string{"https://google.com", "https://github.com"}, + }) + assert.False(t, cors.validateOrigin("chrome-extension://random-extension-id")) + assert.False(t, cors.validateOrigin("file://some-dangerous-file.js")) + assert.False(t, cors.validateOrigin("wss://socket-connection")) + + cors = newCors(nil, Config{ + AllowOrigins: []string{ + "chrome-extension://*", + "safari-extension://my-extension-*-app", + "*.some-domain.com", + }, + AllowBrowserExtensions: true, + AllowWildcard: true, + }) + assert.True(t, cors.validateOrigin("chrome-extension://random-extension-id")) + assert.True(t, cors.validateOrigin("chrome-extension://another-one")) + assert.True(t, cors.validateOrigin("safari-extension://my-extension-one-app")) + assert.True(t, cors.validateOrigin("safari-extension://my-extension-two-app")) + assert.False(t, cors.validateOrigin("moz-extension://ext-id-we-not-allow")) + assert.True(t, cors.validateOrigin("http://api.some-domain.com")) + assert.False(t, cors.validateOrigin("http://api.another-domain.com")) + + cors = newCors(nil, Config{ + AllowOrigins: []string{"file://safe-file.js", "wss://some-session-layer-connection"}, + AllowFiles: true, + AllowWebSockets: true, + }) + assert.True(t, cors.validateOrigin("file://safe-file.js")) + assert.False(t, cors.validateOrigin("file://some-dangerous-file.js")) + assert.True(t, cors.validateOrigin("wss://some-session-layer-connection")) + assert.False(t, cors.validateOrigin("ws://not-what-we-expected")) + + cors = newCors(nil, Config{ + AllowOrigins: []string{"*"}, + }) + assert.True(t, cors.validateOrigin("http://google.com")) + assert.True(t, cors.validateOrigin("https://google.com")) + assert.True(t, cors.validateOrigin("example.com")) + assert.True(t, cors.validateOrigin("chrome-extension://random-extension-id")) +} + +func TestPassesAllowOrigins(t *testing.T) { + router := newTestRouter(Config{ + AllowOrigins: []string{"http://google.com"}, + AllowMethods: []string{" GeT ", "get", "post", "PUT ", "Head", "POST"}, + AllowHeaders: []string{"Content-type", "timeStamp "}, + ExposeHeaders: []string{"Data", "x-User"}, + AllowCredentials: false, + MaxAge: 12 * time.Hour, + AllowOriginFunc: func(origin string) bool { + return origin == "http://github.com" + }, + }) + + // no CORS request, origin == "" + w := performRequest(router, "GET", "") + assert.Equal(t, "get", w.Body.String()) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + + // no CORS request, origin == host + h := http.Header{} + h.Set("Host", "facebook.com") + w = performRequestWithHeaders(router, "GET", "http://facebook.com", h) + assert.Equal(t, "get", w.Body.String()) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + + // allowed CORS request + w = performRequest(router, "GET", "http://google.com") + assert.Equal(t, "get", w.Body.String()) + assert.Equal(t, "http://google.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "Data,X-User", w.Header().Get("Access-Control-Expose-Headers")) + + w = performRequest(router, "GET", "http://github.com") + assert.Equal(t, "get", w.Body.String()) + assert.Equal(t, "http://github.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "Data,X-User", w.Header().Get("Access-Control-Expose-Headers")) + + // deny CORS request + w = performRequest(router, "GET", "https://google.com") + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + + // allowed CORS prefligh request + w = performRequest(router, "OPTIONS", "http://github.com") + assert.Equal(t, http.StatusNoContent, w.Code) + assert.Equal(t, "http://github.com", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "", w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "GET,POST,PUT,HEAD", w.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "Content-Type,Timestamp", w.Header().Get("Access-Control-Allow-Headers")) + assert.Equal(t, "43200", w.Header().Get("Access-Control-Max-Age")) + + // deny CORS prefligh request + w = performRequest(router, "OPTIONS", "http://example.com") + assert.Equal(t, http.StatusForbidden, w.Code) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Methods")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Max-Age")) +} + +func TestPassesAllowAllOrigins(t *testing.T) { + router := newTestRouter(Config{ + AllowAllOrigins: true, + AllowMethods: []string{" Patch ", "get", "post", "POST"}, + AllowHeaders: []string{"Content-type", " testheader "}, + ExposeHeaders: []string{"Data2", "x-User2"}, + AllowCredentials: false, + MaxAge: 10 * time.Hour, + }) + + // no CORS request, origin == "" + w := performRequest(router, "GET", "") + assert.Equal(t, "get", w.Body.String()) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Origin")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Empty(t, w.Header().Get("Access-Control-Expose-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + + // allowed CORS request + w = performRequest(router, "POST", "example.com") + assert.Equal(t, "post", w.Body.String()) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "Data2,X-User2", w.Header().Get("Access-Control-Expose-Headers")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + + // allowed CORS prefligh request + w = performRequest(router, "OPTIONS", "https://facebook.com") + assert.Equal(t, http.StatusNoContent, w.Code) + assert.Equal(t, "*", w.Header().Get("Access-Control-Allow-Origin")) + assert.Equal(t, "PATCH,GET,POST", w.Header().Get("Access-Control-Allow-Methods")) + assert.Equal(t, "Content-Type,Testheader", w.Header().Get("Access-Control-Allow-Headers")) + assert.Equal(t, "36000", w.Header().Get("Access-Control-Max-Age")) + assert.Empty(t, w.Header().Get("Access-Control-Allow-Credentials")) +} + +func TestWildcard(t *testing.T) { + router := newTestRouter(Config{ + AllowOrigins: []string{"https://*.github.com", "https://api.*", "http://*", "https://facebook.com", "*.golang.org"}, + AllowMethods: []string{"GET"}, + AllowWildcard: true, + }) + + w := performRequest(router, "GET", "https://gist.github.com") + assert.Equal(t, 200, w.Code) + + w = performRequest(router, "GET", "https://api.github.com/v1/users") + assert.Equal(t, 200, w.Code) + + w = performRequest(router, "GET", "https://giphy.com/") + assert.Equal(t, 403, w.Code) + + w = performRequest(router, "GET", "http://hard-to-find-http-example.com") + assert.Equal(t, 200, w.Code) + + w = performRequest(router, "GET", "https://facebook.com") + assert.Equal(t, 200, w.Code) + + w = performRequest(router, "GET", "https://something.golang.org") + assert.Equal(t, 200, w.Code) + + w = performRequest(router, "GET", "https://something.go.org") + assert.Equal(t, 403, w.Code) + + router = newTestRouter(Config{ + AllowOrigins: []string{"https://github.com", "https://facebook.com"}, + AllowMethods: []string{"GET"}, + }) + + w = performRequest(router, "GET", "https://gist.github.com") + assert.Equal(t, 403, w.Code) + + w = performRequest(router, "GET", "https://github.com") + assert.Equal(t, 200, w.Code) +} diff --git a/router/pkg/handler/cors/utils.go b/router/pkg/handler/cors/utils.go new file mode 100644 index 0000000000..460ef17800 --- /dev/null +++ b/router/pkg/handler/cors/utils.go @@ -0,0 +1,85 @@ +package cors + +import ( + "net/http" + "strconv" + "strings" + "time" +) + +type converter func(string) string + +func generateNormalHeaders(c Config) http.Header { + headers := make(http.Header) + if c.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if len(c.ExposeHeaders) > 0 { + exposeHeaders := convert(normalize(c.ExposeHeaders), http.CanonicalHeaderKey) + headers.Set("Access-Control-Expose-Headers", strings.Join(exposeHeaders, ",")) + } + if c.AllowAllOrigins { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + headers.Set("Vary", "Origin") + } + return headers +} + +func generatePreflightHeaders(c Config) http.Header { + headers := make(http.Header) + if c.AllowCredentials { + headers.Set("Access-Control-Allow-Credentials", "true") + } + if len(c.AllowMethods) > 0 { + allowMethods := convert(normalize(c.AllowMethods), strings.ToUpper) + value := strings.Join(allowMethods, ",") + headers.Set("Access-Control-Allow-Methods", value) + } + if len(c.AllowHeaders) > 0 { + allowHeaders := convert(normalize(c.AllowHeaders), http.CanonicalHeaderKey) + value := strings.Join(allowHeaders, ",") + headers.Set("Access-Control-Allow-Headers", value) + } + if c.MaxAge > time.Duration(0) { + value := strconv.FormatInt(int64(c.MaxAge/time.Second), 10) + headers.Set("Access-Control-Max-Age", value) + } + if c.AllowAllOrigins { + headers.Set("Access-Control-Allow-Origin", "*") + } else { + // Always set Vary headers + // see https://github.com/rs/cors/issues/10, + // https://github.com/rs/cors/commit/dbdca4d95feaa7511a46e6f1efb3b3aa505bc43f#commitcomment-12352001 + + headers.Add("Vary", "Origin") + headers.Add("Vary", "Access-Control-Request-Method") + headers.Add("Vary", "Access-Control-Request-Headers") + } + return headers +} + +func normalize(values []string) []string { + if values == nil { + return nil + } + distinctMap := make(map[string]bool, len(values)) + normalized := make([]string, 0, len(values)) + for _, value := range values { + value = strings.TrimSpace(value) + value = strings.ToLower(value) + if _, seen := distinctMap[value]; !seen { + normalized = append(normalized, value) + distinctMap[value] = true + } + } + return normalized +} + +func convert(s []string, c converter) []string { + var out []string + for _, i := range s { + out = append(out, c(i)) + } + return out +} diff --git a/router/pkg/handler/health/health.go b/router/pkg/handler/health/health.go new file mode 100644 index 0000000000..23d44d079e --- /dev/null +++ b/router/pkg/handler/health/health.go @@ -0,0 +1,15 @@ +package health + +import ( + "net/http" +) + +func New() http.HandlerFunc { + fn := func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + w.Header().Set("Content-Type", "text/plain; charset=utf-8") + _, _ = w.Write([]byte("OK")) + } + + return fn +} diff --git a/router/pkg/handler/health/health_test.go b/router/pkg/handler/health/health_test.go new file mode 100644 index 0000000000..9444fbc3d7 --- /dev/null +++ b/router/pkg/handler/health/health_test.go @@ -0,0 +1,20 @@ +package health + +import ( + "github.com/stretchr/testify/assert" + "github.com/wundergraph/cosmo/router/pkg/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + handler := New() + rec := httptest.NewRecorder() + + handler.ServeHTTP(rec, test.NewRequest(http.MethodGet, "/health")) + + assert.Equal(t, http.StatusOK, rec.Code) + assert.Equal(t, "text/plain; charset=utf-8", rec.Header().Get("Content-Type")) + assert.Equal(t, "OK", rec.Body.String()) +} diff --git a/router/pkg/handler/recovery/recovery.go b/router/pkg/handler/recovery/recovery.go new file mode 100644 index 0000000000..9859551193 --- /dev/null +++ b/router/pkg/handler/recovery/recovery.go @@ -0,0 +1,104 @@ +package recovery + +import ( + "net" + "net/http" + "net/http/httputil" + "os" + "runtime/debug" + "strings" + "time" + + "go.uber.org/zap" +) + +// handler returns a http.Handler with a custom recovery handler +// that recovers from any panics and logs requests using uber-go/zap. +// All errors are logged using zap.Error(). +// stack means whether output the stack info. +type handler struct { + handler http.Handler + logger *zap.Logger + printStack bool +} + +// Option provides a functional approach to define +// configuration for a handler; such as setting the logging +// whether to print stack traces on panic. +type Option func(handler *handler) + +func parseOptions(r *handler, opts ...Option) http.Handler { + for _, option := range opts { + option(r) + } + + if r.logger == nil { + r.logger = zap.NewNop() + } + + return r +} + +func WithPrintStack() Option { + return func(r *handler) { + r.printStack = true + } +} + +func WithLogger(logger *zap.Logger) Option { + return func(r *handler) { + r.logger = logger + } +} + +func New(opts ...Option) func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + r := &handler{handler: h} + return parseOptions(r, opts...) + } +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + defer func() { + if err := recover(); err != nil { + w.WriteHeader(http.StatusInternalServerError) + + // Check for a broken connection, as it is not really a + // condition that warrants a panic stack trace. + var brokenPipe bool + if ne, ok := err.(*net.OpError); ok { + if se, ok := ne.Err.(*os.SyscallError); ok { + if strings.Contains(strings.ToLower(se.Error()), "broken pipe") || strings.Contains(strings.ToLower(se.Error()), "connection reset by peer") { + brokenPipe = true + } + } + } + + httpRequest, _ := httputil.DumpRequest(r, false) + if brokenPipe { + h.logger.Error(r.URL.Path, + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + return + } + + if h.printStack { + h.logger.Error("[Recovery from panic]", + zap.Time("time", time.Now()), + zap.Any("error", err), + zap.String("request", string(httpRequest)), + zap.String("stack", string(debug.Stack())), + ) + } else { + h.logger.Error("[Recovery from panic]", + zap.Time("time", time.Now()), + zap.Any("error", err), + zap.String("request", string(httpRequest)), + ) + } + } + }() + + h.handler.ServeHTTP(w, r) +} diff --git a/router/pkg/handler/recovery/recovery_test.go b/router/pkg/handler/recovery/recovery_test.go new file mode 100644 index 0000000000..d9386ef8a9 --- /dev/null +++ b/router/pkg/handler/recovery/recovery_test.go @@ -0,0 +1,24 @@ +package recovery + +import ( + "github.com/wundergraph/cosmo/router/pkg/test" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRecoveryLoggerWithDefaultOptions(t *testing.T) { + handler := New() + handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + panic("Unexpected error!") + }) + + recovery := handler(handlerFunc) + rec := httptest.NewRecorder() + recovery.ServeHTTP(rec, test.NewRequest(http.MethodGet, "/subdir/asdf")) + + if rec.Code != http.StatusInternalServerError { + t.Errorf("Expected status code %d, got %d", http.StatusInternalServerError, rec.Code) + } + +} diff --git a/router/pkg/handler/requestlogger/requestlogger.go b/router/pkg/handler/requestlogger/requestlogger.go new file mode 100644 index 0000000000..81dfbe9455 --- /dev/null +++ b/router/pkg/handler/requestlogger/requestlogger.go @@ -0,0 +1,102 @@ +package requestlogger + +import ( + "github.com/go-chi/chi/middleware" + "net/http" + "time" + + "go.opentelemetry.io/otel/trace" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" +) + +type Fn func(r *http.Request) []zapcore.Field + +// Option provides a functional approach to define +// configuration for a handler; such as setting the logging +// whether to print stack traces on panic. +type Option func(handler *handler) + +type handler struct { + timeFormat string + utc bool + skipPaths []string + traceID bool // optionally log Open Telemetry TraceID + context Fn + handler http.Handler + logger *zap.Logger +} + +func parseOptions(r *handler, opts ...Option) http.Handler { + for _, option := range opts { + option(r) + } + + return r +} + +func WithContext(fn Fn) Option { + return func(r *handler) { + r.context = fn + } +} + +func WithDefaultOptions() Option { + return func(r *handler) { + r.timeFormat = time.RFC3339 + r.utc = true + r.skipPaths = []string{} + r.traceID = true + r.context = nil + } +} + +func New(logger *zap.Logger, opts ...Option) func(h http.Handler) http.Handler { + return func(h http.Handler) http.Handler { + r := &handler{handler: h, logger: logger} + return parseOptions(r, opts...) + } +} + +func (h *handler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + start := time.Now() + // some evil middlewares modify this values + path := r.URL.Path + query := r.URL.RawQuery + + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + h.handler.ServeHTTP(ww, r) + + end := time.Now() + latency := end.Sub(start) + if h.utc { + end = end.UTC() + } + + fields := []zapcore.Field{ + zap.Int("status", ww.Status()), + zap.String("method", r.Method), + zap.String("path", path), + zap.String("query", query), + // Has to be set by a middleware before this one + zap.String("ip", r.RemoteAddr), + zap.String("user-agent", r.UserAgent()), + zap.Duration("latency", latency), + } + if h.timeFormat != "" { + fields = append(fields, zap.String("time", end.Format(h.timeFormat))) + } + if h.traceID { + spanContext := trace.SpanFromContext(r.Context()).SpanContext() + if spanContext.HasTraceID() { + fields = append(fields, zap.String("traceID", spanContext.TraceID().String())) + } + } + + if h.context != nil { + fields = append(fields, h.context(r)...) + } + + h.logger.Info(path, fields...) + +} diff --git a/router/pkg/handler/requestlogger/requestlogger_test.go b/router/pkg/handler/requestlogger/requestlogger_test.go new file mode 100644 index 0000000000..86ddf15486 --- /dev/null +++ b/router/pkg/handler/requestlogger/requestlogger_test.go @@ -0,0 +1,49 @@ +package requestlogger + +import ( + "bufio" + "bytes" + "encoding/json" + "github.com/stretchr/testify/assert" + "github.com/wundergraph/cosmo/router/pkg/logging" + "github.com/wundergraph/cosmo/router/pkg/test" + "go.uber.org/zap" + "go.uber.org/zap/zapcore" + "net/http" + "net/http/httptest" + "testing" +) + +func TestRequestLogger(t *testing.T) { + + var buffer bytes.Buffer + + encoder := logging.ZapJsonEncoder() + writer := bufio.NewWriter(&buffer) + + logger := zap.New( + zapcore.NewCore(encoder, zapcore.AddSync(writer), zapcore.DebugLevel)) + + handler := New(logger) + handlerFunc := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + w.WriteHeader(http.StatusOK) + }) + + recovery := handler(handlerFunc) + rec := httptest.NewRecorder() + recovery.ServeHTTP(rec, test.NewRequest(http.MethodGet, "/subdir/asdf")) + + writer.Flush() + + assert.Equal(t, http.StatusOK, rec.Code) + + var data map[string]interface{} + err := json.Unmarshal(buffer.Bytes(), &data) + assert.Nil(t, err) + + assert.Equal(t, "GET", data["method"]) + assert.Equal(t, float64(200), data["status"]) + assert.Equal(t, "/subdir/asdf", data["msg"]) + assert.Equal(t, "/subdir/asdf", data["path"]) + +} diff --git a/router/pkg/handlers/health.go b/router/pkg/handlers/health.go deleted file mode 100644 index 716eba72ff..0000000000 --- a/router/pkg/handlers/health.go +++ /dev/null @@ -1,17 +0,0 @@ -package handlers - -import ( - "github.com/gin-gonic/gin" - "net/http" -) - -type HealthHandler struct { -} - -func NewHealthHandler() *HealthHandler { - return &HealthHandler{} -} - -func (h *HealthHandler) Handler(c *gin.Context) { - c.String(http.StatusOK, "OK") -} diff --git a/router/pkg/logging/logging.go b/router/pkg/logging/logging.go index 686cc14dce..1274c02bac 100644 --- a/router/pkg/logging/logging.go +++ b/router/pkg/logging/logging.go @@ -29,7 +29,7 @@ func zapBaseEncoderConfig() zapcore.EncoderConfig { return ec } -func zapJsonEncoder() zapcore.Encoder { +func ZapJsonEncoder() zapcore.Encoder { ec := zapBaseEncoderConfig() ec.EncodeTime = func(t time.Time, enc zapcore.PrimitiveArrayEncoder) { nanos := t.UnixNano() @@ -68,7 +68,7 @@ func newZapLogger(syncer zapcore.WriteSyncer, prettyLogging bool, debug bool, le if prettyLogging { encoder = zapConsoleEncoder() } else { - encoder = zapJsonEncoder() + encoder = ZapJsonEncoder() } if debug { diff --git a/router/pkg/metric/agent.go b/router/pkg/metric/agent.go new file mode 100644 index 0000000000..448394adad --- /dev/null +++ b/router/pkg/metric/agent.go @@ -0,0 +1,101 @@ +package metric + +import ( + "context" + "fmt" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlpmetric/otlpmetrichttp" + "go.opentelemetry.io/otel/exporters/prometheus" + sdkmetric "go.opentelemetry.io/otel/sdk/metric" + "go.opentelemetry.io/otel/sdk/resource" + semconv "go.opentelemetry.io/otel/semconv/v1.4.0" + "go.uber.org/zap" + "net/url" + "time" +) + +var ( + mp *sdkmetric.MeterProvider +) + +// StartAgent starts an opentelemetry metric agent. +func StartAgent(log *zap.Logger, c *Config) (*sdkmetric.MeterProvider, error) { + return startAgent(log, c) +} + +func createPromExporter() (*prometheus.Exporter, error) { + prometheusExporter, err := prometheus.New() + if err != nil { + return nil, err + } + return prometheusExporter, nil +} + +func createHttpExporter(c *Config) (sdkmetric.Exporter, error) { + u, err := url.Parse(c.Endpoint) + if err != nil { + return nil, fmt.Errorf("invalid OpenTelemetry endpoint: %w", err) + } + + opts := []otlpmetrichttp.Option{ + // Includes host and port + otlpmetrichttp.WithEndpoint(u.Host), + } + + if u.Scheme != "https" { + opts = append(opts, otlpmetrichttp.WithInsecure()) + } + + if len(c.OtlpHeaders) > 0 { + opts = append(opts, otlpmetrichttp.WithHeaders(c.OtlpHeaders)) + } + if len(c.OtlpHttpPath) > 0 { + opts = append(opts, otlpmetrichttp.WithURLPath(c.OtlpHttpPath)) + } + + return otlpmetrichttp.New( + context.Background(), + opts..., + ) +} + +func startAgent(log *zap.Logger, c *Config) (*sdkmetric.MeterProvider, error) { + opts := []sdkmetric.Option{ + // Record information about this application in a Resource. + sdkmetric.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String(c.Name))), + } + + if c.Enabled && len(c.Endpoint) > 0 { + exp, err := createHttpExporter(c) + if err != nil { + log.Error("create exporter error", zap.Error(err)) + return nil, err + } + + opts = append(opts, + sdkmetric.WithReader( + sdkmetric.NewPeriodicReader(exp, + sdkmetric.WithTimeout(30*time.Second), + sdkmetric.WithInterval(30*time.Second), + ), + ), + ) + + log.Info("Metric Exporter agent started", zap.String("url", c.Endpoint+c.OtlpHttpPath)) + } + + if c.Prometheus.Enabled { + promExp, err := createPromExporter() + if err != nil { + return nil, err + } + + opts = append(opts, sdkmetric.WithReader(promExp)) + } + + mp = sdkmetric.NewMeterProvider(opts...) + // Set the global MeterProvider to the SDK metric provider. + otel.SetMeterProvider(mp) + + return mp, nil +} diff --git a/router/pkg/metric/config.go b/router/pkg/metric/config.go new file mode 100644 index 0000000000..aa23da5dd6 --- /dev/null +++ b/router/pkg/metric/config.go @@ -0,0 +1,44 @@ +package metric + +// ServerName Default resource name. +const ServerName = "cosmo-router" + +type Prometheus struct { + Enabled bool + ListenAddr string + Path string +} + +// Config represents the configuration for the agent. +type Config struct { + Enabled bool + // Name represents the service name for tracing. The default value is wundergraph-cosmo-router. + Name string + Endpoint string + // OtlpHeaders represents the headers for HTTP transport. + // For example: + // Authorization: 'Bearer ' + OtlpHeaders map[string]string + // OtlpHttpPath represents the path for OTLP HTTP transport. + // For example + // /v1/metrics + OtlpHttpPath string + + Prometheus Prometheus +} + +// DefaultConfig returns the default config. +func DefaultConfig() *Config { + return &Config{ + Enabled: true, + Name: ServerName, + Endpoint: "http://localhost:4318", + OtlpHeaders: map[string]string{}, + OtlpHttpPath: "/v1/metrics", + Prometheus: Prometheus{ + Enabled: true, + ListenAddr: "0.0.0.0:9090", + Path: "/metrics", + }, + } +} diff --git a/router/pkg/metric/handler.go b/router/pkg/metric/handler.go new file mode 100644 index 0000000000..b62ac23428 --- /dev/null +++ b/router/pkg/metric/handler.go @@ -0,0 +1,143 @@ +package metric + +import ( + "fmt" + "github.com/go-chi/chi/middleware" + "github.com/wundergraph/cosmo/router/pkg/contextx" + "github.com/wundergraph/cosmo/router/pkg/otel" + "go.opentelemetry.io/otel/attribute" + otelmetric "go.opentelemetry.io/otel/metric" + "go.opentelemetry.io/otel/sdk/metric" + semconv "go.opentelemetry.io/otel/semconv/v1.20.0" + "net/http" + "time" +) + +// Server HTTP metrics. +const ( + RequestCount = "router.http.requests" // Incoming request count total + ServerLatency = "router.http.request.duration_milliseconds" // Incoming end to end duration, milliseconds + RequestContentLength = "router.http.request.content_length" // Incoming request bytes total + ResponseContentLength = "router.http.response.content_length" // Outgoing response bytes total + InFlightRequests = "router.http.requests.in_flight.count" // Number of requests in flight +) + +type Handler struct { + meterProvider *metric.MeterProvider + + counters map[string]otelmetric.Int64Counter + valueRecorders map[string]otelmetric.Float64Histogram + updownCounters map[string]otelmetric.Float64UpDownCounter + + baseFields []attribute.KeyValue +} + +func NewMetricHandler(meterProvider *metric.MeterProvider, baseFields ...attribute.KeyValue) (*Handler, error) { + h := &Handler{ + meterProvider: meterProvider, + baseFields: baseFields, + } + + if err := h.createMeasures(); err != nil { + return nil, err + } + + return h, nil +} + +func (h *Handler) createMeasures() error { + h.counters = make(map[string]otelmetric.Int64Counter) + h.valueRecorders = make(map[string]otelmetric.Float64Histogram) + h.updownCounters = make(map[string]otelmetric.Float64UpDownCounter) + + routerMeter := h.meterProvider.Meter("cosmo.router") + requestCounter, err := routerMeter.Int64Counter( + RequestCount, + otelmetric.WithDescription("Total number of requests"), + ) + if err != nil { + return fmt.Errorf("failed to create request counter: %w", err) + } + h.counters[RequestCount] = requestCounter + + serverLatencyMeasure, err := routerMeter.Float64Histogram( + ServerLatency, + otelmetric.WithDescription("Server latency in milliseconds"), + ) + if err != nil { + return fmt.Errorf("failed to create server latency measure: %w", err) + } + h.valueRecorders[ServerLatency] = serverLatencyMeasure + + requestContentLengthCounter, err := routerMeter.Int64Counter( + RequestContentLength, + otelmetric.WithDescription("Total number of request bytes"), + ) + if err != nil { + return fmt.Errorf("failed to create request content length counter: %w", err) + } + h.counters[RequestContentLength] = requestContentLengthCounter + + responseContentLengthCounter, err := routerMeter.Int64Counter( + ResponseContentLength, + otelmetric.WithDescription("Total number of response bytes"), + ) + if err != nil { + return fmt.Errorf("failed to create response content length counter: %w", err) + } + + h.counters[ResponseContentLength] = responseContentLengthCounter + + inFlightRequestsGauge, err := routerMeter.Float64UpDownCounter( + InFlightRequests, + otelmetric.WithDescription("Number of requests in flight"), + ) + if err != nil { + return fmt.Errorf("failed to create in flight requests gauge: %w", err) + } + h.updownCounters[InFlightRequests] = inFlightRequestsGauge + + return nil +} + +func (h *Handler) Handler(handler http.Handler) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + requestStartTime := time.Now() + + h.updownCounters[InFlightRequests].Add(r.Context(), 1) + defer h.updownCounters[InFlightRequests].Add(r.Context(), -1) + + ww := middleware.NewWrapResponseWriter(w, r.ProtoMajor) + + // Process request + handler.ServeHTTP(ww, r) + + ctx := r.Context() + + statusCode := ww.Status() + + var baseKeys []attribute.KeyValue + + baseKeys = append(baseKeys, h.baseFields...) + + opCtx := contextx.GetOperationContext(ctx) + if opCtx != nil { + baseKeys = append(baseKeys, otel.WgOperationName.String(opCtx.Name)) + baseKeys = append(baseKeys, otel.WgOperationType.String(opCtx.Type)) + baseKeys = append(baseKeys, semconv.HTTPStatusCode(statusCode)) + } + + baseAttributes := otelmetric.WithAttributes(baseKeys...) + + // Use floating point division here for higher precision (instead of Millisecond method). + elapsedTime := float64(time.Since(requestStartTime)) / float64(time.Millisecond) + h.valueRecorders[ServerLatency].Record(ctx, elapsedTime, baseAttributes) + + if r.ContentLength > 0 { + h.counters[RequestContentLength].Add(ctx, r.ContentLength, baseAttributes) + } + h.counters[ResponseContentLength].Add(ctx, int64(ww.BytesWritten()), baseAttributes) + + h.counters[RequestCount].Add(ctx, 1, baseAttributes) + } +} diff --git a/router/pkg/otel/attributes.go b/router/pkg/otel/attributes.go new file mode 100644 index 0000000000..242c002705 --- /dev/null +++ b/router/pkg/otel/attributes.go @@ -0,0 +1,20 @@ +package otel + +import "go.opentelemetry.io/otel/attribute" + +const ( + WgOperationName = attribute.Key("wg.operation.name") + WgOperationType = attribute.Key("wg.operation.type") + WgOperationContent = attribute.Key("wg.operation.content") + WgOperationHash = attribute.Key("wg.operation.hash") + WgComponentName = attribute.Key("wg.component.name") + WgClientName = attribute.Key("wg.client.name") + WgClientVersion = attribute.Key("wg.client.version") + WgRouterGraphName = attribute.Key("wg.router.graph.name") + WgRouterConfigVersion = attribute.Key("wg.router.config.version") +) + +var ( + RouterServerAttribute = WgComponentName.String("router-server") + EngineTransportAttribute = WgComponentName.String("engine-transport") +) diff --git a/router/pkg/graphql/builder.go b/router/pkg/planner/planner.go similarity index 82% rename from router/pkg/graphql/builder.go rename to router/pkg/planner/planner.go index 59b9794048..b2834a1dd9 100644 --- a/router/pkg/graphql/builder.go +++ b/router/pkg/planner/planner.go @@ -1,12 +1,12 @@ -package graphql +package planner import ( "context" "fmt" - "github.com/dgraph-io/ristretto" nodev1 "github.com/wundergraph/cosmo/router/gen/proto/wg/cosmo/node/v1" "github.com/wundergraph/cosmo/router/pkg/factoryresolver" "github.com/wundergraph/cosmo/router/pkg/pool" + "github.com/wundergraph/graphql-go-tools/v2/pkg/ast" "github.com/wundergraph/graphql-go-tools/v2/pkg/astparser" "github.com/wundergraph/graphql-go-tools/v2/pkg/asttransform" "github.com/wundergraph/graphql-go-tools/v2/pkg/engine/datasource/introspection_datasource" @@ -16,18 +16,17 @@ import ( "net/http" ) -type BuilderOption func(b *HandlerBuilder) +type BuilderOption func(b *Planner) -type HandlerBuilder struct { +type Planner struct { introspection bool baseURL string transport *http.Transport logger *zap.Logger - planCache *ristretto.Cache } -func NewGraphQLHandlerBuilder(opts ...BuilderOption) *HandlerBuilder { - b := &HandlerBuilder{} +func NewPlanner(opts ...BuilderOption) *Planner { + b := &Planner{} for _, opt := range opts { opt(b) } @@ -39,7 +38,15 @@ func NewGraphQLHandlerBuilder(opts ...BuilderOption) *HandlerBuilder { return b } -func (b *HandlerBuilder) Build(ctx context.Context, routerConfig *nodev1.RouterConfig) (*GraphQLHandler, error) { +type Plan struct { + PlanConfig plan.Configuration + Definition *ast.Document + Resolver *resolve.Resolver + Pool *pool.Pool + RenameTypeNames []resolve.RenameTypeName +} + +func (b *Planner) Build(ctx context.Context, routerConfig *nodev1.RouterConfig) (*Plan, error) { planConfig, err := b.buildPlannerConfiguration(routerConfig) if err != nil { return nil, fmt.Errorf("failed to build planner configuration: %w", err) @@ -92,21 +99,16 @@ func (b *HandlerBuilder) Build(ctx context.Context, routerConfig *nodev1.RouterC } } - // finally, we can create the GraphQL Handler and mount it on the router - graphQLHandler := NewGraphQLHandler(HandlerOptions{ + return &Plan{ PlanConfig: *planConfig, Definition: &definition, Resolver: resolver, RenameTypeNames: renameTypeNames, Pool: pool.New(), - Cache: b.planCache, - Log: b.logger, - }) - - return graphQLHandler, nil + }, nil } -func (b *HandlerBuilder) buildPlannerConfiguration(routerCfg *nodev1.RouterConfig) (*plan.Configuration, error) { +func (b *Planner) buildPlannerConfiguration(routerCfg *nodev1.RouterConfig) (*plan.Configuration, error) { // this loader is used to take the engine config and create a plan config // the plan config is what the engine uses to turn a GraphQL Request into an execution plan // the plan config is stateful as it carries connection pools and other things @@ -133,31 +135,25 @@ func (b *HandlerBuilder) buildPlannerConfiguration(routerCfg *nodev1.RouterConfi } func WithIntrospection() BuilderOption { - return func(b *HandlerBuilder) { + return func(b *Planner) { b.introspection = true } } func WithBaseURL(baseURL string) BuilderOption { - return func(b *HandlerBuilder) { + return func(b *Planner) { b.baseURL = baseURL } } func WithTransport(transport *http.Transport) BuilderOption { - return func(b *HandlerBuilder) { + return func(b *Planner) { b.transport = transport } } func WithLogger(logger *zap.Logger) BuilderOption { - return func(b *HandlerBuilder) { + return func(b *Planner) { b.logger = logger } } - -func WithPlanCache(planCache *ristretto.Cache) BuilderOption { - return func(b *HandlerBuilder) { - b.planCache = planCache - } -} diff --git a/router/pkg/stringsx/string.go b/router/pkg/stringsx/string.go new file mode 100644 index 0000000000..b8282f6393 --- /dev/null +++ b/router/pkg/stringsx/string.go @@ -0,0 +1,20 @@ +package stringsx + +func contains(s []string, e string) bool { + for _, a := range s { + if a == e { + return true + } + } + return false +} + +func RemoveDuplicates(strList []string) []string { + var list []string + for _, item := range strList { + if contains(list, item) == false { + list = append(list, item) + } + } + return list +} diff --git a/router/pkg/test/handler.go b/router/pkg/test/handler.go new file mode 100644 index 0000000000..671648e7c0 --- /dev/null +++ b/router/pkg/test/handler.go @@ -0,0 +1,11 @@ +package test + +import "net/http" + +func NewRequest(method, url string) *http.Request { + req, err := http.NewRequest(method, url, nil) + if err != nil { + panic(err) + } + return req +} diff --git a/router/pkg/trace/agent.go b/router/pkg/trace/agent.go index a3b4a411f2..c080b9cf95 100644 --- a/router/pkg/trace/agent.go +++ b/router/pkg/trace/agent.go @@ -74,7 +74,7 @@ func startAgent(log *zap.Logger, c *Config) (*sdktrace.TracerProvider, error) { sdktrace.WithResource(resource.NewSchemaless(semconv.ServiceNameKey.String(c.Name))), } - if len(c.Endpoint) > 0 { + if c.Enabled && len(c.Endpoint) > 0 { exp, err := createExporter(c) if err != nil { log.Error("create exporter error", zap.Error(err)) @@ -89,6 +89,8 @@ func startAgent(log *zap.Logger, c *Config) (*sdktrace.TracerProvider, error) { sdktrace.WithMaxQueueSize(2048), ), ) + + log.Info("Trace Exporter agent started", zap.String("url", c.Endpoint+c.OtlpHttpPath)) } tp := sdktrace.NewTracerProvider(opts...) diff --git a/router/pkg/trace/attributes.go b/router/pkg/trace/attributes.go deleted file mode 100644 index 1121337ab5..0000000000 --- a/router/pkg/trace/attributes.go +++ /dev/null @@ -1,20 +0,0 @@ -package trace - -import "go.opentelemetry.io/otel/attribute" - -const ( - WgOperationName = attribute.Key("wg.operation.name") - WgOperationType = attribute.Key("wg.operation.type") - WgOperationContent = attribute.Key("wg.operation.content") - WgOperationHash = attribute.Key("wg.operation.hash") - WgComponentName = attribute.Key("wg.component.name") - WgClientName = attribute.Key("wg.client.name") - WgClientVersion = attribute.Key("wg.client.version") - WgRouterGraphName = attribute.Key("wg.router.graph_name") - WgRouterVersion = attribute.Key("wg.router.version") -) - -var ( - RouterServerAttribute = WgComponentName.String("router-server") - EngineTransportAttribute = WgComponentName.String("engine-transport") -) diff --git a/router/pkg/trace/config.go b/router/pkg/trace/config.go index 7854944da8..8a6136185b 100644 --- a/router/pkg/trace/config.go +++ b/router/pkg/trace/config.go @@ -2,11 +2,12 @@ package trace import "time" -// TraceName represents the tracing name. -const TraceName = "wundergraph-cosmo-router" +// ServerName Default resource name. +const ServerName = "cosmo-router" -// A Config is an opentelemetry config. +// Config represents the configuration for the agent. type Config struct { + Enabled bool // Name represents the service name for tracing. The default value is wundergraph-cosmo-router. Name string Endpoint string @@ -27,7 +28,7 @@ type Config struct { // DefaultConfig returns the default config. func DefaultConfig() *Config { return &Config{ - Name: TraceName, + Name: ServerName, Endpoint: "http://localhost:4318", Sampler: 1, Batcher: KindOtlpHttp, diff --git a/router/pkg/trace/transport.go b/router/pkg/trace/transport.go index e20a33f003..e80f2f4a6d 100644 --- a/router/pkg/trace/transport.go +++ b/router/pkg/trace/transport.go @@ -2,6 +2,7 @@ package trace import ( "github.com/wundergraph/cosmo/router/pkg/contextx" + "github.com/wundergraph/cosmo/router/pkg/otel" "go.opentelemetry.io/otel/trace" "net/http" @@ -30,10 +31,10 @@ type transport struct { func (t *transport) RoundTrip(r *http.Request) (*http.Response, error) { span := trace.SpanFromContext(r.Context()) - operation := contextx.GetGraphQLOperationFromContext(r.Context()) + operation := contextx.GetOperationContext(r.Context()) if operation != nil { - span.SetAttributes(WgOperationName.String(operation.Name)) - span.SetAttributes(WgOperationType.String(operation.Type)) + span.SetAttributes(otel.WgOperationName.String(operation.Name)) + span.SetAttributes(otel.WgOperationType.String(operation.Type)) } res, err := t.rt.RoundTrip(r) diff --git a/router/pkg/trace/transport_test.go b/router/pkg/trace/transport_test.go index 07d49b7dc4..17d8fba28f 100644 --- a/router/pkg/trace/transport_test.go +++ b/router/pkg/trace/transport_test.go @@ -3,6 +3,7 @@ package trace import ( "bytes" "context" + "github.com/wundergraph/cosmo/router/pkg/otel" "io" "net/http" "net/http/httptest" @@ -38,7 +39,7 @@ func TestTransport(t *testing.T) { t.Fatal(err) } - tr := NewTransport(http.DefaultTransport, otelhttp.WithSpanOptions(trace.WithAttributes(WgComponentName.String("test")))) + tr := NewTransport(http.DefaultTransport, otelhttp.WithSpanOptions(trace.WithAttributes(otel.WgComponentName.String("test")))) c := http.Client{Transport: tr} res, err := c.Do(r) @@ -66,7 +67,7 @@ func TestTransport(t *testing.T) { assert.Contains(t, sn[0].Attributes(), semconv.HTTPFlavorKey.String("1.1")) assert.Contains(t, sn[0].Attributes(), semconv.HTTPURL(tsURL)) assert.Contains(t, sn[0].Attributes(), semconv.HTTPStatusCode(200)) - assert.Contains(t, sn[0].Attributes(), WgComponentName.String("test")) + assert.Contains(t, sn[0].Attributes(), otel.WgComponentName.String("test")) }) t.Run("set span status to error", func(t *testing.T) { @@ -88,7 +89,7 @@ func TestTransport(t *testing.T) { t.Fatal(err) } - tr := NewTransport(http.DefaultTransport, otelhttp.WithSpanOptions(trace.WithAttributes(WgComponentName.String("test")))) + tr := NewTransport(http.DefaultTransport, otelhttp.WithSpanOptions(trace.WithAttributes(otel.WgComponentName.String("test")))) c := http.Client{Transport: tr} res, err := c.Do(r) @@ -116,6 +117,6 @@ func TestTransport(t *testing.T) { assert.Contains(t, sn[0].Attributes(), semconv.HTTPFlavorKey.String("1.1")) assert.Contains(t, sn[0].Attributes(), semconv.HTTPURL(tsURL)) assert.Contains(t, sn[0].Attributes(), semconv.HTTPStatusCode(http.StatusInternalServerError)) - assert.Contains(t, sn[0].Attributes(), WgComponentName.String("test")) + assert.Contains(t, sn[0].Attributes(), otel.WgComponentName.String("test")) }) } diff --git a/router/pkg/trace/utils.go b/router/pkg/trace/utils.go index ce397ca6d3..b4b94720b9 100644 --- a/router/pkg/trace/utils.go +++ b/router/pkg/trace/utils.go @@ -3,40 +3,56 @@ package trace import ( "context" "fmt" - "github.com/gin-gonic/gin" + "github.com/wundergraph/cosmo/router/pkg/contextx" "net/http" "go.opentelemetry.io/otel" "go.opentelemetry.io/otel/trace" ) +// unnamed is the default operation name used when no operation name is provided +const unnamed = "unnamed" + // TracerFromContext returns a tracer in ctx, otherwise returns a global tracer. func TracerFromContext(ctx context.Context) (tracer trace.Tracer) { if span := trace.SpanFromContext(ctx); span.SpanContext().IsValid() { - tracer = span.TracerProvider().Tracer(TraceName) + tracer = span.TracerProvider().Tracer(ServerName) } else { - tracer = otel.Tracer(TraceName) + tracer = otel.Tracer(ServerName) } return } // SpanNameFormatter formats the span name based on the http request -func SpanNameFormatter(_operation string, r *http.Request) string { +// Note: High cardinality should be avoided because it can be expensive +func SpanNameFormatter(operation string, r *http.Request) string { + if operation != "" { + return operation + } + + opCtx := contextx.GetOperationContext(r.Context()) + if opCtx != nil { + if opCtx.Name != "" { + return fmt.Sprintf("%s %s", r.Method, opCtx.Name) + } + return fmt.Sprintf("%s %s", r.Method, unnamed) + } + return fmt.Sprintf("%s %s", r.Method, r.URL.Path) } -func RequestFilter(req *http.Request) bool { - if req.URL.Path == "/health" || req.URL.Path == "/favicon.ico" || req.Method == "OPTIONS" { +func RequestFilter(r *http.Request) bool { + if r.URL.Path == "/health" || r.URL.Path == "/favicon.ico" || r.Method == "OPTIONS" { return false } return true } -func GetClientInfo(c *gin.Context, primaryHeader, fallbackHeader, defaultValue string) string { - value := c.GetHeader(primaryHeader) +func GetClientInfo(h http.Header, primaryHeader, fallbackHeader, defaultValue string) string { + value := h.Get(primaryHeader) if value == "" { - value = c.GetHeader(fallbackHeader) + value = h.Get(fallbackHeader) if value == "" { value = defaultValue } diff --git a/router/pkg/trace/utils_test.go b/router/pkg/trace/utils_test.go index 0f10b537e3..2991067d76 100644 --- a/router/pkg/trace/utils_test.go +++ b/router/pkg/trace/utils_test.go @@ -39,7 +39,7 @@ func TestTracerFromContext(t *testing.T) { } tp = sdktrace.NewTracerProvider(opts...) otel.SetTracerProvider(tp) - ctx, span := tp.Tracer(TraceName).Start(context.Background(), "a") + ctx, span := tp.Tracer(ServerName).Start(context.Background(), "a") defer span.End() traceFn(ctx, true) diff --git a/router/pkg/trace/wrap_handler.go b/router/pkg/trace/wrap_handler.go index 2889b55b67..feafeebb39 100644 --- a/router/pkg/trace/wrap_handler.go +++ b/router/pkg/trace/wrap_handler.go @@ -16,7 +16,6 @@ import ( func WrapHandler(wrappedHandler http.Handler, componentName attribute.KeyValue, opts ...otelhttp.Option) http.Handler { // Don't trace health check requests, favicon browser requests or OPTIONS request opts = append(opts, otelhttp.WithFilter(RequestFilter)) - // TODO: Use router path as span name, high cardinality should be avoided opts = append(opts, otelhttp.WithSpanNameFormatter(SpanNameFormatter)) setSpanStatusHandler := http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { @@ -26,6 +25,7 @@ func WrapHandler(wrappedHandler http.Handler, componentName attribute.KeyValue, // Add request target as attribute, so we can filter by path and query span.SetAttributes(semconv17.HTTPTarget(req.RequestURI)) + // Add the host request header to the span span.SetAttributes(semconv12.HTTPHostKey.String(req.Host)) diff --git a/router/pkg/trace/wrap_handler_test.go b/router/pkg/trace/wrap_handler_test.go index 223cb5be4b..0c92960038 100644 --- a/router/pkg/trace/wrap_handler_test.go +++ b/router/pkg/trace/wrap_handler_test.go @@ -1,11 +1,12 @@ package trace import ( + "github.com/go-chi/chi" + "github.com/wundergraph/cosmo/router/pkg/otel" "net/http" "net/http/httptest" "testing" - "github.com/gorilla/mux" "github.com/stretchr/testify/assert" "go.opentelemetry.io/otel/codes" sdktrace "go.opentelemetry.io/otel/sdk/trace" @@ -21,13 +22,13 @@ func TestWrapHttpHandler(t *testing.T) { t.Run("create a span for every request", func(t *testing.T) { exporter := tracetest.NewInMemoryExporter(t) - router := mux.NewRouter() + router := chi.NewRouter() router.HandleFunc("/test", func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusOK) }) - h := WrapHandler(router, WgComponentName.String("test")) + h := WrapHandler(router, otel.WgComponentName.String("test")) req := httptest.NewRequest("GET", "/test?a=b", nil) w := httptest.NewRecorder() @@ -47,7 +48,7 @@ func TestWrapHttpHandler(t *testing.T) { assert.Contains(t, sn[0].Attributes(), semconv17.HTTPFlavorKey.String("1.1")) assert.Contains(t, sn[0].Attributes(), semconv17.HTTPTarget("/test?a=b")) assert.Contains(t, sn[0].Attributes(), semconv17.HTTPStatusCode(200)) - assert.Contains(t, sn[0].Attributes(), WgComponentName.String("test")) + assert.Contains(t, sn[0].Attributes(), otel.WgComponentName.String("test")) assert.Contains(t, sn[0].Attributes(), semconv12.HTTPHostKey.String("example.com")) }) @@ -68,7 +69,7 @@ func TestWrapHttpHandler(t *testing.T) { exporter := tracetest.NewInMemoryExporter(t) for _, test := range statusCodeTests { - router := mux.NewRouter() + router := chi.NewRouter() statusCode := test.statusCode @@ -76,7 +77,7 @@ func TestWrapHttpHandler(t *testing.T) { w.WriteHeader(statusCode) }) - h := WrapHandler(router, WgComponentName.String("test")) + h := WrapHandler(router, otel.WgComponentName.String("test")) req := httptest.NewRequest("GET", "/test?a=b", nil) w := httptest.NewRecorder() @@ -97,7 +98,7 @@ func TestWrapHttpHandler(t *testing.T) { assert.Contains(t, sn[0].Attributes(), semconv17.HTTPFlavorKey.String("1.1")) assert.Contains(t, sn[0].Attributes(), semconv17.HTTPTarget("/test?a=b")) assert.Contains(t, sn[0].Attributes(), semconv17.HTTPStatusCode(statusCode)) - assert.Contains(t, sn[0].Attributes(), WgComponentName.String("test")) + assert.Contains(t, sn[0].Attributes(), otel.WgComponentName.String("test")) exporter.Reset() }