"""Cython bindings for RayEventRecorder. This module provides Python access to the C++ RayEventRecorder for emitting internal Ray events from Python code (e.g., submission job events). """ from ray.includes.event_recorder cimport ( CRayEventInterface, CPythonEventRecorder, CreatePythonRayEvent, ) from ray.includes.common cimport move from libc.stdint cimport int64_t from libcpp.memory cimport unique_ptr from libcpp.vector cimport vector as c_vector from libcpp.string cimport string as c_string import logging import threading logger = logging.getLogger(__name__) cdef class RayEvent: """Python wrapper holding event data for transfer to C++. This class stores event metadata and serialized protobuf data. When added to the EventRecorder, it creates the underlying C++ RayEvent object. Args: source_type: Integer value of RayEvent.SourceType enum. event_type: Integer value of RayEvent.EventType enum. severity: Integer value of RayEvent.Severity enum. entity_id: Unique identifier for the event entity (e.g., submission_id). message: Optional message associated with the event. session_name: The Ray session name. serialized_data: Serialized protobuf bytes of the nested event message. nested_event_field_number: The field number in RayEvent proto for the nested event message. Use RayEventProto._FIELD_NUMBER constants. event_id: Optional explicit event id bytes. Pass empty bytes to let the C++ layer generate a random id (matching the convention used by the other RayEventInterface subclasses). A non-empty value is preserved as-is — use this to reuse an id from an upstream source (e.g., a Kubernetes event uid). timestamp_ns: Optional explicit event timestamp in nanoseconds since the unix epoch. Pass 0 to let the C++ layer capture the time at construction via absl::Now(). """ cdef: int _source_type int _event_type int _severity str _entity_id str _message str _session_name bytes _serialized_data int _nested_event_field_number bytes _event_id int64_t _timestamp_ns def __init__( self, int source_type, int event_type, int severity, str entity_id, str message, str session_name, bytes serialized_data, int nested_event_field_number, bytes event_id = b"", int64_t timestamp_ns = 0, ): self._source_type = source_type self._event_type = event_type self._severity = severity self._entity_id = entity_id self._message = message self._session_name = session_name self._serialized_data = serialized_data self._nested_event_field_number = nested_event_field_number self._event_id = event_id self._timestamp_ns = timestamp_ns cdef unique_ptr[CRayEventInterface] to_cpp_event(self): """Create the underlying C++ event. Ownership is transferred to caller.""" return CreatePythonRayEvent( self._source_type, self._event_type, self._severity, self._entity_id.encode("utf-8"), self._message.encode("utf-8"), self._session_name.encode("utf-8"), self._serialized_data, self._nested_event_field_number, self._event_id, self._timestamp_ns, ) @property def entity_id(self): return self._entity_id @property def event_type(self): return self._event_type # module-level Singleton instance, lazily created by EventRecorder.initialize(). _event_recorder_instance = None # Guards singleton lifecycle and emission against concurrent shutdown/initialize. _event_recorder_lock = threading.RLock() cdef class EventRecorder: """Per-process singleton for recording Ray events.""" cdef unique_ptr[CPythonEventRecorder] _recorder def __dealloc__(self): """Safety-net cleanup. C++ destructor also calls Shutdown().""" if self._recorder.get() != NULL: self._recorder.get().Shutdown() self._recorder.reset() @staticmethod def initialize( int aggregator_port, str node_ip, str node_id_hex, size_t max_buffer_size, str metric_source = "python", ): """Initialize the per-process event recorder. Creates the underlying C++ PythonEventRecorder with a background I/O thread and gRPC client. No-op if already initialized. Args: aggregator_port: Port of the event aggregator server (bound on 127.0.0.1). node_ip: IP address of the current node. node_id_hex: Hex-encoded node ID. max_buffer_size: Maximum number of events to buffer. metric_source: Label for the "Source" tag on dropped-events metrics (default "python"). """ global _event_recorder_instance cdef EventRecorder rec with _event_recorder_lock: if _event_recorder_instance is not None: return rec = EventRecorder() rec._recorder.reset( new CPythonEventRecorder( aggregator_port, node_ip.encode("utf-8"), node_id_hex.encode("utf-8"), max_buffer_size, metric_source.encode("utf-8"), ) ) _event_recorder_instance = rec @staticmethod def instance(): """Get the per-process EventRecorder singleton. Returns: The EventRecorder instance if initialized, None otherwise. """ with _event_recorder_lock: return _event_recorder_instance @staticmethod def shutdown(): """Shutdown the event recorder. Stops exporting events, performs a final flush, and releases all C++ resources. After this call, emit() and emit_batch() will be no-ops until initialize() is called again. """ global _event_recorder_instance cdef EventRecorder rec with _event_recorder_lock: if _event_recorder_instance is None: return rec = _event_recorder_instance if rec._recorder.get() != NULL: with nogil: rec._recorder.get().Shutdown() rec._recorder.reset() _event_recorder_instance = None @staticmethod def emit(RayEvent event): """Emit a single event. No-op if not initialized. Args: event: A RayEvent object (created via InternalEventBuilder.build()). Returns: True if the event was successfully queued, False otherwise. """ return EventRecorder.emit_batch([event]) @staticmethod def emit_batch(list events): """Emit multiple events. No-op if not initialized. Args: events: List of RayEvent objects to emit. Returns: True if events were successfully queued, False otherwise. """ cdef EventRecorder rec cdef c_vector[unique_ptr[CRayEventInterface]] cpp_events cdef RayEvent ev if not events: return True for ev in events: cpp_events.push_back(move(ev.to_cpp_event())) with _event_recorder_lock: if _event_recorder_instance is None: logger.debug( "Event recorder not initialized, dropping %d events", len(events), ) return False rec = _event_recorder_instance with nogil: rec._recorder.get().AddEvents(move(cpp_events)) return True