CORE/src/logger.gd

237 lines
11 KiB
GDScript

# CORE FRAMEWORK SOURCE FILE
# Copyright (c) 2024 The StarOpenSource Project & Contributors
# Licensed under the GNU Affero General Public License v3
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 of the
# License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
## Your usual basic logger implementation, with some extra features.
##
## Allows for colored output, better newlines, multiple logger levels and a
## large variety of placeholders usable in [param config_format].
extends CoreBaseModule
signal log_event
## Keeps track of all logger instances.
## Unused instances will be cleaned periodically.
## [b]Danger: [i]Don't modify this.[/i][/b]
var instances: Array[CoreLoggerInstance] = []
## Used to determine if running in verbose/command line mode.[br]
## Makes diagnostic log messages display correctly (workaround for Godot's ANSI true color limitation).
var verbose_mode: bool = false
## The minimum log level you want to be displayed.
var config_level: CoreTypes.LoggerLevel
## Determines if the logger's output should be colored.
var config_colored: bool
## The template for all log messages[br]
## Available placeholders are: [code]%time%[/code], [code]%time_ms%[/code], [code]%level%[/code], [code]%color%[/code], [code]%message%[/code], [code]%source%[/code], [code]%source_raw%[/code], [code]%function%[/code] and [code]%line%[/code]
var config_format: String
## Determines if identation should be provided if the logger encounters a newline.
var config_newlines_override: bool
## The maximum amount of characters excluding the message before newlines are no longer idented. Set to [code]-1[/code] to disable this behaviour.
var config_newlines_sizelimit: int
# +++ module +++
func _initialize() -> void:
if core.config.logger_detect_verbose_mode and OS.is_stdout_verbose():
verbose_mode = true
func _cleanup() -> void:
_schedule()
await get_tree().process_frame
func _schedule() -> void:
for instance in instances:
if !is_instance_valid(instance): continue
if !is_instance_valid(instance.parent):
loggeri.diag("Removing instance '" + instance.name + "'")
instance.queue_free()
instances.remove_at(instances.find(instance))
func _pull_config() -> void:
config_level = core.config.logger_level
config_colored = core.config.logger_colored
config_format = core.config.logger_format
config_newlines_override = core.config.logger_newlines_override
config_newlines_sizelimit = core.config.logger_newlines_sizelimit
# +++ logging +++
## The main logging function that does the heavy lifting.[br]
## [b]Danger: [i]Don't call this.[/i][/b]
func _log(level: CoreTypes.LoggerLevel, origin: String, message: String) -> void:
if !is_level_allowed(level):
emit_signal("log_event", false, level, origin, message, "")
return
var format: String = config_format
format = format.replace("%time_ms%", str(Time.get_ticks_msec()))
format = format.replace("%time%", Time.get_time_string_from_system(true))
format = format.replace("%origin%", origin)
var format_newline: String = format.replace("%color%", "").replace("%message%", "")
if !config_colored: format = format.replace("%color%", "")
match(level):
CoreTypes.LoggerLevel.DIAG:
format = format.replace("%level%", "DIAG")
format_newline = format_newline.replace("%level%", "DIAG")
if verbose_mode: format = format.replace("%color%", "[color=gray]")
else: format = format.replace("%color%", "[color=dark_gray]")
CoreTypes.LoggerLevel.VERB:
format = format.replace("%level%", "VERB")
format_newline = format_newline.replace("%level%", "VERB")
format = format.replace("%color%", "[color=gray]")
CoreTypes.LoggerLevel.INFO:
format = format.replace("%level%", "INFO")
format_newline = format_newline.replace("%level%", "INFO")
format = format.replace("%color%", "[color=white]")
CoreTypes.LoggerLevel.WARN:
format = format.replace("%level%", "WARN")
format_newline = format_newline.replace("%level%", "WARN")
format = format.replace("%color%", "[color=yellow]")
CoreTypes.LoggerLevel.ERROR:
format = format.replace("%level%", "ERR!")
format_newline = format_newline.replace("%level%", "ERR!")
format = format.replace("%color%", "[color=red]")
CoreTypes.LoggerLevel.NONE:
format = format.replace("%level%", "CRSH")
format_newline = format_newline.replace("%level%", "CRSH")
format = format.replace("%color%", "[b][color=red]")
# Replace %message%
if config_newlines_override and config_newlines_sizelimit <= -1 or format_newline.length() <= config_newlines_sizelimit: message = message.replace("\n", "\n" + " ".repeat(format_newline.length()))
format = format.replace("%message%", message)
emit_signal("log_event", true, level, origin, message, format)
if config_colored: print_rich(format)
else: print(format)
# +++ self explanitory +++
## Prints a diagnostic log message.
func diag(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.DIAG, origin, message)
## Prints a verbose log message.
func verb(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.VERB, origin, message)
## Prints an informational log message.
func info(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.INFO, origin, message)
## Prints a warning log message.
func warn(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.WARN, origin, message)
## Prints an error log message.
func error(origin: String, message: String) -> void: _log(CoreTypes.LoggerLevel.ERROR, origin, message)
## Handles crashes. Will terminate your game/application immediately.[br]
## [b]Note: [i]Using the [code]await[/code] keyword is required for this function.[/i][/b][br]
## [b]Danger: [i]Don't set [code]framework_crash[/code] to [code]true[/code], thanks![/i][/b]
func crash(origin: String, message: String, framework_crash: bool = false) -> void:
# Collect information
var stack: Array[Dictionary] = get_stack()
var mem_info: Dictionary = OS.get_memory_info()
var crash_message: String = """Generating crash report...
######################################
### CORE CRASH HANDLER ###
######################################
%causer% inflicted a crash!
%origin% says:
%message%
+++ CORE INFORMATION +++
VERSION
-> Release %version_release%
-> Type %version_type%
-> Typerelease %version_typerelease%
-> Full %version_full%
-> SemVer %version_semantic%
MODES
-> Development %devmode%
-> Headless %headless%
+++ SYSTEM INFORMATION +++
OPERATING SYSTEM
-> Operating System %os%
-> Version %os_version%
-> Distribution %os_distribution%
LOCALE
-> Locale %locale%
-> Language %locale_lang%
MEMORY
-> Free %mem_free%
-> Available %mem_avail%
-> Used (by engine) %mem_used%
-> Total (physical) %mem_total%
-> Peak %mem_peak%
PROCESSOR
-> Name %processor_name%
-> Count %processor_count%
VIDEO
-> Adapter Information %video_adapter%
+++ GODOT ENGINE INFORMATION +++
ENGINE
-> Version %godot_version%
BUILD
-> Debug build %godot_debug%
-> Sandboxed %godot_sandboxed%
USERDATA
-> Persistent %godot_persistance%
STACKTRACE
%godot_stacktrace%
######################################
### CORE CRASH HANDLER ###
######################################"""
# Replace placeholders
if framework_crash: crash_message = crash_message.replace("%causer%", "The CORE Framework")
else: crash_message = crash_message.replace("%causer%", "The running application")
crash_message = crash_message.replace("%origin%", origin)
crash_message = crash_message.replace("%message%", message)
crash_message = crash_message.replace("%version_release%", str(core.version_version))
crash_message = crash_message.replace("%version_type%", await core.get_formatted_string("%version_type%"))
crash_message = crash_message.replace("%version_typerelease%", str(core.version_typerelease))
crash_message = crash_message.replace("%version_full%", str(core.version_version) + await core.get_formatted_string("-%version_type_technical%") + str(core.version_typerelease))
crash_message = crash_message.replace("%version_semantic%", await core.get_formatted_string("%version_semantic%"))
crash_message = crash_message.replace("%devmode%", str(core.is_devmode()))
crash_message = crash_message.replace("%headless%", str(core.config.headless))
crash_message = crash_message.replace("%os%", OS.get_name())
crash_message = crash_message.replace("%os_version%", OS.get_version())
crash_message = crash_message.replace("%os_distribution%", OS.get_distribution_name())
crash_message = crash_message.replace("%locale%", OS.get_locale())
crash_message = crash_message.replace("%locale_lang%", OS.get_locale_language())
crash_message = crash_message.replace("%mem_free%", str(core.misc.mib2gib(core.misc.byte2mib(mem_info["free"], false))) + " GiB")
crash_message = crash_message.replace("%mem_avail%", str(core.misc.mib2gib(core.misc.byte2mib(mem_info["available"], false))) + " GiB")
crash_message = crash_message.replace("%mem_used%", str(core.misc.byte2mib(OS.get_static_memory_usage())) + " MiB")
crash_message = crash_message.replace("%mem_total%", str(core.misc.mib2gib(core.misc.byte2mib(mem_info["physical"], false))) + " GiB")
crash_message = crash_message.replace("%mem_peak%", str(core.misc.byte2mib(OS.get_static_memory_peak_usage())) + " MiB")
crash_message = crash_message.replace("%processor_name%", OS.get_processor_name())
crash_message = crash_message.replace("%processor_count%", str(OS.get_processor_count()))
crash_message = crash_message.replace("%video_adapter%", str(OS.get_video_adapter_driver_info()))
crash_message = crash_message.replace("%godot_version%", str(Engine.get_version_info()["string"]))
crash_message = crash_message.replace("%godot_debug%", str(OS.is_debug_build()))
crash_message = crash_message.replace("%godot_sandboxed%", str(OS.is_sandboxed()))
crash_message = crash_message.replace("%godot_persistance%", str(OS.is_userfs_persistent()))
crash_message = crash_message.replace("%godot_stacktrace%", str(stack))
# Enable newline overrides
config_newlines_override = true
config_newlines_sizelimit = -1
# Print crash message
_log(CoreTypes.LoggerLevel.NONE, origin, crash_message)
# Shutdown
await core.quit_safely(69)
# +++ etc +++
## Checks if the specified log level is allowed by the current configuration.
func is_level_allowed(level: CoreTypes.LoggerLevel) -> bool:
if level <= config_level: return true
else: return false
## Returns a [class CoreLoggerInstance], which is a fancy word meaning you don't need to pass [code]origin[/code] every time you want to log something.
func get_instance(origin: String, parent: Object) -> CoreLoggerInstance:
var instance: CoreLoggerInstance = CoreLoggerInstance.new(self, origin, parent)
instance.name = "CoreLoggerInstance -> " + str(parent) + " (" + origin + ")"
instances.append(instance)
return instance