Repository: nodejs/node
Pull request: diagnostics_channel: capture console messages

I've added diagnostics_channel support to capture and modify inputs to the main console methods. This enables some useful things like APMs injecting tracing data into log messages to correlate log messages to the requests from which they originated. You can also replace the args list entirely, so you could apply your own custom formatting to produce JSON logs instead or things like that.

cc @nodejs/diagnostics @nodejs/console 

Changed files:
filename: doc/api/diagnostics_channel.md
patch: @@ -1121,6 +1121,43 @@ While the diagnostics\_channel API is now considered stable, the built-in
 channels currently available are not. Each channel must be declared stable
 independently.
 
+#### Console
+
+`console.log`
+
+* `args` {any\[]}
+
+Emitted when `console.log()` is called. Receives and array of the arguments
+passed to `console.log()`.
+
+`console.info`
+
+* `args` {any\[]}
+
+Emitted when `console.info()` is called. Receives and array of the arguments
+passed to `console.info()`.
+
+`console.debug`
+
+* `args` {any\[]}
+
+Emitted when `console.debug()` is called. Receives and array of the arguments
+passed to `console.debug()`.
+
+`console.warn`
+
+* `args` {any\[]}
+
+Emitted when `console.warn()` is called. Receives and array of the arguments
+passed to `console.warn()`.
+
+`console.error`
+
+* `args` {any\[]}
+
+Emitted when `console.error()` is called. Receives and array of the arguments
+passed to `console.error()`.
+
 #### HTTP
 
 `http.client.request.created`

filename: lib/internal/console/constructor.js
patch: @@ -61,6 +61,13 @@ const {
 } = require('internal/constants');
 const kCounts = Symbol('counts');
 const { time, timeLog, timeEnd, kNone } = require('internal/util/debuglog');
+const { channel } = require('diagnostics_channel');
+
+const onLog = channel('console.log');
+const onWarn = channel('console.warn');
+const onError = channel('console.error');
+const onInfo = channel('console.info');
+const onDebug = channel('console.debug');
 
 const kTraceConsoleCategory = 'node,node.console';
 
@@ -371,14 +378,39 @@ function timeLogImpl(consoleRef, label, formatted, args) {
 
 const consoleMethods = {
   log(...args) {
+    if (onLog.hasSubscribers) {
+      onLog.publish(args);
+    }
     this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args));
   },
 
+  info(...args) {
+    if (onInfo.hasSubscribers) {
+      onInfo.publish(args);
+    }
+    this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args));
+  },
+
+  debug(...args) {
+    if (onDebug.hasSubscribers) {
+      onDebug.publish(args);
+    }
+    this[kWriteToConsole](kUseStdout, this[kFormatForStdout](args));
+  },
 
   warn(...args) {
+    if (onWarn.hasSubscribers) {
+      onWarn.publish(args);
+    }
     this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args));
   },
 
+  error(...args) {
+    if (onError.hasSubscribers) {
+      onError.publish(args);
+    }
+    this[kWriteToConsole](kUseStderr, this[kFormatForStderr](args));
+  },
 
   dir(object, options) {
     this[kWriteToConsole](kUseStdout, inspect(object, {
@@ -614,10 +646,7 @@ function noop() {}
 for (const method of ReflectOwnKeys(consoleMethods))
   Console.prototype[method] = consoleMethods[method];
 
-Console.prototype.debug = Console.prototype.log;
-Console.prototype.info = Console.prototype.log;
 Console.prototype.dirxml = Console.prototype.log;
-Console.prototype.error = Console.prototype.warn;
 Console.prototype.groupCollapsed = Console.prototype.group;
 
 function initializeGlobalConsole(globalConsole) {

filename: test/parallel/test-console-diagnostics-channels.js
patch: @@ -0,0 +1,77 @@
+'use strict';
+
+const { mustCall } = require('../common');
+const { deepStrictEqual, ok, strictEqual } = require('assert');
+
+const { channel } = require('diagnostics_channel');
+
+const {
+  hijackStdout,
+  hijackStderr,
+  restoreStdout,
+  restoreStderr
+} = require('../common/hijackstdio');
+
+const stdoutMethods = [
+  'log',
+  'info',
+  'debug',
+];
+
+const stderrMethods = [
+  'warn',
+  'error',
+];
+
+const methods = [
+  ...stdoutMethods,
+  ...stderrMethods,
+];
+
+const channels = {
+  log: channel('console.log'),
+  info: channel('console.info'),
+  debug: channel('console.debug'),
+  warn: channel('console.warn'),
+  error: channel('console.error')
+};
+
+process.stdout.isTTY = false;
+process.stderr.isTTY = false;
+
+for (const method of methods) {
+  let intercepted = false;
+  let formatted = false;
+
+  const isStdout = stdoutMethods.includes(method);
+  const hijack = isStdout ? hijackStdout : hijackStderr;
+  const restore = isStdout ? restoreStdout : restoreStderr;
+
+  const foo = 'string';
+  const bar = { key: /value/ };
+  const baz = [ 1, 2, 3 ];
+
+  channels[method].subscribe(mustCall((args) => {
+    // Should not have been formatted yet.
+    intercepted = true;
+    ok(!formatted);
+
+    // Should receive expected log message args.
+    deepStrictEqual(args, [foo, bar, baz]);
+
+    // Should be able to mutate message args and have it reflected in output.
+    bar.added = true;
+  }));
+
+  hijack(mustCall((output) => {
+    // Should have already been intercepted.
+    formatted = true;
+    ok(intercepted);
+
+    // Should produce expected formatted output with mutated message args.
+    strictEqual(output, 'string { key: /value/, added: true } [ 1, 2, 3 ]\n');
+  }));
+
+  console[method](foo, bar, baz);
+  restore();
+}

filename: test/parallel/test-repl.js
patch: @@ -794,7 +794,10 @@ const errorTests = [
     expect: [
       'Object [console] {',
       '  log: [Function: log],',
+      '  info: [Function: info],',
+      '  debug: [Function: debug],',
       '  warn: [Function: warn],',
+      '  error: [Function: error],',
       '  dir: [Function: dir],',
       '  time: [Function: time],',
       '  timeEnd: [Function: timeEnd],',
@@ -807,10 +810,7 @@ const errorTests = [
       '  group: [Function: group],',
       '  groupEnd: [Function: groupEnd],',
       '  table: [Function: table],',
-      / {2}debug: \[Function: (debug|log)],/,
-      / {2}info: \[Function: (info|log)],/,
       / {2}dirxml: \[Function: (dirxml|log)],/,
-      / {2}error: \[Function: (error|warn)],/,
       / {2}groupCollapsed: \[Function: (groupCollapsed|group)],/,
       / {2}Console: \[Function: Console],?/,
       ...process.features.inspector ? [