Skip to content

Commit 526a010

Browse files
committed
Start using tracing for logging
This follows @pjenvey's suggestion to start by switching the app-level logger and forward the slog records from the existing code to tracing events. Depends on: - tracing-slog for slog forwarding - tracing-appender for offloading log writes to a thread - tracing-subscriber as a framework for event subscription/formatting I hand-wrote the code to convert tracing events to MozLog entries, since `tracing-actix-web-mozlog` didn't quite format them how we wanted.
1 parent 9798558 commit 526a010

File tree

9 files changed

+482
-228
lines changed

9 files changed

+482
-228
lines changed

Cargo.lock

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

Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -55,13 +55,12 @@ slog = { version = "2.5", features = [
5555
"release_max_level_info",
5656
"dynamic-keys",
5757
] }
58-
slog-async = "2.5"
59-
slog-envlogger = "2.2.0"
60-
slog-mozlog-json = "0.1"
6158
slog-scope = "4.3"
62-
slog-stdlog = "4.1"
63-
slog-term = "2.6"
6459
tokio = "1"
60+
tracing = "0.1.40"
61+
tracing-appender = "0.2.3"
62+
tracing-slog = "0.2.0"
63+
tracing-subscriber = { version = "0.3.18", features = ["ansi", "env-filter", "json", "registry", "std", "tracing-log"] }
6564

6665
[profile.release]
6766
# Enables line numbers in Sentry reporting

syncserver/Cargo.toml

Lines changed: 4 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,12 +26,11 @@ serde_derive.workspace = true
2626
serde_json.workspace = true
2727
sha2.workspace = true
2828
slog.workspace = true
29-
slog-async.workspace = true
30-
slog-envlogger.workspace = true
31-
slog-mozlog-json.workspace = true
3229
slog-scope.workspace = true
33-
slog-stdlog.workspace = true
34-
slog-term.workspace = true
30+
tracing.workspace = true
31+
tracing-appender.workspace = true
32+
tracing-slog.workspace = true
33+
tracing-subscriber.workspace = true
3534

3635
actix-http = "3"
3736
actix-rt = "2"

syncserver/src/error.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,12 @@ impl From<std::io::Error> for ApiError {
167167
}
168168
}
169169

170+
impl From<tracing::subscriber::SetGlobalDefaultError> for ApiError {
171+
fn from(_: tracing::subscriber::SetGlobalDefaultError) -> Self {
172+
ApiErrorKind::Internal("Logging failed to initialize".to_string()).into()
173+
}
174+
}
175+
170176
impl From<ApiErrorKind> for ApiError {
171177
fn from(kind: ApiErrorKind) -> Self {
172178
let status = match &kind {

syncserver/src/logging.rs

Lines changed: 0 additions & 47 deletions
This file was deleted.

syncserver/src/logging/mod.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
mod mozlog;
2+
3+
use crate::error::ApiResult;
4+
use slog::{self, slog_o};
5+
use std::{io::stdout, sync::Once};
6+
use tracing_appender::non_blocking::WorkerGuard;
7+
use tracing_subscriber::{fmt::MakeWriter, EnvFilter};
8+
9+
/// Initialize logging for the main process
10+
///
11+
/// This sets the global tracing subscriber and must only be called once at startup time.
12+
/// Subsequent calls will panic.
13+
///
14+
/// Returns a `tracing_appender::WorkerGuard` that keeps the logging thread alive. The caller must
15+
/// ensure this this value is not dropped while the server is running.
16+
pub fn init_logging(json: bool) -> ApiResult<WorkerGuard> {
17+
let (writer, guard) = tracing_appender::non_blocking(stdout());
18+
if json {
19+
tracing::subscriber::set_global_default(json_subscriber(writer))?;
20+
} else {
21+
tracing::subscriber::set_global_default(human_subscriber(writer))?;
22+
};
23+
init_slog_drain();
24+
Ok(guard)
25+
}
26+
27+
/// Initialize logging for the tests
28+
///
29+
/// Returns a DefaultGuard that must be kept alive for the duration of the test
30+
pub fn init_test_logging() -> tracing::subscriber::DefaultGuard {
31+
init_slog_drain();
32+
tracing::subscriber::set_default(human_subscriber(stdout))
33+
}
34+
35+
fn json_subscriber<W>(writer: W) -> impl tracing::Subscriber + Send + Sync + 'static
36+
where
37+
W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
38+
{
39+
tracing_subscriber::fmt()
40+
.event_format(mozlog::EventFormatter::new())
41+
.with_env_filter(EnvFilter::from_default_env())
42+
.with_writer(writer)
43+
.finish()
44+
}
45+
46+
fn human_subscriber<W>(writer: W) -> impl tracing::Subscriber + Send + Sync + 'static
47+
where
48+
W: for<'writer> MakeWriter<'writer> + Send + Sync + 'static,
49+
{
50+
tracing_subscriber::fmt()
51+
.pretty()
52+
.with_ansi(true)
53+
.with_env_filter(EnvFilter::from_default_env())
54+
.with_writer(writer)
55+
.finish()
56+
}
57+
58+
fn init_slog_drain() {
59+
static ONCE: Once = Once::new();
60+
61+
ONCE.call_once(|| {
62+
let drain = tracing_slog::TracingSlogDrain;
63+
let logger = slog::Logger::root(drain, slog_o!());
64+
// XXX: cancel slog_scope's NoGlobalLoggerSet for now, it's difficult to
65+
// prevent it from potentially panicing during tests. reset_logging resets
66+
// the global logger during shutdown anyway:
67+
// https://github.com/slog-rs/slog/issues/169
68+
slog_scope::set_global_logger(logger).cancel_reset();
69+
});
70+
}
71+
72+
pub fn reset_logging() {
73+
let logger = slog::Logger::root(slog::Discard, slog_o!());
74+
slog_scope::set_global_logger(logger).cancel_reset();
75+
}

syncserver/src/logging/mozlog.rs

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
//! MozLog formatting for tracing events
2+
3+
use serde::{ser::SerializeMap, Serialize, Serializer};
4+
use std::{fmt, io, time::SystemTime};
5+
use tracing::{field::Visit, Event, Level, Subscriber};
6+
use tracing_subscriber::{
7+
fmt::{
8+
format::{self, FormatEvent, FormatFields},
9+
FmtContext,
10+
},
11+
registry::LookupSpan,
12+
};
13+
14+
/// Top-level formatter for a tracing [Event]
15+
pub struct EventFormatter {
16+
logger: String,
17+
hostname: String,
18+
pid: u32,
19+
}
20+
21+
impl EventFormatter {
22+
pub fn new() -> Self {
23+
Self {
24+
logger: format!("{}-{}", env!("CARGO_PKG_NAME"), env!("CARGO_PKG_VERSION")),
25+
hostname: match hostname::get() {
26+
Ok(h) => h.to_string_lossy().to_string(),
27+
Err(_) => "<unknown>".to_owned(),
28+
},
29+
pid: std::process::id(),
30+
}
31+
}
32+
}
33+
34+
impl<S, N> FormatEvent<S, N> for EventFormatter
35+
where
36+
S: Subscriber + for<'a> LookupSpan<'a>,
37+
N: for<'a> FormatFields<'a> + 'static,
38+
{
39+
fn format_event(
40+
&self,
41+
_ctx: &FmtContext<'_, S, N>,
42+
writer: format::Writer<'_>,
43+
event: &Event<'_>,
44+
) -> fmt::Result {
45+
// This closure returns a `serde_json::Result` which allows for nicer ergonomics with the
46+
// `?` operator. We map this to a `fmt::Result` at the bottom of the method.
47+
//
48+
// It serializes `event`, writes it out to `writer`, then returns the writer back so we can
49+
// write out the trailing newline.
50+
let format_with_serde = move || -> serde_json::Result<format::Writer<'_>> {
51+
let mut serializer = serde_json::Serializer::new(WriteAdaptor(writer));
52+
let mut map = serializer.serialize_map(Some(7))?;
53+
map.serialize_entry(
54+
"Timestamp",
55+
&SystemTime::now()
56+
.duration_since(SystemTime::UNIX_EPOCH)
57+
.unwrap_or_default()
58+
.as_nanos(),
59+
)?;
60+
map.serialize_entry("Type", "syncserver:log")?;
61+
map.serialize_entry("Logger", &self.logger)?;
62+
map.serialize_entry("Hostname", &self.hostname)?;
63+
map.serialize_entry("EnvVersion", "2.0")?;
64+
map.serialize_entry("Pid", &self.pid)?;
65+
map.serialize_entry(
66+
"Severity",
67+
&match *event.metadata().level() {
68+
Level::ERROR => 3,
69+
Level::WARN => 4,
70+
Level::INFO => 5,
71+
Level::DEBUG => 6,
72+
Level::TRACE => 7,
73+
},
74+
)?;
75+
map.serialize_entry("Fields", &SerializableEvent(event))?;
76+
map.end()?;
77+
Ok(serializer.into_inner().0)
78+
};
79+
let mut writer = format_with_serde().map_err(|_| fmt::Error)?;
80+
writeln!(writer)
81+
}
82+
}
83+
84+
/// Newtype that wraps `tracing::Event` and implements `serde::Serialize`. This allows us to
85+
/// serialize the event into the `Fields` field.
86+
struct SerializableEvent<'a, 'event>(&'a Event<'event>);
87+
88+
impl<'a, 'event> Serialize for SerializableEvent<'a, 'event> {
89+
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
90+
let map = serializer.serialize_map(self.0.fields().size_hint().1)?;
91+
let mut visitor = SerdeFieldVisitor::new(map);
92+
self.0.record(&mut visitor);
93+
visitor.end()
94+
}
95+
}
96+
97+
/// Implements `tracing::Visit` by serializing the fields to a `serde` map. This is how we
98+
/// serialize the `tracing::Event` with `serde`.
99+
struct SerdeFieldVisitor<S>
100+
where
101+
S: SerializeMap,
102+
{
103+
map: S,
104+
error: Option<S::Error>,
105+
}
106+
107+
impl<S> SerdeFieldVisitor<S>
108+
where
109+
S: SerializeMap,
110+
{
111+
fn new(serializer: S) -> Self {
112+
Self {
113+
map: serializer,
114+
error: None,
115+
}
116+
}
117+
118+
fn should_serialize_field(&self, field: &tracing::field::Field) -> bool {
119+
!field.name().starts_with("slog.")
120+
}
121+
122+
fn serialize_entry<V: Serialize + ?Sized>(&mut self, name: &str, value: &V) {
123+
if let Err(e) = self.map.serialize_entry(name, value) {
124+
// This is a bit awkward because serde's methods are failable, while the tracing::Visit
125+
// methods aren't. The best we can do is store the error and return it when
126+
// `check_error()` is eventually called. In practice this will probably be okay, since
127+
// the serializer will only fail on IO errors
128+
self.error = Some(e)
129+
}
130+
}
131+
132+
fn end(mut self) -> Result<S::Ok, S::Error> {
133+
match self.error.take() {
134+
Some(e) => Err(e),
135+
None => self.map.end(),
136+
}
137+
}
138+
}
139+
140+
impl<S> Visit for SerdeFieldVisitor<S>
141+
where
142+
S: SerializeMap,
143+
{
144+
fn record_f64(&mut self, field: &tracing::field::Field, value: f64) {
145+
if self.should_serialize_field(field) {
146+
self.serialize_entry(field.name(), &value)
147+
}
148+
}
149+
150+
fn record_i64(&mut self, field: &tracing::field::Field, value: i64) {
151+
if self.should_serialize_field(field) {
152+
self.serialize_entry(field.name(), &value)
153+
}
154+
}
155+
156+
fn record_u64(&mut self, field: &tracing::field::Field, value: u64) {
157+
if self.should_serialize_field(field) {
158+
self.serialize_entry(field.name(), &value)
159+
}
160+
}
161+
162+
fn record_i128(&mut self, field: &tracing::field::Field, value: i128) {
163+
if self.should_serialize_field(field) {
164+
self.serialize_entry(field.name(), &value)
165+
}
166+
}
167+
168+
fn record_u128(&mut self, field: &tracing::field::Field, value: u128) {
169+
if self.should_serialize_field(field) {
170+
self.serialize_entry(field.name(), &value)
171+
}
172+
}
173+
174+
fn record_bool(&mut self, field: &tracing::field::Field, value: bool) {
175+
if self.should_serialize_field(field) {
176+
self.serialize_entry(field.name(), &value)
177+
}
178+
}
179+
180+
fn record_str(&mut self, field: &tracing::field::Field, value: &str) {
181+
if self.should_serialize_field(field) {
182+
self.serialize_entry(field.name(), value)
183+
}
184+
}
185+
186+
fn record_error(
187+
&mut self,
188+
field: &tracing::field::Field,
189+
value: &(dyn std::error::Error + 'static),
190+
) {
191+
if self.should_serialize_field(field) {
192+
self.serialize_entry(field.name(), &value.to_string())
193+
}
194+
}
195+
196+
fn record_debug(&mut self, field: &tracing::field::Field, value: &dyn std::fmt::Debug) {
197+
if self.should_serialize_field(field) {
198+
self.serialize_entry(field.name(), &format!("{value:?}"))
199+
}
200+
}
201+
}
202+
203+
// Adapts tracing-subscriber's `Writer` struct to implement `std::io::Write`
204+
//
205+
// This is needed because `tracing` using the `std::fmt::Write` trait while `serde` uses
206+
// `std::io::Write`.
207+
struct WriteAdaptor<'writer>(format::Writer<'writer>);
208+
209+
impl<'writer> io::Write for WriteAdaptor<'writer> {
210+
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
211+
let s =
212+
std::str::from_utf8(buf).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))?;
213+
214+
self.0
215+
.write_str(s)
216+
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
217+
218+
Ok(s.as_bytes().len())
219+
}
220+
221+
fn flush(&mut self) -> io::Result<()> {
222+
Ok(())
223+
}
224+
}

0 commit comments

Comments
 (0)