CLOSE
Updated on 17 Jun, 202519 mins read 6 views

Problem

Design a logging framework in C++

Requirements

  • Support multiple log levels: INFO, WARNING, ERROR
  • Allow multiple log targets: at minimum, console and file
  • Be easy to extend: new log targets (e.g., network, database) should be added without modifying existing code
  • (Optional) Provide centralized, globally accessible logging (like a singleton)

Design Approach

1. Log Levels

To support multiple log levels in a type-safe and maintainable way, we use an enum class.

enum class LogLevel {
    INFO,
    WARNING,
    ERROR
};
  • This avoids using strings like "INFO" or "ERROR" directly.
  • We can use a switch or if block inside the logger implementation to decide how to prefix messages.

2. Multiple Log Targets (Console, File, etc.)

We apply abstraction and polymorphism to make the system flexible:

Abstract Logger Interface

class Logger {
public:
    virtual void log(LogLevel level, const std::string& message) = 0;
    virtual ~Logger() = default;
};
  • This abstract base class acts as the interface for all logger types.
  • It defines a pure virtual log() function to be implemented by derived classes.

Concrete Implementations

  • ConsoleLogger: Logs messages to standard output.
  • FileLogger: Logs messages to a file using std::ofstream.

Each class will override the log() method with its own behavior, keeping implementation details isolated.

3. Extensibility (Open/Closed Principle)

The use of the Logger interface ensures the system is:

Open for extension, closed for modification

If we want to add:

  • A DatabaseLogger
  • A RemoteAPILogger
  • A BufferedLogger

We just create new subclasses of Logger. The existing code doesn't need to change — we simply plug in the new logger object.

4. Singleton Pattern (Optional Centralized Access)

To allow global, singleton-style access to the logging functionality throughout the application, we create a simple static logger manager:

class GlobalLogger {
    static Logger* logger;

public:
    static void setLogger(Logger* l) {
        logger = l;
    }

    static void log(LogLevel level, const std::string& message) {
        if (logger)
            logger->log(level, message);
    }
};

Logger* GlobalLogger::logger = nullptr;

This avoids passing logger objects around everywhere. You can set the logger once at the start of the application and use it globally via GlobalLogger::log().

Implementation

Folder structure (Clean Layout)

/logger-system
├── Logger.h
├── LogLevel.h
├── ConsoleLogger.h
├── FileLogger.h
├── GlobalLogger.h
├── GlobalLogger.cpp
├── main.cpp

Step 1: Define Log Levels

Create a scoped enum (enum class) to define supported log levels.

// LogLevel.h
#pragma once

enum class LogLevel {
    INFO,
    WARNING,
    ERROR
};

Step 2: Create Logger Interface

Define an abstract base class that all loggers (console, file, etc.) will inherit from.

// Logger.h
#pragma once
#include <string>
#include "LogLevel.h"

class Logger {
public:
    virtual void log(LogLevel level, const std::string& message) = 0;
    virtual ~Logger() = default;
};

Step 3: Implement Console Logger

Implement a concrete logger that prints messages to the terminal.

// ConsoleLogger.h
#pragma once
#include <iostream>
#include "Logger.h"

class ConsoleLogger : public Logger {
public:
    void log(LogLevel level, const std::string& message) override {
        switch (level) {
            case LogLevel::INFO:    std::cout << "[INFO] "; break;
            case LogLevel::WARNING: std::cout << "[WARNING] "; break;
            case LogLevel::ERROR:   std::cout << "[ERROR] "; break;
        }
        std::cout << message << std::endl;
    }
};

Step 4: Implement File Logger

Write logs to a file using std::ofstream.

// FileLogger.h
#pragma once
#include <fstream>
#include "Logger.h"

class FileLogger : public Logger {
    std::ofstream file;

public:
    FileLogger(const std::string& filename) {
        file.open(filename, std::ios::app);
    }

    void log(LogLevel level, const std::string& message) override {
        if (!file.is_open()) return;

        switch (level) {
            case LogLevel::INFO:    file << "[INFO] "; break;
            case LogLevel::WARNING: file << "[WARNING] "; break;
            case LogLevel::ERROR:   file << "[ERROR] "; break;
        }
        file << message << std::endl;
    }

    ~FileLogger() {
        if (file.is_open()) file.close();
    }
};

Step 5: Add Global Logger Manager (Singleton-like)

A static wrapper for centralized access throughout your app.

// GlobalLogger.h
#pragma once
#include "Logger.h"

class GlobalLogger {
    static Logger* logger;

public:
    static void setLogger(Logger* l) {
        logger = l;
    }

    static void log(LogLevel level, const std::string& message) {
        if (logger)
            logger->log(level, message);
    }
};
// GlobalLogger.cpp
#include "GlobalLogger.h"

Logger* GlobalLogger::logger = nullptr;

Step 6: Use the Logger in main()

// main.cpp
#include "ConsoleLogger.h"
#include "FileLogger.h"
#include "GlobalLogger.h"

int main() {
    // Step 1: Use Console Logger
    ConsoleLogger consoleLogger;
    GlobalLogger::setLogger(&consoleLogger);

    GlobalLogger::log(LogLevel::INFO, "Application started");
    GlobalLogger::log(LogLevel::WARNING, "Low memory warning");
    GlobalLogger::log(LogLevel::ERROR, "File not found");

    // Step 2: Switch to File Logger
    FileLogger fileLogger("log.txt");
    GlobalLogger::setLogger(&fileLogger);

    GlobalLogger::log(LogLevel::INFO, "Logging switched to file");
    GlobalLogger::log(LogLevel::ERROR, "Disk read error");

    return 0;
}