Files
engine/modules/agent_log/agent_log.cpp
ozan d291dcdc74 feat: 9 agentic engine modules for agent-native Godot
agent_api (HTTP server), agent_log (structured logging), agent_events (event bus),
agent_console (GameConsole), agent_replay (snapshots), agent_vision (depth/segmentation),
agent_fbx (bone remapping), agent_auth (multi-agent), agent_analytics (feature flags + tracking)

All modules compile clean with mono. Binary uploaded to S3 v1.0.0.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-05-15 03:44:28 +01:00

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