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