Skip to content

Commit

Permalink
fix(prom): graphql-ws integration
Browse files Browse the repository at this point in the history
  • Loading branch information
ardatan committed Jan 14, 2025
1 parent 6dc947e commit e8a7186
Show file tree
Hide file tree
Showing 5 changed files with 166 additions and 11 deletions.
6 changes: 6 additions & 0 deletions .changeset/tiny-hats-do.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
'@graphql-yoga/plugin-prometheus': patch
---

`request` is missing when GraphQL WS is used as expected, and as we don't need HTTP/Yoga specific
metrics, this should be skipped
133 changes: 133 additions & 0 deletions packages/plugins/prometheus/__integration-tests__/graphql-ws.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
import { createServer, type Server } from 'http';

Check failure on line 1 in packages/plugins/prometheus/__integration-tests__/graphql-ws.test.ts

View workflow job for this annotation

GitHub Actions / check

Prefer `node:http` over `http`
import { AddressInfo } from 'net';

Check failure on line 2 in packages/plugins/prometheus/__integration-tests__/graphql-ws.test.ts

View workflow job for this annotation

GitHub Actions / check

Prefer `node:net` over `net`
import { ExecutionResult } from 'graphql';

Check failure on line 3 in packages/plugins/prometheus/__integration-tests__/graphql-ws.test.ts

View workflow job for this annotation

GitHub Actions / check

'ExecutionResult' is defined but never used. Allowed unused vars must match /^_/u
import { Client, createClient } from 'graphql-ws';
import { useServer } from 'graphql-ws/lib/use/ws';
import { createSchema, createYoga } from 'graphql-yoga';
import { register as registry } from 'prom-client';
import WebSocket from 'ws';
import { usePrometheus } from '../src';

describe('GraphQL WS & Prometheus integration', () => {
const schema = createSchema({
typeDefs: /* GraphQL */ `
type Query {
hello: String!
}
`,
resolvers: {
Query: {
hello: () => 'Hello world!',
},
},
});
let client: Client | undefined;
let server: Server | undefined;
afterEach(async () => {
registry.clear();
if (client) {
await client.dispose();
}
if (server) {
await new Promise<void>((resolve, reject) => {
server?.close(err => {
if (err) return reject(err);
resolve();
});
});
}
});

it('should have default configs for the plugin metrics', async () => {
const yoga = createYoga({
schema,
plugins: [
usePrometheus({
registry,
}),
],
});
server = createServer(yoga);
await new Promise<void>((resolve, reject) => {
server?.listen(0, () => {
resolve();
});
server?.once('error', reject);
});
const wss = new WebSocket.WebSocketServer({
server,
path: yoga.graphqlEndpoint,
});

useServer(
{
// eslint-disable-next-line @typescript-eslint/no-explicit-any
execute: (args: any) => args.execute(args),
// eslint-disable-next-line @typescript-eslint/no-explicit-any
subscribe: (args: any) => args.subscribe(args),
onSubscribe: async (ctx, msg) => {
const { schema, execute, subscribe, contextFactory, parse, validate } = yoga.getEnveloped(
{
...ctx,
req: ctx.extra.request,
socket: ctx.extra.socket,
params: msg.payload,
},
);

const args = {
schema,
operationName: msg.payload.operationName,
document: parse(msg.payload.query),
variableValues: msg.payload.variables,
contextValue: await contextFactory(),
execute,
subscribe,
};

const errors = validate(args.schema, args.document);
if (errors.length) return errors;
return args;
},
},
wss,
);

const port = (server.address() as AddressInfo).port;

const client = createClient({
url: `ws://localhost:${port}${yoga.graphqlEndpoint}`,
webSocketImpl: WebSocket,
});

const iterable = client.iterate({
query: /* GraphQL */ `
query Test {
hello
}
`,
});

for await (const result of iterable) {
expect(result.data).toEqual({ hello: 'Hello world!' });
}

const metrics = await registry.metrics();

// enabled by default
expect(metrics).toContain('# TYPE graphql_envelop_phase_parse histogram');
expect(metrics).toContain('# TYPE graphql_envelop_phase_validate histogram');
expect(metrics).toContain('# TYPE graphql_envelop_phase_context histogram');
expect(metrics).toContain('# TYPE graphql_envelop_phase_execute histogram');
expect(metrics).toContain('# TYPE graphql_envelop_phase_subscribe histogram');
expect(metrics).toContain('# TYPE graphql_envelop_request_duration histogram');
expect(metrics).toContain('# TYPE graphql_envelop_request_time_summary summary');
expect(metrics).toContain('# TYPE graphql_envelop_error_result counter');
expect(metrics).toContain('# TYPE graphql_envelop_request counter');
expect(metrics).toContain('# TYPE graphql_envelop_deprecated_field counter');
expect(metrics).toContain('# TYPE graphql_envelop_schema_change counter');

// disabled by default
expect(metrics).not.toContain('graphql_envelop_execute_resolver');
});
});
5 changes: 4 additions & 1 deletion packages/plugins/prometheus/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -45,8 +45,11 @@
"@envelop/prometheus": "^11.1.0"
},
"devDependencies": {
"@types/ws": "^8.5.13",
"graphql-ws": "^5.16.2",
"graphql-yoga": "workspace:*",
"prom-client": "15.1.3"
"prom-client": "15.1.3",
"ws": "^8.18.0"
},
"publishConfig": {
"directory": "dist",
Expand Down
24 changes: 14 additions & 10 deletions packages/plugins/prometheus/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,17 +183,21 @@ export function usePrometheus(options: PrometheusTracingPluginConfig): Plugin {
onRequest({ request }) {
startByRequest.set(request, Date.now());
},
onParse() {
return ({ result: document, context }) => {
const operationAST = getOperationAST(document, context.params.operationName);
const params = {
document,
operationName: operationAST?.name?.value,
operationType: operationAST?.operation,
};
onParse({ context }) {
// If only it is Yoga, we calculate HTTP request time
if (context.request) {
return ({ result: document, context }) => {
const operationAST = getOperationAST(document, context.params.operationName);
const params = {
document,
operationName: operationAST?.name?.value,
operationType: operationAST?.operation,
};

paramsByRequest.set(context.request, params);
};
paramsByRequest.set(context.request, params);
};
}
return undefined;
},
onResponse({ request, response, serverContext }) {
const start = startByRequest.get(request);
Expand Down
9 changes: 9 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit e8a7186

Please sign in to comment.