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
29const MAX_TIMER_SUBSYSTEM_BYTES: usize = 64;
30const MAX_TIMER_NAME_BYTES: usize = 96;
31
32///
33/// MemoryRuntimeApi
34///
35
36pub struct MemoryRuntimeApi;
37
38impl MemoryRuntimeApi {
39    /// Bootstrap Canic's stable-memory declaration snapshot.
40    pub fn bootstrap_registry() -> Result<(), Error> {
41        MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
42
43        Ok(())
44    }
45}
46
47///
48/// RuntimeIntrospectionApi
49///
50
51pub struct RuntimeIntrospectionApi;
52
53impl RuntimeIntrospectionApi {
54    /// Record one heap-only recent-failure summary for guarded runtime status.
55    pub fn record_recent_failure(
56        occurred_at_ns: u64,
57        subsystem: impl Into<String>,
58        code: impl Into<String>,
59        severity: FailureSeverity,
60        summary: impl Into<String>,
61        correlation_id: Option<String>,
62    ) {
63        RecentFailureOps::record(RecentFailureInput {
64            occurred_at_ns,
65            subsystem: subsystem.into(),
66            code: code.into(),
67            severity,
68            summary: summary.into(),
69            correlation_id,
70        });
71    }
72
73    /// Return the minimal health status for a canister that answered the query.
74    #[must_use]
75    pub fn health(observed_at_ns: Option<u64>) -> CanicHealthStatus {
76        CanicHealthStatus {
77            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
78            status: crate::dto::runtime::HealthStatus::Healthy,
79            observed_at_ns,
80            checks: vec![RuntimeCheck {
81                category: "health".to_string(),
82                code: "canister_responsive".to_string(),
83                status: RuntimeCheckStatus::Pass,
84                subject: "canister".to_string(),
85                detail: "canister returned a health response".to_string(),
86                next: None,
87                source: "runtime_observed".to_string(),
88            }],
89        }
90    }
91
92    /// Return guarded readiness status for the local Canic role.
93    #[must_use]
94    pub fn readiness(observed_at_ns: u64) -> CanicReadinessStatus {
95        let ready = ReadyOps::is_ready();
96        let role = EnvOps::canister_role()
97            .ok()
98            .map(crate::ids::CanisterRole::into_string);
99
100        let (status, check_status, detail, next) = if ready {
101            (
102                ReadinessStatus::Ready,
103                RuntimeCheckStatus::Pass,
104                "runtime readiness barrier is marked ready",
105                None,
106            )
107        } else {
108            (
109                ReadinessStatus::NotReady,
110                RuntimeCheckStatus::Fail,
111                "runtime readiness barrier is not ready",
112                Some("wait for bootstrap to complete or inspect canic_bootstrap_status"),
113            )
114        };
115
116        let readiness_check = RuntimeCheck {
117            category: "readiness".to_string(),
118            code: "runtime_ready_barrier".to_string(),
119            status: check_status,
120            subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
121            detail: detail.to_string(),
122            next: next.map(str::to_string),
123            source: "runtime_observed".to_string(),
124        };
125
126        let blockers = if ready {
127            Vec::new()
128        } else {
129            vec![RuntimeDiagnostic {
130                category: "readiness".to_string(),
131                code: "runtime_not_ready".to_string(),
132                severity: RuntimeDiagnosticSeverity::Blocked,
133                subject: role.clone().unwrap_or_else(|| "unknown_role".to_string()),
134                detail: "runtime readiness barrier has not completed".to_string(),
135                next: Some(
136                    "inspect bootstrap status before treating the role as ready".to_string(),
137                ),
138                source: "runtime_observed".to_string(),
139            }]
140        };
141
142        CanicReadinessStatus {
143            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
144            role,
145            status,
146            observed_at_ns,
147            checks: vec![readiness_check],
148            blockers,
149            warnings: Vec::new(),
150        }
151    }
152
153    /// Return guarded runtime status for the local Canic role.
154    #[must_use]
155    pub fn runtime_status_for(
156        canister_id: Principal,
157        observed_at_ns: u64,
158        package_name: &str,
159        package_version: &str,
160        canic_version: &str,
161        canister_version: u64,
162    ) -> CanicRuntimeStatus {
163        let readiness = Self::readiness(observed_at_ns);
164        let role = readiness.role.clone();
165        let state = state_summary(role.as_deref());
166        let root = EnvOps::root_pid().ok();
167        let parent = EnvOps::parent_pid().ok();
168        let subnet = EnvOps::subnet_pid().ok();
169        let status = match readiness.status {
170            ReadinessStatus::Ready => RuntimeStatus::Ok,
171            ReadinessStatus::Degraded | ReadinessStatus::NotEvaluated => RuntimeStatus::Degraded,
172            ReadinessStatus::NotReady => RuntimeStatus::Failing,
173        };
174
175        CanicRuntimeStatus {
176            schema_version: RUNTIME_INTROSPECTION_SCHEMA_VERSION,
177            observed_at_ns,
178            canister_id,
179            role,
180            root,
181            network: None,
182            build: RuntimeBuildInfo {
183                package_name: package_name.to_string(),
184                package_version: package_version.to_string(),
185                canic_version: canic_version.to_string(),
186                canister_version,
187            },
188            features: Vec::new(),
189            topology: Some(RuntimeTopologyStatus {
190                root,
191                parent,
192                subnet,
193                source: "runtime_observed".to_string(),
194            }),
195            timers: timer_statuses(),
196            state,
197            recent_failures: RecentFailureOps::snapshot(),
198            visibility: runtime_visibility(),
199            readiness,
200            status,
201        }
202    }
203
204    /// Return guarded runtime status using ambient IC runtime values.
205    #[must_use]
206    pub fn runtime_status(
207        observed_at_ns: u64,
208        package_name: &str,
209        package_version: &str,
210        canic_version: &str,
211        canister_version: u64,
212    ) -> CanicRuntimeStatus {
213        Self::runtime_status_for(
214            IcOps::canister_self(),
215            observed_at_ns,
216            package_name,
217            package_version,
218            canic_version,
219            canister_version,
220        )
221    }
222}
223
224fn timer_statuses() -> Vec<CanicTimerStatus> {
225    let mut timers = TimerMetrics::snapshot()
226        .entries
227        .into_iter()
228        .map(|(key, ticks)| {
229            let (subsystem, name) = split_timer_label(&key.label);
230            CanicTimerStatus {
231                name,
232                subsystem,
233                status: if ticks > 0 {
234                    TimerStatus::Healthy
235                } else {
236                    TimerStatus::Unknown
237                },
238                enabled: true,
239                registered: true,
240                last_success_at_ns: None,
241                last_failure_at_ns: None,
242                next_due_at_ns: None,
243                consecutive_failures: 0,
244                last_error_code: None,
245                last_error_summary: None,
246            }
247        })
248        .collect::<Vec<_>>();
249    timers.sort_by(|left, right| {
250        left.subsystem
251            .cmp(&right.subsystem)
252            .then_with(|| left.name.cmp(&right.name))
253    });
254    timers
255}
256
257fn state_summary(role: Option<&str>) -> Option<RuntimeStateSummary> {
258    let role = role?;
259    let manifest = canic_state_manifest_for_role(Some(role));
260    let domains = manifest
261        .roles
262        .into_iter()
263        .flat_map(|role| role.state)
264        .map(|domain| RuntimeStateDomainSummary {
265            domain: domain.domain,
266            version: domain.version,
267            storage: state_storage_name(domain.storage).to_string(),
268            memory_id: domain.memory_id,
269            status: RuntimeStateDomainStatus::Ok,
270        })
271        .collect::<Vec<_>>();
272
273    if domains.is_empty() {
274        return None;
275    }
276
277    Some(RuntimeStateSummary {
278        manifest_schema_version: u32::from(manifest.schema_version),
279        domains,
280        total_stable_memory_pages: None,
281    })
282}
283
284const fn state_storage_name(storage: StateStorage) -> &'static str {
285    match storage {
286        StateStorage::StableMemory => "stable_memory",
287        StateStorage::HeapOnly => "heap_only",
288        StateStorage::NotApplicable => "not_applicable",
289    }
290}
291
292fn split_timer_label(label: &str) -> (String, String) {
293    label.split_once(':').map_or_else(
294        || {
295            (
296                "runtime".to_string(),
297                bounded_runtime_text(label, MAX_TIMER_NAME_BYTES),
298            )
299        },
300        |(subsystem, name)| {
301            (
302                bounded_runtime_text(subsystem, MAX_TIMER_SUBSYSTEM_BYTES),
303                bounded_runtime_text(name, MAX_TIMER_NAME_BYTES),
304            )
305        },
306    )
307}
308
309fn bounded_runtime_text(value: &str, max_bytes: usize) -> String {
310    let sanitized = value
311        .chars()
312        .map(|character| {
313            if character.is_control() {
314                ' '
315            } else {
316                character
317            }
318        })
319        .collect::<String>();
320
321    if sanitized.len() <= max_bytes {
322        return sanitized;
323    }
324
325    let mut end = 0;
326    for (index, character) in sanitized.char_indices() {
327        let next = index + character.len_utf8();
328        if next > max_bytes {
329            break;
330        }
331        end = next;
332    }
333
334    sanitized[..end].to_string()
335}
336
337fn runtime_visibility() -> Vec<RuntimeVisibilityEntry> {
338    [
339        ("schema_version", RuntimeFieldVisibility::PublicSafe),
340        ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
341        ("canister_id", RuntimeFieldVisibility::OperatorOnly),
342        ("role", RuntimeFieldVisibility::OperatorOnly),
343        ("root", RuntimeFieldVisibility::OperatorOnly),
344        ("network", RuntimeFieldVisibility::OperatorOnly),
345        ("build", RuntimeFieldVisibility::OperatorOnly),
346        ("features", RuntimeFieldVisibility::OperatorOnly),
347        ("topology", RuntimeFieldVisibility::ControllerOnly),
348        ("timers", RuntimeFieldVisibility::OperatorOnly),
349        ("state", RuntimeFieldVisibility::OperatorOnly),
350        ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
351        ("readiness", RuntimeFieldVisibility::OperatorOnly),
352        ("status", RuntimeFieldVisibility::OperatorOnly),
353        ("visibility", RuntimeFieldVisibility::OperatorOnly),
354    ]
355    .into_iter()
356    .map(|(field, visibility)| RuntimeVisibilityEntry {
357        field: field.to_string(),
358        visibility,
359    })
360    .collect()
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366    use crate::ops::runtime::metrics::timer::{TimerMetrics, TimerMode};
367    use crate::ops::runtime::recent_failure::RecentFailureOps;
368    use std::time::Duration;
369
370    #[test]
371    fn health_is_minimal_and_schema_versioned() {
372        let health = RuntimeIntrospectionApi::health(Some(42));
373
374        assert_eq!(health.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
375        assert_eq!(health.status, crate::dto::runtime::HealthStatus::Healthy);
376        assert_eq!(health.observed_at_ns, Some(42));
377        assert_eq!(health.checks.len(), 1);
378        assert_eq!(health.checks[0].code, "canister_responsive");
379    }
380
381    #[test]
382    fn runtime_status_embeds_guarded_readiness_and_build_info() {
383        let status = RuntimeIntrospectionApi::runtime_status_for(
384            Principal::anonymous(),
385            100,
386            "test-canister",
387            "1.2.3",
388            "0.81.0",
389            7,
390        );
391
392        assert_eq!(status.schema_version, RUNTIME_INTROSPECTION_SCHEMA_VERSION);
393        assert_eq!(status.observed_at_ns, 100);
394        assert_eq!(status.canister_id, Principal::anonymous());
395        assert_eq!(status.build.package_name, "test-canister");
396        assert_eq!(status.build.package_version, "1.2.3");
397        assert_eq!(status.build.canic_version, "0.81.0");
398        assert_eq!(status.build.canister_version, 7);
399        assert_eq!(status.readiness.observed_at_ns, 100);
400        assert!(
401            status
402                .visibility
403                .iter()
404                .any(|entry| entry.field == "topology"
405                    && entry.visibility == RuntimeFieldVisibility::ControllerOnly)
406        );
407    }
408
409    #[test]
410    fn runtime_status_classifies_each_top_level_field_visibility() {
411        let status = RuntimeIntrospectionApi::runtime_status_for(
412            Principal::anonymous(),
413            100,
414            "test-canister",
415            "1.2.3",
416            "0.81.0",
417            7,
418        );
419        let expected = [
420            ("schema_version", RuntimeFieldVisibility::PublicSafe),
421            ("observed_at_ns", RuntimeFieldVisibility::PublicSafe),
422            ("canister_id", RuntimeFieldVisibility::OperatorOnly),
423            ("role", RuntimeFieldVisibility::OperatorOnly),
424            ("root", RuntimeFieldVisibility::OperatorOnly),
425            ("network", RuntimeFieldVisibility::OperatorOnly),
426            ("build", RuntimeFieldVisibility::OperatorOnly),
427            ("features", RuntimeFieldVisibility::OperatorOnly),
428            ("topology", RuntimeFieldVisibility::ControllerOnly),
429            ("timers", RuntimeFieldVisibility::OperatorOnly),
430            ("state", RuntimeFieldVisibility::OperatorOnly),
431            ("recent_failures", RuntimeFieldVisibility::OperatorOnly),
432            ("readiness", RuntimeFieldVisibility::OperatorOnly),
433            ("status", RuntimeFieldVisibility::OperatorOnly),
434            ("visibility", RuntimeFieldVisibility::OperatorOnly),
435        ];
436
437        assert_eq!(status.visibility.len(), expected.len());
438        for (index, (field, visibility)) in expected.into_iter().enumerate() {
439            assert_eq!(status.visibility[index].field, field);
440            assert_eq!(status.visibility[index].visibility, visibility);
441        }
442    }
443
444    #[test]
445    fn runtime_status_projects_registered_timer_metrics() {
446        TimerMetrics::reset();
447        TimerMetrics::record_timer_scheduled(
448            TimerMode::Interval,
449            Duration::from_mins(1),
450            "cycles:interval",
451        );
452        TimerMetrics::record_timer_scheduled(
453            TimerMode::Once,
454            Duration::from_secs(1),
455            "auth_renewal:init",
456        );
457        TimerMetrics::record_timer_tick(
458            TimerMode::Once,
459            Duration::from_secs(1),
460            "auth_renewal:init",
461        );
462
463        let status = RuntimeIntrospectionApi::runtime_status_for(
464            Principal::anonymous(),
465            100,
466            "test-canister",
467            "1.2.3",
468            "0.81.0",
469            7,
470        );
471
472        assert_eq!(status.timers.len(), 2);
473        assert_eq!(status.timers[0].subsystem, "auth_renewal");
474        assert_eq!(status.timers[0].name, "init");
475        assert_eq!(status.timers[0].status, TimerStatus::Healthy);
476        assert_eq!(status.timers[1].subsystem, "cycles");
477        assert_eq!(status.timers[1].name, "interval");
478        assert_eq!(status.timers[1].status, TimerStatus::Unknown);
479
480        TimerMetrics::reset();
481    }
482
483    #[test]
484    fn runtime_status_bounds_timer_labels() {
485        TimerMetrics::reset();
486
487        let label = format!("{}\n:{}\n", "subsystem".repeat(12), "timer_name".repeat(16));
488        TimerMetrics::record_timer_scheduled(TimerMode::Once, Duration::from_secs(1), &label);
489
490        let status = RuntimeIntrospectionApi::runtime_status_for(
491            Principal::anonymous(),
492            100,
493            "test-canister",
494            "1.2.3",
495            "0.81.0",
496            7,
497        );
498
499        assert_eq!(status.timers.len(), 1);
500        assert!(status.timers[0].subsystem.len() <= MAX_TIMER_SUBSYSTEM_BYTES);
501        assert!(status.timers[0].name.len() <= MAX_TIMER_NAME_BYTES);
502        assert!(!status.timers[0].subsystem.contains('\n'));
503        assert!(!status.timers[0].name.contains('\n'));
504
505        TimerMetrics::reset();
506    }
507
508    #[test]
509    fn state_summary_uses_declared_metadata_without_value_counts() {
510        let summary = state_summary(Some("root")).expect("root state declarations");
511
512        assert_eq!(
513            summary.manifest_schema_version,
514            u32::from(crate::state_contract::STATE_MANIFEST_SCHEMA_VERSION)
515        );
516        assert!(summary.total_stable_memory_pages.is_none());
517        assert!(summary.domains.iter().any(|domain| {
518            domain.domain == "env"
519                && domain.storage == "stable_memory"
520                && domain.status == RuntimeStateDomainStatus::Ok
521        }));
522        assert!(state_summary(Some("unknown_role")).is_none());
523        assert!(state_summary(None).is_none());
524    }
525
526    #[test]
527    fn runtime_status_includes_recent_failure_snapshot() {
528        RecentFailureOps::reset();
529        RuntimeIntrospectionApi::record_recent_failure(
530            77,
531            "runtime",
532            "readiness_failed",
533            FailureSeverity::Error,
534            "bounded failure summary",
535            Some("runtime-check".to_string()),
536        );
537
538        let status = RuntimeIntrospectionApi::runtime_status_for(
539            Principal::anonymous(),
540            100,
541            "test-canister",
542            "1.2.3",
543            "0.81.0",
544            7,
545        );
546
547        assert_eq!(status.recent_failures.len(), 1);
548        assert_eq!(status.recent_failures[0].occurred_at_ns, 77);
549        assert_eq!(status.recent_failures[0].subsystem, "runtime");
550        assert_eq!(status.recent_failures[0].code, "readiness_failed");
551
552        RecentFailureOps::reset();
553    }
554}