diff --git a/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js b/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js index 730926bfeb07..7371888f4fbd 100644 --- a/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js +++ b/packages/cli/src/commands/experimental/setupOpentelemetryHandler.js @@ -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' @@ -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), ] diff --git a/packages/graphql-server/src/createGraphQLYoga.ts b/packages/graphql-server/src/createGraphQLYoga.ts index b6e5af3edde7..a02154b2e8dd 100644 --- a/packages/graphql-server/src/createGraphQLYoga.ts +++ b/packages/graphql-server/src/createGraphQLYoga.ts @@ -47,6 +47,7 @@ export const createGraphQLYoga = ({ graphiQLEndpoint = '/graphql', schemaOptions, realtime, + openTelemetryOptions, }: GraphQLYogaOptions) => { let schema: GraphQLSchema let redwoodDirectivePlugins = [] as Plugin[] @@ -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)) diff --git a/packages/graphql-server/src/functions/graphql.ts b/packages/graphql-server/src/functions/graphql.ts index 001afeec55ac..9130770f4c42 100644 --- a/packages/graphql-server/src/functions/graphql.ts +++ b/packages/graphql-server/src/functions/graphql.ts @@ -39,6 +39,7 @@ export const createGraphQLHandler = ({ defaultError = 'Something went wrong.', graphiQLEndpoint = '/graphql', schemaOptions, + openTelemetryOptions, }: GraphQLHandlerOptions) => { const handlerFn = async ( event: APIGatewayProxyEvent, @@ -69,6 +70,7 @@ export const createGraphQLHandler = ({ defaultError, graphiQLEndpoint, schemaOptions, + openTelemetryOptions, }) try { diff --git a/packages/graphql-server/src/makeMergedSchema.ts b/packages/graphql-server/src/makeMergedSchema.ts index eb8ae589b67d..7ffcbd24eee9 100644 --- a/packages/graphql-server/src/makeMergedSchema.ts +++ b/packages/graphql-server/src/makeMergedSchema.ts @@ -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, diff --git a/packages/graphql-server/src/plugins/useRedwoodOpenTelemetry.ts b/packages/graphql-server/src/plugins/useRedwoodOpenTelemetry.ts index e1b0a678f68a..79fb69ca199c 100644 --- a/packages/graphql-server/src/plugins/useRedwoodOpenTelemetry.ts +++ b/packages/graphql-server/src/plugins/useRedwoodOpenTelemetry.ts @@ -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', @@ -24,16 +26,12 @@ type PluginContext = { [tracingSpanSymbol]: opentelemetry.Span } -export const useRedwoodOpenTelemetry = (): Plugin => { +export const useRedwoodOpenTelemetry = ( + options: RedwoodOpenTelemetryConfig +): Plugin => { const spanKind: SpanKind = SpanKind.SERVER const spanAdditionalAttributes: Attributes = {} - const options = { - resolvers: true, - result: true, - variables: true, - } - const tracer = opentelemetry.trace.getTracer('redwoodjs') return { @@ -51,8 +49,7 @@ export const useRedwoodOpenTelemetry = (): Plugin => { context[tracingSpanSymbol] ) const { fieldName, returnType, parentType } = info - - const resolverSpan = tracer.startSpan( + return tracer.startActiveSpan( `${parentType.name}.${fieldName}`, { attributes: { @@ -62,18 +59,21 @@ export const useRedwoodOpenTelemetry = (): Plugin => { [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 () => {} }) @@ -81,7 +81,7 @@ export const useRedwoodOpenTelemetry = (): Plugin => { } }, onExecute({ args, extendContext }) { - const executionSpan = tracer.startSpan( + return tracer.startActiveSpan( `${args.operationName || 'Anonymous Operation'}`, { kind: spanKind, @@ -98,44 +98,46 @@ export const useRedwoodOpenTelemetry = (): Plugin => { } : {}), }, - } - ) - const resultCbs: OnExecuteHookResult = { - 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 = { + 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 + } + ) }, } } diff --git a/packages/graphql-server/src/types.ts b/packages/graphql-server/src/types.ts index 31b571bc132f..9cb323d5b03a 100644 --- a/packages/graphql-server/src/types.ts +++ b/packages/graphql-server/src/types.ts @@ -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 */ @@ -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 } /**