Skip to content

Commit 6a0fa58

Browse files
committed
Log errors as JSON, independently from GitLab
While thinking on #6 and the accidental / intentional use of gitlab.output, it occurred to me that the way we show errors for GitLab is not actually how we'd want them logged with JSON logging (which currently dumps the entire error string, *including ANSI codes*, inside the JSON objects). With this in mind, I realized we can get the color-eyre handler's span traces and just convert it to JSON manually. This would be all, if we could actually log JSON from tracing. Unfortunately, any extra fields logged are converted to strings via Debug and stored as such in the JSON fields. That means we end up with our beautiful JSON errors stuck as strings and filled with dozens of escape sequences, which also makes them entirely unreadable without taking the JSON apart. But! tracing has "unstable" (can change in a bugfix release, but isn't that what Cargo.lock is for anyway?) functionality that integrates with the valuable crate, such that any field value that's a Valuable will be serialized as JSON. So, the eyre Report is converted to an intermediate Valuable that gets sent to tracing, which dutifully serializes it as an actual JSON object. This does, however, still have an unfortunate missing piece, which is serializing the fields in the span trace. tracing-error serializes fields into a string to be retrieved when the span trace is collected, which isn't the JSON format we would want. Now, this *can* be changed to format JSON, but that's not very useful because: - It means that span trace fields will be shown as JSON in the GitLab logs too, which isn't great. - There is no straightforward way to go from a JSON string to a Valuable: valuable only includes ways to go serialize a Valuable via Serde, without any way of *deserializing*. This would only be possible by manually converting everything, which is increasing the already-high complexity. In practice, the individual field values don't really have complex JSON values, so leaving the default non-JSON formatting there is fine. Signed-off-by: Ryan Gonzalez <[email protected]>
1 parent 4090455 commit 6a0fa58

File tree

7 files changed

+204
-9
lines changed

7 files changed

+204
-9
lines changed

.cargo/config.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[build]
2+
rustflags = ["--cfg", "tracing_unstable"]

.vscode/settings.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"rust-analyzer.cargo.extraEnv": {
3+
"RUSTFLAGS": "--cfg tracing_unstable"
4+
}
5+
}

Cargo.lock

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

Cargo.toml

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,10 +27,11 @@ strum = { version = "0.24", features = ["derive"] }
2727
tempfile = "3.3"
2828
tokio = { version = "1.16", features = ["full"] }
2929
tokio-util = { version = "0.7", features = ["full"] }
30-
tracing = "0.1"
30+
tracing = { version = "0.1", features = ["valuable"] }
3131
tracing-error = "0.2"
32-
tracing-subscriber = { version = "0.3", features = ["default", "json"] }
32+
tracing-subscriber = { version = "0.3", features = ["default", "json", "valuable"] }
3333
url = "2.2"
34+
valuable = { version = "0.1.0", features = ["derive"] }
3435

3536
gitlab-runner = "0.0.5"
3637
# gitlab-runner = { path = "../gitlab-runner-rs/gitlab-runner" }

src/errors.rs

Lines changed: 142 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
use color_eyre::{
2+
config::EyreHook,
3+
eyre::{EyreHandler, InstallError},
4+
Report,
5+
};
6+
use tracing::error;
7+
use tracing_error::{ExtractSpanTrace, SpanTrace};
8+
use valuable::Valuable;
9+
10+
struct Handler {
11+
inner: Box<dyn EyreHandler>,
12+
location: Option<&'static std::panic::Location<'static>>,
13+
}
14+
15+
impl Handler {
16+
fn new(inner: Box<dyn EyreHandler>) -> Self {
17+
assert!(inner.downcast_ref::<color_eyre::Handler>().is_some());
18+
19+
Self {
20+
inner,
21+
location: None,
22+
}
23+
}
24+
25+
fn span_trace(&self) -> Option<&SpanTrace> {
26+
self.inner
27+
.downcast_ref::<color_eyre::Handler>()
28+
.unwrap()
29+
.span_trace()
30+
}
31+
}
32+
33+
impl EyreHandler for Handler {
34+
fn debug(
35+
&self,
36+
error: &(dyn std::error::Error + 'static),
37+
f: &mut core::fmt::Formatter<'_>,
38+
) -> core::fmt::Result {
39+
self.inner.debug(error, f)
40+
}
41+
42+
fn display(
43+
&self,
44+
error: &(dyn std::error::Error + 'static),
45+
f: &mut core::fmt::Formatter<'_>,
46+
) -> core::fmt::Result {
47+
self.inner.display(error, f)
48+
}
49+
50+
fn track_caller(&mut self, location: &'static std::panic::Location<'static>) {
51+
self.location = Some(location);
52+
self.inner.track_caller(location)
53+
}
54+
}
55+
56+
#[derive(Valuable)]
57+
struct LocationValue {
58+
// These field names mimic the ones in regular span traces.
59+
filename: &'static str,
60+
line_number: u32,
61+
}
62+
63+
#[derive(Valuable)]
64+
struct SpanFrameValue {
65+
name: String,
66+
location: Option<LocationValue>,
67+
fields: String,
68+
}
69+
70+
#[derive(Valuable)]
71+
struct ReportValue {
72+
location: Option<LocationValue>,
73+
errors: Vec<String>,
74+
spans: Vec<SpanFrameValue>,
75+
}
76+
77+
fn get_span_trace_value(span_trace: &SpanTrace) -> Vec<SpanFrameValue> {
78+
let mut trace: Vec<SpanFrameValue> = vec![];
79+
span_trace.with_spans(|span, fields| {
80+
trace.push(SpanFrameValue {
81+
name: span.name().to_owned(),
82+
location: match (span.file(), span.line()) {
83+
(Some(filename), Some(line_number)) => Some(LocationValue {
84+
filename,
85+
line_number,
86+
}),
87+
(_, _) => None,
88+
},
89+
fields: fields.to_owned(),
90+
});
91+
true
92+
});
93+
trace
94+
}
95+
96+
impl ReportValue {
97+
fn from_report(report: &Report) -> Option<Self> {
98+
report
99+
.handler()
100+
.downcast_ref::<Handler>()
101+
.map(|handler| Self {
102+
location: handler.location.map(|location| LocationValue {
103+
filename: location.file(),
104+
line_number: location.line(),
105+
}),
106+
errors: report
107+
.chain()
108+
.filter_map(|error| {
109+
// The is_none() is something color-eyre does here too,
110+
// but I'm honestly not sure why.
111+
if error.span_trace().is_none() {
112+
Some(format!("{}", error))
113+
} else {
114+
None
115+
}
116+
})
117+
.collect(),
118+
spans: handler
119+
.span_trace()
120+
// If the handler has no span trace, get the one closest to
121+
// the root cause.
122+
.or_else(|| report.chain().rev().filter_map(|e| e.span_trace()).next())
123+
.map(get_span_trace_value)
124+
.unwrap_or_default(),
125+
})
126+
}
127+
}
128+
129+
pub fn install_json_report_hook(hook: EyreHook) -> Result<(), InstallError> {
130+
let error_hook = hook.into_eyre_hook();
131+
color_eyre::eyre::set_hook(Box::new(move |error| {
132+
Box::new(Handler::new(error_hook(error)))
133+
}))
134+
}
135+
136+
pub fn log_report(report: Report) {
137+
if let Some(report) = ReportValue::from_report(&report) {
138+
error!(report = &report.as_value());
139+
} else {
140+
error!(?report);
141+
}
142+
}

src/handler.rs

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ use open_build_service_api as obs;
2121
use serde::{Deserialize, Serialize};
2222
use tokio::{fs::File as AsyncFile, io::AsyncSeekExt};
2323
use tokio_util::{compat::FuturesAsyncWriteCompatExt, io::ReaderStream};
24-
use tracing::{debug, error, instrument};
24+
use tracing::{debug, instrument};
2525

2626
use crate::{
2727
artifacts::{save_to_tempfile, ArtifactDirectory},
2828
binaries::download_binaries,
2929
build_meta::{BuildHistoryRetrieval, BuildMeta, CommitBuildInfo, RepoArch},
30+
errors::log_report,
3031
monitor::{MonitoredPackage, ObsMonitor, PackageCompletion, PackageMonitoringOptions},
3132
pipeline::{generate_monitor_pipeline, GeneratePipelineOptions, PipelineDownloadBinaries},
3233
prune::prune_branch,
@@ -603,15 +604,17 @@ async fn upload_artifact(
603604

604605
#[async_trait]
605606
impl JobHandler for ObsJobHandler {
607+
#[tracing::instrument(skip(self))]
606608
async fn step(&mut self, script: &[String], _phase: Phase) -> JobResult {
607609
for command in script {
608610
if let Err(err) = self.command(command).await {
609611
// Failed builds would already have information on them printed
610612
// above, so don't print anything on them again.
611613
if !err.is::<FailedBuild>() {
612-
error!(gitlab.output = true, "Error running command: {:?}", err);
614+
outputln!("Error running command: {:?}", err);
613615
}
614616

617+
log_report(err);
615618
self.script_failed = true;
616619
return Err(());
617620
}
@@ -620,12 +623,14 @@ impl JobHandler for ObsJobHandler {
620623
Ok(())
621624
}
622625

626+
#[tracing::instrument(skip(self, uploader))]
623627
async fn upload_artifacts(&mut self, uploader: &mut Uploader) -> JobResult {
624628
let mut success = true;
625629

626630
for (name, file) in &mut self.artifacts {
627631
if let Err(err) = upload_artifact(name.clone(), file, uploader).await {
628-
error!(gitlab.output = true, "Failed to upload {}: {:?}", name, err);
632+
outputln!("Failed to upload {}: {:?}", name, err);
633+
log_report(err);
629634
success = false;
630635
}
631636
}

src/main.rs

Lines changed: 15 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,23 @@
11
use std::{fmt, str::FromStr};
22

33
use clap::Parser;
4-
use color_eyre::eyre::Result;
4+
use color_eyre::{config::HookBuilder, eyre::Result};
55
use gitlab_runner::Runner;
66
use strum::{Display, EnumString};
77
use tracing::{error, info};
88
use tracing_subscriber::{filter::targets::Targets, prelude::*, util::SubscriberInitExt, Layer};
99
use url::Url;
1010

11-
use crate::handler::{HandlerOptions, ObsJobHandler};
11+
use crate::{
12+
errors::install_json_report_hook,
13+
handler::{HandlerOptions, ObsJobHandler},
14+
};
1215

1316
mod artifacts;
1417
mod binaries;
1518
mod build_meta;
1619
mod dsc;
20+
mod errors;
1721
mod handler;
1822
mod monitor;
1923
mod pipeline;
@@ -52,7 +56,7 @@ impl fmt::Display for TargetsArg {
5256
}
5357
}
5458

55-
#[derive(Display, EnumString)]
59+
#[derive(Display, EnumString, PartialEq, Eq)]
5660
#[strum(serialize_all = "lowercase")]
5761
enum LogFormat {
5862
Pretty,
@@ -118,7 +122,14 @@ async fn main() {
118122
.init(),
119123
}
120124

121-
color_eyre::install().unwrap();
125+
let (panic_hook, eyre_hook) = HookBuilder::default().into_hooks();
126+
panic_hook.install();
127+
128+
if args.log_format == LogFormat::Json {
129+
install_json_report_hook(eyre_hook).unwrap();
130+
} else {
131+
eyre_hook.install().unwrap();
132+
}
122133

123134
info!("Starting runner...");
124135
runner

0 commit comments

Comments
 (0)