257 lines
7.6 KiB
C++
257 lines
7.6 KiB
C++
|
|
#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<AgentLog *>(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<AgentLog *>(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;
|
||
|
|
}
|