logforth_layout_google_cloud_logging/
lib.rs1#![cfg_attr(docsrs, feature(doc_cfg))]
18
19use std::collections::BTreeMap;
20use std::collections::BTreeSet;
21
22use logforth_core::Diagnostic;
23use logforth_core::Error;
24use logforth_core::kv::Key;
25use logforth_core::kv::Value;
26use logforth_core::kv::Visitor;
27use logforth_core::layout::Layout;
28use logforth_core::record::Record;
29use serde::Serialize;
30
31#[derive(Debug, Clone)]
59pub struct GoogleCloudLoggingLayout {
60 trace_project_id: Option<String>,
61 label_keys: BTreeSet<String>,
62
63 trace_keys: BTreeSet<String>,
66 span_id_keys: BTreeSet<String>,
67 trace_sampled_keys: BTreeSet<String>,
68}
69
70impl Default for GoogleCloudLoggingLayout {
71 fn default() -> Self {
72 Self {
73 trace_project_id: None,
74 label_keys: BTreeSet::new(),
75
76 trace_keys: BTreeSet::from(["trace_id".to_string()]),
77 span_id_keys: BTreeSet::from(["span_id".to_string()]),
78 trace_sampled_keys: BTreeSet::from([
79 "sampled".to_string(),
80 "trace_sampled".to_string(),
81 ]),
82 }
83 }
84}
85
86impl GoogleCloudLoggingLayout {
87 pub fn trace_project_id(mut self, project_id: impl Into<String>) -> Self {
100 self.trace_project_id = Some(project_id.into());
101 self
102 }
103
104 pub fn label_keys(mut self, label_keys: impl IntoIterator<Item = impl Into<String>>) -> Self {
117 let label_keys = label_keys.into_iter().map(Into::into);
118 self.label_keys.extend(label_keys);
119 self
120 }
121}
122
123struct KvCollector<'a> {
124 layout: &'a GoogleCloudLoggingLayout,
125
126 payload_fields: BTreeMap<String, serde_json::Value>,
127 labels: BTreeMap<String, serde_json::Value>,
128 trace: Option<String>,
129 span_id: Option<String>,
130 trace_sampled: Option<bool>,
131}
132
133impl Visitor for KvCollector<'_> {
134 fn visit(&mut self, key: Key, value: Value) -> Result<(), Error> {
135 let key = key.as_str();
136
137 if let Some(trace_project_id) = self.layout.trace_project_id.as_ref() {
138 if self.trace.is_none() && self.layout.trace_keys.contains(key) {
139 self.trace = Some(format!("projects/{trace_project_id}/traces/{value}"));
140 return Ok(());
141 }
142
143 if self.span_id.is_none() && self.layout.span_id_keys.contains(key) {
144 self.span_id = Some(value.to_string());
145 return Ok(());
146 }
147
148 if self.trace_sampled.is_none() && self.layout.trace_sampled_keys.contains(key) {
149 self.trace_sampled = value.to_bool();
150 return Ok(());
151 }
152 }
153
154 let value = match serde_json::to_value(&value) {
155 Ok(value) => value,
156 Err(_) => value.to_string().into(),
157 };
158
159 if self.layout.label_keys.contains(key) {
160 self.labels.insert(key.to_owned(), value);
161 } else {
162 self.payload_fields.insert(key.to_owned(), value);
163 }
164
165 Ok(())
166 }
167}
168
169#[derive(Debug, Clone, Serialize)]
170struct SourceLocation<'a> {
171 #[serde(skip_serializing_if = "Option::is_none")]
172 file: Option<&'a str>,
173 #[serde(skip_serializing_if = "Option::is_none")]
174 line: Option<u32>,
175 #[serde(skip_serializing_if = "Option::is_none")]
176 function: Option<&'a str>,
177}
178
179#[derive(Debug, Clone, Serialize)]
180struct RecordLine<'a> {
181 #[serde(flatten)]
182 extra_fields: BTreeMap<String, serde_json::Value>,
183 severity: &'a str,
184 timestamp: jiff::Timestamp,
185 message: &'a str,
186 #[serde(skip_serializing_if = "BTreeMap::is_empty")]
187 #[serde(rename = "logging.googleapis.com/labels")]
188 labels: BTreeMap<String, serde_json::Value>,
189 #[serde(skip_serializing_if = "Option::is_none")]
190 #[serde(rename = "logging.googleapis.com/trace")]
191 trace: Option<String>,
192 #[serde(skip_serializing_if = "Option::is_none")]
193 #[serde(rename = "logging.googleapis.com/spanId")]
194 span_id: Option<String>,
195 #[serde(skip_serializing_if = "Option::is_none")]
196 #[serde(rename = "logging.googleapis.com/trace_sampled")]
197 trace_sampled: Option<bool>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 #[serde(rename = "logging.googleapis.com/sourceLocation")]
200 source_location: Option<SourceLocation<'a>>,
201}
202
203impl Layout for GoogleCloudLoggingLayout {
204 fn format(&self, record: &Record, diags: &[Box<dyn Diagnostic>]) -> Result<Vec<u8>, Error> {
205 let timestamp = jiff::Timestamp::try_from(record.time()).unwrap();
208
209 let mut visitor = KvCollector {
210 layout: self,
211 payload_fields: BTreeMap::new(),
212 labels: BTreeMap::new(),
213 trace: None,
214 span_id: None,
215 trace_sampled: None,
216 };
217
218 record.key_values().visit(&mut visitor)?;
219 for d in diags {
220 d.visit(&mut visitor)?;
221 }
222
223 let record_line = RecordLine {
224 extra_fields: visitor.payload_fields,
225 timestamp,
226 severity: record.level().as_str(),
227 message: record.payload(),
228 labels: visitor.labels,
229 trace: visitor.trace,
230 span_id: visitor.span_id,
231 trace_sampled: visitor.trace_sampled,
232 source_location: Some(SourceLocation {
233 file: record.file(),
234 line: record.line(),
235 function: record.module_path(),
236 }),
237 };
238
239 Ok(serde_json::to_vec(&record_line).unwrap())
241 }
242}