From 3d224cadaa106720062af8d467a432fcc59a3270 Mon Sep 17 00:00:00 2001 From: Laurin Quast Date: Fri, 11 Nov 2022 13:37:10 +0100 Subject: [PATCH] feat: log level via config --- .../graphql-yoga/__tests__/logging.spec.ts | 8 +- packages/graphql-yoga/__tests__/logging.ts | 2 +- packages/graphql-yoga/src/logger.ts | 150 ++++++++++-------- packages/graphql-yoga/src/server.ts | 17 +- .../v3/features/logging-and-debugging.mdx | 89 +++++++++-- 5 files changed, 169 insertions(+), 97 deletions(-) diff --git a/packages/graphql-yoga/__tests__/logging.spec.ts b/packages/graphql-yoga/__tests__/logging.spec.ts index d772b06682..2534145dfe 100644 --- a/packages/graphql-yoga/__tests__/logging.spec.ts +++ b/packages/graphql-yoga/__tests__/logging.spec.ts @@ -5,7 +5,7 @@ import { createGraphQLError, createSchema, createYoga, - defaultYogaLogger, + createYogaLogger, } from 'graphql-yoga' import { createCustomLogger } from './logging' @@ -30,7 +30,8 @@ describe('logging', () => { describe('default logger', () => { it(`doesn't print debug messages if DEBUG env var isn't set`, () => { jest.spyOn(console, 'debug') - defaultYogaLogger.debug('TEST') + const logger = createYogaLogger() + logger.debug('TEST') // eslint-disable-next-line no-console expect(console.debug).not.toHaveBeenCalled() }) @@ -40,7 +41,8 @@ describe('logging', () => { process.env.DEBUG = '1' // eslint-disable-next-line @typescript-eslint/no-empty-function jest.spyOn(console, 'debug').mockImplementationOnce(() => {}) - defaultYogaLogger.debug('TEST') + const logger = createYogaLogger() + logger.debug('TEST') // eslint-disable-next-line no-console expect(console.debug).toHaveBeenCalled() } finally { diff --git a/packages/graphql-yoga/__tests__/logging.ts b/packages/graphql-yoga/__tests__/logging.ts index b2e3a0157f..c965882877 100644 --- a/packages/graphql-yoga/__tests__/logging.ts +++ b/packages/graphql-yoga/__tests__/logging.ts @@ -16,7 +16,7 @@ const noop = () => {} * utility for creating a conditional logger for tests. */ export const createCustomLogger = ( - logLevel: LogLevel | 'silent' = globalThis?.process.env['DEBUG'] === '1' + logLevel: LogLevel | 'silent' = globalThis.process?.env['DEBUG'] === '1' ? 'debug' : 'info', ) => { diff --git a/packages/graphql-yoga/src/logger.ts b/packages/graphql-yoga/src/logger.ts index 8ee199c446..006671d524 100644 --- a/packages/graphql-yoga/src/logger.ts +++ b/packages/graphql-yoga/src/logger.ts @@ -36,75 +36,93 @@ const LEVEL_COLOR = { reset: ANSI_CODES.reset, } -export interface YogaLogger { - debug: (...args: any[]) => void - info: (...args: any[]) => void - warn: (...args: any[]) => void - error: (...args: any[]) => void +export type LogLevel = 'debug' | 'info' | 'warn' | 'error' + +export type YogaLogger = Record void> + +const logLevelScores: Record = { + debug: 0, + info: 1, + warn: 2, + error: 3, + silent: 4, } -const isDebug = () => !!globalThis.process?.env?.DEBUG +const noop = () => undefined + +export const createYogaLogger = ( + logLevel: LogLevel | 'silent' = globalThis?.process.env['DEBUG'] === '1' + ? 'debug' + : 'info', +): YogaLogger => { + const score = logLevelScores[logLevel] + return { + debug: score > logLevelScores.debug ? noop : debugLog, + info: score > logLevelScores.info ? noop : infoLog, + warn: score > logLevelScores.warn ? noop : warnLog, + error: score > logLevelScores.silent ? noop : errorLog, + } +} const prefix = [LEVEL_COLOR.title, `🧘 Yoga -`, LEVEL_COLOR.reset] -export const defaultYogaLogger: YogaLogger = { - debug(...args: any[]) { - if (isDebug()) { - const fullMessage = [ - `🐛 `, - ...prefix, - LEVEL_COLOR.debug, - ...args, - LEVEL_COLOR.reset, - ] - // Some environments don't have other console methods - if (console.debug) { - console.debug(...fullMessage) - } else { - console.log(...fullMessage) - } - } - }, - info(...args: any[]) { - const fullMessage = [ - `💡 `, - ...prefix, - LEVEL_COLOR.info, - ...args, - LEVEL_COLOR.reset, - ] - if (console.info) { - console.info(...fullMessage) - } else { - console.log(...fullMessage) - } - }, - warn(...args: any[]) { - const fullMessage = [ - `⚠️ `, - ...prefix, - LEVEL_COLOR.warn, - ...args, - LEVEL_COLOR.reset, - ] - if (console.warn) { - console.warn(...fullMessage) - } else { - console.log(...fullMessage) - } - }, - error(...args: any[]) { - const fullMessage = [ - `❌ `, - ...prefix, - LEVEL_COLOR.error, - ...args, - LEVEL_COLOR.reset, - ] - if (console.error) { - console.error(...fullMessage) - } else { - console.log(...fullMessage) - } - }, +const debugLog = (...args: any[]) => { + const fullMessage = [ + `🐛 `, + ...prefix, + LEVEL_COLOR.debug, + ...args, + LEVEL_COLOR.reset, + ] + // Some environments don't have other console methods + if (console.debug) { + console.debug(...fullMessage) + } else { + console.log(...fullMessage) + } +} + +const infoLog = (...args: any[]) => { + const fullMessage = [ + `💡 `, + ...prefix, + LEVEL_COLOR.info, + ...args, + LEVEL_COLOR.reset, + ] + if (console.info) { + console.info(...fullMessage) + } else { + console.log(...fullMessage) + } +} + +const warnLog = (...args: any[]) => { + const fullMessage = [ + `⚠️ `, + ...prefix, + LEVEL_COLOR.warn, + ...args, + LEVEL_COLOR.reset, + ] + if (console.warn) { + console.warn(...fullMessage) + } else { + console.log(...fullMessage) + } +} + +const errorLog = (...args: any[]) => { + const fullMessage = [ + `❌ `, + ...prefix, + LEVEL_COLOR.error, + ...args, + LEVEL_COLOR.reset, + ] + if (console.error) { + console.error(...fullMessage) + } else { + console.log(...fullMessage) + } } diff --git a/packages/graphql-yoga/src/server.ts b/packages/graphql-yoga/src/server.ts index e763eebfab..8d9adb5b45 100644 --- a/packages/graphql-yoga/src/server.ts +++ b/packages/graphql-yoga/src/server.ts @@ -33,7 +33,7 @@ import { processRequest as processGraphQLParams, processResult, } from './process-request.js' -import { defaultYogaLogger, YogaLogger } from './logger.js' +import { createYogaLogger, LogLevel, YogaLogger } from './logger.js' import { CORSPluginOptions, useCORS } from './plugins/useCORS.js' import { useHealthCheck } from './plugins/useHealthCheck.js' import { @@ -78,7 +78,7 @@ export type YogaServerOptions = { * Enable/disable logging or provide a custom logger. * @default true */ - logging?: boolean | YogaLogger + logging?: boolean | YogaLogger | LogLevel /** * Prevent leaking unexpected errors to the client. We highly recommend enabling this in production. * If you throw `EnvelopError`/`GraphQLError` within your GraphQL resolvers then that error will be sent back to the client. @@ -215,15 +215,10 @@ export class YogaServer< this.logger = typeof logger === 'boolean' ? logger === true - ? defaultYogaLogger - : { - /* eslint-disable */ - debug: () => {}, - error: () => {}, - warn: () => {}, - info: () => {}, - /* eslint-enable */ - } + ? createYogaLogger() + : createYogaLogger('silent') + : typeof logger === 'string' + ? createYogaLogger(logger) : logger const maskErrorFn = diff --git a/website/src/pages/v3/features/logging-and-debugging.mdx b/website/src/pages/v3/features/logging-and-debugging.mdx index 79547a6132..22631068b6 100644 --- a/website/src/pages/v3/features/logging-and-debugging.mdx +++ b/website/src/pages/v3/features/logging-and-debugging.mdx @@ -2,21 +2,90 @@ ## Logging -The default logger in Yoga is the [JavaScript console](https://developer.mozilla.org/en-US/docs/Web/API/console) and its respective methods ([debug](https://developer.mozilla.org/en-US/docs/Web/API/console/debug) _when `DEBUG=1` in environment_, [info](https://developer.mozilla.org/en-US/docs/Web/API/console/info), [warn](https://developer.mozilla.org/en-US/docs/Web/API/console/warn) and [error](https://developer.mozilla.org/en-US/docs/Web/API/console/error)) matching the log level. +Yoga uses 4 log levels `debug`, `info`, `warn` and `error`. By default, Yoga will only log `info`, `warn` and `error` messages. + +### Log Level Overview + +#### `error` + +- Only log unexpected errors + +#### `warn` + +- All prior log levels +- deprecation notices + +#### `info` + +- All prior log levels +- Information about the current state of the system + +#### `debug` + +- All prior log levels +- Processing of GraphQL parameters +- Parsing of GraphQL parameters +- Execution or subscription start +- Received GraphQL operation variables +- Execution or subscription end +- [GraphiQL](https://github.com/graphql/graphiql) rendering +- Health checks + +### Enabling Debug Logging + + + Instead, on JavaScript environments that support environment variables via + `process.env` (e.g. Node.js), you can also conditionally enable `debug` + logging by setting the `DEBUG` environment variable to `1`. + + +```ts filename="Custom log level" {7} +import { createYoga } from 'graphql-yoga' +import { createServer } from 'node:http' +import { schema } from './my-schema.js' + +const yoga = createYoga({ + schema, + logging: 'debug' +}) +``` + +### Change log level + +You can customize the log lovel by passing on of the 4 levels to the `logging` option: + + + We recommend not changing the default log level and only recommend changing it + if you know what you are doing. + + +```ts filename="Custom log level" {7} +import { createYoga } from 'graphql-yoga' +import { createServer } from 'node:http' +import { schema } from './my-schema.js' + +const yoga = createYoga({ + schema, + logging: 'warn' +}) +``` + +Specifying the log level of `warn` will only log `warn` and `error` messages being logged. + +### Custom logger You can of course provide your own logger for piping to your favorite logging service/utility: ```ts import { createYoga } from 'graphql-yoga' import { createServer } from 'node:http' -import { schema } from './my-schema' -import { logger } from './my-logger' +import { schema } from './my-schema.js' +import { logger } from './my-logger.js' const yoga = createYoga({ schema, logging: { debug(...args) { - // will only get triggered if DEBUG=1 in environment logger.debug(...args) }, info(...args) { @@ -65,15 +134,3 @@ server.listen(4000, () => { console.info('Server is running on http://localhost:4000/graphql') }) ``` - -## Debugging - -Note that Yoga can run in debug when having `DEBUG=1` in the environment. Doing so will increase verbosity of the logger delivering additional information including: - -- Processing of GraphQL parameters -- Parsing of GraphQL parameters -- Execution or subscription start -- Received GraphQL operation variables -- Execution or subscription end -- [GraphiQL](https://github.com/graphql/graphiql) rendering -- Health checks