118 lines
3.5 KiB
GDScript
118 lines
3.5 KiB
GDScript
extends Node
|
|
## Structured event recorder matching @lilith/flight-recorder schema.
|
|
## Reads DEBUG flag from ../.env (relative to res://).
|
|
## When DEBUG=TRUE: streams colored human-readable lines to stdout
|
|
## and appends JSON entries to ../logs/flight_recorder.jsonl.
|
|
##
|
|
## Schema: {"date","time","source","type","content","metadata"}
|
|
## Usage: FlightRecorder.record("system.event", "Human description", {"key": value})
|
|
|
|
const RESET := "\u001b[0m"
|
|
const DIM := "\u001b[90m"
|
|
const RED := "\u001b[31m"
|
|
const GREEN := "\u001b[32m"
|
|
const YELLOW := "\u001b[33m"
|
|
const BLUE := "\u001b[34m"
|
|
const MAGENTA := "\u001b[35m"
|
|
const CYAN := "\u001b[36m"
|
|
const WHITE := "\u001b[37m"
|
|
|
|
const TYPE_PAD := 24
|
|
|
|
const PREFIX_COLORS := {
|
|
"llm": CYAN,
|
|
"stt": GREEN,
|
|
"tts": MAGENTA,
|
|
"companion": YELLOW,
|
|
"config": BLUE,
|
|
"app_state": BLUE,
|
|
"flight_recorder": DIM,
|
|
}
|
|
|
|
var _debug: bool = false
|
|
var _record_file: FileAccess
|
|
var _log_path: String
|
|
|
|
|
|
func _ready() -> void:
|
|
_debug = _read_debug_flag()
|
|
if not _debug:
|
|
return
|
|
_log_path = _project_root().path_join("logs/flight_recorder.jsonl")
|
|
DirAccess.make_dir_recursive_absolute(_project_root().path_join("logs"))
|
|
_record_file = FileAccess.open(_log_path, FileAccess.READ_WRITE)
|
|
if _record_file != null:
|
|
_record_file.seek_end(0)
|
|
else:
|
|
_record_file = FileAccess.open(_log_path, FileAccess.WRITE)
|
|
record("flight_recorder.init", "Flight recorder started", {"path": _log_path})
|
|
|
|
|
|
func record(type: String, content: String, metadata: Dictionary = {}) -> void:
|
|
if not _debug:
|
|
return
|
|
var dt := Time.get_datetime_dict_from_system()
|
|
var ms := Time.get_ticks_msec() % 1000
|
|
var time_str := "%02d:%02d:%02d.%03d" % [dt.hour, dt.minute, dt.second, ms]
|
|
var entry := {
|
|
"date": "%04d-%02d-%02d" % [dt.year, dt.month, dt.day],
|
|
"time": time_str,
|
|
"source": "godot",
|
|
"type": type,
|
|
"content": content,
|
|
"metadata": metadata if not metadata.is_empty() else null,
|
|
}
|
|
var json_line := JSON.stringify(entry)
|
|
print(_format_colored(time_str, type, content, entry.metadata))
|
|
if _record_file != null:
|
|
_record_file.store_line(json_line)
|
|
_record_file.flush()
|
|
|
|
|
|
func _format_colored(time_str: String, type: String, content: String, metadata: Variant) -> String:
|
|
var color := _color_for_type(type)
|
|
var padded_type := type
|
|
if padded_type.length() < TYPE_PAD:
|
|
padded_type += " ".repeat(TYPE_PAD - padded_type.length())
|
|
var meta_str := _format_metadata(metadata)
|
|
return DIM + time_str + RESET + " " + color + padded_type + RESET + " " + content + meta_str
|
|
|
|
|
|
func _color_for_type(type: String) -> String:
|
|
if type.ends_with(".error"):
|
|
return RED
|
|
var prefix := type.split(".")[0]
|
|
return PREFIX_COLORS.get(prefix, WHITE)
|
|
|
|
|
|
func _format_metadata(metadata: Variant) -> String:
|
|
if metadata == null:
|
|
return ""
|
|
if metadata is Dictionary and metadata.is_empty():
|
|
return ""
|
|
var parts: PackedStringArray = []
|
|
for key: String in metadata:
|
|
parts.append("%s=%s" % [key, str(metadata[key])])
|
|
return " " + DIM + "(" + ", ".join(parts) + ")" + RESET
|
|
|
|
|
|
func _notification(what: int) -> void:
|
|
if what == NOTIFICATION_PREDELETE and _record_file != null:
|
|
_record_file.close()
|
|
|
|
|
|
func _project_root() -> String:
|
|
return ProjectSettings.globalize_path("res://").path_join("..")
|
|
|
|
|
|
func _read_debug_flag() -> bool:
|
|
var env_path := _project_root().path_join(".env")
|
|
var file := FileAccess.open(env_path, FileAccess.READ)
|
|
if file == null:
|
|
return false
|
|
while not file.eof_reached():
|
|
var line := file.get_line().strip_edges()
|
|
if line.begins_with("DEBUG="):
|
|
var value := line.substr(6).strip_edges().to_upper()
|
|
return value == "TRUE" or value == "1"
|
|
return false
|