1use chrono::{SecondsFormat, Utc};
2use serde::Serialize;
3use serde_json::to_string;
4use std::env;
5use tracing::field::{Field, Visit};
6use tracing::span::{Attributes, Id};
7use tracing::{Event, Subscriber};
8use tracing_subscriber::filter::LevelFilter;
9use tracing_subscriber::layer::Context;
10use tracing_subscriber::prelude::*;
11use tracing_subscriber::registry::LookupSpan;
12use tracing_subscriber::{registry, Layer};
13
14struct TraceId(String);
15
16#[derive(Default)]
17struct TraceIdVisitor {
18 trace_id: Option<String>,
19}
20
21impl Visit for TraceIdVisitor {
22 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
23 if field.name() == "trace_id" {
24 self.trace_id = Some(format!("{value:?}"))
25 }
26 }
27}
28
29#[derive(Default)]
30struct EventVisitor {
31 message: Option<String>,
32}
33
34impl Visit for EventVisitor {
35 fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
36 if field.name() == "message" {
37 self.message = Some(format!("{value:?}"))
38 }
39 }
40}
41
42struct GcpLayer {
43 gcp_project_id: String,
44}
45
46#[derive(Serialize)]
47struct SourceLocation {
48 file: String,
49 line: String,
50 function: String,
51}
52
53#[derive(Serialize)]
54struct LogEntry<'a> {
55 severity: &'a str,
56 message: String,
57 time: String,
58 #[serde(rename = "logging.googleapis.com/trace")]
59 #[serde(skip_serializing_if = "Option::is_none")]
60 trace: Option<String>,
61 #[serde(rename = "logging.googleapis.com/sourceLocation")]
62 #[serde(skip_serializing_if = "Option::is_none")]
63 source_location: Option<SourceLocation>,
64}
65
66impl<S> Layer<S> for GcpLayer
67where
68 S: Subscriber + for<'lookup> LookupSpan<'lookup>,
69{
70 fn on_new_span(&self, attrs: &Attributes<'_>, id: &Id, ctx: Context<'_, S>) {
71 if let Some(span) = ctx.span(id) {
72 let mut visitor = TraceIdVisitor::default();
73 attrs.record(&mut visitor);
74 if let Some(trace_id) = visitor.trace_id {
75 span.extensions_mut().insert(TraceId(trace_id));
76 }
77 };
78 }
79
80 fn on_event(&self, event: &Event<'_>, ctx: Context<'_, S>) {
81 let mut trace = None;
82 if let Some(scope) = ctx.event_scope(event) {
83 for span in scope.from_root() {
84 let extensions = span.extensions();
85 let Some(trace_id) = extensions.get::<TraceId>() else {
86 continue;
87 };
88 let t = &trace_id.0;
89 trace = Some(format!("projects/{}/traces/{t}", self.gcp_project_id));
90 }
91 }
92 let mut visitor = EventVisitor::default();
93 event.record(&mut visitor);
94
95 let metadata = event.metadata();
96 let source_location = metadata.file().map(|file| SourceLocation {
97 file: file.to_string(),
98 line: metadata.line().unwrap_or(0).to_string(),
99 function: metadata.target().to_string(),
100 });
101
102 let entry = LogEntry {
103 severity: event.metadata().level().as_str(),
104 message: visitor.message.unwrap_or_default(),
105 time: Utc::now().to_rfc3339_opts(SecondsFormat::Millis, true),
106 trace,
107 source_location,
108 };
109 eprintln!("{}", to_string(&entry).unwrap());
110 }
111}
112
113fn fetch_project_id() -> Result<String, Box<dyn std::error::Error>> {
118 let host = env::var("GCE_METADATA_HOST").unwrap_or_else(|_| "169.254.169.254".to_string());
119 let url = format!("http://{}/computeMetadata/v1/project/project-id", host);
120
121 let response = ureq::get(&url)
122 .set("Metadata-Flavor", "Google")
123 .timeout(std::time::Duration::from_secs(2))
124 .call()?;
125
126 let project_id = response.into_string()?.trim().to_string();
127 Ok(project_id)
128}
129
130pub struct Config {
132 pub gcp_project_id: Option<String>,
135 pub level_filter: Option<LevelFilter>,
137}
138
139impl Config {
140 pub fn new() -> Self {
143 Self {
144 gcp_project_id: None,
145 level_filter: None,
146 }
147 }
148
149 pub fn with_project_id(gcp_project_id: impl Into<String>) -> Self {
152 Self {
153 gcp_project_id: Some(gcp_project_id.into()),
154 level_filter: None,
155 }
156 }
157
158 pub fn with_level(mut self, level: LevelFilter) -> Self {
160 self.level_filter = Some(level);
161 self
162 }
163}
164
165impl Default for Config {
166 fn default() -> Self {
167 Self::new()
168 }
169}
170
171pub fn init(config: Config) {
213 let gcp_project_id = config
214 .gcp_project_id
215 .or_else(|| fetch_project_id().ok())
216 .unwrap_or_else(|| "unknown".to_string());
217
218 let layer = GcpLayer { gcp_project_id };
219 let level_filter = config.level_filter.unwrap_or(LevelFilter::INFO);
220 registry().with(layer.with_filter(level_filter)).init();
221}
222
223#[cfg(test)]
224mod tests {
225 use super::*;
226 use tracing::{info, info_span, warn};
227
228 #[test]
229 fn test_gcp_log_format() {
230 init(Config::with_project_id("test-project-123"));
231
232 info!("Application started");
234
235 let span = info_span!("trace_id", trace_id = %"abc123");
237 let _guard = span.enter();
238 info!("Processing request");
239 warn!("Potential issue detected");
240 }
241}