└── addons └── logger ├── _LogInternalPrinter.gd ├── _ObjectParser.gd ├── config.gd ├── log-stream.gd ├── logger.gd ├── plugin.cfg ├── plugin.gd └── settings.gd /addons/logger/_LogInternalPrinter.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends RefCounted 3 | 4 | ##Intrenal singleton that handles all the printing of logs. 5 | ##If modifications to the logging behaviour is required, this is the place to do it. 6 | ##Generally, an entry is processed per message, and it is processed with _internal_log. 7 | ##Using this and the methods that this calles should be a good start for editing. 8 | ##They are called by a separate thread and thread safe between eachother. Storing data between the threads is done through the _LogEntry class. 9 | class_name _LogInternalPrinter 10 | 11 | ##Log uses two queues, one for writing intries to, and one for processing, that way, the thread is more independent and has potentially higher speed. 12 | static var _front_queue: Array[_LogEntry] 13 | static var _back_queue: Array[_LogEntry] 14 | static var _front_queue_size: int = 0 15 | 16 | const _settings := preload("./settings.gd") 17 | 18 | static var _log_mutex = Mutex.new() 19 | static var _log_thread = Thread.new() 20 | ##Flag for marking when the log thread should stop looping. 21 | static var _is_quitting = false 22 | ##Used for benchmarking the time it takes to log a message on the main thread. Microbe is WIP, but see github.com/albinaask/Microbe for more info. 23 | #static var push_microbe = Microbe.new("push to queue") 24 | 25 | ##Settings are set in ProjectSettings. Look for Log and set them from there. See 'res://addons/logger/settings.gd' for explanations on what each setting do. 26 | static var BREAK_ON_ERROR:bool = _settings._ensure_setting_exists(_settings.BREAK_ON_ERROR_KEY, _settings.BREAK_ON_ERROR_DEFAULT_VALUE) 27 | static var PRINT_TREE_ON_ERROR:bool = _settings._ensure_setting_exists(_settings.PRINT_TREE_ON_ERROR_KEY, _settings.PRINT_TREE_ON_ERROR_DEFAULT_VALUE) 28 | static var CYCLE_BREAK_TIME:int = _settings._ensure_setting_exists(_settings.MIN_PRINTING_CYCLE_TIME_KEY, _settings.MIN_PRINTING_CYCLE_TIME_DEFAULT_VALUE) 29 | static var USE_UTC_TIME_FORMAT:bool = _settings._ensure_setting_exists(_settings.USE_UTC_TIME_FORMAT_KEY, _settings.USE_UTC_TIME_FORMAT_DEFAULT_VALUE) 30 | static var GLOBAL_PATH_LIST = ProjectSettings.get_global_class_list() 31 | static var BB_CODE_REMOVER_REGEX = RegEx.new() 32 | static var VALUE_PRIMER_STRING:String = _settings._ensure_setting_exists(_settings.VALUE_PRIMER_STRING_KEY, _settings.VALUE_PRIMER_STRING_DEFAULT_VALUE) 33 | 34 | ##Note that these are in the same order as LogStream.LogLevel 35 | static var MESSAGE_FORMAT_STRINGS:Array[String] = [ 36 | _settings._ensure_setting_exists(_settings.DEBUG_MESSAGE_FORMAT_KEY, _settings.DEBUG_MESSAGE_FORMAT_DEFAULT_VALUE), 37 | _settings._ensure_setting_exists(_settings.INFO_MESSAGE_FORMAT_KEY, _settings.INFO_MESSAGE_FORMAT_DEFAULT_VALUE), 38 | _settings._ensure_setting_exists(_settings.WARNING_MESSAGE_FORMAT_KEY, _settings.WARNING_MESSAGE_FORMAT_DEFAULT_VALUE), 39 | _settings._ensure_setting_exists(_settings.ERROR_MESSAGE_FORMAT_KEY, _settings.ERROR_MESSAGE_FORMAT_DEFAULT_VALUE), 40 | _settings._ensure_setting_exists(_settings.FATAL_MESSAGE_FORMAT_KEY, _settings.FATAL_MESSAGE_FORMAT_DEFAULT_VALUE) 41 | ] 42 | 43 | 44 | ##Every method in this class is static, therefore a static constructor is used. 45 | static func _static_init() -> void: 46 | var queue_size = _settings._ensure_setting_exists(_settings.LOG_QUEUE_SIZE_KEY, _settings.LOG_QUEUE_SIZE_DEFAULT_VALUE) 47 | BB_CODE_REMOVER_REGEX.compile("\\[(lb|rb)\\]|\\[.*?\\]") 48 | 49 | ProjectSettings.settings_changed.connect(LogStream.sync_project_settings) 50 | if _log_thread.start(_process_logs, Thread.PRIORITY_LOW) != OK: 51 | #TODO: update this to call _internal_log. 52 | #TODO: Make Log able to log from same thread... 53 | printerr("Log error: Couldn't create Log thread. This should never happen, please contact dev. No messages will be printed") 54 | 55 | _front_queue.resize(queue_size) 56 | _back_queue.resize(queue_size) 57 | for i in range(queue_size): 58 | _front_queue[i] = _LogEntry.new() 59 | _back_queue[i] = _LogEntry.new() 60 | 61 | func _ns_push_to_queue(stream_name:String, message:String, message_level:int, stream_level:int, crash_behaviour:Callable, on_log_message_signal_emission_callback:Callable, values:Variant) -> void: 62 | _push_to_queue(stream_name, message, message_level, stream_level, crash_behaviour, on_log_message_signal_emission_callback, values) 63 | 64 | 65 | ##Pushes a log entry onto the front (input queue). This is done so Log can process the log asynchronously. 66 | ##Method is called by LogStream.info and the like. 67 | static func _push_to_queue(stream_name:String, message:String, message_level:int, stream_level:int, crash_behaviour:Callable, on_log_message_signal_emission_callback:Callable, values:Variant = null)-> void: 68 | #push_microbe.start() 69 | if message_level < stream_level: 70 | # push_microbe.finish() 71 | return 72 | 73 | _log_mutex.lock() 74 | #push_microbe.finish_sub_step("lock") 75 | var entry:_LogEntry 76 | if _is_front_queue_full(): 77 | entry = _front_queue[_front_queue_size-1] 78 | entry.stream_name = "Log" 79 | entry.message = "Log queue overflow. Print less messages or increase queue size in project settings or squash messages into one long." 80 | entry.message_level = LogStream.LogLevel.WARN 81 | else: 82 | entry = _front_queue[_front_queue_size] 83 | entry.stream_name = stream_name 84 | entry.message = message 85 | entry.message_level = message_level 86 | entry.crash_behaviour = crash_behaviour 87 | entry.values = values 88 | _front_queue_size += 1 89 | #push_microbe.finish_sub_step("entry assignment") 90 | entry.stack = get_stack() 91 | #push_microbe.finish_sub_step("get_stack") 92 | _log_mutex.unlock() 93 | #push_microbe.finish_sub_step("unlock") 94 | 95 | if message_level >= LogStream.LogLevel.ERROR: 96 | push_error(message) 97 | if BREAK_ON_ERROR: 98 | ##Please go a few steps down the stack to find the errorous code, since you are currently inside the Log error handler, which is probably not what you want. 99 | breakpoint 100 | elif message_level == LogStream.LogLevel.WARN: 101 | push_warning(message) 102 | #push_microbe.finish_sub_step("if statements") 103 | if message_level == LogStream.LogLevel.FATAL: 104 | entry.crash_behaviour.call() 105 | on_log_message_signal_emission_callback.call(message) 106 | #push_microbe.finish_sub_step("signal emission") 107 | #push_microbe.finish() 108 | 109 | ##Helper method for checking if the front queue is full. 110 | static func _is_front_queue_full() -> bool: 111 | return _front_queue_size == _front_queue.size() 112 | 113 | ##Main loop of the logging thread. Runs until godot is quit or a fatal error occurs. 114 | static func _process_logs(): 115 | while !_is_quitting: 116 | var start = Time.get_ticks_usec() 117 | #Swap th buffers, so that the back queue is now the front queue and prepare for overriding the front queue with new messages. 118 | _log_mutex.lock() 119 | var temp = _front_queue 120 | _front_queue = _back_queue 121 | _back_queue = temp 122 | var _back_queue_size = _front_queue_size 123 | _front_queue_size = 0 124 | _log_mutex.unlock() 125 | 126 | for i in range(_back_queue_size): 127 | _internal_log(_back_queue[i]) 128 | var delta = Time.get_ticks_usec() - start 129 | #Sleep for a while, depending on the time it took to process the messages, 130 | OS.delay_usec(max(CYCLE_BREAK_TIME*1000-delta, 0)) 131 | 132 | ##Main internal logging method, please use the methods in LogStream instead of this from the outside, since this is NOT thread safe in any regard and not designed to be used. 133 | static func _internal_log(entry:_LogEntry): 134 | var message = BB_CODE_REMOVER_REGEX.sub(entry.message, "", true) 135 | var message_level:LogStream.LogLevel = entry.message_level 136 | _reduce_stack(entry) 137 | 138 | var value_string = _stringify_values(entry.values) 139 | 140 | if entry.stack.is_empty():#Aka is connected to debug server -> print to the editor console in addition to pushing the warning. 141 | _log_mode_console(entry) 142 | else: 143 | _log_mode_editor(entry) 144 | ##AKA, level is error or fatal, the main tree is accessible and we want to print it. 145 | if message_level > LogStream.LogLevel.WARN && Log.is_inside_tree() && PRINT_TREE_ON_ERROR: 146 | #We want to access the main scene tree since this may be a custom logger that isn't in the main tree. 147 | print("Main tree: ") 148 | Log.get_tree().root.print_tree_pretty() 149 | print("")#Print empty line to mark new message 150 | 151 | static func _log_mode_editor(entry:_LogEntry): 152 | #print("message level: " + str(entry.message_level)) 153 | #print(MESSAGE_FORMAT_STRINGS) 154 | 155 | print_rich(MESSAGE_FORMAT_STRINGS[entry.message_level].format(_get_format_data(entry))) 156 | var stack = entry.stack 157 | var level = entry.message_level 158 | if level == LogStream.LogLevel.WARN: 159 | print(_create_stack_string(stack) + "\n") 160 | if level > LogStream.LogLevel.WARN: 161 | print(_create_stack_string(stack) + "\n") 162 | 163 | static func _log_mode_console(entry:_LogEntry): 164 | ##remove any BBCodes 165 | var msg = BB_CODE_REMOVER_REGEX.sub(entry.message,"", true) 166 | var level = entry.message_level 167 | if level < 3: 168 | print(msg) 169 | elif level == LogStream.LogLevel.WARN: 170 | push_warning(msg) 171 | else: 172 | push_error(msg) 173 | 174 | 175 | static func _get_format_data(entry:_LogEntry)->Dictionary: 176 | var now = Time.get_datetime_dict_from_system(USE_UTC_TIME_FORMAT) 177 | 178 | var log_call = null 179 | var script = "" 180 | var script_class_name = "" 181 | if !entry.stack.is_empty(): 182 | log_call = entry.stack[0] 183 | var source = log_call["source"] 184 | script = source.split("/")[-1] 185 | var result = GLOBAL_PATH_LIST.filter(func(entry):return entry["path"] == source) 186 | script_class_name = script if result.is_empty() else result[0]["class"] 187 | 188 | var format_data := { 189 | "log_name":entry.stream_name, 190 | "message":entry.message, 191 | "level":LogStream.LogLevel.keys()[entry.message_level], 192 | "script": script, 193 | "class": script_class_name, 194 | "function": log_call["function"] if log_call else "", 195 | "line": log_call["line"] if log_call else "", 196 | } 197 | if entry.values != null: 198 | var reduced_values = BB_CODE_REMOVER_REGEX.sub(_stringify_values(entry.values), "", true) 199 | print("values: (" + reduced_values + ")") 200 | format_data["values"] = VALUE_PRIMER_STRING + reduced_values 201 | else: 202 | format_data["values"] = "" 203 | format_data["second"] = "%02d"%now["second"] 204 | format_data["minute"] = "%02d"%now["minute"] 205 | format_data["hour"] = "%02d"%now["hour"] 206 | format_data["day"] = "%02d"%now["day"] 207 | format_data["month"] = "%02d"%now["month"] 208 | format_data["year"] = "%04d"%now["year"] 209 | 210 | return format_data 211 | 212 | static func _stringify_values(values)->String: 213 | match typeof(values): 214 | TYPE_NIL: 215 | return "" 216 | TYPE_ARRAY: 217 | var msg = "[" 218 | for k in values: 219 | msg += "{k}, ".format({"k":JSON.stringify(k)}) 220 | return msg + "]" 221 | TYPE_DICTIONARY: 222 | var msg = "{" 223 | for k in values: 224 | if typeof(values[k]) == TYPE_OBJECT && values[k] != null: 225 | msg += '"{k}":{v},'.format({"k":k,"v":JSON.stringify(_ObjectParser.to_dict(values[k],false))}) 226 | else: 227 | msg += '"{k}":{v},'.format({"k":k,"v":JSON.stringify(values[k])}) 228 | return msg+"}" 229 | TYPE_OBJECT: 230 | return JSON.stringify(_ObjectParser.to_dict(values,false)) 231 | _: 232 | return JSON.stringify(values) 233 | 234 | ##Removes the top of the stack trace in order to remove the logger. 235 | static func _reduce_stack(entry:_LogEntry)->void: 236 | var kept_frames = 0 237 | var stack = entry.stack 238 | for i in range(stack.size()): 239 | ##Search array from back to front, aka from bottom to top, aka into log from the outside. 240 | var stack_entry = stack[stack.size()-1-i] 241 | if stack_entry["source"].contains("log-stream.gd"): 242 | kept_frames = i 243 | break 244 | #cut of log part of stack. 245 | entry.stack = stack.slice(stack.size()-kept_frames) 246 | 247 | static func _create_stack_string(stack:Array)->String: 248 | var stack_trace_message:="" 249 | if !stack.is_empty():#aka has stack trace. 250 | stack_trace_message += "at:\n" 251 | for entry in stack: 252 | stack_trace_message += "\t" + entry["source"] + ":" + str(entry["line"]) + " in func " + entry["function"] + "\n" 253 | else: 254 | stack_trace_message = "No stack trace available, please run from within the editor or connect to a remote debug context." 255 | return stack_trace_message 256 | 257 | 258 | static func _cleanup(): 259 | _log_mutex.lock() 260 | _is_quitting = true 261 | _log_mutex.unlock() 262 | _log_thread.wait_to_finish() 263 | 264 | #Class for lobbing around logging data between the main thread and the logging thread. 265 | class _LogEntry: 266 | ##The level of the message 267 | var message_level:LogStream.LogLevel 268 | ##The 'raw' log message 269 | var message:String 270 | ##The name of the stream 271 | var stream_name:String 272 | ##The call stack of the log entry. 273 | var stack:Array 274 | ##A crash_behaviour callback. This will be called upon a fatal error. 275 | var crash_behaviour:Callable 276 | ## The values that may be attached to the log message. 277 | var values 278 | -------------------------------------------------------------------------------- /addons/logger/_ObjectParser.gd: -------------------------------------------------------------------------------- 1 | class_name _ObjectParser 2 | 3 | const REQUIRED_OBJECT_SUFFIX="_r" 4 | const COMPACT_VAR_SUFFIX="_c" 5 | 6 | const WHITELIST_VAR_NAME = "whitelist" 7 | 8 | static func to_dict(obj:Object,compact:bool,skip_whitelist:bool=false) ->Dictionary: 9 | if obj == null: 10 | return {} 11 | if !skip_whitelist: 12 | return _get_dict_with_list(obj,obj.get_property_list(),compact) 13 | 14 | var output:Dictionary = {} 15 | if WHITELIST_VAR_NAME in obj and obj[WHITELIST_VAR_NAME].size() > 0: 16 | return _get_dict_with_list(obj,obj[WHITELIST_VAR_NAME],false) 17 | 18 | return output 19 | 20 | 21 | static func _get_dict_with_list(obj:Object,property_list:Array,compact:bool) ->Dictionary: 22 | var output:Dictionary = {} 23 | for property in property_list: 24 | var name = "" 25 | if typeof(property) != TYPE_STRING && "name" in property: 26 | name = str(property.name) 27 | else: 28 | name = property 29 | if name.begins_with("_"): 30 | continue 31 | if compact and !_ends_with(name,[COMPACT_VAR_SUFFIX,REQUIRED_OBJECT_SUFFIX]): 32 | continue 33 | if !name in obj: 34 | continue 35 | var data_type = typeof(obj[name]) 36 | var value = obj[name] 37 | match data_type: 38 | TYPE_NIL: 39 | continue 40 | TYPE_OBJECT: 41 | if _ends_with(name,[COMPACT_VAR_SUFFIX,REQUIRED_OBJECT_SUFFIX]): 42 | #var t = Thread.new() 43 | #var lamda = func(): 44 | output[name] = to_dict(value,compact) 45 | 46 | TYPE_ARRAY: # todo 47 | continue 48 | TYPE_DICTIONARY: # todo 49 | var processsed_dictionary = {} 50 | for key in Dictionary(value): 51 | var key_type = typeof(value[key]) 52 | var key_value = value[key] 53 | match key_type: 54 | TYPE_OBJECT: 55 | if _ends_with(name,[COMPACT_VAR_SUFFIX,REQUIRED_OBJECT_SUFFIX]): 56 | processsed_dictionary[name] = to_dict(key_value,compact) 57 | TYPE_ARRAY: 58 | continue 59 | _: 60 | processsed_dictionary[key] = key_value 61 | output[name] = processsed_dictionary 62 | _: 63 | output[name] = value 64 | return output 65 | 66 | static func _ends_with(v:String,list:Array) -> bool: 67 | for s in list: 68 | if v.ends_with(s): 69 | return true 70 | return false -------------------------------------------------------------------------------- /addons/logger/config.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | 4 | ##Class for interacting with command line arguments and environement variables. 5 | class_name Config 6 | 7 | static func get_arguments() -> Dictionary: 8 | var arguments = {} 9 | var key = "" 10 | 11 | for argument in OS.get_cmdline_args(): 12 | var k = _parse_argument_key(argument) 13 | if k != "": 14 | key = k 15 | arguments[k] = "" 16 | elif key != "": 17 | arguments[key] = argument 18 | key == "" 19 | if argument.contains("="): 20 | var key_value = argument.split("=") 21 | arguments[key] = key_value[1] 22 | key == "" 23 | return arguments 24 | 25 | static func _parse_argument_key(argument:String) -> String: 26 | var prefixes = ["+","--","-"] 27 | for prefix in prefixes: 28 | if argument.begins_with(prefix): 29 | if argument.contains("="): 30 | return argument.split("=")[0].lstrip(prefix) 31 | return argument.lstrip(prefix) 32 | return "" 33 | 34 | static func get_steam_flag_name(name:String,prefix:String="") -> String: 35 | return (prefix + name).to_lower().replace("-","_") 36 | 37 | static func get_flag_name(name:String,prefix:String="") -> String: 38 | return (prefix + name).to_lower().replace("_","-") 39 | 40 | static func get_env_name(name:String,prefix:String="") -> String: 41 | return (prefix + name).to_upper().replace("-","_") 42 | 43 | static func get_var(name,default=""): 44 | var env_var_name = get_env_name(name) 45 | var flag_name = get_flag_name(name) 46 | var config_value = OS.get_environment(env_var_name) 47 | var steam_name = get_steam_flag_name(name) 48 | 49 | var args = get_arguments() 50 | if args.has(flag_name): 51 | return args[flag_name] 52 | if args.has(steam_name): 53 | return args[steam_name] 54 | if config_value != "": 55 | return config_value 56 | return default 57 | 58 | static func get_int(name,default=0) -> int: 59 | return int(get_var(name,default)) 60 | 61 | static func get_bool(name,default=false,prefix:String="") -> bool: 62 | var v = get_var(name,default).to_lower() 63 | match v: 64 | "yes","true","t","1": 65 | return true 66 | _: 67 | return false 68 | return false 69 | 70 | static func get_custom_var(name,type,default=null): 71 | match type: 72 | TYPE_ARRAY: 73 | return get_var(name,default).split(",") 74 | TYPE_BOOL: 75 | return get_bool(name,default) 76 | TYPE_DICTIONARY: 77 | return JSON.parse_string(get_var(name,default)) 78 | TYPE_INT: 79 | return get_int(name,default) 80 | TYPE_MAX: 81 | pass 82 | TYPE_NIL: 83 | return default 84 | TYPE_RECT2: 85 | pass 86 | TYPE_RID: 87 | pass 88 | TYPE_STRING: 89 | return get_var(name,default) 90 | TYPE_TRANSFORM2D: 91 | pass 92 | TYPE_VECTOR2: 93 | pass 94 | TYPE_VECTOR3: 95 | pass 96 | return default 97 | -------------------------------------------------------------------------------- /addons/logger/log-stream.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends Node 3 | ##Class that handles all the logging in the addon, methods can either be accessed through 4 | ##the "GodotLogger" singelton, or you can instance this class yourself(no need to add it to the tree) 5 | 6 | class_name LogStream 7 | 8 | enum LogLevel { 9 | DEBUG = 0, 10 | INFO = 1, 11 | WARN = 2, 12 | ERROR = 3, 13 | FATAL= 4, 14 | } 15 | 16 | var _log_name:String 17 | var _crash_behavior:Callable 18 | 19 | ##Emits this signal whenever a message is recieved. 20 | signal log_message(level:LogLevel,message:String) 21 | 22 | ##Represents the minimum level of messages that will be logged. 23 | var current_log_level:LogLevel = LogLevel.INFO:set= _set_level 24 | 25 | func _init(log_name:String, min_log_level:LogLevel=-1, crash_behavior:Callable = default_crash_behavior): 26 | _log_name = log_name 27 | _LogInternalPrinter._settings._ensure_setting_exists(_LogInternalPrinter._settings.STREAM_LEVEL_SETTING_LOCATION+_log_name, LogLevel.INFO, 28 | { 29 | "type":TYPE_INT, 30 | "hint":PROPERTY_HINT_ENUM, 31 | "hint_string":LogLevel.keys().reduce(func(a,b):return a + ", " + b).to_lower() 32 | }) 33 | current_log_level = min_log_level 34 | _crash_behavior = crash_behavior 35 | 36 | func _ready() -> void: 37 | if !get_tree().root.tree_exiting.is_connected(_LogInternalPrinter._cleanup): 38 | get_tree().root.tree_exiting.connect(_LogInternalPrinter._cleanup) 39 | 40 | ##prints a message to the log at the debug level. 41 | func debug(message:String,values:Variant=null): 42 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.DEBUG, current_log_level, _crash_behavior, log_message.emit, values) 43 | 44 | ##Shorthand for debug 45 | func dbg(message:String,values:Variant=null): 46 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.DEBUG, current_log_level, _crash_behavior, log_message.emit, values) 47 | 48 | ##prints a message to the log at the info level. 49 | func info(message:String,values:Variant=null): 50 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.INFO, current_log_level, _crash_behavior, log_message.emit, values) 51 | 52 | ##prints a message to the log at the warning level. 53 | func warn(message:String,values:Variant=null): 54 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.WARN, current_log_level, _crash_behavior, log_message.emit, values) 55 | 56 | ##Prints a message to the log at the error level. 57 | func error(message:String,values:Variant=null): 58 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, values) 59 | 60 | ##Shorthand for error 61 | func err(message:String,values:Variant=null): 62 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, values) 63 | 64 | ##Prints a message to the log at the fatal level, exits the application 65 | ##since there has been a fatal error. 66 | func fatal(message:String,values:Variant=null): 67 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.FATAL, current_log_level, _crash_behavior, log_message.emit, values) 68 | 69 | ##Throws an error if err_code is not of value "OK" and appends the error code string. 70 | func err_cond_not_ok(err_code:Error, message:String, fatal:=true, other_values_to_be_printed=null): 71 | if err_code != OK: 72 | _LogInternalPrinter._push_to_queue(_log_name, message + "" if message.ends_with(".") else "." + " Error string: " + error_string(err_code), LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 73 | 74 | ##Throws an error if the "statement" passed is false. Handy for making code "free" from if statements. 75 | func err_cond_false(statement:bool, message:String, fatal:=true, other_values_to_be_printed={}): 76 | if !statement: 77 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 78 | 79 | ##Throws an error if argument == null 80 | func err_cond_null(arg, message:String, fatal:=true, other_values_to_be_printed=null): 81 | if arg == null: 82 | _LogInternalPrinter._push_to_queue(_log_name, message, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 83 | 84 | ##Throws an error if the arg1 isn't equal to arg2. Handy for making code "free" from if statements. 85 | func err_cond_not_equal(arg1, arg2, message:String, fatal:=true, other_values_to_be_printed=null): 86 | #The type 'Color' is weird in godot, so therefore this edgecase... 87 | if (arg1 is Color && arg2 is Color && !arg1.is_equal_approx(arg2)) || arg1 != arg2: 88 | _LogInternalPrinter._push_to_queue(_log_name, str(arg1) + " != " + str(arg2) + ", not allowed. " + message, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 89 | 90 | ##Internal method. 91 | func _set_level(level:LogLevel): 92 | level = _get_external_log_level() if level == -1 else level 93 | info("setting log level to " + LogLevel.keys()[level]) 94 | current_log_level = level 95 | 96 | ##Internal method. 97 | func _get_external_log_level()->LogLevel: 98 | var cmd_line_level = Config.get_var("log-level","default").to_upper() 99 | var project_settings_level = ProjectSettings.get_setting(_LogInternalPrinter._settings.STREAM_LEVEL_SETTING_LOCATION+_log_name) 100 | if cmd_line_level.to_lower() != "default": 101 | if LogLevel.keys().has(cmd_line_level.to_upper()): 102 | return LogLevel[cmd_line_level] 103 | else: 104 | warn("The variable log-level is set to an illegal type, defaulting to info") 105 | return LogLevel.INFO 106 | else: 107 | return project_settings_level 108 | 109 | 110 | #make sure settings are synced without pulling them all the time. Not that this can take a tick or so. 111 | static func sync_project_settings()->void: 112 | var settings = _LogInternalPrinter._settings 113 | _LogInternalPrinter.BREAK_ON_ERROR = ProjectSettings.get_setting(settings.BREAK_ON_ERROR_KEY) 114 | _LogInternalPrinter.PRINT_TREE_ON_ERROR = ProjectSettings.get_setting(settings.PRINT_TREE_ON_ERROR_KEY) 115 | _LogInternalPrinter.CYCLE_BREAK_TIME = ProjectSettings.get_setting(settings.MIN_PRINTING_CYCLE_TIME_KEY) 116 | _LogInternalPrinter.USE_UTC_TIME_FORMAT = ProjectSettings.get_setting(settings.USE_UTC_TIME_FORMAT_KEY) 117 | _LogInternalPrinter.VALUE_PRIMER_STRING = settings._ensure_setting_exists(settings.VALUE_PRIMER_STRING_KEY, settings.VALUE_PRIMER_STRING_DEFAULT_VALUE) 118 | #See _LogInternalPrinter.MESSAGE_FORMAT_STRINGS for details. 119 | _LogInternalPrinter.MESSAGE_FORMAT_STRINGS = [ 120 | "", 121 | settings._ensure_setting_exists(settings.DEBUG_MESSAGE_FORMAT_KEY, settings.DEBUG_MESSAGE_FORMAT_DEFAULT_VALUE), 122 | settings._ensure_setting_exists(settings.INFO_MESSAGE_FORMAT_KEY, settings.INFO_MESSAGE_FORMAT_DEFAULT_VALUE), 123 | settings._ensure_setting_exists(settings.WARNING_MESSAGE_FORMAT_KEY, settings.WARNING_MESSAGE_FORMAT_DEFAULT_VALUE), 124 | settings._ensure_setting_exists(settings.ERROR_MESSAGE_FORMAT_KEY, settings.ERROR_MESSAGE_FORMAT_DEFAULT_VALUE), 125 | settings._ensure_setting_exists(settings.FATAL_MESSAGE_FORMAT_KEY, settings.FATAL_MESSAGE_FORMAT_DEFAULT_VALUE) 126 | ] 127 | 128 | ##Controls the behavior when a fatal error has been logged. 129 | ##Edit to customize the behavior. 130 | static func default_crash_behavior(): 131 | _LogInternalPrinter._cleanup() 132 | print("Crashing due to Fatal error.") 133 | #Restart the process to the main scene. (Uncomment if wanted), 134 | #note that we don't want to restart if we crash on init, then we get stuck in an infinite crash-loop, which isn't fun for anyone. 135 | #if get_tree().get_frame()>0: 136 | # var _ret = OS.create_process(OS.get_executable_path(), OS.get_cmdline_args()) 137 | 138 | #Choose crash mechanism. Difference is that get_tree().quit() quits at the end of the frame, 139 | #enabling multiple fatal errors to be cast, printing multiple stack traces etc. 140 | #Warning regarding the use of OS.crash() in the docs can safely be regarded in this case. 141 | OS.crash("Crash since falal error ocurred") 142 | #get_tree().quit(-1) 143 | -------------------------------------------------------------------------------- /addons/logger/logger.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends LogStream 3 | 4 | ##A default instance of the LogStream. Instanced as the main log singelton. 5 | 6 | 7 | func _init(): 8 | super("Main") 9 | -------------------------------------------------------------------------------- /addons/logger/plugin.cfg: -------------------------------------------------------------------------------- 1 | [plugin] 2 | 3 | name="Log" 4 | description="A fork of the GodotLogger plugin that has been rewritten to accomodate more advanced features. 5 | Original: 6 | - Adds a basic logger to print out Nodes,Objects,Arrays,Dictionarys etc. 7 | - Adds support for easily reading env vars & cmd line args. 8 | 9 | New for the fork: 10 | - Adds multiple log streams so you can control the log level independently for different parts of your project. 11 | - Adds a fatal log level that causes your project to crash in a way that you can control. 12 | - Adds comments to updated parts of the plugin. 13 | - Adds options in top of the log stream. 14 | - Adds shorthand methods for debug & error. 15 | - Adds err_cond_... methods for quick error checking. 16 | - Adds a scripted breakpoint (optional in setting) so errors freeze the execution and shows relevant info in the godot debugger. 17 | - Adds support for multiple log files. 18 | - Adds a test scene that can be used as an example of how the plugin can be used." 19 | author="albinaask, avivajpeyi, Chrisknyfe, cstaaben, florianvazelle, SeannMoser, ahrbe1" 20 | version="2.0" 21 | script="plugin.gd" 22 | -------------------------------------------------------------------------------- /addons/logger/plugin.gd: -------------------------------------------------------------------------------- 1 | @tool 2 | extends EditorPlugin 3 | 4 | 5 | 6 | func _enable_plugin(): 7 | #make sure log-stream is loaded to prevent godot error. 8 | preload("res://addons/logger/log-stream.gd") 9 | add_autoload_singleton("Log", "res://addons/logger/logger.gd") 10 | tree_exiting.connect(_LogInternalPrinter._cleanup) 11 | 12 | 13 | func _disable_plugin(): 14 | remove_autoload_singleton("Log") 15 | _LogInternalPrinter._cleanup() 16 | -------------------------------------------------------------------------------- /addons/logger/settings.gd: -------------------------------------------------------------------------------- 1 | # Settings for Log, interacts with the ProjectSettings singleton. Note that these may take a tick or two to sync. 2 | 3 | 4 | 5 | ## Controls how the message should be formatted, follows String.format(), valid keys are: 6 | ## "level": the log level, aka debug, warning, FALTAL etc... 7 | ## "log_name": The name of the logger, which is the string passed into a LogStream on init or MAIN for Log methods. 8 | ## "message": The message to print, aka Log.info("my message"). 9 | ## "values": Values or values to print, aka Log.info("my message", {"a": 1, "b": 2}). 10 | ## "script": Script from which the log call is called. 11 | ## "function": Function -||- 12 | ## "line": Line -||- 13 | ## "year": Year at which the log was printed, int 14 | ## "month": Month -||-, int of 1-12 15 | ## "day": Day -||-, int 1-31 16 | ## "weekday": Weekday -||-, int 0-6 17 | ## "hour": Hour -||-, int 0-23? 18 | ## "minute" Minute -||-, int 0-59 19 | ## "second" Second -||-, int 0-59. Note that this may differ on a second or two since the time is get in the logger thread, which may not run for a couple of milliseconds. 20 | ##BBCode friendly, aka any BBCode may be inserted here. 21 | const DEBUG_MESSAGE_FORMAT_DEFAULT_VALUE = "[color=dark_gray]{log_name}/{level} ({script}:{line}) [lb]{hour}:{minute}:{second}[rb] {message}[/color]{values}" 22 | ##The path within the ProjectSettings where this setting is stored. 23 | const DEBUG_MESSAGE_FORMAT_KEY = "addons/Log/debug_message_format" 24 | 25 | ##Same as 'DEBUG_LOG_MESSAGE_FORMAT_KEY'. 26 | const INFO_MESSAGE_FORMAT_DEFAULT_VALUE = "[color=white]{log_name}/{level} ({script}:{line}) [lb]{hour}:{minute}:{second}[rb] {message}[/color]{values}" 27 | ##The path within the ProjectSettings where this setting is stored. 28 | const INFO_MESSAGE_FORMAT_KEY = "addons/Log/info_message_format" 29 | 30 | ##Same as 'DEBUG_LOG_MESSAGE_FORMAT_KEY'. 31 | const WARNING_MESSAGE_FORMAT_DEFAULT_VALUE = "[color=gold]{log_name}/{level} ({script}:{line}) [lb]{hour}:{minute}:{second}[rb] {message}[/color]{values}" 32 | ##The path within the ProjectSettings where this setting is stored. 33 | const WARNING_MESSAGE_FORMAT_KEY = "addons/Log/warning_message_format" 34 | 35 | ##Same as 'DEBUG_LOG_MESSAGE_FORMAT_KEY' 36 | const ERROR_MESSAGE_FORMAT_DEFAULT_VALUE = "[color=red]{log_name}/{level} ({script}:{line}) [lb]{hour}:{minute}:{second}[rb] {message}[/color]{values}" 37 | ##The path within the ProjectSettings where this setting is stored. 38 | const ERROR_MESSAGE_FORMAT_KEY = "addons/Log/error_message_format" 39 | 40 | ##Same as 'DEBUG_LOG_MESSAGE_FORMAT_KEY' 41 | const FATAL_MESSAGE_FORMAT_DEFAULT_VALUE = "[color=red][u][b]{log_name}/{level} ({script}:{line}) [lb]{hour}:{minute}:{second}[rb] {message}[/b][/u][/color]{values}" 42 | ##The path within the ProjectSettings where this setting is stored. 43 | const FATAL_MESSAGE_FORMAT_KEY = "addons/Log/fatal_message_format" 44 | 45 | ##Printed before potential values. 46 | const VALUE_PRIMER_STRING_DEFAULT_VALUE = ", Value(s): " 47 | const VALUE_PRIMER_STRING_KEY = "addons/Log/value_primer_string" 48 | 49 | ## Whether to use the UTC time or the user's local time. 50 | const USE_UTC_TIME_FORMAT_KEY = "addons/Log/use_utc_time_format" 51 | const USE_UTC_TIME_FORMAT_DEFAULT_VALUE = false 52 | 53 | ## Enables a breakpoint to mimic the godot behavior where the application doesn't crash when connected to debug environment, 54 | ## but instead freezed and shows the stack etc in the debug panel. 55 | const BREAK_ON_ERROR_KEY = "addons/Log/break_on_error" 56 | const BREAK_ON_ERROR_DEFAULT_VALUE = true 57 | 58 | ##Whether to dump the tree to the log on error. 59 | const PRINT_TREE_ON_ERROR_KEY = "addons/Log/print_tree_on_error" 60 | const PRINT_TREE_ON_ERROR_DEFAULT_VALUE = false 61 | 62 | ##Controls the size of the log queue. Effectively the maximum amount of messages that can be logged in one batch (at the same time, effectively within one batch cycle). 63 | const LOG_QUEUE_SIZE_KEY = "addons/Log/log_queue_size" 64 | const LOG_QUEUE_SIZE_DEFAULT_VALUE = 128 65 | 66 | ##Controls the maximum time of thread sleeping between printing message batches, in ms. 67 | const MIN_PRINTING_CYCLE_TIME_KEY = "addons/Log/min_printing_cycle_time" 68 | const MIN_PRINTING_CYCLE_TIME_DEFAULT_VALUE = 5 69 | 70 | 71 | ##Controls where all the log streams are found within the project settings. 72 | const STREAM_LEVEL_SETTING_LOCATION = "addons/Log/streams/" 73 | 74 | 75 | static func _ensure_setting_exists(setting: String, default_value, property_info = {}) -> Variant: 76 | if not ProjectSettings.has_setting(setting): 77 | ProjectSettings.set_setting(setting, default_value) 78 | ProjectSettings.set_initial_value(setting, default_value) 79 | if ProjectSettings.has_method("set_as_basic"): # 4.0 backward compatibility 80 | ProjectSettings.call("set_as_basic", setting, true) 81 | if property_info.size()>0: 82 | property_info["name"] = setting 83 | ProjectSettings.add_property_info(property_info) 84 | return ProjectSettings.get_setting(setting) 85 | --------------------------------------------------------------------------------