Skip to content

Commit

Permalink
Merge pull request #136 from TicClick/logging
Browse files Browse the repository at this point in the history
  • Loading branch information
TicClick authored Oct 6, 2024
2 parents d84aa74 + 880cfc2 commit 48a482f
Show file tree
Hide file tree
Showing 8 changed files with 440 additions and 27 deletions.
21 changes: 21 additions & 0 deletions crates/steel_core/src/settings/journal.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ use serde::{Deserialize, Serialize};
#[serde(default)]
pub struct Journal {
pub app_events: AppEvents,
pub chat_events: ChatEvents,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
Expand Down Expand Up @@ -33,3 +34,23 @@ enum LevelFilterDef {
Debug,
Trace,
}

#[derive(Clone, Debug, Serialize, Deserialize)]
#[serde(default)]
pub struct ChatEvents {
pub enabled: bool,
pub directory: String,
pub format: String,
pub with_system_events: bool,
}

impl Default for ChatEvents {
fn default() -> Self {
Self {
enabled: true,
directory: "./chat-logs".to_owned(),
format: "[{date:%Y-%m-%d %H:%M:%S}] <{username}> {text}".to_owned(),
with_system_events: true,
}
}
}
51 changes: 51 additions & 0 deletions src/app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ use steel_core::chat::irc::IRCError;
use steel_core::chat::{ChatLike, ChatState, ConnectionStatus, Message};

use crate::core::irc::IRCActorHandle;
use crate::core::logging::ChatLoggerHandle;
use crate::core::updater::Updater;
use crate::core::{settings, updater};
use steel_core::ipc::{server::AppMessageIn, ui::UIMessageIn};
Expand All @@ -27,6 +28,7 @@ pub struct Application {
events: UnboundedReceiver<AppMessageIn>,

irc: IRCActorHandle,
chat_logger: Option<ChatLoggerHandle>,
updater: Option<Updater>,
ui_queue: UnboundedSender<UIMessageIn>,
pub app_queue: UnboundedSender<AppMessageIn>,
Expand All @@ -40,6 +42,7 @@ impl Application {
events,
updater: None,
irc: IRCActorHandle::new(app_queue.clone()),
chat_logger: None,
ui_queue,
app_queue,
}
Expand Down Expand Up @@ -177,6 +180,8 @@ impl Application {
self.load_settings(true);
log::set_max_level(self.state.settings.journal.app_events.level);

self.enable_chat_logger(&self.state.settings.journal.clone());

self.start_updater();
if self.state.settings.chat.autoconnect {
self.connect();
Expand All @@ -194,13 +199,50 @@ impl Application {
self.ui_handle_settings_requested();
}

fn enable_chat_logger(&mut self, logging_settings: &settings::Journal) {
self.chat_logger = Some(ChatLoggerHandle::new(
&logging_settings.chat_events.directory,
&logging_settings.chat_events.format,
));
}

fn handle_logging_settings_change(&mut self, new_settings: &settings::Journal) {
let old_settings = self.state.settings.journal.clone();
if old_settings.app_events.level != new_settings.app_events.level {
log::set_max_level(new_settings.app_events.level);
}

if old_settings.chat_events.enabled != new_settings.chat_events.enabled {
match new_settings.chat_events.enabled {
true => self.enable_chat_logger(new_settings),
false => {
if let Some(cl) = self.chat_logger.as_ref() {
cl.shutdown()
}
}
}
}

if let Some(chat_logger) = &self.chat_logger {
if old_settings.chat_events.directory != new_settings.chat_events.directory {
chat_logger.change_logging_directory(new_settings.chat_events.directory.clone());
}

if old_settings.chat_events.format != new_settings.chat_events.format {
chat_logger.change_log_format(new_settings.chat_events.format.clone());
}
}
}

pub fn ui_handle_settings_requested(&self) {
self.ui_queue
.send(UIMessageIn::SettingsChanged(self.state.settings.clone()))
.unwrap();
}

pub fn ui_handle_settings_updated(&mut self, settings: settings::Settings) {
self.handle_logging_settings_change(&settings.journal);

self.state.settings = settings;
self.state.settings.to_file(DEFAULT_SETTINGS_PATH);
}
Expand Down Expand Up @@ -286,6 +328,10 @@ impl Application {
message: Message,
switch_if_missing: bool,
) {
if let Some(chat_logger) = &self.chat_logger {
chat_logger.log(target.clone(), message.clone());
}

self.maybe_remember_chat(&target, switch_if_missing);
self.ui_queue
.send(UIMessageIn::NewMessageReceived { target, message })
Expand Down Expand Up @@ -370,6 +416,11 @@ impl Application {
if name.is_channel() {
self.leave_channel(name);
}

if let Some(chat_logger) = &self.chat_logger {
chat_logger.close_log(normalized);
}

self.ui_queue
.send(UIMessageIn::ChatClosed(name.to_owned()))
.unwrap();
Expand Down
224 changes: 224 additions & 0 deletions src/core/logging.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
use std::collections::{hash_map::Entry, HashMap};
use std::fmt::Write as FmtWrite;
use std::fs::File;
use std::io::Write as IOWrite;
use std::path::{Path, PathBuf};

use steel_core::chat::Message;
use tokio::sync::mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender};

use crate::actor::ActorHandle;

pub enum LoggingRequest {
LogMessage { chat_name: String, message: Message },
CloseLog { chat_name: String },
ChangeLogFormat { log_line_format: String },
ChangeLoggingDirectory { logging_directory: String },
ShutdownLogger,
}

pub struct ChatLoggerHandle {
channel: UnboundedSender<LoggingRequest>,
}

impl ActorHandle for ChatLoggerHandle {}

impl ChatLoggerHandle {
pub fn new(log_directory: &str, log_line_format: &str) -> Self {
let (tx, rx) = unbounded_channel();
let mut actor = ChatLoggerBackend::new(log_directory, log_line_format, rx);
std::thread::spawn(move || {
actor.run();
});
Self { channel: tx }
}

pub fn log(&self, chat_name: String, message: Message) {
let _ = self
.channel
.send(LoggingRequest::LogMessage { chat_name, message });
}

pub fn close_log(&self, chat_name: String) {
let _ = self.channel.send(LoggingRequest::CloseLog { chat_name });
}

pub fn change_log_format(&self, log_line_format: String) {
let _ = self
.channel
.send(LoggingRequest::ChangeLogFormat { log_line_format });
}

pub fn change_logging_directory(&self, logging_directory: String) {
let _ = self
.channel
.send(LoggingRequest::ChangeLoggingDirectory { logging_directory });
}

pub fn shutdown(&self) {
let _ = self.channel.send(LoggingRequest::ShutdownLogger);
}
}

struct ChatLoggerBackend {
log_directory: PathBuf,
log_line_format: String,
channel: UnboundedReceiver<LoggingRequest>,
files: HashMap<PathBuf, File>,
}

impl ChatLoggerBackend {
fn new(
log_directory: &str,
log_line_format: &str,
channel: UnboundedReceiver<LoggingRequest>,
) -> Self {
Self {
log_directory: Path::new(&log_directory).to_path_buf(),
log_line_format: log_line_format.to_owned(),
channel,
files: HashMap::new(),
}
}

fn run(&mut self) {
while let Some(evt) = self.channel.blocking_recv() {
match evt {
LoggingRequest::LogMessage { chat_name, message } => {
if self.log(chat_name, message).is_err() {
return;
}
}
LoggingRequest::ChangeLogFormat { log_line_format } => {
self.log_line_format = log_line_format;
}
LoggingRequest::ChangeLoggingDirectory { logging_directory } => {
log::info!(
"Chat logging directory has been changed: {:?} -> {}",
self.log_directory,
logging_directory
);
self.log_directory = Path::new(&logging_directory).to_path_buf();
self.files.clear();
}
LoggingRequest::CloseLog { chat_name } => self.close(chat_name),
LoggingRequest::ShutdownLogger => return,
}
}
}

fn chat_path(&self, chat_name: &str) -> PathBuf {
self.log_directory
.join(chat_name.to_lowercase())
.with_extension("log")
}

fn log(&mut self, chat_name: String, message: Message) -> std::io::Result<()> {
if self.files.is_empty() {
if let Err(e) = std::fs::create_dir_all(&self.log_directory) {
log::error!(
"Failed to create the directory for storing chat logs: {}",
e
);
return Err(e);
}
}

let target_path = self.chat_path(&chat_name);
let (is_new_file, mut f) = match self.files.entry(target_path.clone()) {
Entry::Occupied(e) => (false, e.into_mut()),
Entry::Vacant(e) => {
match std::fs::OpenOptions::new()
.read(true)
.create(true)
.append(true)
.open(target_path)
{
Ok(handle) => (true, e.insert(handle)),
Err(e) => {
log::error!(
"Failed to open or create the chat log for {}: {}",
chat_name,
e
);
return Err(e);
}
}
}
};

if is_new_file {
if let Err(e) = writeln!(&mut f, "\n") {
log::error!(
"Failed to start a new logging session for {}: {}",
chat_name,
e
);
return Err(e);
}
}

let formatted_message = format_message_for_logging(&self.log_line_format, &message);
if let Err(e) = writeln!(&mut f, "{}", formatted_message) {
log::error!("Failed to append a chat log line for {}: {}", chat_name, e);
return Err(e);
}

Ok(())
}

fn close(&mut self, chat_name: String) {
let target_path = self.chat_path(&chat_name);
if let Entry::Occupied(e) = self.files.entry(target_path) {
e.remove_entry();
}
}
}

pub fn format_message_for_logging(log_line_format: &str, message: &Message) -> String {
let mut result = String::new();
let mut placeholder = String::new();
let mut in_placeholder = false;

for c in log_line_format.chars() {
match c {
'{' => {
in_placeholder = true;
placeholder.clear();
}
'}' => {
if in_placeholder {
result.push_str(&resolve_placeholder(&placeholder, message));
in_placeholder = false;
} else {
result.push(c);
}
}
_ => {
if in_placeholder {
placeholder.push(c);
} else {
result.push(c);
}
}
}
}

result
}

fn resolve_placeholder(placeholder: &str, message: &Message) -> String {
if let Some(date_format) = placeholder.strip_prefix("date:") {
let mut buf = String::new();
match write!(&mut buf, "{}", message.time.format(date_format)) {
Ok(_) => buf,
Err(_) => format!("{{date:{}}}", date_format),
}
} else {
match placeholder {
"username" => message.username.clone(),
"text" => message.text.clone(),
_ => String::from("{unknown}"),
}
}
}
1 change: 1 addition & 0 deletions src/core/mod.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
pub mod irc;
pub mod logging;
pub mod os;
pub mod sound;
pub mod updater;
Expand Down
Loading

0 comments on commit 48a482f

Please sign in to comment.