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
orif
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;
}