Skip to content

Commit

Permalink
feat: log level via config
Browse files Browse the repository at this point in the history
  • Loading branch information
n1ru4l committed Nov 11, 2022
1 parent 4e7d58e commit 3d224ca
Show file tree
Hide file tree
Showing 5 changed files with 169 additions and 97 deletions.
8 changes: 5 additions & 3 deletions packages/graphql-yoga/__tests__/logging.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import {
createGraphQLError,
createSchema,
createYoga,
defaultYogaLogger,
createYogaLogger,
} from 'graphql-yoga'
import { createCustomLogger } from './logging'

Expand All @@ -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()
})
Expand All @@ -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 {
Expand Down
2 changes: 1 addition & 1 deletion packages/graphql-yoga/__tests__/logging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
) => {
Expand Down
150 changes: 84 additions & 66 deletions packages/graphql-yoga/src/logger.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<LogLevel, (...args: any[]) => void>

const logLevelScores: Record<LogLevel | 'silent', number> = {
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)
}
}
17 changes: 6 additions & 11 deletions packages/graphql-yoga/src/server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -78,7 +78,7 @@ export type YogaServerOptions<TServerContext, TUserContext> = {
* 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.
Expand Down Expand Up @@ -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 =
Expand Down
89 changes: 73 additions & 16 deletions website/src/pages/v3/features/logging-and-debugging.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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

<Callout>
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`.
</Callout>

```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:

<Callout>
We recommend not changing the default log level and only recommend changing it
if you know what you are doing.
</Callout>

```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) {
Expand Down Expand Up @@ -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

0 comments on commit 3d224ca

Please sign in to comment.