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
32pub struct MemoryRuntimeApi;
37
38impl MemoryRuntimeApi {
39 pub fn bootstrap_registry() -> Result<(), Error> {
41 MemoryRegistryOps::bootstrap_registry().map_err(Error::from)?;
42
43 Ok(())
44 }
45}
46
47pub struct RuntimeIntrospectionApi;
52
53impl RuntimeIntrospectionApi {
54 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 #[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 #[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 #[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 #[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}