fix: otel resiliency (#26857)

Improving the breadth of collected data, and ensuring that the collected
data is more likely to be successfully reported.

- Use `log` crate in more places
- Hook up `log` crate to otel
- Switch to process-wide otel processors
- Handle places that use `process::exit`

Also adds a more robust testing framework, with a deterministic tracing
setting.

Refs: https://github.com/denoland/deno/issues/26852
This commit is contained in:
snek 2024-11-14 13:16:28 +01:00 committed by GitHub
parent cb107a762f
commit 4e899d48cf
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
37 changed files with 411 additions and 175 deletions

View File

@ -141,7 +141,7 @@ jsonc-parser = { version = "=0.26.2", features = ["serde"] }
lazy-regex = "3" lazy-regex = "3"
libc = "0.2.126" libc = "0.2.126"
libz-sys = { version = "1.1.20", default-features = false } libz-sys = { version = "1.1.20", default-features = false }
log = "0.4.20" log = { version = "0.4.20", features = ["kv"] }
lsp-types = "=0.97.0" # used by tower-lsp and "proposed" feature is unstable in patch releases lsp-types = "=0.97.0" # used by tower-lsp and "proposed" feature is unstable in patch releases
memmem = "0.1.1" memmem = "0.1.1"
monch = "=0.5.0" monch = "=0.5.0"

View File

@ -36,6 +36,7 @@ use deno_path_util::normalize_path;
use deno_path_util::url_to_file_path; use deno_path_util::url_to_file_path;
use deno_runtime::deno_permissions::PermissionsOptions; use deno_runtime::deno_permissions::PermissionsOptions;
use deno_runtime::deno_permissions::SysDescriptor; use deno_runtime::deno_permissions::SysDescriptor;
use deno_runtime::ops::otel::OtelConfig;
use log::debug; use log::debug;
use log::Level; use log::Level;
use serde::Deserialize; use serde::Deserialize;
@ -967,6 +968,24 @@ impl Flags {
args args
} }
pub fn otel_config(&self) -> Option<OtelConfig> {
if self
.unstable_config
.features
.contains(&String::from("otel"))
{
Some(OtelConfig {
runtime_name: Cow::Borrowed("deno"),
runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno),
deterministic: std::env::var("DENO_UNSTABLE_OTEL_DETERMINISTIC")
.is_ok(),
..Default::default()
})
} else {
None
}
}
/// Extract the paths the config file should be discovered from. /// Extract the paths the config file should be discovered from.
/// ///
/// Returns `None` if the config file should not be auto-discovered. /// Returns `None` if the config file should not be auto-discovered.

View File

@ -823,10 +823,8 @@ impl CliOptions {
}; };
let msg = let msg =
format!("DANGER: TLS certificate validation is disabled {}", domains); format!("DANGER: TLS certificate validation is disabled {}", domains);
#[allow(clippy::print_stderr)]
{ {
// use eprintln instead of log::warn so this always gets shown log::error!("{}", colors::yellow(msg));
eprintln!("{}", colors::yellow(msg));
} }
} }
@ -1131,20 +1129,7 @@ impl CliOptions {
} }
pub fn otel_config(&self) -> Option<OtelConfig> { pub fn otel_config(&self) -> Option<OtelConfig> {
if self self.flags.otel_config()
.flags
.unstable_config
.features
.contains(&String::from("otel"))
{
Some(OtelConfig {
runtime_name: Cow::Borrowed("deno"),
runtime_version: Cow::Borrowed(crate::version::DENO_VERSION_INFO.deno),
..Default::default()
})
} else {
None
}
} }
pub fn env_file_name(&self) -> Option<&String> { pub fn env_file_name(&self) -> Option<&String> {

View File

@ -1,5 +1,6 @@
disallowed-methods = [ disallowed-methods = [
{ path = "reqwest::Client::new", reason = "create an HttpClient via an HttpClientProvider instead" }, { path = "reqwest::Client::new", reason = "create an HttpClient via an HttpClientProvider instead" },
{ path = "std::process::exit", reason = "use deno_runtime::exit instead" },
] ]
disallowed-types = [ disallowed-types = [
{ path = "reqwest::Client", reason = "use crate::http_util::HttpClient instead" }, { path = "reqwest::Client", reason = "use crate::http_util::HttpClient instead" },

View File

@ -188,7 +188,7 @@ pub fn graph_exit_integrity_errors(graph: &ModuleGraph) {
fn exit_for_integrity_error(err: &ModuleError) { fn exit_for_integrity_error(err: &ModuleError) {
if let Some(err_message) = enhanced_integrity_error_message(err) { if let Some(err_message) = enhanced_integrity_error_message(err) {
log::error!("{} {}", colors::red("error:"), err_message); log::error!("{} {}", colors::red("error:"), err_message);
std::process::exit(10); deno_runtime::exit(10);
} }
} }

View File

@ -11,7 +11,7 @@ pub fn start(parent_process_id: u32) {
std::thread::sleep(Duration::from_secs(10)); std::thread::sleep(Duration::from_secs(10));
if !is_process_active(parent_process_id) { if !is_process_active(parent_process_id) {
std::process::exit(1); deno_runtime::exit(1);
} }
}); });
} }

View File

@ -350,18 +350,17 @@ fn setup_panic_hook() {
eprintln!("Args: {:?}", env::args().collect::<Vec<_>>()); eprintln!("Args: {:?}", env::args().collect::<Vec<_>>());
eprintln!(); eprintln!();
orig_hook(panic_info); orig_hook(panic_info);
std::process::exit(1); deno_runtime::exit(1);
})); }));
} }
#[allow(clippy::print_stderr)]
fn exit_with_message(message: &str, code: i32) -> ! { fn exit_with_message(message: &str, code: i32) -> ! {
eprintln!( log::error!(
"{}: {}", "{}: {}",
colors::red_bold("error"), colors::red_bold("error"),
message.trim_start_matches("error: ") message.trim_start_matches("error: ")
); );
std::process::exit(code); deno_runtime::exit(code);
} }
fn exit_for_error(error: AnyError) -> ! { fn exit_for_error(error: AnyError) -> ! {
@ -380,13 +379,12 @@ fn exit_for_error(error: AnyError) -> ! {
exit_with_message(&error_string, error_code); exit_with_message(&error_string, error_code);
} }
#[allow(clippy::print_stderr)]
pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) { pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) {
eprintln!( log::error!(
"Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.", "Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.",
feature feature
); );
std::process::exit(70); deno_runtime::exit(70);
} }
pub fn main() { pub fn main() {
@ -419,7 +417,7 @@ pub fn main() {
drop(profiler); drop(profiler);
match result { match result {
Ok(exit_code) => std::process::exit(exit_code), Ok(exit_code) => deno_runtime::exit(exit_code),
Err(err) => exit_for_error(err), Err(err) => exit_for_error(err),
} }
} }
@ -433,12 +431,21 @@ fn resolve_flags_and_init(
if err.kind() == clap::error::ErrorKind::DisplayVersion => if err.kind() == clap::error::ErrorKind::DisplayVersion =>
{ {
// Ignore results to avoid BrokenPipe errors. // Ignore results to avoid BrokenPipe errors.
util::logger::init(None);
let _ = err.print(); let _ = err.print();
std::process::exit(0); deno_runtime::exit(0);
}
Err(err) => {
util::logger::init(None);
exit_for_error(AnyError::from(err))
} }
Err(err) => exit_for_error(AnyError::from(err)),
}; };
if let Some(otel_config) = flags.otel_config() {
deno_runtime::ops::otel::init(otel_config)?;
}
util::logger::init(flags.log_level);
// TODO(bartlomieju): remove in Deno v2.5 and hard error then. // TODO(bartlomieju): remove in Deno v2.5 and hard error then.
if flags.unstable_config.legacy_flag_enabled { if flags.unstable_config.legacy_flag_enabled {
log::warn!( log::warn!(
@ -467,7 +474,6 @@ fn resolve_flags_and_init(
deno_core::JsRuntime::init_platform( deno_core::JsRuntime::init_platform(
None, /* import assertions enabled */ false, None, /* import assertions enabled */ false,
); );
util::logger::init(flags.log_level);
Ok(flags) Ok(flags)
} }

View File

@ -40,23 +40,21 @@ use std::env::current_exe;
use crate::args::Flags; use crate::args::Flags;
#[allow(clippy::print_stderr)]
pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) { pub(crate) fn unstable_exit_cb(feature: &str, api_name: &str) {
eprintln!( log::error!(
"Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.", "Unstable API '{api_name}'. The `--unstable-{}` flag must be provided.",
feature feature
); );
std::process::exit(70); deno_runtime::exit(70);
} }
#[allow(clippy::print_stderr)]
fn exit_with_message(message: &str, code: i32) -> ! { fn exit_with_message(message: &str, code: i32) -> ! {
eprintln!( log::error!(
"{}: {}", "{}: {}",
colors::red_bold("error"), colors::red_bold("error"),
message.trim_start_matches("error: ") message.trim_start_matches("error: ")
); );
std::process::exit(code); deno_runtime::exit(code);
} }
fn unwrap_or_exit<T>(result: Result<T, AnyError>) -> T { fn unwrap_or_exit<T>(result: Result<T, AnyError>) -> T {
@ -89,13 +87,19 @@ fn main() {
let future = async move { let future = async move {
match standalone { match standalone {
Ok(Some(data)) => { Ok(Some(data)) => {
if let Some(otel_config) = data.metadata.otel_config.clone() {
deno_runtime::ops::otel::init(otel_config)?;
}
util::logger::init(data.metadata.log_level); util::logger::init(data.metadata.log_level);
load_env_vars(&data.metadata.env_vars_from_env_file); load_env_vars(&data.metadata.env_vars_from_env_file);
let exit_code = standalone::run(data).await?; let exit_code = standalone::run(data).await?;
std::process::exit(exit_code); deno_runtime::exit(exit_code);
} }
Ok(None) => Ok(()), Ok(None) => Ok(()),
Err(err) => Err(err), Err(err) => {
util::logger::init(None);
Err(err)
}
} }
}; };

View File

@ -517,7 +517,7 @@ impl<'a> DenoCompileBinaryWriter<'a> {
Some(bytes) => bytes, Some(bytes) => bytes,
None => { None => {
log::info!("Download could not be found, aborting"); log::info!("Download could not be found, aborting");
std::process::exit(1) deno_runtime::exit(1);
} }
}; };

View File

@ -191,7 +191,7 @@ pub async fn lint(
linter.finish() linter.finish()
}; };
if !success { if !success {
std::process::exit(1); deno_runtime::exit(1);
} }
} }

View File

@ -1357,6 +1357,7 @@ pub async fn report_tests(
if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) { if let Err(err) = reporter.flush_report(&elapsed, &tests, &test_steps) {
eprint!("Test reporter failed to flush: {}", err) eprint!("Test reporter failed to flush: {}", err)
} }
#[allow(clippy::disallowed_methods)]
std::process::exit(130); std::process::exit(130);
} }
} }
@ -1642,6 +1643,7 @@ pub async fn run_tests_with_watch(
loop { loop {
signal::ctrl_c().await.unwrap(); signal::ctrl_c().await.unwrap();
if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) { if !HAS_TEST_RUN_SIGINT_HANDLER.load(Ordering::Relaxed) {
#[allow(clippy::disallowed_methods)]
std::process::exit(130); std::process::exit(130);
} }
} }

View File

@ -540,7 +540,7 @@ pub async fn upgrade(
let Some(archive_data) = download_package(&client, download_url).await? let Some(archive_data) = download_package(&client, download_url).await?
else { else {
log::error!("Download could not be found, aborting"); log::error!("Download could not be found, aborting");
std::process::exit(1) deno_runtime::exit(1)
}; };
log::info!( log::info!(

View File

@ -73,7 +73,6 @@ impl DebouncedReceiver {
} }
} }
#[allow(clippy::print_stderr)]
async fn error_handler<F>(watch_future: F) -> bool async fn error_handler<F>(watch_future: F) -> bool
where where
F: Future<Output = Result<(), AnyError>>, F: Future<Output = Result<(), AnyError>>,
@ -84,7 +83,7 @@ where
Some(e) => format_js_error(e), Some(e) => format_js_error(e),
None => format!("{err:?}"), None => format!("{err:?}"),
}; };
eprintln!( log::error!(
"{}: {}", "{}: {}",
colors::red_bold("error"), colors::red_bold("error"),
error_string.trim_start_matches("error: ") error_string.trim_start_matches("error: ")

View File

@ -29,6 +29,7 @@ impl log::Log for CliLogger {
// thread's state // thread's state
DrawThread::hide(); DrawThread::hide();
self.0.log(record); self.0.log(record);
deno_runtime::ops::otel::handle_log(record);
DrawThread::show(); DrawThread::show();
} }
} }

View File

@ -46,15 +46,14 @@ pub fn init_v8_flags(
.skip(1) .skip(1)
.collect::<Vec<_>>(); .collect::<Vec<_>>();
#[allow(clippy::print_stderr)]
if !unrecognized_v8_flags.is_empty() { if !unrecognized_v8_flags.is_empty() {
for f in unrecognized_v8_flags { for f in unrecognized_v8_flags {
eprintln!("error: V8 did not recognize flag '{f}'"); log::error!("error: V8 did not recognize flag '{f}'");
} }
eprintln!("\nFor a list of V8 flags, use '--v8-flags=--help'"); log::error!("\nFor a list of V8 flags, use '--v8-flags=--help'");
std::process::exit(1); deno_runtime::exit(1);
} }
if v8_flags_includes_help { if v8_flags_includes_help {
std::process::exit(0); deno_runtime::exit(0);
} }
} }

View File

@ -140,7 +140,6 @@ fn napi_fatal_exception(env: &mut Env, err: napi_value) -> napi_status {
} }
#[napi_sym] #[napi_sym]
#[allow(clippy::print_stderr)]
fn napi_fatal_error( fn napi_fatal_error(
location: *const c_char, location: *const c_char,
location_len: usize, location_len: usize,
@ -173,9 +172,9 @@ fn napi_fatal_error(
}; };
if let Some(location) = location { if let Some(location) = location {
eprintln!("NODE API FATAL ERROR: {} {}", location, message); log::error!("NODE API FATAL ERROR: {} {}", location, message);
} else { } else {
eprintln!("NODE API FATAL ERROR: {}", message); log::error!("NODE API FATAL ERROR: {}", message);
} }
std::process::abort(); std::process::abort();

View File

@ -5,10 +5,9 @@ use deno_core::parking_lot::Mutex;
use std::mem::MaybeUninit; use std::mem::MaybeUninit;
use std::ptr::addr_of_mut; use std::ptr::addr_of_mut;
#[allow(clippy::print_stderr)]
fn assert_ok(res: c_int) -> c_int { fn assert_ok(res: c_int) -> c_int {
if res != 0 { if res != 0 {
eprintln!("bad result in uv polyfill: {res}"); log::error!("bad result in uv polyfill: {res}");
// don't panic because that might unwind into // don't panic because that might unwind into
// c/c++ // c/c++
std::process::abort(); std::process::abort();

View File

@ -42,4 +42,5 @@ disallowed-methods = [
{ path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" }, { path = "std::fs::write", reason = "File system operations should be done using FileSystem trait" },
{ path = "std::path::Path::canonicalize", reason = "File system operations should be done using FileSystem trait" }, { path = "std::path::Path::canonicalize", reason = "File system operations should be done using FileSystem trait" },
{ path = "std::path::Path::exists", reason = "File system operations should be done using FileSystem trait" }, { path = "std::path::Path::exists", reason = "File system operations should be done using FileSystem trait" },
{ path = "std::process::exit", reason = "use deno_runtime::exit instead" },
] ]

View File

@ -22,11 +22,14 @@ const {
ReflectApply, ReflectApply,
SymbolFor, SymbolFor,
Error, Error,
NumberPrototypeToString,
StringPrototypePadStart,
} = primordials; } = primordials;
const { AsyncVariable, setAsyncContext } = core; const { AsyncVariable, setAsyncContext } = core;
const CURRENT = new AsyncVariable(); const CURRENT = new AsyncVariable();
let TRACING_ENABLED = false; let TRACING_ENABLED = false;
let DETERMINISTIC = false;
const SPAN_ID_BYTES = 8; const SPAN_ID_BYTES = 8;
const TRACE_ID_BYTES = 16; const TRACE_ID_BYTES = 16;
@ -45,7 +48,19 @@ const hexSliceLookupTable = (function () {
return table; return table;
})(); })();
let counter = 1;
const INVALID_SPAN_ID = "0000000000000000";
const INVALID_TRACE_ID = "00000000000000000000000000000000";
function generateId(bytes) { function generateId(bytes) {
if (DETERMINISTIC) {
return StringPrototypePadStart(
NumberPrototypeToString(counter++, 16),
bytes * 2,
"0",
);
}
let out = ""; let out = "";
for (let i = 0; i < bytes / 4; i += 1) { for (let i = 0; i < bytes / 4; i += 1) {
const r32 = (MathRandom() * 2 ** 32) >>> 0; const r32 = (MathRandom() * 2 ** 32) >>> 0;
@ -112,8 +127,6 @@ function submit(span) {
const now = () => (performance.timeOrigin + performance.now()) / 1000; const now = () => (performance.timeOrigin + performance.now()) / 1000;
const INVALID_SPAN_ID = "0000000000000000";
const INVALID_TRACE_ID = "00000000000000000000000000000000";
const NO_ASYNC_CONTEXT = {}; const NO_ASYNC_CONTEXT = {};
class Span { class Span {
@ -362,9 +375,10 @@ const otelConsoleConfig = {
export function bootstrap(config) { export function bootstrap(config) {
if (config.length === 0) return; if (config.length === 0) return;
const { 0: consoleConfig } = config; const { 0: consoleConfig, 1: deterministic } = config;
TRACING_ENABLED = true; TRACING_ENABLED = true;
DETERMINISTIC = deterministic === 1;
switch (consoleConfig) { switch (consoleConfig) {
case otelConsoleConfig.capture: case otelConsoleConfig.capture:

View File

@ -140,6 +140,12 @@ pub static UNSTABLE_GRANULAR_FLAGS: &[UnstableGranularFlag] = &[
}, },
]; ];
pub fn exit(code: i32) -> ! {
crate::ops::otel::flush();
#[allow(clippy::disallowed_methods)]
std::process::exit(code);
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;

View File

@ -186,10 +186,8 @@ fn op_get_exit_code(state: &mut OpState) -> i32 {
#[op2(fast)] #[op2(fast)]
fn op_exit(state: &mut OpState) { fn op_exit(state: &mut OpState) {
crate::ops::otel::otel_drop_state(state);
let code = state.borrow::<ExitCode>().get(); let code = state.borrow::<ExitCode>().get();
std::process::exit(code) crate::exit(code)
} }
#[op2] #[op2]

View File

@ -13,6 +13,9 @@ use deno_core::op2;
use deno_core::v8; use deno_core::v8;
use deno_core::OpState; use deno_core::OpState;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use once_cell::sync::OnceCell;
use opentelemetry::logs::AnyValue;
use opentelemetry::logs::LogRecord as LogRecordTrait;
use opentelemetry::logs::Severity; use opentelemetry::logs::Severity;
use opentelemetry::trace::SpanContext; use opentelemetry::trace::SpanContext;
use opentelemetry::trace::SpanId; use opentelemetry::trace::SpanId;
@ -58,15 +61,15 @@ type LogProcessor = BatchLogProcessor<OtelSharedRuntime>;
deno_core::extension!( deno_core::extension!(
deno_otel, deno_otel,
ops = [op_otel_log, op_otel_span_start, op_otel_span_continue, op_otel_span_attribute, op_otel_span_attribute2, op_otel_span_attribute3, op_otel_span_flush], ops = [
options = { op_otel_log,
otel_config: Option<OtelConfig>, // `None` means OpenTelemetry is disabled. op_otel_span_start,
}, op_otel_span_continue,
state = |state, options| { op_otel_span_attribute,
if let Some(otel_config) = options.otel_config { op_otel_span_attribute2,
otel_create_globals(otel_config, state).unwrap(); op_otel_span_attribute3,
} op_otel_span_flush,
} ],
); );
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -74,6 +77,7 @@ pub struct OtelConfig {
pub runtime_name: Cow<'static, str>, pub runtime_name: Cow<'static, str>,
pub runtime_version: Cow<'static, str>, pub runtime_version: Cow<'static, str>,
pub console: OtelConsoleConfig, pub console: OtelConsoleConfig,
pub deterministic: bool,
} }
#[derive(Debug, Clone, Copy, Serialize, Deserialize)] #[derive(Debug, Clone, Copy, Serialize, Deserialize)]
@ -90,6 +94,7 @@ impl Default for OtelConfig {
runtime_name: Cow::Borrowed(env!("CARGO_PKG_NAME")), runtime_name: Cow::Borrowed(env!("CARGO_PKG_NAME")),
runtime_version: Cow::Borrowed(env!("CARGO_PKG_VERSION")), runtime_version: Cow::Borrowed(env!("CARGO_PKG_VERSION")),
console: OtelConsoleConfig::Capture, console: OtelConsoleConfig::Capture,
deterministic: false,
} }
} }
} }
@ -295,10 +300,10 @@ mod hyper_client {
} }
} }
fn otel_create_globals( static OTEL_PROCESSORS: OnceCell<(SpanProcessor, LogProcessor)> =
config: OtelConfig, OnceCell::new();
op_state: &mut OpState,
) -> anyhow::Result<()> { pub fn init(config: OtelConfig) -> anyhow::Result<()> {
// Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_* // Parse the `OTEL_EXPORTER_OTLP_PROTOCOL` variable. The opentelemetry_*
// crates don't do this automatically. // crates don't do this automatically.
// TODO(piscisaureus): enable GRPC support. // TODO(piscisaureus): enable GRPC support.
@ -318,7 +323,7 @@ fn otel_create_globals(
return Err(anyhow!( return Err(anyhow!(
"Failed to read env var OTEL_EXPORTER_OTLP_PROTOCOL: {}", "Failed to read env var OTEL_EXPORTER_OTLP_PROTOCOL: {}",
err err
)) ));
} }
}; };
@ -372,7 +377,6 @@ fn otel_create_globals(
let mut span_processor = let mut span_processor =
BatchSpanProcessor::builder(span_exporter, OtelSharedRuntime).build(); BatchSpanProcessor::builder(span_exporter, OtelSharedRuntime).build();
span_processor.set_resource(&resource); span_processor.set_resource(&resource);
op_state.put::<SpanProcessor>(span_processor);
let log_exporter = HttpExporterBuilder::default() let log_exporter = HttpExporterBuilder::default()
.with_http_client(client) .with_http_client(client)
@ -381,36 +385,92 @@ fn otel_create_globals(
let log_processor = let log_processor =
BatchLogProcessor::builder(log_exporter, OtelSharedRuntime).build(); BatchLogProcessor::builder(log_exporter, OtelSharedRuntime).build();
log_processor.set_resource(&resource); log_processor.set_resource(&resource);
op_state.put::<LogProcessor>(log_processor);
OTEL_PROCESSORS
.set((span_processor, log_processor))
.map_err(|_| anyhow!("failed to init otel"))?;
Ok(()) Ok(())
} }
/// This function is called by the runtime whenever it is about to call /// This function is called by the runtime whenever it is about to call
/// `os::process::exit()`, to ensure that all OpenTelemetry logs are properly /// `process::exit()`, to ensure that all OpenTelemetry logs are properly
/// flushed before the process terminates. /// flushed before the process terminates.
pub fn otel_drop_state(state: &mut OpState) { pub fn flush() {
if let Some(processor) = state.try_take::<SpanProcessor>() { if let Some((span_processor, log_processor)) = OTEL_PROCESSORS.get() {
let _ = processor.force_flush(); let _ = span_processor.force_flush();
drop(processor); let _ = log_processor.force_flush();
} }
if let Some(processor) = state.try_take::<LogProcessor>() { }
let _ = processor.force_flush();
drop(processor); pub fn handle_log(record: &log::Record) {
use log::Level;
let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
return;
};
let mut log_record = LogRecord::default();
log_record.set_observed_timestamp(SystemTime::now());
log_record.set_severity_number(match record.level() {
Level::Error => Severity::Error,
Level::Warn => Severity::Warn,
Level::Info => Severity::Info,
Level::Debug => Severity::Debug,
Level::Trace => Severity::Trace,
});
log_record.set_severity_text(record.level().as_str());
log_record.set_body(record.args().to_string().into());
log_record.set_target(record.metadata().target().to_string());
struct Visitor<'s>(&'s mut LogRecord);
impl<'s, 'kvs> log::kv::VisitSource<'kvs> for Visitor<'s> {
fn visit_pair(
&mut self,
key: log::kv::Key<'kvs>,
value: log::kv::Value<'kvs>,
) -> Result<(), log::kv::Error> {
#[allow(clippy::manual_map)]
let value = if let Some(v) = value.to_bool() {
Some(AnyValue::Boolean(v))
} else if let Some(v) = value.to_borrowed_str() {
Some(AnyValue::String(v.to_owned().into()))
} else if let Some(v) = value.to_f64() {
Some(AnyValue::Double(v))
} else if let Some(v) = value.to_i64() {
Some(AnyValue::Int(v))
} else {
None
};
if let Some(value) = value {
let key = Key::from(key.as_str().to_owned());
self.0.add_attribute(key, value);
}
Ok(())
}
} }
let _ = record.key_values().visit(&mut Visitor(&mut log_record));
log_processor.emit(
&mut log_record,
&InstrumentationScope::builder("deno").build(),
);
} }
#[op2(fast)] #[op2(fast)]
fn op_otel_log( fn op_otel_log(
state: &mut OpState,
#[string] message: String, #[string] message: String,
#[smi] level: i32, #[smi] level: i32,
#[string] trace_id: &str, #[string] trace_id: &str,
#[string] span_id: &str, #[string] span_id: &str,
#[smi] trace_flags: u8, #[smi] trace_flags: u8,
) { ) {
let Some(logger) = state.try_borrow::<LogProcessor>() else { let Some((_, log_processor)) = OTEL_PROCESSORS.get() else {
log::error!("op_otel_log: OpenTelemetry Logger not available");
return; return;
}; };
@ -425,23 +485,21 @@ fn op_otel_log(
let mut log_record = LogRecord::default(); let mut log_record = LogRecord::default();
log_record.observed_timestamp = Some(SystemTime::now()); log_record.set_observed_timestamp(SystemTime::now());
log_record.body = Some(message.into()); log_record.set_body(message.into());
log_record.severity_number = Some(severity); log_record.set_severity_number(severity);
log_record.severity_text = Some(severity.name()); log_record.set_severity_text(severity.name());
if let (Ok(trace_id), Ok(span_id)) = if let (Ok(trace_id), Ok(span_id)) =
(TraceId::from_hex(trace_id), SpanId::from_hex(span_id)) (TraceId::from_hex(trace_id), SpanId::from_hex(span_id))
{ {
let span_context = SpanContext::new( log_record.set_trace_context(
trace_id, trace_id,
span_id, span_id,
TraceFlags::new(trace_flags), Some(TraceFlags::new(trace_flags)),
false,
Default::default(),
); );
log_record.trace_context = Some((&span_context).into());
} }
logger.emit(
log_processor.emit(
&mut log_record, &mut log_record,
&InstrumentationScope::builder("deno").build(), &InstrumentationScope::builder("deno").build(),
); );
@ -463,7 +521,7 @@ fn op_otel_span_start<'s>(
end_time: f64, end_time: f64,
) -> Result<(), anyhow::Error> { ) -> Result<(), anyhow::Error> {
if let Some(temporary_span) = state.try_take::<TemporarySpan>() { if let Some(temporary_span) = state.try_take::<TemporarySpan>() {
let Some(span_processor) = state.try_borrow::<SpanProcessor>() else { let Some((span_processor, _)) = OTEL_PROCESSORS.get() else {
return Ok(()); return Ok(());
}; };
span_processor.on_end(temporary_span.0); span_processor.on_end(temporary_span.0);
@ -678,7 +736,7 @@ fn op_otel_span_flush(state: &mut OpState) {
return; return;
}; };
let Some(span_processor) = state.try_borrow::<SpanProcessor>() else { let Some((span_processor, _)) = OTEL_PROCESSORS.get() else {
return; return;
}; };

View File

@ -312,7 +312,7 @@ pub fn create_runtime_snapshot(
), ),
ops::fs_events::deno_fs_events::init_ops(), ops::fs_events::deno_fs_events::init_ops(),
ops::os::deno_os::init_ops(Default::default()), ops::os::deno_os::init_ops(Default::default()),
ops::otel::deno_otel::init_ops(None), ops::otel::deno_otel::init_ops(),
ops::permissions::deno_permissions::init_ops(), ops::permissions::deno_permissions::init_ops(),
ops::process::deno_process::init_ops(None), ops::process::deno_process::init_ops(None),
ops::signal::deno_signal::init_ops(), ops::signal::deno_signal::init_ops(),

View File

@ -512,9 +512,7 @@ impl WebWorker {
), ),
ops::fs_events::deno_fs_events::init_ops_and_esm(), ops::fs_events::deno_fs_events::init_ops_and_esm(),
ops::os::deno_os_worker::init_ops_and_esm(), ops::os::deno_os_worker::init_ops_and_esm(),
ops::otel::deno_otel::init_ops_and_esm( ops::otel::deno_otel::init_ops_and_esm(),
options.bootstrap.otel_config.clone(),
),
ops::permissions::deno_permissions::init_ops_and_esm(), ops::permissions::deno_permissions::init_ops_and_esm(),
ops::process::deno_process::init_ops_and_esm( ops::process::deno_process::init_ops_and_esm(
services.npm_process_state_provider, services.npm_process_state_provider,
@ -831,13 +829,12 @@ impl WebWorker {
// TODO(mmastrac): we don't want to test this w/classic workers because // TODO(mmastrac): we don't want to test this w/classic workers because
// WPT triggers a failure here. This is only exposed via --enable-testing-features-do-not-use. // WPT triggers a failure here. This is only exposed via --enable-testing-features-do-not-use.
#[allow(clippy::print_stderr)]
if self.worker_type == WebWorkerType::Module { if self.worker_type == WebWorkerType::Module {
panic!( panic!(
"coding error: either js is polling or the worker is terminated" "coding error: either js is polling or the worker is terminated"
); );
} else { } else {
eprintln!("classic worker terminated unexpectedly"); log::error!("classic worker terminated unexpectedly");
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
} }
@ -905,7 +902,6 @@ impl WebWorker {
} }
} }
#[allow(clippy::print_stderr)]
fn print_worker_error( fn print_worker_error(
error: &AnyError, error: &AnyError,
name: &str, name: &str,
@ -918,7 +914,7 @@ fn print_worker_error(
}, },
None => error.to_string(), None => error.to_string(),
}; };
eprintln!( log::error!(
"{}: Uncaught (in worker \"{}\") {}", "{}: Uncaught (in worker \"{}\") {}",
colors::red_bold("error"), colors::red_bold("error"),
name, name,

View File

@ -422,9 +422,7 @@ impl MainWorker {
), ),
ops::fs_events::deno_fs_events::init_ops_and_esm(), ops::fs_events::deno_fs_events::init_ops_and_esm(),
ops::os::deno_os::init_ops_and_esm(exit_code.clone()), ops::os::deno_os::init_ops_and_esm(exit_code.clone()),
ops::otel::deno_otel::init_ops_and_esm( ops::otel::deno_otel::init_ops_and_esm(),
options.bootstrap.otel_config.clone(),
),
ops::permissions::deno_permissions::init_ops_and_esm(), ops::permissions::deno_permissions::init_ops_and_esm(),
ops::process::deno_process::init_ops_and_esm( ops::process::deno_process::init_ops_and_esm(
services.npm_process_state_provider, services.npm_process_state_provider,

View File

@ -226,7 +226,7 @@ impl BootstrapOptions {
serve_is_main, serve_is_main,
serve_worker_count, serve_worker_count,
if let Some(otel_config) = self.otel_config.as_ref() { if let Some(otel_config) = self.otel_config.as_ref() {
Box::new([otel_config.console as u8]) Box::new([otel_config.console as u8, otel_config.deterministic as u8])
} else { } else {
Box::new([]) Box::new([])
}, },

View File

@ -1,4 +1,32 @@
{ {
"args": "run -A main.ts", "steps": [
"output": "processed\n" {
"args": "run -A main.ts basic.ts",
"envs": {
"DENO_UNSTABLE_OTEL_DETERMINISTIC": "1"
},
"output": "basic.out"
},
{
"args": "run -A main.ts natural_exit.ts",
"envs": {
"DENO_UNSTABLE_OTEL_DETERMINISTIC": "1"
},
"output": "natural_exit.out"
},
{
"args": "run -A main.ts deno_dot_exit.ts",
"envs": {
"DENO_UNSTABLE_OTEL_DETERMINISTIC": "1"
},
"output": "deno_dot_exit.out"
},
{
"args": "run -A main.ts uncaught.ts",
"envs": {
"DENO_UNSTABLE_OTEL_DETERMINISTIC": "1"
},
"output": "uncaught.out"
}
]
} }

View File

@ -0,0 +1,76 @@
{
"spans": [
{
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003",
"traceState": "",
"parentSpanId": "0000000000000001",
"flags": 1,
"name": "inner span",
"kind": 1,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
},
{
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000001",
"traceState": "",
"parentSpanId": "",
"flags": 1,
"name": "outer span",
"kind": 1,
"startTimeUnixNano": "[WILDCARD]",
"endTimeUnixNano": "[WILDCARD]",
"attributes": [],
"droppedAttributesCount": 0,
"events": [],
"droppedEventsCount": 0,
"links": [],
"droppedLinksCount": 0,
"status": {
"message": "",
"code": 0
}
}
],
"logs": [
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 9,
"severityText": "INFO",
"body": {
"stringValue": "log 1\n"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 1,
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003"
},
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 9,
"severityText": "INFO",
"body": {
"stringValue": "log 2\n"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 1,
"traceId": "00000000000000000000000000000002",
"spanId": "0000000000000003"
}
]
}

View File

@ -7,10 +7,14 @@ async function inner() {
console.log("log 2"); console.log("log 2");
} }
Deno.serve({ const server = Deno.serve({
port: 0, port: 0,
onListen({ port }) { async onListen({ port }) {
console.log(port.toString()); try {
await fetch(`http://localhost:${port}`);
} finally {
server.shutdown();
}
}, },
handler: async (_req) => { handler: async (_req) => {
using _span = new Deno.tracing.Span("outer span"); using _span = new Deno.tracing.Span("outer span");

View File

@ -0,0 +1,19 @@
{
"spans": [],
"logs": [
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 9,
"severityText": "INFO",
"body": {
"stringValue": "log 1\n"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 0,
"traceId": "",
"spanId": ""
}
]
}

View File

@ -0,0 +1,2 @@
console.log("log 1");
Deno.exit(0);

View File

@ -1,76 +1,38 @@
// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
import { assert, assertEquals } from "@std/assert"; const data = {
import { TextLineStream } from "@std/streams/text-line-stream"; spans: [],
logs: [],
};
const logs = []; const server = Deno.serve(
const spans = [];
let child: Deno.ChildProcess;
Deno.serve(
{ {
port: 0, port: 0,
async onListen({ port }) { onListen({ port }) {
const command = new Deno.Command(Deno.execPath(), { const command = new Deno.Command(Deno.execPath(), {
args: ["run", "-A", "--unstable-otel", "child.ts"], args: ["run", "-A", "--unstable-otel", Deno.args[0]],
env: { env: {
OTEL_EXPORTER_OTLP_PROTOCOL: "http/json", OTEL_EXPORTER_OTLP_PROTOCOL: "http/json",
OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`, OTEL_EXPORTER_OTLP_ENDPOINT: `http://localhost:${port}`,
OTEL_BSP_SCHEDULE_DELAY: "10",
OTEL_BLRP_SCHEDULE_DELAY: "10",
}, },
stdin: "piped", stdout: "null",
stdout: "piped", });
stderr: "inherit", const child = command.spawn();
child.output().then(() => {
server.shutdown();
console.log(JSON.stringify(data, null, 2));
}); });
child = command.spawn();
const lines = child.stdout
.pipeThrough(new TextDecoderStream())
.pipeThrough(new TextLineStream())
.getReader();
const line = await lines.read();
await fetch(`http://localhost:${line.value}/`);
}, },
async handler(req) { async handler(req) {
try { const body = await req.json();
const body = await req.json(); if (body.resourceLogs) {
if (body.resourceLogs) { data.logs.push(...body.resourceLogs[0].scopeLogs[0].logRecords);
logs.push(...body.resourceLogs[0].scopeLogs[0].logRecords);
}
if (body.resourceSpans) {
spans.push(...body.resourceSpans[0].scopeSpans[0].spans);
}
if (logs.length > 2 && spans.length > 1) {
child.kill();
const inner = spans.find((s) => s.name === "inner span");
const outer = spans.find((s) => s.name === "outer span");
assertEquals(inner.traceId, outer.traceId);
assertEquals(inner.parentSpanId, outer.spanId);
assertEquals(logs[1].body.stringValue, "log 1\n");
assertEquals(logs[1].traceId, inner.traceId);
assertEquals(logs[1].spanId, inner.spanId);
assertEquals(logs[2].body.stringValue, "log 2\n");
assertEquals(logs[2].traceId, inner.traceId);
assertEquals(logs[2].spanId, inner.spanId);
console.log("processed");
Deno.exit(0);
}
return Response.json({ partialSuccess: {} }, { status: 200 });
} catch (e) {
console.error(e);
Deno.exit(1);
} }
if (body.resourceSpans) {
data.spans.push(...body.resourceSpans[0].scopeSpans[0].spans);
}
return Response.json({ partialSuccess: {} }, { status: 200 });
}, },
}, },
); );
setTimeout(() => {
assert(false, "test did not finish in time");
}, 10e3);

View File

@ -0,0 +1,19 @@
{
"spans": [],
"logs": [
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 9,
"severityText": "INFO",
"body": {
"stringValue": "log 1\n"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 0,
"traceId": "",
"spanId": ""
}
]
}

View File

@ -0,0 +1 @@
console.log("log 1");

View File

@ -0,0 +1,37 @@
error: Uncaught (in promise) Error: uncaught
throw new Error("uncaught");
^
at file:///[WILDCARD]/tests/specs/cli/otel_basic/uncaught.ts:2:7
{
"spans": [],
"logs": [
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 9,
"severityText": "INFO",
"body": {
"stringValue": "log 1\n"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 0,
"traceId": "",
"spanId": ""
},
{
"timeUnixNano": "0",
"observedTimeUnixNano": "[WILDCARD]",
"severityNumber": 17,
"severityText": "ERROR",
"body": {
"stringValue": "error: Uncaught (in promise) Error: uncaught\nthrow new Error(\"uncaught\");\n ^\n at file:///[WILDCARD]/tests/specs/cli/otel_basic/uncaught.ts:2:7"
},
"attributes": [],
"droppedAttributesCount": 0,
"flags": 0,
"traceId": "",
"spanId": ""
}
]
}

View File

@ -0,0 +1,2 @@
console.log("log 1");
throw new Error("uncaught");

View File

@ -33,6 +33,7 @@ macro_rules! timeout {
use std::io::Write; use std::io::Write;
eprintln!("Test {function} timed out after {timeout} seconds, aborting"); eprintln!("Test {function} timed out after {timeout} seconds, aborting");
_ = std::io::stderr().flush(); _ = std::io::stderr().flush();
#[allow(clippy::disallowed_methods)]
::std::process::exit(1); ::std::process::exit(1);
} }
}); });