└── addons └── logger ├── logger.gd ├── plugin.gd ├── plugin.cfg ├── Microbe.gd ├── _ObjectParser.gd ├── LogConfig.gd ├── LogSettings.gd ├── log-stream.gd └── _LogInternalPrinter.gd /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.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/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 micro benchmarking tool called Microbe. 19 | - Adds a test scene that can be used as an example of how the plugin can be used." 20 | author="albinaask, avivajpeyi, Chrisknyfe, cstaaben, florianvazelle, SeannMoser, ahrbe1" 21 | version="2.1.1" 22 | script="plugin.gd" 23 | -------------------------------------------------------------------------------- /addons/logger/Microbe.gd: -------------------------------------------------------------------------------- 1 | extends RefCounted 2 | 3 | ##Micro-benchmarking tool that can be used to time how long a section of code takes. Note that a method call in GDScript takes time! 4 | #TODO: account for this in the calculation of timing... 5 | class_name Microbe 6 | 7 | var _name :String 8 | var _started := false 9 | var _print:bool 10 | var _last_time := 0 11 | var _start_time:int 12 | 13 | func _init(name : String) -> void: 14 | _name = name 15 | 16 | func start(): 17 | if _started: 18 | printerr("Microbe session '" + _name + "' already started, must be finished before restarted.") 19 | return 20 | else: 21 | print("Microbe session '" + _name + "' started.") 22 | _started = true 23 | _last_time = Time.get_ticks_usec() 24 | _start_time = _last_time 25 | 26 | func finish_sub_step(step_name:String): 27 | if _started: 28 | print("Microbe step '" + step_name + "' finished. Took: " + str(Time.get_ticks_usec() - _last_time) + "us.") 29 | _last_time = Time.get_ticks_usec() 30 | else: 31 | print("Microbe session not started.") 32 | 33 | func finish(): 34 | _started = false 35 | print("Microbe session " + _name + " finished. Took: " + str(Time.get_ticks_usec() - _start_time) + "us total.") 36 | -------------------------------------------------------------------------------- /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/LogConfig.gd: -------------------------------------------------------------------------------- 1 | extends Node 2 | 3 | 4 | ##Class for interacting with command line arguments and environement variables. 5 | class_name LogConfig 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 | 98 | static func get_external_log_level(log_name: String, default_level: int) -> int: 99 | var cmd_line_level = get_var("log-level", "default").to_upper() 100 | var project_settings_level = ProjectSettings.get_setting(_LogInternalPrinter._settings.STREAM_LEVEL_SETTING_LOCATION + log_name) 101 | if cmd_line_level.to_lower() != "default": 102 | if LogStream.LogLevel.has(cmd_line_level): 103 | return LogStream.LogLevel.find_key(cmd_line_level) 104 | else: 105 | _warn_via_log("The variable log-level is set to an illegal type, defaulting to info") 106 | return default_level 107 | else: 108 | return project_settings_level 109 | 110 | static func _warn_via_log(message: String) -> void: 111 | push_warning(message) 112 | -------------------------------------------------------------------------------- /addons/logger/LogSettings.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}{values}[/color]" 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}{values}[/color]" 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}{values}[/color]" 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}{values}[/color]" 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]{values}[/color]" 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 | ##Controls where all the log streams are found within the project settings. 71 | const STREAM_LEVEL_SETTING_LOCATION = "addons/Log/streams/" 72 | 73 | 74 | static func _ensure_setting_exists(setting: String, default_value, property_info = {}) -> Variant: 75 | if not ProjectSettings.has_setting(setting): 76 | ProjectSettings.set_setting(setting, default_value) 77 | ProjectSettings.set_initial_value(setting, default_value) 78 | if ProjectSettings.has_method("set_as_basic"): # 4.0 backward compatibility 79 | ProjectSettings.call("set_as_basic", setting, true) 80 | if property_info.size()>0: 81 | property_info["name"] = setting 82 | ProjectSettings.add_property_info(property_info) 83 | return ProjectSettings.get_setting(setting) 84 | -------------------------------------------------------------------------------- /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 | # Preload once so log level resolution works even if LogConfig is not yet in the cache; mirrors the `_settings` naming style. 9 | const _log_config := preload("res://addons/logger/LogConfig.gd") 10 | 11 | enum LogLevel { 12 | DEBUG = 0, 13 | INFO = 1, 14 | WARN = 2, 15 | ERROR = 3, 16 | FATAL= 4, 17 | } 18 | 19 | var _log_name:String 20 | var _crash_behavior:Callable 21 | 22 | ##Emits this signal whenever a message is recieved. 23 | signal log_message(level:LogLevel,message:String) 24 | 25 | ##Represents the minimum level of messages that will be logged. 26 | var current_log_level:LogLevel = LogLevel.INFO:set= _set_level 27 | 28 | 29 | ## Constructor for the LogStream. 30 | ## 31 | ## The parameters are: 32 | ## 33 | ## - `log_name`: The name of the logger. This is used to identify the logger in the log output. 34 | ## - `min_log_level`: The minimum level of messages that will be logged. Defaults to -1, this causes the stream to use either the one found in the project settings, environment variable or the command line. 35 | ## - `crash_behavior`: A Callable that is called when a fatal error is encountered. Defaults to default_crash_behavior. Takes no arguments. 36 | func _init(log_name:String, min_log_level:LogLevel=-1, crash_behavior:Callable = default_crash_behavior): 37 | _log_name = log_name 38 | _LogInternalPrinter._settings._ensure_setting_exists(_LogInternalPrinter._settings.STREAM_LEVEL_SETTING_LOCATION+_log_name, LogLevel.INFO, 39 | { 40 | "type":TYPE_INT, 41 | "hint":PROPERTY_HINT_ENUM, 42 | "hint_string":LogLevel.keys().reduce(func(a,b):return a + ", " + b).to_lower() 43 | }) 44 | current_log_level = min_log_level 45 | _crash_behavior = crash_behavior 46 | 47 | func _ready() -> void: 48 | if !get_tree().root.tree_exiting.is_connected(_LogInternalPrinter._cleanup): 49 | get_tree().root.tree_exiting.connect(_LogInternalPrinter._cleanup) 50 | 51 | ##prints a message to the log at the debug level. 52 | func debug(message:Variant,values:Variant=null): 53 | _LogInternalPrinter._push_to_queue(_log_name, str(message), LogLevel.DEBUG, current_log_level, _crash_behavior, log_message.emit, values) 54 | 55 | func debugs(...params): 56 | debug(str.callv(params)) 57 | 58 | ##Shorthand for debug 59 | func dbg(message:Variant,values:Variant=null): 60 | debug(message,values) 61 | 62 | func dbgs(...params): 63 | debug(str.callv(params)) 64 | 65 | ##prints a message to the log at the info level. 66 | func info(message:Variant,values:Variant=null): 67 | _LogInternalPrinter._push_to_queue(_log_name, str(message), LogLevel.INFO, current_log_level, _crash_behavior, log_message.emit, values) 68 | 69 | func infos(...params): 70 | info(str.callv(params)) 71 | 72 | ##prints a message to the log at the warning level. 73 | func warn(message:Variant,values:Variant=null): 74 | _LogInternalPrinter._push_to_queue(_log_name, str(message), LogLevel.WARN, current_log_level, _crash_behavior, log_message.emit, values) 75 | 76 | func warns(...params): 77 | warn(str.callv(params)) 78 | 79 | ##Prints a message to the log at the error level. 80 | func error(message:Variant,values:Variant=null): 81 | _LogInternalPrinter._push_to_queue(_log_name, str(message), LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, values) 82 | 83 | func errors(...params): 84 | error(str.callv(params)) 85 | 86 | ##Shorthand for error 87 | func err(message:String,values:Variant=null): 88 | error(message,values) 89 | 90 | func errs(...params): 91 | error(str.callv(params)) 92 | 93 | ##Prints a message to the log at the fatal level, exits the application 94 | ##since there has been a fatal error. 95 | func fatal(message:Variant,values:Variant=null): 96 | _LogInternalPrinter._push_to_queue(_log_name, str(message), LogLevel.FATAL, current_log_level, _crash_behavior, log_message.emit, values) 97 | 98 | func fatals(...params): 99 | fatal(str.callv(params)) 100 | 101 | ##Throws an error if err_code is not of value "OK" and appends the error code string. 102 | func err_cond_not_ok(err_code:Error, message_on_err:String, fatal:=true, other_values_to_be_printed=null): 103 | if err_code != OK: 104 | _LogInternalPrinter._push_to_queue(_log_name, message_on_err + "" if message_on_err.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) 105 | 106 | ##Throws an error if the "statement" passed is false. Handy for making code "free" from if statements. 107 | func err_cond_false(statement:bool, message_on_err:String, fatal:=true, other_values_to_be_printed={}): 108 | if !statement: 109 | _LogInternalPrinter._push_to_queue(_log_name, message_on_err, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 110 | 111 | ##Throws an error if argument == null 112 | func err_cond_null(arg, message_on_err:String, fatal:=true, other_values_to_be_printed=null): 113 | if arg == null: 114 | _LogInternalPrinter._push_to_queue(_log_name, message_on_err, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 115 | 116 | ##Throws an error if the arg1 isn't equal to arg2. Handy for making code "free" from if statements. 117 | func err_cond_not_equal(arg1, arg2, message_on_err:String, fatal:=true, other_values_to_be_printed=null): 118 | #The type 'Color' is weird in godot, so therefore this edgecase... 119 | if (arg1 is Color && arg2 is Color && !arg1.is_equal_approx(arg2)) || arg1 != arg2: 120 | _LogInternalPrinter._push_to_queue(_log_name, str(arg1) + " != " + str(arg2) + ", not allowed. " + message_on_err, LogLevel.FATAL if fatal else LogLevel.ERROR, current_log_level, _crash_behavior, log_message.emit, other_values_to_be_printed) 121 | 122 | ##Internal method. 123 | func _set_level(level:LogLevel): 124 | var resolved_level := level 125 | if level == -1: 126 | # -1 is the "inherit from settings" sentinel; resolve it before logging. 127 | resolved_level = _log_config.get_external_log_level(_log_name, LogLevel.INFO) 128 | if resolved_level < 0 or resolved_level >= LogLevel.keys().size(): 129 | # Guard against unexpected values so the log never prints bogus levels. 130 | resolved_level = LogLevel.INFO 131 | info("setting log level to " + LogLevel.keys()[resolved_level]) 132 | current_log_level = resolved_level 133 | 134 | 135 | #make sure settings are synced without pulling them all the time. Not that this can take a tick or so. 136 | static func sync_project_settings()->void: 137 | var settings = _LogInternalPrinter._settings 138 | _LogInternalPrinter.BREAK_ON_ERROR = ProjectSettings.get_setting(settings.BREAK_ON_ERROR_KEY) 139 | _LogInternalPrinter.PRINT_TREE_ON_ERROR = ProjectSettings.get_setting(settings.PRINT_TREE_ON_ERROR_KEY) 140 | _LogInternalPrinter.CYCLE_BREAK_TIME = ProjectSettings.get_setting(settings.MIN_PRINTING_CYCLE_TIME_KEY) 141 | _LogInternalPrinter.USE_UTC_TIME_FORMAT = ProjectSettings.get_setting(settings.USE_UTC_TIME_FORMAT_KEY) 142 | _LogInternalPrinter.VALUE_PRIMER_STRING = settings._ensure_setting_exists(settings.VALUE_PRIMER_STRING_KEY, settings.VALUE_PRIMER_STRING_DEFAULT_VALUE) 143 | #See _LogInternalPrinter.MESSAGE_FORMAT_STRINGS for details. 144 | _LogInternalPrinter.MESSAGE_FORMAT_STRINGS = [ 145 | settings._ensure_setting_exists(settings.DEBUG_MESSAGE_FORMAT_KEY, settings.DEBUG_MESSAGE_FORMAT_DEFAULT_VALUE), 146 | settings._ensure_setting_exists(settings.INFO_MESSAGE_FORMAT_KEY, settings.INFO_MESSAGE_FORMAT_DEFAULT_VALUE), 147 | settings._ensure_setting_exists(settings.WARNING_MESSAGE_FORMAT_KEY, settings.WARNING_MESSAGE_FORMAT_DEFAULT_VALUE), 148 | settings._ensure_setting_exists(settings.ERROR_MESSAGE_FORMAT_KEY, settings.ERROR_MESSAGE_FORMAT_DEFAULT_VALUE), 149 | settings._ensure_setting_exists(settings.FATAL_MESSAGE_FORMAT_KEY, settings.FATAL_MESSAGE_FORMAT_DEFAULT_VALUE) 150 | ] 151 | 152 | ##Controls the behavior when a fatal error has been logged. 153 | ##Edit to customize the default behavior. 154 | static func default_crash_behavior()->void: 155 | #Joins the logging thread(aka waits for it to log all messages that are in the queue) onto the main thread, and cleans up the logging thread. 156 | _LogInternalPrinter._cleanup() 157 | print("Crashing due to Fatal error.") 158 | #Restart the process to the main scene. (Uncomment if wanted), 159 | #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. 160 | #if get_tree().get_frame()>0: 161 | # var _ret = OS.create_process(OS.get_executable_path(), OS.get_cmdline_args()) 162 | 163 | #Choose crash mechanism. Difference is that get_tree().quit() quits at the end of the frame, 164 | #enabling multiple fatal errors to be cast, printing multiple stack traces etc. 165 | #Warning regarding the use of OS.crash() in the docs can safely be regarded in this case. 166 | OS.crash("Crash since falal error ocurred") 167 | #get_tree().quit(-1) 168 | 169 | 170 | #Class for lobbing around logging data between the main thread and the logging thread. 171 | class LogEntry: 172 | ##The level of the message 173 | var message_level:LogStream.LogLevel 174 | ##The 'raw' log message 175 | var message:String 176 | ##The name of the stream 177 | var stream_name:String 178 | ##The call stack of the log entry. 179 | var stack:Array 180 | ##A crash_behaviour callback. This will be called upon a fatal error. 181 | var crash_behaviour:Callable 182 | ## The values that may be attached to the log message. 183 | var values 184 | -------------------------------------------------------------------------------- /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 LogStream.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[LogStream.LogEntry] 13 | static var _back_queue: Array[LogStream.LogEntry] 14 | static var _front_queue_size: int = 0 15 | 16 | const _settings := preload("./LogSettings.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 | static var _is_thread_started = false 23 | static var _script_server: Object 24 | ## Tracks whether logging stays on the main thread (true inside the editor). 25 | static var _single_threaded_mode = true 26 | # Used to double-check ScriptServer notifications target this script. 27 | const LOGGER_SCRIPT_PATH := "res://addons/logger/_LogInternalPrinter.gd" 28 | ##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. 29 | #static var push_microbe = Microbe.new("push to queue") 30 | 31 | ##Settings are set in ProjectSettings. Look for Log and set them from there. See 'res://addons/logger/LogSettings.gd' for explanations on what each setting do. 32 | static var BREAK_ON_ERROR:bool = _settings._ensure_setting_exists(_settings.BREAK_ON_ERROR_KEY, _settings.BREAK_ON_ERROR_DEFAULT_VALUE) 33 | static var PRINT_TREE_ON_ERROR:bool = _settings._ensure_setting_exists(_settings.PRINT_TREE_ON_ERROR_KEY, _settings.PRINT_TREE_ON_ERROR_DEFAULT_VALUE) 34 | static var CYCLE_BREAK_TIME:int = _settings._ensure_setting_exists(_settings.MIN_PRINTING_CYCLE_TIME_KEY, _settings.MIN_PRINTING_CYCLE_TIME_DEFAULT_VALUE) 35 | static var USE_UTC_TIME_FORMAT:bool = _settings._ensure_setting_exists(_settings.USE_UTC_TIME_FORMAT_KEY, _settings.USE_UTC_TIME_FORMAT_DEFAULT_VALUE) 36 | static var GLOBAL_PATH_LIST = ProjectSettings.get_global_class_list() 37 | static var BB_CODE_REMOVER_REGEX = RegEx.new() 38 | static var BB_CODE_EXCLUDING_BRACKETS_REMOVER_REGEX = RegEx.new() 39 | static var VALUE_PRIMER_STRING:String = _settings._ensure_setting_exists(_settings.VALUE_PRIMER_STRING_KEY, _settings.VALUE_PRIMER_STRING_DEFAULT_VALUE) 40 | 41 | ##Note that these are in the same order as LogStream.LogLevel 42 | static var MESSAGE_FORMAT_STRINGS:Array[String] 43 | 44 | 45 | ##Every method in this class is static, therefore a static constructor is used. 46 | static func _static_init() -> void: 47 | # The Godot editor's hot-reload can call _static_init multiple times; shut down any old worker first. 48 | if _is_thread_started: 49 | _cleanup() 50 | _is_quitting = false 51 | # Runtime builds keep the background worker; editor builds force single-threaded logging. 52 | _single_threaded_mode = Engine.is_editor_hint() 53 | 54 | MESSAGE_FORMAT_STRINGS = [ 55 | _settings._ensure_setting_exists(_settings.DEBUG_MESSAGE_FORMAT_KEY, _settings.DEBUG_MESSAGE_FORMAT_DEFAULT_VALUE), 56 | _settings._ensure_setting_exists(_settings.INFO_MESSAGE_FORMAT_KEY, _settings.INFO_MESSAGE_FORMAT_DEFAULT_VALUE), 57 | _settings._ensure_setting_exists(_settings.WARNING_MESSAGE_FORMAT_KEY, _settings.WARNING_MESSAGE_FORMAT_DEFAULT_VALUE), 58 | _settings._ensure_setting_exists(_settings.ERROR_MESSAGE_FORMAT_KEY, _settings.ERROR_MESSAGE_FORMAT_DEFAULT_VALUE), 59 | _settings._ensure_setting_exists(_settings.FATAL_MESSAGE_FORMAT_KEY, _settings.FATAL_MESSAGE_FORMAT_DEFAULT_VALUE) 60 | ] 61 | 62 | var queue_size = _settings._ensure_setting_exists(_settings.LOG_QUEUE_SIZE_KEY, _settings.LOG_QUEUE_SIZE_DEFAULT_VALUE) 63 | queue_size = max(queue_size, 1)#Make sure queue size is at least 1. 64 | 65 | BB_CODE_REMOVER_REGEX.compile("\\[(lb|rb)\\]|\\[.*?\\]") 66 | BB_CODE_EXCLUDING_BRACKETS_REMOVER_REGEX.compile("\\[(?!(?:lb|rb)\\])[a-zA-Z0-9=_\\/]*+\\]") 67 | if !ProjectSettings.settings_changed.is_connected(LogStream.sync_project_settings): 68 | ProjectSettings.settings_changed.connect(LogStream.sync_project_settings) 69 | if Engine.is_editor_hint() and Engine.has_singleton("ScriptServer"): 70 | _script_server = Engine.get_singleton("ScriptServer") 71 | # Listen for editor reloads so we can join the worker before bytecode is swapped out; 72 | # without this the worker keeps running with freed bytecode and crashes on save. 73 | if _script_server and !_script_server.script_changed.is_connected(_on_script_server_script_changed): 74 | _script_server.script_changed.connect(_on_script_server_script_changed) 75 | 76 | # Rebuild the queues; old LogEntry instances may still reference freed script data. 77 | _front_queue = [] as Array[LogStream.LogEntry] 78 | _back_queue = [] as Array[LogStream.LogEntry] 79 | _front_queue_size = 0 80 | _front_queue.resize(queue_size) 81 | _back_queue.resize(queue_size) 82 | for i in range(queue_size): 83 | _front_queue[i] = LogStream.LogEntry.new() 84 | _back_queue[i] = LogStream.LogEntry.new() 85 | 86 | if !_single_threaded_mode: 87 | if _log_thread.start(_process_logs, Thread.PRIORITY_LOW) != OK: 88 | printerr("Log error: Couldn't create Log thread. This should never happen, please contact dev. No messages will be printed") 89 | else: 90 | # Track that we have a live worker so reload can shut it down first. 91 | _is_thread_started = true 92 | 93 | 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: 94 | _push_to_queue(stream_name, message, message_level, stream_level, crash_behaviour, on_log_message_signal_emission_callback, values) 95 | 96 | 97 | ##Pushes a log entry onto the front (input queue). This is done so Log can process the log asynchronously. 98 | ##Method is called by LogStream.info and the like. 99 | 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: 100 | #push_microbe.start() 101 | if message_level < stream_level: 102 | # push_microbe.finish() 103 | return 104 | 105 | # Editor builds skip the worker thread entirely and log on the main thread here. 106 | # Godot reloads editor scripts aggressively for previews/completion; keeping the 107 | # background thread alive across those reloads would leave it running against 108 | # freed bytecode and crash (see https://github.com/albinaask/Log/issues/22). This 109 | # single-threaded path keeps the editor stable while runtime exports still use 110 | # the async worker. 111 | if _single_threaded_mode: 112 | var entry = LogStream.LogEntry.new() 113 | entry.stream_name = stream_name 114 | entry.message = message 115 | entry.message_level = message_level 116 | entry.crash_behaviour = crash_behaviour 117 | entry.values = values 118 | entry.stack = get_stack() 119 | if message_level >= LogStream.LogLevel.ERROR: 120 | push_error(message) 121 | if BREAK_ON_ERROR: 122 | breakpoint 123 | elif message_level == LogStream.LogLevel.WARN: 124 | push_warning(message) 125 | _internal_log(entry) 126 | if message_level == LogStream.LogLevel.FATAL: 127 | entry.crash_behaviour.call() 128 | on_log_message_signal_emission_callback.call(message) 129 | return 130 | 131 | _log_mutex.lock() 132 | #push_microbe.finish_sub_step("lock") 133 | var entry:LogStream.LogEntry 134 | if _is_front_queue_full(): 135 | entry = _front_queue[_front_queue_size-1] 136 | entry.stream_name = "Log" 137 | entry.message = "Log queue overflow. Print less messages or increase queue size in project settings or squash messages into one long." 138 | entry.message_level = LogStream.LogLevel.WARN 139 | else: 140 | entry = _front_queue[_front_queue_size] 141 | entry.stream_name = stream_name 142 | entry.message = message 143 | entry.message_level = message_level 144 | entry.crash_behaviour = crash_behaviour 145 | entry.values = values 146 | _front_queue_size += 1 147 | #push_microbe.finish_sub_step("entry assignment") 148 | entry.stack = get_stack() 149 | #push_microbe.finish_sub_step("get_stack") 150 | _log_mutex.unlock() 151 | #push_microbe.finish_sub_step("unlock") 152 | 153 | if message_level >= LogStream.LogLevel.ERROR: 154 | push_error(message) 155 | if BREAK_ON_ERROR: 156 | ##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. 157 | breakpoint 158 | elif message_level == LogStream.LogLevel.WARN: 159 | push_warning(message) 160 | #push_microbe.finish_sub_step("if statements") 161 | if message_level == LogStream.LogLevel.FATAL: 162 | ##This waits for the log thread to finish processing messages before exiting. 163 | entry.crash_behaviour.call() 164 | on_log_message_signal_emission_callback.call(message) 165 | #push_microbe.finish_sub_step("signal emission") 166 | #push_microbe.finish() 167 | 168 | ##Helper method for checking if the front queue is full. 169 | static func _is_front_queue_full() -> bool: 170 | return _front_queue_size == _front_queue.size() 171 | 172 | ##Main loop of the logging thread. Runs until godot is quit or a fatal error occurs. 173 | static func _process_logs(): 174 | while !_is_quitting: 175 | var start = Time.get_ticks_usec() 176 | #Swap th buffers, so that the back queue is now the front queue and prepare for overriding the front queue with new messages. 177 | _log_mutex.lock() 178 | var temp = _front_queue 179 | _front_queue = _back_queue 180 | _back_queue = temp 181 | var _back_queue_size = _front_queue_size 182 | _front_queue_size = 0 183 | _log_mutex.unlock() 184 | 185 | for i in range(_back_queue_size): 186 | _internal_log(_back_queue[i]) 187 | var delta = Time.get_ticks_usec() - start 188 | #Sleep for a while, depending on the time it took to process the messages, 189 | OS.delay_usec(max(CYCLE_BREAK_TIME*1000-delta, 0)) 190 | 191 | ##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. 192 | static func _internal_log(entry:LogStream.LogEntry): 193 | var message = BB_CODE_REMOVER_REGEX.sub(entry.message, "", true) 194 | var message_level:LogStream.LogLevel = entry.message_level 195 | _reduce_stack(entry) 196 | 197 | var value_string = _stringify_values(entry.values) 198 | 199 | if entry.stack.is_empty():#Aka is connected to debug server -> print to the editor console in addition to pushing the warning. 200 | _log_mode_console(entry) 201 | else: 202 | _log_mode_editor(entry) 203 | ##AKA, level is error or fatal, the main tree is accessible and we want to print it. 204 | if message_level > LogStream.LogLevel.WARN && PRINT_TREE_ON_ERROR: 205 | if !_single_threaded_mode: 206 | # When the worker thread is active we're off the main thread; bounce back before touching the tree. 207 | _print_tree_now.call_deferred() 208 | else: 209 | _print_tree_now() 210 | 211 | static func _log_mode_editor(entry:LogStream.LogEntry): 212 | var message_format_strings = MESSAGE_FORMAT_STRINGS 213 | var message_level = entry.message_level 214 | var message_format = message_format_strings[message_level] 215 | var format_data = _get_format_data(entry) 216 | var output = message_format.format(format_data) 217 | print_rich(output) 218 | var stack = entry.stack 219 | var level = entry.message_level 220 | if level == LogStream.LogLevel.WARN: 221 | print(_create_stack_string(stack) + "\n") 222 | if level > LogStream.LogLevel.WARN: 223 | print(_create_stack_string(stack) + "\n") 224 | 225 | static func _log_mode_console(entry:LogStream.LogEntry): 226 | ##remove any BBCodes 227 | var msg = BB_CODE_REMOVER_REGEX.sub(entry.message,"", true) 228 | var level = entry.message_level 229 | if level < 3: 230 | print(msg) 231 | elif level == LogStream.LogLevel.WARN: 232 | push_warning(msg) 233 | else: 234 | push_error(msg) 235 | 236 | 237 | static func _get_format_data(entry:LogStream.LogEntry)->Dictionary: 238 | var now = Time.get_datetime_dict_from_system(USE_UTC_TIME_FORMAT) 239 | 240 | var log_call = null 241 | var script = "" 242 | var script_class_name = "" 243 | if !entry.stack.is_empty(): 244 | log_call = entry.stack[0] 245 | var source = log_call["source"] 246 | script = source.split("/")[-1] 247 | var result = GLOBAL_PATH_LIST.filter(func(entry):return entry["path"] == source) 248 | script_class_name = script if result.is_empty() else result[0]["class"] 249 | 250 | var format_data := { 251 | "log_name":entry.stream_name, 252 | "message":entry.message, 253 | "level":LogStream.LogLevel.keys()[entry.message_level], 254 | "script": script, 255 | "class": script_class_name, 256 | "function": log_call["function"] if log_call else "", 257 | "line": log_call["line"] if log_call else "", 258 | } 259 | if entry.values != null: 260 | var values = _stringify_values(entry.values) 261 | var reduced_values = BB_CODE_EXCLUDING_BRACKETS_REMOVER_REGEX.sub(values, "", true) 262 | #print("values: (" + reduced_values + ")") 263 | format_data["values"] = VALUE_PRIMER_STRING + reduced_values 264 | else: 265 | format_data["values"] = "" 266 | format_data["second"] = "%02d"%now["second"] 267 | format_data["minute"] = "%02d"%now["minute"] 268 | format_data["hour"] = "%02d"%now["hour"] 269 | format_data["day"] = "%02d"%now["day"] 270 | format_data["month"] = "%02d"%now["month"] 271 | format_data["year"] = "%04d"%now["year"] 272 | 273 | return format_data 274 | 275 | static func _stringify_values(values)->String: 276 | match typeof(values): 277 | TYPE_NIL: 278 | return "" 279 | TYPE_ARRAY: 280 | var msg = "[" 281 | for k in values: 282 | msg += "{k}, ".format({"k":JSON.stringify(k)}) 283 | return msg + "]" 284 | TYPE_DICTIONARY: 285 | var msg = "{" 286 | for k in values: 287 | if typeof(values[k]) == TYPE_OBJECT && values[k] != null: 288 | msg += '"{k}":{v},'.format({"k":k,"v":JSON.stringify(_ObjectParser.to_dict(values[k],false))}) 289 | else: 290 | msg += '"{k}":{v},'.format({"k":k,"v":JSON.stringify(values[k])}) 291 | return msg+"}" 292 | TYPE_OBJECT: 293 | return JSON.stringify(_ObjectParser.to_dict(values,false)) 294 | _: 295 | return JSON.stringify(values) 296 | 297 | ##Removes the top of the stack trace in order to remove the logger. 298 | static func _reduce_stack(entry:LogStream.LogEntry)->void: 299 | var kept_frames = 0 300 | var stack = entry.stack 301 | for i in range(stack.size()): 302 | ##Search array from back to front, aka from bottom to top, aka into log from the outside. 303 | var stack_entry = stack[stack.size()-1-i] 304 | if stack_entry["source"].contains("log-stream.gd"): 305 | kept_frames = i 306 | break 307 | #cut of log part of stack. 308 | entry.stack = stack.slice(stack.size()-kept_frames) 309 | 310 | static func _create_stack_string(stack:Array)->String: 311 | var stack_trace_message:="" 312 | if !stack.is_empty():#aka has stack trace. 313 | stack_trace_message += "at:\n" 314 | for entry in stack: 315 | stack_trace_message += "\t" + entry["source"] + ":" + str(entry["line"]) + " in func " + entry["function"] + "\n" 316 | else: 317 | stack_trace_message = "No stack trace available, please run from within the editor or connect to a remote debug context." 318 | return stack_trace_message 319 | 320 | 321 | ##Shuts down the worker thread and disconnects editor-only hooks. Safe to call repeatedly. 322 | static func _cleanup(): 323 | _log_mutex.lock() 324 | _is_quitting = true 325 | _log_mutex.unlock() 326 | # Only join the worker when we actually spawned one (runtime mode). 327 | if !_single_threaded_mode and _log_thread.is_started(): 328 | _log_thread.wait_to_finish() 329 | _is_thread_started = false 330 | _log_thread = Thread.new() 331 | _is_quitting = false 332 | _front_queue_size = 0 333 | if _script_server and _script_server.script_changed.is_connected(_on_script_server_script_changed): 334 | _script_server.script_changed.disconnect(_on_script_server_script_changed) 335 | _script_server = null 336 | 337 | static func _on_script_server_script_changed(script:Script) -> void: 338 | if script == null: 339 | return 340 | if script.resource_path == LOGGER_SCRIPT_PATH: 341 | _cleanup() 342 | 343 | static func _print_tree_now() -> void: 344 | # May be reached via call_deferred() to ensure we run on the main thread before touching the tree. 345 | var main_loop = Engine.get_main_loop() 346 | if main_loop is SceneTree: 347 | var tree: SceneTree = main_loop 348 | if tree.root: 349 | # Runs on the main thread after call_deferred() so accessing the tree is safe. 350 | print("Main tree: ") 351 | tree.root.print_tree_pretty() 352 | print("")#Print empty line to mark new message 353 | --------------------------------------------------------------------------------