use std::thread::ThreadId as StdThreadId;
use data_privacy::classified;
use observed::event;
use super::{PANIC_MESSAGE, SYSTEM_METADATA};
#[classified(SYSTEM_METADATA)]
#[derive(Clone, Copy)]
pub(crate) struct SystemMetricCount(pub i64);
impl From<usize> for SystemMetricCount {
fn from(value: usize) -> Self {
Self(i64::try_from(value).unwrap_or(i64::MAX))
}
}
#[classified(SYSTEM_METADATA)]
#[derive(Clone, Copy)]
pub(crate) struct ProcessorIndex(pub i64);
impl From<usize> for ProcessorIndex {
fn from(value: usize) -> Self {
Self(i64::try_from(value).unwrap_or(i64::MAX))
}
}
#[classified(SYSTEM_METADATA)]
#[derive(Clone, Copy)]
pub(crate) struct BlockingWorkerPoolMode(pub &'static str);
#[classified(SYSTEM_METADATA)]
#[derive(Clone, Copy)]
pub(crate) struct PlacementLabel(pub &'static str);
#[classified(SYSTEM_METADATA)]
#[derive(Clone)]
pub(crate) struct ThreadName(pub String);
impl ThreadName {
pub(crate) fn new(name: &str) -> Self {
Self(name.to_owned())
}
}
#[classified(SYSTEM_METADATA)]
#[derive(Clone)]
pub(crate) struct ThreadId(pub String);
impl From<StdThreadId> for ThreadId {
fn from(id: StdThreadId) -> Self {
Self(format!("{id:?}"))
}
}
#[classified(PANIC_MESSAGE)]
#[derive(Clone)]
pub(crate) struct PanicMessage(pub String);
#[cfg(any(debug_assertions, test))] #[classified(SYSTEM_METADATA)]
#[derive(Clone)]
pub(crate) struct BacktraceText(pub String);
#[event("arty.rt.started")]
#[info("runtime started")]
#[counter(name = "arty.rt.started")]
pub(crate) struct RuntimeStarted {
#[dimension(log = "processors.available", metric = "processors.available")]
pub processors_available: SystemMetricCount,
#[dimension(log = "processors.used", metric = "processors.used")]
pub processors_used: SystemMetricCount,
#[dimension(log = "blocking_worker_pool.mode", metric = "blocking_worker_pool.mode")]
pub blocking_worker_pool_mode: BlockingWorkerPoolMode,
pub stack_size_bytes: SystemMetricCount,
}
#[event("arty.rt.start_failed")]
#[error("runtime failed to start")]
#[counter(name = "arty.rt.start_failed")]
pub(crate) struct RuntimeStartFailed {
#[dimension(log = "blocking_worker_pool.mode", metric = "blocking_worker_pool.mode")]
pub blocking_worker_pool_mode: BlockingWorkerPoolMode,
}
#[event("arty.rt.stopping")]
#[debug("runtime stopping")]
pub(crate) struct RuntimeStopping;
#[event("arty.rt.stopped")]
#[info("runtime stopped")]
pub(crate) struct RuntimeStopped;
#[event("arty.rt.async_worker.started")]
#[debug("async worker started")]
pub(crate) struct AsyncWorkerStarted {
#[dimension(log = "processor.index")]
pub processor_index: ProcessorIndex,
}
#[event("arty.rt.async_worker.stopped")]
#[debug("async worker stopped")]
pub(crate) struct AsyncWorkerStopped {
#[dimension(log = "processor.index")]
pub processor_index: ProcessorIndex,
}
#[event("arty.rt.async_worker.active")]
#[updown_counter(delta, name = "arty.rt.async_worker.active")]
pub(crate) struct AsyncWorkerActive {
#[unredacted]
pub delta: i64,
}
#[event("arty.rt.task.spawned")]
#[counter(name = "arty.rt.task.spawned")]
pub(crate) struct TaskSpawned {
#[dimension(metric = "placement")]
pub placement: PlacementLabel,
}
#[event("arty.rt.task.succeeded")]
#[counter(name = "arty.rt.task.succeeded")]
pub(crate) struct TaskSucceeded;
#[event("arty.rt.task.panicked")]
#[error("task panicked")]
#[counter(name = "arty.rt.task.panicked")]
pub(crate) struct TaskPanicked;
#[event("arty.rt.thread.spawn")]
#[debug("spawning thread")]
pub(crate) struct ThreadSpawn {
#[dimension(log = "thread.name")]
pub name: ThreadName,
#[dimension(log = "thread.stack_size_bytes")]
pub stack_size_bytes: Option<SystemMetricCount>,
#[dimension(log = "thread.owner.name")]
pub owner_name: Option<ThreadName>,
#[dimension(log = "thread.owner.id")]
pub owner_id: ThreadId,
}
#[event("arty.rt.thread.started")]
#[debug("thread started")]
pub(crate) struct ThreadStarted {
#[dimension(log = "thread.name")]
pub name: Option<ThreadName>,
#[dimension(log = "arty.thread.id")]
pub id: ThreadId,
}
#[event("arty.rt.thread.panic")]
#[error("thread panicked")]
pub(crate) struct ThreadPanicked {
#[dimension(log = "thread.name")]
pub name: Option<ThreadName>,
#[dimension(log = "arty.thread.id")]
pub id: ThreadId,
#[dimension(log = "panic.message")]
pub message: PanicMessage,
}
#[event("arty.rt.thread.exiting")]
#[debug("thread exiting")]
pub(crate) struct ThreadExiting {
#[dimension(log = "thread.name")]
pub name: Option<ThreadName>,
#[dimension(log = "arty.thread.id")]
pub id: ThreadId,
}
#[cfg(any(debug_assertions, test))] #[event("arty.rt.builtins.thread_mismatch")]
#[warning("Builtins accessed from a different thread than it was created on")]
pub(crate) struct BuiltinsThreadMismatch {
#[dimension(log = "thread.name")]
pub name: ThreadName,
#[dimension(log = "arty.thread.id")]
pub id: ThreadId,
#[dimension(log = "backtrace")]
pub backtrace: BacktraceText,
}
#[event("arty.rt.blocking_worker.pool_saturated")]
#[warning("blocking worker pool is saturated and cannot grow")]
#[counter(name = "arty.rt.blocking_worker.pool_saturated")]
pub(crate) struct BlockingWorkerPoolSaturated {
#[dimension(log = "blocking_worker_pool.mode", metric = "blocking_worker_pool.mode")]
pub blocking_worker_pool_mode: BlockingWorkerPoolMode,
#[dimension(log = "blocking_worker_pool.max_threads", metric = "blocking_worker_pool.max_threads")]
pub max_threads: SystemMetricCount,
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))] mod tests {
use data_privacy::RedactionEngine;
use observed::{Severity, emit};
use observed_testing::{ExpectedEvent, TEST_ID, test_emitter};
use super::*;
#[test]
fn runtime_started_has_expected_metadata() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
RuntimeStarted {
processors_available: SystemMetricCount(8),
processors_used: SystemMetricCount(2),
blocking_worker_pool_mode: BlockingWorkerPoolMode("per_worker"),
stack_size_bytes: SystemMetricCount(1024),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.started", Severity::Info)
.body("runtime started")
.dimension("processors.available", "8")
.dimension("processors.used", "2")
.dimension("blocking_worker_pool.mode", "per_worker")
.dimension("stack_size_bytes", "1024")
.metric()
);
}
#[test]
fn runtime_start_failed_has_expected_metadata() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
RuntimeStartFailed {
blocking_worker_pool_mode: BlockingWorkerPoolMode("shared"),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.start_failed", Severity::Error)
.body("runtime failed to start")
.dimension("blocking_worker_pool.mode", "shared")
.metric()
);
}
#[test]
fn runtime_stopping_and_stopped_pair_up() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(sink, RuntimeStopping);
emit!(sink, RuntimeStopped);
let events = processor.events();
assert_eq!(
events[0],
ExpectedEvent::new("arty.rt.stopping", Severity::Debug).body("runtime stopping")
);
assert_eq!(
events[1],
ExpectedEvent::new("arty.rt.stopped", Severity::Info).body("runtime stopped")
);
}
#[test]
fn async_worker_events_carry_processor_index() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
AsyncWorkerStarted {
processor_index: ProcessorIndex(3)
}
);
emit!(
sink,
AsyncWorkerStopped {
processor_index: ProcessorIndex(3)
}
);
let events = processor.events();
assert_eq!(
events[0],
ExpectedEvent::new("arty.rt.async_worker.started", Severity::Debug)
.body("async worker started")
.dimension("processor.index", "3")
);
assert_eq!(
events[1],
ExpectedEvent::new("arty.rt.async_worker.stopped", Severity::Debug)
.body("async worker stopped")
.dimension("processor.index", "3")
);
}
#[test]
fn async_worker_active_is_metric_only() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(sink, AsyncWorkerActive { delta: 1 });
assert_eq!(
processor.single_event(),
ExpectedEvent::without_severity("arty.rt.async_worker.active")
.dimension("delta", 1i64)
.metric()
);
}
#[test]
fn task_spawned_carries_placement() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
TaskSpawned {
placement: PlacementLabel("any")
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::without_severity("arty.rt.task.spawned")
.dimension("placement", "any")
.metric()
);
}
#[test]
fn task_succeeded_and_panicked_are_distinct_events() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(sink, TaskSucceeded);
emit!(sink, TaskPanicked);
let events = processor.events();
assert_eq!(events[0], ExpectedEvent::without_severity("arty.rt.task.succeeded").metric());
assert_eq!(
events[1],
ExpectedEvent::new("arty.rt.task.panicked", Severity::Error)
.body("task panicked")
.metric()
);
}
#[test]
fn thread_spawn_carries_thread_and_owner_metadata() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
ThreadSpawn {
name: ThreadName::new("oxidizer-async-1"),
stack_size_bytes: Some(SystemMetricCount(2_097_152)),
owner_name: Some(ThreadName::new("main")),
owner_id: ThreadId("ThreadId(1)".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.thread.spawn", Severity::Debug)
.body("spawning thread")
.dimension("thread.name", "oxidizer-async-1")
.dimension("thread.stack_size_bytes", "2097152")
.dimension("thread.owner.name", "main")
.dimension("thread.owner.id", "ThreadId(1)")
);
}
#[test]
fn thread_spawn_fills_absent_optional_fields() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
ThreadSpawn {
name: ThreadName::new("oxidizer-async-1"),
stack_size_bytes: None,
owner_name: None,
owner_id: ThreadId("ThreadId(1)".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.thread.spawn", Severity::Debug)
.body("spawning thread")
.dimension("thread.name", "oxidizer-async-1")
.dimension("thread.stack_size_bytes", "n/a")
.dimension("thread.owner.name", "n/a")
.dimension("thread.owner.id", "ThreadId(1)")
);
}
#[test]
fn thread_started_carries_identity() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
ThreadStarted {
name: Some(ThreadName::new("oxidizer-async-1")),
id: ThreadId("ThreadId(7)".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.thread.started", Severity::Debug)
.body("thread started")
.dimension("thread.name", "oxidizer-async-1")
.dimension("arty.thread.id", "ThreadId(7)")
);
}
#[test]
fn thread_panicked_carries_panic_message() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
ThreadPanicked {
name: Some(ThreadName::new("oxidizer-async-1")),
id: ThreadId("ThreadId(7)".to_owned()),
message: PanicMessage("something broke".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.thread.panic", Severity::Error)
.body("thread panicked")
.dimension("thread.name", "oxidizer-async-1")
.dimension("arty.thread.id", "ThreadId(7)")
.dimension("panic.message", "something broke")
);
}
#[test]
fn panic_message_is_not_suppressed_with_system_metadata() {
let engine = RedactionEngine::builder().suppress_redaction(SYSTEM_METADATA.clone()).build();
let mut output = String::new();
engine
.redacted_display(&PanicMessage("secret panic".to_owned()), &mut output)
.unwrap();
assert!(!output.contains("secret panic"));
}
#[test]
fn thread_exiting_carries_identity() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
ThreadExiting {
name: Some(ThreadName::new("oxidizer-async-1")),
id: ThreadId("ThreadId(7)".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.thread.exiting", Severity::Debug)
.body("thread exiting")
.dimension("thread.name", "oxidizer-async-1")
.dimension("arty.thread.id", "ThreadId(7)")
);
}
#[test]
fn builtins_thread_mismatch_carries_backtrace() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
BuiltinsThreadMismatch {
name: ThreadName::new("oxidizer-async-1"),
id: ThreadId("ThreadId(7)".to_owned()),
backtrace: BacktraceText("<backtrace>".to_owned()),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.builtins.thread_mismatch", Severity::Warn)
.body("Builtins accessed from a different thread than it was created on")
.dimension("thread.name", "oxidizer-async-1")
.dimension("arty.thread.id", "ThreadId(7)")
.dimension("backtrace", "<backtrace>")
);
}
#[test]
fn blocking_worker_pool_saturated_logs_and_counts() {
let (sink, processor) = test_emitter(TEST_ID);
emit!(
sink,
BlockingWorkerPoolSaturated {
blocking_worker_pool_mode: BlockingWorkerPoolMode("per_worker"),
max_threads: SystemMetricCount(64),
}
);
assert_eq!(
processor.single_event(),
ExpectedEvent::new("arty.rt.blocking_worker.pool_saturated", Severity::Warn)
.body("blocking worker pool is saturated and cannot grow")
.dimension("blocking_worker_pool.mode", "per_worker")
.dimension("blocking_worker_pool.max_threads", "64")
.metric()
);
}
}