Skip to content

Commit

Permalink
feat(core): Add new Instruments API (#3793)
Browse files Browse the repository at this point in the history
* feat(core): Add new Tracer API

* changeset

* fix root exports

* use latest envelope snapshot

* rename to instruments and use instruments utils

* chore(dependencies): updated changesets for modified dependencies

* refactor request parser to use promise utils

* add tests

* chore(dependencies): updated changesets for modified dependencies

* fix `requestParse` and `resultProcess` instruments can be async

* remove request from operation instruments payload

* add documentation

* use @envelop/instruments released version

* chore(dependencies): updated changesets for modified dependencies

* changeset

* re-export instruments utils

* update whatwg-node alpha

* More refactor

* chore(dependencies): updated changesets for modified dependencies

* Lets go

* Bump versions

* Bump versions

* Bump versions

* Deduped lockfile

* chore(dependencies): updated changesets for modified dependencies

* Fix tests

* Fix

* Deduped lockfile

* chore(dependencies): updated changesets for modified dependencies

---------

Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Arda TANRIKULU <ardatanrikulu@gmail.com>
  • Loading branch information
3 people authored Mar 5, 2025
1 parent 5a677d6 commit 63b78d5
Show file tree
Hide file tree
Showing 36 changed files with 1,045 additions and 727 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-yoga/nestjs-federation": patch
---
dependencies updates:
- Updated dependency [`@envelop/apollo-federation@^6.1.1` ↗︎](https://www.npmjs.com/package/@envelop/apollo-federation/v/6.1.1) (from `^6.0.0`, in `dependencies`)
- Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.0`, in `dependencies`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-apollo-inline-trace": patch
---
dependencies updates:
- Updated dependency [`@envelop/on-resolve@^5.1.1` ↗︎](https://www.npmjs.com/package/@envelop/on-resolve/v/5.1.1) (from `^5.0.0`, in `dependencies`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-disable-introspection": patch
---
dependencies updates:
- Added dependency [`@whatwg-node/promise-helpers@^1.2.4` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.2.4) (to `dependencies`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@graphql-yoga/plugin-prometheus": patch
---
dependencies updates:
- Updated dependency [`@envelop/prometheus@^12.1.1` ↗︎](https://www.npmjs.com/package/@envelop/prometheus/v/12.1.1) (from `^12.0.0`, in `dependencies`)
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"@graphql-yoga/plugin-response-cache": patch
---
dependencies updates:
- Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.2`, in `dependencies`)
- Updated dependency [`@envelop/response-cache@^7.1.1` ↗︎](https://www.npmjs.com/package/@envelop/response-cache/v/7.1.1) (from `^7.0.0`, in `dependencies`)
90 changes: 90 additions & 0 deletions .changeset/afraid-jars-burn.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
---
'graphql-yoga': minor
---

Add new Instruments API

Introduction of a new API allowing to instrument the graphql pipeline.

This new API differs from already existing Hooks by not having access to input/output of phases. The
goal of `Instruments` is to run allow running code before, after or around the **whole process of a
phase**, including plugins hooks executions.

The main use case of this new API is observability (monitoring, tracing, etc...).

### Basic usage

```ts
import { createYoga } from 'graphql-yoga'
import Sentry from '@sentry/node'
import schema from './schema'

const server = createYoga({
schema,
plugins: [
{
instruments: {
request: ({ request }, wrapped) =>
Sentry.startSpan({ name: 'Graphql Operation' }, async () => {
try {
await wrapped()
} catch (err) {
Sentry.captureException(err)
}
})
}
}
]
})
```

### Multiple instruments plugins

It is possible to have multiple instruments plugins (Prometheus and Sentry for example), they will
be automatically composed by envelop in the same order than the plugin array (first is outermost,
last is inner most).

```ts
import { createYoga } from 'graphql-yoga'
import schema from './schema'

const server = createYoga({
schema,
plugins: [useSentry(), useOpentelemetry()]
})
```

```mermaid
sequenceDiagram
Sentry->>Opentelemetry: ;
Opentelemetry->>Server Adapter: ;
Server Adapter->>Opentelemetry: ;
Opentelemetry->>Sentry: ;
```

### Custom instruments ordering

If the default composition ordering doesn't suite your need, you can manually compose instruments.
This allows to have a different execution order of hooks and instruments.

```ts
import { composeInstruments, createYoga } from 'graphql-yoga'
import schema from './schema'

const { instruments: sentryInstruments, ...sentryPlugin } = useSentry()
const { instruments: otelInstruments, ...otelPlugin } = useOpentelemetry()
const instruments = composeInstruments([otelInstruments, sentryInstruments])

const server = createYoga({
schema,
plugins: [{ instruments }, useSentry(), useOpentelemetry()]
})
```

```mermaid
sequenceDiagram
Opentelemetry->>Sentry: ;
Sentry->>Server Adapter: ;
Server Adapter->>Sentry: ;
Sentry->>Opentelemetry: ;
```
7 changes: 7 additions & 0 deletions .changeset/graphql-yoga-3793-dependencies.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
"graphql-yoga": patch
---
dependencies updates:
- Updated dependency [`@envelop/core@^5.2.1` ↗︎](https://www.npmjs.com/package/@envelop/core/v/5.2.1) (from `^5.0.2`, in `dependencies`)
- Added dependency [`@envelop/instruments@^1.0.0` ↗︎](https://www.npmjs.com/package/@envelop/instruments/v/1.0.0) (to `dependencies`)
- Added dependency [`@whatwg-node/promise-helpers@^1.2.4` ↗︎](https://www.npmjs.com/package/@whatwg-node/promise-helpers/v/1.2.4) (to `dependencies`)
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,4 @@ packages/graphql-yoga/src/landing-page-html.ts
packages/graphql-yoga/src/graphiql-html.ts
run/
website/public/_pagefind/
.helix/languages.toml
4 changes: 3 additions & 1 deletion examples/error-handling/src/yoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@ export const yoga = createYoga({
Query: {
greeting: async () => {
// This service does not exist
const greeting = await fetch('http://localhost:9999/greeting').then(res => res.text());
const greeting = await fetch('http://0.0.0.0:9999/greeting', {
signal: AbortSignal.timeout(1000),
}).then(res => res.text());

return greeting;
},
Expand Down
2 changes: 1 addition & 1 deletion examples/sveltekit/__integration-tests__/sveltekit.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ describe('SvelteKit integration', () => {
});

afterAll(async () => {
await browser.close();
await browser?.close();
sveltekitProcess.kill();
});

Expand Down
19 changes: 17 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,24 @@
},
"overrides": {
"graphql": "16.10.0",
"@envelop/core": "5.2.1",
"@changesets/assemble-release-plan": "5.2.3",
"@types/react": "19.0.10"
}
},
"onlyBuiltDependencies": [
"@nestjs/core",
"@playwright/browser-chromium",
"@prisma/client",
"@prisma/engines",
"@pulumi/docker",
"@pulumi/docker-build",
"@sveltejs/kit",
"@swc/core",
"egg-bin",
"esbuild",
"netlify-cli",
"prisma",
"svelte-preprocess",
"workerd"
]
}
}
113 changes: 113 additions & 0 deletions packages/graphql-yoga/__tests__/instruments.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
import { createSchema, createYoga, Plugin } from '../src';

describe('instruments', () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
type Subscription {
greetings: String!
}
`,
resolvers: {
Query: {
hello: () => 'world',
},
Subscription: {
greetings: {
async *subscribe() {
yield { greetings: 'Hi' };
},
},
},
},
});

it('should wrap all the phases with the default composition order', async () => {
const result: string[] = [];
const make = (name: string): Plugin => ({
instruments: {
context: (_, w) => {
result.push(`pre-context-${name}`);
w();
result.push(`post-context-${name}`);
},
execute: async (_, w) => {
result.push(`pre-execute-${name}`);
await w();
result.push(`post-execute-${name}`);
},
init: (_, w) => {
result.push(`pre-init-${name}`);
w();
result.push(`post-init-${name}`);
},
parse: (_, w) => {
result.push(`pre-parse-${name}`);
w();
result.push(`post-parse-${name}`);
},
request: async (_, w) => {
result.push(`pre-request-${name}`);
await w();
result.push(`post-request-${name}`);
},
subscribe: async (_, w) => {
result.push(`pre-subscribe-${name}`);
await w();
result.push('post-subscribe-${name}');
},
validate: (_, w) => {
result.push(`pre-validate-${name}`);
w();
result.push(`post-validate-${name}`);
},
operation: async (_, w) => {
result.push(`pre-operation-${name}`);
await w();
result.push(`post-operation-${name}`);
},
requestParse: async (_, w) => {
result.push(`pre-request-parse-${name}`);
await w();
result.push(`post-request-parse-${name}`);
},
resultProcess: async (_, w) => {
result.push(`pre-result-process-${name}`);
await w();
result.push(`post-result-process-${name}`);
},
},
});

const yoga = createYoga({
schema,
plugins: [make('1'), make('2'), make('3')],
});

await yoga.fetch('http://yoga/graphql?query={hello}');

const withPrefix = (prefix: string) => [`${prefix}-1`, `${prefix}-2`, `${prefix}-3`];
expect(result).toEqual([
...withPrefix('pre-request'),
...withPrefix('pre-request-parse'),
...withPrefix('post-request-parse').reverse(),
...withPrefix('pre-operation'),
...withPrefix('pre-init'),
...withPrefix('post-init').reverse(),
...withPrefix('pre-parse'),
...withPrefix('post-parse').reverse(),
...withPrefix('pre-validate'),
...withPrefix('post-validate').reverse(),
...withPrefix('pre-context'),
...withPrefix('post-context').reverse(),
...withPrefix('pre-execute'),
...withPrefix('post-execute').reverse(),
...withPrefix('post-operation').reverse(),
...withPrefix('pre-result-process'),
...withPrefix('post-result-process').reverse(),
...withPrefix('post-request').reverse(),
]);
});
});
4 changes: 3 additions & 1 deletion packages/graphql-yoga/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,15 @@
"graphql": "^15.2.0 || ^16.0.0"
},
"dependencies": {
"@envelop/core": "^5.0.2",
"@envelop/core": "^5.2.1",
"@envelop/instruments": "^1.0.0",
"@graphql-tools/executor": "^1.4.0",
"@graphql-tools/schema": "^10.0.11",
"@graphql-tools/utils": "^10.6.2",
"@graphql-yoga/logger": "workspace:^",
"@graphql-yoga/subscription": "workspace:^",
"@whatwg-node/fetch": "^0.10.5",
"@whatwg-node/promise-helpers": "^1.2.4",
"@whatwg-node/server": "^0.10.0",
"dset": "^3.1.4",
"lru-cache": "^10.0.0",
Expand Down
5 changes: 3 additions & 2 deletions packages/graphql-yoga/src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
export * from '@graphql-yoga/logger';
export { type Plugin } from './plugins/types.js';
export { type GraphiQLOptions } from './plugins/use-graphiql.js';
export { renderGraphiQL, shouldRenderGraphiQL } from './plugins/use-graphiql.js';
export { useReadinessCheck } from './plugins/use-readiness-check.js';
Expand All @@ -9,7 +8,7 @@ export * from './server.js';
export * from './subscription.js';
export * from './types.js';
export { maskError } from './utils/mask-error.js';
export { type OnParamsEventPayload } from './plugins/types.js';
export { type OnParamsEventPayload, type Plugin, type Instruments } from './plugins/types.js';
export { _createLRUCache, createLRUCache } from './utils/create-lru-cache.js';
export { mergeSchemas } from '@graphql-tools/schema';
export {
Expand All @@ -36,6 +35,7 @@ export {
useLogger,
usePayloadFormatter,
} from '@envelop/core';
export { getInstrumentsAndPlugins, chain, composeInstruments } from '@envelop/instruments';
export { createGraphQLError, isPromise, mapMaybePromise } from '@graphql-tools/utils';
export { getSSEProcessor } from './plugins/result-processor/sse.js';
export { processRegularResult } from './plugins/result-processor/regular.js';
Expand All @@ -45,3 +45,4 @@ export {
type LandingPageRendererOpts,
} from './plugins/use-unhandled-route.js';
export { DisposableSymbols } from '@whatwg-node/server';
export * from '@envelop/instruments';
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ export function isPOSTFormUrlEncodedRequest(request: Request) {
);
}

export async function parsePOSTFormUrlEncodedRequest(request: Request): Promise<GraphQLParams> {
const requestBody = await request.text();
return parseURLSearchParams(requestBody);
export function parsePOSTFormUrlEncodedRequest(request: Request): Promise<GraphQLParams> {
return request.text().then(parseURLSearchParams);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,6 @@ export function isPOSTGraphQLStringRequest(request: Request) {
return request.method === 'POST' && isContentTypeMatch(request, 'application/graphql');
}

export async function parsePOSTGraphQLStringRequest(request: Request): Promise<GraphQLParams> {
const requestBody = await request.text();
return {
query: requestBody,
};
export function parsePOSTGraphQLStringRequest(request: Request): Promise<GraphQLParams> {
return request.text().then(query => ({ query }));
}
Loading

0 comments on commit 63b78d5

Please sign in to comment.