Repository: denoland/deno
Pull request: fix(ext/console): make timer and counter state per-instance to fix OTEL console wrapping

## Summary

- Fixes `console.time` / `console.timeEnd` / `console.count` / `console.countReset` producing spurious warnings when OTEL is enabled (`OTEL_DENO=true`)
- Root cause: `timerMap` and `countMap` were module-level singletons shared across all `Console` instances. When OTEL wraps the console via `wrapConsole`, both the OTEL Console and the original Console methods are called sequentially, but they were sharing the same timer/counter state, so the second call would see already-modified state.
- Fix: move `timerMap` and `countMap` to private instance fields (`#timerMap`, `#countMap`) so each `Console` instance maintains independent state.

Fixes #28818

## Test plan

- [x] Added `tests/specs/cli/otel_basic/console_time` spec test that exercises `console.time`/`console.timeEnd`/`console.count` with OTEL enabled
- [x] Existing console tests pass (`cargo test console`)
- [x] Existing OTEL spec tests unaffected (pre-existing metric test failures are unrelated)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: ext/web/01_console.js
patch: @@ -3639,8 +3639,6 @@ function createStylizeWithColor(styles, colors) {
   };
 }
 
-const countMap = new SafeMap();
-const timerMap = new SafeMap();
 const isConsoleInstance = Symbol("isConsoleInstance");
 
 /** @param noColor {boolean} */
@@ -3654,6 +3652,8 @@ function getConsoleInspectOptions(noColor) {
 
 class Console {
   #printFunc = null;
+  #countMap = new SafeMap();
+  #timerMap = new SafeMap();
   [isConsoleInstance] = false;
 
   constructor(printFunc) {
@@ -3766,23 +3766,26 @@ class Console {
   count = (label = "default") => {
     label = String(label);
 
-    if (MapPrototypeHas(countMap, label)) {
-      const current = MapPrototypeGet(countMap, label) || 0;
-      MapPrototypeSet(countMap, label, current + 1);
+    if (MapPrototypeHas(this.#countMap, label)) {
+      const current = MapPrototypeGet(this.#countMap, label) || 0;
+      MapPrototypeSet(this.#countMap, label, current + 1);
     } else {
-      MapPrototypeSet(countMap, label, 1);
+      MapPrototypeSet(this.#countMap, label, 1);
     }
 
-    this.info(`${label}: ${MapPrototypeGet(countMap, label)}`);
+    this.#printFunc(
+      `${label}: ${MapPrototypeGet(this.#countMap, label)}\n`,
+      1,
+    );
   };
 
   countReset = (label = "default") => {
     label = String(label);
 
-    if (MapPrototypeHas(countMap, label)) {
-      MapPrototypeSet(countMap, label, 0);
+    if (MapPrototypeHas(this.#countMap, label)) {
+      MapPrototypeSet(this.#countMap, label, 0);
     } else {
-      this.warn(`Count for '${label}' does not exist`);
+      this.#printFunc(`Count for '${label}' does not exist\n`, 2);
     }
   };
 
@@ -3892,23 +3895,23 @@ class Console {
   time = (label = "default") => {
     label = String(label);
 
-    if (MapPrototypeHas(timerMap, label)) {
-      this.warn(`Timer '${label}' already exists`);
+    if (MapPrototypeHas(this.#timerMap, label)) {
+      this.#printFunc(`Timer '${label}' already exists\n`, 2);
       return;
     }
 
-    MapPrototypeSet(timerMap, label, currentTime());
+    MapPrototypeSet(this.#timerMap, label, currentTime());
   };
 
   timeLog = (label = "default", ...args) => {
     label = String(label);
 
-    if (!MapPrototypeHas(timerMap, label)) {
-      this.warn(`Timer '${label}' does not exist`);
+    if (!MapPrototypeHas(this.#timerMap, label)) {
+      this.#printFunc(`Timer '${label}' does not exist\n`, 2);
       return;
     }
 
-    const startTime = MapPrototypeGet(timerMap, label);
+    const startTime = MapPrototypeGet(this.#timerMap, label);
     let duration = currentTime() - startTime;
     if (duration < 1) {
       duration = NumberPrototypeToFixed(duration, 3);
@@ -3920,19 +3923,28 @@ class Console {
       duration = NumberPrototypeToFixed(duration, 0);
     }
 
-    this.info(`${label}: ${duration}ms`, ...new SafeArrayIterator(args));
+    this.#printFunc(
+      inspectArgs(
+        [`${label}: ${duration}ms`, ...new SafeArrayIterator(args)],
+        {
+          ...getConsoleInspectOptions(noColorStdout()),
+          indentLevel: this.indentLevel,
+        },
+      ) + "\n",
+      1,
+    );
   };
 
   timeEnd = (label = "default") => {
     label = String(label);
 
-    if (!MapPrototypeHas(timerMap, label)) {
-      this.warn(`Timer '${label}' does not exist`);
+    if (!MapPrototypeHas(this.#timerMap, label)) {
+      this.#printFunc(`Timer '${label}' does not exist\n`, 2);
       return;
     }
 
-    const startTime = MapPrototypeGet(timerMap, label);
-    MapPrototypeDelete(timerMap, label);
+    const startTime = MapPrototypeGet(this.#timerMap, label);
+    MapPrototypeDelete(this.#timerMap, label);
     let duration = currentTime() - startTime;
     if (duration < 1) {
       duration = NumberPrototypeToFixed(duration, 3);
@@ -3944,7 +3956,7 @@ class Console {
       duration = NumberPrototypeToFixed(duration, 0);
     }
 
-    this.info(`${label}: ${duration}ms`);
+    this.#printFunc(`${label}: ${duration}ms\n`, 1);
   };
 
   group = (...label) => {

filename: tests/specs/cli/otel_basic/__test__.jsonc
patch: @@ -100,6 +100,10 @@
     "http_server_status": {
       "args": "run -A main.ts http_server_status.ts",
       "output": "http_server_status.out"
+    },
+    "console_time": {
+      "args": "run -A main.ts console_time.ts",
+      "output": "console_time.out"
     }
   }
 }

filename: tests/specs/cli/otel_basic/console_time.out
patch: @@ -0,0 +1,69 @@
+{
+  "spans": [],
+  "logs": [
+    {
+      "timeUnixNano": "[WILDLINE]",
+      "observedTimeUnixNano": "[WILDCARD]",
+      "severityNumber": 9,
+      "severityText": "INFO",
+      "body": {
+        "stringValue": "my timer: [WILDCARD]\n"
+      },
+      "attributes": [
+        {
+          "key": "log.iostream",
+          "value": {
+            "stringValue": "stdout"
+          }
+        }
+      ],
+      "droppedAttributesCount": 0,
+      "flags": 0,
+      "traceId": "",
+      "spanId": ""
+    },
+    {
+      "timeUnixNano": "[WILDLINE]",
+      "observedTimeUnixNano": "[WILDCARD]",
+      "severityNumber": 9,
+      "severityText": "INFO",
+      "body": {
+        "stringValue": "my counter: 1\n"
+      },
+      "attributes": [
+        {
+          "key": "log.iostream",
+          "value": {
+            "stringValue": "stdout"
+          }
+        }
+      ],
+      "droppedAttributesCount": 0,
+      "flags": 0,
+      "traceId": "",
+      "spanId": ""
+    },
+    {
+      "timeUnixNano": "[WILDLINE]",
+      "observedTimeUnixNano": "[WILDCARD]",
+      "severityNumber": 9,
+      "severityText": "INFO",
+      "body": {
+        "stringValue": "my counter: 2\n"
+      },
+      "attributes": [
+        {
+          "key": "log.iostream",
+          "value": {
+            "stringValue": "stdout"
+          }
+        }
+      ],
+      "droppedAttributesCount": 0,
+      "flags": 0,
+      "traceId": "",
+      "spanId": ""
+    }
+  ],
+  "metrics": []
+}

filename: tests/specs/cli/otel_basic/console_time.ts
patch: @@ -0,0 +1,5 @@
+console.time("my timer");
+console.timeEnd("my timer");
+console.count("my counter");
+console.count("my counter");
+console.countReset("my counter");