Repository: denoland/deno
Pull request: feat(otel): allow feeding permission audit data into OTEL 


Changed files:
filename: cli/args/flags.rs
patch: @@ -4745,8 +4745,9 @@ fn permission_args(app: Command, requires: Option<&'static str>) -> Command {
                                              <p(245)>--ignore-read  |  --ignore-read="/etc,/var/log.txt"</>
       <g>DENO_TRACE_PERMISSIONS</>                Environmental variable to enable stack traces in permission prompts.
                                              <p(245)>DENO_TRACE_PERMISSIONS=1 deno run main.ts</>
-      <g>DENO_AUDIT_PERMISSIONS</>               Environmental variable to generate a JSONL file with all permissions accesses.
-                                             <p(245)>DENO_TRACE_PERMISSIONS=./audit.jsonl deno run main.ts</>
+      <g>DENO_AUDIT_PERMISSIONS</>               Environmental variable to audit all permissions accesses. Set to a file path for JSONL output, or "otel" to emit as OpenTelemetry log events via the configured OTel exporter.
+                                             <p(245)>DENO_AUDIT_PERMISSIONS=./audit.jsonl deno run main.ts</>
+                                             <p(245)>DENO_AUDIT_PERMISSIONS=otel deno run main.ts</>
 "#))
     .arg(
       {

filename: cli/lib.rs
patch: @@ -791,12 +791,35 @@ async fn resolve_flags_and_init(
     );
   }
 
-  if let Ok(audit_path) = std::env::var("DENO_AUDIT_PERMISSIONS") {
-    let audit_file = deno_runtime::deno_permissions::AUDIT_FILE.set(
-      deno_core::parking_lot::Mutex::new(std::fs::File::create(audit_path)?),
-    );
-    if audit_file.is_err() {
-      log::warn!("⚠️  {}", colors::yellow("Audit file is already set"));
+  if let Ok(audit_target) = std::env::var("DENO_AUDIT_PERMISSIONS") {
+    use deno_runtime::deno_permissions::AuditSink;
+
+    let sink = if audit_target == "otel" {
+      AuditSink::Otel(|permission, value, stack| {
+        let stack = stack.unwrap_or_default().join("\n");
+        let kvs = HashMap::from([
+          ("deno.permission.type", permission),
+          ("deno.permission.value", value),
+          ("deno.permission.stack", stack.as_str()),
+        ]);
+        deno_telemetry::handle_log(
+          &log::Record::builder()
+            .level(log::Level::Info)
+            .target("deno.permission.access")
+            .args(format_args!("{permission}: {value}"))
+            .key_values(&kvs)
+            .build(),
+        );
+      })
+    } else {
+      AuditSink::File(deno_core::parking_lot::Mutex::new(
+        std::fs::File::create(audit_target)?,
+      ))
+    };
+
+    let result = deno_runtime::deno_permissions::AUDIT_SINK.set(sink);
+    if result.is_err() {
+      log::warn!("⚠️  {}", colors::yellow("Audit sink is already set"));
     }
   }
 

filename: ext/telemetry/lib.rs
patch: @@ -43,7 +43,7 @@ pub use opentelemetry::KeyValue;
 pub use opentelemetry::StringValue;
 pub use opentelemetry::Value;
 use opentelemetry::logs::AnyValue;
-use opentelemetry::logs::LogRecord as LogRecordTrait;
+pub use opentelemetry::logs::LogRecord as LogRecordTrait;
 use opentelemetry::logs::Severity;
 use opentelemetry::metrics::AsyncInstrumentBuilder;
 pub use opentelemetry::metrics::Gauge;
@@ -70,7 +70,7 @@ use opentelemetry_sdk::Resource;
 use opentelemetry_sdk::export::trace::SpanData;
 use opentelemetry_sdk::logs::BatchLogProcessor;
 use opentelemetry_sdk::logs::LogProcessor;
-use opentelemetry_sdk::logs::LogRecord;
+pub use opentelemetry_sdk::logs::LogRecord;
 use opentelemetry_sdk::metrics::ManualReader;
 use opentelemetry_sdk::metrics::MetricResult;
 use opentelemetry_sdk::metrics::SdkMeterProvider;

filename: runtime/permissions/lib.rs
patch: @@ -55,7 +55,15 @@ pub enum BrokerResponse {
 use self::broker::has_broker;
 use self::broker::maybe_check_with_broker;
 
-pub static AUDIT_FILE: OnceLock<Mutex<std::fs::File>> = OnceLock::new();
+pub type OtelAuditFn =
+  fn(permission: &str, value: &str, stack: Option<&[String]>);
+
+pub enum AuditSink {
+  File(Mutex<std::fs::File>),
+  Otel(OtelAuditFn),
+}
+
+pub static AUDIT_SINK: OnceLock<AuditSink> = OnceLock::new();
 
 #[derive(Debug, thiserror::Error, deno_error::JsError)]
 #[error("{}", custom_message.as_ref().cloned().unwrap_or_else(|| format!("Requires {access}, {}", format_permission_error(.name))))]
@@ -81,34 +89,51 @@ fn write_audit<T>(flag_name: &str, value: T)
 where
   T: Serialize,
 {
-  let Some(file) = AUDIT_FILE.get() else {
+  let Some(sink) = AUDIT_SINK.get() else {
     return;
   };
 
-  let mut file = file.lock();
-
-  let mut map = serde_json::Map::with_capacity(5);
-  let _ = map.insert("v".into(), serde_json::Value::Number(1.into()));
-  let _ = map.insert(
-    "datetime".into(),
-    serde_json::Value::String(
-      chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
-    ),
-  );
-  let _ = map.insert(
-    "permission".into(),
-    serde_json::to_value(flag_name).unwrap(),
-  );
-  let _ = map.insert("value".into(), serde_json::to_value(value).unwrap());
-
   let get_stack = MAYBE_CURRENT_STACKTRACE.lock();
-  if let Some(stack) = get_stack.as_ref().map(|s| s()) {
-    let _ = map.insert("stack".into(), serde_json::to_value(&stack).unwrap());
-  }
+  let stack = get_stack.as_ref().map(|s| s());
+
+  match sink {
+    AuditSink::File(file) => {
+      let mut file = file.lock();
+
+      let mut map = serde_json::Map::with_capacity(6);
+      let _ = map.insert("v".into(), serde_json::Value::Number(1.into()));
+      let _ = map.insert(
+        "datetime".into(),
+        serde_json::Value::String(
+          chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Secs, true),
+        ),
+      );
+      let _ = map.insert(
+        "permission".into(),
+        serde_json::to_value(flag_name).unwrap(),
+      );
+      let _ = map.insert("value".into(), serde_json::to_value(&value).unwrap());
 
-  let _ = file.write_all(
-    format!("{}\n", serde_json::to_string(&map).unwrap()).as_bytes(),
-  );
+      if let Some(ref stack) = stack {
+        let _ =
+          map.insert("stack".into(), serde_json::to_value(stack).unwrap());
+      }
+
+      let _ = file.write_all(
+        format!("{}\n", serde_json::to_string(&map).unwrap()).as_bytes(),
+      );
+    }
+    AuditSink::Otel(report_fn) => {
+      let value_str = serde_json::to_value(&value)
+        .map(|v| match v {
+          serde_json::Value::String(s) => s,
+          other => other.to_string(),
+        })
+        .unwrap_or_default();
+
+      report_fn(flag_name, &value_str, stack.as_deref());
+    }
+  }
 }
 
 /// Fast exit from permission check routines if this permission