Skip to content

Commit

Permalink
fix(graphql-server): Conditionally enable OTel plugin and OTel plugin…
Browse files Browse the repository at this point in the history
… updates (#8782)

* Enable options to the OTel plugin

* Start active spans within OTel plugin

* Update setup command

* Conditionally enable resolver wrapping for makeMergedSchema

* Add description to plugin options
  • Loading branch information
Josh-Walker-GM authored and jtoar committed Jun 30, 2023
1 parent 4cc45ab commit e637077
Show file tree
Hide file tree
Showing 6 changed files with 134 additions and 55 deletions.
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 {
/**
* @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

0 comments on commit e637077

Please sign in to comment.