shaperail_runtime/observability/
logging.rs1use std::collections::HashSet;
2
3use shaperail_core::ResourceDefinition;
4use tracing_subscriber::fmt::format::FmtSpan;
5use tracing_subscriber::layer::SubscriberExt;
6use tracing_subscriber::util::SubscriberInitExt;
7use tracing_subscriber::EnvFilter;
8
9pub fn init_logging() {
15 let env_filter = EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info"));
16
17 tracing_subscriber::registry()
18 .with(env_filter)
19 .with(
20 tracing_subscriber::fmt::layer()
21 .json()
22 .with_target(true)
23 .with_thread_ids(false)
24 .with_span_events(FmtSpan::CLOSE)
25 .flatten_event(true),
26 )
27 .init();
28}
29
30pub fn sensitive_fields(resources: &[ResourceDefinition]) -> HashSet<String> {
32 let mut fields = HashSet::new();
33 for resource in resources {
34 for (name, schema) in &resource.schema {
35 if schema.sensitive {
36 fields.insert(name.clone());
37 }
38 }
39 }
40 fields
41}
42
43pub fn redact_sensitive(
47 value: &serde_json::Value,
48 sensitive: &HashSet<String>,
49) -> serde_json::Value {
50 match value {
51 serde_json::Value::Object(map) => {
52 let mut redacted = serde_json::Map::new();
53 for (key, val) in map {
54 if sensitive.contains(key) {
55 redacted.insert(
56 key.clone(),
57 serde_json::Value::String("[REDACTED]".to_string()),
58 );
59 } else {
60 redacted.insert(key.clone(), redact_sensitive(val, sensitive));
61 }
62 }
63 serde_json::Value::Object(redacted)
64 }
65 serde_json::Value::Array(arr) => {
66 serde_json::Value::Array(arr.iter().map(|v| redact_sensitive(v, sensitive)).collect())
67 }
68 other => other.clone(),
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn sensitive_fields_collected() {
78 use indexmap::IndexMap;
79 use shaperail_core::{FieldSchema, FieldType};
80
81 let mut schema = IndexMap::new();
82 schema.insert(
83 "email".to_string(),
84 FieldSchema {
85 field_type: FieldType::String,
86 primary: false,
87 generated: false,
88 required: true,
89 unique: false,
90 nullable: false,
91 reference: None,
92 min: None,
93 max: None,
94 format: None,
95 values: None,
96 default: None,
97 sensitive: true,
98 search: false,
99 items: None,
100 },
101 );
102 schema.insert(
103 "name".to_string(),
104 FieldSchema {
105 field_type: FieldType::String,
106 primary: false,
107 generated: false,
108 required: true,
109 unique: false,
110 nullable: false,
111 reference: None,
112 min: None,
113 max: None,
114 format: None,
115 values: None,
116 default: None,
117 sensitive: false,
118 search: false,
119 items: None,
120 },
121 );
122
123 let resources = vec![ResourceDefinition {
124 resource: "users".to_string(),
125 version: 1,
126 db: None,
127 tenant_key: None,
128 schema,
129 endpoints: None,
130 relations: None,
131 indexes: None,
132 }];
133
134 let fields = sensitive_fields(&resources);
135 assert!(fields.contains("email"));
136 assert!(!fields.contains("name"));
137 }
138
139 #[test]
140 fn redact_sensitive_values() {
141 let mut sensitive = HashSet::new();
142 sensitive.insert("password".to_string());
143 sensitive.insert("ssn".to_string());
144
145 let value = serde_json::json!({
146 "name": "Alice",
147 "password": "secret123",
148 "ssn": "123-45-6789",
149 "nested": {
150 "password": "also_secret"
151 }
152 });
153
154 let redacted = redact_sensitive(&value, &sensitive);
155 assert_eq!(redacted["name"], "Alice");
156 assert_eq!(redacted["password"], "[REDACTED]");
157 assert_eq!(redacted["ssn"], "[REDACTED]");
158 assert_eq!(redacted["nested"]["password"], "[REDACTED]");
159 }
160
161 #[test]
162 fn redact_handles_arrays() {
163 let mut sensitive = HashSet::new();
164 sensitive.insert("secret".to_string());
165
166 let value = serde_json::json!([
167 {"secret": "a", "public": "b"},
168 {"secret": "c", "public": "d"},
169 ]);
170
171 let redacted = redact_sensitive(&value, &sensitive);
172 let arr = redacted.as_array().unwrap();
173 assert_eq!(arr[0]["secret"], "[REDACTED]");
174 assert_eq!(arr[0]["public"], "b");
175 assert_eq!(arr[1]["secret"], "[REDACTED]");
176 }
177}