Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(graphql-server): Conditionally enable OTel plugin and OTel plugin updates #8782

Merged
merged 9 commits into from
Jun 30, 2023
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import execa from 'execa'
import { Listr } from 'listr2'

import { addApiPackages } from '@redwoodjs/cli-helpers'
import { getConfigPath } from '@redwoodjs/project-config'
import { getConfigPath, resolveFile } from '@redwoodjs/project-config'
import { errorTelemetry } from '@redwoodjs/telemetry'

import { getPaths, transformTSToJS, writeFile } from '../../lib'
Expand Down Expand Up @@ -82,6 +82,52 @@ export const handler = async ({ force, verbose }) => {
}
},
},
{
title: 'Notice: GraphQL function update...',
enabled: () => {
return fs.existsSync(
resolveFile(path.join(getPaths().api.functions, 'graphql'))
)
},
task: (_ctx, task) => {
task.output = [
"Please add the following to your 'createGraphQLHandler' function options to enable OTel for your graphql",
'openTelemetryOptions: {',
' resolvers: true,',
' result: true,',
' variables: true,',
'}',
'',
`Which can found at ${c.info(
path.join(getPaths().api.functions, 'graphql')
)}`,
].join('\n')
},
options: { persistentOutput: true },
},
{
title: 'Notice: GraphQL function update (server file)...',
enabled: () => {
return fs.existsSync(
resolveFile(path.join(getPaths().api.src, 'server'))
)
},
task: (_ctx, task) => {
task.output = [
"Please add the following to your 'redwoodFastifyGraphQLServer' plugin options to enable OTel for your graphql",
'openTelemetryOptions: {',
' resolvers: true,',
' result: true,',
' variables: true,',
'}',
'',
`Which can found at ${c.info(
path.join(getPaths().api.src, 'server')
)}`,
].join('\n')
},
options: { persistentOutput: true },
},
addApiPackages(opentelemetryPackages),
]

Expand Down
5 changes: 4 additions & 1 deletion packages/graphql-server/src/createGraphQLYoga.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ export const createGraphQLYoga = ({
graphiQLEndpoint = '/graphql',
schemaOptions,
realtime,
openTelemetryOptions,
}: GraphQLYogaOptions) => {
let schema: GraphQLSchema
let redwoodDirectivePlugins = [] as Plugin[]
Expand Down Expand Up @@ -136,7 +137,9 @@ export const createGraphQLYoga = ({
plugins.push(...redwoodDirectivePlugins)

// Custom Redwood OpenTelemetry plugin
plugins.push(useRedwoodOpenTelemetry())
if (openTelemetryOptions !== undefined) {
plugins.push(useRedwoodOpenTelemetry(openTelemetryOptions))
}

// Secure the GraphQL server
plugins.push(useArmor(logger, armorConfig))
Expand Down
2 changes: 2 additions & 0 deletions packages/graphql-server/src/functions/graphql.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ export const createGraphQLHandler = ({
defaultError = 'Something went wrong.',
graphiQLEndpoint = '/graphql',
schemaOptions,
openTelemetryOptions,
}: GraphQLHandlerOptions) => {
const handlerFn = async (
event: APIGatewayProxyEvent,
Expand Down Expand Up @@ -69,6 +70,7 @@ export const createGraphQLHandler = ({
defaultError,
graphiQLEndpoint,
schemaOptions,
openTelemetryOptions,
})

try {
Expand Down
6 changes: 5 additions & 1 deletion packages/graphql-server/src/makeMergedSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,11 @@ const mapFieldsToService = ({
// Swallow the error for now
}

if (experimentalOpenTelemetryEnabled) {
const captureResolvers =
// @ts-expect-error context is unknown
context && context['OPEN_TELEMETRY_GRAPHQL'] !== undefined

if (experimentalOpenTelemetryEnabled && captureResolvers) {
return wrapWithOpenTelemetry(
services[name],
args,
Expand Down
106 changes: 54 additions & 52 deletions packages/graphql-server/src/plugins/useRedwoodOpenTelemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import { Attributes, SpanKind } from '@opentelemetry/api'
import * as opentelemetry from '@opentelemetry/api'
import { print } from 'graphql'

import { RedwoodOpenTelemetryConfig } from 'src/types'

export enum AttributeName {
EXECUTION_ERROR = 'graphql.execute.error',
EXECUTION_RESULT = 'graphql.execute.result',
Expand All @@ -24,16 +26,12 @@ type PluginContext = {
[tracingSpanSymbol]: opentelemetry.Span
}

export const useRedwoodOpenTelemetry = (): Plugin<PluginContext> => {
export const useRedwoodOpenTelemetry = (
options: RedwoodOpenTelemetryConfig
): Plugin<PluginContext> => {
const spanKind: SpanKind = SpanKind.SERVER
const spanAdditionalAttributes: Attributes = {}

const options = {
resolvers: true,
result: true,
variables: true,
}

const tracer = opentelemetry.trace.getTracer('redwoodjs')

return {
Expand All @@ -51,8 +49,7 @@ export const useRedwoodOpenTelemetry = (): Plugin<PluginContext> => {
context[tracingSpanSymbol]
)
const { fieldName, returnType, parentType } = info

const resolverSpan = tracer.startSpan(
return tracer.startActiveSpan(
`${parentType.name}.${fieldName}`,
{
attributes: {
Expand All @@ -62,26 +59,29 @@ export const useRedwoodOpenTelemetry = (): Plugin<PluginContext> => {
[AttributeName.RESOLVER_ARGS]: JSON.stringify(args || {}),
},
},
ctx
)
ctx,
(resolverSpan) => {
resolverSpan.spanContext()

return ({ result }) => {
if (result instanceof Error) {
resolverSpan.recordException({
name: AttributeName.RESOLVER_EXCEPTION,
message: JSON.stringify(result),
})
return ({ result }: { result: unknown }) => {
if (result instanceof Error) {
resolverSpan.recordException({
name: AttributeName.RESOLVER_EXCEPTION,
message: JSON.stringify(result),
})
}
resolverSpan.end()
}
}
resolverSpan.end()
}
)
}
return () => {}
})
)
}
},
onExecute({ args, extendContext }) {
const executionSpan = tracer.startSpan(
return tracer.startActiveSpan(
`${args.operationName || 'Anonymous Operation'}`,
{
kind: spanKind,
Expand All @@ -98,44 +98,46 @@ export const useRedwoodOpenTelemetry = (): Plugin<PluginContext> => {
}
: {}),
},
}
)
const resultCbs: OnExecuteHookResult<PluginContext> = {
onExecuteDone({ result }) {
if (isAsyncIterable(result)) {
executionSpan.end()
// eslint-disable-next-line no-console
console.warn(
`Plugin "RedwoodOpenTelemetry" encountered an AsyncIterator which is not supported yet, so tracing data is not available for the operation.`
)
return
}
},
(executionSpan) => {
const resultCbs: OnExecuteHookResult<PluginContext> = {
onExecuteDone({ result }) {
if (isAsyncIterable(result)) {
executionSpan.end()
// eslint-disable-next-line no-console
console.warn(
`Plugin "RedwoodOpenTelemetry" encountered an AsyncIterator which is not supported yet, so tracing data is not available for the operation.`
)
return
}

if (result.data && options.result) {
executionSpan.setAttribute(
AttributeName.EXECUTION_RESULT,
JSON.stringify(result)
)
if (result.data && options.result) {
executionSpan.setAttribute(
AttributeName.EXECUTION_RESULT,
JSON.stringify(result)
)
}

if (result.errors && result.errors.length > 0) {
executionSpan.recordException({
name: AttributeName.EXECUTION_ERROR,
message: JSON.stringify(result.errors),
})
}

executionSpan.end()
},
}

if (result.errors && result.errors.length > 0) {
executionSpan.recordException({
name: AttributeName.EXECUTION_ERROR,
message: JSON.stringify(result.errors),
if (options.resolvers) {
extendContext({
[tracingSpanSymbol]: executionSpan,
})
}

executionSpan.end()
},
}

if (options.resolvers) {
extendContext({
[tracingSpanSymbol]: executionSpan,
})
}

return resultCbs
return resultCbs
}
)
},
}
}
22 changes: 22 additions & 0 deletions packages/graphql-server/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,23 @@ export interface RedwoodGraphQLContext {
[index: string]: unknown
}

export interface RedwoodOpenTelemetryConfig {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we perhaps have some inline comments to describe what enabling each does?

for example, setting resolvers to true adds ... ?

This?

          if (options.resolvers) {
             extendContext({
               [tracingSpanSymbol]: executionSpan,

Adds execution span telemetry for each graph resolver?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added the documentation to the options

/**
* @description Enables the creation of a span for each resolver execution.
*/
resolvers: boolean

/**
* @description Includes the execution result in the span attributes.
*/
variables: boolean

/**
* @description Includes the variables in the span attributes.
*/
result: boolean
}

/**
* GraphQLYogaOptions
*/
Expand Down Expand Up @@ -214,6 +231,11 @@ export type GraphQLYogaOptions = {
* Only supported in a swerver deploy and not allowed with GraphQLHandler config
*/
realtime?: RedwoodRealtimeOptions

/**
* @description Configure OpenTelemetry plugin behaviour
*/
openTelemetryOptions?: RedwoodOpenTelemetryConfig
}

/**
Expand Down