Requirements for the Logging Framework
Functional Requirements
Let's define exactly what our Logging Framework must do. Each requirement will directly drive a class, method, or design decision in our final system.
-
R1: Support Multiple Log Levels — The framework shall support at least five severity levels in increasing order:
DEBUG,INFO,WARN,ERROR, andFATAL. Each log message is tagged with exactly one level. This hierarchy is essential for filtering — a level ordering allows us to say "show me everything at WARN or above." -
R2: Filter Messages by Minimum Level — The Logger shall have a configurable minimum log level. Messages below this threshold are silently discarded. For example, if the minimum level is
WARN, thenDEBUGandINFOmessages are ignored, whileWARN,ERROR, andFATALmessages are processed. This must be changeable at runtime without restarting the application. -
R3: Dispatch to Multiple Sinks Simultaneously — When a log message passes the level filter, it shall be delivered to all registered sinks (output destinations). A single
log(WARN, "Disk space low")call might write to the console, append to a file, and insert into a database — all in the same invocation. -
R4: Support Console Sink — The framework shall provide a built-in
ConsoleSinkthat writes formatted log messages to standard output (stdout). The output format shall include the timestamp, log level, and message text. -
R5: Support File Sink — The framework shall provide a built-in
FileSinkthat appends log messages to a specified file path. Each message is written as a new line. The sink is responsible for opening, writing, and closing the file handle appropriately. -
R6: Support Database Sink — The framework shall provide a built-in
DatabaseSinkthat persists log messages to a storage backend (simulated for this exercise). Each log entry is stored with its timestamp, level, and message. -
R7: Pluggable Sink Architecture — Adding a new sink (e.g., Slack notifications, HTTP webhook, CloudWatch) shall require only implementing a new class that conforms to the
LogSinkinterface. No existing code — not the Logger, not the existing sinks — should need modification. This is the Open/Closed Principle in practice. -
R8: Global Logger Access (Singleton) — The application shall access the Logger through a single, globally shared instance. Creating multiple Logger instances would lead to fragmented log output and inconsistent level configuration. The Singleton Pattern ensures exactly one Logger exists.
-
R9: Timestamped Log Entries — Every log message shall be automatically enriched with a timestamp at the moment it's logged. The application code should pass only the level and message; the framework adds metadata.
-
R10: Dynamic Sink Registration — Sinks shall be registerable and removable at runtime. This allows the application to start with console logging, then add file logging after configuration is loaded, and optionally add database logging in production environments.
Non-Functional Requirements
Beyond the core functionality, our design should address:
-
Extensibility (Open/Closed) — The single most important non-functional requirement. The system must be designed so that adding a new log sink never requires touching the Logger class or any existing sink. This is achieved through the
LogSinkinterface — new sinks implement it, and the Logger dispatches to all registered sinks polymorphically. -
Thread Safety — In real-world applications, multiple threads log simultaneously. While we won't build a fully concurrent implementation in this Easy-level problem, the design should be aware of thread safety. The Singleton's instance creation should be thread-safe, and sink dispatch should not corrupt shared state.
-
Testability — Each component should be testable in isolation. We should be able to create a mock sink that captures log messages in memory for assertion, without needing a real file or database. The interface-based sink design makes this straightforward.
-
Minimal Performance Overhead — Logging happens on every significant operation in a system. The level check (R2) should be O(1) — a simple integer comparison, not a string lookup. Messages below the threshold should be discarded as cheaply as possible.
-
Separation of Concerns — The Logger is responsible for level filtering and sink dispatch. Individual sinks are responsible for formatting and writing. The
LogMessage(orLogEntry) object is responsible for carrying metadata. No class should have multiple responsibilities.
Scope Clarification
In Scope:
- Log level hierarchy and filtering (DEBUG < INFO < WARN < ERROR < FATAL)
- Three built-in sinks: Console, File, Database
- Pluggable sink architecture via interface
- Singleton Logger with global access
- Dynamic sink registration and removal
- Timestamped log entries
Out of Scope:
- Log rotation and archival — managing file sizes, compressing old logs, and deleting expired files is an operational concern, not an OOP design concern
- Structured logging (JSON format) — while valuable in production, it's an output formatting detail, not a structural design decision
- Asynchronous logging — buffering messages in a queue and writing them in a background thread is a performance optimization that adds significant concurrency complexity. Our design supports async sinks (a sink could buffer internally), but we won't mandate it
- Remote transport (TCP/UDP/HTTP) — sending logs over the network to services like Datadog or the ELK stack is a sink implementation detail, not a framework design concern. Our pluggable architecture makes adding a
HttpSinktrivial - Log context / MDC — thread-local context (like request IDs) is useful but beyond the scope of this Easy-level problem
- Configuration file parsing — reading log levels and sink configuration from a YAML/JSON file is an integration concern, not a design concern
Requirement-to-Design Traceability
Before we start designing, let's preview how each requirement maps to a design element. This traceability ensures nothing is lost between requirements and implementation:
| Requirement | Design Element |
|---|---|
| R1: Multiple log levels | LogLevel enum (DEBUG, INFO, WARN, ERROR, FATAL) |
| R2: Filter by minimum level | Logger.minLevel attribute + comparison in log() method |
| R3: Dispatch to multiple sinks | Logger holds a list of LogSink references — iterates and calls each |
| R4: Console sink | ConsoleSink class implementing LogSink interface |
| R5: File sink | FileSink class implementing LogSink interface |
| R6: Database sink | DatabaseSink class implementing LogSink interface |
| R7: Pluggable architecture | LogSink interface with single write(LogMessage) method |
| R8: Global access | Logger as Singleton with getInstance() |
| R9: Timestamps | LogMessage data class with timestamp, level, message fields |
| R10: Dynamic registration | Logger.addSink() and Logger.removeSink() methods |
Every requirement has a home. Every class we design can justify its existence by pointing to a requirement.
Interview Tip
In a real interview, spend 3-5 minutes clarifying requirements before jumping into design. For a Logging Framework, the clarifying questions that impress interviewers are:
- "Should the logger support multiple independent instances (like named loggers in Log4j), or a single global instance?" — This decides whether Singleton is appropriate. For this problem, we chose Singleton for simplicity.
- "Should log messages be dispatched synchronously or asynchronously?" — Synchronous is simpler but blocks the calling thread. This reveals whether the interviewer wants a concurrency discussion.
- "Should sinks be configurable per log level (e.g., ERROR goes to database, DEBUG only to console)?" — This is the Chain of Responsibility angle. Our design supports this through per-sink level filtering.
- "Is thread safety required? Will multiple threads log simultaneously?" — This scopes the concurrency work. For an Easy-level problem, basic thread safety on the Singleton is sufficient.
These questions show the interviewer that you think about design trade-offs before writing code — a hallmark of senior engineering thinking.