Skip to content

Commit 396bccb

Browse files
authored
Improvements for opentelemetry plugin (#2201)
1 parent eafae3b commit 396bccb

File tree

6 files changed

+224
-16
lines changed

6 files changed

+224
-16
lines changed

.changeset/early-books-jam.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@envelop/opentelemetry': minor
3+
---
4+
5+
feat: exclude operations by using list of names or a custom function
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
'@envelop/opentelemetry': minor
3+
---
4+
5+
feat: allow to use a custom function to resolve/sanitize variables

packages/plugins/opentelemetry/README.md

Lines changed: 52 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,8 +35,53 @@ const getEnveloped = envelop({
3535
})
3636
```
3737

38-
If you wish to use custom tracer/exporter, create it and pass it. This example integrates Jaeger
39-
tracer:
38+
## Integration with `@opentelemetry` packages
39+
40+
By default, this plugin is creating a console exporter for exporting the `Span`s.
41+
42+
If you wish to use custom tracer/exporter, create it and pass it to the plugin factory function.
43+
44+
The following example is taking the current global tracer:
45+
46+
```ts
47+
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
48+
import { envelop, useEngine } from '@envelop/core'
49+
import { useOpenTelemetry } from '@envelop/opentelemetry'
50+
import { trace } from '@opentelemetry/api'
51+
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
52+
53+
const getEnveloped = envelop({
54+
plugins: [
55+
useEngine({ parse, validate, specifiedRules, execute, subscribe }),
56+
// ... other plugins ...
57+
useOpenTelemetry(
58+
{
59+
resolvers: true, // Tracks resolvers calls, and tracks resolvers thrown errors
60+
variables: true, // Includes the operation variables values as part of the metadata collected
61+
result: true // Includes execution result object as part of the metadata collected
62+
},
63+
trace.getTracerProvider()
64+
)
65+
]
66+
})
67+
```
68+
69+
This example integrates Jaeger tracer. To use this example, start by running Jaeger locally in a
70+
Docker container (or, follow
71+
[other options to run Jaeger locally](https://www.npmjs.com/package/@opentelemetry/exporter-jaeger#prerequisites)):
72+
73+
```
74+
docker run -d --name jaeger \
75+
-e COLLECTOR_ZIPKIN_HTTP_PORT=9411 \
76+
-p 5775:5775/udp \
77+
-p 6831:6831/udp \
78+
-p 6832:6832/udp \
79+
-p 5778:5778 \
80+
-p 16686:16686 \
81+
-p 14268:14268 \
82+
-p 9411:9411 \
83+
jaegertracing/all-in-one:latest
84+
```
4085

4186
```ts
4287
import { execute, parse, specifiedRules, subscribe, validate } from 'graphql'
@@ -45,10 +90,14 @@ import { useOpenTelemetry } from '@envelop/opentelemetry'
4590
import { JaegerExporter } from '@opentelemetry/exporter-jaeger'
4691
import { BasicTracerProvider, SimpleSpanProcessor } from '@opentelemetry/sdk-trace-base'
4792

93+
// Create your exporter
4894
const exporter = new JaegerExporter({
49-
serviceName: 'my-service-name'
95+
serviceName: 'my-service-name',
96+
host: 'localhost',
97+
port: 6832
5098
})
5199

100+
// Configure opentelmetry to run for your service
52101
const provider = new BasicTracerProvider()
53102
provider.addSpanProcessor(new SimpleSpanProcessor(exporter))
54103
provider.register()

packages/plugins/opentelemetry/src/index.ts

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,16 @@ export enum AttributeName {
3232

3333
const tracingSpanSymbol = Symbol('OPEN_TELEMETRY_GRAPHQL');
3434

35+
export type ResolveVariablesAttributesFn = (variableValues: any) => opentelemetry.AttributeValue;
36+
export type ExcludeOperationNamesFn = (operationName: string | undefined) => boolean;
37+
3538
export type TracingOptions = {
3639
document?: boolean;
3740
resolvers?: boolean;
38-
variables?: boolean;
41+
variables?: boolean | ResolveVariablesAttributesFn;
3942
result?: boolean;
4043
traceIdInResult?: string;
44+
excludedOperationNames?: string[] | ExcludeOperationNamesFn;
4145
};
4246

4347
type PluginContext = {
@@ -131,6 +135,16 @@ export const useOpenTelemetry = (
131135
if (!operationAst) {
132136
return;
133137
}
138+
139+
if (
140+
options.excludedOperationNames &&
141+
(typeof options.excludedOperationNames === 'function'
142+
? options.excludedOperationNames(operationAst.name?.value)
143+
: options.excludedOperationNames.includes(operationAst.name?.value || ''))
144+
) {
145+
return;
146+
}
147+
134148
const operationType = operationAst.operation;
135149
let isDocumentLoggable: boolean;
136150
if (options.document == null || options.document === true) {
@@ -146,6 +160,15 @@ export const useOpenTelemetry = (
146160
}
147161
const operationName = operationAst.name?.value || 'anonymous';
148162
const currOtelContext = getCurrentOtelContext(args.contextValue);
163+
164+
const variablesAttribute = options.variables
165+
? {
166+
[AttributeName.EXECUTION_VARIABLES]:
167+
typeof options.variables === 'function'
168+
? options.variables(args.variableValues)
169+
: JSON.stringify(args.variableValues ?? {}),
170+
}
171+
: {};
149172
const executionSpan = tracer.startSpan(
150173
`${spanPrefix}${operationType}.${operationName}`,
151174
{
@@ -157,9 +180,7 @@ export const useOpenTelemetry = (
157180
[AttributeName.EXECUTION_OPERATION_DOCUMENT]: isDocumentLoggable
158181
? getDocumentString(args.document, print)
159182
: undefined,
160-
...(options.variables
161-
? { [AttributeName.EXECUTION_VARIABLES]: JSON.stringify(args.variableValues ?? {}) }
162-
: {}),
183+
...variablesAttribute,
163184
},
164185
},
165186
currOtelContext,
@@ -216,6 +237,16 @@ export const useOpenTelemetry = (
216237
if (!operationAst) {
217238
return;
218239
}
240+
241+
if (
242+
options.excludedOperationNames &&
243+
(typeof options.excludedOperationNames === 'function'
244+
? options.excludedOperationNames(operationAst.name?.value)
245+
: options.excludedOperationNames.includes(operationAst.name?.value || ''))
246+
) {
247+
return;
248+
}
249+
219250
const operationType = 'subscription';
220251
let isDocumentLoggable: boolean;
221252
if (options.variables) {
@@ -227,6 +258,15 @@ export const useOpenTelemetry = (
227258
}
228259
const currOtelContext = getCurrentOtelContext(args.contextValue);
229260
const operationName = operationAst.name?.value || 'anonymous';
261+
const variablesAttribute = options.variables
262+
? {
263+
[AttributeName.EXECUTION_VARIABLES]:
264+
typeof options.variables === 'function'
265+
? options.variables(args.variableValues)
266+
: JSON.stringify(args.variableValues ?? {}),
267+
}
268+
: {};
269+
230270
const subscriptionSpan = tracer.startSpan(
231271
`${operationType}.${operationName}`,
232272
{
@@ -238,9 +278,7 @@ export const useOpenTelemetry = (
238278
[AttributeName.EXECUTION_OPERATION_DOCUMENT]: isDocumentLoggable
239279
? getDocumentString(args.document, print)
240280
: undefined,
241-
...(options.variables
242-
? { [AttributeName.EXECUTION_VARIABLES]: JSON.stringify(args.variableValues ?? {}) }
243-
: {}),
281+
...variablesAttribute,
244282
},
245283
},
246284
currOtelContext,

packages/plugins/opentelemetry/test/use-open-telemetry.spec.ts

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,66 @@ describe('useOpenTelemetry', () => {
156156
expect(actual[0].name).toBe('query.ping');
157157
});
158158

159+
it('can exclude operation by using list of strings', async () => {
160+
const exporter = new InMemorySpanExporter();
161+
const testInstance = createTestkit(
162+
[
163+
useTestOpenTelemetry(exporter, {
164+
excludedOperationNames: ['ping'],
165+
}),
166+
],
167+
schema,
168+
);
169+
170+
await testInstance.execute(/* GraphQL */ `
171+
query ping {
172+
ping
173+
}
174+
`);
175+
const actual = exporter.getFinishedSpans();
176+
expect(actual.length).toBe(0);
177+
});
178+
179+
it('can exclude anonymous operations with fn', async () => {
180+
const exporter = new InMemorySpanExporter();
181+
const testInstance = createTestkit(
182+
[
183+
useTestOpenTelemetry(exporter, {
184+
excludedOperationNames: name => name === undefined,
185+
}),
186+
],
187+
schema,
188+
);
189+
190+
await testInstance.execute(/* GraphQL */ `
191+
query {
192+
ping
193+
}
194+
`);
195+
const actual = exporter.getFinishedSpans();
196+
expect(actual.length).toBe(0);
197+
});
198+
199+
it('can exclude anonymous operations with fn', async () => {
200+
const exporter = new InMemorySpanExporter();
201+
const testInstance = createTestkit(
202+
[
203+
useTestOpenTelemetry(exporter, {
204+
excludedOperationNames: name => name !== undefined && name.startsWith('pi'),
205+
}),
206+
],
207+
schema,
208+
);
209+
210+
await testInstance.execute(/* GraphQL */ `
211+
query ping {
212+
ping
213+
}
214+
`);
215+
const actual = exporter.getFinishedSpans();
216+
expect(actual.length).toBe(0);
217+
});
218+
159219
it('execute span should add attributes', async () => {
160220
const exporter = new InMemorySpanExporter();
161221
const testInstance = createTestkit([useTestOpenTelemetry(exporter)], schema);
@@ -208,6 +268,57 @@ describe('useOpenTelemetry', () => {
208268
});
209269
});
210270

271+
it('custom variables attributes with fn', async () => {
272+
const exporter = new InMemorySpanExporter();
273+
const testInstance = createTestkit(
274+
[
275+
useTestOpenTelemetry(exporter, {
276+
variables: v => {
277+
if (v && typeof v === 'object' && 'selector' in v) {
278+
return JSON.stringify(v.selector);
279+
}
280+
281+
return '';
282+
},
283+
}),
284+
],
285+
schema,
286+
);
287+
288+
const queryStr = /* GraphQL */ `
289+
query echo($testVar: String!, $testVar2: String!, $selector: String!) {
290+
echo1: echo(message: $testVar)
291+
echo2: echo(message: $testVar2)
292+
echo3: echo(message: $selector)
293+
}
294+
`;
295+
296+
const resp = await testInstance.execute(queryStr, {
297+
testVar: 'hello',
298+
testVar2: 'world',
299+
selector: '1',
300+
});
301+
302+
console.log(resp);
303+
304+
assertSingleValue(resp);
305+
306+
const actual = exporter.getFinishedSpans();
307+
expect(actual.length).toBe(1);
308+
expect(actual[0].attributes).toEqual({
309+
[AttributeName.EXECUTION_OPERATION_DOCUMENT]: `
310+
query echo($testVar: String!, $testVar2: String!, $selector: String!) {
311+
echo1: echo(message: $testVar)
312+
echo2: echo(message: $testVar2)
313+
echo3: echo(message: $selector)
314+
}
315+
`,
316+
[AttributeName.EXECUTION_OPERATION_NAME]: 'echo',
317+
[AttributeName.EXECUTION_OPERATION_TYPE]: 'query',
318+
[AttributeName.EXECUTION_VARIABLES]: '"1"',
319+
});
320+
});
321+
211322
it('span should not add document attribute if options false', async () => {
212323
const exporter = new InMemorySpanExporter();
213324
const testInstance = createTestkit(

pnpm-lock.yaml

Lines changed: 6 additions & 6 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)