522 lines
18 KiB
Plaintext
522 lines
18 KiB
Plaintext
// Every thread logs through app_logger, which only queues the line: each thread is its own producer,
|
|
// tagged by its Thread (context.logger_data) and context.thread_index, onto one multi-producer,
|
|
// single-consumer queue (log_queue.jai). One writer thread, the queue's single consumer, formats
|
|
// each line once, writes it to the console and the log file, and hands it to the Event Log ring.
|
|
// A thread that logs never waits on the file, the ring or the Event Log panel.
|
|
|
|
LOG_SOURCE_LOCATION :: DEBUG;
|
|
APP_LOG_FILES_TO_KEEP :: 30;
|
|
|
|
LOGGER_WAKE_MS :: 250; // the writer's longest sleep, should a wake-up go astray
|
|
LOGGER_DRAIN_CHUNK :: 1024; // lines formatted and written per file write
|
|
LOGGER_CRASH_FLUSH_MS :: 1000; // how long a crash waits for its report to reach the log file
|
|
|
|
Log_Level :: enum s32 {
|
|
General :: 0;
|
|
Warning :: 1;
|
|
Error :: 2;
|
|
Fatal_Error :: 3;
|
|
}
|
|
|
|
// One logged line. A single allocation holds the struct and both strings, which point just past
|
|
// it. Queued by the thread that logged it, then owned by the writer, then by the Event Log ring.
|
|
Log_Message :: struct {
|
|
using link: Mpsc_Node; // first, so a queue node is the message
|
|
timestamp: Apollo_Time;
|
|
location: Source_Code_Location;
|
|
flags: Log_Flags;
|
|
user_flags: u32;
|
|
level: Log_Level;
|
|
thread_index: s64;
|
|
thread_name: string; // "" for an unnamed thread
|
|
message: string;
|
|
}
|
|
|
|
logger: Logger;
|
|
Logger :: struct {
|
|
// Producers.
|
|
queue: Mpsc_Queue;
|
|
wake: Semaphore;
|
|
wake_pending: bool; // a wake is signalled and not yet taken, so a burst signals once
|
|
running: bool; // lines go through the queue; otherwise app_logger writes the console only
|
|
pushed: s64; // lines ever queued
|
|
|
|
// The writer, the queue's single consumer.
|
|
writer: Thread;
|
|
writer_started: bool;
|
|
stopping: bool;
|
|
written: s64; // lines ever finished with; logger_flush waits on it
|
|
inline_mutex: Mutex; // without a writer thread, each producer drains under this instead
|
|
start_timestamp: Apollo_Time;
|
|
file_path: string;
|
|
file: File;
|
|
|
|
// The Event Log: written by the writer, read by the panel, each under ring_mutex.
|
|
ring_mutex: Mutex;
|
|
ring: Circle_Buffer(*Log_Message);
|
|
dropped: s64;
|
|
requested_capacity: s64; // set by any thread; the writer applies it
|
|
clear_requested: bool; // likewise
|
|
|
|
// Panel state; the main thread only.
|
|
text_filter: ImGui.TextFilter;
|
|
show_general: bool = DEBUG;
|
|
show_warnings: bool = DEBUG;
|
|
}
|
|
|
|
to_log_level :: (log_flags: Log_Flags) -> Log_Level {
|
|
if log_flags == .NONE
|
|
return .General;
|
|
if log_flags & .WARNING == .WARNING
|
|
return .Warning;
|
|
if log_flags & .ERROR == .ERROR
|
|
return .Error;
|
|
|
|
// We're not handling the following flags:
|
|
// CONTENT, TO_FILE_ONLY, VERBOSE_ONLY, VERY_VERBOSE_ONLY, TOPIC_ONLY
|
|
return .General;
|
|
}
|
|
|
|
get_color :: (level: Log_Level) -> Vector4 {
|
|
if #complete level == {
|
|
case .General; return app_theme.palette.accent_alt;
|
|
case .Warning; return app_theme.palette.warning;
|
|
case .Error; return app_theme.palette.error;
|
|
case .Fatal_Error; return app_theme.palette.error_dim;
|
|
}
|
|
|
|
return app_theme.palette.dim;
|
|
}
|
|
|
|
// The producer side; any thread. data is context.logger_data: the logging thread's Thread, or
|
|
// null on the main thread and on a context made from scratch.
|
|
app_logger :: (message: string, data: *void, info: Log_Info) {
|
|
if !atomic_load(*logger.running) {
|
|
console_logger(message, info.common_flags, info.user_flags);
|
|
return;
|
|
}
|
|
|
|
thread := cast(*Thread) data;
|
|
name := ifx thread then thread.name else ifx context.thread_index == 0 then "main" else "";
|
|
entry := log_message_new(message, name, context.thread_index, info);
|
|
|
|
mpsc_push(*logger.queue, *entry.link);
|
|
atomic_add(*logger.pushed, 1);
|
|
|
|
if logger.writer_started {
|
|
if !atomic_exchange(*logger.wake_pending, true) signal(*logger.wake);
|
|
} else {
|
|
lock(*logger.inline_mutex);
|
|
logger_drain();
|
|
unlock(*logger.inline_mutex);
|
|
}
|
|
}
|
|
|
|
use_app_logger :: (ctx: *#Context) #expand {
|
|
if atomic_load(*logger.running) ctx.logger = app_logger;
|
|
}
|
|
|
|
// Every thread made after init_logger logs as itself; Thread_Group workers and wavefront lanes too,
|
|
// since they are made by thread_init.
|
|
logger_thread_init :: (thread: *Thread) {
|
|
thread.starting_context.logger = app_logger;
|
|
thread.starting_context.logger_data = thread;
|
|
}
|
|
|
|
init_logger :: () { using logger;
|
|
if atomic_load(*running) then return;
|
|
push_allocator(context.default_allocator);
|
|
|
|
mpsc_init(*queue);
|
|
init(*wake);
|
|
init(*inline_mutex, "logger inline");
|
|
init(*ring_mutex, "event log");
|
|
ring.allocator = context.default_allocator;
|
|
requested_capacity = EVENT_LOG_CAPACITY_DEFAULT;
|
|
start_timestamp = current_time_consensus();
|
|
|
|
context.logger = app_logger;
|
|
context.logger_data = null;
|
|
thread_init_handler = logger_thread_init;
|
|
crash_log_hook = logger_crash_hook;
|
|
atomic_store(*running, true);
|
|
|
|
working_directory := app_install_directory();
|
|
log_directory := tprint("%/%", working_directory, APP_LOG_DIRECTORY);
|
|
file_path = sprint("%/%-%.log", log_directory, APP_FOLDER_NAME, format_datetime(start_timestamp,, temp));
|
|
if !make_directory_if_it_does_not_exist(log_directory, recursive=true) {
|
|
log_error("Failed to create the log directory %. Check permissions. Logging to the Event Log only.", log_directory);
|
|
} else {
|
|
success: bool;
|
|
file, success = file_open(file_path, for_writing=true, log_errors=true);
|
|
if !success log_error("Failed to open the log file %. Logging to the Event Log only.", file_path);
|
|
else prune_files_keeping_newest(log_directory, tprint("%-", APP_FOLDER_NAME), ".log", APP_LOG_FILES_TO_KEEP);
|
|
}
|
|
|
|
if thread_init(*writer, logger_writer_proc, name = "logger") {
|
|
writer_started = true;
|
|
thread_start(*writer);
|
|
} else {
|
|
log_error("Could not start the logger thread; each thread will write its own lines.");
|
|
}
|
|
log("Logging through the % queue.", ifx writer_started then "logger thread's" else "inline");
|
|
}
|
|
|
|
close_logger :: () { using logger;
|
|
if !atomic_load(*running) return;
|
|
push_allocator(context.default_allocator);
|
|
|
|
if writer_started {
|
|
atomic_store(*stopping, true);
|
|
signal(*wake);
|
|
thread_is_done(*writer, -1);
|
|
thread_deinit(*writer);
|
|
writer_started = false;
|
|
}
|
|
|
|
// From here a line goes straight to the console. With the writer gone this thread is the queue's
|
|
// only consumer, so it takes whatever was queued before running dropped.
|
|
atomic_store(*running, false);
|
|
thread_init_handler = null;
|
|
crash_log_hook = null;
|
|
context.logger = runtime_support_default_logger;
|
|
logger_drain();
|
|
|
|
lock(*ring_mutex);
|
|
for ring free(it,, context.default_allocator);
|
|
delete(*ring);
|
|
unlock(*ring_mutex);
|
|
|
|
if is_valid(file) file_close(*file);
|
|
string_free(file_path);
|
|
file_path = "";
|
|
}
|
|
|
|
// Asks for the Event Log to keep at most capacity entries; any thread. The writer applies it.
|
|
logger_set_event_log_capacity :: (capacity: s64) {
|
|
atomic_store(*logger.requested_capacity, clamp(capacity, EVENT_LOG_CAPACITY_MIN, EVENT_LOG_CAPACITY_MAX));
|
|
logger_wake_writer();
|
|
}
|
|
|
|
// Waits, up to timeout_ms, for every line queued before the call to be written. For the moments the
|
|
// process may end before close_logger: a session ending, a signal, a crash.
|
|
logger_flush :: (timeout_ms: s64) {
|
|
if !atomic_load(*logger.running) || !logger.writer_started return; // inline lines are written as they are logged
|
|
if context.thread_index == logger.writer.index return; // the writer cannot wait on itself
|
|
|
|
target := atomic_load(*logger.pushed);
|
|
logger_wake_writer();
|
|
start := current_time_monotonic();
|
|
while atomic_load(*logger.written) < target {
|
|
if to_milliseconds(current_time_monotonic() - start) >= timeout_ms break;
|
|
sleep_milliseconds(1);
|
|
}
|
|
}
|
|
|
|
#scope_file
|
|
|
|
log_message_new :: (message: string, thread_name: string, thread_index: s64, info: Log_Info) -> *Log_Message {
|
|
memory := cast(*u8) alloc(size_of(Log_Message) + thread_name.count + message.count,, context.default_allocator);
|
|
entry := cast(*Log_Message) memory;
|
|
memset(entry, 0, size_of(Log_Message));
|
|
|
|
entry.timestamp = current_time_consensus();
|
|
entry.location = info.location;
|
|
entry.flags = info.common_flags;
|
|
entry.user_flags = info.user_flags;
|
|
entry.level = to_log_level(info.common_flags);
|
|
entry.thread_index = thread_index;
|
|
|
|
text := memory + size_of(Log_Message);
|
|
memcpy(text, thread_name.data, thread_name.count);
|
|
entry.thread_name = .{thread_name.count, text};
|
|
memcpy(text + thread_name.count, message.data, message.count);
|
|
entry.message = .{message.count, text + thread_name.count};
|
|
return entry;
|
|
}
|
|
|
|
logger_wake_writer :: () {
|
|
if logger.writer_started && !atomic_exchange(*logger.wake_pending, true) signal(*logger.wake);
|
|
}
|
|
|
|
logger_writer_proc :: (thread: *Thread) -> s64 {
|
|
while true {
|
|
wait_for(*logger.wake, LOGGER_WAKE_MS);
|
|
atomic_store(*logger.wake_pending, false); // before draining, so a line queued during it wakes us again
|
|
stopping := atomic_load(*logger.stopping);
|
|
logger_drain();
|
|
if stopping break;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
// The consumer side: the writer, the one producer holding inline_mutex, or close_logger once the
|
|
// writer has stopped. Takes everything queued, a chunk at a time.
|
|
logger_drain :: () {
|
|
mark := get_temporary_storage_mark();
|
|
defer set_temporary_storage_mark(mark);
|
|
|
|
while true {
|
|
batch: [..] *Log_Message;
|
|
batch.allocator = temp;
|
|
builder: String_Builder;
|
|
builder.allocator = temp;
|
|
|
|
while batch.count < LOGGER_DRAIN_CHUNK {
|
|
node := mpsc_pop(*logger.queue);
|
|
if !node break;
|
|
entry := cast(*Log_Message) node;
|
|
log_format_line(*builder, entry);
|
|
console_logger(entry.message, entry.flags, entry.user_flags);
|
|
array_add(*batch, entry);
|
|
}
|
|
|
|
if is_valid(logger.file) && builder_string_length(*builder) {
|
|
file_write(*logger.file, builder_to_string(*builder,, temp));
|
|
#if OS != .WINDOWS POSIX.fflush(logger.file.handle); // WriteFile is already unbuffered
|
|
}
|
|
|
|
lock(*logger.ring_mutex);
|
|
event_log_apply_requests();
|
|
for batch {
|
|
if it.flags & .TO_FILE_ONLY {
|
|
free(it,, context.default_allocator);
|
|
continue;
|
|
}
|
|
if event_log_insert(*logger.ring, it) logger.dropped += 1;
|
|
}
|
|
unlock(*logger.ring_mutex);
|
|
|
|
atomic_add(*logger.written, batch.count);
|
|
if batch.count < LOGGER_DRAIN_CHUNK break;
|
|
set_temporary_storage_mark(mark);
|
|
}
|
|
}
|
|
|
|
// Under ring_mutex, on the consumer.
|
|
event_log_apply_requests :: () { using logger;
|
|
if atomic_exchange(*clear_requested, false) {
|
|
for ring free(it,, context.default_allocator);
|
|
reset(*ring, overwrite=true); // insert hands back a slot's old entry to free, so none may linger
|
|
dropped = 0;
|
|
}
|
|
|
|
capacity := atomic_load(*requested_capacity);
|
|
if capacity <= 0 || capacity == ring.allocated return;
|
|
|
|
dropped += event_log_resize(*ring, capacity);
|
|
log("[logger] The Event Log keeps the newest % entries.", capacity);
|
|
}
|
|
|
|
#scope_export
|
|
|
|
// Adds entry, freeing the oldest when the ring is full. True when one was dropped.
|
|
event_log_insert :: (ring: *Circle_Buffer(*Log_Message), entry: *Log_Message) -> dropped: bool {
|
|
oldest := insert(ring, entry);
|
|
if !oldest return false;
|
|
free(oldest,, context.default_allocator);
|
|
return true;
|
|
}
|
|
|
|
// Keeps the newest capacity entries and frees the rest. Returns how many were freed.
|
|
event_log_resize :: (ring: *Circle_Buffer(*Log_Message), capacity: s64) -> freed: s64 {
|
|
// resize keeps the newest entries but drops the rest without freeing them, so free those first.
|
|
excess := max(count(ring.*) - capacity, 0);
|
|
for 0..excess-1 free(ring.*[it],, context.default_allocator);
|
|
resize(ring, capacity);
|
|
return excess;
|
|
}
|
|
|
|
#scope_file
|
|
|
|
logger_crash_hook :: (crash_report: string) {
|
|
// Called from a #no_context crash too, whose fresh context has the default logger: queue it here.
|
|
info: Log_Info;
|
|
info.common_flags = .ERROR | .TO_FILE_ONLY;
|
|
app_logger(crash_report, ifx context.logger == app_logger then context.logger_data else null, info);
|
|
logger_flush(LOGGER_CRASH_FLUSH_MS);
|
|
}
|
|
|
|
log_thread_label :: (entry: *Log_Message) -> string {
|
|
if entry.thread_name.count return entry.thread_name;
|
|
return tprint("thread %", entry.thread_index);
|
|
}
|
|
|
|
log_format_line :: (builder: *String_Builder, entry: *Log_Message) {
|
|
#if LOG_SOURCE_LOCATION {
|
|
print_to_builder(builder, "[%, %:%] ", format_datetime(entry.timestamp,, temp), entry.location.fully_pathed_filename, entry.location.line_number);
|
|
} else {
|
|
print_to_builder(builder, "[%] ", format_datetime(entry.timestamp,, temp));
|
|
}
|
|
print_to_builder(builder, "[%] ", log_thread_label(entry));
|
|
if entry.flags != .NONE print_to_builder(builder, "[%] ", entry.flags);
|
|
append(builder, entry.message);
|
|
if !entry.message.count || entry.message[entry.message.count-1] != #char "\n" append(builder, "\n");
|
|
}
|
|
|
|
console_logger :: (message: string, common_flags: Log_Flags, user_flags: u32) {
|
|
if !message then return;
|
|
|
|
to_standard_error := (common_flags & .ERROR) != 0;
|
|
|
|
color := Console_Color.WHITE;
|
|
if user_flags {
|
|
color = PROGRAM_LOG_FLAGS_COLOR[user_flags];
|
|
} else {
|
|
if common_flags & .ERROR color = COLOR_ERROR;
|
|
else if common_flags & .WARNING color = COLOR_WARNING;
|
|
else if common_flags & .VERBOSE_ONLY color = COLOR_DEBUG;
|
|
else if common_flags & .VERY_VERBOSE_ONLY color = COLOR_TRACE;
|
|
}
|
|
|
|
with_console_color(color, #code {
|
|
if message[message.count-1] != #char "\n" {
|
|
write_strings(message, "\n", to_standard_error = to_standard_error);
|
|
} else {
|
|
write_string(message, to_standard_error = to_standard_error);
|
|
}
|
|
}, to_standard_error = to_standard_error);
|
|
}
|
|
|
|
PROGRAM_LOG_FLAGS_COLOR :: Console_Color.[
|
|
.BLACK, // None
|
|
.WHITE, // Info
|
|
COLOR_WARNING,
|
|
COLOR_ERROR,
|
|
COLOR_DEBUG,
|
|
COLOR_TRACE,
|
|
.GREEN, // TODO
|
|
];
|
|
|
|
COLOR_TRACE :: Console_Color.CYAN;
|
|
COLOR_DEBUG :: Console_Color.MAGENTA;
|
|
COLOR_ERROR :: Console_Color.RED;
|
|
COLOR_WARNING :: Console_Color.YELLOW;
|
|
|
|
#import "Print_Color"()(USE_ANSI_CODES_ON_WINDOWS = false);
|
|
#if OS != .WINDOWS POSIX :: #import "POSIX";
|
|
|
|
#scope_export
|
|
|
|
//
|
|
// Logger UI
|
|
//
|
|
|
|
app_logger_panel_ui :: () { using ImGui; using logger;
|
|
if !app.show_logger_panel then return;
|
|
|
|
push_allocator(temp);
|
|
|
|
push_imgui_window("Event Log", p_open=*app.show_logger_panel);
|
|
|
|
// Held for the whole draw: the rows point into the ring's entries, which the writer frees as it
|
|
// overwrites them. Only the writer waits on it, never a thread that logs.
|
|
lock(*ring_mutex);
|
|
log_count := count(ring);
|
|
if dropped Text("Log count: % (% older dropped; see the log file)", log_count, dropped);
|
|
else Text("Log count: %", log_count);
|
|
ButtonTooltip(tprint("Keeps the newest %; event_log_capacity under [logging] in settings.config.", ring.allocated));
|
|
SameLine();
|
|
Checkbox("Show info logs##logger", *logger.show_general);
|
|
SameLine();
|
|
Checkbox("Show warnings##logger", *logger.show_warnings);
|
|
SameLine();
|
|
if Button("Clear##logger") {
|
|
atomic_store(*clear_requested, true);
|
|
logger_wake_writer();
|
|
}
|
|
|
|
text_filter.Draw(*text_filter, "Filter##logger", -100.0);
|
|
|
|
// Newest first.
|
|
visible: [..] *Log_Message;
|
|
for < i: 0..log_count-1 {
|
|
message := ring[i];
|
|
if !show_general && message.level == .General continue;
|
|
if !show_warnings && message.level == .Warning continue;
|
|
if !text_filter.PassFilter(*text_filter, message.message) continue;
|
|
array_add(*visible, message);
|
|
}
|
|
|
|
// Copied once ring_mutex is released, since copying logs.
|
|
copy_text: string;
|
|
|
|
if BeginChild("scrolling##logger", .{0, 0}, .None, .HorizontalScrollbar) {
|
|
PushStyleVar(.ItemSpacing, .{0, 0}); defer PopStyleVar(); // .ItemSpacing
|
|
|
|
flags: TableFlags = .RowBg | .Resizable;
|
|
table_size := Vector2.{GetWindowWidth(), -1};
|
|
|
|
if BeginTable("Event log##logger", 4, flags, table_size) {
|
|
defer EndTable();
|
|
TableSetupColumn("Timestamp");
|
|
TableSetupColumn("Thread");
|
|
TableSetupColumn("Level");
|
|
TableSetupColumn("Message");
|
|
|
|
TableHeadersRow();
|
|
|
|
clipper: ListClipper;
|
|
clipper.Constructor(*clipper); defer clipper.Destructor(*clipper);
|
|
clipper.Begin(*clipper, xx visible.count);
|
|
|
|
while clipper.Step(*clipper) {
|
|
for row: clipper.DisplayStart..clipper.DisplayEnd-1 {
|
|
message := visible[row];
|
|
|
|
TableNextRow();
|
|
|
|
log_color := get_color(message.level);
|
|
PushStyleColor(.Text, log_color);
|
|
|
|
TableSetColumnIndex(0);
|
|
|
|
selectable_label := tprint("##sel%\0", row);
|
|
selectable_flags: SelectableFlags = .SpanAllColumns | .AllowDoubleClick;
|
|
if Selectable(selectable_label.data, false, selectable_flags) {
|
|
if IsMouseDoubleClicked(0) {
|
|
copy_text = tprint("[%, %, %] %",
|
|
format_log_timestamp(message.timestamp),
|
|
log_thread_label(message),
|
|
message.level,
|
|
message.message);
|
|
}
|
|
}
|
|
|
|
SameLine();
|
|
Text("%", format_log_timestamp(message.timestamp));
|
|
|
|
TableSetColumnIndex(1);
|
|
Text("%", log_thread_label(message));
|
|
|
|
TableSetColumnIndex(2);
|
|
Text("%", message.level);
|
|
|
|
TableSetColumnIndex(3);
|
|
Text("%", message.message);
|
|
|
|
PopStyleColor(1); // Text: log_color
|
|
}
|
|
}
|
|
}
|
|
}
|
|
EndChild();
|
|
unlock(*ring_mutex);
|
|
|
|
if copy_text {
|
|
clipboard_set_text(copy_text);
|
|
toast_show(.Success, "Copied log line");
|
|
}
|
|
}
|
|
|
|
calendar_to_log_timestamp :: (using ct: Calendar_Time) -> string {
|
|
date_part :: #bake_arguments formatInt(minimum_digits = 2);
|
|
time_string := sprint("%-%-% %:%:%", year, date_part(month_starting_at_0 + 1), date_part(day_of_month_starting_at_0 + 1), date_part(hour), date_part(minute), date_part(second));
|
|
return time_string;
|
|
}
|
|
|
|
format_log_timestamp :: (at: Apollo_Time) -> string {
|
|
current_time := to_calendar(at, time_zone=.LOCAL);
|
|
return calendar_to_log_timestamp(current_time);
|
|
}
|