hyperstack_interpreter/
vm_metrics.rs

1//! VM metrics for OpenTelemetry integration.
2//!
3//! This module provides metrics recording functions that are always available.
4//! When the `otel` feature is disabled, all functions are no-ops.
5//! When enabled, metrics are recorded via OpenTelemetry.
6
7#[cfg(feature = "otel")]
8use opentelemetry::{
9    global,
10    metrics::{Counter, Gauge, Histogram},
11    KeyValue,
12};
13#[cfg(feature = "otel")]
14use std::sync::OnceLock;
15
16#[cfg(feature = "otel")]
17static VM_METRICS: OnceLock<VmMetrics> = OnceLock::new();
18
19#[cfg(feature = "otel")]
20pub struct VmMetrics {
21    pub state_table_entries: Gauge<i64>,
22    pub state_table_capacity: Gauge<i64>,
23    pub lookup_index_count: Gauge<i64>,
24    pub lookup_index_entries: Gauge<i64>,
25    pub temporal_index_count: Gauge<i64>,
26    pub temporal_index_entries: Gauge<i64>,
27    pub pda_reverse_lookup_count: Gauge<i64>,
28    pub pda_reverse_lookup_entries: Gauge<i64>,
29    pub version_tracker_entries: Gauge<i64>,
30    pub path_cache_size: Gauge<i64>,
31    pub pending_queue_updates: Gauge<i64>,
32    pub pending_queue_unique_pdas: Gauge<i64>,
33    pub pending_queue_memory_bytes: Gauge<i64>,
34    pub pending_queue_oldest_age: Histogram<f64>,
35    pub state_table_evictions: Counter<u64>,
36    pub state_table_at_capacity_events: Counter<u64>,
37    pub cleanup_pending_removed: Counter<u64>,
38    pub cleanup_temporal_removed: Counter<u64>,
39    pub path_cache_hits: Counter<u64>,
40    pub path_cache_misses: Counter<u64>,
41    pub lookup_index_hits: Counter<u64>,
42    pub lookup_index_misses: Counter<u64>,
43    pub pending_updates_queued: Counter<u64>,
44    pub pending_updates_flushed: Counter<u64>,
45    pub pending_updates_expired: Counter<u64>,
46}
47
48#[cfg(feature = "otel")]
49impl VmMetrics {
50    fn new() -> Self {
51        let meter = global::meter("hyperstack-interpreter");
52
53        Self {
54            state_table_entries: meter
55                .i64_gauge("hyperstack.vm.state_table.entries")
56                .with_description("Current entries in the state table")
57                .init(),
58            state_table_capacity: meter
59                .i64_gauge("hyperstack.vm.state_table.capacity")
60                .with_description("Maximum capacity of the state table")
61                .init(),
62            lookup_index_count: meter
63                .i64_gauge("hyperstack.vm.lookup_index.count")
64                .with_description("Number of lookup indexes")
65                .init(),
66            lookup_index_entries: meter
67                .i64_gauge("hyperstack.vm.lookup_index.entries")
68                .with_description("Total entries across lookup indexes")
69                .init(),
70            temporal_index_count: meter
71                .i64_gauge("hyperstack.vm.temporal_index.count")
72                .with_description("Number of temporal indexes")
73                .init(),
74            temporal_index_entries: meter
75                .i64_gauge("hyperstack.vm.temporal_index.entries")
76                .with_description("Total entries across temporal indexes")
77                .init(),
78            pda_reverse_lookup_count: meter
79                .i64_gauge("hyperstack.vm.pda_reverse_lookup.count")
80                .with_description("Number of PDA reverse lookup tables")
81                .init(),
82            pda_reverse_lookup_entries: meter
83                .i64_gauge("hyperstack.vm.pda_reverse_lookup.entries")
84                .with_description("Total entries across PDA reverse lookups")
85                .init(),
86            version_tracker_entries: meter
87                .i64_gauge("hyperstack.vm.version_tracker.entries")
88                .with_description("Entries in the version tracker")
89                .init(),
90            path_cache_size: meter
91                .i64_gauge("hyperstack.vm.path_cache.size")
92                .with_description("Size of the compiled path cache")
93                .init(),
94            pending_queue_updates: meter
95                .i64_gauge("hyperstack.vm.pending_queue.updates")
96                .with_description("Total pending updates in queue")
97                .init(),
98            pending_queue_unique_pdas: meter
99                .i64_gauge("hyperstack.vm.pending_queue.unique_pdas")
100                .with_description("Unique PDAs with pending updates")
101                .init(),
102            pending_queue_memory_bytes: meter
103                .i64_gauge("hyperstack.vm.pending_queue.memory_bytes")
104                .with_description("Estimated memory usage of pending queue")
105                .init(),
106            pending_queue_oldest_age: meter
107                .f64_histogram("hyperstack.vm.pending_queue.oldest_age_seconds")
108                .with_description("Age of oldest pending update in seconds")
109                .init(),
110            state_table_evictions: meter
111                .u64_counter("hyperstack.vm.state_table.evictions")
112                .with_description("State table LRU evictions")
113                .init(),
114            state_table_at_capacity_events: meter
115                .u64_counter("hyperstack.vm.state_table.at_capacity_events")
116                .with_description("State table at capacity events")
117                .init(),
118            cleanup_pending_removed: meter
119                .u64_counter("hyperstack.vm.cleanup.pending_removed")
120                .with_description("Pending updates removed during cleanup")
121                .init(),
122            cleanup_temporal_removed: meter
123                .u64_counter("hyperstack.vm.cleanup.temporal_removed")
124                .with_description("Temporal entries removed during cleanup")
125                .init(),
126            path_cache_hits: meter
127                .u64_counter("hyperstack.vm.path_cache.hits")
128                .with_description("Path cache hits")
129                .init(),
130            path_cache_misses: meter
131                .u64_counter("hyperstack.vm.path_cache.misses")
132                .with_description("Path cache misses")
133                .init(),
134            lookup_index_hits: meter
135                .u64_counter("hyperstack.vm.lookup_index.hits")
136                .with_description("Lookup index hits")
137                .init(),
138            lookup_index_misses: meter
139                .u64_counter("hyperstack.vm.lookup_index.misses")
140                .with_description("Lookup index misses")
141                .init(),
142            pending_updates_queued: meter
143                .u64_counter("hyperstack.vm.pending_updates.queued")
144                .with_description("Updates queued for later processing")
145                .init(),
146            pending_updates_flushed: meter
147                .u64_counter("hyperstack.vm.pending_updates.flushed")
148                .with_description("Queued updates flushed after PDA resolution")
149                .init(),
150            pending_updates_expired: meter
151                .u64_counter("hyperstack.vm.pending_updates.expired")
152                .with_description("Queued updates that expired")
153                .init(),
154        }
155    }
156}
157
158#[cfg(feature = "otel")]
159pub fn get_vm_metrics() -> &'static VmMetrics {
160    VM_METRICS.get_or_init(VmMetrics::new)
161}
162
163#[cfg(feature = "otel")]
164pub fn record_state_table_eviction(count: u64, entity: &str) {
165    get_vm_metrics()
166        .state_table_evictions
167        .add(count, &[KeyValue::new("entity", entity.to_string())]);
168}
169
170#[cfg(not(feature = "otel"))]
171#[inline]
172pub fn record_state_table_eviction(_count: u64, _entity: &str) {}
173
174#[cfg(feature = "otel")]
175pub fn record_state_table_at_capacity(entity: &str) {
176    get_vm_metrics()
177        .state_table_at_capacity_events
178        .add(1, &[KeyValue::new("entity", entity.to_string())]);
179}
180
181#[cfg(not(feature = "otel"))]
182#[inline]
183pub fn record_state_table_at_capacity(_entity: &str) {}
184
185#[cfg(feature = "otel")]
186pub fn record_cleanup(pending_removed: usize, temporal_removed: usize, entity: &str) {
187    let m = get_vm_metrics();
188    let attrs = &[KeyValue::new("entity", entity.to_string())];
189    m.cleanup_pending_removed.add(pending_removed as u64, attrs);
190    m.cleanup_temporal_removed
191        .add(temporal_removed as u64, attrs);
192}
193
194#[cfg(not(feature = "otel"))]
195#[inline]
196pub fn record_cleanup(_pending_removed: usize, _temporal_removed: usize, _entity: &str) {}
197
198#[cfg(feature = "otel")]
199pub fn record_path_cache_hit() {
200    get_vm_metrics().path_cache_hits.add(1, &[]);
201}
202
203#[cfg(not(feature = "otel"))]
204#[inline]
205pub fn record_path_cache_hit() {}
206
207#[cfg(feature = "otel")]
208pub fn record_path_cache_miss() {
209    get_vm_metrics().path_cache_misses.add(1, &[]);
210}
211
212#[cfg(not(feature = "otel"))]
213#[inline]
214pub fn record_path_cache_miss() {}
215
216#[cfg(feature = "otel")]
217pub fn record_lookup_index_hit(index_name: &str) {
218    get_vm_metrics()
219        .lookup_index_hits
220        .add(1, &[KeyValue::new("index", index_name.to_string())]);
221}
222
223#[cfg(not(feature = "otel"))]
224#[inline]
225pub fn record_lookup_index_hit(_index_name: &str) {}
226
227#[cfg(feature = "otel")]
228pub fn record_lookup_index_miss(index_name: &str) {
229    get_vm_metrics()
230        .lookup_index_misses
231        .add(1, &[KeyValue::new("index", index_name.to_string())]);
232}
233
234#[cfg(not(feature = "otel"))]
235#[inline]
236pub fn record_lookup_index_miss(_index_name: &str) {}
237
238#[cfg(feature = "otel")]
239pub fn record_pending_update_queued(entity: &str) {
240    get_vm_metrics()
241        .pending_updates_queued
242        .add(1, &[KeyValue::new("entity", entity.to_string())]);
243}
244
245#[cfg(not(feature = "otel"))]
246#[inline]
247pub fn record_pending_update_queued(_entity: &str) {}
248
249#[cfg(feature = "otel")]
250pub fn record_pending_updates_flushed(count: u64, entity: &str) {
251    get_vm_metrics()
252        .pending_updates_flushed
253        .add(count, &[KeyValue::new("entity", entity.to_string())]);
254}
255
256#[cfg(not(feature = "otel"))]
257#[inline]
258pub fn record_pending_updates_flushed(_count: u64, _entity: &str) {}
259
260#[cfg(feature = "otel")]
261pub fn record_pending_updates_expired(count: u64, entity: &str) {
262    get_vm_metrics()
263        .pending_updates_expired
264        .add(count, &[KeyValue::new("entity", entity.to_string())]);
265}
266
267#[cfg(not(feature = "otel"))]
268#[inline]
269pub fn record_pending_updates_expired(_count: u64, _entity: &str) {}
270
271#[cfg(feature = "otel")]
272pub fn record_memory_stats(stats: &crate::vm::VmMemoryStats, entity: &str) {
273    let m = get_vm_metrics();
274    let attrs = &[KeyValue::new("entity", entity.to_string())];
275
276    m.state_table_entries
277        .record(stats.state_table_entity_count as i64, attrs);
278    m.state_table_capacity
279        .record(stats.state_table_max_entries as i64, attrs);
280    m.lookup_index_count
281        .record(stats.lookup_index_count as i64, attrs);
282    m.lookup_index_entries
283        .record(stats.lookup_index_total_entries as i64, attrs);
284    m.temporal_index_count
285        .record(stats.temporal_index_count as i64, attrs);
286    m.temporal_index_entries
287        .record(stats.temporal_index_total_entries as i64, attrs);
288    m.pda_reverse_lookup_count
289        .record(stats.pda_reverse_lookup_count as i64, attrs);
290    m.pda_reverse_lookup_entries
291        .record(stats.pda_reverse_lookup_total_entries as i64, attrs);
292    m.version_tracker_entries
293        .record(stats.version_tracker_entries as i64, attrs);
294    m.path_cache_size
295        .record(stats.path_cache_size as i64, attrs);
296
297    if let Some(ref pq) = stats.pending_queue_stats {
298        m.pending_queue_updates
299            .record(pq.total_updates as i64, attrs);
300        m.pending_queue_unique_pdas
301            .record(pq.unique_pdas as i64, attrs);
302        m.pending_queue_memory_bytes
303            .record(pq.estimated_memory_bytes as i64, attrs);
304        m.pending_queue_oldest_age
305            .record(pq.oldest_age_seconds as f64, attrs);
306    }
307}
308
309#[cfg(not(feature = "otel"))]
310#[inline]
311pub fn record_memory_stats(_stats: &crate::vm::VmMemoryStats, _entity: &str) {}