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
69pub struct MemoryRuntimeApi;
74
75impl MemoryRuntimeApi {
76 pub fn bootstrap_registry() -> Result<(), Error> {
78 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
79
80 Ok(())
81 }
82}
83
84pub struct RuntimeIntrospectionApi;
89
90impl RuntimeIntrospectionApi {
91 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 #[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 #[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 #[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 #[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}