Repository: denoland/deno
Pull request: fix(ext/otel): remove panicking unwraps in telemetry code

## Summary

- Replace `.unwrap()` calls on `OTEL_GLOBALS.get()` and isolate slot accesses
  with graceful error handling throughout `ext/telemetry/lib.rs`
- GC prologue/epilogue callbacks now return early if the isolate slot is missing
  (can happen during isolate teardown in watch mode restarts)
- `op_otel_enable_isolate_metrics` / `op_otel_collect_isolate_metrics` return
  early if globals or slots are not available
- `OtelTracer::builtin()`, `OtelTracer::start_span()`,
  `OtelTracer::start_span_foreign()`, and `OtelMeter::new()` return JS errors
  instead of panicking

## Context

These unwraps could panic in watch mode when:
1. V8 GC callbacks fire during isolate teardown after slots have been cleaned up
2. OTEL globals are accessed before initialization completes

Refs: https://github.com/denoland/deno/issues/29478
Refs: https://github.com/denoland/deno/issues/30567

## Test plan

- Existing otel spec tests pass (confirmed no regressions in passing tests)
- Manual testing with `OTEL_DENO=true deno run --watch` + rapid file changes shows no panics

Changed files:
filename: ext/telemetry/lib.rs
patch: @@ -1433,12 +1433,14 @@ impl OtelTracer {
 
   #[static_method]
   #[cppgc]
-  fn builtin() -> OtelTracer {
+  fn builtin() -> Result<OtelTracer, JsErrorBox> {
     let OtelGlobals {
       builtin_instrumentation_scope,
       ..
-    } = OTEL_GLOBALS.get().unwrap();
-    OtelTracer(builtin_instrumentation_scope.clone())
+    } = OTEL_GLOBALS
+      .get()
+      .ok_or_else(|| JsErrorBox::generic("otel not initialized"))?;
+    Ok(OtelTracer(builtin_instrumentation_scope.clone()))
   }
 
   #[cppgc]
@@ -1451,7 +1453,9 @@ impl OtelTracer {
     start_time: Option<f64>,
     #[smi] attribute_count: usize,
   ) -> Result<OtelSpan, JsErrorBox> {
-    let OtelGlobals { id_generator, .. } = OTEL_GLOBALS.get().unwrap();
+    let OtelGlobals { id_generator, .. } = OTEL_GLOBALS
+      .get()
+      .ok_or_else(|| JsErrorBox::generic("otel not initialized"))?;
     let span_context;
     let parent_span_id;
     match parent {
@@ -1540,7 +1544,9 @@ impl OtelTracer {
     if parent_span_id == SpanId::INVALID {
       return Err(JsErrorBox::generic("invalid span id"));
     };
-    let OtelGlobals { id_generator, .. } = OTEL_GLOBALS.get().unwrap();
+    let OtelGlobals { id_generator, .. } = OTEL_GLOBALS
+      .get()
+      .ok_or_else(|| JsErrorBox::generic("otel not initialized"))?;
     let span_context = SpanContext::new(
       parent_trace_id,
       id_generator.new_span_id(),
@@ -1915,7 +1921,7 @@ impl OtelMeter {
     #[string] name: String,
     #[string] version: Option<String>,
     #[string] schema_url: Option<String>,
-  ) -> OtelMeter {
+  ) -> Result<OtelMeter, JsErrorBox> {
     let mut builder = opentelemetry::InstrumentationScope::builder(name);
     if let Some(version) = version {
       builder = builder.with_version(version);
@@ -1926,10 +1932,10 @@ impl OtelMeter {
     let scope = builder.build();
     let meter = OTEL_GLOBALS
       .get()
-      .unwrap()
+      .ok_or_else(|| JsErrorBox::generic("otel not initialized"))?
       .meter_provider
       .meter_with_scope(scope);
-    OtelMeter(meter)
+    Ok(OtelMeter(meter))
   }
 
   #[cppgc]
@@ -2556,7 +2562,9 @@ impl GcMetricData {
     // SAFETY: Isolate is valid during callback
     let isolate =
       unsafe { v8::Isolate::from_raw_isolate_ptr_unchecked(isolate) };
-    let this = isolate.get_slot::<Self>().unwrap();
+    let Some(this) = isolate.get_slot::<Self>() else {
+      return;
+    };
     this.0.borrow_mut().start = Instant::now();
   }
 
@@ -2569,7 +2577,9 @@ impl GcMetricData {
     // SAFETY: Isolate is valid during callback
     let isolate =
       unsafe { v8::Isolate::from_raw_isolate_ptr_unchecked(isolate) };
-    let this = isolate.get_slot::<Self>().unwrap();
+    let Some(this) = isolate.get_slot::<Self>() else {
+      return;
+    };
     let this = this.0.borrow_mut();
 
     let elapsed = this.start.elapsed();
@@ -2605,7 +2615,10 @@ fn op_otel_enable_isolate_metrics(scope: &mut v8::PinScope<'_, '_>) {
     return;
   }
 
-  let meter = OTEL_GLOBALS.get().unwrap().meter_provider.meter("v8js");
+  let Some(globals) = OTEL_GLOBALS.get() else {
+    return;
+  };
+  let meter = globals.meter_provider.meter("v8js");
 
   // https://opentelemetry.io/docs/specs/semconv/runtime/v8js-metrics/#metric-v8jsgcduration
   let duration = meter
@@ -2663,7 +2676,10 @@ fn op_otel_enable_isolate_metrics(scope: &mut v8::PinScope<'_, '_>) {
 
 #[op2(fast)]
 fn op_otel_collect_isolate_metrics(scope: &mut v8::PinScope<'_, '_>) {
-  let data = scope.get_slot::<HeapMetricData>().unwrap().clone();
+  let Some(data) = scope.get_slot::<HeapMetricData>() else {
+    return;
+  };
+  let data = data.clone();
   for i in 0..scope.get_number_of_data_slots() {
     let Some(space) = scope.get_heap_space_statistics(i as _) else {
       continue;