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 otel;
61pub(crate) mod otlp;
62mod pool;
63mod rate_limit;
64pub mod recorder;
65pub mod sinks;
66mod spool;
67pub mod sqlite;
68pub mod task;
69pub mod trace;
70pub mod trace_dispatcher;
71mod unix_sink;
72
73// Re-export key types for external sink implementations
74use std::collections::hash_map::DefaultHasher;
75use std::hash::Hash;
76use std::hash::Hasher;
77use std::io::Write;
78use std::sync::Arc;
79use std::sync::LazyLock;
80use std::sync::Mutex;
81use std::sync::atomic::AtomicU64;
82use std::sync::atomic::Ordering;
83use std::sync::mpsc;
84use std::time::Instant;
85use std::time::SystemTime;
86
87pub use opentelemetry;
88pub use opentelemetry::Key;
89pub use opentelemetry::KeyValue;
90pub use opentelemetry::Value;
91pub use opentelemetry::global::meter;
92pub use trace_dispatcher::DispatcherControl;
93pub use trace_dispatcher::FieldValue;
94pub use trace_dispatcher::TraceEvent;
95pub use trace_dispatcher::TraceEventSink;
96use trace_dispatcher::TraceFields;
97pub use tracing;
98pub use tracing::Level;
99use tracing_appender::rolling::RollingFileAppender;
100pub use unix_sink::set_unix_socket_sink_path;
101pub use unix_sink::unix_socket_sink_dropped_frames;
102pub use unix_sink::unix_socket_sink_is_active;
103
104#[cfg(all(fbcode_build, target_os = "linux"))]
105use crate::config::ENABLE_OTEL_METRICS;
106#[cfg(all(fbcode_build, target_os = "linux"))]
107use crate::config::ENABLE_OTEL_TRACING;
108use crate::config::ENABLE_RECORDER_TRACING;
109use crate::config::ENABLE_SQLITE_TRACING;
110use crate::config::MONARCH_LOG_SUFFIX;
111use crate::recorder::Recorder;
112
113/// Hash any hashable value to a u64 using DefaultHasher.
114pub fn hash_to_u64(value: &impl Hash) -> u64 {
115    let mut hasher = DefaultHasher::new();
116    value.hash(&mut hasher);
117    hasher.finish()
118}
119
120pub trait TelemetryClock {
121    fn now(&self) -> tokio::time::Instant;
122    fn system_time_now(&self) -> std::time::SystemTime;
123}
124
125pub struct DefaultTelemetryClock {}
126
127impl TelemetryClock for DefaultTelemetryClock {
128    fn now(&self) -> tokio::time::Instant {
129        tokio::time::Instant::now()
130    }
131
132    fn system_time_now(&self) -> std::time::SystemTime {
133        std::time::SystemTime::now()
134    }
135}
136
137pub fn username() -> String {
138    let env = env::Env::current();
139    match env {
140        env::Env::Mast => {
141            std::env::var("MAST_JOB_OWNER_UNIXNAME").unwrap_or_else(|_| "mast_owner".to_string())
142        }
143        _ => whoami::username(),
144    }
145}
146
147// Given an environment, determine the log file path to write to.
148// If a suffix is provided, it will be prepended with "_" and then appended to file name
149pub fn log_file_path(
150    env: env::Env,
151    suffix: Option<&str>,
152) -> Result<(String, String), anyhow::Error> {
153    let suffix = suffix
154        .map(|s| {
155            if s.is_empty() {
156                String::new()
157            } else {
158                format!("_{}", s)
159            }
160        })
161        .unwrap_or_default();
162    match env {
163        env::Env::Local | env::Env::MastEmulator => {
164            let username = if whoami::username().is_empty() {
165                "monarch".to_string()
166            } else {
167                whoami::username()
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", or "complete".
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 _ = REQUEST_TIMER.start(kv_pairs!("endpoint" => "/api/users", "method" => "GET"));
670///     // do something expensive
671/// }
672/// # }
673/// ```
674#[macro_export]
675macro_rules! declare_static_timer {
676    ($name:ident, $key:expr, $unit:path) => {
677        #[doc = "a global histogram timer named: "]
678        #[doc = $key]
679        pub static $name: std::sync::LazyLock<$crate::Timer> = std::sync::LazyLock::new(|| {
680            $crate::Timer::new(
681                $crate::meter(module_path!())
682                    .u64_histogram(format!("{}.{}", $key, $unit.as_str()))
683                    .with_unit($unit.as_str())
684                    .build(),
685                $unit,
686            )
687        });
688    };
689}
690
691/// Create a thread safe static counter that can be incremeneted or decremented.
692/// This is useful to avoid creating temporary counters.
693/// You can safely create counters with the same name. They will be joined by the underlying
694/// runtime and are thread safe.
695///
696/// Example:
697/// ```
698/// struct Url {
699///     pub path: String,
700///     pub proto: String,
701/// }
702///
703/// # #[macro_use] extern crate hyperactor_telemetry;
704/// # fn main() {
705/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
706/// declare_static_counter!(REQUESTS_RECEIVED, "requests_received");
707///
708/// REQUESTS_RECEIVED.add(40, kv_pairs!("path" => url.path, "proto" => url.proto))
709///
710/// # }
711/// ```
712#[macro_export]
713macro_rules! declare_static_counter {
714    ($name:ident, $key:expr) => {
715        #[doc = "a global counter named: "]
716        #[doc = $key]
717        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Counter<u64>> =
718            std::sync::LazyLock::new(|| $crate::meter(module_path!()).u64_counter($key).build());
719    };
720}
721
722/// Create a thread safe static counter that can be incremeneted or decremented.
723/// This is useful to avoid creating temporary counters.
724/// You can safely create counters with the same name. They will be joined by the underlying
725/// runtime and are thread safe.
726///
727/// Example:
728/// ```
729/// struct Url {
730///     pub path: String,
731///     pub proto: String,
732/// }
733///
734/// # #[macro_use] extern crate hyperactor_telemetry;
735/// # fn main() {
736/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
737/// declare_static_counter!(REQUESTS_RECEIVED, "requests_received");
738///
739/// REQUESTS_RECEIVED.add(40, kv_pairs!("path" => url.path, "proto" => url.proto))
740///
741/// # }
742/// ```
743#[macro_export]
744macro_rules! declare_static_up_down_counter {
745    ($name:ident, $key:expr) => {
746        #[doc = "a global up down counter named: "]
747        #[doc = $key]
748        pub static $name: std::sync::LazyLock<opentelemetry::metrics::UpDownCounter<i64>> =
749            std::sync::LazyLock::new(|| {
750                $crate::meter(module_path!())
751                    .i64_up_down_counter($key)
752                    .build()
753            });
754    };
755}
756
757/// Create a thread safe static gauge that can be set to a specific value.
758/// This is useful to avoid creating temporary gauges.
759/// You can safely create gauges with the same name. They will be joined by the underlying
760/// runtime and are thread safe.
761///
762/// Example:
763/// ```
764/// struct System {
765///     pub memory_usage: f64,
766///     pub cpu_usage: f64,
767/// }
768///
769/// # #[macro_use] extern crate hyperactor_telemetry;
770/// # fn main() {
771/// # let system = System{memory_usage: 512.5, cpu_usage: 25.0};
772/// declare_static_gauge!(MEMORY_USAGE, "memory_usage");
773///
774/// MEMORY_USAGE.record(system.memory_usage, kv_pairs!("unit" => "MB", "process" => "hyperactor"))
775///
776/// # }
777/// ```
778#[macro_export]
779macro_rules! declare_static_gauge {
780    ($name:ident, $key:expr) => {
781        #[doc = "a global gauge named: "]
782        #[doc = $key]
783        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Gauge<f64>> =
784            std::sync::LazyLock::new(|| $crate::meter(module_path!()).f64_gauge($key).build());
785    };
786}
787/// Create a thread safe static observable gauge that can be set to a specific value based on the provided callback.
788/// This is useful for metrics that need to be calculated or retrieved dynamically.
789/// The callback will be executed whenever the gauge is observed by the metrics system.
790///
791/// Example:
792/// ```
793/// # #[macro_use] extern crate hyperactor_telemetry;
794///
795/// # fn main() {
796/// declare_observable_gauge!(MEMORY_USAGE_GAUGE, "memory_usage", |observer| {
797///     // Simulate getting memory usage - this could be any complex operation
798///     observer.observe(512.0, &[]);
799/// });
800///
801/// // The gauge will be automatically updated when observed
802/// # }
803/// ```
804#[macro_export]
805macro_rules! declare_observable_gauge {
806    ($name:ident, $key:expr, $cb:expr) => {
807        #[doc = "a global gauge named: "]
808        #[doc = $key]
809        pub static $name: std::sync::LazyLock<opentelemetry::metrics::ObservableGauge<f64>> =
810            std::sync::LazyLock::new(|| {
811                $crate::meter(module_path!())
812                    .f64_observable_gauge($key)
813                    .with_callback($cb)
814                    .build()
815            });
816    };
817}
818/// Create a thread safe static histogram that can be incremeneted or decremented.
819/// This is useful to avoid creating temporary histograms.
820/// You can safely create histograms with the same name. They will be joined by the underlying
821/// runtime and are thread safe.
822///
823/// Example:
824/// ```
825/// struct Url {
826///     pub path: String,
827///     pub proto: String,
828/// }
829///
830/// # #[macro_use] extern crate hyperactor_telemetry;
831/// # fn main() {
832/// # let url = Url{path: "/request/1".into(), proto: "https".into()};
833/// declare_static_histogram!(REQUEST_LATENCY, "request_latency");
834///
835/// REQUEST_LATENCY.record(40.0, kv_pairs!("path" => url.path, "proto" => url.proto))
836///
837/// # }
838/// ```
839#[macro_export]
840macro_rules! declare_static_histogram {
841    ($name:ident, $key:expr) => {
842        #[doc = "a global histogram named: "]
843        #[doc = $key]
844        pub static $name: std::sync::LazyLock<opentelemetry::metrics::Histogram<f64>> =
845            std::sync::LazyLock::new(|| {
846                hyperactor_telemetry::meter(module_path!())
847                    .f64_histogram($key)
848                    .build()
849            });
850    };
851}
852
853/// Set up logging based on the given execution environment. We specialize logging based on how the
854/// logs are consumed. The destination scuba table is specialized based on the execution environment.
855/// mast -> monarch_tracing/prod
856/// devserver -> monarch_tracing/local
857/// unit test  -> monarch_tracing/test
858/// scuba logging won't normally be enabled for a unit test unless we are specifically testing logging, so
859/// you don't need to worry about your tests being flakey due to scuba logging. You have to manually call initialize_logging()
860/// to get this behavior.
861pub fn initialize_logging(clock: impl TelemetryClock + Send + 'static) {
862    initialize_logging_with_log_prefix(clock, None);
863}
864
865/// testing
866pub fn initialize_logging_for_test() {
867    initialize_logging(DefaultTelemetryClock {});
868}
869
870/// Set up logging based on the given execution environment. We specialize logging based on how the
871/// logs are consumed. The destination scuba table is specialized based on the execution environment.
872/// mast -> monarch_tracing/prod
873/// devserver -> monarch_tracing/local
874/// unit test  -> monarch_tracing/test
875/// scuba logging won't normally be enabled for a unit test unless we are specifically testing logging, so
876/// you don't need to worry about your tests being flakey due to scuba logging. You have to manually call initialize_logging()
877/// to get this behavior.
878///
879/// tracing logs will be prefixed with the given prefix and routed to:
880/// test -> stderr
881/// local -> /tmp/monarch_log.log
882/// mast -> /logs/dedicated_monarch_logs.log
883/// Additionally, is MONARCH_STDERR_LOG sets logs level, then logs will be routed to stderr as well.
884pub fn initialize_logging_with_log_prefix(
885    clock: impl TelemetryClock + Send + 'static,
886    prefix_env_var: Option<String>,
887) {
888    let should_install_subscriber = !tracing::dispatcher::has_been_set();
889
890    swap_telemetry_clock(clock);
891    if !should_install_subscriber {
892        tracing::debug!("logging already initialized for this process");
893    }
894    let file_log_level = match env::Env::current() {
895        env::Env::Local => LOG_LEVEL_INFO,
896        env::Env::MastEmulator => LOG_LEVEL_INFO,
897        env::Env::Mast => LOG_LEVEL_INFO,
898        env::Env::Test => LOG_LEVEL_DEBUG,
899    };
900
901    use tracing_subscriber::Registry;
902    use tracing_subscriber::layer::SubscriberExt;
903    use tracing_subscriber::util::SubscriberInitExt;
904
905    #[cfg(all(fbcode_build, target_os = "linux"))]
906    {
907        if should_install_subscriber {
908            let mut sinks: Vec<Box<dyn trace_dispatcher::TraceEventSink>> = Vec::new();
909            sinks.push(Box::new(sinks::glog::GlogSink::new(
910                writer(),
911                prefix_env_var.clone(),
912                file_log_level,
913            )));
914
915            let sqlite_enabled = hyperactor_config::global::get(ENABLE_SQLITE_TRACING);
916
917            if sqlite_enabled {
918                match create_sqlite_sink() {
919                    Ok(sink) => {
920                        sinks.push(Box::new(sink));
921                    }
922                    Err(e) => {
923                        tracing::warn!("failed to create SqliteSink: {}", e);
924                    }
925                }
926            }
927
928            if hyperactor_config::global::get(sinks::perfetto::PERFETTO_TRACE_MODE)
929                != sinks::perfetto::PerfettoTraceMode::Off
930            {
931                let exec_id = env::execution_id();
932                let process_name = std::env::var("HYPERACTOR_PROCESS_NAME")
933                    .unwrap_or_else(|_| "client".to_string());
934                match sinks::perfetto::PerfettoFileSink::new(
935                    sinks::perfetto::default_trace_dir(),
936                    &exec_id,
937                    &process_name,
938                ) {
939                    Ok(sink) => {
940                        sinks.push(Box::new(sink));
941                    }
942                    Err(e) => {
943                        tracing::warn!("failed to create PerfettoFileSink: {}", e);
944                    }
945                }
946            }
947
948            if hyperactor_config::global::get(ENABLE_OTEL_TRACING) {
949                use crate::meta;
950
951                sinks.push(Box::new(
952                    meta::scuba_sink::ScubaSink::new(meta::tracing_resource())
953                        .with_target_filter(crate::config::get_tracing_targets()),
954                ));
955            }
956
957            sinks.push(unix_sink::install_unix_socket_sink_inactive());
958
959            let dispatcher = trace_dispatcher::TraceEventDispatcher::new(sinks);
960            let synthetic_sender = dispatcher.sender();
961
962            if let Err(err) = Registry::default()
963                .with(if hyperactor_config::global::get(ENABLE_RECORDER_TRACING) {
964                    Some(recorder().layer())
965                } else {
966                    None
967                })
968                .with(dispatcher)
969                .try_init()
970            {
971                tracing::debug!("logging already initialized for this process: {}", err);
972            } else {
973                set_synthetic_trace_event_sender(synthetic_sender);
974            }
975        }
976        let exec_id = env::execution_id();
977        let process_name =
978            std::env::var("HYPERACTOR_PROCESS_NAME").unwrap_or_else(|_| "client".to_string());
979
980        // setting target to "execution" will prevent the monarch_tracing scuba client from logging this
981        tracing::info!(
982            target: "execution",
983            execution_id = exec_id,
984            environment = %env::Env::current(),
985            args = ?std::env::args(),
986            build_mode = build_info::BuildInfo::get_build_mode(),
987            compiler = build_info::BuildInfo::get_compiler(),
988            compiler_version = build_info::BuildInfo::get_compiler_version(),
989            buck_rule = build_info::BuildInfo::get_rule(),
990            package_name = build_info::BuildInfo::get_package_name(),
991            package_release = build_info::BuildInfo::get_package_release(),
992            upstream_revision = build_info::BuildInfo::get_upstream_revision(),
993            revision = build_info::BuildInfo::get_revision(),
994            process_name = process_name,
995            "logging_initialized",
996        );
997        // here we have the monarch_executions scuba client log
998        meta::log_execution_event(
999            &exec_id,
1000            &env::Env::current().to_string(),
1001            std::env::args().collect(),
1002            build_info::BuildInfo::get_build_mode(),
1003            build_info::BuildInfo::get_compiler(),
1004            build_info::BuildInfo::get_compiler_version(),
1005            build_info::BuildInfo::get_rule(),
1006            build_info::BuildInfo::get_package_name(),
1007            build_info::BuildInfo::get_package_release(),
1008            build_info::BuildInfo::get_upstream_revision(),
1009            build_info::BuildInfo::get_revision(),
1010            &process_name,
1011        );
1012
1013        if hyperactor_config::global::get(ENABLE_OTEL_METRICS) {
1014            otel::init_metrics();
1015        }
1016    }
1017    #[cfg(not(all(fbcode_build, target_os = "linux")))]
1018    {
1019        let registry =
1020            Registry::default().with(if hyperactor_config::global::get(ENABLE_RECORDER_TRACING) {
1021                Some(recorder().layer())
1022            } else {
1023                None
1024            });
1025
1026        if should_install_subscriber {
1027            let mut sinks: Vec<Box<dyn trace_dispatcher::TraceEventSink>> = Vec::new();
1028
1029            let sqlite_enabled = hyperactor_config::global::get(ENABLE_SQLITE_TRACING);
1030
1031            if sqlite_enabled {
1032                match create_sqlite_sink() {
1033                    Ok(sink) => {
1034                        sinks.push(Box::new(sink));
1035                    }
1036                    Err(e) => {
1037                        tracing::warn!("failed to create SqliteSink: {}", e);
1038                    }
1039                }
1040            }
1041
1042            sinks.push(Box::new(sinks::glog::GlogSink::new(
1043                writer(),
1044                prefix_env_var.clone(),
1045                file_log_level,
1046            )));
1047
1048            if let Some(log_sink) = otlp::otlp_log_sink() {
1049                sinks.push(log_sink);
1050            }
1051
1052            sinks.push(unix_sink::install_unix_socket_sink_inactive());
1053
1054            let dispatcher = trace_dispatcher::TraceEventDispatcher::new(sinks);
1055            let synthetic_sender = dispatcher.sender();
1056
1057            if let Err(err) = registry.with(dispatcher).try_init() {
1058                tracing::debug!("logging already initialized for this process: {}", err);
1059            } else {
1060                set_synthetic_trace_event_sender(synthetic_sender);
1061            }
1062        }
1063
1064        otel::init_metrics();
1065    }
1066}
1067
1068fn create_sqlite_sink() -> anyhow::Result<sinks::sqlite::SqliteSink> {
1069    let (db_path, _) = log_file_path(env::Env::current(), Some("traces"))
1070        .expect("failed to determine trace db path");
1071    let db_file = format!("{}/hyperactor_trace_{}.db", db_path, std::process::id());
1072
1073    sinks::sqlite::SqliteSink::new_with_file(&db_file, 100)
1074}
1075
1076/// Create a context span at ERROR level with skip_record enabled.
1077/// This is intended to create spans whose only purpose it is to add context
1078/// to child events; the span itself is never independently recorded.
1079///
1080/// Example:
1081/// ```ignore
1082/// use hyperactor_telemetry::context_span;
1083///
1084/// let span = context_span!("my_context", field1 = value1, field2 = value2);
1085/// let _guard = span.enter();
1086/// // ... do work that will be logged with this context
1087/// ```
1088#[macro_export]
1089macro_rules! context_span {
1090    (target: $target:expr, parent: $parent:expr, $name:expr, $($field:tt)*) => {
1091        ::tracing::error_span!(
1092            target: $target,
1093            parent: $parent,
1094            $name,
1095            skip_record = $crate::skip_record,
1096            $($field)*
1097        )
1098    };
1099    (target: $target:expr, parent: $parent:expr, $name:expr) => {
1100        ::tracing::error_span!(
1101            target: $target,
1102            parent: $parent,
1103            $name,
1104            skip_record = $crate::skip_record,
1105        )
1106    };
1107    (parent: $parent:expr, $name:expr, $($field:tt)*) => {
1108        ::tracing::error_span!(
1109            target: module_path!(),
1110            parent: $parent,
1111            $name,
1112            skip_record = $crate::skip_record,
1113            $($field)*
1114        )
1115    };
1116    (parent: $parent:expr, $name:expr) => {
1117        ::tracing::error_span!(
1118            parent: $parent,
1119            $name,
1120            skip_record = $crate::skip_record,
1121        )
1122    };
1123    (target: $target:expr, $name:expr, $($field:tt)*) => {
1124        ::tracing::error_span!(
1125            target: $target,
1126            $name,
1127            skip_record = $crate::skip_record,
1128            $($field)*
1129        )
1130    };
1131    (target: $target:expr, $name:expr) => {
1132        ::tracing::error_span!(
1133            target: $target,
1134            $name,
1135            skip_record = $crate::skip_record,
1136        )
1137    };
1138    ($name:expr, $($field:tt)*) => {
1139        ::tracing::error_span!(
1140            target: module_path!(),
1141            $name,
1142            skip_record = $crate::skip_record,
1143            $($field)*
1144        )
1145    };
1146    ($name:expr) => {
1147        ::tracing::error_span!(
1148            $name,
1149            skip_record = $crate::skip_record,
1150        )
1151    };
1152}
1153
1154pub mod env {
1155    /// Env var name set when monarch launches subprocesses to forward the execution context
1156    pub const HYPERACTOR_EXECUTION_ID_ENV: &str = "HYPERACTOR_EXECUTION_ID";
1157    pub const OTEL_EXPORTER: &str = "HYPERACTOR_OTEL_EXPORTER";
1158    pub const MAST_ENVIRONMENT: &str = "MAST_ENVIRONMENT";
1159
1160    /// Forward or generate a uuid for this execution. When running in production on mast, this is provided to
1161    /// us via the MAST_HPC_JOB_NAME env var. Subprocesses should either forward the MAST_HPC_JOB_NAME
1162    /// variable, or set the "MONARCH_EXECUTION_ID" var for subprocesses launched by this process.
1163    /// We keep these env vars separate so that other applications that depend on the MAST_HPC_JOB_NAME existing
1164    /// to understand their environment do not get confused and think they are running on mast when we are doing
1165    ///  local testing.
1166    pub fn execution_id() -> String {
1167        let id = std::env::var(HYPERACTOR_EXECUTION_ID_ENV).unwrap_or_else(|_| {
1168            // not able to find an existing id so generate a unique one: username + current_time + random number.
1169            let username = crate::username();
1170            let now = {
1171                let now = std::time::SystemTime::now();
1172                let datetime: chrono::DateTime<chrono::Local> = now.into();
1173                datetime.format("%b-%d_%H:%M").to_string()
1174            };
1175            let random_number: u16 = (rand::random::<u32>() % 1000) as u16;
1176            let execution_id = format!("{}_{}_{}", username, now, random_number);
1177            execution_id
1178        });
1179        // Safety: Can be unsound if there are multiple threads
1180        // reading and writing the environment.
1181        unsafe {
1182            std::env::set_var(HYPERACTOR_EXECUTION_ID_ENV, id.clone());
1183        }
1184        id
1185    }
1186
1187    #[derive(PartialEq)]
1188    pub enum Env {
1189        Local,
1190        Mast,
1191        MastEmulator,
1192        Test,
1193    }
1194
1195    impl std::fmt::Display for Env {
1196        fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1197            write!(
1198                f,
1199                "{}",
1200                match self {
1201                    Self::Local => crate::ENV_VALUE_LOCAL,
1202                    Self::MastEmulator => crate::ENV_VALUE_MAST_EMULATOR,
1203                    Self::Mast => crate::ENV_VALUE_MAST,
1204                    Self::Test => crate::ENV_VALUE_TEST,
1205                }
1206            )
1207        }
1208    }
1209
1210    impl Env {
1211        #[cfg(test)]
1212        pub fn current() -> Self {
1213            Self::Test
1214        }
1215
1216        #[cfg(not(test))]
1217        pub fn current() -> Self {
1218            match std::env::var(MAST_ENVIRONMENT).unwrap_or_default().as_str() {
1219                // Constant from https://fburl.com/fhysd3fd
1220                crate::ENV_VALUE_LOCAL_MAST_SIMULATOR => Self::MastEmulator,
1221                _ => match std::env::var(crate::MAST_HPC_JOB_NAME_ENV).is_ok() {
1222                    true => Self::Mast,
1223                    false => Self::Local,
1224                },
1225            }
1226        }
1227    }
1228}
1229
1230#[cfg(test)]
1231mod test {
1232    use opentelemetry::*;
1233    extern crate self as hyperactor_telemetry;
1234    use super::*;
1235
1236    #[test]
1237    fn infer_kv_pair_types() {
1238        assert_eq!(
1239            key_value!("str", "str"),
1240            KeyValue::new(Key::new("str"), Value::String("str".into()))
1241        );
1242        assert_eq!(
1243            key_value!("str", 25),
1244            KeyValue::new(Key::new("str"), Value::I64(25))
1245        );
1246        assert_eq!(
1247            key_value!("str", 1.1),
1248            KeyValue::new(Key::new("str"), Value::F64(1.1))
1249        );
1250    }
1251    #[test]
1252    fn kv_pair_slices() {
1253        assert_eq!(
1254            kv_pairs!("1" => "1", "2" => 2, "3" => 3.0),
1255            &[
1256                key_value!("1", "1"),
1257                key_value!("2", 2),
1258                key_value!("3", 3.0),
1259            ],
1260        );
1261    }
1262
1263    #[test]
1264    fn test_static_gauge() {
1265        // Create a static gauge using the macro
1266        declare_static_gauge!(TEST_GAUGE, "test_gauge");
1267        declare_static_gauge!(MEMORY_GAUGE, "memory_usage");
1268
1269        // Set values to the gauge with different attributes
1270        // This shouldn't actually log to scribe/scuba in test environment
1271        TEST_GAUGE.record(42.5, kv_pairs!("component" => "test", "unit" => "MB"));
1272        MEMORY_GAUGE.record(512.0, kv_pairs!("type" => "heap", "process" => "test"));
1273
1274        // Test with empty attributes
1275        TEST_GAUGE.record(50.0, &[]);
1276    }
1277}