Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions codex-rs/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions codex-rs/state/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ clap = { workspace = true, features = ["derive", "env"] }
codex-otel = { workspace = true }
codex-protocol = { workspace = true }
dirs = { workspace = true }
owo-colors = { workspace = true }
serde = { workspace = true, features = ["derive"] }
serde_json = { workspace = true }
sqlx = { workspace = true }
Expand Down
1 change: 0 additions & 1 deletion codex-rs/state/migrations/0002_logs.sql
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ CREATE TABLE logs (
level TEXT NOT NULL,
target TEXT NOT NULL,
message TEXT,
fields_json TEXT NOT NULL,
module_path TEXT,
file TEXT,
line INTEGER
Expand Down
36 changes: 27 additions & 9 deletions codex-rs/state/src/bin/logs_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use chrono::Utc;
use clap::Parser;
use codex_state::STATE_DB_FILENAME;
use dirs::home_dir;
use owo_colors::OwoColorize;
use sqlx::QueryBuilder;
use sqlx::Row;
use sqlx::Sqlite;
Expand Down Expand Up @@ -64,8 +65,6 @@ struct LogRow {
ts_nanos: i64,
level: String,
message: Option<String>,
fields_json: String,
module_path: Option<String>,
file: Option<String>,
line: Option<i64>,
}
Expand Down Expand Up @@ -237,7 +236,7 @@ async fn fetch_max_id(pool: &SqlitePool, filter: &LogFilter) -> anyhow::Result<i

fn base_select_builder<'a>() -> QueryBuilder<'a, Sqlite> {
QueryBuilder::<Sqlite>::new(
"SELECT id, ts, ts_nanos, level, message, fields_json, module_path, file, line FROM logs WHERE 1 = 1",
"SELECT id, ts, ts_nanos, level, message, file, line FROM logs WHERE 1 = 1",
)
}

Expand Down Expand Up @@ -269,19 +268,38 @@ fn push_filters<'a>(builder: &mut QueryBuilder<'a, Sqlite>, filter: &'a LogFilte

fn format_row(row: &LogRow) -> String {
let timestamp = format_timestamp(row.ts, row.ts_nanos);
let level = row.level.as_str();
let location = match (&row.file, row.line) {
(Some(file), Some(line)) => format!("{file}:{line}"),
(Some(file), None) => file.clone(),
_ => "-".to_string(),
};
let module = row.module_path.as_deref().unwrap_or("-");
let message = row.message.as_deref().unwrap_or("");
let fields = row.fields_json.as_str();
let level = row.level.as_str();
if fields == "{}" || fields.is_empty() {
return format!("{timestamp} {level:<5} [{module}] {location} - {message}");
let level_colored = color_level(level);
let timestamp_colored = timestamp.dimmed().to_string();
let location_colored = location.dimmed().to_string();
let message_colored = message.bold().to_string();
format!("{timestamp_colored} {level_colored} {location_colored} - {message_colored}")
}

fn color_level(level: &str) -> String {
let padded = format!("{level:<5}");
if level.eq_ignore_ascii_case("error") {
return padded.red().bold().to_string();
}
if level.eq_ignore_ascii_case("warn") {
return padded.yellow().bold().to_string();
}
if level.eq_ignore_ascii_case("info") {
return padded.green().bold().to_string();
}
if level.eq_ignore_ascii_case("debug") {
return padded.blue().bold().to_string();
}
if level.eq_ignore_ascii_case("trace") {
return padded.magenta().bold().to_string();
}
format!("{timestamp} {level:<5} [{module}] {location} - {message} {fields}")
padded.bold().to_string()
}

fn format_timestamp(ts: i64, ts_nanos: i64) -> String {
Expand Down
33 changes: 13 additions & 20 deletions codex-rs/state/src/log_db.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ use std::time::Duration;
use std::time::SystemTime;
use std::time::UNIX_EPOCH;

use serde_json::Value;
use tokio::sync::mpsc;
use tracing::Event;
use tracing::field::Field;
Expand Down Expand Up @@ -54,7 +53,7 @@ where
{
fn on_event(&self, event: &Event<'_>, _ctx: tracing_subscriber::layer::Context<'_, S>) {
let metadata = event.metadata();
let mut visitor = JsonVisitor::default();
let mut visitor = MessageVisitor::default();
event.record(&mut visitor);

let now = SystemTime::now()
Expand All @@ -66,7 +65,6 @@ where
level: metadata.level().as_str().to_string(),
target: metadata.target().to_string(),
message: visitor.message,
fields_json: Value::Object(visitor.fields).to_string(),
module_path: metadata.module_path().map(ToString::to_string),
file: metadata.file().map(ToString::to_string),
line: metadata.line().map(|line| line as i64),
Expand Down Expand Up @@ -114,49 +112,44 @@ async fn flush(state_db: &std::sync::Arc<StateRuntime>, buffer: &mut Vec<LogEntr
}

#[derive(Default)]
struct JsonVisitor {
fields: serde_json::Map<String, Value>,
struct MessageVisitor {
message: Option<String>,
}

impl JsonVisitor {
fn record_value(&mut self, field: &Field, value: Value) {
impl MessageVisitor {
fn record_message(&mut self, field: &Field, value: String) {
if field.name() == "message" && self.message.is_none() {
self.message = Some(match &value {
Value::String(message) => message.clone(),
_ => value.to_string(),
});
self.message = Some(value);
}
self.fields.insert(field.name().to_string(), value);
}
}

impl Visit for JsonVisitor {
impl Visit for MessageVisitor {
fn record_i64(&mut self, field: &Field, value: i64) {
self.record_value(field, Value::from(value));
self.record_message(field, value.to_string());
}

fn record_u64(&mut self, field: &Field, value: u64) {
self.record_value(field, Value::from(value));
self.record_message(field, value.to_string());
}

fn record_bool(&mut self, field: &Field, value: bool) {
self.record_value(field, Value::from(value));
self.record_message(field, value.to_string());
}

fn record_f64(&mut self, field: &Field, value: f64) {
self.record_value(field, Value::from(value));
self.record_message(field, value.to_string());
}

fn record_str(&mut self, field: &Field, value: &str) {
self.record_value(field, Value::from(value));
self.record_message(field, value.to_string());
}

fn record_error(&mut self, field: &Field, value: &(dyn std::error::Error + 'static)) {
self.record_value(field, Value::from(value.to_string()));
self.record_message(field, value.to_string());
}

fn record_debug(&mut self, field: &Field, value: &dyn std::fmt::Debug) {
self.record_value(field, Value::from(format!("{value:?}")));
self.record_message(field, format!("{value:?}"));
}
}
1 change: 0 additions & 1 deletion codex-rs/state/src/model/log.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ pub struct LogEntry {
pub level: String,
pub target: String,
pub message: Option<String>,
pub fields_json: String,
pub module_path: Option<String>,
pub file: Option<String>,
pub line: Option<i64>,
Expand Down
3 changes: 1 addition & 2 deletions codex-rs/state/src/runtime.rs
Original file line number Diff line number Diff line change
Expand Up @@ -214,15 +214,14 @@ FROM threads
}

let mut builder = QueryBuilder::<Sqlite>::new(
"INSERT INTO logs (ts, ts_nanos, level, target, message, fields_json, module_path, file, line) ",
"INSERT INTO logs (ts, ts_nanos, level, target, message, module_path, file, line) ",
);
builder.push_values(entries, |mut row, entry| {
row.push_bind(entry.ts)
.push_bind(entry.ts_nanos)
.push_bind(&entry.level)
.push_bind(&entry.target)
.push_bind(&entry.message)
.push_bind(&entry.fields_json)
.push_bind(&entry.module_path)
.push_bind(&entry.file)
.push_bind(entry.line);
Expand Down
Loading