Machine Coding Problem

Logger Library

maco30maco60macoAllinfrastructurestrategy-formatter-&-asynchronous-composite-sinks
Commonly Asked By:GoogleAmazonSplunkMicrosoft

Requirements & System Scope

Functional Scope (In-Scope)

  • Pluggable LogLevels: Support flexible severity levels (DEBUG, INFO, WARN, ERROR, FATAL) systematically.
  • Multiple Sinks Output: Enable swappable output sinks (Console, local File log records, Remote database logs) polymorphically.
  • Non-Blocking Async Buffer Queues: Call logs in non-blocking manner using bounded consumer writing queues.
  • Extensible Formatting: Strategy formats supporting JSON as well as Text formatting configurations.
  • Graceful Shutdown Flushing: Drain log buffers completely on execution completion before shutdown.

Explicit Boundaries (Out-of-Scope)

  • No Real Network Call Sockets: Remote writing steps utilize mock endpoints rather than raw socket transports.
  • No Complex Log Rotation Scheduling: Overwrite/cleanup strategies for local logs are out-of-scope.

Class Diagram & Entity Relationships

Structural blueprint showing entities and logging sinks:

Loading...
  • Polymorphic Log Sinks: Custom output sinks implement a shared LogSink contract, decoupling the output destination from the core logger thread.
  • Isolated Async Coordinator: AsyncLogManager houses bounded execution queues, protecting developer threads from slow I/O writes.

Design Patterns & SOLID Principles

  • Strategy Pattern (Formatting Strategy): Decouples log formats (JSON vs standard Text) from log targets, satisfying the Open-Closed Principle (OCP).
  • Composite & Observer Pattern (Output Delivery): By maintaining collections of output targets behind interfaces, loggers distribute records to multiple channels seamlessly without concrete coupling.
  • Single Responsibility Principle (SRP):Logger enforces filtering. LogRecord formats data fields. AsyncLogManager directs thread queues.

Core Execution Workflows

Asynchronous Writing Sequence

  1. Caller triggers logging event: logger.info("Transaction processed").
  2. Evaluate level threshold. If level meets parameters, create new formatted LogRecord.
  3. Offer record to async buffer queue:
    1. If queue is full, write alert to backup streams or drop the record to maintain availability.
    2. Otherwise, append the entry and instantly return (non-blocking).
  4. The dedicated background thread pulls records from queue continuously, flushing formatted data to each registered output sink.

Concurrency & Thread Safety Strategy

Because multiple threads execute logging commands simultaneously in high-traffic software systems, raw shared log structures will cause output corruption:

  • Bounded Blocking Queues: Buffer operations in thread-safe LinkedBlockingQueue containers, protecting array boundaries.
  • Immutable Log Records: Construct records as read-only variables once initialized, avoiding write clashes.

Complete Clean Code Blueprint

Thread-safe async logging setups in Java and Python:

// โ”€โ”€โ”€ JAVA BLUEPRINT โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
import java.util.*;
import java.util.concurrent.*;

enum LogLevel { DEBUG, INFO, WARN, ERROR, FATAL }

class LogRecord {
    private final long timestamp;
    private final LogLevel level;
    private final String message;
    private final String threadName;
    private final String loggerName;

    public LogRecord(String loggerName, LogLevel level, String message) {
        this.timestamp = System.currentTimeMillis();
        this.loggerName = loggerName;
        this.level = level;
        this.message = message;
        this.threadName = Thread.currentThread().getName();
    }

    public long getTimestamp() { return timestamp; }
    public LogLevel getLevel() { return level; }
    public String getMessage() { return message; }
    public String getThreadName() { return threadName; }
    public String getLoggerName() { return loggerName; }
}

interface LogFormatter {
    String format(LogRecord record);
}

class TextFormatter implements LogFormatter {
    @Override
    public String format(LogRecord record) {
        return String.format("[%d] [%s] [%s] [%s] - %s",
                record.getTimestamp(),
                record.getLevel(),
                record.getLoggerName(),
                record.getThreadName(),
                record.getMessage());
    }
}

class JsonFormatter implements LogFormatter {
    @Override
    public String format(LogRecord record) {
        return String.format("{"timestamp":%d, "level":"%s", "logger":"%s", "thread":"%s", "message":"%s"}",
                record.getTimestamp(),
                record.getLevel(),
                record.getLoggerName(),
                record.getThreadName(),
                record.getMessage().replace(""", "\""));
    }
}

interface LogSink {
    void write(LogRecord record, LogFormatter formatter);
}

class ConsoleSink implements LogSink {
    @Override
    public void write(LogRecord record, LogFormatter formatter) {
        System.out.println("[Console] " + formatter.format(record));
    }
}

class FileSink implements LogSink {
    private final List<String> lines = new CopyOnWriteArrayList<>();
    @Override
    public void write(LogRecord record, LogFormatter formatter) {
        String logLine = formatter.format(record);
        lines.add(logLine);
        // Simulate writing to physical disk
        System.out.println("[FileSink Written] " + logLine);
    }
}

class AsyncLogManager {
    private final BlockingQueue<LogRecord> queue = new LinkedBlockingQueue<>(1000);
    private final List<LogSink> sinks = new CopyOnWriteArrayList<>();
    private final ExecutorService executor = Executors.newSingleThreadExecutor();
    private final LogFormatter formatter;
    private volatile boolean running = true;

    public AsyncLogManager(LogFormatter formatter) {
        this.formatter = formatter;
        executor.submit(this::processLogs);
    }

    public void addSink(LogSink sink) { sinks.add(sink); }

    public void log(LogRecord record) {
        if (!running) return;
        if (!queue.offer(record)) {
            System.err.println("Logger buffer overflow! Log message dropped: " + record.getMessage());
        }
    }

    private void processLogs() {
        while (running || !queue.isEmpty()) {
            try {
                LogRecord record = queue.poll(100, TimeUnit.MILLISECONDS);
                if (record != null) {
                    for (LogSink sink : sinks) {
                        sink.write(record, formatter);
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                break;
            }
        }
    }

    public void shutdown() {
        running = false;
        executor.shutdown();
        try {
            executor.awaitTermination(2, TimeUnit.SECONDS);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
    }
}

class Logger {
    private final String name;
    private final LogLevel minLevel;
    private final AsyncLogManager manager;

    public Logger(String name, LogLevel minLevel, AsyncLogManager manager) {
        this.name = name;
        this.minLevel = minLevel;
        this.manager = manager;
    }

    public void log(LogLevel level, String message) {
        if (level.ordinal() >= minLevel.ordinal()) {
            manager.log(new LogRecord(name, level, message));
        }
    }

    public void debug(String msg) { log(LogLevel.DEBUG, msg); }
    public void info(String msg) { log(LogLevel.INFO, msg); }
    public void warn(String msg) { log(LogLevel.WARN, msg); }
    public void error(String msg) { log(LogLevel.ERROR, msg); }
    public void fatal(String msg) { log(LogLevel.FATAL, msg); }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== RUNNING LOGGER JAVA SIMULATION ===");
        LogFormatter textFormat = new TextFormatter();
        AsyncLogManager manager = new AsyncLogManager(textFormat);
        manager.addSink(new ConsoleSink());
        manager.addSink(new FileSink());

        Logger sysLog = new Logger("SystemController", LogLevel.INFO, manager);

        // Spawn multiple concurrent logging threads
        ExecutorService concurrentPublishers = Executors.newFixedThreadPool(3);
        for (int i = 1; i <= 3; i++) {
            final int id = i;
            concurrentPublishers.submit(() -> {
                sysLog.info("Log message from thread agent #" + id);
                sysLog.debug("This is a debug log (should be ignored by INFO minimum level setup)");
                sysLog.warn("Warning trace from thread agent #" + id);
            });
        }

        concurrentPublishers.shutdown();
        concurrentPublishers.awaitTermination(1, TimeUnit.SECONDS);

        Thread.sleep(500); // Wait for background worker processing
        manager.shutdown();
    }
}

๐Ÿ’ฌReview

Help Us Improve

How helpful was this walkthrough?

Click a star to rate. We actively use this feedback to refine and update our system design content.

Placeholder
Optional but highly appreciated!

Discussion

Share your thoughts, ask questions, or help others.

Loading comments...