#include "agent_log.h" #include "core/io/file_access.h" #include "core/io/json.h" #include "core/os/os.h" #include "core/os/time.h" #include "core/string/print_string.h" // Forward declaration for SSE push. #include "modules/agent_api/agent_server.h" AgentLog *AgentLog::singleton = nullptr; AgentLog::AgentLog() { singleton = this; // Register error handler to capture push_error/push_warning. error_handler.errfunc = _error_handler; error_handler.userdata = this; add_error_handler(&error_handler); // Register print handler to capture print() calls. print_handler.printfunc = _print_handler; print_handler.userdata = this; add_print_handler(&print_handler); } AgentLog::~AgentLog() { remove_error_handler(&error_handler); remove_print_handler(&print_handler); disable_file_sink(); singleton = nullptr; } void AgentLog::_bind_methods() { ClassDB::bind_method(D_METHOD("log_trace", "category", "message", "data"), &AgentLog::log_trace, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("log_debug", "category", "message", "data"), &AgentLog::log_debug, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("log_info", "category", "message", "data"), &AgentLog::log_info, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("log_warn", "category", "message", "data"), &AgentLog::log_warn, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("log_error", "category", "message", "data"), &AgentLog::log_error, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("log_fatal", "category", "message", "data"), &AgentLog::log_fatal, DEFVAL(Dictionary())); ClassDB::bind_method(D_METHOD("get_entries", "count", "min_level", "category", "since_msec"), &AgentLog::get_entries, DEFVAL(100), DEFVAL(LEVEL_TRACE), DEFVAL(""), DEFVAL(0)); ClassDB::bind_method(D_METHOD("get_entry_count"), &AgentLog::get_entry_count); ClassDB::bind_method(D_METHOD("set_min_level", "level"), &AgentLog::set_min_level); ClassDB::bind_method(D_METHOD("get_min_level"), &AgentLog::get_min_level); ClassDB::bind_method(D_METHOD("enable_file_sink", "path"), &AgentLog::enable_file_sink); ClassDB::bind_method(D_METHOD("disable_file_sink"), &AgentLog::disable_file_sink); ClassDB::bind_method(D_METHOD("clear"), &AgentLog::clear); BIND_ENUM_CONSTANT(LEVEL_TRACE); BIND_ENUM_CONSTANT(LEVEL_DEBUG); BIND_ENUM_CONSTANT(LEVEL_INFO); BIND_ENUM_CONSTANT(LEVEL_WARN); BIND_ENUM_CONSTANT(LEVEL_ERROR); BIND_ENUM_CONSTANT(LEVEL_FATAL); } // --- Logging Methods --- void AgentLog::log_trace(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_TRACE, p_category, p_message, p_data); } void AgentLog::log_debug(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_DEBUG, p_category, p_message, p_data); } void AgentLog::log_info(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_INFO, p_category, p_message, p_data); } void AgentLog::log_warn(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_WARN, p_category, p_message, p_data); } void AgentLog::log_error(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_ERROR, p_category, p_message, p_data); } void AgentLog::log_fatal(const String &p_category, const String &p_message, const Dictionary &p_data) { log_message(LEVEL_FATAL, p_category, p_message, p_data); } void AgentLog::log_message(Level p_level, const String &p_category, const String &p_message, const Dictionary &p_data) { if (p_level < min_level) { return; } LogEntry entry; entry.timestamp_msec = OS::get_singleton()->get_ticks_msec(); entry.level = p_level; entry.category = p_category; entry.message = p_message; entry.data = p_data; _write_entry(entry); } void AgentLog::_write_entry(const LogEntry &p_entry) { LogEntry entry = p_entry; { MutexLock lock(buffer_mutex); entry.id = next_id++; ring_buffer[write_pos] = entry; write_pos = (write_pos + 1) % RING_BUFFER_SIZE; if (entry_count < RING_BUFFER_SIZE) { entry_count++; } } // Write to file sink. if (file_sink_enabled) { _write_to_file(entry); } // Push to SSE clients via AgentServer. AgentServer *server = AgentServer::get_singleton(); if (server && server->is_running()) { Dictionary dict = _entry_to_dict(entry); server->push_sse("logs", "log", JSON::stringify(dict)); } } void AgentLog::_write_to_file(const LogEntry &p_entry) { if (file_sink.is_null()) { return; } Dictionary dict = _entry_to_dict(p_entry); String line = JSON::stringify(dict) + "\n"; file_sink->store_string(line); file_sink->flush(); } // --- Error/Print Hooks --- void AgentLog::_error_handler(void *p_self, const char *p_func, const char *p_file, int p_line, const char *p_error, const char *p_errorexp, bool p_editor_notify, ErrorHandlerType p_type) { AgentLog *self = static_cast(p_self); Level level; String category = "engine"; switch (p_type) { case ERR_HANDLER_ERROR: level = LEVEL_ERROR; break; case ERR_HANDLER_WARNING: level = LEVEL_WARN; break; default: level = LEVEL_INFO; break; } String message = String::utf8(p_error); if (p_errorexp && p_errorexp[0]) { message += ": " + String::utf8(p_errorexp); } Dictionary data; data["function"] = String::utf8(p_func); data["file"] = String::utf8(p_file); data["line"] = p_line; self->log_message(level, category, message, data); } void AgentLog::_print_handler(void *p_self, const String &p_string, bool p_error, bool p_rich) { AgentLog *self = static_cast(p_self); // Don't re-log our own output. if (p_string.begins_with("AgentLog:") || p_string.begins_with("AgentServer:")) { return; } Level level = p_error ? LEVEL_ERROR : LEVEL_DEBUG; self->log_message(level, "print", p_string, Dictionary()); } // --- Query --- Array AgentLog::get_entries(int p_count, Level p_min_level, const String &p_category, uint64_t p_since_msec) { Array result; MutexLock lock(buffer_mutex); int start = (entry_count < RING_BUFFER_SIZE) ? 0 : write_pos; int count = entry_count; // Walk the ring buffer from newest to oldest. for (int i = count - 1; i >= 0 && result.size() < p_count; i--) { int idx = (start + i) % RING_BUFFER_SIZE; const LogEntry &entry = ring_buffer[idx]; if (entry.level < p_min_level) { continue; } if (!p_category.is_empty() && entry.category != p_category) { continue; } if (p_since_msec > 0 && entry.timestamp_msec < p_since_msec) { break; // Entries are ordered by time. } result.push_back(_entry_to_dict(entry)); } return result; } // --- File Sink --- void AgentLog::enable_file_sink(const String &p_path) { disable_file_sink(); file_sink = FileAccess::open(p_path, FileAccess::WRITE); if (file_sink.is_valid()) { file_sink_enabled = true; file_sink_path = p_path; } } void AgentLog::disable_file_sink() { file_sink_enabled = false; file_sink.unref(); } void AgentLog::clear() { MutexLock lock(buffer_mutex); write_pos = 0; entry_count = 0; } // --- Utilities --- String AgentLog::_level_string(Level p_level) { switch (p_level) { case LEVEL_TRACE: return "trace"; case LEVEL_DEBUG: return "debug"; case LEVEL_INFO: return "info"; case LEVEL_WARN: return "warn"; case LEVEL_ERROR: return "error"; case LEVEL_FATAL: return "fatal"; default: return "unknown"; } } Dictionary AgentLog::_entry_to_dict(const LogEntry &p_entry) { Dictionary dict; dict["id"] = p_entry.id; dict["timestamp_msec"] = p_entry.timestamp_msec; dict["level"] = _level_string(p_entry.level); dict["category"] = p_entry.category; dict["message"] = p_entry.message; if (!p_entry.data.is_empty()) { dict["data"] = p_entry.data; } return dict; }