1use std::io::{self, Read, Write};
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use interprocess::local_socket::traits::Listener;
7use prost::Message;
8use serde::Serialize;
9use serde_json::json;
10
11use crate::broker::protocol::{
12 read_frame, write_frame, AdminReply, AdminReplyKind, AdminRequest, AdminVerb, Frame, FrameKind,
13 FramingError, PayloadEncoding, ENVELOPE_VERSION, MAX_FRAME_BYTES, MAX_HELLO_BYTES,
14 PROTOCOL_VERSION,
15};
16
17use super::backend_registry::BackendRegistry;
18use super::connection::{bind_local_socket, BrokerConnectionError, LocalSocketCleanup};
19use super::deadline_stream::{hello_read_deadline, with_nonblocking_deadline};
20use super::service_def_loader::{service_definition_dir, SERVICE_DEF_DIR_ENV};
21use super::spawn_coordinator::{
22 SpawnBudgetSnapshot, DEFAULT_SPAWN_ATTEMPTS_PER_WINDOW, DEFAULT_SPAWN_BUDGET_WINDOW,
23};
24use crate::broker::server::metrics::{MetricKind, BROKER_METRICS};
25
26pub const ADMIN_SCHEMA_VERSION: u32 = 1;
28pub use crate::broker::protocol::registry::ADMIN_PAYLOAD_PROTOCOL;
34
35const DIAGNOSTIC_BUNDLE_FORMAT: &str = "tar.gz";
36const DIAGNOSTIC_BUNDLE_MODE: &str = "metadata-only";
37const DIAGNOSTIC_REDACTIONS: &[&str] = &["home", "secret-env", "acl-identities"];
38
39#[derive(Clone, Debug)]
41pub struct AdminSnapshot {
42 pub broker_instance: String,
44 pub broker_pid: u32,
46 pub generated_at_unix_ms: u64,
48 pub uptime: Duration,
50 pub accepting_hello: bool,
52 pub connections_open: u64,
54 pub backends: Vec<AdminBackend>,
56 pub spawn_budgets: Vec<AdminSpawnBudget>,
58 pub fd_pressure_demoted: bool,
60 pub inode_pressure: AdminInodePressure,
62}
63
64#[derive(Clone, Debug, Default)]
68pub struct AdminInodePressure {
69 pub supported: bool,
71 pub error: Option<String>,
73 pub total: u64,
75 pub free: u64,
77}
78
79impl AdminInodePressure {
80 pub fn probe() -> Self {
82 match crate::broker::fs_health::daemon_data_dir_inode_usage() {
83 Ok(Some(usage)) => Self {
84 supported: true,
85 error: None,
86 total: usage.total,
87 free: usage.free,
88 },
89 Ok(None) => Self::default(),
90 Err(err) => Self {
91 supported: false,
92 error: Some(err.to_string()),
93 total: 0,
94 free: 0,
95 },
96 }
97 }
98
99 fn to_json(&self) -> serde_json::Value {
100 if self.supported {
101 let usage = crate::broker::fs_health::InodeUsage {
102 total: self.total,
103 free: self.free,
104 };
105 json!({
106 "supported": true,
107 "inodes_total": self.total,
108 "inodes_free": self.free,
109 "inodes_used": usage.used(),
110 "used_ratio": usage.used_ratio(),
111 })
112 } else {
113 json!({
114 "supported": false,
115 "detail": self.error.clone().unwrap_or_else(|| {
116 "inode accounting not applicable on this filesystem".into()
117 }),
118 })
119 }
120 }
121}
122
123impl AdminSnapshot {
124 pub fn local_not_serving() -> Self {
126 Self {
127 broker_instance: "local".into(),
128 broker_pid: std::process::id(),
129 generated_at_unix_ms: unix_now_ms(),
130 uptime: Duration::ZERO,
131 accepting_hello: false,
132 connections_open: 0,
133 backends: Vec::new(),
134 spawn_budgets: Vec::new(),
135 fd_pressure_demoted: false,
136 inode_pressure: AdminInodePressure::probe(),
137 }
138 }
139
140 pub fn from_registry(
142 broker_instance: impl Into<String>,
143 uptime: Duration,
144 accepting_hello: bool,
145 connections_open: u64,
146 registry: &BackendRegistry,
147 spawn_budgets: &[SpawnBudgetSnapshot],
148 ) -> Self {
149 Self::from_registry_at(
150 broker_instance,
151 std::process::id(),
152 unix_now_ms(),
153 uptime,
154 accepting_hello,
155 connections_open,
156 registry,
157 spawn_budgets,
158 )
159 }
160
161 #[allow(clippy::too_many_arguments)]
163 pub fn from_registry_at(
164 broker_instance: impl Into<String>,
165 broker_pid: u32,
166 generated_at_unix_ms: u64,
167 uptime: Duration,
168 accepting_hello: bool,
169 connections_open: u64,
170 registry: &BackendRegistry,
171 spawn_budgets: &[SpawnBudgetSnapshot],
172 ) -> Self {
173 Self {
174 broker_instance: broker_instance.into(),
175 broker_pid,
176 generated_at_unix_ms,
177 uptime,
178 accepting_hello,
179 connections_open,
180 backends: registry
181 .iter()
182 .map(|(_key, handle)| AdminBackend {
183 service_name: handle.service_name.clone(),
184 service_version: handle.service_version.clone(),
185 pid: handle.daemon_process.pid,
186 backend_pipe: handle.daemon_process.ipc_endpoint.path.clone(),
187 last_active_unix_ms: handle.daemon_process.started_at_unix_ms,
188 state: if handle.is_alive() {
189 "running".into()
190 } else {
191 "stale".into()
192 },
193 last_hello_unix_ms: 0,
194 last_error: None,
195 })
196 .collect(),
197 spawn_budgets: spawn_budgets
198 .iter()
199 .map(AdminSpawnBudget::from_snapshot)
200 .collect(),
201 fd_pressure_demoted: false,
202 inode_pressure: AdminInodePressure::probe(),
203 }
204 }
205
206 pub fn with_fd_pressure_demoted(mut self, demoted: bool) -> Self {
208 self.fd_pressure_demoted = demoted;
209 self
210 }
211
212 pub fn with_inode_pressure(mut self, inode_pressure: AdminInodePressure) -> Self {
214 self.inode_pressure = inode_pressure;
215 self
216 }
217}
218
219#[derive(Clone, Debug)]
221pub struct AdminBackend {
222 pub service_name: String,
224 pub service_version: String,
226 pub pid: u32,
228 pub backend_pipe: String,
230 pub last_active_unix_ms: u64,
232 pub state: String,
234 pub last_hello_unix_ms: u64,
236 pub last_error: Option<String>,
238}
239
240#[derive(Clone, Debug)]
242pub struct AdminSpawnBudget {
243 pub broker_instance: String,
245 pub service_name: String,
247 pub service_version: String,
249 pub attempts_used: u32,
251 pub remaining: u32,
253 pub in_flight: bool,
255 pub retry_after_ms: Option<u64>,
257}
258
259impl AdminSpawnBudget {
260 fn from_snapshot(snapshot: &SpawnBudgetSnapshot) -> Self {
261 Self {
262 broker_instance: snapshot.key.instance.id(),
263 service_name: snapshot.key.service_name.clone(),
264 service_version: snapshot.key.service_version.clone(),
265 attempts_used: snapshot.attempts_used,
266 remaining: snapshot.remaining,
267 in_flight: snapshot.in_flight,
268 retry_after_ms: snapshot
269 .retry_after
270 .map(|duration| u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)),
271 }
272 }
273}
274
275pub fn render_status_json(snapshot: &AdminSnapshot) -> String {
277 json!({
278 "schema_version": ADMIN_SCHEMA_VERSION,
279 "command": "status",
280 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
281 "broker_instance": snapshot.broker_instance,
282 "broker_pid": snapshot.broker_pid,
283 "uptime_seconds": snapshot.uptime.as_secs_f64(),
284 "accepting_hello": snapshot.accepting_hello,
285 "connections_open": snapshot.connections_open,
286 "fd_pressure": {
287 "demoted": snapshot.fd_pressure_demoted,
288 },
289 "inode_pressure": snapshot.inode_pressure.to_json(),
290 "backends": snapshot.backends.iter().map(|backend| {
291 json!({
292 "service_name": backend.service_name,
293 "service_version": backend.service_version,
294 "pid": backend.pid,
295 "backend_pipe": backend.backend_pipe,
296 "last_active_unix_ms": backend.last_active_unix_ms,
297 "state": backend.state,
298 })
299 }).collect::<Vec<_>>(),
300 })
301 .to_string()
302}
303
304pub fn render_dump_json(snapshot: &AdminSnapshot) -> String {
306 json!({
307 "schema_version": ADMIN_SCHEMA_VERSION,
308 "command": "dump",
309 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
310 "broker_instance": snapshot.broker_instance,
311 "effective_config": effective_config_json(snapshot),
312 "backend_table": snapshot.backends.iter().map(|backend| {
313 json!({
314 "service_name": backend.service_name,
315 "service_version": backend.service_version,
316 "pid": backend.pid,
317 "backend_pipe": backend.backend_pipe,
318 "state": backend.state,
319 })
320 }).collect::<Vec<_>>(),
321 "spawn_budgets": snapshot.spawn_budgets.iter().map(|budget| {
322 json!({
323 "broker_instance": budget.broker_instance,
324 "service_name": budget.service_name,
325 "service_version": budget.service_version,
326 "attempts_used": budget.attempts_used,
327 "remaining": budget.remaining,
328 "in_flight": budget.in_flight,
329 "retry_after_ms": budget.retry_after_ms,
330 })
331 }).collect::<Vec<_>>(),
332 "recent_lifecycle_events": [],
333 })
334 .to_string()
335}
336
337pub fn render_list_instances_json(snapshot: &AdminSnapshot) -> String {
339 json!({
340 "schema_version": ADMIN_SCHEMA_VERSION,
341 "command": "list-instances",
342 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
343 "instances": [{
344 "broker_instance": snapshot.broker_instance,
345 "pipe": "",
346 "pid": snapshot.broker_pid,
347 "state": if snapshot.accepting_hello { "running" } else { "not-serving" },
348 }],
349 })
350 .to_string()
351}
352
353pub fn render_backend_health_json(snapshot: &AdminSnapshot, service_name: &str) -> String {
355 json!({
356 "schema_version": ADMIN_SCHEMA_VERSION,
357 "command": "backend-health",
358 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
359 "service_name": service_name,
360 "backends": snapshot.backends.iter()
361 .filter(|backend| backend.service_name == service_name)
362 .map(|backend| {
363 json!({
364 "service_version": backend.service_version,
365 "pid": backend.pid,
366 "state": backend.state,
367 "last_hello_unix_ms": backend.last_hello_unix_ms,
368 "last_error": backend.last_error,
369 })
370 })
371 .collect::<Vec<_>>(),
372 })
373 .to_string()
374}
375
376pub fn render_config_json(snapshot: &AdminSnapshot) -> String {
378 json!({
379 "schema_version": ADMIN_SCHEMA_VERSION,
380 "command": "config",
381 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
382 "values": effective_config_json(snapshot),
383 })
384 .to_string()
385}
386
387pub fn render_diagnose_json(snapshot: &AdminSnapshot, output: &str) -> String {
389 let entries = diagnostic_bundle_entries_json(snapshot);
390 json!({
391 "schema_version": ADMIN_SCHEMA_VERSION,
392 "command": "diagnose",
393 "generated_at_unix_ms": snapshot.generated_at_unix_ms,
394 "output": output,
395 "bundle": {
396 "format": DIAGNOSTIC_BUNDLE_FORMAT,
397 "mode": DIAGNOSTIC_BUNDLE_MODE,
398 "created": false,
399 "entries": entries,
400 },
401 "files": diagnostic_bundle_file_paths(snapshot),
402 "redactions": diagnostic_redaction_names(),
403 "redaction_policy": diagnostic_redaction_policy_json(),
404 })
405 .to_string()
406}
407
408pub fn render_metrics_text(snapshot: &AdminSnapshot) -> String {
410 let mut out = String::new();
411 for metric in BROKER_METRICS {
412 out.push_str("# TYPE ");
413 out.push_str(metric.name);
414 out.push(' ');
415 out.push_str(metric_kind_name(metric.kind));
416 out.push('\n');
417 if metric.labels.is_empty() {
418 out.push_str(metric.name);
419 out.push(' ');
420 out.push_str(&metric_value(metric.name, snapshot));
421 out.push('\n');
422 }
423 }
424 out.push_str("# EOF\n");
425 out
426}
427
428pub fn render_healthz() -> &'static str {
430 "ok\n"
431}
432
433pub fn render_readyz(snapshot: &AdminSnapshot) -> &'static str {
435 if snapshot.accepting_hello {
436 "ready\n"
437 } else {
438 "not ready\n"
439 }
440}
441
442pub fn render_admin_reply(snapshot: &AdminSnapshot, request: &AdminRequest) -> AdminReply {
444 match AdminVerb::try_from(request.verb) {
445 Ok(AdminVerb::Status) => {
446 if request.json {
447 json_reply(render_status_json(snapshot))
448 } else {
449 text_reply(
450 format!(
451 "broker_instance: {}\naccepting_hello: {}\n",
452 snapshot.broker_instance, snapshot.accepting_hello
453 ),
454 0,
455 )
456 }
457 }
458 Ok(AdminVerb::Dump) => json_reply(render_dump_json(snapshot)),
459 Ok(AdminVerb::ListInstances) => json_reply(render_list_instances_json(snapshot)),
460 Ok(AdminVerb::Healthz) => text_reply(render_healthz(), 0),
461 Ok(AdminVerb::Readyz) => {
462 let exit_code = if snapshot.accepting_hello { 0 } else { 1 };
463 text_reply(render_readyz(snapshot), exit_code)
464 }
465 Ok(AdminVerb::BackendHealth) => {
466 let service_name = if request.service_name.is_empty() {
467 "unknown"
468 } else {
469 &request.service_name
470 };
471 json_reply(render_backend_health_json(snapshot, service_name))
472 }
473 Ok(AdminVerb::Config) => json_reply(render_config_json(snapshot)),
474 Ok(AdminVerb::Diagnose) => {
475 let output = if request.output_path.is_empty() {
476 "bundle.tar.gz"
477 } else {
478 &request.output_path
479 };
480 json_reply(render_diagnose_json(snapshot, output))
481 }
482 Ok(AdminVerb::Metrics) => AdminReply {
483 kind: AdminReplyKind::Openmetrics as i32,
484 body: render_metrics_text(snapshot),
485 exit_code: 0,
486 content_type: "application/openmetrics-text".into(),
487 },
488 Ok(AdminVerb::Unspecified) | Err(_) => text_reply("unsupported admin verb\n", 2),
489 }
490}
491
492pub fn handle_admin_frame(
494 frame: Frame,
495 snapshot: &AdminSnapshot,
496) -> Result<Frame, AdminFrameError> {
497 if frame.envelope_version != PROTOCOL_VERSION {
498 return Err(AdminFrameError::UnsupportedEnvelopeVersion(
499 frame.envelope_version,
500 ));
501 }
502 if FrameKind::try_from(frame.kind) != Ok(FrameKind::Request) {
503 return Err(AdminFrameError::UnexpectedKind(frame.kind));
504 }
505 if frame.payload_protocol != ADMIN_PAYLOAD_PROTOCOL {
506 return Err(AdminFrameError::UnexpectedPayloadProtocol(
507 frame.payload_protocol,
508 ));
509 }
510 if PayloadEncoding::try_from(frame.payload_encoding) != Ok(PayloadEncoding::None) {
511 return Err(AdminFrameError::UnsupportedPayloadEncoding(
512 frame.payload_encoding,
513 ));
514 }
515
516 let request =
517 AdminRequest::decode(frame.payload.as_slice()).map_err(AdminFrameError::Decode)?;
518 let reply = render_admin_reply(snapshot, &request);
519 Ok(Frame {
520 envelope_version: PROTOCOL_VERSION,
521 kind: FrameKind::Response as i32,
522 payload_protocol: ADMIN_PAYLOAD_PROTOCOL,
523 payload: reply.encode_to_vec(),
524 request_id: frame.request_id,
525 payload_encoding: PayloadEncoding::None as i32,
526 deadline_unix_ms: 0,
527 traceparent: frame.traceparent,
528 tracestate: frame.tracestate,
529 })
530}
531
532pub fn handle_admin_connection<S: Read + Write>(
539 stream: &mut S,
540 snapshot: &AdminSnapshot,
541) -> Result<AdminReply, AdminConnectionError> {
542 let request_bytes = read_frame(stream)?;
543 let request_frame =
544 Frame::decode(request_bytes.as_slice()).map_err(AdminConnectionError::DecodeFrame)?;
545 let response_frame = handle_admin_frame(request_frame, snapshot)?;
546 write_frame(stream, &response_frame.encode_to_vec())?;
547 AdminReply::decode(response_frame.payload.as_slice()).map_err(AdminConnectionError::DecodeReply)
548}
549
550pub fn serve_one_admin_socket(
556 socket_path: &str,
557 snapshot: &AdminSnapshot,
558) -> Result<AdminReply, AdminConnectionError> {
559 let listener = bind_local_socket(socket_path)?;
560 let cleanup = LocalSocketCleanup(socket_path);
561 let result = (|| {
562 let mut stream = listener.accept()?;
563 with_nonblocking_deadline(&mut stream, hello_read_deadline(), |stream| {
564 handle_admin_connection(stream, snapshot)
565 })
566 })();
567 drop(listener);
568 drop(cleanup);
569 result
570}
571
572#[derive(Debug, thiserror::Error)]
574pub enum AdminFrameError {
575 #[error("unsupported admin frame envelope_version {0}")]
577 UnsupportedEnvelopeVersion(u32),
578 #[error("admin frame kind must be REQUEST, got {0}")]
580 UnexpectedKind(i32),
581 #[error("admin frame payload_protocol must be 0xAD01, got {0}")]
583 UnexpectedPayloadProtocol(u32),
584 #[error("admin frame payload must not be compressed, got {0}")]
586 UnsupportedPayloadEncoding(i32),
587 #[error(transparent)]
589 Decode(prost::DecodeError),
590}
591
592#[derive(Debug, thiserror::Error)]
594pub enum AdminConnectionError {
595 #[error(transparent)]
597 Framing(#[from] FramingError),
598 #[error("failed to decode admin request Frame: {0}")]
600 DecodeFrame(prost::DecodeError),
601 #[error(transparent)]
603 AdminFrame(#[from] AdminFrameError),
604 #[error("failed to decode admin reply payload: {0}")]
606 DecodeReply(prost::DecodeError),
607 #[error(transparent)]
609 LocalSocket(#[from] BrokerConnectionError),
610 #[error(transparent)]
612 Io(#[from] io::Error),
613}
614
615fn json_reply(body: String) -> AdminReply {
616 AdminReply {
617 kind: AdminReplyKind::Json as i32,
618 body,
619 exit_code: 0,
620 content_type: "application/json".into(),
621 }
622}
623
624fn text_reply(body: impl Into<String>, exit_code: u32) -> AdminReply {
625 AdminReply {
626 kind: AdminReplyKind::Text as i32,
627 body: body.into(),
628 exit_code,
629 content_type: "text/plain".into(),
630 }
631}
632
633fn metric_kind_name(kind: MetricKind) -> &'static str {
634 match kind {
635 MetricKind::Counter => "counter",
636 MetricKind::Gauge => "gauge",
637 MetricKind::Histogram => "histogram",
638 }
639}
640
641fn metric_value(name: &str, snapshot: &AdminSnapshot) -> String {
642 match name {
643 "running_process_broker_v1_connections_open" => snapshot.connections_open.to_string(),
644 "running_process_broker_v1_fd_usage_ratio" => "0".into(),
645 "running_process_broker_v1_uptime_seconds" => snapshot.uptime.as_secs().to_string(),
646 _ => "0".into(),
647 }
648}
649
650fn effective_config_json(snapshot: &AdminSnapshot) -> serde_json::Value {
651 json!({
652 "broker": {
653 "broker_instance": sourced_value(&snapshot.broker_instance, "runtime"),
654 "broker_pid": sourced_value(snapshot.broker_pid, "runtime"),
655 "accepting_hello": sourced_value(snapshot.accepting_hello, "runtime"),
656 },
657 "protocol": {
658 "admin_payload_protocol": sourced_value(format!("0x{ADMIN_PAYLOAD_PROTOCOL:04X}"), "protocol-v1"),
659 "envelope_version": sourced_value(PROTOCOL_VERSION, "protocol-v1"),
660 "framing_version": sourced_value(ENVELOPE_VERSION, "protocol-v1"),
661 },
662 "limits": {
663 "max_frame_bytes": sourced_value(MAX_FRAME_BYTES, "protocol-v1"),
664 "max_hello_bytes": sourced_value(MAX_HELLO_BYTES, "protocol-v1"),
665 "connections_open": sourced_value(snapshot.connections_open, "runtime"),
666 },
667 "paths": {
668 "service_definition_dir": sourced_value(
669 service_definition_dir().display().to_string(),
670 service_definition_dir_source(),
671 ),
672 },
673 "spawn_budget": {
674 "default_attempts_per_window": sourced_value(DEFAULT_SPAWN_ATTEMPTS_PER_WINDOW, "default"),
675 "default_window_ms": sourced_value(duration_ms(DEFAULT_SPAWN_BUDGET_WINDOW), "default"),
676 "active_budget_rows": sourced_value(snapshot.spawn_budgets.len(), "runtime"),
677 },
678 "diagnostics": {
679 "bundle_format": sourced_value(DIAGNOSTIC_BUNDLE_FORMAT, "schema-v1"),
680 "bundle_mode": sourced_value(DIAGNOSTIC_BUNDLE_MODE, "schema-v1"),
681 "redactions": sourced_value(diagnostic_redaction_names(), "schema-v1"),
682 },
683 })
684}
685
686fn service_definition_dir_source() -> &'static str {
687 if std::env::var_os(SERVICE_DEF_DIR_ENV).is_some() {
688 "env:RUNNING_PROCESS_SERVICE_DEF_DIR"
689 } else {
690 "platform-default"
691 }
692}
693
694fn diagnostic_bundle_entries_json(snapshot: &AdminSnapshot) -> Vec<serde_json::Value> {
695 vec![
696 diagnostic_bundle_entry("admin/status.json", "json", "status", true, false, None),
697 diagnostic_bundle_entry("admin/dump.json", "json", "dump", true, true, None),
698 diagnostic_bundle_entry(
699 "config/effective.json",
700 "json",
701 "effective-config",
702 true,
703 false,
704 None,
705 ),
706 diagnostic_bundle_entry(
707 "metrics/openmetrics.txt",
708 "openmetrics",
709 "metrics",
710 true,
711 false,
712 None,
713 ),
714 diagnostic_bundle_entry(
715 "events/lifecycle.jsonl",
716 "jsonl",
717 "lifecycle-events",
718 false,
719 true,
720 None,
721 ),
722 diagnostic_bundle_entry(
723 "manifest/backend-manifests.json",
724 "json",
725 "backend-manifest-index",
726 false,
727 true,
728 None,
729 ),
730 diagnostic_bundle_entry(
731 "process/backends.json",
732 "json",
733 "backend-table",
734 true,
735 true,
736 Some(snapshot.backends.len()),
737 ),
738 diagnostic_bundle_entry(
739 "system/summary.json",
740 "json",
741 "host-summary",
742 false,
743 true,
744 None,
745 ),
746 ]
747}
748
749fn diagnostic_bundle_file_paths(snapshot: &AdminSnapshot) -> Vec<String> {
750 diagnostic_bundle_entries_json(snapshot)
751 .into_iter()
752 .filter_map(|entry| {
753 entry
754 .get("path")
755 .and_then(serde_json::Value::as_str)
756 .map(str::to_owned)
757 })
758 .collect()
759}
760
761fn diagnostic_bundle_entry(
762 path: &str,
763 kind: &str,
764 source: &str,
765 required: bool,
766 redacted: bool,
767 record_count: Option<usize>,
768) -> serde_json::Value {
769 let mut entry = json!({
770 "path": path,
771 "kind": kind,
772 "source": source,
773 "required": required,
774 "redacted": redacted,
775 });
776 if let Some(record_count) = record_count {
777 entry["record_count"] = json!(record_count);
778 }
779 entry
780}
781
782fn diagnostic_redaction_names() -> Vec<&'static str> {
783 DIAGNOSTIC_REDACTIONS.to_vec()
784}
785
786fn diagnostic_redaction_policy_json() -> Vec<serde_json::Value> {
787 vec![
788 json!({
789 "name": "home",
790 "replacement": "~",
791 }),
792 json!({
793 "name": "secret-env",
794 "matches": ["KEY", "TOKEN", "SECRET", "PASS"],
795 }),
796 json!({
797 "name": "acl-identities",
798 "replacement": "stable-hash",
799 }),
800 ]
801}
802
803fn sourced_value(value: impl Serialize, source: &'static str) -> serde_json::Value {
804 json!({
805 "value": value,
806 "source": source,
807 })
808}
809
810fn duration_ms(duration: Duration) -> u64 {
811 u64::try_from(duration.as_millis()).unwrap_or(u64::MAX)
812}
813
814fn unix_now_ms() -> u64 {
815 SystemTime::now()
816 .duration_since(UNIX_EPOCH)
817 .map(|duration| duration.as_millis() as u64)
818 .unwrap_or(0)
819}