Skip to content

Commit

Permalink
feat: Initial release of sapphire-logging at mark-marks/sapphire-logg…
Browse files Browse the repository at this point in the history
  • Loading branch information
Mark-Marks committed Aug 27, 2024
1 parent b843a1d commit 4ff5625
Show file tree
Hide file tree
Showing 6 changed files with 479 additions and 1 deletion.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
230 changes: 230 additions & 0 deletions crates/sapphire-logging/README.md
Original file line number Diff line number Diff line change
@@ -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<T...>
```luau
type signal<T...> = {
Root: signal_node<T...>?,
Connect: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
Wait: (self: signal<T...>) -> T...,
Once: (self: signal<T...>, Callback: (T...) -> ()) -> () -> (),
Fire: (self: signal<T...>, T...) -> (),
DisconnectAll: (self: signal<T...>) -> (),
}
```

### 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<log>,
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<log>
```

### .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
) -> () -> ()
```
47 changes: 47 additions & 0 deletions crates/sapphire-logging/lib/init.luau
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
--!strict
local logger = require(script.logger)
local sinks = require(script.sinks)
local types = require(script.types)

export type signal<T...> = types.signal<T...>
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
77 changes: 77 additions & 0 deletions crates/sapphire-logging/lib/logger.luau
Original file line number Diff line number Diff line change
@@ -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
24 changes: 24 additions & 0 deletions crates/sapphire-logging/lib/sinks.luau
Original file line number Diff line number Diff line change
@@ -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,
}
Loading

0 comments on commit 4ff5625

Please sign in to comment.