In modern development, logging plays a crucial role in monitoring applications, debugging issues, and keeping track of runtime
events. However, the default console.log can often be limiting, especially when managing different log levels or adding
dynamic prefixes and styles.
One of the common challenges is ensuring that verbose logs, like console.log, don't accidentally make their way into production,
where they can clutter logs, expose unnecessary information, or slow down the application.
Key Objectives for the Logger
- Log Levels: Support multiple log types, such as
log,info,error,warn,debugandtrace. - Dynamic Prefixes: Allow dynamic prefixes for log types, making it easier to filter or group logs by context.
- Customizable Styles: Enable each log type to have its own customizable style, improving visibility in the console.
- Environment-Specific Logging: Only show
console.logand other verbose logs in the development environment, while limiting production logs to errors and warnings. - Prevent
console.login Production: Automatically remove or disableconsole.logand other non-essential logs in production to avoid performance hits or exposing unnecessary information.
Logger Design
The logger is built in TypeScript to ensure type safety and better developer experience, especially when handling various log levels, prefixes, and environment-specific behavior.
TypeScript Setup
-
Type Definitions
type TConsoleType = 'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn'
type TCallback = (...args: any[]) => void
type TCreateParams = {
type: TConsoleType
prefix?: string
style?: string
}
type TLogMethods = {
log: TCallback
error: TCallback
info: TCallback
}
type TInitLogger = TLogMethods & {
name: string
style: string
}
- TConsoleType: Defines the available log types (e.g., log, error, warn).
- TCallback: Represents a generic log function, accepting any number of arguments. - TCreateParams: Parameters for creating a log function, including log type, optional prefix, and optional style. - TLogMethods: Defines the main logging methods (log, error, info). - TInitLogger: Extends the logging methods to include name and style for dynamic prefix and style setting.
-
Logger Initialization Function
The initLogger function initializes the logger instance and manages which log types are enabled in different environments.
const initLogger = (loadTime?: number) => {
let instance: TInitLogger
const ALLOW_PROD: TConsoleType[] = ['error', 'warn']
const LOGS_STYLE: Partial<Record<TConsoleType, string>> = {
log: 'color: blue; font-weight: bold;',
error: 'color: red; font-weight: bold;',
warn: 'color: orange; font-weight: bold;',
info: 'color: green; font-weight: bold;',
trace: 'color: violet; font-weight: bold;'
}
}
ALLOW_PROD: Specifies which log types are allowed in production (error and warn). -LOGS_STYLE: Provides predefined styles for each log type for better console visibility.
-
Log Creation Function
The create function dynamically generates log methods based on the environment (development or production).
It ensures that only errors and warnings are logged in production, while more verbose logs like log or debug are
restricted to development.
const create = ({ type, prefix, style }: TCreateParams) => {
if (__SERVER__) {
return console[type]?.bind(console, ...[`${prefix ?? type}::`])
}
if (__DEV__) {
return console[type]?.bind(
console,
...[
`%c${prefix ?? type}%c`,
style ?? LOGS_STYLE[type] ?? 'font-weight: bold;',
''
]
)
}
if (ALLOW_PROD.includes(type)) {
prefix = ''
return console[type]?.bind(console)
}
return (..._args: any[]) => undefined
}
In development, all log types are available, and each can be styled dynamically. In production, only error and warning logs are enabled. Logs like log, info, debug, etc., are suppressed
-
Create Instance
The logger instance is lazily initialized and allows setting customname(prefix) andstylefor logs.
return function () {
if (!instance) {
instance = {
log: create({ type: 'log' }),
info: create({ type: 'info' }),
error: create({ type: 'error' }),
set name(prefix: string) {
instance.log = create({ type: 'log', prefix })
instance.error = create({ type: 'error', prefix })
},
set style(style: string) {
instance.log = create({ type: 'log', style })
instance.error = create({ type: 'error', style })
}
}
}
return instance
}
name: Allows setting a dynamic prefix for log messages, which is useful for grouping or identifying logs from specific parts of the application. -style: Allows customizing the visual style of log messages.
-
Exporting the Logger
The logger is initialized with the current timestamp and exported as a default object.
const Logger = initLogger(Date.now())()
export { Logger as default }
This allows you to import and use Logger across your application, ensuring that logs are controlled based on the environment.
Use in a Next.js or TypeScript Project
This logger fits well into a Next.js project for several reasons:
**Next.js Server-Side Rendering (SSR): **The logger can differentiate between server-side and client-side environments
(__SERVER__ vs. __DEV__), making it suitable for applications with SSR where logging needs to be controlled
more strictly.
**TypeScript Support: **The logger is built with TypeScript, providing a safe and reliable development experience by preventing runtime errors and offering IntelliSense for log types and arguments.
No Accidental Logs in Production: One of the key features of this logger is its ability to automatically disable non-essential logs in production, ensuring that only important logs (like errors and warnings) make it through. This makes it ideal for applications where performance and clean log output are essential.
Full functional code
You can easily integrate this logger into your existing projects. Feel free to further customize it by adding additional log levels, methods for saving logs to files or remote servers, or features like log aggregation for error monitoring in production environments.
type TConsoleType = 'debug' | 'error' | 'info' | 'log' | 'trace' | 'warn' type TCallback = (...args: any[]) => void type TCreateParams = { type: TConsoleType prefix?: string style?: string } type TLogMethods = { log: TCallback error: TCallback info: TCallback } type TInitLogger = TLogMethods & { name: string style: string } const initLogger = (loadTime?: number) => { let instance: TInitLogger const ALLOW_PROD: TConsoleType[] = ['error', 'warn'] const LOGS_STYLE: Partial<Record<TConsoleType, string>> = { log: 'color: blue; font-weight: bold;', error: 'color: red; font-weight: bold;', warn: 'color: orange; font-weight: bold;', info: 'color: green; font-weight: bold;', trace: 'color: violet; font-weight: bold;' } /* eslint-disable no-console */ console.log('loadTime', loadTime) const create = ({ type, prefix, style }: TCreateParams) => { if (__SERVER__) return console[type]?.bind(console, ...[`${prefix ?? type}::`]) if (__DEV__) return console[type]?.bind( console, ...[ `%c${prefix ?? type}%c`, style ?? LOGS_STYLE[type] ?? 'font-weight: bold;', '' ] ) if (ALLOW_PROD.includes(type)) { prefix = '' return console[type]?.bind(console) } return (..._args: any[]) => undefined } /* eslint-enable no-console */ return function () { if (!instance) { instance = { log: create({ type: 'log' }), info: create({ type: 'info' }), error: create({ type: 'error' }), set name(prefix: string) { instance.log = create({ type: 'log', prefix }) instance.error = create({ type: 'error', prefix }) }, set style(style: string) { instance.log = create({ type: 'log', style }) instance.error = create({ type: 'error', style }) } } } return instance } } const Logger = initLogger(Date.now())() export { Logger as default }
Conclusion
This custom logger is a robust, flexible solution for managing logs in TypeScript or Next.js projects. It solves the problem of accidentally leaving console.log statements in production, allowing developers to focus on meaningful log output during development while maintaining a clean and efficient log output in production.
With features like dynamic log prefixes, customizable styles, and environment-specific logging behavior, this logger is an essential tool for any modern JavaScript project that needs enhanced logging functionality without sacrificing performance or security in production.