From 4ff56252b0ce61bcf5fa9049ebb84bbd0d6ad07c Mon Sep 17 00:00:00 2001 From: Mark Marks Date: Tue, 27 Aug 2024 12:45:09 +0200 Subject: [PATCH] feat: Initial release of sapphire-logging at mark-marks/sapphire-logging@0.1.2 --- README.md | 2 +- crates/sapphire-logging/README.md | 230 ++++++++++++++++++++++++ crates/sapphire-logging/lib/init.luau | 47 +++++ crates/sapphire-logging/lib/logger.luau | 77 ++++++++ crates/sapphire-logging/lib/sinks.luau | 24 +++ crates/sapphire-logging/lib/types.luau | 100 +++++++++++ 6 files changed, 479 insertions(+), 1 deletion(-) create mode 100644 crates/sapphire-logging/lib/logger.luau create mode 100644 crates/sapphire-logging/lib/sinks.luau create mode 100644 crates/sapphire-logging/lib/types.luau diff --git a/README.md b/README.md index 7dd2d48..62e5315 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ A lightweight module loader or a batteries included framework - [x] Make basic, extensible module loader - [ ] Add pre-built extensions: - [x] `sapphire-lifecycles` - extra lifecycles for `RunService` and `Players` - - [ ] `sapphire-logging` - a nice logging library with a log history + - [x] `sapphire-logging` - a nice logging library with a log history - [x] `sapphire-net` - optimized networking library that features defined (like `ByteNet`) events and undefined events, both with buffer serdes, albeit undefined events performing worse due to having to define types and lengths in the buffer - [x] `sapphire-data` - batteries included wrapper for an existing data library like `keyForm` - [x] `sapphire-ecr` - scheduler for ECR with niceties diff --git a/crates/sapphire-logging/README.md b/crates/sapphire-logging/README.md index 7ac6e49..8702ab3 100644 --- a/crates/sapphire-logging/README.md +++ b/crates/sapphire-logging/README.md @@ -1,2 +1,232 @@ # sapphire-logging A simple logging libary with a logbook for [Mark-Marks/sapphire](https://github.com/Mark-Marks/sapphire) + +# Installation +1. Install it with wally +```toml +[dependencies] +sapphire_logging = "mark-marks/sapphire-logging@LATEST" +``` +2. `wally install` +3. Extend sapphire with it +```luau +local sapphire_logging = require("@pkg/sapphire_logging") + +sapphire + :use(sapphire_logging) +``` + +# API + +## Types + +### signal +```luau +type signal = { + Root: signal_node?, + + Connect: (self: signal, Callback: (T...) -> ()) -> () -> (), + Wait: (self: signal) -> T..., + Once: (self: signal, Callback: (T...) -> ()) -> () -> (), + Fire: (self: signal, T...) -> (), + DisconnectAll: (self: signal) -> (), +} +``` + +### log_type +```luau +type log_type = "info" | "debug" | "warn" | "error" | "fatal" +``` + +### log +```luau +type log = { + --- Timestamps representing when the log took place + timestamp: { + --- CPU time at the time of log + clock: number, + --- Seconds passed since the start of the UNIX epoch at the time of log + unix: number, + }, + --- Type of the log + type: log_type, + --- Message passed to the log + msg: string, + --- Full traceback of all function calls leading to this log + trace: string, +} +``` + +### sink +```luau +type sink = (log) -> () +``` + +### logger +```luau +type logger = { + _debug: boolean, + msg_out: signal, + logbook: { log }, + + new: (debug: boolean?) -> logger, + write: (self: logger, log_type: log_type, msg: string, trace: string) -> log, + debug: (self: logger, msg: string) -> log?, + warn: (self: logger, msg: string) -> log, + error: (self: logger, msg: string) -> log, + fatal: (self: logger, msg: string) -> (), + + connect_sink: (self: logger, sink: sink) -> () -> (), +} +``` + +## Exports + +### .identifier +Readonly, extension identifier required by sapphire. +```luau +type identifier = string +``` + +### .methods +Readonly +```luau +type methods = {} +``` + +### .cache +Readonly, cache for `sapphire_logging.get()` +```luau +type cache = { [string]: logger } +``` + +### .default +A default, uncached logger +```luau +type default = logger +``` + +### .sinks +Default sinks, connectable with `logger:connect_sink()` +```luau +type sinks = { + --- Roblox logging sink. + roblox: sink, +} +``` + +## Registered Methods + +N/A + +## Functions + +### .extensions() +Readonly, required by sapphire for extension startup. +```luau +() -> () +``` + +### .get() +Gets an existing cached logger or creates a new one. +```luau +( + identifier: string, + debug: boolean?, -- Only applied if the logger doesn't exist +) -> logger +``` + +## logger + +### ._debug +Readonly - log debug messages? +```luau +type _debug = boolean +``` + +### .msg_out +Readonly - on log signal, connect with `:connect_sink()` +```luau +type msg_out = signal +``` + +### .logbook +Readonly logbook of all logs that happened +```luau +type logbook = { log } +``` + +### .new() +Creates a new logger. +```luau +( + debug: boolean? -- Should debug messages be logged? Defaults to false +) -> logger +``` + +### :write() +Writes a message to the logger. +Prefer to use the logging methods instead of this. +```luau +( + self: logger, + log_type: log_type, + msg: string, + trace: string -- Full traceback of all function calls leading to this +) -> log +``` + +### :info() +Writes an `info` to the logger. +```luau +( + self: logger, + msg: string +) -> log +``` + +### :debug() +Writes a `debug` to the logger, IF logger is in debug mode. +```luau +( + self: logger, + msg: string +) -> log? +``` + +### :warn() +Writes a `warn` to the logger. +```luau +( + self: logger, + msg: string +) -> log +``` + +### :error() +Writes an `error` to the logger, continues execution. +```luau +( + self: logger, + msg: string +) -> log +``` + +### :fatal() +Kills the current thread after writing a `fatal` to the logger. +```luau +( + self: logger, + msg: string +) +``` + +### :connect_sink() +Connects the given sink to the loggers `msg_out` signal. +Returns a disconnect function. +```luau +( + self: logger, + sink: sink +) -> () -> () +``` diff --git a/crates/sapphire-logging/lib/init.luau b/crates/sapphire-logging/lib/init.luau index e69de29..7d35f8a 100644 --- a/crates/sapphire-logging/lib/init.luau +++ b/crates/sapphire-logging/lib/init.luau @@ -0,0 +1,47 @@ +--!strict +local logger = require(script.logger) +local sinks = require(script.sinks) +local types = require(script.types) + +export type signal = types.signal +export type log_type = types.log_type +export type log = types.log +export type sink = types.sink +export type logger = types.logger + +local SapphireLogging = {} +--- @readonly +SapphireLogging.identifier = "sapphire-logging" +--- @readonly +SapphireLogging.methods = {} + +--- @readonly +SapphireLogging.cache = {} :: { [string]: logger } + +--- @readonly +function SapphireLogging.extension() + -- noop +end + +--- Gets an existing cached logger or creates a new one. +--- @param identifier string +--- @param debug boolean? -- Only applied if the logger doesn't exist +--- @return logger +function SapphireLogging.get(identifier: string, debug: boolean?): logger + local existing_logger = SapphireLogging.cache[identifier] + if existing_logger then + return existing_logger + end + + local new_logger = logger.new(debug) + SapphireLogging.cache[identifier] = new_logger + return new_logger +end + +--- A default, uncached logger +SapphireLogging.default = logger.new() + +--- Default sinks, connectable with `logger:connect_sink(sink)` +SapphireLogging.sinks = sinks + +return SapphireLogging diff --git a/crates/sapphire-logging/lib/logger.luau b/crates/sapphire-logging/lib/logger.luau new file mode 100644 index 0000000..768bf9c --- /dev/null +++ b/crates/sapphire-logging/lib/logger.luau @@ -0,0 +1,77 @@ +--!strict +local signal = require(script.Parent.Parent.signal) + +local types = require(script.Parent.types) +type log_type = types.log_type +type log = types.log + +local function kill() + local thread = coroutine.running() + task.defer(coroutine.close, thread) + return coroutine.yield() +end + +local logger = {} :: types.logger_interface +logger.__index = logger + +function logger.new(debug: boolean?) + local self = { + _debug = debug or false, + logbook = {}, + msg_out = signal(), + } + + return setmetatable(self, logger) +end + +function logger:write(log_type: log_type, msg: string, trace: string): log + local log = { + timestamp = { + clock = os.clock(), + unix = os.time(), + }, + type = log_type, + msg = msg, + trace = trace, + } + + table.insert(self.logbook, log) + self.msg_out:Fire(log) + return log +end + +function logger:info(msg: string): log + local trace = debug.traceback(nil, 2) + return self:write("info", msg, trace) +end + +function logger:debug(msg: string): log? + if not self._debug then + return + end + + local trace = debug.traceback(nil, 2) + return self:write("debug", msg, trace) +end + +function logger:warn(msg: string): log + local trace = debug.traceback(nil, 2) + return self:write("warn", msg, trace) +end + +function logger:error(msg: string): log + local trace = debug.traceback(nil, 2) + return self:write("error", msg, trace) +end + +function logger:fatal(msg: string) + local trace = debug.traceback(nil, 2) + self:write("fatal", msg, trace) + kill() +end + +function logger:connect_sink(sink: types.sink) + return self.msg_out:Connect(sink) +end + +return logger diff --git a/crates/sapphire-logging/lib/sinks.luau b/crates/sapphire-logging/lib/sinks.luau new file mode 100644 index 0000000..f09417f --- /dev/null +++ b/crates/sapphire-logging/lib/sinks.luau @@ -0,0 +1,24 @@ +--!strict +local RunService = game:GetService("RunService") +local platform_icon = if RunService:IsServer() then "[đŸ—ŧ]" else "[đŸ’ģ]" + +local types = require(script.Parent.types) + +--- Roblox logging sink. +local roblox: types.sink = function(log) + if log.type == "debug" then + print(`[{platform_icon}][🕒{log.timestamp.clock}][🔨]: {log.msg}`) + elseif log.type == "info" then + print(`[{platform_icon}][🕒{log.timestamp.clock}][ℹī¸]: {log.msg}`) + elseif log.type == "warn" then + warn(`[{platform_icon}][🕒{log.timestamp.clock}][⚠ī¸]: {log.msg}`) + elseif log.type == "error" then + warn(`[{platform_icon}][🕒{log.timestamp.clock}][✖ī¸]: {log.msg}\n{log.trace}`) + elseif log.type == "fatal" then + task.spawn(error, `[{platform_icon}][🕒{log.timestamp.clock}][❌]: {log.msg}\n{log.trace}`, 0) + end +end + +return { + roblox = roblox, +} diff --git a/crates/sapphire-logging/lib/types.luau b/crates/sapphire-logging/lib/types.luau new file mode 100644 index 0000000..525f487 --- /dev/null +++ b/crates/sapphire-logging/lib/types.luau @@ -0,0 +1,100 @@ +--!strict +type signal_node = { + Next: signal_node?, + Callback: (T...) -> (), +} + +--- redblox signal type +export type signal = { + Root: signal_node?, + + Connect: (self: signal, Callback: (T...) -> ()) -> () -> (), + Wait: (self: signal) -> T..., + Once: (self: signal, Callback: (T...) -> ()) -> () -> (), + Fire: (self: signal, T...) -> (), + DisconnectAll: (self: signal) -> (), +} + +export type log_type = "info" | "debug" | "warn" | "error" | "fatal" + +export type log = { + --- Timestamps representing when the log took place + timestamp: { + --- CPU time at the time of log + clock: number, + --- Seconds passed since the start of the UNIX epoch at the time of log + unix: number, + }, + --- Type of the log + type: log_type, + --- Message passed to the log + msg: string, + --- Full traceback of all function calls leading to this log + trace: string, +} + +export type sink = (log) -> () + +export type logger_interface = { + __index: logger_interface, + + --- Creates a new logger. + --- @param debug boolean? -- Should debug messages be logged? Defaults to false + --- @return logger + new: (debug: boolean?) -> logger, + --- Writes a message to the logger. + --- Prefer to use the logging methods instead of this. + --- @param log_type log_type + --- @param msg string + --- @param trace string -- Full traceback of all function calls leading to this + --- @return log + write: (self: logger, log_type: log_type, msg: string, trace: string) -> log, + --- Writes an `info` to the logger. + --- @param self logger + --- @param msg string + --- @return log + info: (self: logger, msg: string) -> log, + --- Writes a `debug` to the logger, IF logger is in debug mode. + --- @param self logger + --- @param msg string + --- @return log? + debug: (self: logger, msg: string) -> log?, + --- Writes a `warn` to the logger. + --- @param self logger + --- @param msg string + --- @return log + warn: (self: logger, msg: string) -> log, + --- Writes an `error` to the logger, continues execution. + --- @param self logger + --- @param msg string + --- @return log + error: (self: logger, msg: string) -> log, + --- Kills the current thread after writing a `fatal` to the logger. + --- @param self logger + --- @param msg string + fatal: (self: logger, msg: string) -> (), + + --- Connects the given sink to the loggers `msg_out` signal. + --- Returns a disconnect function. + --- @param self logger + --- @param sink sink + --- @return () -> () -- Disconnect connection + connect_sink: (self: logger, sink: sink) -> () -> (), +} + +export type logger = typeof(setmetatable( + {} :: { + --- @readonly + --- Log debug messages? + _debug: boolean, + --- @readonly + --- On log signal, connect with `:connect_sink()` + msg_out: signal, + --- @readonly + --- Logbook of all logs that happened + logbook: { log }, + }, + {} :: logger_interface +)) + +return "types"