Repository: wix/Detox
Pull request #3569 (2022-08-30 15:37:33)

[Detox 20] finishing touches to the logger and tracer


Patch:
diff --git a/detox/index.d.ts b/detox/index.d.ts
index 227397d03..bf4bc4d4d 100644
--- a/detox/index.d.ts
+++ b/detox/index.d.ts
@@ -9,7 +9,6 @@
 // * Dor Ben Baruch <https://github.com/Dor256>
 
 import { BunyanDebugStreamOptions } from 'bunyan-debug-stream';
-import { DurationEventHandle } from 'trace-event-lib';
 
 declare global {
     const device: Detox.DetoxExportWrapper['device'];
@@ -107,7 +106,7 @@ declare global {
              * @default 'sandbox'
              */
             overrideConsole?: 'all' | 'sandbox' | 'none';
-            options?: BunyanDebugStreamOptions;
+            options?: BunyanDebugStreamOptions | ((config: Partial<DetoxLoggerConfig>) => BunyanDebugStreamOptions);
         }
 
         interface DetoxSessionConfig {
@@ -311,11 +310,7 @@ declare global {
 
         // endregion DetoxConfig
 
-        type DetoxExportWrapper = DetoxWorker;
-
-        interface DetoxWorker {
-            readonly id: string;
-
+        interface DetoxExportWrapper {
             readonly device: Device;
 
             readonly element: ElementFacade;
@@ -348,65 +343,74 @@ declare global {
             readonly log: Logger;
 
             /**
+             * @deprecated
+             *
+             * Deprecated - use {@link Detox.Logger#trace}
              * Detox tracer instance. Can be used for building timelines in Google Event Tracing format.
              */
-            readonly trace: Tracer;
+            readonly trace: {
+                /** @deprecated */
+                readonly startSection: (name: string) => void;
+                /** @deprecated */
+                readonly endSection: (name: string) => void;
+            };
 
             /**
              * @deprecated
              */
-            readonly traceCall: _TraceCallSignature;
+            readonly traceCall: <T>(event: string, action: () => Promise<T>) => Promise<T>;
         }
 
-        /** @internal */
-        type _TraceEventArgs = Record<string, unknown>;
+        interface Logger {
+            readonly level: DetoxLogLevel;
 
-        type TraceEvent = {
-            name?: string;
-            cat?: string;
-            cname?: string;
-            id?: number;
-            args?: _TraceEventArgs;
-        };
+            readonly fatal: _LogMethod;
+            readonly error: _LogMethod;
+            readonly warn: _LogMethod;
+            readonly info: _LogMethod;
+            readonly debug: _LogMethod;
+            readonly trace: _LogMethod;
 
-        /**
-         * Trace a duration event before and after executing the action function
-         *
-         * @internal
-         */
-        interface _TraceCallSignature {
-            <T>(event: string | TraceEvent, action: () => T): T;
-            <T>(event: string | TraceEvent, action: Promise<T>): Promise<T>;
-            <T>(event: string | TraceEvent, action: () => Promise<T>): Promise<T>;
+            child(context?: Partial<LogEvent>): Logger;
         }
 
         /** @internal */
-        interface _TraceSectionSignature<T> {
-            (event: string, args?: _TraceEventArgs): T;
-            (event: TraceEvent): T;
+        interface _LogMethod extends _LogMethodSignature {
+            readonly begin: _LogMethodSignature;
+            readonly complete: _CompleteMethodSignature;
+            readonly end: _LogMethodSignature;
         }
 
-        interface Tracer extends _TraceCallSignature {
-            readonly begin: _TraceSectionSignature<DurationEventHandle>;
-            readonly end: _TraceSectionSignature<void>;
+        /** @internal */
+        interface _LogMethodSignature {
+            (...args: unknown[]): void
+            (event: LogEvent, ...args: unknown[]): void;
+        }
 
-            /** @deprecated */
-            readonly startSection: _TraceSectionSignature<DurationEventHandle>;
-            /** @deprecated */
-            readonly endSection: _TraceSectionSignature<void>;
+        /** @internal */
+        interface _CompleteMethodSignature {
+            <T>(message: string, action: T | (() => T)): T;
+            <T>(event: LogEvent, message: string, action: T | (() => T)): T;
         }
 
-        type Logger = {
-            readonly level: DetoxLogLevel;
+        type LogEvent = {
+            /** Use when there's a risk of logging several parallel duration events. */
+            id?: string | number;
+            /** Optional. Event categories (tags) to facilitate filtering. */
+            cat?: string | string[];
+            /** Optional. Color name (applicable in Google Chrome Trace Format) */
+            cname?: string;
 
-            fatal(context?: unknown, ...args: any[]): void;
-            error(context?: unknown, ...args: any[]): void;
-            warn(context?: unknown, ...args: any[]): void;
-            info(context?: unknown, ...args: any[]): void;
-            debug(context?: unknown, ...args: any[]): void;
-            trace(context?: unknown, ...args: any[]): void;
+            /** Reserved property. Process ID. */
+            pid?: never;
+            /** Reserved property. Thread ID. */
+            tid?: never;
+            /** Reserved property. Timestamp. */
+            ts?: never;
+            /** Reserved property. Event phase. */
+            ph?: never;
 
-            child(context?: Record<string, unknown>): Logger;
+            [customProperty: string]: unknown;
         };
 
         type DetoxLogLevel = 'fatal' | 'error' | 'warn' | 'info' | 'debug' | 'trace';
diff --git a/detox/internals.d.ts b/detox/internals.d.ts
index e9bc51038..bef654421 100644
--- a/detox/internals.d.ts
+++ b/detox/internals.d.ts
@@ -66,10 +66,13 @@ declare global {
 
       readonly config: RuntimeConfig;
       readonly log: Detox.Logger;
-      readonly trace: Detox.Tracer;
       readonly session: SessionState;
 
-      readonly worker: Detox.DetoxWorker;
+      readonly worker: Worker;
+    }
+
+    interface Worker extends Detox.DetoxExportWrapper {
+      readonly id: string;
     }
 
     type DetoxInitOptions = {
@@ -140,14 +143,14 @@ declare global {
       apps: Record<string, Readonly<Detox.DetoxAppConfig>>;
       artifacts: Readonly<Detox.DetoxArtifactsConfig>;
       behavior: Readonly<Detox.DetoxBehaviorConfig>;
-      cli: Readonly<DetoxCLIConfig>;
+      cli: Readonly<CLIConfig>;
       device: Readonly<Detox.DetoxDeviceConfig>;
       logger: Readonly<Detox.DetoxLoggerConfig>;
       testRunner: Readonly<Detox.DetoxTestRunnerConfig>;
       session: Readonly<Detox.DetoxSessionConfig>;
     }>;
 
-    type DetoxCLIConfig = Readonly<Partial<{
+    type CLIConfig = Readonly<Partial<{
       appLaunchArgs: string;
       artifactsLocation: string;
       captureViewHierarchy: string;
diff --git a/detox/local-cli/cli.js b/detox/local-cli/cli.js
index 0e8e2d42c..b81c0a7e1 100755
--- a/detox/local-cli/cli.js
+++ b/detox/local-cli/cli.js
@@ -2,7 +2,7 @@
 const yargs = require('yargs');
 
 const DetoxError = require('../src/errors/DetoxError');
-const logger = require('../src/utils/logger').child({ __filename });
+const logger = require('../src/utils/logger').child({ cat: 'cli' });
 
 yargs
   .scriptName('detox')
diff --git a/detox/local-cli/rebuild-framework-cache.js b/detox/local-cli/rebuild-framework-cache.js
index a322953ba..a150041a5 100644
--- a/detox/local-cli/rebuild-framework-cache.js
+++ b/detox/local-cli/rebuild-framework-cache.js
@@ -4,7 +4,7 @@ const path = require('path');
 
 const fs = require('fs-extra');
 
-const log = require('../src/utils/logger').child({ __filename });
+const log = require('../src/utils/logger').child({ cat: 'cli' });
 
 module.exports.command = 'rebuild-framework-cache';
 module.exports.desc = 'Rebuilds a cached Detox framework for the current environment in ~/Library/Detox. The cached framework is unique for each combination of Xcode and Detox version. (macOS only)';
diff --git a/detox/package.json b/detox/package.json
index 879b2c006..f49aa419f 100644
--- a/detox/package.json
+++ b/detox/package.json
@@ -57,13 +57,14 @@
   "dependencies": {
     "ajv": "^8.6.3",
     "bunyan": "^1.8.12",
-    "bunyan-debug-stream": "^3.0.2",
+    "bunyan-debug-stream": "^3.1.0",
     "chalk": "^2.4.2",
     "child-process-promise": "^2.2.0",
     "duplexify": "^4.1.2",
     "find-up": "^4.1.0",
     "fs-extra": "^4.0.2",
     "funpermaproxy": "^1.0.1",
+    "glob": "^8.0.3",
     "ini": "^1.3.4",
     "json-cycle": "^1.3.0",
     "lodash": "^4.17.5",
@@ -83,7 +84,7 @@
     "tail": "^2.0.0",
     "telnet-client": "1.2.8",
     "tempfile": "^2.0.0",
-    "trace-event-lib": "^1.1.0",
+    "trace-event-lib": "^1.3.1",
     "which": "^1.3.1",
     "ws": "^7.0.0",
     "yargs": "^16.0.3",
diff --git a/detox/src/artifacts/ArtifactsManager.js b/detox/src/artifacts/ArtifactsManager.js
index 3ebe8c659..c74144cdb 100644
--- a/detox/src/artifacts/ArtifactsManager.js
+++ b/detox/src/artifacts/ArtifactsManager.js
@@ -1,14 +1,13 @@
 const EventEmitter = require('events');
 const path = require('path');
-const util = require('util');
 
 const fs = require('fs-extra');
 const _ = require('lodash');
 
 const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'artifacts-manager,artifact' });
 const resolveModuleFromPath = require('../utils/resolveModuleFromPath');
-const { traceMethods } = require('../utils/trace');
+const traceMethods = require('../utils/traceMethods');
 
 const FileArtifact = require('./templates/artifact/FileArtifact');
 const ArtifactPathBuilder = require('./utils/ArtifactPathBuilder');
@@ -23,7 +22,7 @@ class ArtifactsManager extends EventEmitter {
     this._artifactPlugins = {};
     this._pathBuilder = this._resolveArtifactsPathBuilder(pathBuilder, rootDir);
 
-    traceMethods(this, 'artifacts-manager,artifacts', [
+    traceMethods(log, this, [
       'onAppReady',
       'onBeforeCleanup',
       'onBeforeLaunchApp',
@@ -228,27 +227,21 @@ class ArtifactsManager extends EventEmitter {
   }
 
   async _callSinglePlugin(pluginId, methodName, ...args) {
-    const callSignature = this._composeCallSignature('artifactsManager', methodName, args);
-    log.trace(Object.assign({ event: 'ARTIFACTS_LIFECYCLE', fn: methodName }, ...args), callSignature);
-
     const plugin = this._artifactPlugins[pluginId];
     try {
       await plugin[methodName](...args);
     } catch (e) {
-      this._unhandledPluginExceptionHandler(e, { plugin, methodName, args });
+      this._unhandledPluginExceptionHandler(e, { plugin, methodName });
     }
   }
 
   async _callPlugins(strategy, methodName, ...args) {
-    const callSignature = this._composeCallSignature('artifactsManager', methodName, args);
-    log.trace(Object.assign({ event: 'ARTIFACTS_LIFECYCLE', fn: methodName }, ...args), callSignature);
-
     for (const pluginGroup of this._groupPlugins(strategy)) {
       await Promise.all(pluginGroup.map(async (plugin) => {
         try {
           await plugin[methodName](...args);
         } catch (e) {
-          this._unhandledPluginExceptionHandler(e, { plugin, methodName, args });
+          this._unhandledPluginExceptionHandler(e, { plugin, methodName });
         }
       }));
     }
@@ -278,28 +271,20 @@ class ArtifactsManager extends EventEmitter {
     }
   }
 
-  _composeCallSignature(object, methodName, args) {
-    const argsString = args.map(arg => util.inspect(arg)).join(', ');
-    return `${object}.${methodName}(${argsString})`;
-  }
-
-  _unhandledPluginExceptionHandler(err, { plugin, methodName, args }) {
+  _unhandledPluginExceptionHandler(err, { plugin, methodName }) {
     const logObject = {
-      event: 'ERROR',
       plugin: plugin.name,
-      err,
       methodName,
+      err,
     };
 
-    const callSignature = this._composeCallSignature(plugin.name, methodName, args);
-    log.warn(logObject, `Suppressed error inside function call: ${callSignature}`);
+    log.warn(logObject, `Suppressed error inside function call.`);
   }
 
   _idleCallbackErrorHandle(err, caller) {
     this._unhandledPluginExceptionHandler(err, {
       plugin: caller,
       methodName: 'onIdleCallback',
-      args: []
     });
   }
 }
diff --git a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js
index 32f446b01..c60b04148 100644
--- a/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js
+++ b/detox/src/artifacts/instruments/ios/SimulatorInstrumentsRecording.js
@@ -2,7 +2,7 @@
 const fs = require('fs-extra');
 const _ = require('lodash');
 
-const log = require('../../../utils/logger').child({ __filename });
+const log = require('../../../utils/logger').child({ cat: 'artifact' });
 const FileArtifact = require('../../templates/artifact/FileArtifact');
 const InstrumentsArtifactRecording = require('../InstrumentsArtifactRecording');
 
@@ -37,9 +37,9 @@ class SimulatorInstrumentsRecording extends InstrumentsArtifactRecording {
 SimulatorInstrumentsRecording.hintAboutDetoxInstruments = _.once(() => {
   log.warn(`Make sure either:
 1. You have installed Detox Instruments:
-   https://github.com/wix/DetoxInstruments#installation 
+   https://github.com/wix/DetoxInstruments#installation
 2. You have integrated Detox Instruments in your app:
-   https://github.com/wix/DetoxInstruments/blob/master/Documentation/XcodeIntegrationGuide.md 
+   https://github.com/wix/DetoxInstruments/blob/master/Documentation/XcodeIntegrationGuide.md
 3. You have set the environment variable with your custom Detox Instruments location:
    export DETOX_INSTRUMENTS_PATH="/path/to/Detox Instruments.app"`);
 });
diff --git a/detox/src/artifacts/log/ios/SimulatorLogRecording.js b/detox/src/artifacts/log/ios/SimulatorLogRecording.js
index ca20233ff..6f75ac72a 100644
--- a/detox/src/artifacts/log/ios/SimulatorLogRecording.js
+++ b/detox/src/artifacts/log/ios/SimulatorLogRecording.js
@@ -2,7 +2,7 @@
 const fs = require('fs-extra');
 
 const childProcess = require('../../../utils/childProcess');
-const log = require('../../../utils/logger').child({ __filename });
+const log = require('../../../utils/logger').child({ cat: 'artifact' });
 const sleep = require('../../../utils/sleep');
 const Artifact = require('../../templates/artifact/Artifact');
 const FileArtifact = require('../../templates/artifact/FileArtifact');
diff --git a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
index 1db929cf3..6e3210baa 100644
--- a/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
+++ b/detox/src/artifacts/screenshot/SimulatorScreenshotPlugin.js
@@ -1,7 +1,7 @@
 const path = require('path');
 
 const fs = require('../../utils/fsext');
-const log = require('../../utils/logger').child({ __filename });
+const log = require('../../utils/logger').child({ cat: 'artifacts-plugin,artifacts' });
 const FileArtifact = require('../templates/artifact/FileArtifact');
 const temporaryPath = require('../utils/temporaryPath');
 
diff --git a/detox/src/artifacts/templates/artifact/Artifact.js b/detox/src/artifacts/templates/artifact/Artifact.js
index c9a3e4e04..2613bf685 100644
--- a/detox/src/artifacts/templates/artifact/Artifact.js
+++ b/detox/src/artifacts/templates/artifact/Artifact.js
@@ -1,5 +1,5 @@
 // @ts-nocheck
-const log = require('../../../utils/logger').child({ __filename });
+const log = require('../../../utils/logger').child({ cat: 'artifact' });
 
 class Artifact {
   constructor(template) {
diff --git a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
index 00489e113..9c521fabe 100644
--- a/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
+++ b/detox/src/artifacts/templates/plugin/ArtifactPlugin.js
@@ -3,7 +3,7 @@
 
 const _ = require('lodash');
 
-const log = require('../../../utils/logger').child({ __filename });
+const log = require('../../../utils/logger').child({ cat: 'artifacts-plugin,artifacts' });
 
 /***
  * Almost non-opinionated building block for any artifact type
diff --git a/detox/src/artifacts/utils/temporaryPath.js b/detox/src/artifacts/utils/temporaryPath.js
index bcd2e474b..82086aba2 100644
--- a/detox/src/artifacts/utils/temporaryPath.js
+++ b/detox/src/artifacts/utils/temporaryPath.js
@@ -2,15 +2,26 @@ const path = require('path');
 
 const tempfile = require('tempfile');
 
+function createTempFileBuilderFn(fileExtension) {
+  /**
+   * @param {string} [basename]
+   */
+  return (basename) => {
+    return basename
+      ? path.join(path.dirname(tempfile()), `${basename}.detox.${fileExtension}`)
+      : tempfile(`.detox.${fileExtension}`);
+  };
+}
+
 module.exports = {
   for: {
-    json: () => tempfile('.detox.json'),
-    jsonl: () => tempfile('.detox.jsonl'),
-    png: () => tempfile('.detox.png'),
-    log: () => tempfile('.detox.log'),
-    mp4: () => tempfile('.detox.mp4'),
-    dtxrec: () => tempfile('.detox.dtxrec'),
-    viewhierarchy: () => tempfile('.detox.viewhierarchy'),
+    json: createTempFileBuilderFn('json'),
+    jsonl: createTempFileBuilderFn('jsonl'),
+    png: createTempFileBuilderFn('png'),
+    log: createTempFileBuilderFn('log'),
+    mp4: createTempFileBuilderFn('mp4'),
+    dtxrec: createTempFileBuilderFn('dtxrec'),
+    viewhierarchy: createTempFileBuilderFn('viewhierarchy'),
   },
   mask: () => path.join(tempfile(), '..') + path.sep + '*.detox.*',
 };
diff --git a/detox/src/artifacts/video/SimulatorRecordVideoPlugin.js b/detox/src/artifacts/video/SimulatorRecordVideoPlugin.js
index e33c4c2f0..dce5b4057 100644
--- a/detox/src/artifacts/video/SimulatorRecordVideoPlugin.js
+++ b/detox/src/artifacts/video/SimulatorRecordVideoPlugin.js
@@ -1,7 +1,7 @@
 const fs = require('fs-extra');
 
 const { interruptProcess } = require('../../utils/childProcess');
-const log = require('../../utils/logger').child({ __filename });
+const log = require('../../utils/logger').child({ cat: 'artifacts-plugin,artifact' });
 const Artifact = require('../templates/artifact/Artifact');
 const FileArtifact = require('../templates/artifact/FileArtifact');
 const temporaryPath = require('../utils/temporaryPath');
diff --git a/detox/src/client/AsyncWebSocket.js b/detox/src/client/AsyncWebSocket.js
index 99e4d28a2..39e1a7b32 100644
--- a/detox/src/client/AsyncWebSocket.js
+++ b/detox/src/client/AsyncWebSocket.js
@@ -5,25 +5,16 @@ const WebSocket = require('ws');
 const DetoxInternalError = require('../errors/DetoxInternalError');
 const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const Deferred = require('../utils/Deferred');
-const log = require('../utils/logger').child({ __filename, cat: 'ws-client,ws' });
+const log = require('../utils/logger').child({ cat: 'ws-client,ws' });
 
 const InflightRequest = require('./InflightRequest');
 
-const EVENTS = {
-  OPEN: Object.freeze({ event: 'WS_OPEN' }),
-  ERROR: Object.freeze({ event: 'WS_ERROR' }),
-  MESSAGE: Object.freeze({ event: 'WS_MESSAGE' }),
-  SEND: Object.freeze({ event: 'WS_SEND' }),
-  LATE_RESPONSE: Object.freeze({ event: 'WS_LATE_RESPONSE' }),
-};
-
 const DEFAULT_SEND_OPTIONS = {
   timeout: 0,
 };
 
 class AsyncWebSocket {
   constructor(url) {
-    this._log = log.child({ url });
     this._url = url;
     this._ws = null;
     this._eventCallbacks = {};
@@ -99,9 +90,9 @@ class AsyncWebSocket {
 
     this.handleMultipleNonAtomicPendingActions();
 
-    const messageAsString = JSON.stringify(message);
-    this._log.trace(EVENTS.SEND, messageAsString);
-    this._ws.send(messageAsString);
+    const payload = JSON.stringify(message);
+    log.trace({ data: payload }, 'send message');
+    this._ws.send(payload);
 
     return inFlight.promise;
   }
@@ -157,7 +148,7 @@ class AsyncWebSocket {
     }
 
     if (!hasPendingActions) {
-      log.error(EVENTS.ERROR, DetoxRuntimeError.format(error));
+      log.error({ error });
     }
   }
 
@@ -186,7 +177,7 @@ class AsyncWebSocket {
    * @private
    */
   _onOpen(event) { // eslint-disable-line no-unused-vars
-    this._log.trace(EVENTS.OPEN, `opened web socket to: ${this._url}`);
+    log.trace(`opened web socket to: ${this._url}`);
     this._opening.resolve();
     this._opening = null;
   }
@@ -234,7 +225,7 @@ class AsyncWebSocket {
     const data = event && event.data || null;
 
     try {
-      this._log.trace(EVENTS.MESSAGE, data);
+      log.trace({ data }, 'get message');
 
       const json = JSON.parse(data);
       if (!json || !json.type) {
@@ -259,7 +250,7 @@ class AsyncWebSocket {
 
       if (!handled) {
         if (this._abortedMessageIds.has(json.messageId)) {
-          log.debug(EVENTS.LATE_RESPONSE, `Received late response for messageId=${json.messageId}`);
+          log.debug({ messageId: json.messageId }, `late response`);
         } else {
           throw new DetoxRuntimeError('Unexpected message received over the web socket: ' + json.type);
         }
diff --git a/detox/src/client/Client.js b/detox/src/client/Client.js
index 9b513d72b..2d170ad69 100644
--- a/detox/src/client/Client.js
+++ b/detox/src/client/Client.js
@@ -9,7 +9,7 @@ const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
 const failedToReachTheApp = require('../errors/longreads/failedToReachTheApp');
 const Deferred = require('../utils/Deferred');
 const { asError, createErrorWithUserStack, replaceErrorStack } = require('../utils/errorUtils');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'ws-client,ws' });
 
 const AsyncWebSocket = require('./AsyncWebSocket');
 const actions = require('./actions/actions');
diff --git a/detox/src/configuration/collectCliConfig.js b/detox/src/configuration/collectCliConfig.js
index 2bc6d78e5..ec6012480 100644
--- a/detox/src/configuration/collectCliConfig.js
+++ b/detox/src/configuration/collectCliConfig.js
@@ -1,7 +1,7 @@
 const _ = require('lodash');
 
 const argparse = require('../utils/argparse');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'config' });
 
 const { DEVICE_LAUNCH_ARGS_GENERIC_DEPRECATION } = require('./utils/warnings');
 
diff --git a/detox/src/configuration/composeDeviceConfig.js b/detox/src/configuration/composeDeviceConfig.js
index 521b8a6e0..aa0d9151b 100644
--- a/detox/src/configuration/composeDeviceConfig.js
+++ b/detox/src/configuration/composeDeviceConfig.js
@@ -2,7 +2,7 @@
 const _ = require('lodash');
 
 const environmentFactory = require('../environmentFactory');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'config' });
 
 /**
  * @param {DetoxConfigErrorComposer} opts.errorComposer
diff --git a/detox/src/configuration/composeLoggerConfig.js b/detox/src/configuration/composeLoggerConfig.js
index 63d0703c1..1172478ad 100644
--- a/detox/src/configuration/composeLoggerConfig.js
+++ b/detox/src/configuration/composeLoggerConfig.js
@@ -7,25 +7,34 @@ const { castLevel, defaultOptions } = require('../logger/DetoxLogger');
  * @param {Detox.DetoxConfig} opts.globalConfig
  * @param {Detox.DetoxConfiguration} opts.localConfig
  * @param {*} opts.cliConfig
- * @returns {Detox.DetoxLoggerConfig}
  */
 function composeLoggerConfig(opts) {
   const { globalConfig, localConfig, cliConfig } = opts;
 
-  return _.merge(
+  const items = [
     {
       level: 'info',
       overrideConsole: 'sandbox',
-      options: {
-        ...defaultOptions,
-        prefixers: {
-          ...defaultOptions.prefixers,
-        },
-      },
+      options: defaultOptions,
     },
     globalConfig.logger,
     localConfig.logger,
     adaptCLI(cliConfig),
+  ];
+
+  return items.reduce(
+    /**
+     * @param {Partial<Detox.DetoxLoggerConfig>} acc
+     * @param config
+     */
+    (acc, config) => {
+      if (!config) return acc;
+      const { options } = config;
+      return _.merge(acc, {
+        options: typeof options === 'function' ? options(acc) : options
+      });
+    },
+    items.reduce((a, b) => _.merge(a, _.omit(b, 'options')))
   );
 }
 
diff --git a/detox/src/configuration/composeRunnerConfig.js b/detox/src/configuration/composeRunnerConfig.js
index d873499bd..7d5470214 100644
--- a/detox/src/configuration/composeRunnerConfig.js
+++ b/detox/src/configuration/composeRunnerConfig.js
@@ -8,7 +8,7 @@ const log = require('../utils/logger');
  * @param {object} opts
  * @param {Detox.DetoxConfig} opts.globalConfig
  * @param {Detox.DetoxConfiguration} opts.localConfig
- * @param {DetoxInternals.DetoxCLIConfig} opts.cliConfig
+ * @param {DetoxInternals.CLIConfig} opts.cliConfig
  * @param {Record<string, any>} opts.testRunnerArgv
  * @param {import('../errors/DetoxConfigErrorComposer')} opts.errorComposer
  * @returns {Detox.DetoxTestRunnerConfig} opts.testRunnerArgv
diff --git a/detox/src/configuration/index.js b/detox/src/configuration/index.js
index bfba07368..88fc40cfb 100644
--- a/detox/src/configuration/index.js
+++ b/detox/src/configuration/index.js
@@ -106,7 +106,6 @@ async function composeDetoxConfig({
 
   const result = {
     configurationName,
-    errorComposer,
 
     apps: appsConfig,
     artifacts: artifactsConfig,
@@ -118,6 +117,11 @@ async function composeDetoxConfig({
     session: sessionConfig,
   };
 
+  Object.defineProperty(result, 'errorComposer', {
+    enumerable: false,
+    value: errorComposer,
+  });
+
   return result;
 }
 
diff --git a/detox/src/configuration/loadExternalConfig.js b/detox/src/configuration/loadExternalConfig.js
index 8bb4f9f2e..ab0fdc040 100644
--- a/detox/src/configuration/loadExternalConfig.js
+++ b/detox/src/configuration/loadExternalConfig.js
@@ -6,7 +6,7 @@ const fs = require('fs-extra');
 const _ = require('lodash');
 const resolveFrom = require('resolve-from');
 
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'config' });
 
 async function locateExternalConfig(cwd) {
   return findUp([
diff --git a/detox/src/devices/allocation/DeviceAllocator.js b/detox/src/devices/allocation/DeviceAllocator.js
index 1144df09c..5cd689865 100644
--- a/detox/src/devices/allocation/DeviceAllocator.js
+++ b/detox/src/devices/allocation/DeviceAllocator.js
@@ -1,5 +1,6 @@
 // @ts-nocheck
-const { traceMethods } = require('../../utils/trace');
+const log = require('../../utils/logger').child({ cat: 'device' });
+const traceMethods = require('../../utils/traceMethods');
 
 class DeviceAllocator {
   /**
@@ -7,7 +8,7 @@ class DeviceAllocator {
    */
   constructor(allocationDriver) {
     this._driver = allocationDriver;
-    traceMethods(this, 'device', ['allocate', 'free']);
+    traceMethods(log, this, ['allocate', 'free']);
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
index 95ad52d0f..939ed4581 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/AVDValidator.js
@@ -2,7 +2,7 @@ const _ = require('lodash');
 
 const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
 const environment = require('../../../../../utils/environment');
-const logger = require('../../../../../utils/logger').child({ __filename });
+const logger = require('../../../../../utils/logger').child({ cat: 'device' });
 
 const REQUIRED_EMULATOR_MAJOR = 29;
 
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
index d1a2600a3..bed0179f3 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocDriver.js
@@ -1,7 +1,8 @@
 // @ts-nocheck
 const _ = require('lodash');
 
-const { traceMethods } = require('../../../../../utils/trace');
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
+const traceMethods = require('../../../../../utils/traceMethods');
 const AndroidEmulatorCookie = require('../../../../cookies/AndroidEmulatorCookie');
 const AllocationDriverBase = require('../../AllocationDriverBase');
 
@@ -23,7 +24,7 @@ class EmulatorAllocDriver extends AllocationDriverBase {
     this._emulatorLauncher = emulatorLauncher;
     this._allocationHelper = allocationHelper;
 
-    traceMethods(this, 'device', ['_launchEmulator']);
+    traceMethods(log, this, ['_launchEmulator']);
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocationHelper.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocationHelper.js
index 83653298a..b3544981e 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocationHelper.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorAllocationHelper.js
@@ -1,4 +1,4 @@
-const logger = require('../../../../../utils/logger').child({ __filename });
+const logger = require('../../../../../utils/logger').child({ cat: 'device' });
 const DeviceAllocationHelper  = require('../../../../common/drivers/DeviceAllocationHelper');
 
 const DetoxEmulatorsPortRange = {
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
index a18236770..0c5edde97 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorLauncher.js
@@ -1,7 +1,8 @@
 // @ts-nocheck
 const { DetoxRuntimeError } = require('../../../../../errors');
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
 const retry = require('../../../../../utils/retry');
-const { traceMethods } = require('../../../../../utils/trace');
+const traceMethods = require('../../../../../utils/traceMethods');
 const DeviceLauncher = require('../../../../common/drivers/DeviceLauncher');
 const { LaunchCommand } = require('../../../../common/drivers/android/emulator/exec/EmulatorExec');
 
@@ -14,7 +15,7 @@ class EmulatorLauncher extends DeviceLauncher {
     super(eventEmitter);
     this._adb = adb;
     this._emulatorExec = emulatorExec;
-    traceMethods(this, 'device', ['_awaitEmulatorBoot']);
+    traceMethods(log, this, ['_awaitEmulatorBoot']);
   }
 
   /**
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
index 39aeb2bb5..9bc9335ac 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.js
@@ -1,8 +1,6 @@
-const log = require('../../../../../utils/logger').child({ __filename });
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
 const { QueryVersionCommand } = require('../../../../common/drivers/android/emulator/exec/EmulatorExec');
 
-const EMU_BIN_VERSION_DETECT_EV = 'EMU_BIN_VERSION_DETECT';
-
 class EmulatorVersionResolver {
   constructor(emulatorExec) {
     this._emulatorExec = emulatorExec;
@@ -21,18 +19,18 @@ class EmulatorVersionResolver {
     try {
       rawOutput = await this._emulatorExec.exec(new QueryVersionCommand({ headless })) || '';
     } catch (error) {
-      log.error({ event: EMU_BIN_VERSION_DETECT_EV, success: false, error }, 'Could not detect emulator binary version', error);
+      log.error({ success: false, error }, 'Could not detect emulator binary version', error);
       return null;
     }
 
     const matches = rawOutput.match(/Android emulator version ([0-9]+\.[0-9]+\.[0-9]+\.[0-9]*)/);
     if (!matches) {
-      log.warn({ event: EMU_BIN_VERSION_DETECT_EV, success: false }, 'Could not detect emulator binary version, got:', rawOutput);
+      log.warn({ success: false }, 'Could not detect emulator binary version, got:', rawOutput);
       return null;
     }
 
     const version = this._parseVersionString(matches[1]);
-    log.debug({ event: EMU_BIN_VERSION_DETECT_EV, success: true }, 'Detected emulator binary version', version);
+    log.debug({ success: true }, 'Detected emulator binary version', version);
     return version;
   }
 
diff --git a/detox/src/devices/allocation/drivers/android/emulator/launchEmulatorProcess.js b/detox/src/devices/allocation/drivers/android/emulator/launchEmulatorProcess.js
index bae6612d5..0f2ea2756 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/launchEmulatorProcess.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/launchEmulatorProcess.js
@@ -3,7 +3,7 @@ const fs = require('fs');
 const _ = require('lodash');
 const { Tail } = require('tail');
 
-const unitLogger = require('../../../../../utils/logger').child({ __filename });
+const unitLogger = require('../../../../../utils/logger').child({ cat: 'device' });
 
 function launchEmulatorProcess(emulatorName, emulatorExec, emulatorLaunchCommand) {
   let childProcessOutput;
diff --git a/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceAllocationHelper.js b/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceAllocationHelper.js
index fcc49ba86..aa8a5606b 100644
--- a/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceAllocationHelper.js
+++ b/detox/src/devices/allocation/drivers/android/genycloud/GenyInstanceAllocationHelper.js
@@ -1,5 +1,5 @@
 // @ts-nocheck
-const logger = require('../../../../../utils/logger').child({ __filename });
+const logger = require('../../../../../utils/logger').child({ cat: 'device' });
 const DeviceAllocationHelper = require('../../../../common/drivers/DeviceAllocationHelper');
 
 const { ALLOCATE_DEVICE_LOG_EVT } = DeviceAllocationHelper;
diff --git a/detox/src/devices/common/drivers/android/genycloud/services/GenyRecipesService.js b/detox/src/devices/common/drivers/android/genycloud/services/GenyRecipesService.js
index fe522aa92..ce36f4fbd 100644
--- a/detox/src/devices/common/drivers/android/genycloud/services/GenyRecipesService.js
+++ b/detox/src/devices/common/drivers/android/genycloud/services/GenyRecipesService.js
@@ -1,4 +1,4 @@
-const logger = require('../../../../../../utils/logger').child({ __filename });
+const logger = require('../../../../../../utils/logger').child({ cat: 'device' });
 
   const Recipe = require('./dto/GenyRecipe');
 
diff --git a/detox/src/devices/common/drivers/android/tools/EmulatorTelnet.js b/detox/src/devices/common/drivers/android/tools/EmulatorTelnet.js
index 08fc17991..63867830c 100644
--- a/detox/src/devices/common/drivers/android/tools/EmulatorTelnet.js
+++ b/detox/src/devices/common/drivers/android/tools/EmulatorTelnet.js
@@ -5,7 +5,7 @@ const path = require('path');
 const fs = require('fs-extra');
 const Telnet = require('telnet-client');
 
-const log = require('../../../../../utils/logger').child({ __filename });
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
 
 class EmulatorTelnet {
   constructor() {
diff --git a/detox/src/devices/common/drivers/android/tools/FreeDeviceFinder.js b/detox/src/devices/common/drivers/android/tools/FreeDeviceFinder.js
index 185f9e53f..fb43b8810 100644
--- a/detox/src/devices/common/drivers/android/tools/FreeDeviceFinder.js
+++ b/detox/src/devices/common/drivers/android/tools/FreeDeviceFinder.js
@@ -1,4 +1,4 @@
-const log = require('../../../../../utils/logger').child({ __filename });
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
 
 const DEVICE_LOOKUP_LOG_EVT = 'DEVICE_LOOKUP';
 
diff --git a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
index 68f1b2a9f..eee02f770 100644
--- a/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
+++ b/detox/src/devices/common/drivers/ios/tools/AppleSimUtils.js
@@ -7,7 +7,7 @@ const DetoxRuntimeError = require('../../../../../errors/DetoxRuntimeError');
 const { joinArgs } = require('../../../../../utils/argparse');
 const childProcess = require('../../../../../utils/childProcess');
 const environment = require('../../../../../utils/environment');
-const log = require('../../../../../utils/logger').child({ __filename });
+const log = require('../../../../../utils/logger').child({ cat: 'device' });
 const { quote } = require('../../../../../utils/shellQuote');
 
 class AppleSimUtils {
diff --git a/detox/src/devices/lifecycle/GenyGlobalLifecycleHandler.js b/detox/src/devices/lifecycle/GenyGlobalLifecycleHandler.js
index 4027fdcc8..23db14c56 100644
--- a/detox/src/devices/lifecycle/GenyGlobalLifecycleHandler.js
+++ b/detox/src/devices/lifecycle/GenyGlobalLifecycleHandler.js
@@ -1,4 +1,4 @@
-const logger = require('../../utils/logger').child({ __filename });
+const logger = require('../../utils/logger').child({ cat: 'device' });
 
 const cleanupLogData = {
   event: 'GENYCLOUD_TEARDOWN',
diff --git a/detox/src/devices/runtime/RuntimeDevice.js b/detox/src/devices/runtime/RuntimeDevice.js
index ce9482dd9..53c11369d 100644
--- a/detox/src/devices/runtime/RuntimeDevice.js
+++ b/detox/src/devices/runtime/RuntimeDevice.js
@@ -1,6 +1,8 @@
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
 const debug = require('../../utils/debug'); // debug utils, leave here even if unused
-const { traceCall, traceMethods } = require('../../utils/trace');
+const log = require('../../utils/logger').child({ cat: 'device' });
+const { traceCall } = require('../../utils/trace');
+const traceMethods = require('../../utils/traceMethods');
 const wrapWithStackTraceCutter = require('../../utils/wrapWithStackTraceCutter');
 
 const LaunchArgsEditor = require('./utils/LaunchArgsEditor');
@@ -48,7 +50,7 @@ class RuntimeDevice {
       'unreverseTcpPort',
     ];
 
-    traceMethods(this, 'device', methodNames);
+    traceMethods(log, this, methodNames);
     wrapWithStackTraceCutter(this, methodNames);
 
     this._appsConfig = appsConfig;
diff --git a/detox/src/devices/runtime/drivers/DeviceDriverBase.js b/detox/src/devices/runtime/drivers/DeviceDriverBase.js
index b52c18eb3..c38765aab 100644
--- a/detox/src/devices/runtime/drivers/DeviceDriverBase.js
+++ b/detox/src/devices/runtime/drivers/DeviceDriverBase.js
@@ -4,7 +4,7 @@ const path = require('path');
 
 const fs = require('fs-extra');
 
-const log = require('../../../utils/logger').child({ __filename });
+const log = require('../../../utils/logger').child({ cat: 'device' });
 
 /**
  * @typedef DeviceDriverDeps
diff --git a/detox/src/devices/runtime/drivers/android/AndroidDriver.js b/detox/src/devices/runtime/drivers/android/AndroidDriver.js
index c235b7ce3..e603dcc16 100644
--- a/detox/src/devices/runtime/drivers/android/AndroidDriver.js
+++ b/detox/src/devices/runtime/drivers/android/AndroidDriver.js
@@ -18,7 +18,7 @@ const sleep = require('../../../../utils/sleep');
 const apkUtils = require('../../../common/drivers/android/tools/apk');
 const DeviceDriverBase = require('../DeviceDriverBase');
 
-const log = logger.child({ __filename });
+const log = logger.child({ cat: 'device' });
 
 /**
  * @typedef AndroidDriverProps
diff --git a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js b/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js
index fef35a1d0..55695374f 100644
--- a/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js
+++ b/detox/src/devices/runtime/drivers/ios/SimulatorDriver.js
@@ -7,7 +7,7 @@ const _ = require('lodash');
 const temporaryPath = require('../../../../artifacts/utils/temporaryPath');
 const DetoxRuntimeError = require('../../../../errors/DetoxRuntimeError');
 const getAbsoluteBinaryPath = require('../../../../utils/getAbsoluteBinaryPath');
-const log = require('../../../../utils/logger').child({ __filename });
+const log = require('../../../../utils/logger').child({ cat: 'device' });
 const pressAnyKey = require('../../../../utils/pressAnyKey');
 
 const IosDriver = require('./IosDriver');
diff --git a/detox/src/ipc/IPCClient.js b/detox/src/ipc/IPCClient.js
index 18643436c..d94c3cbc5 100644
--- a/detox/src/ipc/IPCClient.js
+++ b/detox/src/ipc/IPCClient.js
@@ -6,8 +6,8 @@ class IPCClient {
   constructor({ id, logger, state }) {
     this._id = id;
     /** @type {import('../logger/DetoxLogger')} logger */
-    this._logger = logger.child({ __filename, cat: 'ipc' });
-    /** @type {import('./state').SecondarySessionState} */
+    this._logger = logger.child({ cat: 'ipc' });
+    /** @type {import('./SessionState')} */
     this._state = state;
 
     this._client = null;
@@ -74,11 +74,7 @@ class IPCClient {
   }
 
   async _registerContext() {
-    const sessionState = await this._emit('registerContext', {
-      id: this._id,
-      logFile: this._logger.file,
-    });
-
+    const sessionState = await this._emit('registerContext', { id: this._id });
     this._state.patch(sessionState);
   }
 
diff --git a/detox/src/ipc/IPCServer.js b/detox/src/ipc/IPCServer.js
index a1c2ca36c..fa35c46da 100644
--- a/detox/src/ipc/IPCServer.js
+++ b/detox/src/ipc/IPCServer.js
@@ -3,12 +3,12 @@ const { IPC } = require('node-ipc');
 class IPCServer {
   /**
    * @param {object} options
-   * @param {import('./state').PrimarySessionState} options.sessionState
+   * @param {import('./SessionState')} options.sessionState
    * @param {Detox.Logger} options.logger
    */
   constructor({ sessionState, logger }) {
     this._sessionState = sessionState;
-    this._logger = logger.child({ __filename, cat: 'ipc' });
+    this._logger = logger.child({ cat: 'ipc,ipc-server' });
     this._ipc = null;
     this._workers = new Set();
   }
@@ -49,13 +49,9 @@ class IPCServer {
     });
   }
 
-  onRegisterContext({ id, logFile }, socket) {
+  onRegisterContext({ id }, socket) {
     this._sessionState.contexts.push(id);
 
-    if (logFile && !this._sessionState.logFiles.includes(logFile)) {
-      this._sessionState.logFiles.push(logFile);
-    }
-
     this._ipc.server.emit(socket, 'registerContextDone', {
       failedTestFiles: this._sessionState.failedTestFiles,
       testFilesToRetry: this._sessionState.testFilesToRetry,
diff --git a/detox/src/ipc/state.js b/detox/src/ipc/SessionState.js
similarity index 80%
rename from detox/src/ipc/state.js
rename to detox/src/ipc/SessionState.js
index a97550656..d52127cf2 100644
--- a/detox/src/ipc/state.js
+++ b/detox/src/ipc/SessionState.js
@@ -3,6 +3,28 @@ const cycle = require('json-cycle');
 const uuid = require('../utils/uuid');
 
 class SessionState {
+  constructor({
+    id = uuid.UUID(),
+    contexts = [],
+    detoxConfigSnapshotPath = '',
+    detoxConfig = null,
+    detoxIPCServer = '',
+    failedTestFiles = [],
+    testFilesToRetry = [],
+    testSessionIndex = 0,
+    workersCount = 0
+  }) {
+    this.id = id;
+    this.contexts = contexts;
+    this.detoxConfigSnapshotPath = detoxConfigSnapshotPath;
+    this.detoxConfig = detoxConfig;
+    this.detoxIPCServer = detoxIPCServer;
+    this.failedTestFiles = failedTestFiles;
+    this.testFilesToRetry = testFilesToRetry;
+    this.testSessionIndex = testSessionIndex;
+    this.workersCount = workersCount;
+  }
+
   patch(state) {
     Object.assign(this, state);
   }
@@ -37,46 +59,4 @@ class SessionState {
   }
 }
 
-class SecondarySessionState extends SessionState {
-  constructor({
-    id = uuid.UUID(),
-    detoxConfigSnapshotPath = '',
-    detoxConfig = null,
-    detoxIPCServer = '',
-    failedTestFiles = [],
-    testFilesToRetry = [],
-    testSessionIndex = 0,
-    workersCount = 0
-  }) {
-    super();
-
-    this.id = id;
-    this.detoxConfigSnapshotPath = detoxConfigSnapshotPath;
-    this.detoxConfig = detoxConfig;
-    this.detoxIPCServer = detoxIPCServer;
-    this.failedTestFiles = failedTestFiles;
-    this.testFilesToRetry = testFilesToRetry;
-    this.testSessionIndex = testSessionIndex;
-    this.workersCount = workersCount;
-  }
-}
-
-class PrimarySessionState extends SecondarySessionState {
-  constructor({
-    contexts = [],
-    logFiles = [],
-
-    ...baseOpts
-  }) {
-    super(baseOpts);
-
-    this.contexts = contexts;
-    this.logFiles = logFiles;
-  }
-}
-
-module.exports = {
-  SessionState,
-  PrimarySessionState,
-  SecondarySessionState,
-};
+module.exports = SessionState;
diff --git a/detox/src/logger/DetoxLogger.js b/detox/src/logger/DetoxLogger.js
index 931a1f3ad..1da0dd6fd 100644
--- a/detox/src/logger/DetoxLogger.js
+++ b/detox/src/logger/DetoxLogger.js
@@ -1,201 +1,335 @@
 const path = require('path');
-const { PassThrough } = require('stream');
 
-const bunyan = require('bunyan');
-const bunyanDebugStream = require('bunyan-debug-stream');
 const _ = require('lodash');
 
 const temporaryPath = require('../artifacts/utils/temporaryPath');
-const { DetoxInternalError } = require('../errors');
+const { DetoxInternalError, DetoxError } = require('../errors');
 const { shortFormat } = require('../utils/dateUtils');
+const isPromise = require('../utils/isPromise');
 
-const customConsoleLogger = require('./customConsoleLogger');
+const BunyanLogger = require('./utils/BunyanLogger');
+const CategoryThreadDispatcher = require('./utils/CategoryThreadDispatcher');
+const MessageStack = require('./utils/MessageStack');
+const customConsoleLogger = require('./utils/customConsoleLogger');
+const sanitizeBunyanContext = require('./utils/sanitizeBunyanContext');
 
 /**
- * @typedef PrivateLoggerConfig
- * @property {string} [file]
+ * @typedef SharedLoggerConfig
+ * @property {string} file
+ * @property {Detox.DetoxLoggerConfig} userConfig
+ * @property {CategoryThreadDispatcher} [dispatcher]
+ * @property {BunyanLogger} [bunyan]
+ * @property {MessageStack} [messageStack]
  */
 
 class DetoxLogger {
   /**
-   * @param {Detox.DetoxLoggerConfig & PrivateLoggerConfig} [config]
+   * @param {Pick<SharedLoggerConfig, 'file' | 'userConfig'>} sharedConfig
    * @param {object} [context]
-   * @param {bunyan} [bunyanLogger]
    */
-  constructor(config, context, bunyanLogger) {
-    // IMPORTANT: all the loggers should share the same object instance of this._config
-    this._config = config || {
-      level: 'info',
-      overrideConsole: 'none',
-      options: {
-        showDate: true,
-        showLoggerName: true,
-        showPid: true,
-        showMetadata: false,
-      },
-    };
-
-    if (config && !context) {
-      this._config.file = temporaryPath.for.jsonl();
+  constructor(sharedConfig, context) {
+    /**
+     * @type {SharedLoggerConfig}
+     * IMPORTANT: all instances of {@link DetoxLogger} must share the same object instance of this._sharedConfig.
+     */
+    this._sharedConfig = sharedConfig;
+
+    if (!context) {
+      // In this branch, `this` refers to the first (root) logger instance.
+
+      this._sharedConfig.userConfig = this._sharedConfig.userConfig || {
+        level: 'info',
+        overrideConsole: 'none',
+        options: {
+          showDate: true,
+          showLoggerName: true,
+          showPid: true,
+          showPrefixes: false,
+          showMetadata: false,
+        },
+      };
+
+      this._sharedConfig.bunyan = new BunyanLogger()
+        .installFileStream(this.file)
+        .installDebugStream(this.config);
+
+      this._sharedConfig.dispatcher = new CategoryThreadDispatcher({
+        logger: this,
+        categories: {
+          'lifecycle': [0],
+          'cli': [1],
+          'logger': [2],
+          'ipc': [29],
+          'ws-server': [50, 99],
+          'ws-client': [100, 149],
+          'device': [150, 159],
+          'artifacts-manager': [300],
+          'artifacts-plugin': [310, 349],
+          'artifact': [350, 399],
+          'child-process': [400, 499],
+          'default': [999],
+          'user': [10000, 10999]
+        },
+      });
+
+      this._sharedConfig.messageStack = new MessageStack();
+
+      this.overrideConsole();
     }
 
     /** @type {object | undefined} */
     this._context = context;
-    /** @type {bunyan} */
-    this._bunyan = bunyanLogger || this._initBunyanLogger();
 
-    this.fatal = this._forward.bind(this, 'fatal');
-    this.error = this._forward.bind(this, 'error');
-    this.warn = this._forward.bind(this, 'warn');
-    this.info = this._forward.bind(this, 'info');
-    this.debug = this._forward.bind(this, 'debug');
-    this.trace = this._forward.bind(this, 'trace');
-
-    this.overrideConsole();
+    /** @public */
+    this.fatal = this._setupLogMethod('fatal');
+    /** @public */
+    this.error = this._setupLogMethod('error');
+    /** @public */
+    this.warn = this._setupLogMethod('warn');
+    /** @public */
+    this.info = this._setupLogMethod('info');
+    /** @public */
+    this.debug = this._setupLogMethod('debug');
+    /** @public */
+    this.trace = this._setupLogMethod('trace');
   }
 
   /**
-   * @internal
+   * @public
+   * @returns {Detox.DetoxLogLevel}
    */
-  get config() {
-    return this._config;
-  }
-
-  /** @returns {Detox.DetoxLogLevel} */
   get level() {
-    return this._config.level;
+    return this.config.level;
   }
 
   /**
-   * @internal
+   * @public
+   * @param {object} [overrides]
+   * @returns {DetoxLogger}
    */
+  child(overrides) {
+    const merged = this._mergeContexts(this._context, overrides);
+    return new DetoxLogger(this._sharedConfig, merged);
+  }
+
+  /** @internal */
+  get config() {
+    return this._sharedConfig.userConfig;
+  }
+
+  /** @internal */
   get file() {
-    return this._config.file;
+    return this._sharedConfig.file;
   }
 
   /**
+   * @internal
    * @param config
    */
   async setConfig(config) {
-    if (this._context !== undefined) {
-      throw new DetoxInternalError('Trying to set a config for a non-root logger');
+    if (this._context) {
+      throw new DetoxInternalError('Trying to set a config in a non-root logger');
     }
 
-    if (this.file) {
-      throw new DetoxInternalError('Trying to set a config for a fully initialized logger');
-    }
-
-    _.merge(this._config, config);
-
-    // @ts-ignore
-    const [oldStream] = this._bunyan.streams.splice(0, 1);
-    oldStream.stream.end();
-    this._bunyan.addStream(this._createDebugStream());
+    _.merge(this.config, config);
+    this._sharedConfig.file = temporaryPath.for.jsonl();
+    this._sharedConfig.bunyan.installDebugStream(this.config);
+    this.overrideConsole();
+  }
 
-    this._config.file = temporaryPath.for.jsonl();
-    this._bunyan.addStream({
-      level: 'trace',
-      path: this._config.file,
-    });
+  /**
+   * @internal
+   */
+  overrideConsole(sandbox) {
+    const option = this.config.overrideConsole;
+    if (option === 'none') {
+      return;
+    }
 
-    this.overrideConsole();
+    if ((option === 'sandbox' && sandbox) || option === 'all') {
+      customConsoleLogger.overrideConsoleMethods((sandbox || global).console, this);
+    }
   }
 
   /**
-   * @param {object} [overrides]
-   * @returns {DetoxLogger}
+   * @private
    */
-  child(overrides) {
-    if (overrides && overrides.__filename) {
-      overrides.__filename = path.basename(overrides.__filename, '.js');
+  _mergeContexts(...contexts) {
+    const context = Object.assign({}, ...contexts);
+
+    if (context.error || context.err) {
+      context.error = DetoxError.format(context.error || context.err);
+      delete context.err;
+    }
+
+    if (Array.isArray(context.cat)) {
+      context.cat = context.cat.join(',');
+    }
+
+    if (context.__filename) {
+      context.__filename = path.basename(context.__filename);
     }
 
-    return new DetoxLogger(this._config, {
-      ...this._context,
-      ...overrides,
-    }, this._bunyan);
+    context.ph = context.ph || 'i';
+    context.cat = context.cat || '';
+
+    context.tid = this._sharedConfig.dispatcher.resolve(
+      context.ph,
+      context.cat,
+      context.id || 0
+    );
+
+    return sanitizeBunyanContext(context);
   }
 
   /**
-   * @param {bunyan.LogLevel} level
-   * @param {any[]} args
    * @private
+   * @param {Detox.DetoxLogLevel} level
+   * @returns {Detox._LogMethod}
    */
-  _forward(level, ...args) {
-    const msgContext = _.isError(args[0]) ? { err: args[0] } : _.isObject(args[0]) ? args[0] : undefined;
-    const msgArgs = msgContext !== undefined ? args.slice(1) : args;
-    const mergedContext = sanitizeBunyanContext({
-      ...this._context,
-      ...msgContext,
-    });
+  _setupLogMethod(level) {
+    const logMethod = this[level] = this._instant.bind(this, level);
 
-    this._bunyan[level](mergedContext, ...msgArgs);
+    return Object.assign(logMethod, {
+      begin: this._begin.bind(this, level),
+      complete: this._complete.bind(this, level),
+      end: this._end.bind(this, level),
+    });
   }
 
-  _initBunyanLogger() {
-    /** @type {bunyan.Stream[]} */
-    const streams = [this._createDebugStream()];
+  /** @private */
+  _begin(level, ...args) {
+    const { context, msg } = this._parseArgs({ ph: 'B' }, args);
+    this._sharedConfig.messageStack.push(context.tid, msg);
+    this._sharedConfig.bunyan.logger[level](context, ...msg);
+  }
 
-    if (this._config.file) {
-      streams.push({
-        level: 'trace',
-        path: this._config.file,
-      });
+  /** @private */
+  _end(level, ...args) {
+    let { context, msg } = this._parseArgs({ ph: 'E' }, args);
+    if (msg.length === 0) {
+      msg = this._sharedConfig.messageStack.pop(context.tid);
     }
 
-    return bunyan.createLogger({ name: 'detox', streams });
+    this._sharedConfig.bunyan.logger[level](context, ...msg);
   }
 
-  _createDebugStream() {
-    const { out = process.stderr, ...streamOptions } = this._config.options;
-    const passthrough = new PassThrough().pipe(out);
-
-    return {
-      type: 'raw',
-      level: this._config.level,
-      stream: bunyanDebugStream.default({
-        ...streamOptions,
-        out: passthrough,
-      }),
-    };
+  /**
+   * @private
+   * @param {import('bunyan').LogLevel} level
+   * @param {any[]} args
+   */
+  _instant(level, ...args) {
+    const { context, msg } = this._parseArgs(null, args);
+    this._sharedConfig.bunyan.logger[level](context, ...msg);
   }
 
-  overrideConsole(sandbox) {
-    const option = this._config.overrideConsole;
-    if (option === 'none') {
-      return;
+  /**
+   * @param {import('bunyan').LogLevel} level
+   * @private
+   */
+  _complete(level, maybeContext, maybeMessage, maybeAction) {
+    const action = typeof maybeContext !== 'string' ? maybeAction : maybeMessage;
+    const args = maybeAction === action ? [maybeContext, maybeMessage] : [maybeContext];
+    const { context, msg } = this._parseArgs(null, args);
+    const end = (ctx) => this[level].end(ctx);
+
+    let result;
+    this[level].begin(context, ...msg);
+    try {
+      result = typeof action === 'function'
+        ? action()
+        : action;
+
+      if (!isPromise(result)) {
+        end(context);
+      } else {
+        result.then(
+          () => end({ success: true }),
+          (err) => end({ success: false, err }),
+        );
+      }
+
+      return result;
+    } catch (err) {
+      end({ success: false, err });
+      throw err;
     }
+  }
 
-    if ((option === 'sandbox' && sandbox) || option === 'all') {
-      customConsoleLogger.overrideConsoleMethods((sandbox || global).console, this);
+  /** @private */
+  _parseArgs(boundContext, args) {
+    const userContext = _.isError(args[0])
+      ? { err: args[0] }
+      : _.isObject(args[0])
+        ? args[0]
+        : undefined;
+    const msg = userContext !== undefined ? args.slice(1) : args;
+
+    if (userContext) {
+      delete userContext.pid;
+      delete userContext.tid;
+      delete userContext.ts;
+      delete userContext.time;
+      delete userContext.ph;
     }
+
+    const context = this._mergeContexts(
+      this._context,
+      boundContext,
+      userContext,
+    );
+
+    return { context, msg };
   }
 
-  /** @type {bunyanDebugStream.BunyanDebugStreamOptions} */
-  static defaultOptions = {
-    showDate: shortFormat,
-    showLoggerName: true,
-    showPid: true,
-    showMetadata: false,
-    basepath: path.join(__dirname, '..'),
-    prefixers: {
-      '__filename': (filename, { entry }) => {
-        if (entry.event === 'USER_LOG') {
-          return '';
-        }
-
-        if (entry.event === 'ERROR') {
-          return `${filename}/${entry.event}`;
-        }
-
-        return entry.event ? entry.event : filename;
-      },
-      'trackingId': id => ` #${id}`,
-      'cpid': pid => ` cpid=${pid}`,
-    },
-  };
+  /** @internal */
+  static defaultOptions({ level }) {
+    const ph = level === 'trace' || level === 'debug'
+      ? value => require('chalk').grey(value) + ' '
+      : value => require('chalk').grey(value);
+
+    const id = level === 'trace'
+      ? value => require('chalk').yellow(`@${value}`)
+      : undefined;
+
+    const cat = level === 'trace' || level === 'debug'
+      ? (value) => require('chalk').yellow((value || '').split(',', 1)[0])
+      : undefined;
+
+    const event = level === 'trace' || level === 'debug'
+      ? (value) => require('chalk').grey(`:${value}`)
+      : undefined;
+
+    const identity = x => x;
+
+    return ({
+      showDate: shortFormat,
+      showLoggerName: true,
+      showPid: true,
+      showLevel: false,
+      showMetadata: false,
+      showPrefixes: (p) => p.join(''),
+      basepath: path.join(__dirname, '..'),
+      prefixers: _.omitBy({
+        ph,
+        cat,
+        event,
+        id,
+      }, _.isUndefined),
+      stringifiers: _.omitBy({
+        // eslint-disable-next-line unicorn/no-array-method-this-argument
+        'args': args => `(${require('lodash').map(args, a => JSON.stringify(a)).join(', ')})`,
+        'error': identity,
+        'data': json => typeof json === 'string' ? json : JSON.stringify(json, null, 2),
+        'stack': level === 'trace' || level === 'debug' ? identity : undefined,
+        'origin': level === 'trace' || level === 'debug' ? identity : undefined,
+      }, _.isUndefined),
+    });
+  }
 
   /**
+   * @internal
    * @param {string} level
    * @returns {Detox.DetoxLogLevel}
    */
@@ -216,29 +350,4 @@ class DetoxLogger {
   }
 }
 
-const RESERVED_PROPERTIES = [
-  'hostname',
-  'level',
-  'msg',
-  'name',
-  'pid',
-  'time',
-];
-
-function hasProperty(p) {
-  return _.has(this, p);
-}
-
-function hasReservedProperties(o) {
-  return RESERVED_PROPERTIES.some(hasProperty, o); // eslint-disable-line unicorn/no-array-method-this-argument
-}
-
-function escapeCallback(value, key) {
-  return RESERVED_PROPERTIES.includes(key) ? `${key}$` : key;
-}
-
-function sanitizeBunyanContext(context) {
-  return hasReservedProperties(context) ? _.mapKeys(context, escapeCallback) : context;
-}
-
 module.exports = DetoxLogger;
diff --git a/detox/src/logger/DetoxTraceEventBuilder.js b/detox/src/logger/DetoxTraceEventBuilder.js
deleted file mode 100644
index 531100d7d..000000000
--- a/detox/src/logger/DetoxTraceEventBuilder.js
+++ /dev/null
@@ -1,21 +0,0 @@
-const tel = require('trace-event-lib');
-
-class DetoxTraceEventBuilder extends tel.AbstractEventBuilder {
-  constructor(forward) {
-    super();
-
-    this._forward = forward;
-  }
-
-  send(event) {
-    // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
-    const { pid, ts, name, args = {}, ...trace } = event;
-    if (trace.ph === 'E') {
-      this._forward({ ...args, trace }, `end`);
-    } else {
-      this._forward({ ...args, trace }, name);
-    }
-  }
-}
-
-module.exports = DetoxTraceEventBuilder;
diff --git a/detox/src/logger/DetoxTracer.js b/detox/src/logger/DetoxTracer.js
deleted file mode 100644
index d0db2a231..000000000
--- a/detox/src/logger/DetoxTracer.js
+++ /dev/null
@@ -1,136 +0,0 @@
-const _ = require('lodash');
-
-const isPromise = require('../utils/isPromise');
-
-const DetoxTraceEventBuilder = require('./DetoxTraceEventBuilder');
-const TraceThreadDispatcher = require('./TraceThreadDispatcher');
-
-class DetoxTracer {
-  /**
-   * @param {object} options
-   * @param {Detox.Logger} options.logger
-   */
-  constructor({ logger }) {
-    this.logger = logger;
-    this.builder = new DetoxTraceEventBuilder(this.logger.trace);
-    this.categories = {};
-    this.defaultEventDispatcher = new TraceThreadDispatcher({
-      logger,
-      name: 'Default',
-      min: 10000,
-    });
-    this.eventDispatchers = { '': this.defaultEventDispatcher };
-  }
-
-  /**
-   * @param {Detox.TraceEvent | string} eventOrName
-   * @param {Detox.TraceEvent['args']} [args]
-   * @returns {import('trace-event-lib').DurationEventHandle}
-   */
-  begin(eventOrName, args) {
-    const event = typeof eventOrName === 'string' ? { name: eventOrName, args } : eventOrName;
-    const mainCategory = event.cat ? event.cat.split(',', 1)[0] : '';
-    const dispatcher = this.eventDispatchers[mainCategory] || this.defaultEventDispatcher;
-    const tid = dispatcher.begin(event.id);
-
-    return this.builder.begin({
-      args: event.args,
-      cat: event.cat || '',
-      cname: event.cname,
-      name: event.name,
-      tid,
-    });
-  }
-
-  end(eventOrName, args) {
-    const event = typeof eventOrName === 'string' ? { name: eventOrName, args } : eventOrName || {};
-    const mainCategory = event.cat ? event.cat.split(',', 1)[0] : '';
-    const dispatcher = this.eventDispatchers[mainCategory] || this.defaultEventDispatcher;
-    const tid = dispatcher.end(event.id);
-
-    this.builder.end({
-      args: event.args,
-      cname: event.cname,
-      tid,
-    });
-  }
-
-  section(sectionName, funcOrPromise) {
-    let result;
-
-    this.begin(sectionName);
-    try {
-      result = typeof funcOrPromise === 'function'
-        ? funcOrPromise()
-        : funcOrPromise;
-
-      if (!isPromise(result)) {
-        this.end(sectionName, { success: true });
-      } else {
-        result.then(
-          () => this.end(sectionName, { success: true }),
-          (error) => this.end(sectionName, { success: false, error }),
-        );
-      }
-    } catch (error) {
-      this.end(sectionName, { success: false, error });
-      throw error;
-    }
-
-    return result;
-  }
-
-  registerThreads(event, [min, max]) {
-    this.eventDispatchers[event] = new TraceThreadDispatcher({
-      logger: this.logger,
-      name: event,
-      min,
-      max,
-    });
-
-    return this;
-  }
-
-  static default(options) {
-    const tracer = new DetoxTracer(options);
-    for (const [key, range] of Object.entries(this.CATEGORIES)) {
-      tracer.registerThreads(key, range);
-    }
-
-    return Object.assign(
-      tracer.section.bind(tracer),
-      {
-        begin: tracer.begin.bind(tracer),
-        startSection: tracer.begin.bind(tracer),
-        end: tracer.end.bind(tracer),
-        endSection: tracer.end.bind(tracer),
-      });
-  }
-
-  static categorize(tid) {
-    return _.findKey(this.CATEGORIES, ([min, max]) => min <= tid && tid <= max) || 'user';
-  }
-
-  static threadize(cat) {
-    const [mainCategory] = cat ? cat.split(',', 1) : 'user';
-    const threads = _.find(this.CATEGORIES, (_, key) => key === mainCategory);
-    return threads ? threads[0] : 10000;
-  }
-
-  /**
-   * @type {Record<string, [number, number]>}
-   */
-  static CATEGORIES = {
-    'lifecycle': [0, 0],
-    'ipc': [29, 29],
-    'ws-server': [50, 99],
-    'ws-client': [100, 149],
-    'device': [150, 159],
-    'artifacts-manager': [300, 300],
-    'artifact-plugin': [310, 349],
-    'artifact': [350, 399],
-    'child-process': [400, 499],
-  };
-}
-
-module.exports = DetoxTracer;
diff --git a/detox/src/logger/__mocks__/DetoxLogger.js b/detox/src/logger/__mocks__/DetoxLogger.js
index 2b1f04050..9314a9022 100644
--- a/detox/src/logger/__mocks__/DetoxLogger.js
+++ b/detox/src/logger/__mocks__/DetoxLogger.js
@@ -6,7 +6,7 @@ const METHODS = [
 
 class FakeLogger {
   static instances = [];
-  static defaultOptions = DetoxLogger.defaultOptions;
+  static defaultOptions = DetoxLogger.defaultOptions.bind(DetoxLogger);
   static castLevel = DetoxLogger.castLevel;
 
   fatal;
@@ -30,6 +30,21 @@ class FakeLogger {
       this[method] = jest.fn((...args) => {
         this.log(method, this.opts, ...args);
       });
+      this[method].begin = jest.fn((...args) => {
+        this.log(method, this.opts, ...args, '(begin)');
+      });
+      this[method].complete = jest.fn((...args) => {
+        const [action] = args.slice(-1);
+        this.log(method, this.opts, ...args, '(begin)');
+        try {
+          return (typeof action === 'function' ? action() : action);
+        } finally {
+          this.log(method, this.opts, ...args, '(end)');
+        }
+      });
+      this[method].end = jest.fn((...args) => {
+        this.log(method, this.opts, ...args, '(end)');
+      });
     }
   }
 
diff --git a/detox/src/logger/index.js b/detox/src/logger/index.js
new file mode 100644
index 000000000..968165ba2
--- /dev/null
+++ b/detox/src/logger/index.js
@@ -0,0 +1,4 @@
+module.exports = {
+  DetoxLogger: require('./DetoxLogger'),
+  installLegacyTracerInterface: require('./utils/tracerLegacy').install,
+};
diff --git a/detox/src/logger/utils/BunyanLogger.js b/detox/src/logger/utils/BunyanLogger.js
new file mode 100644
index 000000000..2fd58930b
--- /dev/null
+++ b/detox/src/logger/utils/BunyanLogger.js
@@ -0,0 +1,72 @@
+const { PassThrough } = require('stream');
+
+const bunyan = require('bunyan');
+const bds = require('bunyan-debug-stream');
+const _ = require('lodash');
+
+const { DetoxInternalError } = require('../../errors');
+
+class BunyanLogger {
+  constructor() {
+    this._bunyan = bunyan.createLogger({ name: 'detox', streams: [] });
+    /** @type {bunyan.Stream} */
+    this._debugStream = null;
+    /** @type {bunyan.Stream} */
+    this._fileStream = null;
+  }
+
+  /**
+   * @returns {bunyan}
+   */
+  get logger() {
+    return this._bunyan;
+  }
+
+  /**
+   * @param {Detox.DetoxLoggerConfig} config
+   * @returns {this}
+   */
+  installDebugStream(config) {
+    const level = config.level || 'info';
+
+    if (this._debugStream) {
+      _.remove(this._bunyan['streams'], this._debugStream);
+      // @ts-ignore
+      this._debugStream.stream.end();
+      this._debugStream = null;
+    }
+
+    this._debugStream = {
+      type: 'raw',
+      level,
+      stream: bds.default({
+        ...config.options,
+        out: new PassThrough().pipe(process.stderr),
+      }),
+    };
+
+    this._bunyan.addStream(this._debugStream);
+    return this;
+  }
+
+  /**
+   * @param {string} file
+   * @returns {this}
+   */
+  installFileStream(file) {
+    if (this._fileStream) {
+      throw new DetoxInternalError('Trying to install a second file stream inside already initialized Bunyan logger');
+    }
+
+    this._fileStream = {
+      level: 'trace',
+      path: file,
+    };
+
+    this._bunyan.addStream(this._fileStream);
+
+    return this;
+  }
+}
+
+module.exports = BunyanLogger;
diff --git a/detox/src/logger/utils/CategoryThreadDispatcher.js b/detox/src/logger/utils/CategoryThreadDispatcher.js
new file mode 100644
index 000000000..ff2d6cf60
--- /dev/null
+++ b/detox/src/logger/utils/CategoryThreadDispatcher.js
@@ -0,0 +1,58 @@
+const _ = require('lodash');
+
+const ThreadDispatcher = require('./ThreadDispatcher');
+
+class CategoryThreadDispatcher {
+  /**
+   * @param {object} config
+   * @param {Record<string, [number, number?]>} config.categories
+   * @param {Detox.Logger} config.logger
+   */
+  constructor(config) {
+    this.categories = config.categories;
+    this.dispatchers = _.mapValues(this.categories, (range, name) => {
+      return new ThreadDispatcher({
+        name,
+        logger: config.logger,
+        min: range[0],
+        max: range[1] || range[0],
+      });
+    });
+  }
+
+  /**
+   * @param {'B' | 'E' | 'i'} ph
+   * @param {string[] | undefined} cat
+   * @param {string | number} id
+   * @returns {number}
+   */
+  resolve(ph, cat, id) {
+    const dispatcher = this._resolveDispatcher(cat);
+
+    switch (ph) {
+      case 'B': return dispatcher.begin(id);
+      case 'E': return dispatcher.end(id);
+      default: return dispatcher.resolve(id);
+    }
+  }
+
+  /** @returns {ThreadDispatcher} */
+  _resolveDispatcher(cat) {
+    const mainCategory = cat ? cat.split(',', 1)[0] : '';
+    return this.dispatchers[mainCategory] || this.dispatchers.default;
+  }
+
+  categorize(tid) {
+    return _.findKey(this.categories, ([min, max]) => min <= tid && tid <= max) || 'default';
+  }
+
+  threadize(cat) {
+    if (!cat) {
+      return this.categories.default[0];
+    }
+
+    return _.find(this.categories, (_, key) => key === cat[0]);
+  }
+}
+
+module.exports = CategoryThreadDispatcher;
diff --git a/detox/src/logger/utils/MessageStack.js b/detox/src/logger/utils/MessageStack.js
new file mode 100644
index 000000000..275349958
--- /dev/null
+++ b/detox/src/logger/utils/MessageStack.js
@@ -0,0 +1,24 @@
+class MessageStack {
+  constructor() {
+    this._map = {};
+  }
+
+  push(tid, msg) {
+    if (this._map[tid] == null) {
+      this._map[tid] = [];
+    }
+
+    return this._map[tid].push(msg);
+  }
+
+  pop(tid) {
+    const stack = this._map[tid];
+    if (stack == null || stack.length === 0) {
+      return ['<no begin message>'];
+    }
+
+    return stack.pop();
+  }
+}
+
+module.exports = MessageStack;
diff --git a/detox/src/logger/TraceThreadDispatcher.js b/detox/src/logger/utils/ThreadDispatcher.js
similarity index 56%
rename from detox/src/logger/TraceThreadDispatcher.js
rename to detox/src/logger/utils/ThreadDispatcher.js
index 1cb7c72b2..452662bb6 100644
--- a/detox/src/logger/TraceThreadDispatcher.js
+++ b/detox/src/logger/utils/ThreadDispatcher.js
@@ -1,6 +1,6 @@
 const isUndefined = (x) => x === undefined;
 
-class TraceThreadDispatcher {
+class ThreadDispatcher {
   /**
    * @param {object} options
    * @param {Detox.Logger} options.logger
@@ -17,14 +17,31 @@ class TraceThreadDispatcher {
     this.threads = [];
   }
 
-  begin(id = 0) {
+  /**
+   * @param {string | number} [id]
+   * @returns {number}
+   */
+  begin(id) {
     const tid = this._findTID(id);
     this.threads[tid] = id;
     this.stacks[tid] = (this.stacks[tid] || 0) + 1;
     return this._transpose(tid);
   }
 
-  end(id = 0) {
+  /**
+   * @param {string | number} [id]
+   * @returns {number}
+   */
+  resolve(id) {
+    const tid = this._findTID(id);
+    return this._transpose(tid);
+  }
+
+  /**
+   * @param {string | number} [id]
+   * @returns {number}
+   */
+  end(id) {
     const tid = this._findTID(id);
     if (this.stacks[tid] && --this.stacks[tid] === 0) {
       delete this.threads[tid];
@@ -32,9 +49,17 @@ class TraceThreadDispatcher {
     return this._transpose(tid);
   }
 
+  /**
+   * @param {string | number | undefined} id
+   * @returns {number}
+   * @private
+   *
+   * Memory-efficient finder of a free item index in the threads array.
+   */
   _findTID(id) {
     let tid = this.threads.indexOf(id);
     if (tid === -1) {
+      // Try to find a recently released slot in the array:
       tid = this.threads.findIndex(isUndefined);
     }
     return tid === -1 ? this.threads.length : tid;
@@ -43,10 +68,14 @@ class TraceThreadDispatcher {
   _transpose(id) {
     const result = this.min + id;
     if (result > this.max) {
-      this.logger.warn({ event: 'THREAD_DISPATCHER' }, `${this.name} trace thread dispatcher has run out of available thread IDs: ${this.min}..${this.max}`);
+      this.logger.warn(
+        { cat: ['logger', 'thread-dispatcher'] },
+        `${this.name} trace thread dispatcher has run out of available thread IDs: ${this.min}..${this.max}`
+      );
     }
+
     return Math.min(result, this.max);
   }
 }
 
-module.exports = TraceThreadDispatcher;
+module.exports = ThreadDispatcher;
diff --git a/detox/src/logger/customConsoleLogger.js b/detox/src/logger/utils/customConsoleLogger.js
similarity index 79%
rename from detox/src/logger/customConsoleLogger.js
rename to detox/src/logger/utils/customConsoleLogger.js
index 43d67d1ba..c7d121678 100644
--- a/detox/src/logger/customConsoleLogger.js
+++ b/detox/src/logger/utils/customConsoleLogger.js
@@ -1,6 +1,6 @@
 const util = require('util');
 
-const callsites = require('../utils/callsites');
+const callsites = require('../../utils/callsites');
 
 const USER_STACK_FRAME_INDEX = 2;
 
@@ -25,20 +25,20 @@ function getStackDump() {
 
 function proxyLog(bunyanLoggerFn) {
   return (...args) => {
-    bunyanLoggerFn({ event: 'USER_LOG' }, getOrigin(), '\n', util.format(...args));
+    bunyanLoggerFn({ origin: getOrigin() }, util.format(...args));
   };
 }
 
 function proxyTracing(bunyanLoggerFn) {
   return (...args) => {
-    bunyanLoggerFn({ event: 'USER_LOG' }, getOrigin(), '\n  Trace:', util.format(...args), '\n\r' + getStackDump());
+    bunyanLoggerFn({ origin: getOrigin(), stack: getStackDump() }, util.format(...args));
   };
 }
 
 function proxyAssert(bunyanLoggerFn) {
   return (condition, ...args) => {
     if (!condition) {
-      bunyanLoggerFn({ event: 'USER_LOG' }, getOrigin(), '\n  AssertionError:', util.format(...args));
+      bunyanLoggerFn({ origin: getOrigin() }, 'AssertionError:', util.format(...args));
     }
   };
 }
diff --git a/detox/src/logger/utils/sanitizeBunyanContext.js b/detox/src/logger/utils/sanitizeBunyanContext.js
new file mode 100644
index 000000000..91ed6e252
--- /dev/null
+++ b/detox/src/logger/utils/sanitizeBunyanContext.js
@@ -0,0 +1,28 @@
+const _ = require('lodash');
+
+const RESERVED_PROPERTIES = [
+  'hostname',
+  'level',
+  'msg',
+  'name',
+  'pid',
+  'time',
+];
+
+function hasProperty(p) {
+  return _.has(this, p);
+}
+
+function hasReservedProperties(o) {
+  return RESERVED_PROPERTIES.some(hasProperty, o); // eslint-disable-line unicorn/no-array-method-this-argument
+}
+
+function escapeCallback(value, key) {
+  return RESERVED_PROPERTIES.includes(key) ? `${key}$` : key;
+}
+
+function sanitizeBunyanContext(context) {
+  return hasReservedProperties(context) ? _.mapKeys(context, escapeCallback) : context;
+}
+
+module.exports = sanitizeBunyanContext;
diff --git a/detox/src/logger/utils/tracerLegacy.js b/detox/src/logger/utils/tracerLegacy.js
new file mode 100644
index 000000000..867762503
--- /dev/null
+++ b/detox/src/logger/utils/tracerLegacy.js
@@ -0,0 +1,25 @@
+const methods = {
+  startSection(logger) {
+    return (msg, args) => logger.trace.begin(args, msg);
+  },
+
+  endSection(logger) {
+    return (msg, args) => logger.trace.end(args);
+  },
+
+  traceCall(logger) {
+    return (name, action) => logger.trace.complete(name, action);
+  },
+};
+
+function installLegacyTracerInterface(logger, target) {
+  target.traceCall = methods.traceCall(logger);
+  target.trace = Object.freeze({
+    startSection: methods.startSection(logger),
+    endSection: methods.endSection(logger),
+  });
+
+  return this;
+}
+
+module.exports = { install: installLegacyTracerInterface };
diff --git a/detox/src/realms/DetoxContext.js b/detox/src/realms/DetoxContext.js
index c03129f5e..73dadf153 100644
--- a/detox/src/realms/DetoxContext.js
+++ b/detox/src/realms/DetoxContext.js
@@ -1,17 +1,15 @@
 const funpermaproxy = require('funpermaproxy');
 
+const temporary = require('../artifacts/utils/temporaryPath');
 const { DetoxRuntimeError } = require('../errors');
-const DetoxLogger = require('../logger/DetoxLogger');
-const DetoxTracer = require('../logger/DetoxTracer');
+const { DetoxLogger, installLegacyTracerInterface } = require('../logger');
 const symbols = require('../symbols');
 
 const DetoxConstants = require('./DetoxConstants');
 
 const $cleanup = Symbol('cleanup');
-const $logger = Symbol('logger');
 const $restoreSessionState = Symbol('restoreSessionState');
 const $sessionState = Symbol('restoreSessionState');
-const $tracer = Symbol('tracer');
 const $status = Symbol('status');
 const $worker = Symbol('worker');
 
@@ -45,21 +43,19 @@ class DetoxContext {
 
     this[$sessionState] = this[$restoreSessionState]();
 
-    const loggerConfig = this[$sessionState].detoxConfig
-      ? this[$sessionState].detoxConfig.logger
-      : undefined;
-
     /**
-     * @protected
      * @type {DetoxLogger & Detox.Logger}
      */
-    this[$logger] = new DetoxLogger(loggerConfig);
-    /** @protected */
-    this[$tracer] = DetoxTracer.default({
-      logger: this[$logger],
+    this[symbols.logger] = new DetoxLogger({
+      file: temporary.for.jsonl(`${this[$sessionState].id}.${process.pid}`),
+      userConfig: this[$sessionState].detoxConfig
+        ? this[$sessionState].detoxConfig.logger
+        : null,
     });
-    /** @deprecated */
-    this.traceCall = this[$tracer].bind(this[$tracer]);
+
+    this.log = this[symbols.logger].child({ cat: 'user' });
+    installLegacyTracerInterface(this.log, this);
+
     /** @type {import('../DetoxWorker') | null} */
     this[$worker] = null;
   }
@@ -81,19 +77,6 @@ class DetoxContext {
     return DetoxConstants;
   }
 
-  /**
-   * @returns {Detox.Logger}
-   */
-  get log() {
-    return this[$logger];
-  }
-
-  /**
-   * @returns {Detox.Tracer}
-   */
-  get trace() {
-    return this[$tracer];
-  }
   //#endregion
 
   //#region Internal members
@@ -148,7 +131,7 @@ class DetoxContext {
   async [symbols.installWorker](opts) {
     if (opts.global) {
       opts.global['__detox__'] = this;
-      this[$logger].overrideConsole(opts.global);
+      this.log.overrideConsole(opts.global);
     }
 
     const DetoxWorker = require('../DetoxWorker');
@@ -188,10 +171,8 @@ class DetoxContext {
 module.exports = DetoxContext;
 module.exports.protected = {
   $cleanup,
-  $logger,
   $restoreSessionState,
   $status,
   $sessionState,
-  $tracer,
   $worker,
 };
diff --git a/detox/src/realms/DetoxInternalsFacade.js b/detox/src/realms/DetoxInternalsFacade.js
index 0e1f99836..7e2763cae 100644
--- a/detox/src/realms/DetoxInternalsFacade.js
+++ b/detox/src/realms/DetoxInternalsFacade.js
@@ -11,7 +11,7 @@ class DetoxInternalsFacade {
     this.getStatus = context[symbols.getStatus];
     this.init = context[symbols.init];
     this.cleanup = context[symbols.cleanup];
-    this.log = context.log;
+    this.log = context[symbols.logger];
     this.installWorker = context[symbols.installWorker];
     this.uninstallWorker = context[symbols.uninstallWorker];
     this.onHookFailure = context[symbols.onHookFailure];
diff --git a/detox/src/realms/DetoxPrimaryContext.js b/detox/src/realms/DetoxPrimaryContext.js
index bf48dfd1b..64ca2d412 100644
--- a/detox/src/realms/DetoxPrimaryContext.js
+++ b/detox/src/realms/DetoxPrimaryContext.js
@@ -1,18 +1,23 @@
 const path = require('path');
 const { URL } = require('url');
+const { promisify } = require('util');
 
 const fs = require('fs-extra');
+const glob = require('glob');
 const pipe = require('multipipe');
 const onSignalExit = require('signal-exit');
 
 const temporary = require('../artifacts/utils/temporaryPath');
 const { DetoxRuntimeError } = require('../errors');
-const { PrimarySessionState } = require('../ipc/state');
+const SessionState = require('../ipc/SessionState');
 const symbols = require('../symbols');
 
+const globAsync = promisify(glob);
+const globSync = glob.sync;
+
 const DetoxContext = require('./DetoxContext');
 
-const { $logger, $restoreSessionState, $sessionState, $worker } = DetoxContext.protected;
+const { $restoreSessionState, $sessionState, $worker } = DetoxContext.protected;
 
 const _finalizeLogs = Symbol('finalizeLogs');
 const _finalizeLogsSync = Symbol('finalizeLogsSync');
@@ -23,6 +28,7 @@ const _wss = Symbol('wss');
 const _dirty = Symbol('dirty');
 const _emergencyTeardown = Symbol('emergencyTeardown');
 const _areLogsEnabled = Symbol('areLogsEnabled');
+const _lifecycleLogger = Symbol('lifecycleLogger');
 
 class DetoxPrimaryContext extends DetoxContext {
   constructor() {
@@ -35,6 +41,8 @@ class DetoxPrimaryContext extends DetoxContext {
      * @type {import('../ipc/IPCServer') | null}
      */
     this[_ipcServer] = null;
+    /** @type {Detox.Logger} */
+    this[_lifecycleLogger] = this[symbols.logger].child({ cat: 'lifecycle' });
   }
 
   //#region Internal members
@@ -77,18 +85,16 @@ class DetoxPrimaryContext extends DetoxContext {
       logger: loggerConfig,
       session: sessionConfig
     } = detoxConfig;
-    await this[$logger].setConfig(loggerConfig);
+    await this[symbols.logger].setConfig(loggerConfig);
 
-    this.trace.begin({
-      cat: 'lifecycle',
-      args: this[$sessionState],
-      name: process.argv.slice(1).join(' '),
-    });
+    this[_lifecycleLogger].trace.begin({
+      data: this[$sessionState],
+    }, process.argv.slice(1).join(' '));
 
     const IPCServer = require('../ipc/IPCServer');
     this[_ipcServer] = new IPCServer({
       sessionState: this[$sessionState],
-      logger: this[$logger],
+      logger: this[symbols.logger],
     });
 
     await this[_ipcServer].init();
@@ -165,10 +171,10 @@ class DetoxPrimaryContext extends DetoxContext {
       await fs.remove(this[$sessionState].detoxConfigSnapshotPath);
 
       try {
-        this.trace.end({ cat: 'lifecycle' });
+        this[_lifecycleLogger].trace.end();
         await this[_finalizeLogs]();
       } catch (err) {
-        this[$logger].error({ err }, 'Encountered an error while merging the process logs:');
+        this[_lifecycleLogger].error({ err }, 'Encountered an error while merging the process logs:');
       }
     }
   }
@@ -192,10 +198,10 @@ class DetoxPrimaryContext extends DetoxContext {
     }
 
     try {
-      this.trace.end({ cat: 'lifecycle', args: { abortSignal: signal } });
+      this[_lifecycleLogger].trace.end({ abortSignal: signal });
       this[_finalizeLogsSync]();
     } catch (err) {
-      this[$logger].error({ err }, 'Encountered an error while merging the process logs:');
+      this[symbols.logger].error({ err }, 'Encountered an error while merging the process logs:');
     }
   };
 
@@ -205,10 +211,10 @@ class DetoxPrimaryContext extends DetoxContext {
   /**
    * @protected
    * @override
-   * @return {PrimarySessionState}
+   * @return {SessionState}
    */
   [$restoreSessionState]() {
-    return new PrimarySessionState({
+    return new SessionState({
       detoxConfigSnapshotPath: temporary.for.json(),
       detoxIPCServer: `primary-${process.pid}`,
     });
@@ -217,7 +223,8 @@ class DetoxPrimaryContext extends DetoxContext {
 
   //#region Private members
   async[_finalizeLogs]() {
-    const logs = [this[$logger].file, ...this[$sessionState].logFiles].filter(f => f && fs.existsSync(f));
+    const sessionId = this[$sessionState].id;
+    const logs = await globAsync(temporary.for.jsonl(`${sessionId}.*`));
     if (logs.length === 0) {
       return;
     }
@@ -237,7 +244,7 @@ class DetoxPrimaryContext extends DetoxContext {
 
       await Promise.all([
         pipe(mergedStream, streamUtils.writeJSONL(), out1Stream),
-        pipe(mergedStream, streamUtils.debugStream(this[$logger].config.options), out2Stream),
+        pipe(mergedStream, streamUtils.debugStream(this[symbols.logger].config.options), out2Stream),
         pipe(mergedStream, streamUtils.chromeTraceStream(), streamUtils.writeJSON(), out3Stream),
       ]);
     }
@@ -251,7 +258,9 @@ class DetoxPrimaryContext extends DetoxContext {
     const { rootDir } = this[symbols.config].artifacts;
     fs.mkdirpSync(rootDir);
 
-    const logs = [this[$logger].file, ...this[$sessionState].logFiles].filter(f => f && fs.existsSync(f));
+    const sessionId = this[$sessionState].id;
+    const logs = globSync(temporary.for.jsonl(`${sessionId}.*`));
+
     for (const log of logs) {
       if (logsEnabled) {
         fs.moveSync(log, path.join(rootDir, path.basename(log)));
diff --git a/detox/src/realms/DetoxSecondaryContext.js b/detox/src/realms/DetoxSecondaryContext.js
index aac859987..92b4f4267 100644
--- a/detox/src/realms/DetoxSecondaryContext.js
+++ b/detox/src/realms/DetoxSecondaryContext.js
@@ -1,12 +1,12 @@
 const fs = require('fs');
 
 const { DetoxInternalError } = require('../errors');
-const { SecondarySessionState } = require('../ipc/state');
+const SessionState = require('../ipc/SessionState');
 const symbols = require('../symbols');
 
 const DetoxContext = require('./DetoxContext');
 
-const { $logger, $restoreSessionState, $sessionState, $worker } = DetoxContext.protected;
+const { $restoreSessionState, $sessionState, $worker } = DetoxContext.protected;
 const _ipcClient = Symbol('ipcClient');
 const _shortLifecycle = Symbol('shortLifecycle');
 
@@ -43,14 +43,12 @@ class DetoxSecondaryContext extends DetoxContext {
 
   /** @override */
   async [symbols.init](opts = {}) {
-    this[$logger].overrideConsole();
-
     const IPCClient = require('../ipc/IPCClient');
 
     this[_ipcClient] = new IPCClient({
       id: `secondary-${process.pid}`,
       state: this[$sessionState],
-      logger: this[$logger],
+      logger: this[symbols.logger],
     });
 
     await this[_ipcClient].init();
@@ -86,10 +84,10 @@ class DetoxSecondaryContext extends DetoxContext {
   /**
    * @protected
    * @override
-   * @return {SecondarySessionState}
+   * @return {SessionState}
    */
   [$restoreSessionState]() {
-    return SecondarySessionState.parse(fs.readFileSync(process.env.DETOX_CONFIG_SNAPSHOT_PATH));
+    return SessionState.parse(fs.readFileSync(process.env.DETOX_CONFIG_SNAPSHOT_PATH));
   }
   //#endregion
 }
diff --git a/detox/src/server/DetoxConnection.js b/detox/src/server/DetoxConnection.js
index 633842cef..1aa143824 100644
--- a/detox/src/server/DetoxConnection.js
+++ b/detox/src/server/DetoxConnection.js
@@ -1,43 +1,37 @@
-// @ts-nocheck
 const _ = require('lodash');
-const { WebSocket } = require('ws'); // eslint-disable-line @typescript-eslint/no-unused-vars, no-unused-vars
 
 const DetoxRuntimeError = require('../errors/DetoxRuntimeError');
-const logger = require('../utils/logger').child({ __filename, cat: 'ws-server,ws' });
+const logger = require('../utils/logger').child({ cat: 'ws-server,ws' });
 
 const AnonymousConnectionHandler = require('./handlers/AnonymousConnectionHandler');
 
-const EVENTS = {
-  NEW: { event: 'WSS_CONNECTION' },
-  GET: { event: 'WSS_GET_FROM' },
-  SEND: { event: 'WSS_SEND_TO' },
-  ERROR: { event: 'ERROR' },
-  SOCKET_ERROR: { event: 'WSS_SOCKET_ERROR' },
-};
-
 class DetoxConnection {
   /**
-   * @param {DetoxSessionManager} sessionManager
-   * @param {WebSocket} webSocket
-   * @param {Socket} socket
+   * @param {{
+   *   sessionManager: import('./DetoxSessionManager');
+   *   webSocket: import('ws');
+   *   socket: import('net').Socket;
+   * }} config
    */
   constructor({ sessionManager, webSocket, socket }) {
     this._onMessage = this._onMessage.bind(this);
     this._onError = this._onError.bind(this);
     this._onClose = this._onClose.bind(this);
 
-    this._log = logger.child({ trackingId: socket.remotePort });
+    this._log = logger.child({ id: socket.remotePort });
+    this._log.debug.begin(`connection :${socket.localPort}<->:${socket.remotePort}`);
+
     this._sessionManager = sessionManager;
     this._webSocket = webSocket;
     this._webSocket.on('message', this._onMessage);
     this._webSocket.on('error', this._onError);
     this._webSocket.on('close', this._onClose);
-    this._log.debug(EVENTS.NEW, 'registered a new connection.');
 
-    const log = this._log;
+    // eslint-disable-next-line unicorn/no-this-assignment
+    const self = this;
     this._handler = new AnonymousConnectionHandler({
       api: {
-        get log() { return log; },
+        get log() { return self._log; },
         appendLogDetails: (details) => { this._log = this._log.child(details); },
 
         registerSession: (params) => this._sessionManager.registerSession(this, params),
@@ -49,13 +43,13 @@ class DetoxConnection {
 
   sendAction(action) {
     const messageAsString = JSON.stringify(action);
-    this._log.trace(EVENTS.SEND, messageAsString);
+    this._log.trace({ data: action }, 'send');
     this._webSocket.send(messageAsString + '\n ');
   }
 
   _onMessage(rawData) {
     const data = _.isString(rawData) ? rawData : rawData.toString('utf8');
-    this._log.trace(EVENTS.GET, data);
+    this._log.trace({ data }, 'get');
 
     try {
       let action;
@@ -84,16 +78,17 @@ class DetoxConnection {
         this._handler.onError(handlerError, action);
       }
     } catch (error) {
-      this._log.warn({ ...EVENTS.ERROR }, error instanceof DetoxRuntimeError ? error.message : `${error}`);
+      this._log.warn({ error }, 'Caught unhandled error:');
     }
   }
 
-  _onError(e) {
-    this._log.warn(EVENTS.SOCKET_ERROR, DetoxRuntimeError.format(e));
+  _onError(error) {
+    this._log.warn({ error }, 'Caught socket error:');
   }
 
   _onClose() {
     this._sessionManager.unregisterConnection(this._webSocket);
+    this._log.debug.end();
   }
 }
 
diff --git a/detox/src/server/DetoxServer.js b/detox/src/server/DetoxServer.js
index be0673a41..1ab5f0869 100644
--- a/detox/src/server/DetoxServer.js
+++ b/detox/src/server/DetoxServer.js
@@ -4,7 +4,7 @@ const WebSocket = require('ws');
 
 const { DetoxInternalError, DetoxRuntimeError } = require('../errors');
 const Deferred = require('../utils/Deferred');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'ws-server,ws' });
 
 const DetoxSessionManager = require('./DetoxSessionManager');
 
@@ -40,18 +40,16 @@ class DetoxServer {
     await this._startListening();
 
     const level = this._options.standalone ? 'info' : 'debug';
-    log[level]({ event: 'WSS_CREATE' }, `Detox server listening on localhost:${this.port}...`);
+    log[level](`Detox server listening on localhost:${this.port}...`);
   }
 
   async close() {
     try {
       await this._closeWithTimeout(10000);
-      log.debug({ event: 'WSS_CLOSE' }, 'Detox server has been closed gracefully');
-    } catch (e) {
-      log.warn({ event: 'ERROR' },
-        `Detox server has been closed abruptly! See the error details below:\n`
-        + DetoxRuntimeError.format(e) + '\n'
-        + DetoxRuntimeError.reportIssue
+      log.debug('Detox server has been closed gracefully');
+    } catch (err) {
+      log.warn({ err },
+        `Detox server has been closed abruptly! See the error details below:`
       );
     }
   }
@@ -90,8 +88,7 @@ class DetoxServer {
     } else if (this._closing) {
       this._closing.reject(err);
     } else {
-      const formattedError = DetoxRuntimeError.format(err);
-      log.error('Detox server has got an unhandled error:\n' + formattedError);
+      log.error({ err }, 'Detox server has got an unhandled error:');
     }
   }
 
diff --git a/detox/src/server/DetoxSession.js b/detox/src/server/DetoxSession.js
index 5537f0c77..bec12bc2b 100644
--- a/detox/src/server/DetoxSession.js
+++ b/detox/src/server/DetoxSession.js
@@ -1,6 +1,6 @@
 // @ts-nocheck
 const DetoxInternalError = require('../errors/DetoxInternalError');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'ws-server,ws-session' });
 
 class DetoxSession {
   /**
@@ -17,7 +17,7 @@ class DetoxSession {
     /** @type {boolean | null} */
     this._pendingTesterStatus = null;
 
-    log.trace({ event: 'SESSION_CREATED' }, `created session ${id}`);
+    log.trace(`created session ${id}`);
   }
 
   get id() {
@@ -120,7 +120,7 @@ class DetoxSession {
   }
 
   _notifyAboutAppConnect() {
-    log.trace({ event: 'SESSION_JOINED' }, `app joined session ${this.id}`);
+    log.trace(`app joined session ${this.id}`);
 
     if (!this._tester) {
       return;
@@ -132,7 +132,7 @@ class DetoxSession {
   }
 
   _notifyAboutAppDisconnect() {
-    log.trace({ event: 'SESSION_TORN' }, `app exited session ${this.id}`);
+    log.trace(`app exited session ${this.id}`);
 
     if (!this._tester) {
       return;
@@ -144,11 +144,11 @@ class DetoxSession {
   }
 
   _notifyAboutTesterConnect() {
-    log.trace({ event: 'SESSION_JOINED' }, `tester joined session ${this.id}`);
+    log.trace(`tester joined session ${this.id}`);
   }
 
   _notifyAboutTesterDisconnect() {
-    log.trace({ event: 'SESSION_TORN' }, `tester exited session ${this.id}`);
+    log.trace(`tester exited session ${this.id}`);
 
     if (!this._app) {
       return;
diff --git a/detox/src/server/DetoxSessionManager.js b/detox/src/server/DetoxSessionManager.js
index b423e1496..bdead1fae 100644
--- a/detox/src/server/DetoxSessionManager.js
+++ b/detox/src/server/DetoxSessionManager.js
@@ -1,6 +1,6 @@
 // @ts-nocheck
 const DetoxInternalError = require('../errors/DetoxInternalError');
-const log = require('../utils/logger').child({ __filename });
+const log = require('../utils/logger').child({ cat: 'ws-server,ws' });
 
 const DetoxConnection = require('./DetoxConnection');
 const DetoxSession = require('./DetoxSession');
diff --git a/detox/src/server/__mocks__/FakeWebSocket.js b/detox/src/server/__mocks__/FakeWebSocket.js
index 5a5b22a71..15c777b26 100644
--- a/detox/src/server/__mocks__/FakeWebSocket.js
+++ b/detox/src/server/__mocks__/FakeWebSocket.js
@@ -1,5 +1,6 @@
 class FakeWebSocket {
-  constructor({ remotePort }) {
+  constructor({ localPort, remotePort }) {
+    this._localPort = localPort;
     this._remotePort = remotePort;
     this._events = {};
 
@@ -52,6 +53,10 @@ class FakeNetworkSocket {
     this._owner = owner;
   }
 
+  get localPort() {
+    return this._owner._localPort;
+  }
+
   get remotePort() {
     return this._owner._remotePort;
   }
diff --git a/detox/src/server/handlers/RegisteredConnectionHandler.js b/detox/src/server/handlers/RegisteredConnectionHandler.js
index 420c532aa..3103c40da 100644
--- a/detox/src/server/handlers/RegisteredConnectionHandler.js
+++ b/detox/src/server/handlers/RegisteredConnectionHandler.js
@@ -1,7 +1,6 @@
 // @ts-nocheck
 const { serializeError } = require('serialize-error');
 
-const DetoxError = require('../../errors/DetoxError');
 const DetoxInternalError = require('../../errors/DetoxInternalError');
 
 class RegisteredConnectionHandler {
@@ -41,7 +40,7 @@ class RegisteredConnectionHandler {
         messageId: action && action.messageId,
       });
     } catch (err) {
-      this._api.log.error('Cannot forward the error details to the tester due to the error:\n' + DetoxError.format(err));
+      this._api.log.error({ err }, 'Cannot forward the error details to the tester due to the error:');
       throw error;
     }
   }
diff --git a/detox/src/symbols.js b/detox/src/symbols.js
index 372af05ef..360c2ad91 100644
--- a/detox/src/symbols.js
+++ b/detox/src/symbols.js
@@ -5,6 +5,7 @@
  *   readonly getStatus: unique symbol;
  *   readonly init: unique symbol;
  *   readonly installWorker: unique symbol;
+ *   readonly logger: unique symbol;
  *   readonly onHookFailure: unique symbol;
  *   readonly onHookStart: unique symbol;
  *   readonly onHookSuccess: unique symbol;
@@ -50,6 +51,7 @@ module.exports = {
   getStatus: Symbol('getStatus'),
   init: Symbol('init'),
   installWorker: Symbol('installWorker'),
+  logger: Symbol('logger'),
   resolveConfig: Symbol('resolveConfig'),
   session: Symbol('session'),
   uninstallWorker: Symbol('uninstallWorker'),
diff --git a/detox/src/utils/__mocks__/trace.js b/detox/src/utils/__mocks__/trace.js
index 8aea02f3d..78c2494c6 100644
--- a/detox/src/utils/__mocks__/trace.js
+++ b/detox/src/utils/__mocks__/trace.js
@@ -1,22 +1,11 @@
-const traceCall = jest.fn().mockImplementation((_, fn) => fn());
-
-function traceMethods(obj, cat, methodNames) {
-  for (const name of methodNames) {
-    const originalMethod = obj[name];
-
-    obj[name] = function tracedMethod() {
-      return traceCall({ cat, name }, originalMethod.apply.bind(originalMethod, obj, arguments));
-    };
-  }
-}
+const traceCall = jest.fn().mockImplementation((_, fn) => {
+  return typeof fn === 'function' ? fn() : fn;
+});
 
 module.exports = {
   trace: {
-    begin: jest.fn(() => ({ payload: {}, end: jest.fn() })),
-    end: jest.fn(),
     startSection: jest.fn(),
     endSection: jest.fn(),
   },
   traceCall,
-  traceMethods,
 };
diff --git a/detox/src/utils/childProcess/exec.js b/detox/src/utils/childProcess/exec.js
index b79fcb476..4cf25c7aa 100644
--- a/detox/src/utils/childProcess/exec.js
+++ b/detox/src/utils/childProcess/exec.js
@@ -3,7 +3,7 @@ const { exec } = require('child-process-promise');
 const _ = require('lodash');
 
 const DetoxRuntimeError = require('../../errors/DetoxRuntimeError');
-const rootLogger = require('../logger').child({ __filename });
+const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-exec'] });
 const retry = require('../retry');
 
 const execsCounter = require('./opsCounter');
diff --git a/detox/src/utils/childProcess/spawn.js b/detox/src/utils/childProcess/spawn.js
index cfbb2a001..b0f0ed30d 100644
--- a/detox/src/utils/childProcess/spawn.js
+++ b/detox/src/utils/childProcess/spawn.js
@@ -2,7 +2,7 @@
 const { spawn } = require('child-process-promise');
 const _ = require('lodash');
 
-const rootLogger = require('../logger').child({ __filename });
+const rootLogger = require('../logger').child({ cat: ['child-process', 'child-process-spawn'] });
 const { escape } = require('../pipeCommands');
 const retry = require('../retry');
 
diff --git a/detox/src/utils/streamUtils.js b/detox/src/utils/streamUtils.js
index 5fb5e7e27..b1639bd80 100644
--- a/detox/src/utils/streamUtils.js
+++ b/detox/src/utils/streamUtils.js
@@ -8,9 +8,7 @@ const JsonlParser = require('stream-json/jsonl/Parser');
 const stripAnsi = require('strip-ansi');
 const { AbstractEventBuilder } = require('trace-event-lib');
 
-const DetoxTracer = require('../logger/DetoxTracer');
-
-const log = require('./logger').child({ __filename });
+const log = require('./logger').child({ cat: 'logger' });
 
 function compareTimestamps(a, b) {
   return +(a.value.time > b.value.time) - +(a.value.time < b.value.time);
@@ -136,6 +134,7 @@ class JsonlStringer extends Transform {
 
 class SimpleEventBuilder extends AbstractEventBuilder {
   events = [];
+
   send(event) {
     this.events.push(event);
   }
@@ -147,8 +146,7 @@ function chromeTraceStream() {
 
   return flatMapTransform((data) => {
     // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
-    const { pid, trace, msg, time, name: _name, hostname: _hostname, ...args } = data;
-    const tid = trace ? trace.tid : DetoxTracer.threadize(args.cat);
+    const { cat = 'default', msg: name, ph = 'i', pid, tid, time, name: _name, hostname: _hostname, ...args } = data;
     const ts = new Date(time).getTime() * 1E3;
 
     const builder = new SimpleEventBuilder();
@@ -159,14 +157,15 @@ function chromeTraceStream() {
 
     const tidHash = `${pid}:${tid}`;
     if (!knownTids.has(tidHash)) {
-      builder.thread_name(DetoxTracer.categorize(tid), tid, pid);
+      const primaryCategory = cat.split(',', 1)[0];
+      builder.thread_name(primaryCategory, tid, pid);
       builder.thread_sort_index(tid, tid, pid);
       knownTids.add(tidHash);
     }
 
-    const event = { ph: 'i', ...data.trace, pid, tid, ts, args };
-    if (!trace || trace.ph !== 'E') {
-      event.name = msg || '';
+    const event = { ph, name, pid, tid, cat, ts, args };
+    if (ph === 'E') {
+      delete event.name;
     }
 
     builder.events.push(event);
@@ -190,11 +189,11 @@ function preventErrorSubscriptions(emitter) {
 }
 
 function readJSONL() {
-  const readable = new PassThrough({ objectMode: true });
+  const readable = through();
   const writable = JsonlParser.make({ checkErrors: true })
     .on('error', (err) => {
       if (err instanceof SyntaxError) {
-        log.debug({ event: 'JSONL_ERROR', err });
+        log.debug({ err });
         readable.end();
       } else {
         readable.emit('error', err);
diff --git a/detox/src/utils/trace.js b/detox/src/utils/trace.js
index 34b5502be..c5891a1d3 100644
--- a/detox/src/utils/trace.js
+++ b/detox/src/utils/trace.js
@@ -1,19 +1,3 @@
-/** @type {Detox.Tracer} */
-const trace = require('../..').trace;
-const traceCall = trace.bind(null);
+const { trace, traceCall } = require('../..');
 
-function traceMethods(obj, cat, methodNames) {
-  for (const name of methodNames) {
-    const originalMethod = obj[name];
-
-    obj[name] = function tracedMethod() {
-      return traceCall({ cat, name }, originalMethod.apply.bind(originalMethod, obj, arguments));
-    };
-  }
-}
-
-module.exports = {
-  trace,
-  traceCall,
-  traceMethods,
-};
+module.exports = { trace, traceCall };
diff --git a/detox/src/utils/traceMethods.js b/detox/src/utils/traceMethods.js
new file mode 100644
index 000000000..7a93d2ed1
--- /dev/null
+++ b/detox/src/utils/traceMethods.js
@@ -0,0 +1,15 @@
+function traceMethods(logger, obj, methodNames) {
+  for (const name of methodNames) {
+    const originalMethod = obj[name];
+
+    obj[name] = function tracedMethod(...args) {
+      return logger.trace.complete(
+        { args },
+        name,
+        originalMethod.apply.bind(originalMethod, obj, args)
+      );
+    };
+  }
+}
+
+module.exports = traceMethods;
diff --git a/scripts/demo-projects.android.sh b/scripts/demo-projects.android.sh
index 9f4b0f023..dcdab6212 100755
--- a/scripts/demo-projects.android.sh
+++ b/scripts/demo-projects.android.sh
@@ -30,11 +30,6 @@ if [ "$REACT_NATIVE_COMPAT_TEST" = "true" ]; then
   exit 0
 fi
 
-pushd examples/demo-react-native
-run_f "npm run test:android-release-ci"
-DETOX_EXPOSE_GLOBALS=0 run_f "npm run test:android-release-ci"
-popd
-
 pushd examples/demo-plugin
 run_f "npm run test:plugin"
 popd


Test patch:
diff --git a/detox/__tests__/setupJest.js b/detox/__tests__/setupJest.js
index ba7ece9f1..22acf6966 100644
--- a/detox/__tests__/setupJest.js
+++ b/detox/__tests__/setupJest.js
@@ -1,4 +1,5 @@
 jest.mock('proper-lockfile');
+jest.mock('signal-exit');
 jest.mock('../src/logger/DetoxLogger');
 
 const path = require('path');
@@ -37,5 +38,5 @@ function callCli(modulePath, cmd) {
   });
 }
 
+// @ts-ignore
 global.callCli = callCli;
-global.IS_RUNNING_DETOX_UNIT_TESTS = true;
diff --git a/detox/local-cli/test.test.js b/detox/local-cli/test.test.js
index 45de42c3b..948b51ab5 100644
--- a/detox/local-cli/test.test.js
+++ b/detox/local-cli/test.test.js
@@ -504,7 +504,7 @@ describe('CLI', () => {
     return {
       ...mockCall,
       fullCommand: _.chain(logger().log.mock.calls)
-        .filter(([_level, _childMeta, meta]) => meta && meta.event === 'RUN_START')
+        .filter(([_level, _childMeta, meta]) => meta && meta.env)
         .get(index)
         .get(3)
         .value(),
diff --git a/detox/local-cli/testCommand/TestRunnerCommand.js b/detox/local-cli/testCommand/TestRunnerCommand.js
index da8bed9ab..f5aea481e 100644
--- a/detox/local-cli/testCommand/TestRunnerCommand.js
+++ b/detox/local-cli/testCommand/TestRunnerCommand.js
@@ -5,6 +5,7 @@ const parser = require('yargs-parser');
 const unparse = require('yargs-unparser');
 
 const detox = require('../../internals');
+const log = detox.log.child({ cat: ['lifecycle', 'cli'] });
 const { DetoxRuntimeError } = require('../../src/errors');
 const { printEnvironmentVariables, prependNodeModulesBinToPATH } = require('../../src/utils/envUtils');
 const { escapeSpaces } = require('../../src/utils/shellUtils');
@@ -45,7 +46,7 @@ class TestRunnerCommand {
   }
 
   /**
-   * @param {Partial<Readonly<DetoxInternals.DetoxCLIConfig>>} cliConfig
+   * @param {Partial<Readonly<DetoxInternals.CLIConfig>>} cliConfig
    * @returns {this}
    */
   replicateCLIConfig(cliConfig) {
@@ -88,7 +89,7 @@ class TestRunnerCommand {
       try {
         if (launchError) {
           const list = this._argv._.map((file, index) => `  ${index + 1}. ${file}`).join('\n');
-          detox.log.error({ event: 'RETRY_RUN' },
+          log.error(
             `There were failing tests in the following files:\n${list}\n\n` +
             'Detox CLI is going to restart the test runner with those files...\n'
           );
@@ -119,7 +120,7 @@ class TestRunnerCommand {
     const fullCommand = this._buildSpawnArguments().map(escapeSpaces);
     const fullCommandWithHint = printEnvironmentVariables(this._envHint) + fullCommand.join(' ');
 
-    detox.log.info({ event: 'RUN_START', env: this._envHint }, fullCommandWithHint);
+    log.info({ env: this._envHint }, fullCommandWithHint);
 
     return new Promise((resolve, reject) => {
       cp.spawn(fullCommand[0], fullCommand.slice(1), {
diff --git a/detox/runners/jest/testEnvironment/index.js b/detox/runners/jest/testEnvironment/index.js
index 24cd3a335..3788e1c17 100644
--- a/detox/runners/jest/testEnvironment/index.js
+++ b/detox/runners/jest/testEnvironment/index.js
@@ -3,7 +3,6 @@ const maybeNodeEnvironment = require('jest-environment-node'); // eslint-disable
 const NodeEnvironment = maybeNodeEnvironment.default || maybeNodeEnvironment;
 
 const detox = require('../../../internals');
-const { DetoxError } = require('../../../src/errors');
 const Timer = require('../../../src/utils/Timer');
 
 const {
@@ -24,6 +23,8 @@ const SYNC_CIRCUS_EVENTS = new Set([
   'error',
 ]);
 
+const log = detox.log.child({ cat: 'lifecycle,jest-environment' });
+
 /**
  * @see https://www.npmjs.com/package/jest-circus#overview
  */
@@ -50,32 +51,30 @@ class DetoxCircusEnvironment extends NodeEnvironment {
     /** @protected */
     this.initTimeout = detox.config.testRunner.jest.initTimeout;
     /** @internal */
-    this.traceEvent = detox.trace.begin({
-      name: this.testPath,
-      cat: 'lifecycle',
-    });
+    log.trace.begin(this.testPath);
+
+    this.setup = log.trace.complete.bind(null, 'set up environment', this.setup.bind(this));
+    const _teardown = this.teardown.bind(this);
+    this.teardown = async () => {
+      try {
+        await log.trace.complete('tear down environment', _teardown);
+      } finally {
+        await log.trace.end();
+      }
+    };
   }
 
   /** @override */
   async setup() {
-    try {
-      this.traceEvent.begin({ name: 'set up environment' });
-
-      await super.setup();
-      await Timer.run({
-        description: `setting up Detox environment`,
-        timeout: this.initTimeout,
-        fn: async () => {
-          await this.initDetox();
-          this._instantiateListeners();
-        },
-      });
-
-      this.traceEvent.end({ args: { success: true } });
-    } catch (error) {
-      this.traceEvent.end({ args: { success: false, error } });
-      throw error;
-    }
+    await super.setup();
+    await Timer.run({
+      description: `setting up Detox environment`,
+      timeout: this.initTimeout,
+      fn: async () => {
+        await this.initDetox();
+        this._instantiateListeners();
+      },
+    });
   }
 
   /** @override */
@@ -100,7 +99,7 @@ class DetoxCircusEnvironment extends NodeEnvironment {
         try {
           await this._timer.run(() => listener[name](event, state));
         } catch (listenerError) {
-          this._logError(listenerError);
+          log.error(listenerError);
           break;
         }
       }
@@ -112,23 +111,11 @@ class DetoxCircusEnvironment extends NodeEnvironment {
 
   /** @override */
   async teardown() {
-    try {
-      this.traceEvent.begin({ name: 'tear down environment' });
-
-      await Timer.run({
-        description: `tearing down Detox environment`,
-        timeout: this.initTimeout,
-        fn: async () => this.cleanupDetox(),
-      });
-
-      this.traceEvent.end({ args: { success: true } });
-    } catch (error) {
-      this.traceEvent.end({ args: { success: false, error } });
-      throw error;
-    } finally {
-      this.traceEvent.end();
-      this.traceEvent = null;
-    }
+    await Timer.run({
+      description: `tearing down Detox environment`,
+      timeout: this.initTimeout,
+      fn: async () => await this.cleanupDetox(),
+    });
   }
 
   /** @protected */
@@ -180,11 +167,6 @@ class DetoxCircusEnvironment extends NodeEnvironment {
       }));
     }
   }
-
-  /** @private */
-  _logError(e) {
-    detox.log.error(DetoxError.format(e));
-  }
 }
 
 module.exports = DetoxCircusEnvironment;
diff --git a/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js b/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
index 648336507..af94a9d46 100644
--- a/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
+++ b/detox/runners/jest/testEnvironment/listeners/DetoxCoreListener.js
@@ -7,14 +7,14 @@ const { getFullTestName, hasTimedOut } = require('../utils');
 
 const RETRY_TIMES = Symbol.for('RETRY_TIMES');
 
+const log = detoxInternals.log.child({ cat: 'lifecycle,jest-environment' });
+
 class DetoxCoreListener {
   constructor({ env }) {
     this._startedTests = new WeakSet();
     this._testsFailedBeforeStart = new WeakSet();
     this._env = env;
     this._testRunTimes = 1;
-    /** @type import('trace-event-lib').DurationEventHandle */
-    this._testTraceEvent = null;
   }
 
   async setup() {
@@ -24,17 +24,19 @@ class DetoxCoreListener {
     }
   }
 
-  async run_describe_start({ describeBlock: { name, children } }) {
-    if (children.length) {
-      this._env.traceEvent.begin({ name });
-      await detoxInternals.onRunDescribeStart({ name });
+  async run_describe_start({ describeBlock }) {
+    if (describeBlock.children.length) {
+      log.trace.begin(describeBlock.parent ? describeBlock.name : 'run the tests');
+      await detoxInternals.onRunDescribeStart({
+        name: describeBlock.name,
+      });
     }
   }
 
-  async run_describe_finish({ describeBlock: { name, children } }) {
-    if (children.length) {
-      await detoxInternals.onRunDescribeFinish({ name });
-      this._env.traceEvent.end();
+  async run_describe_finish({ describeBlock }) {
+    if (describeBlock.children.length) {
+      await detoxInternals.onRunDescribeFinish({ name: describeBlock.name });
+      log.trace.end();
     }
   }
 
@@ -49,82 +51,46 @@ class DetoxCoreListener {
 
   async hook_start(event, state) {
     await this._onBeforeActualTestStart(state.currentlyRunningTest);
-
-    if (this._testTraceEvent) {
-      this._testTraceEvent.begin({
-        name: event.hook.type,
-        args: {
-          fn: event.hook.fn.toString()
-        },
-      });
-    }
+    log.trace.begin({ functionCode: event.hook.fn.toString() }, event.hook.type);
   }
 
   async hook_success() {
-    if (this._testTraceEvent) {
-      this._testTraceEvent.end({
-        args: { success: true }
-      });
-    }
+    log.trace.end({ success: true });
   }
 
   async hook_failure({ error }) {
-    if (this._testTraceEvent) {
-      this._testTraceEvent.end({
-        args: { success: false, error: error.toString() }
-      });
-    }
+    log.trace.end({ success: false, error });
   }
 
   async test_fn_start({ test }) {
     await this._onBeforeActualTestStart(test);
-
-    if (this._testTraceEvent) {
-      this._testTraceEvent.begin({
-        name: 'test_fn',
-        args: { fn: test.fn.toString() }
-      });
-    }
+    log.trace.begin({ functionCode: test.fn.toString() }, 'test_fn');
   }
 
-  async test_fn_success({ test }) {
-    await this._onBeforeActualTestStart(test);
-
-    if (this._testTraceEvent) {
-      this._testTraceEvent.end({
-        args: { success: true }
-      });
-    }
+  async test_fn_success() {
+    log.trace.end({ success: true });
   }
 
   async test_fn_failure({ error }) {
     await detoxInternals.onTestFnFailure({ error });
-
-    if (this._testTraceEvent) {
-      this._testTraceEvent.end({
-        args: { success: false, error: error.toString() }
-      });
-    }
+    log.trace.end({ success: false, error });
   }
 
   async test_done({ test }) {
     if (this._startedTests.has(test)) {
-      await detoxInternals.onTestDone({
-        title: test.name,
-        fullName: getFullTestName(test),
-        status: test.errors.length ? 'failed' : 'passed',
-        invocations: this._getTestInvocations(test),
-        timedOut: hasTimedOut(test)
-      });
-
+      const failed = test.errors.length > 0;
+      const metadata = {
+        ...this._getTestMetadata(test),
+        status: failed ? 'failed' : 'passed',
+        timedOut: failed ? hasTimedOut(test) : undefined,
+      };
+
+      await detoxInternals.onTestDone(metadata);
       this._startedTests.delete(test);
-      this._testTraceEvent.end({
-        args: {
-          success: !test.errors.length,
-          errors: test.errors.map(String).join('\n\n'),
-        },
+      log.trace.end({
+        status: metadata.status,
+        timedOut: metadata.timedOut,
       });
-      this._testTraceEvent = null;
     }
   }
 
@@ -141,18 +107,21 @@ class DetoxCoreListener {
       return false;
     }
 
+    const metadata = {
+      ...this._getTestMetadata(test),
+      status: 'running',
+    };
+
     this._startedTests.add(test);
-    this._testTraceEvent = this._env.traceEvent.begin({
-      name: test.name,
-    });
 
-    await detoxInternals.onTestStart({
-      title: test.name,
-      fullName: getFullTestName(test),
-      status: 'running',
-      invocations: this._getTestInvocations(test),
-    });
+    log.trace.begin({
+      context: 'test',
+      status: metadata.status,
+      fullName: metadata.fullName,
+      invocations: metadata.invocations,
+    }, metadata.title);
 
+    await detoxInternals.onTestStart(metadata);
     return true;
   }
 
@@ -172,6 +141,14 @@ class DetoxCoreListener {
 
     return block.errors ? block.errors.length > 0 : false;
   }
+
+  _getTestMetadata(test) {
+    return {
+      title: test.name,
+      fullName: getFullTestName(test),
+      invocations: this._getTestInvocations(test),
+    };
+  }
 }
 
 module.exports = DetoxCoreListener;
diff --git a/detox/runners/jest/testEnvironment/listeners/SpecReporter.js b/detox/runners/jest/testEnvironment/listeners/SpecReporter.js
index 368e78f2d..e5ef52e65 100644
--- a/detox/runners/jest/testEnvironment/listeners/SpecReporter.js
+++ b/detox/runners/jest/testEnvironment/listeners/SpecReporter.js
@@ -118,7 +118,7 @@ class SpecReporter {
     const retriesDescription = (invocations > 1) ? chalk.gray(` [Retry #${invocations - 1}]`) : '';
     const status = chalk.gray(_status ? ` [${_status}]` : '');
     const desc = this._suitesDesc + testDescription + retriesDescription + status;
-    log.info({ event: 'SPEC_STATE_CHANGE' }, desc);
+    log.info({ cat: 'lifecycle' }, desc);
   }
 }
 
diff --git a/detox/runners/jest/testEnvironment/listeners/WorkerAssignReporter.js b/detox/runners/jest/testEnvironment/listeners/WorkerAssignReporter.js
index 1cda09f8c..c5783fbf8 100644
--- a/detox/runners/jest/testEnvironment/listeners/WorkerAssignReporter.js
+++ b/detox/runners/jest/testEnvironment/listeners/WorkerAssignReporter.js
@@ -13,7 +13,7 @@ class WorkerAssignReporter {
 
   run_start() {
     if (config.testRunner.jest.reportWorkerAssign) {
-      log.info({ event: 'WORKER_ASSIGN' }, `${this._formatTestName()} is assigned to ${this._formatDeviceName()}`);
+      log.info({ cat: 'lifecycle' }, `${this._formatTestName()} is assigned to ${this._formatDeviceName()}`);
     }
   }
 
diff --git a/detox/src/artifacts/ArtifactsManager.test.js b/detox/src/artifacts/ArtifactsManager.test.js
index b46163c3e..d7d056e99 100644
--- a/detox/src/artifacts/ArtifactsManager.test.js
+++ b/detox/src/artifacts/ArtifactsManager.test.js
@@ -230,11 +230,10 @@ describe('ArtifactsManager', () => {
             expect(proxy.logger.warn.mock.calls).toEqual([[
               {
                 err: expect.any(Error),
-                event: 'ERROR',
                 methodName: hookName,
                 plugin: 'testPlugin',
               },
-              expect.stringContaining(`Suppressed error inside function call: testPlugin.${hookName}`)
+              expect.stringContaining(`Suppressed error inside function call.`)
             ]]);
           });
         }
diff --git a/detox/src/client/AsyncWebSocket.test.js b/detox/src/client/AsyncWebSocket.test.js
index e4c63621f..c410b8f2f 100644
--- a/detox/src/client/AsyncWebSocket.test.js
+++ b/detox/src/client/AsyncWebSocket.test.js
@@ -169,12 +169,9 @@ describe('AsyncWebSocket', () => {
         socket.mockMessage({ type: 'somethingElse' });
 
         expect(onErrorCallback).not.toHaveBeenCalled();
-        expect(log.error).toHaveBeenCalledWith(
-          { event: 'WS_ERROR' },
-          expect.stringMatching('Unexpected error')
-        );
+        expect(log.error).toHaveBeenCalledWith({ error: expect.anything() });
 
-        const error = log.error.mock.calls[0][1];
+        const { error } = log.error.mock.calls[0][0];
         expect(error).toMatchSnapshot();
       });
 
@@ -330,7 +327,7 @@ describe('AsyncWebSocket', () => {
       aws.resetInFlightPromises();
 
       socket.mockMessage({ type: 'someReply', messageId: 1 });
-      expect(log.debug).toHaveBeenCalledWith({ event: 'WS_LATE_RESPONSE' }, expect.stringContaining('messageId=1'));
+      expect(log.debug).toHaveBeenCalledWith({ messageId: 1 }, 'late response');
     });
   });
 
diff --git a/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap b/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap
index 8ce030b07..55939c975 100644
--- a/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap
+++ b/detox/src/client/__snapshots__/AsyncWebSocket.test.js.snap
@@ -76,7 +76,7 @@ The tester has not received a response within 5000ms timeout to the message:
 `;
 
 exports[`AsyncWebSocket .send() when opened should log an error if the incoming message was completely unexpected 1`] = `
-"Unexpected error on an attempt to handle the response received over the web socket.
+[DetoxRuntimeError: Unexpected error on an attempt to handle the response received over the web socket.
 
 HINT: Examine the inner error:
 
@@ -84,7 +84,7 @@ Unexpected message received over the web socket: somethingElse
 
 The payload was:
 
-{\\"type\\":\\"somethingElse\\"}"
+{"type":"somethingElse"}]
 `;
 
 exports[`AsyncWebSocket .send() when opened should reject all messages in the flight if there's an error 1`] = `
diff --git a/detox/src/configuration/composeLoggerConfig.test.js b/detox/src/configuration/composeLoggerConfig.test.js
index ca77694bb..a16921338 100644
--- a/detox/src/configuration/composeLoggerConfig.test.js
+++ b/detox/src/configuration/composeLoggerConfig.test.js
@@ -24,12 +24,17 @@ describe('composeLoggerConfig', () => {
         showDate: expect.any(Function),
         showLoggerName: true,
         showPid: true,
+        showLevel: false,
         showMetadata: false,
+        showPrefixes: expect.any(Function),
         basepath: expect.any(String),
         prefixers: {
-          '__filename': expect.any(Function),
-          'trackingId': expect.any(Function),
-          'cpid': expect.any(Function),
+          'ph': expect.any(Function),
+        },
+        stringifiers: {
+          'args': expect.any(Function),
+          'data': expect.any(Function),
+          'error': expect.any(Function),
         },
       },
     });
@@ -59,9 +64,10 @@ describe('composeLoggerConfig', () => {
         overrideConsole: 'all',
         options: expect.objectContaining({
           showLoggerName: false,
+          showPrefixes: expect.any(Function),
           showPid: true,
           prefixers: expect.objectContaining({
-            '__filename': expect.any(Function),
+            'cat': expect.any(Function),
             somethingElse: expect.any(Function),
           }),
         }),
@@ -83,6 +89,10 @@ describe('composeLoggerConfig', () => {
         options: expect.objectContaining({
           colors: false,
           showPid: true,
+          prefixers: expect.objectContaining({
+            ph: expect.any(Function),
+            id: expect.any(Function),
+          }),
         }),
       });
     });
diff --git a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.test.js b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.test.js
index 919182260..4b5b95216 100644
--- a/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.test.js
+++ b/detox/src/devices/allocation/drivers/android/emulator/EmulatorVersionResolver.test.js
@@ -32,13 +32,7 @@ describe('Emulator binary version', () => {
       exec: jest.fn().mockResolvedValue(versionResult),
     };
 
-    jest.mock('../../../../../utils/logger', () => ({
-      child: jest.fn().mockReturnValue({
-        debug: jest.fn(),
-        warn: jest.fn(),
-        error: jest.fn(),
-      }),
-    }));
+    jest.mock('../../../../../utils/logger');
     log = require('../../../../../utils/logger').child();
 
     const EmulatorVersionResolver = require('./EmulatorVersionResolver');
@@ -77,7 +71,7 @@ describe('Emulator binary version', () => {
   it('should log in case of a parsing error', async () => {
     emulatorExec.exec.mockResolvedValue('non-parsable result');
     await uut.resolve();
-    expect(log.warn).toHaveBeenCalledWith({ event: 'EMU_BIN_VERSION_DETECT', success: false }, expect.any(String), 'non-parsable result');
+    expect(log.warn).toHaveBeenCalledWith({ success: false }, expect.any(String), 'non-parsable result');
   });
 
   it('should return null in case of a version-query failure', async () => {
@@ -90,7 +84,7 @@ describe('Emulator binary version', () => {
     const error = new Error('some error');
     emulatorExec.exec.mockRejectedValue(error);
     await uut.resolve();
-    expect(log.error).toHaveBeenCalledWith({ event: 'EMU_BIN_VERSION_DETECT', success: false, error }, expect.any(String), error);
+    expect(log.error).toHaveBeenCalledWith({ success: false, error }, expect.any(String), error);
   });
 
   it('should cache the version', async () => {
@@ -103,6 +97,6 @@ describe('Emulator binary version', () => {
 
   it('should log the version', async () => {
     await uut.resolve();
-    expect(log.debug).toHaveBeenCalledWith({ event: 'EMU_BIN_VERSION_DETECT', success: true }, expect.any(String), expect.objectContaining(expectedVersion));
+    expect(log.debug).toHaveBeenCalledWith({ success: true }, expect.any(String), expect.objectContaining(expectedVersion));
   });
 });
diff --git a/detox/src/logger/customConsoleLogger.test.js b/detox/src/logger/utils/customConsoleLogger.test.js
similarity index 70%
rename from detox/src/logger/customConsoleLogger.test.js
rename to detox/src/logger/utils/customConsoleLogger.test.js
index 444f3ed3e..daac324b6 100644
--- a/detox/src/logger/customConsoleLogger.test.js
+++ b/detox/src/logger/utils/customConsoleLogger.test.js
@@ -2,8 +2,6 @@ describe('customConsoleLogger.overrideConsoleMethods(console, bunyanLogger)', ()
   let overrideConsoleMethods;
   let fakeConsole, bunyanLogger;
 
-  const USER_LOG_EVENT = { event: 'USER_LOG' };
-
   beforeEach(() => {
     overrideConsoleMethods = require('./customConsoleLogger').overrideConsoleMethods;
 
@@ -40,8 +38,8 @@ describe('customConsoleLogger.overrideConsoleMethods(console, bunyanLogger)', ()
   });
 
   describe('- proxying to bunyan -', () => {
-    const expectedOrigin = expect.stringMatching(/at src[\\/]logger[\\/]customConsoleLogger\.test\.js:\d+:\d+/);
-    const expectedStackDump = expect.stringMatching(/at.*src[\\/]logger[\\/]customConsoleLogger\.test\.js:\d+:\d+/m);
+    const expectedOrigin = { origin: expect.stringMatching(/at src[\\/]logger[\\/]utils[\\/]customConsoleLogger\.test\.js:\d+:\d+/) };
+    const expectedStackDump = { stack: expect.stringMatching(/at.*src[\\/]logger[\\/]utils[\\/]customConsoleLogger\.test\.js:\d+:\d+/m) };
 
     beforeEach(() => {
       overrideConsoleMethods(fakeConsole, bunyanLogger);
@@ -49,32 +47,32 @@ describe('customConsoleLogger.overrideConsoleMethods(console, bunyanLogger)', ()
 
     it('should connect: console.log -> logger.info', () => {
       fakeConsole.log('OK %d', 200);
-      expect(bunyanLogger.info).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n', 'OK 200');
+      expect(bunyanLogger.info).toHaveBeenCalledWith(expectedOrigin, 'OK 200');
     });
 
     it('should connect: console.warn -> logger.warn', () => {
       fakeConsole.warn('Warning %d', 301);
-      expect(bunyanLogger.warn).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n', 'Warning 301');
+      expect(bunyanLogger.warn).toHaveBeenCalledWith(expectedOrigin, 'Warning 301');
     });
 
     it('should connect: console.trace -> logger.info', () => {
       fakeConsole.trace('TraceMe %d', 100500);
-      expect(bunyanLogger.info).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n  Trace:', 'TraceMe 100500', expectedStackDump);
+      expect(bunyanLogger.info).toHaveBeenCalledWith({ ...expectedOrigin, ...expectedStackDump }, 'TraceMe 100500');
     });
 
     it('should connect: console.error -> logger.error', () => {
       fakeConsole.error('MyError %d', 500);
-      expect(bunyanLogger.error).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n', 'MyError 500');
+      expect(bunyanLogger.error).toHaveBeenCalledWith(expectedOrigin, 'MyError 500');
     });
 
     it('should connect: console.debug -> logger.debug', () => {
       fakeConsole.debug('Debug %d', 0);
-      expect(bunyanLogger.debug).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n', 'Debug 0');
+      expect(bunyanLogger.debug).toHaveBeenCalledWith(expectedOrigin, 'Debug 0');
     });
 
     it('should connect: console.assert -> logger.error', () => {
       fakeConsole.assert(false, 'Failed condition %s', false);
-      expect(bunyanLogger.error).toHaveBeenCalledWith(USER_LOG_EVENT, expectedOrigin, '\n  AssertionError:', 'Failed condition false');
+      expect(bunyanLogger.error).toHaveBeenCalledWith(expectedOrigin, 'AssertionError:', 'Failed condition false');
     });
 
     it('should not connect console.assert to logger.error, if the condition is true', () => {
diff --git a/detox/src/server/DetoxServer.test.js b/detox/src/server/DetoxServer.test.js
index 56cbfdbaa..8801eac2e 100644
--- a/detox/src/server/DetoxServer.test.js
+++ b/detox/src/server/DetoxServer.test.js
@@ -63,8 +63,8 @@ describe('DetoxServer', () => {
         await server.open();
 
         const expectedString = expect.stringContaining(`localhost:${server.port}`);
-        expect(log.info).toHaveBeenCalledWith({ event: 'WSS_CREATE' }, expectedString);
-        expect(log.debug).not.toHaveBeenCalledWith({ event: 'WSS_CREATE' }, expectedString);
+        expect(log.info).toHaveBeenCalledWith(expectedString);
+        expect(log.debug).not.toHaveBeenCalledWith(expectedString);
       } finally {
         await server.close();
       }
@@ -77,8 +77,8 @@ describe('DetoxServer', () => {
         await server.open();
 
         const expectedString = expect.stringContaining(`localhost:${server.port}`);
-        expect(log.debug).toHaveBeenCalledWith({ event: 'WSS_CREATE' }, expectedString);
-        expect(log.info).not.toHaveBeenCalledWith({ event: 'WSS_CREATE' }, expectedString);
+        expect(log.debug).toHaveBeenCalledWith(expectedString);
+        expect(log.info).not.toHaveBeenCalledWith(expectedString);
       } finally {
         await server.close();
       }
@@ -90,7 +90,7 @@ describe('DetoxServer', () => {
       await server.close();
 
       const expectedString = expect.stringContaining(`has been closed gracefully`);
-      expect(log.debug).toHaveBeenCalledWith({ event: 'WSS_CLOSE' }, expectedString);
+      expect(log.debug).toHaveBeenCalledWith(expectedString);
     });
 
     it('should throw upon an unsuccessful server opening', async () => {
@@ -122,9 +122,7 @@ describe('DetoxServer', () => {
       jest.advanceTimersByTime(10000);
       await closePromise;
 
-      const expectedString = expect.stringContaining(`closed abruptly`);
-      expect(log.warn).toHaveBeenCalledWith({ event: 'ERROR' }, expectedString);
-      expect(log.warn.mock.calls[0][1]).toMatchSnapshot();
+      expect(log.warn.mock.calls[0]).toMatchSnapshot();
     });
 
     it('should WARN log a message upon unsuccessful server closing (rejection case)', async () => {
@@ -138,9 +136,7 @@ describe('DetoxServer', () => {
       await server.open();
       await server.close();
 
-      const expectedString = expect.stringContaining(`TEST_ERROR`);
-      expect(log.warn).toHaveBeenCalledWith({ event: 'ERROR' }, expectedString);
-      expect(log.warn.mock.calls[0][1]).toMatchSnapshot();
+      expect(log.warn.mock.calls[0]).toMatchSnapshot();
     });
 
     it('should WARN log a message upon unsuccessful server closing (error emit case)', async () => {
@@ -162,9 +158,7 @@ describe('DetoxServer', () => {
       await server.open();
       await server.close();
 
-      const expectedString = expect.stringContaining(`TEST_ERROR`);
-      expect(log.warn).toHaveBeenCalledWith({ event: 'ERROR' }, expectedString);
-      expect(log.warn.mock.calls[0][1]).toMatchSnapshot();
+      expect(log.warn.mock.calls[0]).toMatchSnapshot();
     });
   });
 
@@ -183,9 +177,7 @@ describe('DetoxServer', () => {
     await server.open();
     errorCallback(new TestError());
 
-    const expectedString = expect.stringContaining(`unhandled error`);
-    expect(log.error).toHaveBeenCalledWith(expectedString);
-    expect(log.error.mock.calls[0][0]).toMatchSnapshot();
+    expect(log.error.mock.calls[0]).toMatchSnapshot();
   });
 
   it('should register an incoming connection in SessionManager', async () => {
diff --git a/detox/src/server/__snapshots__/DetoxServer.test.js.snap b/detox/src/server/__snapshots__/DetoxServer.test.js.snap
index 3dae38a99..aa56a028b 100644
--- a/detox/src/server/__snapshots__/DetoxServer.test.js.snap
+++ b/detox/src/server/__snapshots__/DetoxServer.test.js.snap
@@ -1,27 +1,37 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
 exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (error emit case) 1`] = `
-"Detox server has been closed abruptly! See the error details below:
-Error: TEST_ERROR
-Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+Array [
+  Object {
+    "err": [Error: TEST_ERROR],
+  },
+  "Detox server has been closed abruptly! See the error details below:",
+]
 `;
 
 exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (rejection case) 1`] = `
-"Detox server has been closed abruptly! See the error details below:
-Error: TEST_ERROR
-Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+Array [
+  Object {
+    "err": [Error: TEST_ERROR],
+  },
+  "Detox server has been closed abruptly! See the error details below:",
+]
 `;
 
 exports[`DetoxServer .open() / .close() should WARN log a message upon unsuccessful server closing (timeout case) 1`] = `
-"Detox server has been closed abruptly! See the error details below:
-Detox server close callback was not invoked within the 10000 ms timeout
-Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+Array [
+  Object {
+    "err": [DetoxRuntimeError: Detox server close callback was not invoked within the 10000 ms timeout],
+  },
+  "Detox server has been closed abruptly! See the error details below:",
+]
 `;
 
 exports[`DetoxServer should ERROR log messages from wss.Server 1`] = `
-"Detox server has got an unhandled error:
-Error: TEST_ERROR"
+Array [
+  Object {
+    "err": [Error: TEST_ERROR],
+  },
+  "Detox server has got an unhandled error:",
+]
 `;
diff --git a/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap b/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap
index 5515a9921..e6c77eacc 100644
--- a/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap
+++ b/detox/src/server/__tests__/__snapshots__/server-integration.test.js.snap
@@ -1,42 +1,103 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Detox server integration "app" connects first, and then disconnects 1`] = `"registered a new connection."`;
+exports[`Detox server integration "app" connects first, and then disconnects 1`] = `
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 8081,
+  },
+  "connection :8000<->:8081",
+  "(begin)",
+]
+`;
 
-exports[`Detox server integration "tester" connects first, and then disconnects 1`] = `"registered a new connection."`;
+exports[`Detox server integration "tester" connects first, and then disconnects 1`] = `
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 8081,
+  },
+  "connection :8000<->:8081",
+  "(begin)",
+]
+`;
 
 exports[`Detox server integration edge cases .registerSession - calling twice 1`] = `
-"DetoxInternalError: Cannot login the same WebSocket instance twice into the same session.
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+    "role": "app",
+    "sessionId": "aSession",
+    "trackingId": "app",
+  },
+  "DetoxInternalError: Cannot login the same WebSocket instance twice into the same session.
 Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues",
+]
 `;
 
-exports[`Detox server integration edge cases app dispatches "ready" action before login 1`] = `"The app has dispatched \\"ready\\" action too early."`;
+exports[`Detox server integration edge cases app dispatches "ready" action before login 1`] = `
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  "The app has dispatched \\"ready\\" action too early.",
+]
+`;
 
 exports[`Detox server integration edge cases attempt to register the same connection twice 1`] = `
-"DetoxInternalError: Cannot register the same WebSocket instance twice.
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  "DetoxInternalError: Cannot register the same WebSocket instance twice.
 Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues",
+]
 `;
 
 exports[`Detox server integration edge cases attempt to unregister an unknown connection 1`] = `
-"DetoxInternalError: Cannot unregister an unknown WebSocket instance.
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+  },
+  "DetoxInternalError: Cannot unregister an unknown WebSocket instance.
 Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues",
+]
 `;
 
 exports[`Detox server integration edge cases login - empty .params 1`] = `
-"Invalid login action received, it has no .params
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Invalid login action received, it has no .params
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
 
 {
   type: 'login'
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases login - invalid .role 1`] = `
-"Invalid login action received, it has invalid .role
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Invalid login action received, it has invalid .role
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
@@ -47,11 +108,20 @@ https://github.com/wix/Detox/issues
     sessionId: 'Session',
     role: 'Meteora'
   }
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases login - missing .sessionId 1`] = `
-"Invalid login action received, it has no .sessionId
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Invalid login action received, it has no .sessionId
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
@@ -62,11 +132,20 @@ https://github.com/wix/Detox/issues
     sessionId: '',
     role: 'tester'
   }
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases login - non-string .sessionId 1`] = `
-"Invalid login action received, it has a non-string .sessionId
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Invalid login action received, it has a non-string .sessionId
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
@@ -79,46 +158,108 @@ https://github.com/wix/Detox/issues
     },
     role: 'tester'
   }
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases login twice (as app) 1`] = `
-"DetoxInternalError: Cannot log in twice into the same session (aSession) being \\"app\\" already
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+    "role": "app",
+    "sessionId": "aSession",
+    "trackingId": "app",
+  },
+  Object {
+    "error": [DetoxInternalError: Cannot log in twice into the same session (aSession) being "app" already
 Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases login twice (as tester) + socket send error 1`] = `
-"Cannot forward the error details to the tester due to the error:
-Error: TestError"
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+    "role": "tester",
+    "sessionId": "aSession",
+    "trackingId": "tester",
+  },
+  Object {
+    "err": [Error: TestError],
+  },
+  "Cannot forward the error details to the tester due to the error:",
+]
 `;
 
 exports[`Detox server integration edge cases login twice (as tester) + socket send error 2`] = `
-"DetoxInternalError: Cannot log in twice into the same session (aSession) being \\"tester\\" already
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+    "role": "tester",
+    "sessionId": "aSession",
+    "trackingId": "tester",
+  },
+  Object {
+    "error": [DetoxInternalError: Cannot log in twice into the same session (aSession) being "tester" already
 Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases on(message) - malformed data 1`] = `
-"The payload received is not a valid JSON.
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: The payload received is not a valid JSON.
 
 HINT: Please report this issue on our GitHub tracker:
-https://github.com/wix/Detox/issues"
+https://github.com/wix/Detox/issues],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases on(message) - no .type 1`] = `
-"Cannot process an action without a type.
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Cannot process an action without a type.
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
 
 {
   some: 'message'
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
 exports[`Detox server integration edge cases receiving an action before we login 1`] = `
-"Action dispatched too early, there is no session to use:
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [DetoxRuntimeError: Action dispatched too early, there is no session to use:
 
 HINT: Please report this issue on our GitHub tracker:
 https://github.com/wix/Detox/issues
@@ -126,13 +267,36 @@ https://github.com/wix/Detox/issues
 {
   type: 'reloadReactNative',
   messageId: -1000
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
 
-exports[`Detox server integration edge cases socket error 1`] = `"Error: Test error"`;
+exports[`Detox server integration edge cases socket error 1`] = `
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 10,
+  },
+  Object {
+    "error": [Error: Test error],
+  },
+  "Caught socket error:",
+]
+`;
 
-exports[`Detox server integration tester and app interconnect and then disconnect: CANNOT_FORWARD 1`] = `
-"Cannot forward the message to the Detox client.
+exports[`Detox server integration tester and app interconnect and then disconnect 1`] = `
+Array [
+  Object {
+    "cat": "ws-server,ws-session",
+    "id": 11,
+    "role": "app",
+    "sessionId": "aSession",
+    "trackingId": "app",
+  },
+  Object {
+    "error": [DetoxRuntimeError: Cannot forward the message to the Detox client.
 
 {
   type: 'currentStatus',
@@ -140,5 +304,8 @@ exports[`Detox server integration tester and app interconnect and then disconnec
   params: {
     status: 'I am fine'
   }
-}"
+}],
+  },
+  "Caught unhandled error:",
+]
 `;
diff --git a/detox/src/server/__tests__/server-integration.test.js b/detox/src/server/__tests__/server-integration.test.js
index c99d7543a..0cf3c015f 100644
--- a/detox/src/server/__tests__/server-integration.test.js
+++ b/detox/src/server/__tests__/server-integration.test.js
@@ -4,15 +4,15 @@ const FakeWebSocket = require('../__mocks__/FakeWebSocket');
 describe('Detox server integration', () => {
   let logger;
   /**
-   * @type {typeof require('../DetoxSessionManager')}
+   * @type {typeof import('../DetoxSessionManager')}
    */
   let DetoxSessionManager;
   /**
-   * @type {typeof require('../DetoxSession')}
+   * @type {typeof import('../DetoxSession')}
    */
   let DetoxSession;
   /**
-   * @type {typeof require('../DetoxConnection')}
+   * @type {typeof import('../DetoxConnection')}
    */
   let DetoxConnection;
   /**
@@ -35,10 +35,10 @@ describe('Detox server integration', () => {
     ['app'],
     ['tester']
   ])('%j connects first, and then disconnects', async (role) => {
-    const webSocket = new FakeWebSocket({ remotePort: 8081 });
+    const webSocket = new FakeWebSocket({ localPort: 8000, remotePort: 8081 });
     sessionManager.registerConnection(webSocket, webSocket.socket);
 
-    expect(getLoggerMsg('debug', 0, 1)).toMatchSnapshot();
+    expect(getLoggerMsg('debug', 0)).toMatchSnapshot();
     expect(sessionManager.getSession(webSocket)).toBe(null);
 
     webSocket.mockLogin({ messageId: 100500, role });
@@ -157,8 +157,7 @@ describe('Detox server integration', () => {
       }
     });
 
-    expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-    expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot('CANNOT_FORWARD');
+    expect(getLoggerMsg('warn', 0)).toMatchSnapshot();
 
     // app disconnects
     appSocket.mockClose();
@@ -194,43 +193,37 @@ describe('Detox server integration', () => {
     test('on(message) - malformed data', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage(Buffer.alloc(0));
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('on(message) - no .type', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ some: 'message' });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login - empty .params', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ type: 'login' });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login - invalid .role', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ type: 'login', params: { sessionId: 'Session', role: 'Meteora' } });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login - missing .sessionId', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ type: 'login', params: { sessionId: '', role: 'tester' } });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login - non-string .sessionId', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ type: 'login', params: { sessionId: { 0: 2 }, role: 'tester' } });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login twice (as tester)', () => {
@@ -252,16 +245,15 @@ describe('Detox server integration', () => {
       webSocket.mockLogin({ role: 'tester' });
       expect(webSocket.send).toHaveBeenCalledWith(expect.stringContaining('"type":"serverError"'));
 
-      expect(getLoggerMsg('error', 0)).toMatchSnapshot();
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('error')).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('login twice (as app)', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockLogin({ role: 'app' });
       webSocket.mockLogin({ role: 'app' });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('.registerSession - calling twice', () => {
@@ -272,14 +264,13 @@ describe('Detox server integration', () => {
       const newDetoxSession = sessionManager.registerSession(detoxConnection, { role: 'app', sessionId: '10101' });
 
       expect(priorDetoxSession === newDetoxSession).toBe(true); // assert no new sessions were created
-      expect(getLoggerMsg('error', 0)).toMatchSnapshot();
+      expect(getLoggerMsg('error')).toMatchSnapshot();
     });
 
     test('receiving an action before we login', () => {
       sessionManager.registerConnection(webSocket, webSocket.socket);
       webSocket.mockMessage({ type: 'reloadReactNative', messageId: -1000 });
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
 
     test('app dispatches "ready" action before login', async () => {
@@ -295,13 +286,17 @@ describe('Detox server integration', () => {
       delete err.stack;
       webSocket.mockError(err);
 
-      expect(getLoggerMsg('warn', 0, 0)).toEqual({ event: 'WSS_SOCKET_ERROR' });
-      expect(getLoggerMsg('warn', 0, 1)).toMatchSnapshot();
+      expect(getLoggerMsg('warn')).toMatchSnapshot();
     });
   });
 
-  function getLoggerMsg(level, callIndex = 0, argIndex = 0) {
-    return logger[level].mock.calls[callIndex][argIndex];
+  function getLoggerMsg(level, callIndex = 0, argIndex) {
+    const call = logger.log.mock.calls.filter(c => c[0] === level).map(c => c.slice(1))[callIndex];
+    if (argIndex === undefined) {
+      return call;
+    } else {
+      return call[argIndex];
+    }
   }
 
   function aMessage(obj) {
diff --git a/detox/src/utils/errorUtils.test.js b/detox/src/utils/errorUtils.test.js
index ad4197e35..6df1dab23 100644
--- a/detox/src/utils/errorUtils.test.js
+++ b/detox/src/utils/errorUtils.test.js
@@ -7,7 +7,7 @@ describe('sliceErrorStack(error, fromIndex)', () => {
     function innerFunction() { throw new Error('Source Error'); }
     function outerFunction() { innerFunction(); }
     function attemptFunction() {
-      try { outerFunction(); } catch (e) { console.error('err', e); return e; }
+      try { outerFunction(); } catch (e) { return e; }
     }
 
     const slicer = at => (_line) => --at < 0;
diff --git a/detox/src/utils/streamUtils.test.js b/detox/src/utils/streamUtils.test.js
index 083e3aaed..363af7a7e 100644
--- a/detox/src/utils/streamUtils.test.js
+++ b/detox/src/utils/streamUtils.test.js
@@ -48,7 +48,7 @@ describe('JSONL', () => {
     ]);
 
     const logger = require('./logger');
-    expect(logger.debug).toHaveBeenCalledWith({ event: 'JSONL_ERROR', err: expect.any(Error) });
+    expect(logger.debug).toHaveBeenCalledWith({ err: expect.any(Error) });
   });
 
   it('should convert bunyan JSON to bunyan debug streams', async () => {
diff --git a/detox/src/utils/trace.test.js b/detox/src/utils/trace.test.js
index 7af9e4123..aa222e603 100644
--- a/detox/src/utils/trace.test.js
+++ b/detox/src/utils/trace.test.js
@@ -10,65 +10,33 @@ describe('Trace util', () => {
     ({ trace, traceCall } = require('./trace'));
   });
 
-  it('should log { ph: B } event', () => {
+  it('startSection -> logger.trace.begin(...)', () => {
     const event = {
-      name: 'event-name',
+      msg: 'event-name',
       args: { arg1: 'val1' },
     };
 
-    trace.startSection(event.name, event.args);
-    expect(logger().trace).toHaveBeenCalledWith(
-      expect.objectContaining({ arg1: 'val1', trace: traceShape({ ph: 'B' }) }),
-      'event-name'
+    trace.startSection(event.msg, event.args);
+    expect(logger().trace.begin).toHaveBeenCalledWith(
+      event.args,
+      event.msg
     );
   });
 
-  it('should log { ph: E } event', () => {
+  it('endSection -> logger.trace.end(...)', () => {
     const section = {
-      name: 'section-name',
+      msg: 'section-name',
       args: { arg1: 'val1' },
     };
 
-    trace.startSection(section.name, section.args);
-    trace.endSection(section.name, section.args);
-    expect(logger().trace).toHaveBeenCalledWith(
-      expect.objectContaining({ arg1: 'val1', trace: { ph: 'E', tid: expect.any(Number) } }),
-      'end'
-    );
+    trace.endSection(section.msg, section.args);
+    expect(logger().trace.end).toHaveBeenCalledWith(section.args);
   });
 
-  describe('trace-call function', () => {
-    it('should trace a successful function', async () => {
-      const functionCall = () => 42;
-
-      expect(traceCall('42-call', functionCall)).toBe(42);
-      expect(logger().trace).toHaveBeenCalledWith(
-        expect.objectContaining({ trace: traceShape({ ph: 'B' }) }),
-        '42-call'
-      );
-      expect(logger().trace).toHaveBeenCalledWith(
-        expect.objectContaining({ success: true, trace: traceShape({ ph: 'E' }) }),
-        'end'
-      );
-    });
+  it('should trace a successful function', async () => {
+    const functionCall = () => 42;
 
-    it('should trace a failed function call', async () => {
-      const error = new Error('error mock');
-      const functionCall = () => Promise.reject(error);
-
-      await expect(traceCall('error-call', functionCall)).rejects.toThrowError(error);
-      expect(logger().trace).toHaveBeenCalledWith(
-        expect.objectContaining({ trace: traceShape({ ph: 'B' }) }),
-        'error-call'
-      );
-      expect(logger().trace).toHaveBeenCalledWith(
-        expect.objectContaining({ success: false, error, trace: traceShape({ ph: 'E' }) }),
-        'end'
-      );
-    });
+    expect(traceCall('42-call', functionCall)).toBe(42);
+    expect(logger().trace.complete).toHaveBeenCalledWith('42-call', functionCall);
   });
-
-  function traceShape({ ph = expect.any(String), tid = expect.any(Number) } = {}) {
-    return expect.objectContaining({ ph, tid });
-  }
 });
diff --git a/detox/test/types/detox-internals-tests.ts b/detox/test/types/detox-internals-tests.ts
index 3382b94e4..d8e859713 100644
--- a/detox/test/types/detox-internals-tests.ts
+++ b/detox/test/types/detox-internals-tests.ts
@@ -21,7 +21,6 @@ import {
   onTestStart,
   resolveConfig,
   session,
-  trace,
   uninstallWorker,
   worker,
 } from 'detox/internals';
@@ -58,7 +57,7 @@ async function internalsTest() {
     workerId: 'worker-1',
   });
 
-  assert<Detox.DetoxWorker>(worker);
+  assert<DetoxInternals.Worker>(worker);
 
   await uninstallWorker();
   await cleanup();
@@ -77,6 +76,18 @@ async function logTest() {
 
   log.trace('msg');
   log.trace({ event: 'EVENT' }, 'msg');
+
+  log.trace.begin('Outer section');
+  log.debug.begin({ arg: 'value' }, 'Inner section');
+
+  log.info.complete('Sync section', () => 'sync').toUpperCase();
+  log.warn.complete('Async section', async () => 42).then(() => 84);
+  log.error.complete('Promise section', Promise.resolve(42)).finally(() => {});
+  log.fatal.complete('Value section', 42).toFixed(1);
+
+  log.warn.end({ extra: 'data' });
+  log.info.end();
+
   log.debug('msg');
   log.debug({ event: 'EVENT' }, 'msg');
   log.info('msg');
@@ -91,40 +102,13 @@ async function logTest() {
   log.child().info('msg');
   log.child({ anything: 'value' }).trace('msg');
 
-  const event1: Detox.TraceEvent = {
-    id: 1,
-    name: 'Long method',
-    cat: 'user',
-    args: {},
-    cname: 'inactive'
-  };
-
-  const event2: Detox.TraceEvent = {
-    cname: 'red',
-    args: { $success: true },
-  };
-
-  const handle1 = trace.begin('Long method', event1.args);
-  if (Math.random() > 0.5) {
-    handle1.end();
-  } else {
-    handle1.end(event2);
-  }
-
-  const handle2 = trace.begin(event1);
-  if (Math.random() > 0.5) {
-    handle2.end();
-  } else {
-    handle2.end(event2);
-  }
-
-  await trace('Long method', async () => {
-    // do something
+  const serverLogger = log.child({ cat: 'server', id: 4333 });
+  serverLogger.info.begin({}, 'Starting server...');
+  await serverLogger.trace.complete('something', async () => {
+    // ... do something ...
   });
 
-  trace(event1, () => {
-    // do something
-  });
+  serverLogger.trace.end();
 }
 
 function configTest() {
@@ -133,7 +117,7 @@ function configTest() {
   assert<Record<string, Detox.DetoxAppConfig>>(config.apps);
   assert<Detox.DetoxArtifactsConfig>(config.artifacts);
   assert<Detox.DetoxBehaviorConfig>(config.behavior);
-  assert<DetoxInternals.DetoxCLIConfig>(config.cli);
+  assert<DetoxInternals.CLIConfig>(config.cli);
   assert<Detox.DetoxDeviceConfig>(config.device);
   assert<Detox.DetoxLoggerConfig>(config.logger);
   assert<Detox.DetoxSessionConfig>(config.session);
diff --git a/detox/test/types/detox-module-tests.ts b/detox/test/types/detox-module-tests.ts
index 158859a9e..d3ae389e9 100644
--- a/detox/test/types/detox-module-tests.ts
+++ b/detox/test/types/detox-module-tests.ts
@@ -3,7 +3,7 @@ declare var beforeAll: (callback: () => void) => void;
 declare var afterAll: (callback: () => void) => void;
 declare var test: (name: string, callback: () => void) => void;
 
-import { by, device, element, expect, waitFor } from 'detox';
+import { by, device, element, expect, waitFor, log, trace, traceCall } from 'detox';
 
 describe('Test', () => {
   beforeAll(async () => {
@@ -78,4 +78,59 @@ describe('Test', () => {
     await expect(element(by.id('element'))).not.toBeVisible();
     await expect(element(by.id('element'))).not.toExist();
   });
+
+  test('Trace', async () => {
+    trace.startSection('Long method');
+    trace.endSection('Long method');
+
+    await traceCall('Another long method', async () => {
+      // do something
+    });
+
+    switch (log.level) {
+      case 'fatal':
+      case 'error':
+      case 'warn':
+      case 'info':
+      case 'debug':
+      case 'trace':
+        break;
+    }
+
+    log.trace('msg');
+    log.trace({ event: 'EVENT' }, 'msg');
+
+    log.trace.begin('Outer section');
+    log.debug.begin({ arg: 'value' }, 'Inner section');
+
+    log.info.complete('Sync section', () => 'sync').toUpperCase();
+    log.warn.complete('Async section', async () => 42).then(() => 84);
+    log.error.complete('Promise section', Promise.resolve(42)).finally(() => {});
+    log.fatal.complete('Value section', 42).toFixed(1);
+
+    log.warn.end({ extra: 'data' });
+    log.info.end();
+
+    log.debug('msg');
+    log.debug({ event: 'EVENT' }, 'msg');
+    log.info('msg');
+    log.info({ event: 'EVENT' }, 'msg');
+    log.warn('msg');
+    log.warn({ event: 'EVENT' }, 'msg');
+    log.error('msg');
+    log.error({ event: 'EVENT' }, 'msg');
+    log.fatal('msg');
+    log.fatal({ event: 'EVENT' }, 'msg');
+
+    log.child().info('msg');
+    log.child({ anything: 'value' }).trace('msg');
+
+    const serverLogger = log.child({ cat: 'server', id: 4333 });
+    serverLogger.info.begin({}, 'Starting server...');
+    await serverLogger.trace.complete('something', async () => {
+      // ... do something ...
+    });
+
+    serverLogger.trace.end();
+  })
 });