Skip to main content

canic_core/api/runtime/
mod.rs

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
29///
30/// MemoryRuntimeApi
31///
32
33pub struct MemoryRuntimeApi;
34
35impl MemoryRuntimeApi {
36    /// Bootstrap Canic's stable-memory declaration snapshot.
37    pub fn bootstrap_registry() -> Result<(), Error> {
38        MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
39
40        Ok(())
41    }
42}
43
44///
45/// RuntimeIntrospectionApi
46///
47
48pub struct RuntimeIntrospectionApi;
49
50impl RuntimeIntrospectionApi {
51    /// Record one heap-only recent-failure summary for guarded runtime status.
52    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    /// Return the minimal health status for a canister that answered the query.
71    #[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    /// Return guarded readiness status for the local Canic role.
90    #[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    /// Return guarded runtime status for the local Canic role.
151    #[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    /// Return guarded runtime status using ambient IC runtime values.
202    #[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}