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 loggernameits own level and returnstrue. 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. Returnsfalseand changes nothing for an unknown level, or for"INHERIT"on"root".effectiveLevel(name): the level that applies toname: its own if it has one, otherwise the nearest ancestor's.addAppender(appender, logger, threshold, format): creates an appender calledappender, attached to loggerlogger, that only writes records atthresholdor above, informat("plain"or"json"). Returnstrue. Returnsfalseand creates nothing if the appender name is taken, or the threshold or format is unknown.removeAppender(appender): detaches it and forgets its lines. Returnstrue, orfalseif 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: iflevelis below the logger's effective level, nothing is written and the result is0. Otherwise the record goes to the appenders attached tologger, then to those attached to its parent, and so on up to"root". Each of those appenders writes it only iflevelis at or above its own threshold. An unknownlevelreturns-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 example7 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.