Logging Framework

Build the core of a logging framework. Loggers form a tree by name, so "app.db" is a child of "app", which is a child of "root". Each logger may have its own level or inherit one. Appenders collect the lines, and each appender has its own threshold and its own format. The log call itself must not contain an if (format === ...) ladder, and adding a third format later must not mean editing the two that already work. This is Strategy for the formatter and Observer for the appender list.

Implement the class LogManager:

  • LogManager(): starts with one logger, "root", at level "INFO", and no appenders. Any other logger springs into existence the first time its name is used.
  • setLevel(name, level): gives logger name its own level and returns true. The levels in order are "DEBUG", "INFO", "WARN", "ERROR". The special level "INHERIT" removes the logger's own level, so it uses its parent's again. Returns false and changes nothing for an unknown level, or for "INHERIT" on "root".
  • effectiveLevel(name): the level that applies to name: its own if it has one, otherwise the nearest ancestor's.
  • addAppender(appender, logger, threshold, format): creates an appender called appender, attached to logger logger, that only writes records at threshold or above, in format ("plain" or "json"). Returns true. Returns false and creates nothing if the appender name is taken, or the threshold or format is unknown.
  • removeAppender(appender): detaches it and forgets its lines. Returns true, or false if there is no such appender. The name can be used again afterwards.
  • log(logger, level, message, time): returns how many appenders wrote the record. First the level check: if level is below the logger's effective level, nothing is written and the result is 0. Otherwise the record goes to the appenders attached to logger, then to those attached to its parent, and so on up to "root". Each of those appenders writes it only if level is at or above its own threshold. An unknown level returns -1.
  • lines(appender): every line the appender has written, oldest first. An unknown appender has no lines: [].

The two formats

  • "plain": <time> <LEVEL> <logger> - <message>, for example 7 WARN app.db - slow query.
  • "json": JSON.stringify({time, level, logger, message}) with the keys in exactly that order and no spaces, for example {"time":7,"level":"WARN","logger":"app.db","message":"slow query"}. Quotes inside the message are escaped as in any JSON.

Notes

  • A name has dots only as separators. The parent of "a.b.c" is "a.b", the parent of "a" is "root", and "root" has no parent.
  • An appender's threshold does not change the logger's level, and the logger's level does not change an appender's threshold. Both must pass.
  • Time is passed in, never read from a clock.
  • Return values must be exactly the types above.

Examples

Example 1

Input: ["LogManager","addAppender","log","log","lines"]
       [[],["mem","root","DEBUG","plain"],["app","DEBUG","noise",1],["app","INFO","hello",2],["mem"]]
Output: [null,true,0,1,["2 INFO app - hello"]]

The root level is INFO, so the DEBUG call is dropped before any appender sees it. The INFO call is written in plain format.

Example 2

Input: ["LogManager","setLevel","effectiveLevel","effectiveLevel","effectiveLevel"]
       [[],["app","WARN"],["app"],["app.db"],["other"]]
Output: [null,true,"WARN","WARN","INFO"]

app.db has no level of its own, so it asks app, which says WARN. other has no ancestor with a level, so it falls through to root's INFO.

Example 3

Input: ["LogManager","addAppender","log","lines"]
       [[],["j","root","DEBUG","json"],["api","ERROR","say \"hi\"",5],["j"]]
Output: [null,true,1,["{\"time\":5,\"level\":\"ERROR\",\"logger\":\"api\",\"message\":\"say \\\"hi\\\"\"}"]]

JSON format has the keys in a fixed order and escapes quotes inside the message.

Each test creates the class with the first operation, then calls the methods in order. Output lists what each call returns; null marks the constructor and methods that return nothing, and their return values aren't checked.

class LogManager {
  constructor() {
    
  }

  /** Give a logger its own level, or "INHERIT" to remove it. @return {boolean} false for an unknown level, or INHERIT on "root" */
  setLevel(name, level) {
    
  }

  /** @return {string} the level that applies to this logger: its own, or the nearest ancestor's */
  effectiveLevel(name) {
    
  }

  /** Create a named appender on a logger. @return {boolean} false if the name is taken or the threshold or format is unknown */
  addAppender(appender, logger, threshold, format) {
    
  }

  /** @return {boolean} true if the appender existed */
  removeAppender(appender) {
    
  }

  /** @return {number} how many appenders wrote the record; 0 if the level is off; -1 for an unknown level */
  log(logger, level, message, time) {
    
  }

  /** @return {string[]} the lines this appender has written, oldest first */
  lines(appender) {
    
  }
}

Ctrl/⌘ + ' runs the examples, Ctrl/⌘ + Enter submits. Use console.log freely: output shows under Console. In the editor, press Esc then Tab to move focus out.