Skip to content

Commit 9a4cec7

Browse files
authored
Merge pull request github#11956 from aibaars/json-log
Ruby: structured logging
2 parents c8cfb6a + a460615 commit 9a4cec7

File tree

6 files changed

+461
-103
lines changed

6 files changed

+461
-103
lines changed

ruby/Cargo.lock

Lines changed: 55 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

ruby/extractor/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,6 @@ num_cpus = "1.14.0"
2020
regex = "1.7.1"
2121
encoding = "0.2"
2222
lazy_static = "1.4.0"
23+
serde = { version = "1.0", features = ["derive"] }
24+
serde_json = "1.0"
25+
chrono = { version = "0.4.19", features = ["serde"] }

ruby/extractor/src/diagnostics.rs

Lines changed: 268 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,268 @@
1+
use serde::Serialize;
2+
use std::io::Write;
3+
use std::path::PathBuf;
4+
/** SARIF severity */
5+
#[derive(Serialize)]
6+
pub enum Severity {
7+
Error,
8+
Warning,
9+
#[allow(unused)]
10+
Note,
11+
}
12+
13+
#[derive(Serialize)]
14+
#[serde(rename_all = "camelCase")]
15+
pub struct Source {
16+
/** An identifier under which it makes sense to group this diagnostic message. This is used to build the SARIF reporting descriptor object.*/
17+
pub id: String,
18+
/** Display name for the ID. This is used to build the SARIF reporting descriptor object. */
19+
pub name: String,
20+
#[serde(skip_serializing_if = "Option::is_none")]
21+
/** Name of the CodeQL extractor. This is used to identify which tool component the reporting descriptor object should be nested under in SARIF.*/
22+
pub extractor_name: Option<String>,
23+
}
24+
25+
#[derive(Serialize)]
26+
#[serde(rename_all = "camelCase")]
27+
pub struct Visibility {
28+
#[serde(skip_serializing_if = "std::ops::Not::not")]
29+
/** True if the message should be displayed on the status page (defaults to false) */
30+
pub status_page: bool,
31+
#[serde(skip_serializing_if = "std::ops::Not::not")]
32+
/** True if the message should be counted in the diagnostics summary table printed by `codeql database analyze` (defaults to false) */
33+
pub cli_summary_table: bool,
34+
#[serde(skip_serializing_if = "std::ops::Not::not")]
35+
/** True if the message should be sent to telemetry (defaults to false) */
36+
pub telemetry: bool,
37+
}
38+
39+
#[derive(Serialize, Clone, Default)]
40+
#[serde(rename_all = "camelCase")]
41+
pub struct Location {
42+
#[serde(skip_serializing_if = "Option::is_none")]
43+
/** Path to the affected file if appropriate, relative to the source root */
44+
pub file: Option<String>,
45+
#[serde(skip_serializing_if = "Option::is_none")]
46+
pub start_line: Option<usize>,
47+
#[serde(skip_serializing_if = "Option::is_none")]
48+
pub start_column: Option<usize>,
49+
#[serde(skip_serializing_if = "Option::is_none")]
50+
pub end_line: Option<usize>,
51+
#[serde(skip_serializing_if = "Option::is_none")]
52+
pub end_column: Option<usize>,
53+
}
54+
55+
#[derive(Serialize)]
56+
#[serde(rename_all = "camelCase")]
57+
pub struct DiagnosticMessage {
58+
/** Unix timestamp */
59+
pub timestamp: chrono::DateTime<chrono::Utc>,
60+
pub source: Source,
61+
#[serde(skip_serializing_if = "String::is_empty")]
62+
/** GitHub flavored Markdown formatted message. Should include inline links to any help pages. */
63+
pub markdown_message: String,
64+
#[serde(skip_serializing_if = "String::is_empty")]
65+
/** Plain text message. Used by components where the string processing needed to support Markdown is cumbersome. */
66+
pub plaintext_message: String,
67+
#[serde(skip_serializing_if = "Vec::is_empty")]
68+
/** List of help links intended to supplement the `plaintextMessage`. */
69+
pub help_links: Vec<String>,
70+
#[serde(skip_serializing_if = "Option::is_none")]
71+
pub severity: Option<Severity>,
72+
#[serde(skip_serializing_if = "std::ops::Not::not")]
73+
pub internal: bool,
74+
#[serde(skip_serializing_if = "is_default_visibility")]
75+
pub visibility: Visibility,
76+
#[serde(skip_serializing_if = "Option::is_none")]
77+
pub location: Option<Location>,
78+
}
79+
80+
fn is_default_visibility(v: &Visibility) -> bool {
81+
!v.cli_summary_table && !v.status_page && !v.telemetry
82+
}
83+
84+
pub struct LogWriter {
85+
extractor: String,
86+
path: Option<PathBuf>,
87+
inner: Option<std::io::BufWriter<std::fs::File>>,
88+
}
89+
90+
impl LogWriter {
91+
pub fn message(&self, id: &str, name: &str) -> DiagnosticMessage {
92+
DiagnosticMessage {
93+
timestamp: chrono::Utc::now(),
94+
source: Source {
95+
id: format!("{}/{}", self.extractor, id),
96+
name: name.to_owned(),
97+
extractor_name: Some(self.extractor.to_owned()),
98+
},
99+
markdown_message: String::new(),
100+
plaintext_message: String::new(),
101+
help_links: vec![],
102+
severity: None,
103+
internal: false,
104+
visibility: Visibility {
105+
cli_summary_table: false,
106+
status_page: false,
107+
telemetry: false,
108+
},
109+
location: None,
110+
}
111+
}
112+
pub fn write(&mut self, mesg: &DiagnosticMessage) {
113+
let full_error_message = mesg.full_error_message();
114+
115+
match mesg.severity {
116+
Some(Severity::Error) => tracing::error!("{}", full_error_message),
117+
Some(Severity::Warning) => tracing::warn!("{}", full_error_message),
118+
Some(Severity::Note) => tracing::info!("{}", full_error_message),
119+
None => tracing::debug!("{}", full_error_message),
120+
}
121+
if self.inner.is_none() {
122+
if let Some(path) = self.path.as_ref() {
123+
match std::fs::OpenOptions::new()
124+
.create(true)
125+
.append(true)
126+
.write(true)
127+
.open(&path)
128+
{
129+
Err(e) => {
130+
tracing::error!(
131+
"Could not open log file '{}': {}",
132+
&path.to_string_lossy(),
133+
e
134+
);
135+
self.path = None;
136+
self.inner = None
137+
}
138+
Ok(file) => self.inner = Some(std::io::BufWriter::new(file)),
139+
}
140+
}
141+
}
142+
if let Some(mut writer) = self.inner.as_mut() {
143+
serde_json::to_writer(&mut writer, mesg)
144+
.unwrap_or_else(|e| tracing::debug!("Failed to write log entry: {}", e));
145+
&mut writer
146+
.write_all(b"\n")
147+
.unwrap_or_else(|e| tracing::debug!("Failed to write log entry: {}", e));
148+
}
149+
}
150+
}
151+
152+
pub struct DiagnosticLoggers {
153+
extractor: String,
154+
root: Option<PathBuf>,
155+
}
156+
157+
impl DiagnosticLoggers {
158+
pub fn new(extractor: &str) -> Self {
159+
let env_var = format!(
160+
"CODEQL_EXTRACTOR_{}_DIAGNOSTIC_DIR",
161+
extractor.to_ascii_uppercase()
162+
);
163+
164+
let root = match std::env::var(&env_var) {
165+
Err(e) => {
166+
tracing::error!("{}: {}", e, &env_var);
167+
None
168+
}
169+
Ok(dir) => {
170+
if let Err(e) = std::fs::create_dir_all(&dir) {
171+
tracing::error!("Failed to create log directory {}: {}", &dir, e);
172+
None
173+
} else {
174+
Some(PathBuf::from(dir))
175+
}
176+
}
177+
};
178+
DiagnosticLoggers {
179+
extractor: extractor.to_owned(),
180+
root,
181+
}
182+
}
183+
184+
pub fn logger(&self) -> LogWriter {
185+
thread_local! {
186+
static THREAD_NUM: usize = {
187+
static COUNT: std::sync::atomic::AtomicUsize = std::sync::atomic::AtomicUsize::new(0);
188+
COUNT.fetch_add(1, std::sync::atomic::Ordering::SeqCst)
189+
};
190+
}
191+
THREAD_NUM.with(|n| LogWriter {
192+
extractor: self.extractor.to_owned(),
193+
inner: None,
194+
path: self
195+
.root
196+
.as_ref()
197+
.map(|root| root.to_owned().join(format!("extractor_{}.jsonl", n))),
198+
})
199+
}
200+
}
201+
202+
impl DiagnosticMessage {
203+
pub fn full_error_message(&self) -> String {
204+
match &self.location {
205+
Some(Location {
206+
file: Some(path),
207+
start_line: Some(line),
208+
..
209+
}) => format!("{}:{}: {}", path, line, self.plaintext_message),
210+
_ => self.plaintext_message.to_owned(),
211+
}
212+
}
213+
214+
pub fn text(&mut self, text: &str) -> &mut Self {
215+
self.plaintext_message = text.to_owned();
216+
self
217+
}
218+
219+
#[allow(unused)]
220+
pub fn markdown(&mut self, text: &str) -> &mut Self {
221+
self.markdown_message = text.to_owned();
222+
self
223+
}
224+
pub fn severity(&mut self, severity: Severity) -> &mut Self {
225+
self.severity = Some(severity);
226+
self
227+
}
228+
#[allow(unused)]
229+
pub fn help_link(&mut self, link: &str) -> &mut Self {
230+
self.help_links.push(link.to_owned());
231+
self
232+
}
233+
#[allow(unused)]
234+
pub fn internal(&mut self) -> &mut Self {
235+
self.internal = true;
236+
self
237+
}
238+
#[allow(unused)]
239+
pub fn cli_summary_table(&mut self) -> &mut Self {
240+
self.visibility.cli_summary_table = true;
241+
self
242+
}
243+
pub fn status_page(&mut self) -> &mut Self {
244+
self.visibility.status_page = true;
245+
self
246+
}
247+
#[allow(unused)]
248+
pub fn telemetry(&mut self) -> &mut Self {
249+
self.visibility.telemetry = true;
250+
self
251+
}
252+
pub fn location(
253+
&mut self,
254+
path: &str,
255+
start_line: usize,
256+
start_column: usize,
257+
end_line: usize,
258+
end_column: usize,
259+
) -> &mut Self {
260+
let loc = self.location.get_or_insert(Default::default());
261+
loc.file = Some(path.to_owned());
262+
loc.start_line = Some(start_line);
263+
loc.start_column = Some(start_column);
264+
loc.end_line = Some(end_line);
265+
loc.end_column = Some(end_column);
266+
self
267+
}
268+
}

0 commit comments

Comments
 (0)