Skip to content

Commit a747d24

Browse files
feat(prometheus): allow customizations (#2411)
* feat(prometheus): allow customizations * httpRequestHeaders flag * Add tests * chore(dependencies): updated changesets for modified dependencies * Go --------- Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
1 parent 8660bf2 commit a747d24

8 files changed

+274
-20
lines changed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-yoga/plugin-apollo-inline-trace': patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@whatwg-node/fetch@^0.8.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.8.1) (from `^0.7.0`, in `peerDependencies`)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-yoga/plugin-apq': patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@whatwg-node/fetch@^0.8.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.8.1) (from `^0.7.0`, in `dependencies`)
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-yoga/plugin-prometheus': patch
3+
---
4+
dependencies updates:
5+
- Added dependency [`graphql@^15.2.0 || ^16.0.0` ↗︎](https://www.npmjs.com/package/graphql/v/15.2.0) (to `peerDependencies`)
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
'graphql-yoga': patch
3+
---
4+
dependencies updates:
5+
- Updated dependency [`@whatwg-node/fetch@^0.8.1` ↗︎](https://www.npmjs.com/package/@whatwg-node/fetch/v/0.8.1) (from `^0.7.0`, in `dependencies`)
6+
- Updated dependency [`@whatwg-node/server@^0.6.7` ↗︎](https://www.npmjs.com/package/@whatwg-node/server/v/0.6.7) (from `^0.6.5`, in `dependencies`)

.changeset/wicked-horses-shake.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@graphql-yoga/plugin-prometheus': patch
3+
---
4+
5+
Allow customizations in prometheus plugin

packages/plugins/prometheus/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
},
4545
"peerDependencies": {
4646
"prom-client": "^13 || ^14.0.0",
47+
"graphql": "^15.2.0 || ^16.0.0",
4748
"graphql-yoga": "^3.5.1"
4849
},
4950
"devDependencies": {

packages/plugins/prometheus/src/index.ts

Lines changed: 77 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,22 @@
11
import {
22
PrometheusTracingPluginConfig as EnvelopPrometheusTracingPluginConfig,
33
usePrometheus as useEnvelopPrometheus,
4+
createCounter,
5+
createHistogram,
6+
createSummary,
7+
FillLabelsFnParams,
48
} from '@envelop/prometheus'
9+
import { getOperationAST } from 'graphql'
510
import { Plugin } from 'graphql-yoga'
611
import { Histogram, register as defaultRegistry } from 'prom-client'
712

13+
export { createCounter, createHistogram, createSummary, FillLabelsFnParams }
14+
815
export interface PrometheusTracingPluginConfig
916
extends EnvelopPrometheusTracingPluginConfig {
10-
http?: boolean | EnvelopPrometheusTracingPluginConfig['execute']
17+
http?: boolean | ReturnType<typeof createHistogram>
18+
httpRequestHeaders?: boolean
19+
httpResponseHeaders?: boolean
1120
/**
1221
* The endpoint to serve metrics exposed by this plugin.
1322
* Defaults to "/metrics".
@@ -27,21 +36,61 @@ export function usePrometheus(options: PrometheusTracingPluginConfig): Plugin {
2736
const endpoint = options.endpoint || '/metrics'
2837
const registry = options.registry || defaultRegistry
2938

30-
const httpHistogram = new Histogram({
31-
name: 'graphql_yoga_http_duration',
32-
help: 'Time spent on HTTP connection',
33-
labelNames: [
39+
let httpHistogram: ReturnType<typeof createHistogram> | undefined
40+
41+
if (options.http) {
42+
const labelNames = [
3443
'url',
3544
'method',
36-
'requestHeaders',
3745
'statusCode',
3846
'statusText',
39-
'responseHeaders',
40-
],
41-
registers: [registry],
42-
})
47+
'operationName',
48+
'operationType',
49+
]
50+
if (options.httpRequestHeaders) {
51+
labelNames.push('requestHeaders')
52+
}
53+
if (options.httpResponseHeaders) {
54+
labelNames.push('responseHeaders')
55+
}
56+
httpHistogram =
57+
typeof options.http === 'object'
58+
? options.http
59+
: createHistogram({
60+
histogram: new Histogram({
61+
name: 'graphql_yoga_http_duration',
62+
help: 'Time spent on HTTP connection',
63+
labelNames,
64+
registers: [registry],
65+
}),
66+
fillLabelsFn(params, { request, response }) {
67+
const labels: Record<string, string> = {
68+
operationName: params.operationName || 'Anonymous',
69+
url: request.url,
70+
method: request.method,
71+
statusCode: response.status,
72+
statusText: response.statusText,
73+
}
74+
if (params?.operationType) {
75+
labels.operationType = params.operationType
76+
}
77+
if (options.httpRequestHeaders) {
78+
labels.requestHeaders = JSON.stringify(
79+
headersToObj(request.headers),
80+
)
81+
}
82+
if (options.httpResponseHeaders) {
83+
labels.responseHeaders = JSON.stringify(
84+
headersToObj(response.headers),
85+
)
86+
}
87+
return labels
88+
},
89+
})
90+
}
4391

4492
const startByRequest = new WeakMap<Request, number>()
93+
const paramsByRequest = new WeakMap<Request, FillLabelsFnParams>()
4594

4695
return {
4796
onPluginInit({ addPlugin }) {
@@ -59,19 +108,27 @@ export function usePrometheus(options: PrometheusTracingPluginConfig): Plugin {
59108
endResponse(response)
60109
}
61110
},
62-
onResponse({ request, response }) {
111+
onExecute({ args }) {
112+
const operationAST = getOperationAST(args.document, args.operationName)
113+
const operationType = operationAST?.operation
114+
const operationName = operationAST?.name?.value
115+
paramsByRequest.set(args.contextValue.request, {
116+
document: args.document,
117+
operationName,
118+
operationType,
119+
})
120+
},
121+
onResponse({ request, response, serverContext }) {
63122
const start = startByRequest.get(request)
64123
if (start) {
65124
const duration = Date.now() - start
66-
httpHistogram.observe(
67-
{
68-
url: request.url,
69-
method: request.method,
70-
requestHeaders: JSON.stringify(headersToObj(request.headers)),
71-
statusCode: response.status,
72-
statusText: response.statusText,
73-
responseHeaders: JSON.stringify(headersToObj(response.headers)),
74-
},
125+
const params = paramsByRequest.get(request)
126+
httpHistogram?.histogram.observe(
127+
httpHistogram.fillLabelsFn(params || {}, {
128+
...serverContext,
129+
request,
130+
response,
131+
}),
75132
duration,
76133
)
77134
}
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import { usePrometheus } from '@graphql-yoga/plugin-prometheus'
2+
import { createSchema, createYoga } from 'graphql-yoga'
3+
import { register as registry } from 'prom-client'
4+
5+
describe('Prometheus', () => {
6+
const schema = createSchema({
7+
typeDefs: /* GraphQL */ `
8+
type Query {
9+
hello: String!
10+
}
11+
`,
12+
resolvers: {
13+
Query: {
14+
hello: () => 'Hello world!',
15+
},
16+
},
17+
})
18+
afterEach(() => {
19+
registry.clear()
20+
})
21+
it('http flag should work and do not send headers by default', async () => {
22+
const yoga = createYoga({
23+
schema,
24+
plugins: [
25+
usePrometheus({
26+
http: true,
27+
registry,
28+
}),
29+
],
30+
})
31+
const result = await yoga.fetch('http://localhost:4000/graphql', {
32+
method: 'POST',
33+
headers: {
34+
'Content-Type': 'application/json',
35+
'x-test': 'test',
36+
},
37+
body: JSON.stringify({
38+
query: /* GraphQL */ `
39+
query TestProm {
40+
hello
41+
}
42+
`,
43+
}),
44+
})
45+
await result.text()
46+
const metrics = await registry.metrics()
47+
expect(metrics).toContain('graphql_yoga_http_duration_bucket')
48+
expect(metrics).toContain('operationName="TestProm"')
49+
expect(metrics).toContain('operationType="query"')
50+
expect(metrics).toContain('url="http://localhost:4000/graphql"')
51+
expect(metrics).toContain('method="POST"')
52+
expect(metrics).toContain('statusCode="200"')
53+
expect(metrics).toContain('statusText="OK"')
54+
expect(metrics).not.toContain('requestHeaders')
55+
expect(metrics).not.toContain('x-test=test')
56+
})
57+
it('httpRequestHeaders should work', async () => {
58+
const yoga = createYoga({
59+
schema,
60+
plugins: [
61+
usePrometheus({
62+
http: true,
63+
httpRequestHeaders: true,
64+
registry,
65+
}),
66+
],
67+
})
68+
const result = await yoga.fetch('http://localhost:4000/graphql', {
69+
method: 'POST',
70+
headers: {
71+
'Content-Type': 'application/json',
72+
'x-test': 'test',
73+
},
74+
body: JSON.stringify({
75+
query: /* GraphQL */ `
76+
query TestProm {
77+
hello
78+
}
79+
`,
80+
}),
81+
})
82+
await result.text()
83+
const metrics = await registry.metrics()
84+
expect(metrics).toContain('graphql_yoga_http_duration_bucket')
85+
expect(metrics).toContain('operationName="TestProm"')
86+
expect(metrics).toContain('operationType="query"')
87+
expect(metrics).toContain('url="http://localhost:4000/graphql"')
88+
expect(metrics).toContain('method="POST"')
89+
expect(metrics).toContain('statusCode="200"')
90+
expect(metrics).toContain('statusText="OK"')
91+
expect(metrics).toContain(
92+
'requestHeaders="{\\"content-type\\":\\"application/json\\",\\"x-test\\":\\"test\\",\\"content-length\\":\\"82\\"}"}',
93+
)
94+
})
95+
it('httpResponseHeaders should work', async () => {
96+
const yoga = createYoga({
97+
schema,
98+
plugins: [
99+
usePrometheus({
100+
http: true,
101+
httpResponseHeaders: true,
102+
registry,
103+
}),
104+
],
105+
})
106+
const result = await yoga.fetch('http://localhost:4000/graphql', {
107+
method: 'POST',
108+
headers: {
109+
'Content-Type': 'application/json',
110+
'x-test': 'test',
111+
},
112+
body: JSON.stringify({
113+
query: /* GraphQL */ `
114+
query TestProm {
115+
hello
116+
}
117+
`,
118+
}),
119+
})
120+
await result.text()
121+
const metrics = await registry.metrics()
122+
expect(metrics).toContain('graphql_yoga_http_duration_bucket')
123+
expect(metrics).toContain('operationName="TestProm"')
124+
expect(metrics).toContain('operationType="query"')
125+
expect(metrics).toContain('url="http://localhost:4000/graphql"')
126+
expect(metrics).toContain('method="POST"')
127+
expect(metrics).toContain('statusCode="200"')
128+
expect(metrics).toContain('statusText="OK"')
129+
expect(metrics).toContain(
130+
`responseHeaders="{\\"content-type\\":\\"application/json; charset=utf-8\\",\\"content-length\\\":\\"33\\",\\"access-control-allow-origin\\":\\"*\\"}"}`,
131+
)
132+
})
133+
it('endpoint should work', async () => {
134+
const yoga = createYoga({
135+
schema,
136+
plugins: [
137+
usePrometheus({
138+
endpoint: '/metrics',
139+
http: true,
140+
execute: true,
141+
registry,
142+
}),
143+
],
144+
})
145+
const graphqlResult = await yoga.fetch('http://localhost:4000/graphql', {
146+
method: 'POST',
147+
headers: {
148+
'Content-Type': 'application/json',
149+
},
150+
body: JSON.stringify({
151+
query: /* GraphQL */ `
152+
query TestProm {
153+
hello
154+
}
155+
`,
156+
}),
157+
})
158+
await graphqlResult.text()
159+
const result = await yoga.fetch('http://localhost:4000/metrics')
160+
const metrics = await result.text()
161+
expect(metrics).toContain('graphql_envelop_phase_execute_bucket')
162+
expect(metrics).toContain('graphql_yoga_http_duration_bucket')
163+
expect(metrics).toContain('operationName="TestProm"')
164+
expect(metrics).toContain('operationType="query"')
165+
expect(metrics).toContain('url="http://localhost:4000/graphql"')
166+
expect(metrics).toContain('method="POST"')
167+
expect(metrics).toContain('statusCode="200"')
168+
expect(metrics).toContain('statusText="OK"')
169+
})
170+
})

0 commit comments

Comments
 (0)