1pub mod install;
2
3use crate::{
4 cdk::types::Principal,
5 dto::{
6 error::Error,
7 runtime::{
8 CanicHealthStatus, CanicReadinessStatus, CanicRuntimeStatus, CanicTimerStatus,
9 FailureSeverity, RUNTIME_INTROSPECTION_SCHEMA_VERSION, ReadinessStatus,
10 RuntimeBuildInfo, RuntimeCheck, RuntimeCheckStatus, RuntimeDiagnostic,
11 RuntimeDiagnosticSeverity, RuntimeFieldVisibility, RuntimeStateDomainStatus,
12 RuntimeStateDomainSummary, RuntimeStateSummary, RuntimeStatus, RuntimeTopologyStatus,
13 RuntimeVisibilityEntry, TimerStatus,
14 },
15 },
16 ops::{
17 ic::IcOps,
18 runtime::{
19 env::EnvOps,
20 memory::MemoryRegistryOps,
21 metrics::timer::TimerMetrics,
22 ready::ReadyOps,
23 recent_failure::{RecentFailureInput, RecentFailureOps},
24 },
25 },
26 state_contract::{StateStorage, canic_state_manifest_for_role},
27};
28
29pub struct MemoryRuntimeApi;
34
35impl MemoryRuntimeApi {
36 pub fn bootstrap_registry() -> Result<(), Error> {
38 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
39
40 Ok(())
41 }
42}
43
44pub struct RuntimeIntrospectionApi;
49
50impl RuntimeIntrospectionApi {
51 pub fn record_recent_failure(
53 occurred_at_ns: u64,
54 subsystem: impl Into<String>,
55 code: impl Into<String>,
56 severity: FailureSeverity,
57 summary: impl Into<String>,
58 correlation_id: Option<String>,
59 ) {
60 RecentFailureOps::record(RecentFailureInput {
61 occurred_at_ns,
62 subsystem: subsystem.into(),
63 code: code.into(),
64 severity,
65 summary: summary.into(),
66 correlation_id,
67 });
68 }
69
70 #[must_use]
72 pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
73 CanicHealthStatus {
74 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
75 status: crate::dto::runtime::HealthStatus::Healthy,
76 observed_at_ns,
77 checks: vec![RuntimeCheck {
78 category: "health".to_string(),
79 code: "canister_responsive".to_string(),
80 status: RuntimeCheckStatus::Pass,
81 subject: "canister".to_string(),
82 detail: "canister returned a health response".to_string(),
83 next: None,
84 source: "runtime_observed".to_string(),
85 }],
86 }
87 }
88
89 #[must_use]
91 pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
92 let ready = ReadyOps::is_ready();
93 let role = EnvOps::canister_role()
94 .ok()
95 .map(crate::ids::CanisterRole::into_string);
96
97 let (status, check_status, detail, next) = if ready {
98 (
99 ReadinessStatus::Ready,
100 RuntimeCheckStatus::Pass,
101 "runtime readiness barrier is marked ready",
102 None,
103 )
104 } else {
105 (
106 ReadinessStatus::NotReady,
107 RuntimeCheckStatus::Fail,
108 "runtime readiness barrier is not ready",
109 Some("wait for bootstrap to complete or inspect canic_bootstrap_status"),
110 )
111 };
112
113 let readiness_check = RuntimeCheck {
114 category: "readiness".to_string(),
115 code: "runtime_ready_barrier".to_string(),
116 status: check_status,
117 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
118 detail: detail.to_string(),
119 next: next.map(str::to_string),
120 source: "runtime_observed".to_string(),
121 };
122
123 let blockers = if ready {
124 Vec::new()
125 } else {
126 vec![RuntimeDiagnostic {
127 category: "readiness".to_string(),
128 code: "runtime_not_ready".to_string(),
129 severity: RuntimeDiagnosticSeverity::Blocked,
130 subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
131 detail: "runtime readiness barrier has not completed".to_string(),
132 next: Some(
133 "inspect bootstrap status before treating the role as ready".to_string(),
134 ),
135 source: "runtime_observed".to_string(),
136 }]
137 };
138
139 CanicReadinessStatus {
140 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
141 role,
142 status,
143 observed_at_ns,
144 checks: vec![readiness_check],
145 blockers,
146 warnings: Vec::new(),
147 }
148 }
149
150 #[must_use]
152 pub fn runtime_status_for(
153 canister_id: Principal,
154 observed_at_ns: u64,
155 package_name: &str,
156 package_version: &str,
157 canic_version: &str,
158 canister_version: u64,
159 ) -> CanicRuntimeStatus {
160 let readiness = Self::readiness(observed_at_ns);
161 let role = readiness.role.clone();
162 let state = state_summary(role.as_deref());
163 let root = EnvOps::root_pid().ok();
164 let parent = EnvOps::parent_pid().ok();
165 let subnet = EnvOps::subnet_pid().ok();
166 let status = match readiness.status {
167 ReadinessStatus::Ready => RuntimeStatus::Ok,
168 ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated => RuntimeStatus::Degraded,
169 ReadinessStatus::NotReady => RuntimeStatus::Failing,
170 };
171
172 CanicRuntimeStatus {
173 schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
174 observed_at_ns,
175 canister_id,
176 role,
177 root,
178 network: None,
179 build: RuntimeBuildInfo {
180 package_name: package_name.to_string(),
181 package_version: package_version.to_string(),
182 canic_version: canic_version.to_string(),
183 canister_version,
184 },
185 features: Vec::new(),
186 topology: Some(RuntimeTopologyStatus {
187 root,
188 parent,
189 subnet,
190 source: "runtime_observed".to_string(),
191 }),
192 timers: timer_statuses(),
193 state,
194 recent_failures: RecentFailureOps::snapshot(),
195 visibility: runtime_visibility(),
196 readiness,
197 status,
198 }
199 }
200
201 #[must_use]
203 pub fn runtime_status(
204 observed_at_ns: u64,
205 package_name: &str,
206 package_version: &str,
207 canic_version: &str,
208 canister_version: u64,
209 ) -> CanicRuntimeStatus {
210 Self::runtime_status_for(
211 IcOps::canister_self(),
212 observed_at_ns,
213 package_name,
214 package_version,
215 canic_version,
216 canister_version,
217 )
218 }
219}
220
221fn timer_statuses() -> Vec<CanicTimerStatus> {
222 let mut timers = TimerMetrics::snapshot()
223 .entries
224 .into_iter()
225 .map(|(key, ticks)| {
226 let (subsystem, name) = split_timer_label(&key.label);
227 CanicTimerStatus {
228 name,
229 subsystem,
230 status: if ticks > 0 {
231 TimerStatus::Healthy
232 } else {
233 TimerStatus::Unknown
234 },
235 enabled: true,
236 registered: true,
237 last_success_at_ns: None,
238 last_failure_at_ns: None,
239 next_due_at_ns: None,
240 consecutive_failures: 0,
241 last_error_code: None,
242 last_error_summary: None,
243 }
244 })
245 .collect::<Vec<_>>();
246 timers.sort_by(|left, right| {
247 left.subsystem
248 .cmp(&right.subsystem)
249 .then_with(|| left.name.cmp(&right.name))
250 });
251 timers
252}
253
254fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
255 let role = role?;
256 let manifest = canic_state_manifest_for_role(Some(role));
257 let domains = manifest
258 .roles
259 .into_iter()
260 .flat_map(|role| role.state)
261 .map(|domain| RuntimeStateDomainSummary {
262 domain: domain.domain,
263 version: domain.version,
264 storage: state_storage_name(domain.storage).to_string(),
265 memory_id: domain.memory_id,
266 status: RuntimeStateDomainStatus::Ok,
267 })
268 .collect::<Vec<_>>();
269
270 if domains.is_empty() {
271 return None;
272 }
273
274 Some(RuntimeStateSummary {
275 manifest_schema_version: u32::from(manifest.schema_version),
276 domains,
277 total_stable_memory_pages: None,
278 })
279}
280
281const fn state_storage_name(storage: StateStorage) -> &'static str {
282 match storage {
283 StateStorage::StableMemory => "stable_memory",
284 StateStorage::HeapOnly => "heap_only",
285 StateStorage::NotApplicable => "not_applicable",
286 }
287}
288
289fn split_timer_label(label: &str) -> (String, String) {
290 label.split_once(':').map_or_else(
291 || ("runtime".to_string(), label.to_string()),
292 |(subsystem, name)| (subsystem.to_string(), name.to_string()),
293 )
294}
295
296fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
297 vec![
298 RuntimeVisibilityEntry {
299 field: "schema_version".to_string(),
300 visibility: RuntimeFieldVisibility::PublicSafe,
301 },
302 RuntimeVisibilityEntry {
303 field: "status".to_string(),
304 visibility: RuntimeFieldVisibility::OperatorOnly,
305 },
306 RuntimeVisibilityEntry {
307 field: "role".to_string(),
308 visibility: RuntimeFieldVisibility::OperatorOnly,
309 },
310 RuntimeVisibilityEntry {
311 field: "root".to_string(),
312 visibility: RuntimeFieldVisibility::OperatorOnly,
313 },
314 RuntimeVisibilityEntry {
315 field: "topology".to_string(),
316 visibility: RuntimeFieldVisibility::ControllerOnly,
317 },
318 RuntimeVisibilityEntry {
319 field: "timers".to_string(),
320 visibility: RuntimeFieldVisibility::OperatorOnly,
321 },
322 RuntimeVisibilityEntry {
323 field: "state".to_string(),
324 visibility: RuntimeFieldVisibility::OperatorOnly,
325 },
326 RuntimeVisibilityEntry {
327 field: "recent_failures".to_string(),
328 visibility: RuntimeFieldVisibility::OperatorOnly,
329 },
330 ]
331}
332
333#[cfg(test)]
334mod tests {
335 use super::*;
336 use crate::ops::runtime::metrics::timer::{TimerMetrics, TimerMode};
337 use crate::ops::runtime::recent_failure::RecentFailureOps;
338 use std::time::Duration;
339
340 #[test]
341 fn health_is_minimal_and_schema_versioned() {
342 let health = RuntimeIntrospectionApi::health(Some(42));
343
344 assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
345 assert_eq!(health.status, crate::dto::runtime::HealthStatus::Healthy);
346 assert_eq!(health.observed_at_ns, Some(42));
347 assert_eq!(health.checks.len(), 1);
348 assert_eq!(health.checks[0].code, "canister_responsive");
349 }
350
351 #[test]
352 fn runtime_status_embeds_guarded_readiness_and_build_info() {
353 let status = RuntimeIntrospectionApi::runtime_status_for(
354 Principal::anonymous(),
355 100,
356 "test-canister",
357 "1.2.3",
358 "0.81.0",
359 7,
360 );
361
362 assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
363 assert_eq!(status.observed_at_ns, 100);
364 assert_eq!(status.canister_id, Principal::anonymous());
365 assert_eq!(status.build.package_name, "test-canister");
366 assert_eq!(status.build.package_version, "1.2.3");
367 assert_eq!(status.build.canic_version, "0.81.0");
368 assert_eq!(status.build.canister_version, 7);
369 assert_eq!(status.readiness.observed_at_ns, 100);
370 assert!(
371 status
372 .visibility
373 .iter()
374 .any(|entry| entry.field == "topology"
375 && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
376 );
377 }
378
379 #[test]
380 fn runtime_status_projects_registered_timer_metrics() {
381 TimerMetrics::reset();
382 TimerMetrics::record_timer_scheduled(
383 TimerMode::Interval,
384 Duration::from_mins(1),
385 "cycles:interval",
386 );
387 TimerMetrics::record_timer_scheduled(
388 TimerMode::Once,
389 Duration::from_secs(1),
390 "auth_renewal:init",
391 );
392 TimerMetrics::record_timer_tick(
393 TimerMode::Once,
394 Duration::from_secs(1),
395 "auth_renewal:init",
396 );
397
398 let status = RuntimeIntrospectionApi::runtime_status_for(
399 Principal::anonymous(),
400 100,
401 "test-canister",
402 "1.2.3",
403 "0.81.0",
404 7,
405 );
406
407 assert_eq!(status.timers.len(), 2);
408 assert_eq!(status.timers[0].subsystem, "auth_renewal");
409 assert_eq!(status.timers[0].name, "init");
410 assert_eq!(status.timers[0].status, TimerStatus::Healthy);
411 assert_eq!(status.timers[1].subsystem, "cycles");
412 assert_eq!(status.timers[1].name, "interval");
413 assert_eq!(status.timers[1].status, TimerStatus::Unknown);
414
415 TimerMetrics::reset();
416 }
417
418 #[test]
419 fn state_summary_uses_declared_metadata_without_value_counts() {
420 let summary = state_summary(Some("root")).expect("root state declarations");
421
422 assert_eq!(
423 summary.manifest_schema_version,
424 u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
425 );
426 assert!(summary.total_stable_memory_pages.is_none());
427 assert!(summary.domains.iter().any(|domain| {
428 domain.domain == "env"
429 && domain.storage == "stable_memory"
430 && domain.status == RuntimeStateDomainStatus::Ok
431 }));
432 assert!(state_summary(Some("unknown_role")).is_none());
433 assert!(state_summary(None).is_none());
434 }
435
436 #[test]
437 fn runtime_status_includes_recent_failure_snapshot() {
438 RecentFailureOps::reset();
439 RuntimeIntrospectionApi::record_recent_failure(
440 77,
441 "runtime",
442 "readiness_failed",
443 FailureSeverity::Error,
444 "bounded failure summary",
445 Some("runtime-check".to_string()),
446 );
447
448 let status = RuntimeIntrospectionApi::runtime_status_for(
449 Principal::anonymous(),
450 100,
451 "test-canister",
452 "1.2.3",
453 "0.81.0",
454 7,
455 );
456
457 assert_eq!(status.recent_failures.len(), 1);
458 assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
459 assert_eq!(status.recent_failures[0].subsystem, "runtime");
460 assert_eq!(status.recent_failures[0].code, "readiness_failed");
461
462 RecentFailureOps::reset();
463 }
464}