codejump
MediumPro challengeJavaScriptTypeScript

Node.js — EventEmitter Access Log

Node.jsBackendEvents

Half of Node's API is built on [`EventEmitter`](https://nodejs.org/api/events.html): servers
emit 'request', streams emit 'data', processes emit 'exit'. tinyserve's access log is a
good place to build your own: the server *records* requests; statistics, alerts or a file writer
*listen*, without the server knowing about them.

Your task

class AccessLog extends EventEmitter — record(entry) receives
{ method, path, status, ms } and emits, in this order:

1. If status is not an integer between 100 and 599: emit 'error' with
new Error('Invalid status: <status>') and stop (no other event).
2. 'request' with the entry.
3. 'slow' with the entry when ms > slowMs (strictly).
4. 'server-error' with the entry when status >= 500.

trackStats(log) — return a stats object kept up to date by listeners:

  • total — number of 'request' events; byStatus — count per status code;
  • slow — paths of the 'slow' events, in order;
  • firstServerError — path of the first 'server-error' only: register it with

once, so the listener removes itself.

The driver records each entry and reports the stats, the error messages it caught with its own
'error' listener (when the test enables one), the message of an error that escaped
record(), and how many 'server-error' listeners remain.

The 'error' event

Emitting 'error' when nobody listens throws the error: an unhandled error should not
vanish. Run the tests with and without the driver's listener to see both behaviours.

Sample tests

Test #1Two normal requests
Input: [[{"ms":12,"path":"/","method":"GET","status":200},{"ms":30,"path":"/about","method":"GET","status":200}],1000,true]
Output: {"stats":{"slow":[],"total":2,"byStatus":{"200":2},"firstServerError":null},"crashed":null,"invalid":[],"serverErrorListeners":1}
Test #2Slow requests and server errors
Input: [[{"ms":12,"path":"/","method":"GET","status":200},{"ms":1500,"path":"/report","method":"GET","status":500},{"ms":20,"path":"/api","method":"POST","status":503},{"ms":2500,"path":"/big","method":"GET","status":200}],1000,true]
Output: {"stats":{"slow":["/report","/big"],"total":4,"byStatus":{"200":2,"500":1,"503":1},"firstServerError":"/report"},"crashed":null,"invalid":[],"serverErrorListeners":0}
Test #3Invalid entry with an error listener
Input: [[{"ms":5,"path":"/","method":"GET","status":200},{"ms":5,"path":"/x","method":"GET","status":999},{"ms":5,"path":"/y","method":"GET","status":404}],1000,true]
Output: {"stats":{"slow":[],"total":2,"byStatus":{"200":1,"404":1},"firstServerError":null},"crashed":null,"invalid":["Invalid status: 999"],"serverErrorListeners":1}
Test #4Invalid entry without listener throws
Input: [[{"ms":5,"path":"/","method":"GET","status":200},{"ms":5,"path":"/x","method":"GET","status":999},{"ms":5,"path":"/y","method":"GET","status":404}],1000,false]
Output: {"stats":{"slow":[],"total":1,"byStatus":{"200":1},"firstServerError":null},"crashed":"Invalid status: 999","invalid":[],"serverErrorListeners":1}