relay_event_schema/protocol/
logentry.rs1use relay_protocol::{Annotated, Empty, Error, FromValue, IntoValue, Meta, Object, Value};
2
3use crate::processor::ProcessValue;
4use crate::protocol::JsonLenientString;
5
6#[derive(Clone, Debug, Default, PartialEq, Empty, IntoValue, ProcessValue)]
29#[metastructure(process_func = "process_logentry", value_type = "LogEntry")]
30pub struct LogEntry {
31 #[metastructure(max_chars = LogEntry::MAX_MESSAGE_CHARS, max_chars_allowance = 200)]
39 pub message: Annotated<Message>,
40
41 #[metastructure(max_chars = LogEntry::MAX_MESSAGE_CHARS, max_chars_allowance = 200, pii = "true")]
46 pub formatted: Annotated<Message>,
47
48 #[metastructure(max_depth = 5, max_bytes = 2048, pii = "true")]
51 pub params: Annotated<Value>,
52
53 #[metastructure(additional_properties, pii = "true")]
55 pub other: Object<Value>,
56}
57
58impl LogEntry {
59 pub const MAX_MESSAGE_CHARS: usize = 8192;
61}
62
63impl From<String> for LogEntry {
64 fn from(formatted_msg: String) -> Self {
65 LogEntry {
66 formatted: Annotated::new(formatted_msg.into()),
67 ..Self::default()
68 }
69 }
70}
71
72#[derive(Clone, Debug, Default, PartialEq, Empty, FromValue, IntoValue, ProcessValue)]
73#[metastructure(value_type = "Message", value_type = "String")]
74pub struct Message(String);
75
76impl From<String> for Message {
77 fn from(msg: String) -> Message {
78 Message(msg)
79 }
80}
81
82impl AsRef<str> for Message {
83 fn as_ref(&self) -> &str {
84 self.0.as_ref()
85 }
86}
87
88impl FromValue for LogEntry {
89 fn from_value(value: Annotated<Value>) -> Annotated<Self> {
90 match value {
95 x @ Annotated(Some(Value::Object(_)), _) => {
96 #[derive(Debug, FromValue)]
97 struct Helper {
98 message: Annotated<String>,
99 formatted: Annotated<String>,
100 params: Annotated<Value>,
101 #[metastructure(additional_properties)]
102 other: Object<Value>,
103 }
104
105 Helper::from_value(x).map_value(|helper| {
106 let params = match helper.params {
107 a @ Annotated(Some(Value::Object(_)), _) => a,
108 a @ Annotated(Some(Value::Array(_)), _) => a,
109 a @ Annotated(None, _) => a,
110 Annotated(Some(value), _) => Annotated::from_error(
111 Error::expected("message parameters"),
112 Some(value),
113 ),
114 };
115
116 LogEntry {
117 message: helper.message.map_value(Message),
118 formatted: helper.formatted.map_value(Message),
119 params,
120 other: helper.other,
121 }
122 })
123 }
124 Annotated(None, meta) => Annotated(None, meta),
125 Annotated(Some(Value::Bool(false)), _) => Annotated(None, Meta::default()),
129 x => Annotated::new(LogEntry {
130 formatted: JsonLenientString::from_value(x)
131 .map_value(JsonLenientString::into_inner)
132 .map_value(Message),
133 ..Default::default()
134 }),
135 }
136 }
137}
138
139#[cfg(test)]
140mod tests {
141 use similar_asserts::assert_eq;
142
143 use super::*;
144
145 #[test]
146 fn test_logentry_roundtrip() {
147 let json = r#"{
148 "message": "Hello, %s %s!",
149 "params": [
150 "World",
151 1
152 ],
153 "other": "value"
154}"#;
155
156 let entry = Annotated::new(LogEntry {
157 message: Annotated::new("Hello, %s %s!".to_owned().into()),
158 formatted: Annotated::empty(),
159 params: Annotated::new(Value::Array(vec![
160 Annotated::new(Value::String("World".to_owned())),
161 Annotated::new(Value::I64(1)),
162 ])),
163 other: {
164 let mut map = Object::new();
165 map.insert(
166 "other".to_owned(),
167 Annotated::new(Value::String("value".to_owned())),
168 );
169 map
170 },
171 });
172
173 assert_eq!(entry, Annotated::from_json(json).unwrap());
174 assert_eq!(json, entry.to_json_pretty().unwrap());
175 }
176
177 #[test]
178 fn test_logentry_from_message() {
179 let input = r#""hi""#;
180 let output = r#"{
181 "formatted": "hi"
182}"#;
183
184 let entry = Annotated::new(LogEntry {
185 formatted: Annotated::new("hi".to_owned().into()),
186 ..Default::default()
187 });
188
189 assert_eq!(entry, Annotated::from_json(input).unwrap());
190 assert_eq!(output, entry.to_json_pretty().unwrap());
191 }
192
193 #[test]
194 fn test_logentry_empty_params() {
195 let input = r#"{"params":[]}"#;
196 let entry = Annotated::new(LogEntry {
197 params: Annotated::new(Value::Array(vec![])),
198 ..Default::default()
199 });
200
201 assert_eq!(entry, Annotated::from_json(input).unwrap());
202 assert_eq!(input, entry.to_json().unwrap());
203 }
204
205 #[test]
206 fn test_logentry_named_params() {
207 let json = r#"{
208 "message": "Hello, %s!",
209 "params": {
210 "name": "World"
211 }
212}"#;
213
214 let entry = Annotated::new(LogEntry {
215 message: Annotated::new("Hello, %s!".to_owned().into()),
216 params: Annotated::new(Value::Object({
217 let mut object = Object::new();
218 object.insert(
219 "name".to_owned(),
220 Annotated::new(Value::String("World".to_owned())),
221 );
222 object
223 })),
224 ..LogEntry::default()
225 });
226
227 assert_eq!(entry, Annotated::from_json(json).unwrap());
228 assert_eq!(json, entry.to_json_pretty().unwrap());
229 }
230
231 #[test]
232 fn test_logentry_invalid_params() {
233 let json = r#"{
234 "message": "Hello, %s!",
235 "params": 42
236}"#;
237
238 let entry = Annotated::new(LogEntry {
239 message: Annotated::new("Hello, %s!".to_owned().into()),
240 params: Annotated::from_error(
241 Error::expected("message parameters"),
242 Some(Value::I64(42)),
243 ),
244 ..LogEntry::default()
245 });
246
247 assert_eq!(entry, Annotated::from_json(json).unwrap());
248 }
249}