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