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

feat: log level via config #2085

Merged
merged 3 commits into from
Nov 11, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
91 changes: 75 additions & 16 deletions website/src/pages/v3/features/logging-and-debugging.mdx
Original file line number Diff line number Diff line change
@@ -1,22 +1,93 @@
import { Callout } from '@theguild/components'

# Logging and Debugging

## 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 +136,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