// 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); }