Skip to content

Commit e85e61b

Browse files
committed
Ruby: add diagnostics module
1 parent 664fdc3 commit e85e61b

File tree

3 files changed

+276
-17
lines changed

3 files changed

+276
-17
lines changed

ruby/Cargo.lock

Lines changed: 19 additions & 17 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: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,3 +20,5 @@ num_cpus = "1.13.0"
2020
regex = "1.5.5"
2121
encoding = "0.2"
2222
lazy_static = "1.4.0"
23+
serde = { version = "1.0", features = ["derive"] }
24+
serde_json = "1.0"

ruby/extractor/src/diagnostics.rs

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

0 commit comments

Comments
 (0)