Skip to main content

hyperactor_telemetry/
lib.rs

1/*
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 * All rights reserved.
4 *
5 * This source code is licensed under the BSD-style license found in the
6 * LICENSE file in the root directory of this source tree.
7 */
8
9#![allow(internal_features)]
10#![cfg_attr(target_os = "macos", feature(thread_id_value))]
11#![feature(sync_unsafe_cell)]
12#![feature(mpmc_channel)]
13#![feature(formatting_options)]
14#![recursion_limit = "256"]
15
16// Environment variable for job name (used for environment detection)
17pub const MAST_HPC_JOB_NAME_ENV: &str = "MAST_HPC_JOB_NAME";
18
19// Log level constants
20const LOG_LEVEL_INFO: &str = "info";
21const LOG_LEVEL_DEBUG: &str = "debug";
22
23// Span field constants
24const SPAN_FIELD_RECORDING: &str = "recording";
25#[allow(dead_code)]
26const SPAN_FIELD_RECORDER: &str = "recorder";
27
28/// Well-known tracing field name for the log subject.
29/// Spans carrying this field identify the entity (actor, proc, etc.)
30/// that log events within the span pertain to.
31pub const SUBJECT_KEY: &str = "subject";
32
33// Environment value constants
34const ENV_VALUE_LOCAL: &str = "local";
35const ENV_VALUE_MAST_EMULATOR: &str = "mast_emulator";
36const ENV_VALUE_MAST: &str = "mast";
37const ENV_VALUE_TEST: &str = "test";
38#[allow(dead_code)]
39const ENV_VALUE_LOCAL_MAST_SIMULATOR: &str = "local_mast_simulator";
40
41/// A marker field used to indicate that a span should not be recorded as
42/// individual start/end span events; rather the span is purely used to
43/// provide context for child events.
44///
45/// Note that the mechanism for skipping span recording uses the precise
46/// name "skip_record", thus it must be used as a naked identifier:
47/// ```ignore
48/// use hyperactor_telemetry::skip_record;
49///
50/// tracing::span!(..., skip_record);
51/// ```
52#[allow(non_upper_case_globals)]
53// pub const skip_record: tracing::field::Empty = tracing::field::Empty;
54pub const skip_record: bool = true;
55
56mod config;
57pub mod in_memory_reader;
58#[cfg(all(fbcode_build, target_os = "linux"))]
59mod meta;
60mod metrics_table;
61mod otel;
62pub(crate) mod otlp;
63mod pool;
64mod rate_limit;
65pub mod recorder;
66pub mod sinks;
67mod spool;
68pub mod sqlite;
69pub mod task;
70pub mod trace;
71pub mod trace_dispatcher;
72mod unix_sink;
73
74// Re-export key types for external sink implementations
75use std::collections::hash_map::DefaultHasher;
76use std::hash::Hash;
77use std::hash::Hasher;
78use std::io::Write;
79use std::sync::Arc;
80use std::sync::LazyLock;
81use std::sync::Mutex;
82use std::sync::atomic::AtomicU64;
83use std::sync::atomic::Ordering;
84use std::sync::mpsc;
85use std::time::Instant;
86use std::time::SystemTime;
87
88pub use opentelemetry;
89pub use opentelemetry::Key;
90pub use opentelemetry::KeyValue;
91pub use opentelemetry::Value;
92pub use opentelemetry::global::meter;
93pub use trace_dispatcher::DispatcherControl;
94pub use trace_dispatcher::FieldValue;
95pub use trace_dispatcher::TraceEvent;
96pub use trace_dispatcher::TraceEventSink;
97use trace_dispatcher::TraceFields;
98pub use tracing;
99pub use tracing::Level;
100use tracing_appender::rolling::RollingFileAppender;
101pub use unix_sink::set_unix_socket_sink_path;
102pub use unix_sink::unix_socket_sink_dropped_frames;
103pub use unix_sink::unix_socket_sink_is_active;
104
105#[cfg(all(fbcode_build, target_os = "linux"))]
106use crate::config::ENABLE_OTEL_METRICS;
107#[cfg(all(fbcode_build, target_os = "linux"))]
108use crate::config::ENABLE_OTEL_TRACING;
109use crate::config::ENABLE_RECORDER_TRACING;
110use crate::config::ENABLE_SQLITE_TRACING;
111use crate::config::MONARCH_LOG_SUFFIX;
112use crate::recorder::Recorder;
113
114/// Hash any hashable value to a u64 using DefaultHasher.
115pub fn hash_to_u64(value: &impl Hash) -> u64 {
116    let mut hasher = DefaultHasher::new();
117    value.hash(&mut hasher);
118    hasher.finish()
119}
120
121pub trait TelemetryClock {
122    fn now(&self) -> tokio::time::Instant;
123    fn system_time_now(&self) -> std::time::SystemTime;
124}
125
126pub struct DefaultTelemetryClock {}
127
128impl TelemetryClock for DefaultTelemetryClock {
129    fn now(&self) -> tokio::time::Instant {
130        tokio::time::Instant::now()
131    }
132
133    fn system_time_now(&self) -> std::time::SystemTime {
134        std::time::SystemTime::now()
135    }
136}
137
138pub fn username() -> String {
139    let env = env::Env::current();
140    match env {
141        env::Env::Mast => {
142            std::env::var("MAST_JOB_OWNER_UNIXNAME").unwrap_or_else(|_| "mast_owner".to_string())
143        }
144        _ => whoami::username().unwrap_or_else(|_| "unknown".to_owned()),
145    }
146}
147
148// Given an environment, determine the log file path to write to.
149// If a suffix is provided, it will be prepended with "_" and then appended to file name
150pub fn log_file_path(
151    env: env::Env,
152    suffix: Option<&str>,
153) -> Result<(String, String), anyhow::Error> {
154    let suffix = suffix
155        .map(|s| {
156            if s.is_empty() {
157                String::new()
158            } else {
159                format!("_{}", s)
160            }
161        })
162        .unwrap_or_default();
163    match env {
164        env::Env::Local | env::Env::MastEmulator => {
165            let mut username = whoami::username().unwrap_or_else(|_| "unknown".to_owned());
166            if username.is_empty() {
167                username.push_str("monarch");
168            }
169            Ok((
170                format!("/tmp/{}", username),
171                format!("monarch_log{}", suffix),
172            ))
173        }
174        env::Env::Mast => Ok((
175            "/logs/".to_string(),
176            format!("dedicated_log_monarch{}", suffix),
177        )),
178        _ => Err(anyhow::anyhow!(
179            "file writer unsupported for environment {}",
180            env
181        )),
182    }
183}
184
185fn try_create_appender(
186    path: &str,
187    filename: &str,
188    create_dir: bool,
189) -> Result<RollingFileAppender, Box<dyn std::error::Error>> {
190    if create_dir {
191        std::fs::create_dir_all(path)?;
192    }
193    Ok(RollingFileAppender::builder()
194        .filename_prefix(filename)
195        .filename_suffix("log")
196        .build(path)?)
197}
198
199fn writer() -> Box<dyn Write + Send> {
200    match env::Env::current() {
201        env::Env::Test => Box::new(std::io::stderr()),
202        env::Env::Local | env::Env::MastEmulator | env::Env::Mast => {
203            let suffix = hyperactor_config::global::try_get_cloned(MONARCH_LOG_SUFFIX);
204            let (path, filename) = log_file_path(env::Env::current(), suffix.as_deref()).unwrap();
205            match try_create_appender(&path, &filename, true) {
206                Ok(file_appender) => Box::new(file_appender),
207                Err(e) => {
208                    eprintln!(
209                        "unable to create log file in {}: {}. Falling back to stderr",
210                        path, e
211                    );
212                    Box::new(std::io::stderr())
213                }
214            }
215        }
216    }
217}
218
219static TELEMETRY_CLOCK: LazyLock<Arc<Mutex<Box<dyn TelemetryClock + Send>>>> =
220    LazyLock::new(|| Arc::new(Mutex::new(Box::new(DefaultTelemetryClock {}))));
221
222/// Global sender into the active `TraceEventDispatcher` queue.
223///
224/// This is for `TraceEvent`s synthesized outside normal `tracing` subscriber callbacks,
225/// such as Python user spans. Once telemetry initializes and constructs the dispatcher,
226/// we stash its sender here so those synthetic events flow through the same sink fan-out
227/// path as native Rust tracing events.
228static SYNTHETIC_TRACE_EVENT_SENDER: Mutex<Option<mpsc::SyncSender<TraceEvent>>> = Mutex::new(None);
229
230/// Global control channel for sink registration.
231/// Created upfront so sinks can be registered at any time (before or after telemetry init).
232/// The receiver is taken once when the TraceEventDispatcher is created.
233static SINK_CONTROL_CHANNEL: LazyLock<(
234    mpsc::Sender<DispatcherControl>,
235    Mutex<Option<mpsc::Receiver<DispatcherControl>>>,
236)> = LazyLock::new(|| {
237    let (sender, receiver) = mpsc::channel();
238    (sender, Mutex::new(Some(receiver)))
239});
240
241const SYNTHETIC_USER_SPAN_ID_BASE: u64 = 1 << 63;
242static USER_SPAN_SEQ: AtomicU64 = AtomicU64::new(SYNTHETIC_USER_SPAN_ID_BASE);
243const SYNTHETIC_USER_SPAN_TRACK_NAME: &str = "python";
244
245/// Install the sender for the active dispatcher so synthesized events can join the
246/// same pipeline as events captured from native `tracing` callbacks.
247pub(crate) fn set_synthetic_trace_event_sender(sender: mpsc::SyncSender<TraceEvent>) {
248    *SYNTHETIC_TRACE_EVENT_SENDER
249        .lock()
250        .expect("SYNTHETIC_TRACE_EVENT_SENDER mutex should not be poisoned") = Some(sender);
251}
252
253/// Sends a synthesized trace event to the active dispatcher queue.
254/// Returns `true` if sent successfully.
255pub(crate) fn emit_trace_event(event: TraceEvent) -> bool {
256    match synthetic_trace_event_sender() {
257        Some(sender) => sender.try_send(event).is_ok(),
258        None => false,
259    }
260}
261
262/// Begins a user-defined span and returns its id. Returns 0 if the dispatcher is not initialized.
263pub fn start_user_span(
264    name: &'static str,
265    target: &'static str,
266    fields: impl IntoIterator<Item = (&'static str, FieldValue)>,
267) -> u64 {
268    if SYNTHETIC_TRACE_EVENT_SENDER
269        .lock()
270        .expect("SYNTHETIC_TRACE_EVENT_SENDER mutex should not be poisoned")
271        .is_none()
272    {
273        return 0;
274    }
275
276    let id = USER_SPAN_SEQ.fetch_add(1, Ordering::Relaxed);
277
278    let fields = fields.into_iter().collect::<TraceFields>();
279
280    let _ = emit_trace_event(TraceEvent::NewSpan {
281        id,
282        name,
283        target,
284        level: tracing::Level::INFO,
285        fields,
286        timestamp: SystemTime::now(),
287        parent_id: None,
288        thread_name: SYNTHETIC_USER_SPAN_TRACK_NAME,
289        file: None,
290        line: None,
291    });
292
293    let _ = emit_trace_event(TraceEvent::SpanEnter {
294        id,
295        timestamp: SystemTime::now(),
296        thread_name: SYNTHETIC_USER_SPAN_TRACK_NAME,
297    });
298
299    id
300}
301
302/// Ends a user-defined span previously started with [`start_user_span`].
303pub fn end_user_span(id: u64) {
304    if id == 0 {
305        return;
306    }
307
308    let _ = emit_trace_event(TraceEvent::SpanExit {
309        id,
310        timestamp: SystemTime::now(),
311        thread_name: SYNTHETIC_USER_SPAN_TRACK_NAME,
312    });
313
314    let _ = emit_trace_event(TraceEvent::SpanClose {
315        id,
316        timestamp: SystemTime::now(),
317    });
318}
319
320/// Event data for actor creation.
321#[derive(Debug, Clone)]
322pub struct ActorEvent {
323    /// Unique identifier for this actor, hashed from ActorId.
324    pub id: u64,
325    /// Timestamp when the actor was created
326    pub timestamp: SystemTime,
327    /// ID of the mesh this actor belongs to, matching `MeshEvent.id`.
328    pub mesh_id: u64,
329    /// Rank index into the mesh shape
330    pub rank: u64,
331    /// Full hierarchical name of this actor
332    pub full_name: String,
333    /// User-facing name for this actor
334    pub display_name: Option<String>,
335}
336
337/// Notify telemetry that an actor was created.
338pub fn notify_actor_created(event: ActorEvent) {
339    emit_entity_event(EntityEvent::Actor(event));
340}
341
342/// Event data for mesh creation.
343#[derive(Debug, Clone)]
344pub struct MeshEvent {
345    /// Unique identifier for this mesh (hashed)
346    pub id: u64,
347    /// Timestamp when the mesh was created
348    pub timestamp: SystemTime,
349    /// Mesh class (e.g., "Proc", "Host", "Python<SomeUserDefinedActor>")
350    pub class: String,
351    /// User-provided name for this mesh
352    pub given_name: String,
353    /// Full hierarchical name as it appears in supervision events
354    pub full_name: String,
355    /// Shape of the mesh, serialized from ndslice::Extent
356    pub shape_json: String,
357    /// Parent mesh ID (None for root meshes)
358    pub parent_mesh_id: Option<u64>,
359    /// Region over which the parent spawned this mesh, serialized from ndslice::Region
360    pub parent_view_json: Option<String>,
361}
362
363/// Notify telemetry that a mesh was created.
364pub fn notify_mesh_created(event: MeshEvent) {
365    emit_entity_event(EntityEvent::Mesh(event));
366}
367
368/// Event data for actor status changes.
369#[derive(Debug, Clone)]
370pub struct ActorStatusEvent {
371    /// Unique identifier for this event
372    pub id: u64,
373    /// Timestamp when the status change occurred
374    pub timestamp: SystemTime,
375    /// ID of the actor whose status changed
376    pub actor_id: u64,
377    /// New status value (e.g. "Created", "Idle", "Failed")
378    pub new_status: String,
379    /// Reason for the status change (e.g. error details for Failed)
380    pub reason: Option<String>,
381}
382
383/// Notify telemetry that an actor changed status.
384pub fn notify_actor_status_changed(event: ActorStatusEvent) {
385    emit_entity_event(EntityEvent::ActorStatus(event));
386}
387
388/// Event fired when a message is sent to an actor mesh.
389///
390/// Emitted from `cast_all_or_choose` in `actor_mesh.rs`, which is the common
391/// path for all Python send methods: `call`, `call_one`, `broadcast`, and `choose`.
392#[derive(Debug, Clone)]
393pub struct SentMessageEvent {
394    pub timestamp: SystemTime,
395    /// Hash of the sending actor's ActorId.
396    pub sender_actor_id: u64,
397    /// Hash of the target actor mesh's `(ProcMeshId, ActorMeshId)`.
398    pub actor_mesh_id: u64,
399    /// The view (slice) of the actor mesh that was targeted, serialized from
400    /// [`ndslice::Region`]. For full-mesh sends (call, broadcast) this covers
401    /// all dimensions; for sliced sends (call_one) collapsed dimensions are
402    /// absent; for choose this is a scalar (0-dim) Region.
403    pub view_json: String,
404    /// The shape of the view, serialized from [`ndslice::Shape`] (converted
405    /// from the view Region via `Region::into::<Shape>`).
406    pub shape_json: String,
407}
408
409/// Notify telemetry that a message was sent.
410pub fn notify_sent_message(event: SentMessageEvent) {
411    emit_entity_event(EntityEvent::SentMessage(event));
412}
413
414/// Event fired when a message is received (from receiver's perspective).
415#[derive(Debug, Clone)]
416pub struct MessageEvent {
417    pub timestamp: SystemTime,
418    /// Unique identifier for this received message.
419    pub id: u64,
420    /// Hash of sender's ActorId.
421    pub from_actor_id: u64,
422    /// Hash of receiver's ActorId.
423    pub to_actor_id: u64,
424    /// Endpoint name if this message targets a specific actor endpoint
425    pub endpoint: Option<String>,
426    /// Destination port index, scoped by `to_actor_id`.
427    pub port_index: Option<u64>,
428}
429
430/// Notify telemetry that a message was received.
431pub fn notify_message(event: MessageEvent) {
432    emit_entity_event(EntityEvent::Message(event));
433}
434
435/// Event fired when a received message changes status.
436#[derive(Debug, Clone)]
437pub struct MessageStatusEvent {
438    pub timestamp: SystemTime,
439    /// Unique identifier for this status event.
440    pub id: u64,
441    /// The message whose status changed (FK to MessageEvent.id).
442    pub message_id: u64,
443    /// New status: "queued", "active", "complete", or "failed".
444    pub status: String,
445}
446
447/// Notify telemetry that a message changed status.
448pub fn notify_message_status(event: MessageStatusEvent) {
449    emit_entity_event(EntityEvent::MessageStatus(event));
450}
451
452static ACTOR_STATUS_SEQ: AtomicU64 = AtomicU64::new(1);
453
454/// Generate a globally unique ActorStatusEvent ID.
455///
456/// Combines the actor's unique ID with a process-local sequence number,
457/// then hashes the pair to produce an ID that is unique across processes.
458pub fn generate_actor_status_event_id(actor_id: u64) -> u64 {
459    let seq = ACTOR_STATUS_SEQ.fetch_add(1, Ordering::Relaxed);
460    hash_to_u64(&(actor_id, seq))
461}
462
463static SEND_SEQ: AtomicU64 = AtomicU64::new(1);
464
465/// Generate a globally unique SentMessage ID.
466pub fn generate_sent_message_id(sender_actor_id: u64) -> u64 {
467    let seq = SEND_SEQ.fetch_add(1, Ordering::Relaxed);
468    hash_to_u64(&(sender_actor_id, seq))
469}
470
471static RECV_MSG_SEQ: AtomicU64 = AtomicU64::new(1);
472
473/// Generate a unique received-message ID (cross-process unique).
474///
475/// Hashes (to_actor_id, seq) following the same pattern as
476/// `generate_sent_message_id`.
477pub fn generate_message_id(to_actor_id: u64) -> u64 {
478    let seq = RECV_MSG_SEQ.fetch_add(1, Ordering::Relaxed);
479    hash_to_u64(&(to_actor_id, seq))
480}
481
482static STATUS_EVENT_SEQ: AtomicU64 = AtomicU64::new(1);
483
484/// Generate a unique message-status-event ID (cross-process unique).
485///
486/// Hashes (message_id, seq) following the same pattern as
487/// `generate_sent_message_id`.
488pub fn generate_status_event_id(message_id: u64) -> u64 {
489    let seq = STATUS_EVENT_SEQ.fetch_add(1, Ordering::Relaxed);
490    hash_to_u64(&(message_id, seq))
491}
492
493/// Unified event enum for all entity lifecycle events.
494///
495/// This enum wraps all entity events (actors, meshes, and future event types)
496/// into a single type. This enables a single sink to handle all entity events,
497/// simplifying the registration and notification infrastructure.
498#[derive(Debug, Clone)]
499pub enum EntityEvent {
500    /// An actor was created.
501    Actor(ActorEvent),
502    /// A mesh was created.
503    Mesh(MeshEvent),
504    /// An actor changed status.
505    ActorStatus(ActorStatusEvent),
506    /// A message was sent.
507    SentMessage(SentMessageEvent),
508    /// A message was received.
509    Message(MessageEvent),
510    /// A received message changed status.
511    MessageStatus(MessageStatusEvent),
512}
513
514/// Emit an entity event through the unified trace dispatcher queue.
515fn emit_entity_event(event: EntityEvent) {
516    if let Some(sender) = synthetic_trace_event_sender() {
517        let _ = sender.try_send(TraceEvent::Entity(event));
518    }
519}
520
521fn synthetic_trace_event_sender() -> Option<mpsc::SyncSender<TraceEvent>> {
522    SYNTHETIC_TRACE_EVENT_SENDER
523        .lock()
524        .expect("SYNTHETIC_TRACE_EVENT_SENDER mutex should not be poisoned")
525        .clone()
526}
527
528/// Register a sink to receive trace events.
529/// This can be called at any time - before or after telemetry initialization.
530/// The sink will receive all trace events on the background worker thread.
531///
532/// # Example
533/// ```ignore
534/// use hyperactor_telemetry::{register_sink, TraceEventSink, TraceEvent};
535///
536/// struct MySink;
537/// impl TraceEventSink for MySink {
538///     fn consume(&mut self, event: &TraceEvent) -> Result<(), anyhow::Error> { Ok(()) }
539///     fn flush(&mut self) -> Result<(), anyhow::Error> { Ok(()) }
540/// }
541///
542/// register_sink(Box::new(MySink));
543/// ```
544pub fn register_sink(sink: Box<dyn TraceEventSink>) {
545    let sender = &SINK_CONTROL_CHANNEL.0;
546    if let Err(e) = sender.send(DispatcherControl::AddSink(sink)) {
547        eprintln!("[telemetry] failed to register sink: {}", e);
548    }
549}
550
551/// Take the control receiver for use by the TraceEventDispatcher.
552/// This can only be called once; subsequent calls return None.
553pub(crate) fn take_sink_control_receiver() -> Option<mpsc::Receiver<DispatcherControl>> {
554    SINK_CONTROL_CHANNEL.1.lock().unwrap().take()
555}
556
557/// The recorder singleton that is configured as a layer in the the default tracing
558/// subscriber, as configured by `initialize_logging`.
559pub fn recorder() -> &'static Recorder {
560    static RECORDER: std::sync::OnceLock<Recorder> = std::sync::OnceLock::new();
561    RECORDER.get_or_init(Recorder::new)
562}
563
564/// Hotswap the telemetry clock at runtime. This allows changing the clock implementation
565/// after initialization, which is useful for testing or switching between real and simulated time.
566pub fn swap_telemetry_clock(clock: impl TelemetryClock + Send + 'static) {
567    *TELEMETRY_CLOCK.lock().unwrap() = Box::new(clock);
568}
569
570/// Create key value pairs for use in opentelemetry. These pairs can be stored and used multiple
571/// times. Opentelemetry adds key value attributes when you bump counters and histograms.
572/// so MY_COUNTER.add(42, &[key_value!("key", "value")])  and MY_COUNTER.add(42, &[key_value!("key", "other_value")]) will actually bump two separete counters.
573#[macro_export]
574macro_rules! key_value {
575    ($key:expr, $val:expr) => {
576        $crate::opentelemetry::KeyValue::new(
577            $crate::opentelemetry::Key::new($key),
578            $crate::opentelemetry::Value::from($val),
579        )
580    };
581}
582/// Construct the key value attribute slice using mapping syntax.
583/// Example:
584/// ```
585/// # #[macro_use] extern crate hyperactor_telemetry;
586/// # fn main() {
587/// assert_eq!(
588///     kv_pairs!("1" => "1", "2" => 2, "3" => 3.0),
589///     &[
590///         key_value!("1", "1"),
591///         key_value!("2", 2),
592///         key_value!("3", 3.0),
593///     ],
594/// );
595/// # }
596/// ```
597#[macro_export]
598macro_rules! kv_pairs {
599    ($($k:expr => $v:expr),* $(,)?) => {
600        &[$($crate::key_value!($k, $v),)*]
601    };
602}
603
604#[derive(Debug, Clone, Copy)]
605pub enum TimeUnit {
606    Millis,
607    Micros,
608    Nanos,
609}
610
611impl TimeUnit {
612    pub fn as_str(&self) -> &'static str {
613        match self {
614            TimeUnit::Millis => "ms",
615            TimeUnit::Micros => "us",
616            TimeUnit::Nanos => "ns",
617        }
618    }
619}
620pub struct Timer(opentelemetry::metrics::Histogram<u64>, TimeUnit);
621
622impl<'a> Timer {
623    pub fn new(data: opentelemetry::metrics::Histogram<u64>, unit: TimeUnit) -> Self {
624        Timer(data, unit)
625    }
626    pub fn start(&'static self, pairs: &'a [opentelemetry::KeyValue]) -> TimerGuard<'a> {
627        TimerGuard {
628            data: self,
629            pairs,
630            start: Instant::now(),
631        }
632    }
633
634    pub fn record(&'static self, dur: std::time::Duration, pairs: &'a [opentelemetry::KeyValue]) {
635        let dur = match self.1 {
636            TimeUnit::Millis => dur.as_millis(),
637            TimeUnit::Micros => dur.as_micros(),
638            TimeUnit::Nanos => dur.as_nanos(),
639        } as u64;
640
641        self.0.record(dur, pairs);
642    }
643}
644pub struct TimerGuard<'a> {
645    data: &'static Timer,
646    pairs: &'a [opentelemetry::KeyValue],
647    start: Instant,
648}
649
650impl Drop for TimerGuard<'_> {
651    fn drop(&mut self) {
652        let now = Instant::now();
653        let dur = now.duration_since(self.start);
654        self.data.record(dur, self.pairs);
655    }
656}
657
658/// Create a thread safe static timer that can be used to measure durations.
659/// This macro creates a histogram with predefined boundaries appropriate for the specified time unit.
660/// Supported units are "ms" (milliseconds), "us" (microseconds), and "ns" (nanoseconds).
661///
662/// Example:
663/// ```
664/// # #[macro_use] extern crate hyperactor_telemetry;
665/// # fn main() {
666/// declare_static_timer!(REQUEST_TIMER, "request_processing_time", hyperactor_telemetry::TimeUnit::Millis);
667///
668/// {
669///     let pairs = kv_pairs!("endpoint" => "/api/users", "method" => "GET");
670///     let _timer = REQUEST_TIMER.start(pairs);
671///     // do something expensive
672/// }
673/// # }
674/// ```
675#[macro_export]
676macro_rules! declare_static_timer {
677    ($name:ident, $key:expr, $unit:path) => {
678        #[doc = "a global histogram timer named: "]
679        #[doc = $key]
680        pub static $name: std::sync::LazyLock<$crate::Timer> = std::sync::LazyLock::new(|| {
681            $crate::Timer::new(
682                $crate::meter(module_path!())
683                    .u64_histogram(format!("{}.{}", $key, $unit.as_str()))
684                    .with_unit($unit.as_str())
685                    .build(),
686                $unit,
687            )
688        });
689    };
690}
691
692/// Create a thread safe static counter that can be incremeneted or decremented.
693/// This is useful to avoid creating temporary counters.
694/// You can safely create counters with the same name. They will be joined by the underlying
695/// runtime and are thread safe.
696///
697/// Example:
698/// ```
699/// struct Url {
700///     pub path: String,
701///     pub proto: String,
702/// }
703///
704/// # #[macro_use] extern crate hyperactor_telemetry;
705/// # fn main() {
706/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
707/// declare_static_counter!(REQUESTS_RECEIVED, "requests_received");
708///
709/// REQUESTS_RECEIVED.add(40, kv_pairs!("path" => url.path, "proto" => url.proto))
710///
711/// # }
712/// ```
713#[macro_export]
714macro_rules! declare_static_counter {
715    ($name:ident, $key:expr) => {
716        #[doc = "a global counter named: "]
717        #[doc = $key]
718        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Counter<u64>> =
719            std::sync::LazyLock::new(|| $crate::meter(module_path!()).u64_counter($key).build());
720    };
721}
722
723/// Create a thread safe static counter that can be incremeneted or decremented.
724/// This is useful to avoid creating temporary counters.
725/// You can safely create counters with the same name. They will be joined by the underlying
726/// runtime and are thread safe.
727///
728/// Example:
729/// ```
730/// struct Url {
731///     pub path: String,
732///     pub proto: String,
733/// }
734///
735/// # #[macro_use] extern crate hyperactor_telemetry;
736/// # fn main() {
737/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
738/// declare_static_counter!(REQUESTS_RECEIVED, "requests_received");
739///
740/// REQUESTS_RECEIVED.add(40, kv_pairs!("path" => url.path, "proto" => url.proto))
741///
742/// # }
743/// ```
744#[macro_export]
745macro_rules! declare_static_up_down_counter {
746    ($name:ident, $key:expr) => {
747        #[doc = "a global up down counter named: "]
748        #[doc = $key]
749        pub static $name: std::sync::LazyLock<opentelemetry::metrics::UpDownCounter<i64>> =
750            std::sync::LazyLock::new(|| {
751                $crate::meter(module_path!())
752                    .i64_up_down_counter($key)
753                    .build()
754            });
755    };
756}
757
758/// Create a thread safe static gauge that can be set to a specific value.
759/// This is useful to avoid creating temporary gauges.
760/// You can safely create gauges with the same name. They will be joined by the underlying
761/// runtime and are thread safe.
762///
763/// Example:
764/// ```
765/// struct System {
766///     pub memory_usage: f64,
767///     pub cpu_usage: f64,
768/// }
769///
770/// # #[macro_use] extern crate hyperactor_telemetry;
771/// # fn main() {
772/// # let system = System{memory_usage: 512.5, cpu_usage: 25.0};
773/// declare_static_gauge!(MEMORY_USAGE, "memory_usage");
774///
775/// MEMORY_USAGE.record(system.memory_usage, kv_pairs!("unit" => "MB", "process" => "hyperactor"))
776///
777/// # }
778/// ```
779#[macro_export]
780macro_rules! declare_static_gauge {
781    ($name:ident, $key:expr) => {
782        #[doc = "a global gauge named: "]
783        #[doc = $key]
784        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Gauge<f64>> =
785            std::sync::LazyLock::new(|| $crate::meter(module_path!()).f64_gauge($key).build());
786    };
787}
788/// Create a thread safe static observable gauge that can be set to a specific value based on the provided callback.
789/// This is useful for metrics that need to be calculated or retrieved dynamically.
790/// The callback will be executed whenever the gauge is observed by the metrics system.
791///
792/// Example:
793/// ```
794/// # #[macro_use] extern crate hyperactor_telemetry;
795///
796/// # fn main() {
797/// declare_observable_gauge!(MEMORY_USAGE_GAUGE, "memory_usage", |observer| {
798///     // Simulate getting memory usage - this could be any complex operation
799///     observer.observe(512.0, &[]);
800/// });
801///
802/// // The gauge will be automatically updated when observed
803/// # }
804/// ```
805#[macro_export]
806macro_rules! declare_observable_gauge {
807    ($name:ident, $key:expr, $cb:expr) => {
808        #[doc = "a global gauge named: "]
809        #[doc = $key]
810        pub static $name: std::sync::LazyLock<opentelemetry::metrics::ObservableGauge<f64>> =
811            std::sync::LazyLock::new(|| {
812                $crate::meter(module_path!())
813                    .f64_observable_gauge($key)
814                    .with_callback($cb)
815                    .build()
816            });
817    };
818}
819/// Create a thread safe static histogram that can be incremeneted or decremented.
820/// This is useful to avoid creating temporary histograms.
821/// You can safely create histograms with the same name. They will be joined by the underlying
822/// runtime and are thread safe.
823///
824/// Example:
825/// ```
826/// struct Url {
827///     pub path: String,
828///     pub proto: String,
829/// }
830///
831/// # #[macro_use] extern crate hyperactor_telemetry;
832/// # fn main() {
833/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
834/// declare_static_histogram!(REQUEST_LATENCY, "request_latency");
835///
836/// REQUEST_LATENCY.record(40.0, kv_pairs!("path" => url.path, "proto" => url.proto))
837///
838/// # }
839/// ```
840#[macro_export]
841macro_rules! declare_static_histogram {
842    ($name:ident, $key:expr) => {
843        #[doc = "a global histogram named: "]
844        #[doc = $key]
845        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Histogram<f64>> =
846            std::sync::LazyLock::new(|| {
847                hyperactor_telemetry::meter(module_path!())
848                    .f64_histogram($key)
849                    .build()
850            });
851    };
852}
853
854/// Set up logging based on the given execution environment. We specialize logging based on how the
855/// logs are consumed. The destination scuba table is specialized based on the execution environment.
856/// mast -> monarch_tracing/prod
857/// devserver -> monarch_tracing/local
858/// unit test  -> monarch_tracing/test
859/// scuba logging won't normally be enabled for a unit test unless we are specifically testing logging, so
860/// you don't need to worry about your tests being flakey due to scuba logging. You have to manually call initialize_logging()
861/// to get this behavior.
862pub fn initialize_logging(clock: impl TelemetryClock + Send + 'static) {
863    initialize_logging_with_log_prefix(clock, None);
864}
865
866/// testing
867pub fn initialize_logging_for_test() {
868    initialize_logging(DefaultTelemetryClock {});
869}
870
871/// Set up logging based on the given execution environment. We specialize logging based on how the
872/// logs are consumed. The destination scuba table is specialized based on the execution environment.
873/// mast -> monarch_tracing/prod
874/// devserver -> monarch_tracing/local
875/// unit test  -> monarch_tracing/test
876/// scuba logging won't normally be enabled for a unit test unless we are specifically testing logging, so
877/// you don't need to worry about your tests being flakey due to scuba logging. You have to manually call initialize_logging()
878/// to get this behavior.
879///
880/// tracing logs will be prefixed with the given prefix and routed to:
881/// test -> stderr
882/// local -> /tmp/monarch_log.log
883/// mast -> /logs/dedicated_monarch_logs.log
884/// Additionally, is MONARCH_STDERR_LOG sets logs level, then logs will be routed to stderr as well.
885pub fn initialize_logging_with_log_prefix(
886    clock: impl TelemetryClock + Send + 'static,
887    prefix_env_var: Option<String>,
888) {
889    let should_install_subscriber = !tracing::dispatcher::has_been_set();
890
891    swap_telemetry_clock(clock);
892    if !should_install_subscriber {
893        tracing::debug!("logging already initialized for this process");
894    }
895    let file_log_level = match env::Env::current() {
896        env::Env::Local => LOG_LEVEL_INFO,
897        env::Env::MastEmulator => LOG_LEVEL_INFO,
898        env::Env::Mast => LOG_LEVEL_INFO,
899        env::Env::Test => LOG_LEVEL_DEBUG,
900    };
901
902    use tracing_subscriber::Registry;
903    use tracing_subscriber::layer::SubscriberExt;
904    use tracing_subscriber::util::SubscriberInitExt;
905
906    #[cfg(all(fbcode_build, target_os = "linux"))]
907    {
908        if should_install_subscriber {
909            let mut sinks: Vec<Box<dyn trace_dispatcher::TraceEventSink>> = Vec::new();
910            sinks.push(Box::new(sinks::glog::GlogSink::new(
911                writer(),
912                prefix_env_var.clone(),
913                file_log_level,
914            )));
915
916            let sqlite_enabled = hyperactor_config::global::get(ENABLE_SQLITE_TRACING);
917
918            if sqlite_enabled {
919                match create_sqlite_sink() {
920                    Ok(sink) => {
921                        sinks.push(Box::new(sink));
922                    }
923                    Err(e) => {
924                        tracing::warn!("failed to create SqliteSink: {}", e);
925                    }
926                }
927            }
928
929            if hyperactor_config::global::get(sinks::perfetto::PERFETTO_TRACE_MODE)
930                != sinks::perfetto::PerfettoTraceMode::Off
931            {
932                let exec_id = env::execution_id();
933                let process_name = std::env::var("HYPERACTOR_PROCESS_NAME")
934                    .unwrap_or_else(|_| "client".to_string());
935                match sinks::perfetto::PerfettoFileSink::new(
936                    sinks::perfetto::default_trace_dir(),
937                    &exec_id,
938                    &process_name,
939                ) {
940                    Ok(sink) => {
941                        sinks.push(Box::new(sink));
942                    }
943                    Err(e) => {
944                        tracing::warn!("failed to create PerfettoFileSink: {}", e);
945                    }
946                }
947            }
948
949            if hyperactor_config::global::get(ENABLE_OTEL_TRACING) {
950                use crate::meta;
951
952                sinks.push(Box::new(
953                    meta::scuba_sink::ScubaSink::new(meta::tracing_resource())
954                        .with_target_filter(crate::config::get_tracing_targets()),
955                ));
956            }
957
958            sinks.push(unix_sink::install_unix_socket_sink_inactive());
959
960            let dispatcher = trace_dispatcher::TraceEventDispatcher::new(sinks);
961            let synthetic_sender = dispatcher.sender();
962
963            if let Err(err) = Registry::default()
964                .with(if hyperactor_config::global::get(ENABLE_RECORDER_TRACING) {
965                    Some(recorder().layer())
966                } else {
967                    None
968                })
969                .with(dispatcher)
970                .try_init()
971            {
972                tracing::debug!("logging already initialized for this process: {}", err);
973            } else {
974                set_synthetic_trace_event_sender(synthetic_sender);
975            }
976        }
977        let exec_id = env::execution_id();
978        let process_name =
979            std::env::var("HYPERACTOR_PROCESS_NAME").unwrap_or_else(|_| "client".to_string());
980
981        // setting target to "execution" will prevent the monarch_tracing scuba client from logging this
982        tracing::info!(
983            target: "execution",
984            execution_id = exec_id,
985            environment = %env::Env::current(),
986            args = ?std::env::args(),
987            build_mode = build_info::BuildInfo::get_build_mode(),
988            compiler = build_info::BuildInfo::get_compiler(),
989            compiler_version = build_info::BuildInfo::get_compiler_version(),
990            buck_rule = build_info::BuildInfo::get_rule(),
991            package_name = build_info::BuildInfo::get_package_name(),
992            package_release = build_info::BuildInfo::get_package_release(),
993            upstream_revision = build_info::BuildInfo::get_upstream_revision(),
994            revision = build_info::BuildInfo::get_revision(),
995            process_name = process_name,
996            "logging_initialized",
997        );
998        // here we have the monarch_executions scuba client log
999        meta::log_execution_event(
1000            &exec_id,
1001            &env::Env::current().to_string(),
1002            std::env::args().collect(),
1003            build_info::BuildInfo::get_build_mode(),
1004            build_info::BuildInfo::get_compiler(),
1005            build_info::BuildInfo::get_compiler_version(),
1006            build_info::BuildInfo::get_rule(),
1007            build_info::BuildInfo::get_package_name(),
1008            build_info::BuildInfo::get_package_release(),
1009            build_info::BuildInfo::get_upstream_revision(),
1010            build_info::BuildInfo::get_revision(),
1011            &process_name,
1012        );
1013
1014        if hyperactor_config::global::get(ENABLE_OTEL_METRICS) {
1015            otel::init_metrics();
1016        }
1017    }
1018    #[cfg(not(all(fbcode_build, target_os = "linux")))]
1019    {
1020        let registry =
1021            Registry::default().with(if hyperactor_config::global::get(ENABLE_RECORDER_TRACING) {
1022                Some(recorder().layer())
1023            } else {
1024                None
1025            });
1026
1027        if should_install_subscriber {
1028            let mut sinks: Vec<Box<dyn trace_dispatcher::TraceEventSink>> = Vec::new();
1029
1030            let sqlite_enabled = hyperactor_config::global::get(ENABLE_SQLITE_TRACING);
1031
1032            if sqlite_enabled {
1033                match create_sqlite_sink() {
1034                    Ok(sink) => {
1035                        sinks.push(Box::new(sink));
1036                    }
1037                    Err(e) => {
1038                        tracing::warn!("failed to create SqliteSink: {}", e);
1039                    }
1040                }
1041            }
1042
1043            sinks.push(Box::new(sinks::glog::GlogSink::new(
1044                writer(),
1045                prefix_env_var.clone(),
1046                file_log_level,
1047            )));
1048
1049            if let Some(log_sink) = otlp::otlp_log_sink() {
1050                sinks.push(log_sink);
1051            }
1052
1053            sinks.push(unix_sink::install_unix_socket_sink_inactive());
1054
1055            let dispatcher = trace_dispatcher::TraceEventDispatcher::new(sinks);
1056            let synthetic_sender = dispatcher.sender();
1057
1058            if let Err(err) = registry.with(dispatcher).try_init() {
1059                tracing::debug!("logging already initialized for this process: {}", err);
1060            } else {
1061                set_synthetic_trace_event_sender(synthetic_sender);
1062            }
1063        }
1064
1065        otel::init_metrics();
1066    }
1067}
1068
1069fn create_sqlite_sink() -> anyhow::Result<sinks::sqlite::SqliteSink> {
1070    let (db_path, _) = log_file_path(env::Env::current(), Some("traces"))
1071        .expect("failed to determine trace db path");
1072    let db_file = format!("{}/hyperactor_trace_{}.db", db_path, std::process::id());
1073
1074    sinks::sqlite::SqliteSink::new_with_file(&db_file, 100)
1075}
1076
1077/// Create a context span at ERROR level with skip_record enabled.
1078/// This is intended to create spans whose only purpose it is to add context
1079/// to child events; the span itself is never independently recorded.
1080///
1081/// Example:
1082/// ```ignore
1083/// use hyperactor_telemetry::context_span;
1084///
1085/// let span = context_span!("my_context", field1 = value1, field2 = value2);
1086/// let _guard = span.enter();
1087/// // ... do work that will be logged with this context
1088/// ```
1089#[macro_export]
1090macro_rules! context_span {
1091    (target: $target:expr, parent: $parent:expr, $name:expr, $($field:tt)*) => {
1092        ::tracing::error_span!(
1093            target: $target,
1094            parent: $parent,
1095            $name,
1096            skip_record = $crate::skip_record,
1097            $($field)*
1098        )
1099    };
1100    (target: $target:expr, parent: $parent:expr, $name:expr) => {
1101        ::tracing::error_span!(
1102            target: $target,
1103            parent: $parent,
1104            $name,
1105            skip_record = $crate::skip_record,
1106        )
1107    };
1108    (parent: $parent:expr, $name:expr, $($field:tt)*) => {
1109        ::tracing::error_span!(
1110            target: module_path!(),
1111            parent: $parent,
1112            $name,
1113            skip_record = $crate::skip_record,
1114            $($field)*
1115        )
1116    };
1117    (parent: $parent:expr, $name:expr) => {
1118        ::tracing::error_span!(
1119            parent: $parent,
1120            $name,
1121            skip_record = $crate::skip_record,
1122        )
1123    };
1124    (target: $target:expr, $name:expr, $($field:tt)*) => {
1125        ::tracing::error_span!(
1126            target: $target,
1127            $name,
1128            skip_record = $crate::skip_record,
1129            $($field)*
1130        )
1131    };
1132    (target: $target:expr, $name:expr) => {
1133        ::tracing::error_span!(
1134            target: $target,
1135            $name,
1136            skip_record = $crate::skip_record,
1137        )
1138    };
1139    ($name:expr, $($field:tt)*) => {
1140        ::tracing::error_span!(
1141            target: module_path!(),
1142            $name,
1143            skip_record = $crate::skip_record,
1144            $($field)*
1145        )
1146    };
1147    ($name:expr) => {
1148        ::tracing::error_span!(
1149            $name,
1150            skip_record = $crate::skip_record,
1151        )
1152    };
1153}
1154
1155pub mod env {
1156    /// Env var name set when monarch launches subprocesses to forward the execution context
1157    pub const HYPERACTOR_EXECUTION_ID_ENV: &str = "HYPERACTOR_EXECUTION_ID";
1158    pub const OTEL_EXPORTER: &str = "HYPERACTOR_OTEL_EXPORTER";
1159    pub const MAST_ENVIRONMENT: &str = "MAST_ENVIRONMENT";
1160
1161    /// Forward or generate a uuid for this execution. When running in production on mast, this is provided to
1162    /// us via the MAST_HPC_JOB_NAME env var. Subprocesses should either forward the MAST_HPC_JOB_NAME
1163    /// variable, or set the "MONARCH_EXECUTION_ID" var for subprocesses launched by this process.
1164    /// We keep these env vars separate so that other applications that depend on the MAST_HPC_JOB_NAME existing
1165    /// to understand their environment do not get confused and think they are running on mast when we are doing
1166    ///  local testing.
1167    pub fn execution_id() -> String {
1168        let id = std::env::var(HYPERACTOR_EXECUTION_ID_ENV).unwrap_or_else(|_| {
1169            // not able to find an existing id so generate a unique one: username + current_time + random number.
1170            let username = crate::username();
1171            let now = {
1172                let now = std::time::SystemTime::now();
1173                let datetime: chrono::DateTime<chrono::Local> = now.into();
1174                datetime.format("%b-%d_%H:%M").to_string()
1175            };
1176            let random_number: u16 = (rand::random::<u32>() % 1000) as u16;
1177            let execution_id = format!("{}_{}_{}", username, now, random_number);
1178            execution_id
1179        });
1180        // Safety: Can be unsound if there are multiple threads
1181        // reading and writing the environment.
1182        unsafe {
1183            std::env::set_var(HYPERACTOR_EXECUTION_ID_ENV, id.clone());
1184        }
1185        id
1186    }
1187
1188    #[derive(PartialEq)]
1189    pub enum Env {
1190        Local,
1191        Mast,
1192        MastEmulator,
1193        Test,
1194    }
1195
1196    impl std::fmt::Display for Env {
1197        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1198            write!(
1199                f,
1200                "{}",
1201                match self {
1202                    Self::Local => crate::ENV_VALUE_LOCAL,
1203                    Self::MastEmulator => crate::ENV_VALUE_MAST_EMULATOR,
1204                    Self::Mast => crate::ENV_VALUE_MAST,
1205                    Self::Test => crate::ENV_VALUE_TEST,
1206                }
1207            )
1208        }
1209    }
1210
1211    impl Env {
1212        #[cfg(test)]
1213        pub fn current() -> Self {
1214            Self::Test
1215        }
1216
1217        #[cfg(not(test))]
1218        pub fn current() -> Self {
1219            match std::env::var(MAST_ENVIRONMENT).unwrap_or_default().as_str() {
1220                // Constant from https://fburl.com/fhysd3fd
1221                crate::ENV_VALUE_LOCAL_MAST_SIMULATOR => Self::MastEmulator,
1222                _ => match std::env::var(crate::MAST_HPC_JOB_NAME_ENV).is_ok() {
1223                    true => Self::Mast,
1224                    false => Self::Local,
1225                },
1226            }
1227        }
1228    }
1229}
1230
1231#[cfg(test)]
1232mod test {
1233    use opentelemetry::*;
1234    extern crate self as hyperactor_telemetry;
1235    use super::*;
1236
1237    #[test]
1238    fn infer_kv_pair_types() {
1239        assert_eq!(
1240            key_value!("str", "str"),
1241            KeyValue::new(Key::new("str"), Value::String("str".into()))
1242        );
1243        assert_eq!(
1244            key_value!("str", 25),
1245            KeyValue::new(Key::new("str"), Value::I64(25))
1246        );
1247        assert_eq!(
1248            key_value!("str", 1.1),
1249            KeyValue::new(Key::new("str"), Value::F64(1.1))
1250        );
1251    }
1252    #[test]
1253    fn kv_pair_slices() {
1254        assert_eq!(
1255            kv_pairs!("1" => "1", "2" => 2, "3" => 3.0),
1256            &[
1257                key_value!("1", "1"),
1258                key_value!("2", 2),
1259                key_value!("3", 3.0),
1260            ],
1261        );
1262    }
1263
1264    #[test]
1265    fn test_static_gauge() {
1266        // Create a static gauge using the macro
1267        declare_static_gauge!(TEST_GAUGE, "test_gauge");
1268        declare_static_gauge!(MEMORY_GAUGE, "memory_usage");
1269
1270        // Set values to the gauge with different attributes
1271        // This shouldn't actually log to scribe/scuba in test environment
1272        TEST_GAUGE.record(42.5, kv_pairs!("component" => "test", "unit" => "MB"));
1273        MEMORY_GAUGE.record(512.0, kv_pairs!("type" => "heap", "process" => "test"));
1274
1275        // Test with empty attributes
1276        TEST_GAUGE.record(50.0, &[]);
1277    }
1278}