Skip to content

Commit ecc00ca

Browse files
authored
Add reducer name to database log records (#3274)
# Description of Changes Extends log records with a new field, `function`, which stores the name of the reducer being executed when the log was produced. I have chosen to name this field `function` rather than `reducer` because we will soon be adding procedures, which are not reducers but will also be valid values for this field. While making this change, I noticed inconsistent values for injected logs. Previously, we injected logs in three places, with different values for the record fields: 1. `SystemLogger` (used when publishing and updating) set `filename: Some("spacetimedb")` and `target: None`. 2. `log_reducer_error` (used for reducer error returns) set `filename: None` and `target: Some(reducer)`, with `reducer` being the name of the reducer. 3. `ModuleHost::inject_logs` (used for calls to nonexistent reducers and calls with ill-typed arguments) set `filename: Some("external")` and `target: None`. With this change, I have decided that injected logs universally have `filename: Some("__spacetimedb__")` and `target: Some("__spacetimedb__")`. I have chosen the double-underscore convention for reserved names to avoid confusion should a user name a source file or reducer `spacetimedb`. I am not terribly attached to using `spacetimedb` here, and would be happy to change to some other string, like `internal` or `system`. I have further decided that `log_reducer_error` and `inject_logs` both have access to a sensible non-`None` value for `function` and so should set it to the name of the reducer. The `target` field is not used for this. Note that in cases where a client attempts to call a non-existent reducer, the `function` field of the log record will be set to that client-supplied non-existent reducer name. # API and ABI breaking changes Changes our log record format. This is at least backwards-compatible (as in, newer consumers can read older logs) because the new field is optional. I am unsure if it is forwards-compatible (as in, older consumers reading newer logs) because I don't know if `serde_json` will ignore unknown fields or will error. # Expected complexity level and risk 1 # Testing - [x] Manually ran `quickstart-chat` (modified to log messages and name changes) and saw sensible output.
1 parent ddbebbc commit ecc00ca

6 files changed

Lines changed: 98 additions & 25 deletions

File tree

crates/cli/src/subcommands/logs.rs

Lines changed: 34 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ pub enum LogLevel {
6161
Panic,
6262
}
6363

64+
/// Sentinel value used for injected system logs.
65+
///
66+
/// Keep this in sync with the constants in `spacetimedb_core::database_logger::Record`.
67+
const SENTINEL: &str = "__spacetimedb__";
68+
69+
/// Keep this in sync with `spacetimedb_core::database_logger::Record`.
6470
#[serde_with::serde_as]
6571
#[derive(serde::Deserialize)]
6672
struct Record<'a> {
@@ -74,6 +80,8 @@ struct Record<'a> {
7480
filename: Option<Cow<'a, str>>,
7581
line_number: Option<u32>,
7682
#[serde(borrow)]
83+
function: Option<Cow<'a, str>>,
84+
#[serde(borrow)]
7785
message: Cow<'a, str>,
7886
trace: Option<Vec<BacktraceFrame<'a>>>,
7987
}
@@ -195,16 +203,36 @@ pub async fn exec(mut config: Config, args: &ArgMatches) -> Result<(), anyhow::E
195203
out.set_color(&color)?;
196204
write!(out, "{level:>5}: ")?;
197205
out.reset()?;
206+
let mut need_space_before_filename = false;
207+
let mut need_colon_sep = false;
198208
let dimmed = ColorSpec::new().set_dimmed(true).clone();
209+
if let Some(function) = record.function {
210+
if function != SENTINEL {
211+
out.set_color(&dimmed)?;
212+
write!(out, "{function}")?;
213+
out.reset()?;
214+
need_space_before_filename = true;
215+
need_colon_sep = true;
216+
}
217+
}
199218
if let Some(filename) = record.filename {
200-
out.set_color(&dimmed)?;
201-
write!(out, "{filename}")?;
202-
if let Some(line) = record.line_number {
203-
write!(out, ":{line}")?;
219+
if filename != SENTINEL {
220+
out.set_color(&dimmed)?;
221+
if need_space_before_filename {
222+
write!(out, " ")?;
223+
}
224+
write!(out, "{filename}")?;
225+
if let Some(line) = record.line_number {
226+
write!(out, ":{line}")?;
227+
}
228+
out.reset()?;
229+
need_colon_sep = true;
204230
}
205-
out.reset()?;
206231
}
207-
writeln!(out, ": {}", record.message)?;
232+
if need_colon_sep {
233+
write!(out, ": ")?;
234+
}
235+
writeln!(out, "{}", record.message)?;
208236
if let Some(trace) = &record.trace {
209237
for frame in trace {
210238
write!(out, " in ")?;

crates/core/src/database_logger.rs

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -49,12 +49,52 @@ impl From<u8> for LogLevel {
4949
pub struct Record<'a> {
5050
#[serde_as(as = "serde_with::TimestampMicroSeconds")]
5151
pub ts: chrono::DateTime<Utc>,
52+
/// Target of the log call (usually source namespace or `mod`).
53+
///
54+
/// Provided by the WASM guest as an argument to the `console_log` host function.
55+
///
56+
/// The special sentinel value [`Record::SENTINEL_INJECTED_TARGET`]` denotes logs injected by the [`SystemLogger`].
5257
pub target: Option<&'a str>,
58+
/// Filename of the source location of the log call.
59+
///
60+
/// Provided by the WASM guest as an argument to the `console_log` host function.
61+
///
62+
/// The special sentinel value [`Record::SENTINEL_INJECTED_FILENAME`]` denotes logs injected by the [`SystemLogger`].
5363
pub filename: Option<&'a str>,
5464
pub line_number: Option<u32>,
65+
/// Which exported function (i.e. reducer) was being called when this message was produced.
66+
///
67+
/// Unlike `target`, `filename` and `line_number`, this is not provided by the WASM guest.
68+
/// Instead, the `WasmInstanceEnv` remembers what function call is in progress and adds it to the record.
69+
///
70+
/// The special sentinel value [`Record::SENTINEL_INJECTED_FUNCTION`] denotes logs injected by the [`SystemLogger`].
71+
pub function: Option<&'a str>,
5572
pub message: &'a str,
5673
}
5774

75+
impl<'a> Record<'a> {
76+
pub const SENTINEL_INJECTED_FUNCTION: Option<&'static str> = Some("__spacetimedb__");
77+
pub const SENTINEL_INJECTED_TARGET: Option<&'static str> = Some("__spacetimedb__");
78+
pub const SENTINEL_INJECTED_FILENAME: Option<&'static str> = Some("__spacetimedb__");
79+
80+
/// Create a log `Record` for a system message, not attributed to any reducer or user filename.
81+
///
82+
/// The resulting `Record` will draw from [`chrono::Utc::now`] for its timestamp,
83+
/// have `line_number: None`,
84+
/// and will use [`Self::SENTINEL_INJECTED_FILENAME`], [`Self::SENTINEL_INJECTED_FUNCTION`]
85+
/// and [`Self::SENTINEL_INJECTED_TARGET`].
86+
pub fn injected(message: &'a str) -> Self {
87+
Record {
88+
ts: chrono::Utc::now(),
89+
target: Self::SENTINEL_INJECTED_TARGET,
90+
filename: Self::SENTINEL_INJECTED_FILENAME,
91+
line_number: None,
92+
function: Self::SENTINEL_INJECTED_FUNCTION,
93+
message,
94+
}
95+
}
96+
}
97+
5898
pub trait BacktraceProvider {
5999
fn capture(&self) -> Box<dyn ModuleBacktrace>;
60100
}
@@ -277,12 +317,6 @@ impl SystemLogger {
277317
}
278318

279319
fn record(message: &str) -> Record {
280-
Record {
281-
ts: Utc::now(),
282-
target: None,
283-
filename: Some("spacetimedb"),
284-
line_number: None,
285-
message,
286-
}
320+
Record::injected(message)
287321
}
288322
}

crates/core/src/host/module_host.rs

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1021,7 +1021,7 @@ impl ModuleHost {
10211021
_ => None,
10221022
};
10231023
if let Some(log_message) = log_message {
1024-
self.inject_logs(LogLevel::Error, &log_message)
1024+
self.inject_logs(LogLevel::Error, reducer_name, &log_message)
10251025
}
10261026

10271027
res
@@ -1110,15 +1110,12 @@ impl ModuleHost {
11101110
tokio::join!(self.module.scheduler().closed(), self.job_tx.closed());
11111111
}
11121112

1113-
pub fn inject_logs(&self, log_level: LogLevel, message: &str) {
1113+
pub fn inject_logs(&self, log_level: LogLevel, reducer_name: &str, message: &str) {
11141114
self.replica_ctx().logger.write(
11151115
log_level,
11161116
&Record {
1117-
ts: chrono::Utc::now(),
1118-
target: None,
1119-
filename: Some("external"),
1120-
line_number: None,
1121-
message,
1117+
function: Some(reducer_name),
1118+
..Record::injected(message)
11221119
},
11231120
&(),
11241121
)

crates/core/src/host/wasm_common/module_host_actor.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -584,12 +584,12 @@ fn maybe_log_long_running_reducer(reducer_name: &str, total_duration: Duration)
584584

585585
/// Logs an error `message` for `reducer` at `timestamp` into `replica_ctx`.
586586
fn log_reducer_error(replica_ctx: &ReplicaContext, timestamp: Timestamp, reducer: &str, message: &str) {
587-
let record = database_logger::Record {
587+
use database_logger::Record;
588+
589+
let record = Record {
588590
ts: chrono::DateTime::from_timestamp_micros(timestamp.to_micros_since_unix_epoch()).unwrap(),
589-
target: Some(reducer),
590-
filename: None,
591-
line_number: None,
592-
message,
591+
function: Some(reducer),
592+
..Record::injected(message)
593593
};
594594
replica_ctx.logger.write(database_logger::LogLevel::Error, &record, &());
595595
}

crates/core/src/host/wasmtime/wasm_instance_env.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,6 +164,13 @@ impl WasmInstanceEnv {
164164
&self.reducer_name
165165
}
166166

167+
/// Returns the name of the most recent reducer to be run in this environment,
168+
/// or `None` if no reducer is actively being invoked.
169+
fn log_record_function(&self) -> Option<&str> {
170+
let function = self.reducer_name();
171+
(!function.is_empty()).then_some(function)
172+
}
173+
167174
/// Returns the name of the most recent reducer to be run in this environment.
168175
pub fn reducer_start(&self) -> Instant {
169176
self.reducer_start
@@ -1145,12 +1152,15 @@ impl WasmInstanceEnv {
11451152
// The line number cannot be `u32::MAX` as this represents `Option::None`.
11461153
let line_number = (line_number != u32::MAX).then_some(line_number);
11471154

1155+
let function = env.log_record_function();
1156+
11481157
let record = Record {
11491158
// TODO: figure out whether to use walltime now or logical reducer now (env.reducer_start)
11501159
ts: chrono::Utc::now(),
11511160
target: target.as_deref(),
11521161
filename: filename.as_deref(),
11531162
line_number,
1163+
function,
11541164
message: &message,
11551165
};
11561166

@@ -1191,12 +1201,14 @@ impl WasmInstanceEnv {
11911201

11921202
let elapsed = span.start.elapsed();
11931203
let message = format!("Timing span {:?}: {:?}", &span.name, elapsed);
1204+
let function = caller.data().log_record_function();
11941205

11951206
let record = Record {
11961207
ts: chrono::Utc::now(),
11971208
target: None,
11981209
filename: None,
11991210
line_number: None,
1211+
function,
12001212
message: &message,
12011213
};
12021214
caller.data().instance_env.console_log(

modules/quickstart-chat/src/lib.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ fn validate_name(name: String) -> Result<String, String> {
2727
pub fn set_name(ctx: &ReducerContext, name: String) -> Result<(), String> {
2828
let name = validate_name(name)?;
2929
if let Some(user) = ctx.db.user().identity().find(ctx.sender) {
30+
log::info!("User {} sets name to {name}", ctx.sender);
3031
ctx.db.user().identity().update(User {
3132
name: Some(name),
3233
..user
@@ -51,6 +52,7 @@ pub fn send_message(ctx: &ReducerContext, text: String) -> Result<(), String> {
5152
// - Rate-limit messages per-user.
5253
// - Reject messages from unnamed user.
5354
let text = validate_message(text)?;
55+
log::info!("User {}: {text}", ctx.sender);
5456
ctx.db.message().insert(Message {
5557
sender: ctx.sender,
5658
text,

0 commit comments

Comments
 (0)