Skip to main content

hyperactor_telemetry/
unix_sink.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//! Producer-side Unix-socket sink for telemetry sidecars.
10//!
11//! This module is the producer-side transport for telemetry. It shares one
12//! process-global `UnixSocketSink` across traces, entities, and metrics. It buffers
13//! trace and entity events into schema-specific `RecordBatchBuffer`s and, once
14//! activated with a socket path, forwards flushed batches to the sidecar over a
15//! Unix socket. Metrics are sent only while the sink is active.
16//!
17//! The dispatcher thread only does cheap row buffering and bounded channel
18//! sends. Arrow IPC serialization and socket I/O run on a dedicated writer
19//! thread so slow or missing sidecars do not block application tracing.
20//!
21//! Frame layout matches `socket_ingest.rs`: table-name length, UTF-8 table
22//! name, Arrow IPC payload length, and one non-empty `RecordBatch` payload.
23//! Frames are lossy by design: if the queue is full, serialization fails, or a
24//! socket write fails, we increment `dropped` and keep the tracing path moving.
25
26use std::io::Write;
27use std::os::unix::net::UnixStream;
28use std::path::PathBuf;
29use std::sync::Arc;
30use std::sync::LazyLock;
31use std::sync::Mutex;
32use std::sync::OnceLock;
33use std::sync::atomic::AtomicU64;
34use std::sync::atomic::Ordering;
35use std::sync::mpsc;
36use std::time::SystemTime;
37
38use monarch_record_batch::RecordBatchBuffer;
39use monarch_telemetry_schema::MAX_FRAME_LEN;
40use monarch_telemetry_schema::entity_tables::ACTOR_STATUS_EVENTS;
41use monarch_telemetry_schema::entity_tables::ACTORS;
42use monarch_telemetry_schema::entity_tables::Actor;
43use monarch_telemetry_schema::entity_tables::ActorBuffer;
44use monarch_telemetry_schema::entity_tables::ActorStatusEvent as ActorStatusEventRow;
45use monarch_telemetry_schema::entity_tables::ActorStatusEventBuffer;
46use monarch_telemetry_schema::entity_tables::MESHES;
47use monarch_telemetry_schema::entity_tables::MESSAGE_STATUS_EVENTS;
48use monarch_telemetry_schema::entity_tables::MESSAGES;
49use monarch_telemetry_schema::entity_tables::Mesh;
50use monarch_telemetry_schema::entity_tables::MeshBuffer;
51use monarch_telemetry_schema::entity_tables::Message;
52use monarch_telemetry_schema::entity_tables::MessageBuffer;
53use monarch_telemetry_schema::entity_tables::MessageStatusEvent as MessageStatusEventRow;
54use monarch_telemetry_schema::entity_tables::MessageStatusEventBuffer;
55use monarch_telemetry_schema::entity_tables::SENT_MESSAGES;
56use monarch_telemetry_schema::entity_tables::SentMessage;
57use monarch_telemetry_schema::entity_tables::SentMessageBuffer;
58use monarch_telemetry_schema::metric_tables::METRIC_GAUGES;
59use monarch_telemetry_schema::metric_tables::METRIC_HISTOGRAMS;
60use monarch_telemetry_schema::metric_tables::METRIC_SUMS;
61use monarch_telemetry_schema::metric_tables::MetricGaugeBuffer;
62use monarch_telemetry_schema::metric_tables::MetricHistogramBuffer;
63use monarch_telemetry_schema::metric_tables::MetricSumBuffer;
64use monarch_telemetry_schema::trace_tables::EVENTS;
65use monarch_telemetry_schema::trace_tables::Event;
66use monarch_telemetry_schema::trace_tables::EventBuffer;
67use monarch_telemetry_schema::trace_tables::SPAN_EVENTS;
68use monarch_telemetry_schema::trace_tables::SPANS;
69use monarch_telemetry_schema::trace_tables::Span;
70use monarch_telemetry_schema::trace_tables::SpanBuffer;
71use monarch_telemetry_schema::trace_tables::SpanEvent;
72use monarch_telemetry_schema::trace_tables::SpanEventBuffer;
73use monarch_telemetry_schema::write_frame_header;
74use tracing_subscriber::filter::Targets;
75
76use crate::EntityEvent;
77use crate::FieldValue;
78use crate::TraceEvent;
79use crate::TraceEventSink;
80use crate::config::get_tracing_targets;
81use crate::generate_sent_message_id;
82
83/// Maximum queued table batches before the tracing path starts dropping frames.
84const WORKER_QUEUE_CAPACITY: usize = 10_000;
85const HYPERACTOR_PROCESS_NAME_ENV: &str = "HYPERACTOR_PROCESS_NAME";
86const HOSTNAME_ENV: &str = "HOSTNAME";
87
88fn fallback_process_id() -> String {
89    let pid = std::process::id();
90    let hostname = std::env::var(HOSTNAME_ENV)
91        .ok()
92        .filter(|hostname| !hostname.is_empty())
93        .unwrap_or_else(|| format!("{:032x}", rand::random::<u128>()));
94
95    format!("unnamed@{hostname}:{pid}")
96}
97
98/// Namespace for process-local span IDs after distributed aggregation.
99static PROCESS_ID: LazyLock<String> = LazyLock::new(|| {
100    std::env::var(HYPERACTOR_PROCESS_NAME_ENV)
101        .ok()
102        .filter(|process_id| !process_id.is_empty())
103        .unwrap_or_else(fallback_process_id)
104});
105
106/// Process-global sink installed with the tracing subscriber and activated later.
107static UNIX_SOCKET_SINK: OnceLock<Arc<UnixSocketSink>> = OnceLock::new();
108
109fn unix_socket_sink() -> &'static Arc<UnixSocketSink> {
110    UNIX_SOCKET_SINK.get_or_init(|| Arc::new(UnixSocketSink::new()))
111}
112
113/// Producer-side sink that forwards telemetry batches to a sidecar socket.
114pub struct UnixSocketSink {
115    // Shared between the dispatcher adapter and the global activation handle.
116    // The mutex covers buffered rows plus the path/worker activation state.
117    inner: Mutex<UnixSocketSinkInner>,
118    // The writer thread increments this for asynchronous serialization and
119    // socket failures, so callers can observe producer-side frame loss.
120    dropped: Arc<AtomicU64>,
121}
122
123struct UnixSocketSinkInner {
124    spans_buffer: SpanBuffer,
125    span_events_buffer: SpanEventBuffer,
126    events_buffer: EventBuffer,
127    actors_buffer: ActorBuffer,
128    meshes_buffer: MeshBuffer,
129    actor_status_events_buffer: ActorStatusEventBuffer,
130    sent_messages_buffer: SentMessageBuffer,
131    messages_buffer: MessageBuffer,
132    message_status_events_buffer: MessageStatusEventBuffer,
133    path: Option<PathBuf>,
134    worker: Option<WorkerHandle>,
135}
136
137struct WorkerHandle {
138    sender: mpsc::SyncSender<TelemetryTableBuffer>,
139    _join_handle: std::thread::JoinHandle<()>,
140}
141
142enum TelemetryTableBuffer {
143    Spans(SpanBuffer),
144    SpanEvents(SpanEventBuffer),
145    Events(EventBuffer),
146    Actors(ActorBuffer),
147    Meshes(MeshBuffer),
148    ActorStatusEvents(ActorStatusEventBuffer),
149    SentMessages(SentMessageBuffer),
150    Messages(MessageBuffer),
151    MessageStatusEvents(MessageStatusEventBuffer),
152    MetricGauges(MetricGaugeBuffer),
153    MetricSums(MetricSumBuffer),
154    MetricHistograms(MetricHistogramBuffer),
155}
156
157struct UnixSocketSinkAdapter {
158    // The dispatcher owns this adapter, while `UNIX_SOCKET_SINK` keeps another
159    // handle so callers can supply the socket path after logging initialization.
160    sink: Arc<UnixSocketSink>,
161    target_filter: Targets,
162}
163
164impl UnixSocketSink {
165    /// Create a sink with no socket path and no writer thread.
166    pub fn new() -> Self {
167        Self {
168            inner: Mutex::new(UnixSocketSinkInner {
169                spans_buffer: SpanBuffer::default(),
170                span_events_buffer: SpanEventBuffer::default(),
171                events_buffer: EventBuffer::default(),
172                actors_buffer: ActorBuffer::default(),
173                meshes_buffer: MeshBuffer::default(),
174                actor_status_events_buffer: ActorStatusEventBuffer::default(),
175                sent_messages_buffer: SentMessageBuffer::default(),
176                messages_buffer: MessageBuffer::default(),
177                message_status_events_buffer: MessageStatusEventBuffer::default(),
178                path: None,
179                worker: None,
180            }),
181            dropped: Arc::new(AtomicU64::new(0)),
182        }
183    }
184
185    /// Activate the sink and lazily spawn a writer thread.
186    ///
187    /// Reapplying the same path is a no-op; changing paths is rejected because
188    /// the process-global sink should not be retargeted after activation.
189    pub fn set_path(&self, path: PathBuf) -> anyhow::Result<()> {
190        let mut inner = self
191            .inner
192            .lock()
193            .map_err(|_| anyhow::anyhow!("lock poisoned"))?;
194
195        if inner.path.as_ref() == Some(&path) {
196            return Ok(());
197        }
198        if let Some(existing) = &inner.path {
199            anyhow::bail!(
200                "unix socket sink already activated with {}",
201                existing.display()
202            );
203        }
204
205        // The tracing subscriber is installed before the sidecar path is
206        // available. Start the writer only after activation so the inactive
207        // sink is cheap and does not repeatedly try to connect to nowhere.
208        let (sender, receiver) = mpsc::sync_channel(WORKER_QUEUE_CAPACITY);
209        let dropped = Arc::clone(&self.dropped);
210        let writer_path = path.clone();
211        let join_handle = std::thread::Builder::new()
212            .name("monarch-telemetry-unix-sink".into())
213            .spawn(move || writer_loop(writer_path, receiver, dropped))
214            .map_err(|error| anyhow::anyhow!("failed to spawn unix socket sink: {error}"))?;
215
216        inner.path = Some(path);
217        inner.worker = Some(WorkerHandle {
218            sender,
219            _join_handle: join_handle,
220        });
221        Ok(())
222    }
223
224    /// Return whether this sink has an active socket path.
225    pub fn is_active(&self) -> bool {
226        self.inner
227            .lock()
228            .map(|inner| inner.worker.is_some())
229            .unwrap_or(false)
230    }
231
232    /// Return the cumulative number of dropped socket frames.
233    pub fn dropped_frames(&self) -> u64 {
234        self.dropped.load(Ordering::Relaxed)
235    }
236
237    fn send_metric_buffers(
238        &self,
239        gauges: MetricGaugeBuffer,
240        sums: MetricSumBuffer,
241        histograms: MetricHistogramBuffer,
242    ) {
243        let sender = match self.inner.lock() {
244            Ok(inner) => inner.worker.as_ref().map(|worker| worker.sender.clone()),
245            Err(_) => {
246                let dropped = [gauges.is_empty(), sums.is_empty(), histograms.is_empty()]
247                    .into_iter()
248                    .filter(|empty| !empty)
249                    .count() as u64;
250                self.dropped.fetch_add(dropped, Ordering::Relaxed);
251                return;
252            }
253        };
254
255        let Some(sender) = sender else {
256            return;
257        };
258
259        if !gauges.is_empty() {
260            try_send_table_buffer(
261                TelemetryTableBuffer::MetricGauges(gauges),
262                &sender,
263                &self.dropped,
264            );
265        }
266        if !sums.is_empty() {
267            try_send_table_buffer(
268                TelemetryTableBuffer::MetricSums(sums),
269                &sender,
270                &self.dropped,
271            );
272        }
273        if !histograms.is_empty() {
274            try_send_table_buffer(
275                TelemetryTableBuffer::MetricHistograms(histograms),
276                &sender,
277                &self.dropped,
278            );
279        }
280    }
281
282    /// Convert one dispatcher event into its table row and buffer it locally.
283    fn consume_shared(&self, event: &TraceEvent) -> anyhow::Result<()> {
284        let mut inner = self
285            .inner
286            .lock()
287            .map_err(|_| anyhow::anyhow!("lock poisoned"))?;
288
289        match event {
290            TraceEvent::NewSpan {
291                id,
292                name,
293                target,
294                level,
295                fields,
296                timestamp,
297                parent_id,
298                thread_name,
299                file,
300                line,
301            } => {
302                inner.spans_buffer.insert(Span {
303                    process_id: PROCESS_ID.as_str(),
304                    id: *id,
305                    name: name.to_string(),
306                    target: target.to_string(),
307                    level: level.to_string(),
308                    fields_json: fields_to_json(fields),
309                    timestamp_us: timestamp_to_micros(timestamp),
310                    parent_id: *parent_id,
311                    thread_name: thread_name.to_string(),
312                    file: file.map(|s| s.to_string()),
313                    line: *line,
314                });
315            }
316            TraceEvent::SpanEnter { id, timestamp, .. } => {
317                inner.span_events_buffer.insert(SpanEvent {
318                    process_id: PROCESS_ID.as_str(),
319                    id: *id,
320                    timestamp_us: timestamp_to_micros(timestamp),
321                    event_type: "enter".to_string(),
322                });
323            }
324            TraceEvent::SpanExit { id, timestamp, .. } => {
325                inner.span_events_buffer.insert(SpanEvent {
326                    process_id: PROCESS_ID.as_str(),
327                    id: *id,
328                    timestamp_us: timestamp_to_micros(timestamp),
329                    event_type: "exit".to_string(),
330                });
331            }
332            TraceEvent::SpanClose { id, timestamp } => {
333                inner.span_events_buffer.insert(SpanEvent {
334                    process_id: PROCESS_ID.as_str(),
335                    id: *id,
336                    timestamp_us: timestamp_to_micros(timestamp),
337                    event_type: "close".to_string(),
338                });
339            }
340            TraceEvent::Event {
341                name,
342                target,
343                level,
344                fields,
345                timestamp,
346                parent_span,
347                thread_id,
348                thread_name,
349                module_path,
350                file,
351                line,
352            } => {
353                inner.events_buffer.insert(Event {
354                    name: name.to_string(),
355                    target: target.to_string(),
356                    level: level.to_string(),
357                    fields_json: fields_to_json(fields),
358                    timestamp_us: timestamp_to_micros(timestamp),
359                    parent_span: *parent_span,
360                    thread_id: thread_id.to_string(),
361                    thread_name: thread_name.to_string(),
362                    module_path: module_path.map(|s| s.to_string()),
363                    file: file.map(|s| s.to_string()),
364                    line: *line,
365                });
366            }
367            TraceEvent::Entity(event) => buffer_entity_event(&mut inner, event),
368        }
369        Ok(())
370    }
371
372    /// Move non-empty table buffers to the writer queue, or drop them while inactive.
373    fn flush_shared(&self) -> anyhow::Result<()> {
374        let mut inner = self
375            .inner
376            .lock()
377            .map_err(|_| anyhow::anyhow!("lock poisoned"))?;
378
379        if inner.worker.is_none() {
380            // The buffers provide only a small pre-activation window: rows
381            // consumed before `set_path` can be sent if activation wins the
382            // race with the next dispatcher flush. Otherwise, drop them here
383            // so startup telemetry does not accumulate without a socket path.
384            inner.spans_buffer = SpanBuffer::default();
385            inner.span_events_buffer = SpanEventBuffer::default();
386            inner.events_buffer = EventBuffer::default();
387            inner.actors_buffer = ActorBuffer::default();
388            inner.meshes_buffer = MeshBuffer::default();
389            inner.actor_status_events_buffer = ActorStatusEventBuffer::default();
390            inner.sent_messages_buffer = SentMessageBuffer::default();
391            inner.messages_buffer = MessageBuffer::default();
392            inner.message_status_events_buffer = MessageStatusEventBuffer::default();
393            return Ok(());
394        }
395
396        let sender = inner
397            .worker
398            .as_ref()
399            .expect("worker should exist")
400            .sender
401            .clone();
402
403        flush_buffer(
404            &mut inner.spans_buffer,
405            TelemetryTableBuffer::Spans,
406            &sender,
407            &self.dropped,
408        );
409        flush_buffer(
410            &mut inner.span_events_buffer,
411            TelemetryTableBuffer::SpanEvents,
412            &sender,
413            &self.dropped,
414        );
415        flush_buffer(
416            &mut inner.events_buffer,
417            TelemetryTableBuffer::Events,
418            &sender,
419            &self.dropped,
420        );
421        flush_buffer(
422            &mut inner.actors_buffer,
423            TelemetryTableBuffer::Actors,
424            &sender,
425            &self.dropped,
426        );
427        flush_buffer(
428            &mut inner.meshes_buffer,
429            TelemetryTableBuffer::Meshes,
430            &sender,
431            &self.dropped,
432        );
433        flush_buffer(
434            &mut inner.actor_status_events_buffer,
435            TelemetryTableBuffer::ActorStatusEvents,
436            &sender,
437            &self.dropped,
438        );
439        flush_buffer(
440            &mut inner.sent_messages_buffer,
441            TelemetryTableBuffer::SentMessages,
442            &sender,
443            &self.dropped,
444        );
445        flush_buffer(
446            &mut inner.messages_buffer,
447            TelemetryTableBuffer::Messages,
448            &sender,
449            &self.dropped,
450        );
451        flush_buffer(
452            &mut inner.message_status_events_buffer,
453            TelemetryTableBuffer::MessageStatusEvents,
454            &sender,
455            &self.dropped,
456        );
457        Ok(())
458    }
459}
460
461impl TraceEventSink for UnixSocketSinkAdapter {
462    fn consume(&mut self, event: &TraceEvent) -> Result<(), anyhow::Error> {
463        self.sink.consume_shared(event)
464    }
465
466    fn target_filter(&self) -> Option<&Targets> {
467        Some(&self.target_filter)
468    }
469
470    fn flush(&mut self) -> Result<(), anyhow::Error> {
471        self.sink.flush_shared()
472    }
473
474    fn name(&self) -> &str {
475        "UnixSocketSink"
476    }
477}
478
479/// Install the inactive process-global Unix socket sink.
480pub(crate) fn install_unix_socket_sink_inactive() -> Box<dyn TraceEventSink> {
481    let sink = Arc::clone(unix_socket_sink());
482    Box::new(UnixSocketSinkAdapter {
483        sink,
484        target_filter: get_tracing_targets(),
485    })
486}
487
488/// Activate the process-global Unix socket sink against a socket path.
489pub fn set_unix_socket_sink_path(path: impl Into<PathBuf>) -> anyhow::Result<()> {
490    unix_socket_sink().set_path(path.into())
491}
492
493/// Return whether the process-global Unix socket sink is active.
494pub fn unix_socket_sink_is_active() -> bool {
495    UNIX_SOCKET_SINK
496        .get()
497        .map(|sink| sink.is_active())
498        .unwrap_or(false)
499}
500
501/// Return the process-global Unix socket sink's cumulative dropped frames.
502pub fn unix_socket_sink_dropped_frames() -> Option<u64> {
503    UNIX_SOCKET_SINK.get().map(|sink| sink.dropped_frames())
504}
505
506/// Send encoded metric table buffers through the active sidecar transport.
507pub(crate) fn send_metric_buffers(
508    gauges: MetricGaugeBuffer,
509    sums: MetricSumBuffer,
510    histograms: MetricHistogramBuffer,
511) {
512    let Some(sink) = UNIX_SOCKET_SINK.get() else {
513        return;
514    };
515    sink.send_metric_buffers(gauges, sums, histograms);
516}
517
518fn flush_buffer<B>(
519    buffer: &mut B,
520    table_buffer: impl FnOnce(B) -> TelemetryTableBuffer,
521    sender: &mpsc::SyncSender<TelemetryTableBuffer>,
522    dropped: &AtomicU64,
523) where
524    B: RecordBatchBuffer + Default,
525{
526    if buffer.is_empty() {
527        return;
528    }
529
530    let batch = table_buffer(std::mem::take(buffer));
531    try_send_table_buffer(batch, sender, dropped);
532}
533
534fn try_send_table_buffer(
535    buffer: TelemetryTableBuffer,
536    sender: &mpsc::SyncSender<TelemetryTableBuffer>,
537    dropped: &AtomicU64,
538) {
539    match sender.try_send(buffer) {
540        Ok(()) => {}
541        Err(mpsc::TrySendError::Full(_)) | Err(mpsc::TrySendError::Disconnected(_)) => {
542            // Socket delivery must never backpressure the tracing dispatcher.
543            // Drop the whole table batch when the worker cannot accept it.
544            dropped.fetch_add(1, Ordering::Relaxed);
545        }
546    }
547}
548
549fn buffer_entity_event(inner: &mut UnixSocketSinkInner, event: &EntityEvent) {
550    match event {
551        EntityEvent::Actor(event) => {
552            inner.actors_buffer.insert(Actor {
553                id: event.id,
554                timestamp_us: timestamp_to_micros(&event.timestamp),
555                mesh_id: event.mesh_id,
556                rank: event.rank,
557                full_name: event.full_name.clone(),
558                display_name: event.display_name.clone(),
559            });
560        }
561        EntityEvent::Mesh(event) => {
562            inner.meshes_buffer.insert(Mesh {
563                id: event.id,
564                timestamp_us: timestamp_to_micros(&event.timestamp),
565                class: event.class.clone(),
566                given_name: event.given_name.clone(),
567                full_name: event.full_name.clone(),
568                shape_json: event.shape_json.clone(),
569                parent_mesh_id: event.parent_mesh_id,
570                parent_view_json: event.parent_view_json.clone(),
571            });
572        }
573        EntityEvent::ActorStatus(event) => {
574            inner
575                .actor_status_events_buffer
576                .insert(ActorStatusEventRow {
577                    id: event.id,
578                    timestamp_us: timestamp_to_micros(&event.timestamp),
579                    actor_id: event.actor_id,
580                    new_status: event.new_status.clone(),
581                    reason: event.reason.clone(),
582                });
583        }
584        EntityEvent::SentMessage(event) => {
585            inner.sent_messages_buffer.insert(SentMessage {
586                id: generate_sent_message_id(event.sender_actor_id),
587                timestamp_us: timestamp_to_micros(&event.timestamp),
588                sender_actor_id: event.sender_actor_id,
589                actor_mesh_id: event.actor_mesh_id,
590                view_json: event.view_json.clone(),
591                shape_json: event.shape_json.clone(),
592            });
593        }
594        EntityEvent::Message(event) => {
595            inner.messages_buffer.insert(Message {
596                id: event.id,
597                timestamp_us: timestamp_to_micros(&event.timestamp),
598                from_actor_id: event.from_actor_id,
599                to_actor_id: event.to_actor_id,
600                endpoint: event.endpoint.clone(),
601                port_index: event.port_index,
602            });
603        }
604        EntityEvent::MessageStatus(event) => {
605            inner
606                .message_status_events_buffer
607                .insert(MessageStatusEventRow {
608                    id: event.id,
609                    timestamp_us: timestamp_to_micros(&event.timestamp),
610                    message_id: event.message_id,
611                    status: event.status.clone(),
612                });
613        }
614    }
615}
616
617/// Serialize drained table buffers and write framed Arrow IPC payloads to the sidecar.
618fn writer_loop(
619    path: PathBuf,
620    receiver: mpsc::Receiver<TelemetryTableBuffer>,
621    dropped: Arc<AtomicU64>,
622) {
623    let mut stream = None;
624
625    while let Ok(mut buffer) = receiver.recv() {
626        // Convert buffered rows into the same one-table, one-batch frame shape
627        // that the ingest server validates. Failures here are producer-side
628        // frame drops; the writer keeps processing later batches.
629        //
630        // Keep the table-name selection next to the concrete buffer variant so
631        // adding a socket table requires an explicit writer-loop update.
632        let (table_name, batch) = match &mut buffer {
633            TelemetryTableBuffer::Spans(buffer) => (SPANS, buffer.drain_to_record_batch()),
634            TelemetryTableBuffer::SpanEvents(buffer) => {
635                (SPAN_EVENTS, buffer.drain_to_record_batch())
636            }
637            TelemetryTableBuffer::Events(buffer) => (EVENTS, buffer.drain_to_record_batch()),
638            TelemetryTableBuffer::Actors(buffer) => (ACTORS, buffer.drain_to_record_batch()),
639            TelemetryTableBuffer::Meshes(buffer) => (MESHES, buffer.drain_to_record_batch()),
640            TelemetryTableBuffer::ActorStatusEvents(buffer) => {
641                (ACTOR_STATUS_EVENTS, buffer.drain_to_record_batch())
642            }
643            TelemetryTableBuffer::SentMessages(buffer) => {
644                (SENT_MESSAGES, buffer.drain_to_record_batch())
645            }
646            TelemetryTableBuffer::Messages(buffer) => (MESSAGES, buffer.drain_to_record_batch()),
647            TelemetryTableBuffer::MessageStatusEvents(buffer) => {
648                (MESSAGE_STATUS_EVENTS, buffer.drain_to_record_batch())
649            }
650            TelemetryTableBuffer::MetricGauges(buffer) => {
651                (METRIC_GAUGES, buffer.drain_to_record_batch())
652            }
653            TelemetryTableBuffer::MetricSums(buffer) => {
654                (METRIC_SUMS, buffer.drain_to_record_batch())
655            }
656            TelemetryTableBuffer::MetricHistograms(buffer) => {
657                (METRIC_HISTOGRAMS, buffer.drain_to_record_batch())
658            }
659        };
660        let Ok(batch) = batch else {
661            dropped.fetch_add(1, Ordering::Relaxed);
662            continue;
663        };
664        if batch.num_rows() == 0 {
665            continue;
666        }
667
668        let Ok(payload) = monarch_telemetry_schema::serialize_batch(&batch) else {
669            dropped.fetch_add(1, Ordering::Relaxed);
670            continue;
671        };
672        if payload.len() > MAX_FRAME_LEN {
673            dropped.fetch_add(1, Ordering::Relaxed);
674            continue;
675        }
676
677        // Exact frame size: u16 table-name length, table-name bytes, u32
678        // payload length, then the Arrow IPC payload bytes.
679        let mut frame = Vec::with_capacity(2 + table_name.len() + 4 + payload.len());
680        if write_frame_header(&mut frame, table_name, payload.len()).is_err() {
681            dropped.fetch_add(1, Ordering::Relaxed);
682            continue;
683        }
684        frame.extend_from_slice(&payload);
685
686        if write_frame(&path, &mut stream, &frame).is_err() {
687            dropped.fetch_add(1, Ordering::Relaxed);
688            stream = None;
689        }
690    }
691}
692
693fn write_frame(
694    path: &PathBuf,
695    stream: &mut Option<UnixStream>,
696    frame: &[u8],
697) -> std::io::Result<()> {
698    if stream.is_none() {
699        *stream = Some(UnixStream::connect(path)?);
700    }
701
702    // Reuse a connected stream across frames. On any write error, discard the
703    // stream so the next frame attempts a fresh connect to a restarted sidecar.
704    match stream
705        .as_mut()
706        .expect("stream should exist")
707        .write_all(frame)
708    {
709        Ok(()) => Ok(()),
710        Err(error) => {
711            *stream = None;
712            Err(error)
713        }
714    }
715}
716
717fn fields_to_json(fields: &[(&str, FieldValue)]) -> String {
718    monarch_telemetry_schema::fields_to_json(fields.iter().map(|(key, value)| {
719        let json_value = match value {
720            FieldValue::Bool(b) => serde_json::Value::Bool(*b),
721            FieldValue::I64(i) => serde_json::Value::Number((*i).into()),
722            FieldValue::U64(u) => serde_json::Value::Number((*u).into()),
723            FieldValue::F64(f) => serde_json::Number::from_f64(*f)
724                .map(serde_json::Value::Number)
725                .unwrap_or(serde_json::Value::Null),
726            FieldValue::Str(s) => serde_json::Value::String(s.clone()),
727            FieldValue::Debug(d) => serde_json::Value::String(d.clone()),
728        };
729        (*key, json_value)
730    }))
731}
732
733fn timestamp_to_micros(timestamp: &SystemTime) -> i64 {
734    timestamp
735        .duration_since(std::time::UNIX_EPOCH)
736        .unwrap_or_default()
737        .as_micros() as i64
738}
739
740#[cfg(test)]
741pub(crate) fn adapter_for_test(sink: Arc<UnixSocketSink>) -> Box<dyn TraceEventSink> {
742    Box::new(UnixSocketSinkAdapter {
743        sink,
744        target_filter: get_tracing_targets(),
745    })
746}
747
748#[cfg(test)]
749mod tests {
750    use std::io::ErrorKind;
751    use std::io::Read;
752    use std::os::unix::net::UnixListener;
753    use std::os::unix::net::UnixStream;
754    use std::sync::atomic::AtomicU64;
755    use std::sync::atomic::Ordering;
756    use std::sync::mpsc;
757    use std::time::Duration;
758    use std::time::Instant;
759    use std::time::SystemTime;
760
761    use monarch_record_batch::RecordBatchBuffer;
762    use monarch_telemetry_schema::metric_tables::MetricGauge;
763    use monarch_telemetry_schema::metric_tables::MetricHistogram;
764    use monarch_telemetry_schema::metric_tables::MetricSum;
765
766    use super::*;
767    use crate::ActorEvent;
768    use crate::ActorStatusEvent;
769    use crate::MeshEvent;
770    use crate::MessageEvent;
771    use crate::MessageStatusEvent;
772    use crate::SentMessageEvent;
773
774    static TEST_SEQ: AtomicU64 = AtomicU64::new(0);
775
776    struct Frame {
777        table_name: String,
778        payload: Vec<u8>,
779    }
780
781    fn socket_path(name: &str) -> PathBuf {
782        let seq = TEST_SEQ.fetch_add(1, Ordering::Relaxed);
783        let dir = std::env::temp_dir().join(format!(
784            "monarch_unix_socket_sink_{}_{}",
785            std::process::id(),
786            seq
787        ));
788        std::fs::create_dir_all(&dir).unwrap();
789        dir.join(name)
790    }
791
792    fn timestamp() -> SystemTime {
793        SystemTime::UNIX_EPOCH + Duration::from_micros(123)
794    }
795
796    fn fields() -> crate::trace_dispatcher::TraceFields {
797        let mut fields = crate::trace_dispatcher::TraceFields::new();
798        fields.push(("count", FieldValue::U64(3)));
799        fields
800    }
801
802    fn span() -> TraceEvent {
803        TraceEvent::NewSpan {
804            id: 7,
805            name: "test_span",
806            target: "test_target",
807            level: tracing::Level::INFO,
808            fields: fields(),
809            timestamp: timestamp(),
810            parent_id: None,
811            thread_name: "test_thread",
812            file: Some("test.rs"),
813            line: Some(42),
814        }
815    }
816
817    fn span_enter() -> TraceEvent {
818        TraceEvent::SpanEnter {
819            id: 7,
820            timestamp: timestamp(),
821            thread_name: "test_thread",
822        }
823    }
824
825    fn event() -> TraceEvent {
826        TraceEvent::Event {
827            name: "test_event",
828            target: "test_target",
829            level: tracing::Level::INFO,
830            fields: fields(),
831            timestamp: timestamp(),
832            parent_span: Some(7),
833            thread_id: "99",
834            thread_name: "test_thread",
835            module_path: Some("test_module"),
836            file: Some("test.rs"),
837            line: Some(43),
838        }
839    }
840
841    fn entity_events() -> Vec<TraceEvent> {
842        vec![
843            TraceEvent::Entity(EntityEvent::Actor(ActorEvent {
844                id: 1,
845                timestamp: timestamp(),
846                mesh_id: 2,
847                rank: 3,
848                full_name: "actor/full".to_string(),
849                display_name: Some("actor".to_string()),
850            })),
851            TraceEvent::Entity(EntityEvent::Mesh(MeshEvent {
852                id: 2,
853                timestamp: timestamp(),
854                class: "Host".to_string(),
855                given_name: "hosts".to_string(),
856                full_name: "hosts/full".to_string(),
857                shape_json: r#"{"dims":[1]}"#.to_string(),
858                parent_mesh_id: None,
859                parent_view_json: None,
860            })),
861            TraceEvent::Entity(EntityEvent::ActorStatus(ActorStatusEvent {
862                id: 3,
863                timestamp: timestamp(),
864                actor_id: 1,
865                new_status: "Running".to_string(),
866                reason: Some("test".to_string()),
867            })),
868            TraceEvent::Entity(EntityEvent::SentMessage(SentMessageEvent {
869                timestamp: timestamp(),
870                sender_actor_id: 1,
871                actor_mesh_id: 2,
872                view_json: r#"{"rank":0}"#.to_string(),
873                shape_json: r#"{"dims":[1]}"#.to_string(),
874            })),
875            TraceEvent::Entity(EntityEvent::Message(MessageEvent {
876                timestamp: timestamp(),
877                id: 4,
878                from_actor_id: 1,
879                to_actor_id: 5,
880                endpoint: Some("endpoint".to_string()),
881                port_index: Some(6),
882            })),
883            TraceEvent::Entity(EntityEvent::MessageStatus(MessageStatusEvent {
884                timestamp: timestamp(),
885                id: 7,
886                message_id: 4,
887                status: "complete".to_string(),
888            })),
889        ]
890    }
891
892    fn metric_gauge_buffer() -> MetricGaugeBuffer {
893        let mut buffer = MetricGaugeBuffer::default();
894        buffer.insert(MetricGauge {
895            name: "load".to_string(),
896            timestamp_us: 123,
897            start_timestamp_us: None,
898            scope_name: "test".to_string(),
899            unit: "1".to_string(),
900            attributes_json: "{}".to_string(),
901            resource_attributes_json: "{}".to_string(),
902            value_f64: Some(1.5),
903            value_i64: None,
904            value_u64: None,
905        });
906        buffer
907    }
908
909    fn metric_sum_buffer() -> MetricSumBuffer {
910        let mut buffer = MetricSumBuffer::default();
911        buffer.insert(MetricSum {
912            name: "mailbox.posts".to_string(),
913            timestamp_us: 123,
914            start_timestamp_us: 100,
915            scope_name: "hyperactor::mailbox".to_string(),
916            unit: "1".to_string(),
917            temporality: "delta".to_string(),
918            is_monotonic: true,
919            attributes_json: r#"{"actor_id":"actor"}"#.to_string(),
920            resource_attributes_json: r#"{"execution_id":"test"}"#.to_string(),
921            sum_f64: None,
922            sum_i64: None,
923            sum_u64: Some(3),
924        });
925        buffer
926    }
927
928    fn metric_histogram_buffer() -> MetricHistogramBuffer {
929        let mut buffer = MetricHistogramBuffer::default();
930        buffer.insert(MetricHistogram {
931            name: "latency".to_string(),
932            timestamp_us: 123,
933            start_timestamp_us: 100,
934            scope_name: "test".to_string(),
935            unit: "ms".to_string(),
936            temporality: "delta".to_string(),
937            attributes_json: "{}".to_string(),
938            resource_attributes_json: "{}".to_string(),
939            count: 1,
940            sum_f64: Some(5.0),
941            sum_i64: None,
942            sum_u64: None,
943            min_f64: Some(5.0),
944            min_i64: None,
945            min_u64: None,
946            max_f64: Some(5.0),
947            max_i64: None,
948            max_u64: None,
949            bounds_json: "[10.0]".to_string(),
950            bucket_counts_json: "[1,0]".to_string(),
951        });
952        buffer
953    }
954
955    fn read_frame(stream: &mut UnixStream) -> Frame {
956        let mut name_len_bytes = [0; 2];
957        stream.read_exact(&mut name_len_bytes).unwrap();
958        let name_len = u16::from_be_bytes(name_len_bytes) as usize;
959
960        let mut name_bytes = vec![0; name_len];
961        stream.read_exact(&mut name_bytes).unwrap();
962        let table_name = String::from_utf8(name_bytes).unwrap();
963
964        let mut payload_len_bytes = [0; 4];
965        stream.read_exact(&mut payload_len_bytes).unwrap();
966        let payload_len = u32::from_be_bytes(payload_len_bytes) as usize;
967        assert!((1..=MAX_FRAME_LEN).contains(&payload_len));
968
969        let mut payload = vec![0; payload_len];
970        stream.read_exact(&mut payload).unwrap();
971        Frame {
972            table_name,
973            payload,
974        }
975    }
976
977    fn read_frame_tables(stream: UnixStream, count: usize) -> Vec<String> {
978        read_frames(stream, count)
979            .into_iter()
980            .map(|frame| frame.table_name)
981            .collect()
982    }
983
984    fn read_frames(mut stream: UnixStream, count: usize) -> Vec<Frame> {
985        stream
986            .set_read_timeout(Some(Duration::from_secs(5)))
987            .unwrap();
988
989        (0..count).map(|_| read_frame(&mut stream)).collect()
990    }
991
992    fn assert_no_frame_available(stream: &mut UnixStream) {
993        stream
994            .set_read_timeout(Some(Duration::from_millis(500)))
995            .unwrap();
996        let mut name_len_bytes = [0; 2];
997        match stream.read_exact(&mut name_len_bytes) {
998            Ok(()) => panic!("unexpected extra telemetry frame"),
999            Err(error) if matches!(error.kind(), ErrorKind::WouldBlock | ErrorKind::TimedOut) => {}
1000            Err(error) => panic!("unexpected read error: {error}"),
1001        }
1002    }
1003
1004    fn wait_for_dropped_frames(sink: &UnixSocketSink, expected: u64) {
1005        let deadline = Instant::now() + Duration::from_secs(5);
1006        while Instant::now() < deadline {
1007            if sink.dropped_frames() == expected {
1008                return;
1009            }
1010            std::thread::sleep(Duration::from_millis(10));
1011        }
1012        assert_eq!(sink.dropped_frames(), expected);
1013    }
1014
1015    fn assert_frame_batch<B>(frame: &Frame)
1016    where
1017        B: RecordBatchBuffer + Default,
1018    {
1019        let batch = monarch_telemetry_schema::deserialize_one_batch(&frame.payload).unwrap();
1020        let expected = B::default().drain_to_record_batch().unwrap();
1021
1022        assert_eq!(batch.num_rows(), 1);
1023        assert_eq!(batch.schema(), expected.schema());
1024    }
1025
1026    fn assert_entity_frame_schema(frame: &Frame) {
1027        match frame.table_name.as_str() {
1028            ACTORS => assert_frame_batch::<ActorBuffer>(frame),
1029            MESHES => assert_frame_batch::<MeshBuffer>(frame),
1030            ACTOR_STATUS_EVENTS => assert_frame_batch::<ActorStatusEventBuffer>(frame),
1031            SENT_MESSAGES => assert_frame_batch::<SentMessageBuffer>(frame),
1032            MESSAGES => assert_frame_batch::<MessageBuffer>(frame),
1033            MESSAGE_STATUS_EVENTS => assert_frame_batch::<MessageStatusEventBuffer>(frame),
1034            other => panic!("unexpected entity table frame: {other}"),
1035        }
1036    }
1037
1038    #[test]
1039    fn inactive_flush_discards_buffered_events_without_counting_drops() {
1040        let sink = UnixSocketSink::new();
1041
1042        sink.consume_shared(&span()).unwrap();
1043        sink.consume_shared(&span_enter()).unwrap();
1044        sink.consume_shared(&event()).unwrap();
1045        for event in entity_events() {
1046            sink.consume_shared(&event).unwrap();
1047        }
1048
1049        {
1050            let inner = sink.inner.lock().unwrap();
1051            assert_eq!(inner.spans_buffer.len(), 1);
1052            assert_eq!(inner.span_events_buffer.len(), 1);
1053            assert_eq!(inner.events_buffer.len(), 1);
1054            assert_eq!(inner.actors_buffer.len(), 1);
1055            assert_eq!(inner.meshes_buffer.len(), 1);
1056            assert_eq!(inner.actor_status_events_buffer.len(), 1);
1057            assert_eq!(inner.sent_messages_buffer.len(), 1);
1058            assert_eq!(inner.messages_buffer.len(), 1);
1059            assert_eq!(inner.message_status_events_buffer.len(), 1);
1060        }
1061
1062        sink.flush_shared().unwrap();
1063
1064        {
1065            let inner = sink.inner.lock().unwrap();
1066            assert_eq!(inner.spans_buffer.len(), 0);
1067            assert_eq!(inner.span_events_buffer.len(), 0);
1068            assert_eq!(inner.events_buffer.len(), 0);
1069            assert_eq!(inner.actors_buffer.len(), 0);
1070            assert_eq!(inner.meshes_buffer.len(), 0);
1071            assert_eq!(inner.actor_status_events_buffer.len(), 0);
1072            assert_eq!(inner.sent_messages_buffer.len(), 0);
1073            assert_eq!(inner.messages_buffer.len(), 0);
1074            assert_eq!(inner.message_status_events_buffer.len(), 0);
1075        }
1076        assert!(!sink.is_active());
1077        assert_eq!(sink.dropped_frames(), 0);
1078    }
1079
1080    #[test]
1081    fn set_path_is_idempotent_for_same_path() {
1082        let path = socket_path("same.sock");
1083        let sink = UnixSocketSink::new();
1084
1085        sink.set_path(path.clone()).unwrap();
1086        sink.set_path(path).unwrap();
1087    }
1088
1089    #[test]
1090    fn set_path_rejects_different_path_after_activation() {
1091        let sink = UnixSocketSink::new();
1092
1093        sink.set_path(socket_path("first.sock")).unwrap();
1094
1095        assert!(sink.set_path(socket_path("second.sock")).is_err());
1096    }
1097
1098    #[test]
1099    fn active_flush_sends_one_frame_per_non_empty_trace_table() {
1100        let path = socket_path("telemetry.sock");
1101        let listener = UnixListener::bind(&path).unwrap();
1102        let (sender, receiver) = mpsc::channel();
1103        let read_handle = std::thread::spawn(move || {
1104            let (stream, _addr) = listener.accept().unwrap();
1105            sender.send(read_frame_tables(stream, 3)).unwrap();
1106        });
1107
1108        let sink = UnixSocketSink::new();
1109        sink.set_path(path).unwrap();
1110        sink.consume_shared(&span()).unwrap();
1111        sink.consume_shared(&span_enter()).unwrap();
1112        sink.consume_shared(&event()).unwrap();
1113
1114        sink.flush_shared().unwrap();
1115
1116        let tables = receiver.recv_timeout(Duration::from_secs(5)).unwrap();
1117        read_handle.join().unwrap();
1118        assert_eq!(
1119            tables,
1120            vec![
1121                SPANS.to_string(),
1122                SPAN_EVENTS.to_string(),
1123                EVENTS.to_string()
1124            ]
1125        );
1126        assert_eq!(sink.dropped_frames(), 0);
1127    }
1128
1129    #[test]
1130    fn active_flush_sends_one_frame_per_non_empty_entity_table() {
1131        let path = socket_path("entities.sock");
1132        let listener = UnixListener::bind(&path).unwrap();
1133        let (sender, receiver) = mpsc::channel();
1134        let read_handle = std::thread::spawn(move || {
1135            let (stream, _addr) = listener.accept().unwrap();
1136            sender.send(read_frames(stream, 6)).unwrap();
1137        });
1138
1139        let sink = UnixSocketSink::new();
1140        sink.set_path(path).unwrap();
1141        for event in entity_events() {
1142            sink.consume_shared(&event).unwrap();
1143        }
1144
1145        sink.flush_shared().unwrap();
1146
1147        let frames = receiver.recv_timeout(Duration::from_secs(5)).unwrap();
1148        read_handle.join().unwrap();
1149        assert_eq!(
1150            frames
1151                .iter()
1152                .map(|frame| frame.table_name.as_str())
1153                .collect::<Vec<_>>(),
1154            vec![
1155                ACTORS,
1156                MESHES,
1157                ACTOR_STATUS_EVENTS,
1158                SENT_MESSAGES,
1159                MESSAGES,
1160                MESSAGE_STATUS_EVENTS,
1161            ]
1162        );
1163        frames.iter().for_each(assert_entity_frame_schema);
1164        assert_eq!(sink.dropped_frames(), 0);
1165    }
1166
1167    #[test]
1168    fn active_sink_sends_metric_frames() {
1169        let path = socket_path("metrics.sock");
1170        let listener = UnixListener::bind(&path).unwrap();
1171        let (sender, receiver) = mpsc::channel();
1172        let read_handle = std::thread::spawn(move || {
1173            let (stream, _addr) = listener.accept().unwrap();
1174            sender.send(read_frames(stream, 3)).unwrap();
1175        });
1176
1177        let sink = UnixSocketSink::new();
1178        sink.set_path(path).unwrap();
1179        sink.send_metric_buffers(
1180            metric_gauge_buffer(),
1181            metric_sum_buffer(),
1182            metric_histogram_buffer(),
1183        );
1184
1185        let frames = receiver.recv_timeout(Duration::from_secs(5)).unwrap();
1186        read_handle.join().unwrap();
1187        assert_eq!(frames[0].table_name, METRIC_GAUGES);
1188        assert_frame_batch::<MetricGaugeBuffer>(&frames[0]);
1189        assert_eq!(frames[1].table_name, METRIC_SUMS);
1190        assert_frame_batch::<MetricSumBuffer>(&frames[1]);
1191        assert_eq!(frames[2].table_name, METRIC_HISTOGRAMS);
1192        assert_frame_batch::<MetricHistogramBuffer>(&frames[2]);
1193        assert_eq!(sink.dropped_frames(), 0);
1194    }
1195
1196    #[test]
1197    fn poisoned_sink_counts_dropped_metric_frames() {
1198        let sink = Arc::new(UnixSocketSink::new());
1199        let poison_sink = Arc::clone(&sink);
1200        assert!(
1201            std::thread::spawn(move || {
1202                let _guard = poison_sink.inner.lock().unwrap();
1203                panic!("poison metric sink lock");
1204            })
1205            .join()
1206            .is_err()
1207        );
1208
1209        sink.send_metric_buffers(
1210            metric_gauge_buffer(),
1211            metric_sum_buffer(),
1212            metric_histogram_buffer(),
1213        );
1214
1215        assert_eq!(sink.dropped_frames(), 3);
1216    }
1217
1218    #[test]
1219    fn active_flush_omits_empty_trace_tables() {
1220        let path = socket_path("events_only.sock");
1221        let listener = UnixListener::bind(&path).unwrap();
1222        let (sender, receiver) = mpsc::channel();
1223        let read_handle = std::thread::spawn(move || {
1224            let (mut stream, _addr) = listener.accept().unwrap();
1225            stream
1226                .set_read_timeout(Some(Duration::from_secs(5)))
1227                .unwrap();
1228            let table_name = read_frame(&mut stream).table_name;
1229            assert_no_frame_available(&mut stream);
1230            sender.send(table_name).unwrap();
1231        });
1232
1233        let sink = UnixSocketSink::new();
1234        sink.set_path(path).unwrap();
1235        sink.consume_shared(&event()).unwrap();
1236
1237        sink.flush_shared().unwrap();
1238
1239        let table = receiver.recv_timeout(Duration::from_secs(5)).unwrap();
1240        read_handle.join().unwrap();
1241        assert_eq!(table, EVENTS);
1242        assert_eq!(sink.dropped_frames(), 0);
1243    }
1244
1245    #[test]
1246    fn flush_counts_missing_sidecar_connection_as_dropped_frame() {
1247        let sink = UnixSocketSink::new();
1248        sink.set_path(socket_path("missing.sock")).unwrap();
1249        sink.consume_shared(&event()).unwrap();
1250
1251        sink.flush_shared().unwrap();
1252
1253        wait_for_dropped_frames(&sink, 1);
1254    }
1255}