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