1#[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) {}