Frontend Logger: Dev/Prod Mode Implementation Guide

by Alex Johnson 52 views

In modern web development, logging plays a crucial role in debugging and monitoring applications. However, excessive logging in production can lead to performance issues and clutter the console with unnecessary information. This article will guide you through implementing a frontend logger that intelligently switches between development and production modes, ensuring that logs are only displayed when needed. This approach enhances the debugging experience during development while maintaining a clean and efficient production environment.

Why a Frontend Logger with Dev/Prod Switching?

Implementing a frontend logger with development and production switching offers several key advantages. Let's explore why this approach is beneficial for modern web applications.

Avoid Noisy Console Output in Production

In production environments, the console should remain clean and free from debugging messages. A frontend logger with dev/prod switching ensures that verbose log messages are suppressed in production, preventing unnecessary clutter and potential security vulnerabilities. By selectively disabling logging in production, you maintain a cleaner console, making it easier to identify and address critical issues without being overwhelmed by irrelevant information.

Keep UI Components Clean from Environment Checks

Embedding environment checks directly within UI components can lead to code duplication and make the codebase harder to maintain. A centralized logging module abstracts these checks away from the components, resulting in cleaner and more readable code. This abstraction promotes a separation of concerns, allowing developers to focus on the UI logic without being distracted by environment-specific configurations.

Centralize Logging Logic for Future Extensions

A centralized logging module provides a single point of control for all logging-related functionality. This makes it easier to introduce new features, such as debug levels or remote logging, without modifying individual components. Centralization also simplifies the process of updating logging behavior, ensuring consistency across the application. By consolidating the logging logic, you create a flexible and scalable system that can adapt to future requirements.

Scope of Implementation

To implement the frontend logger with dev/prod switching, we will focus on creating a module that encapsulates the logging functionality. This module will include methods for different log levels and handle the switching behavior based on the environment.

Adding frontend/src/Presentation/utils/log.ts

This module will contain the core logic for the logger. It will include the following components:

  • createLogger(isProd: boolean): This function will produce a no-op logger in production mode. In development mode, it will return a logger that delegates to the console.
  • Shared log instance using import.meta.env.PROD: This instance will be used throughout the application to log messages.
  • Methods: error, warn, info, debug: These methods will correspond to the different log levels and will be used to log messages accordingly.
  • Dev mode delegates to console.* with a [Daylog] prefix: In development mode, the logger will delegate to the console object, prefixing each message with [Daylog] for easy identification.

Adding frontend/src/Presentation/utils/log.test.ts

This module will contain the unit tests for the logger. It will cover the following scenarios:

  • Dev mode: Verify that all console methods are called with the correct prefix.
  • Prod mode: Verify that all logger methods are silent (i.e., no console calls are made).

Detailed Implementation Steps

Now, let's dive into the detailed steps for implementing the frontend logger with dev/prod switching. We'll cover the code structure, the logic for handling different environments, and the testing strategy.

Creating the Logger Module (log.ts)

First, we'll create the log.ts module, which will contain the core logic for the logger. This module will include the createLogger function and the shared log instance.

// frontend/src/Presentation/utils/log.ts

interface Logger {
 error: (message: any, ...args: any[]) => void;
 warn: (message: any, ...args: any[]) => void;
 info: (message: any, ...args: any[]) => void;
 debug: (message: any, ...args: any[]) => void;
}

function createLogger(isProd: boolean): Logger {
 if (isProd) {
 return {
 error: () => {},
 warn: () => {},
 info: () => {},
 debug: () => {},
 };
 } else {
 const prefix = '[Daylog]';
 return {
 error: (message: any, ...args: any[]) => console.error(prefix, message, ...args),
 warn: (message: any, ...args: any[]) => console.warn(prefix, message, ...args),
 info: (message: any, ...args: any[]) => console.info(prefix, message, ...args),
 debug: (message: any, ...args: any[]) => console.debug(prefix, message, ...args),
 };
 }
}

const log: Logger = createLogger(import.meta.env.PROD);

export default log;

In this code:

  • We define a Logger interface with methods for error, warn, info, and debug.
  • The createLogger function takes a boolean isProd parameter, which indicates whether the application is running in production mode.
  • In production mode, the function returns a no-op logger, where all methods are empty functions.
  • In development mode, the function returns a logger that delegates to the console object, prefixing each message with [Daylog].
  • We create a shared log instance using import.meta.env.PROD to determine the environment.

Writing Unit Tests (log.test.ts)

Next, we'll create the log.test.ts module, which will contain the unit tests for the logger. These tests will ensure that the logger behaves correctly in both development and production modes.

// frontend/src/Presentation/utils/log.test.ts

import log from './log';

describe('log', () => {
 let consoleErrorSpy: jest.SpyInstance;
 let consoleWarnSpy: jest.SpyInstance;
 let consoleInfoSpy: jest.SpyInstance;
 let consoleDebugSpy: jest.SpyInstance;

 beforeEach(() => {
 consoleErrorSpy = jest.spyOn(console, 'error').mockImplementation();
 consoleWarnSpy = jest.spyOn(console, 'warn').mockImplementation();
 consoleInfoSpy = jest.spyOn(console, 'info').mockImplementation();
 consoleDebugSpy = jest.spyOn(console, 'debug').mockImplementation();
 });

 afterEach(() => {
 consoleErrorSpy.mockRestore();
 consoleWarnSpy.mockRestore();
 consoleInfoSpy.mockRestore();
 consoleDebugSpy.mockRestore();
 });

 it('should log messages with prefix in development mode', () => {
 // Mock import.meta.env.PROD to be false (development mode)
 const originalProd = import.meta.env.PROD;
 import.meta.env.PROD = false;
 const log = require('./log').default; // Import the module again to apply the mock

 log.error('test error');
 expect(consoleErrorSpy).toHaveBeenCalledWith('[Daylog]', 'test error');

 log.warn('test warn');
 expect(consoleWarnSpy).toHaveBeenCalledWith('[Daylog]', 'test warn');

 log.info('test info');
 expect(consoleInfoSpy).toHaveBeenCalledWith('[Daylog]', 'test info');

 log.debug('test debug');
 expect(consoleDebugSpy).toHaveBeenCalledWith('[Daylog]', 'test debug');

 // Restore the original value of import.meta.env.PROD
 import.meta.env.PROD = originalProd;
 });

 it('should not log messages in production mode', () => {
 // Mock import.meta.env.PROD to be true (production mode)
 const originalProd = import.meta.env.PROD;
 import.meta.env.PROD = true;
 const log = require('./log').default; // Import the module again to apply the mock

 log.error('test error');
 expect(consoleErrorSpy).not.toHaveBeenCalled();

 log.warn('test warn');
 expect(consoleWarnSpy).not.toHaveBeenCalled();

 log.info('test info');
 expect(consoleInfoSpy).not.toHaveBeenCalled();

 log.debug('test debug');
 expect(consoleDebugSpy).not.toHaveBeenCalled();

 // Restore the original value of import.meta.env.PROD
 import.meta.env.PROD = originalProd;
 });
});

In this code:

  • We use Jest to write unit tests.
  • We mock the console methods (error, warn, info, debug) to spy on their calls.
  • We have two test cases: one for development mode and one for production mode.
  • In development mode, we verify that all console methods are called with the correct prefix.
  • In production mode, we verify that no console methods are called.
  • We use import.meta.env.PROD to mock the environment variable for testing purposes. Note that mocking import.meta.env requires re-importing the module to apply the mock.

Integrating the Logger in UI Components

With the logger module created and tested, you can now integrate it into your UI components. To use the logger, simply import the log instance and call the appropriate method.

// frontend/src/Presentation/components/MyComponent.tsx

import React, { useEffect } from 'react';
import log from '../utils/log';

const MyComponent: React.FC = () => {
 useEffect(() => {
 log.info('MyComponent mounted');

 return () => {
 log.info('MyComponent unmounted');
 };
 }, []);

 const handleClick = () => {
 log.debug('Button clicked');
 try {
 // Some code that might throw an error
 throw new Error('Test error');
 } catch (error: any) {
 log.error('An error occurred:', error);
 }
 };

 return (
 <button onClick={handleClick}>Click me</button>
 );
};

export default MyComponent;

In this example:

  • We import the log instance from the ../utils/log module.
  • We use the log.info method to log messages when the component mounts and unmounts.
  • We use the log.debug method to log a message when a button is clicked.
  • We use the log.error method to log an error message if an error occurs.

Acceptance Criteria

To ensure that the implementation meets the requirements, we need to verify the following acceptance criteria.

UI Components Can Import log and Call log.error() Safely in Any Environment

UI components should be able to import the log instance and call the log.error() method without causing any errors. This ensures that the logger is accessible and usable throughout the application.

No Console Output Appears in Production Bundle

When the application is built for production, no console output should be generated by the logger. This prevents unnecessary clutter and potential security vulnerabilities in the production environment.

Tests Fully Cover Both Development and Production Behaviors

The unit tests should fully cover both development and production behaviors of the logger. This ensures that the logger functions correctly in all environments and that any future changes do not introduce regressions.

Conclusion

Implementing a frontend logger with dev/prod switching is a crucial step in building robust and maintainable web applications. By centralizing logging logic and selectively disabling logging in production, you can improve the debugging experience during development and maintain a clean and efficient production environment. The provided code examples and guidelines will help you implement a logger that meets your specific needs and enhances the overall quality of your application.

For more information on logging best practices, you can visit **[this external resource on logging](https://owasp.org/www-project-top-ten/