From 76515db7d0060a8671536df6220c22447e1a9e50 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Mon, 16 Mar 2026 16:54:13 +0000 Subject: [PATCH 1/6] refactor log service: enhance connection handling and message formatting --- src/services/log.lua | 74 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 59 insertions(+), 15 deletions(-) diff --git a/src/services/log.lua b/src/services/log.lua index 6127b58d..91f878ab 100644 --- a/src/services/log.lua +++ b/src/services/log.lua @@ -1,31 +1,75 @@ -local rxilog = require 'rxilog' -local new_msg = require 'bus'.new_msg +-- services/log.lua +-- +-- Log service (new fibers): +-- - singleton logger: log.trace/debug/info/warn/error/fatal callable from any service +-- - when conn is set (after start), publishes log entries to {'logs', } +-- - publishes service lifecycle status to {'svc', , 'status'} -local log_service = { - name = "log", -} -log_service.__index = log_service +local rxilog = require 'rxilog' +local runtime = require 'fibers.runtime' +local fibers = require 'fibers' +local sleep = require 'fibers.sleep' +local perform = require 'fibers.performer'.perform +local log_service = {} + +local function t(...) + return { ... } +end + +local function now() + return runtime.now() +end + +local function publish_status(conn, name, state, extra) + local payload = { state = state, ts = now() } + if type(extra) == 'table' then + for k, v in pairs(extra) do payload[k] = v end + end + conn:retain(t('svc', name, 'status'), payload) +end + +-- Level methods: always log to console; publish to bus when a connection is set. for _, mode in ipairs(rxilog.modes) do local level = mode.name log_service[level] = function(...) local msg = rxilog.tostring(...) rxilog[level](msg) - if log_service.conn then - local info = debug.getinfo(2, "Sl") + if log_service._conn then + local info = debug.getinfo(2, "Sl") local lineinfo = info.short_src .. ":" .. info.currentline - local formatted_msg = rxilog.format_log_message(level:upper(), lineinfo, msg) - - log_service.conn:publish(new_msg({ "logs", level }, formatted_msg)) + local time_utils = require 'fibers.utils.time' + log_service._conn:publish(t('logs', level), { + message = rxilog.format_log_message(level:upper(), lineinfo, msg), + timestamp = time_utils.realtime(), + }) end end end -function log_service:start(ctx, conn) - self.ctx = ctx - self.conn = conn - log_service.trace("Starting Log Service") +function log_service.start(conn, opts) + opts = opts or {} + local name = opts.name or 'log' + + publish_status(conn, name, 'starting') + + fibers.current_scope():finally(function() + log_service._conn = nil + publish_status(conn, name, 'stopped') + rxilog.trace("Log: stopped") + end) + + log_service._conn = conn + log_service._name = name + + publish_status(conn, name, 'running') + log_service.trace("Log service started") + + -- Block until scope is cancelled or fails (sleep_op is interrupted by cancellation). + while true do + perform(sleep.sleep_op(math.huge)) + end end -- Make singleton From 8e0321baf0270e7df161021079ca565d4312be26 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 17 Mar 2026 15:58:44 +0000 Subject: [PATCH 2/6] add service-level tests for log service functionality --- tests/test_log.lua | 368 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 368 insertions(+) create mode 100644 tests/test_log.lua diff --git a/tests/test_log.lua b/tests/test_log.lua new file mode 100644 index 00000000..4c482fca --- /dev/null +++ b/tests/test_log.lua @@ -0,0 +1,368 @@ +-- test_log.lua +-- +-- Service-level tests for the log service. +-- +-- Each test spins up the log service in a child scope (or wires the singleton +-- connection directly), interacts with it via the bus, and asserts on bus- +-- published results. +-- +-- Run standalone: luajit test_log.lua + +local this_file = debug.getinfo(1, "S").source:match("@?([^/]+)$") +local is_entry_point = arg and arg[0] and arg[0]:match("[^/]+$") == this_file + +if is_entry_point then + package.path = "../vendor/lua-fibers/src/?.lua;" + .. "../vendor/lua-trie/src/?.lua;" + .. "../vendor/lua-bus/src/?.lua;" + .. "../src/?.lua;" + .. "./test_utils/?.lua;" + .. package.path + .. ";/usr/lib/lua/?.lua;/usr/lib/lua/?/init.lua" + + _G._TEST = true +end + +local luaunit = require 'luaunit' +local fibers = require 'fibers' +local perform = fibers.perform +local op = require 'fibers.op' +local sleep = require 'fibers.sleep' +local busmod = require 'bus' +local rxilog = require 'rxilog' + +------------------------------------------------------------------------------- +-- Helpers +------------------------------------------------------------------------------- + +local function make_bus() + return busmod.new({ q_length = 100, s_wild = '+', m_wild = '#' }) +end + +-- Return a fresh singleton for each test. +local function fresh_log() + package.loaded['services.log'] = nil + local svc = require 'services.log' + svc._conn = nil + return svc +end + +-- Start the log service in its own child scope; return the scope. +local function start_log(bus, root_scope, opts) + local svc_scope = root_scope:child() + svc_scope:spawn(function() + package.loaded['services.log'] = nil + local log_svc = require 'services.log' + local svc_conn = bus:connect() + log_svc.start(svc_conn, opts or { name = 'log' }) + end) + return svc_scope +end + +local function stop_scope(svc_scope) + svc_scope:cancel('test done') + perform(svc_scope:join_op()) +end + +-- Receive up to `max` messages from `sub`, skipping lifecycle status msgs. +local function drain(sub, max) + max = max or 20 + local msgs = {} + for _ = 1, max do + local ok, m = perform(op.choice( + sub:recv_op():wrap(function(msg) return true, msg end), + fibers.always(false) + )) + if not ok then break end + if m.topic[1] ~= 'svc' then + msgs[#msgs + 1] = m + end + end + return msgs +end + +------------------------------------------------------------------------------- +-- Unit tests: singleton API (no scheduler needed) +------------------------------------------------------------------------------- + +TestLogSingleton = {} + +function TestLogSingleton:setUp() + self.log = fresh_log() +end + +function TestLogSingleton:test_has_all_log_level_methods() + for _, mode in ipairs(rxilog.modes) do + luaunit.assertEquals(type(self.log[mode.name]), 'function', + 'missing log level method: ' .. mode.name) + end +end + +function TestLogSingleton:test_log_without_conn_does_not_error() + luaunit.assertNil(self.log._conn) + -- Must not raise even though no connection is wired up. + self.log.info('no-conn test') + self.log.warn('no-conn warn') +end + +function TestLogSingleton:test_singleton_identity() + local a = require 'services.log' + local b = require 'services.log' + luaunit.assertIs(a, b) +end + +------------------------------------------------------------------------------- +-- Integration tests: bus publish behaviour +-- +-- LuaUnit.run() is called inside fibers.run() at the entry point, so every +-- test method body already runs inside a fiber and may call perform() freely. +------------------------------------------------------------------------------- + +TestLogBusPublish = {} + +-- Wire the singleton connection manually (without running the full service) +-- and verify that a single log call produces exactly one bus message. +function TestLogBusPublish:test_info_publishes_to_bus() + local log = fresh_log() + local bus = make_bus() + local conn = bus:connect() + log._conn = conn + + local sub = conn:subscribe({ 'logs', '+' }) + + fibers.spawn(function() + log.info('hello from test') + end) + + local received = perform(op.choice( + sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + + log._conn = nil + + luaunit.assertNotNil(received, 'expected one published log message') + luaunit.assertEquals(received.topic[1], 'logs') + luaunit.assertEquals(received.topic[2], 'info') + luaunit.assertNotNil(received.payload.message) + luaunit.assertNotNil(received.payload.timestamp) + luaunit.assertStrContains(received.payload.message, 'hello from test') +end + +-- Every rxilog level must produce a message on {'logs', }. +function TestLogBusPublish:test_all_levels_publish_correct_topic() + local log = fresh_log() + local bus = make_bus() + local conn = bus:connect() + log._conn = conn + + local sub = conn:subscribe({ 'logs', '+' }) + + fibers.spawn(function() + for _, mode in ipairs(rxilog.modes) do + log[mode.name]('level test: ' .. mode.name) + end + end) + + local levels_seen = {} + for _ = 1, #rxilog.modes do + local msg = perform(op.choice( + sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if msg then + levels_seen[msg.topic[2]] = true + end + end + + log._conn = nil + + for _, mode in ipairs(rxilog.modes) do + luaunit.assertTrue(levels_seen[mode.name], + 'no message published for level: ' .. mode.name) + end +end + +-- Payload must contain a non-empty string `message` and a positive `timestamp`. +function TestLogBusPublish:test_message_payload_fields() + local log = fresh_log() + local bus = make_bus() + local conn = bus:connect() + log._conn = conn + + local sub = conn:subscribe({ 'logs', '+' }) + + fibers.spawn(function() + log.warn('payload field check') + end) + + local msg = perform(op.choice( + sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + + log._conn = nil + + luaunit.assertNotNil(msg) + local payload = msg.payload + luaunit.assertEquals(type(payload.message), 'string', 'message must be a string') + luaunit.assertEquals(type(payload.timestamp), 'number', 'timestamp must be a number') + luaunit.assertTrue(payload.timestamp > 0, 'timestamp must be positive') + luaunit.assertStrContains(payload.message, 'WARN') + luaunit.assertStrContains(payload.message, 'payload field check') +end + +-- When _conn is nil, log calls must not publish anything to the bus. +function TestLogBusPublish:test_no_publish_without_connection() + local log = fresh_log() -- _conn is nil + local bus = make_bus() + local conn = bus:connect() + + local sub = conn:subscribe({ 'logs', '+' }) + + fibers.spawn(function() + log.info('should not be published') + end) + + local count = 0 + perform(op.choice( + sub:recv_op():wrap(function() + count = count + 1 + return true + end), + sleep.sleep_op(0.05):wrap(function() return false end) + )) + + luaunit.assertEquals(count, 0, 'no messages should be published when _conn is nil') +end + +------------------------------------------------------------------------------- +-- Service lifecycle tests +-- +-- These run the full log service in a child scope and verify the status +-- messages it retains on {'svc', 'log', 'status'}. +------------------------------------------------------------------------------- + +TestLogServiceLifecycle = {} + +function TestLogServiceLifecycle:test_publishes_starting_running_stopped() + local root = fibers.current_scope() + local bus = make_bus() + local conn = bus:connect() + + local status_sub = conn:subscribe({ 'svc', 'log', 'status' }) + local svc_scope = start_log(bus, root) + + -- Collect 'starting' and 'running'. + local states = {} + for _ = 1, 2 do + local msg = perform(op.choice( + status_sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if msg and msg.payload then + states[#states + 1] = msg.payload.state + end + end + + -- Trigger shutdown; expect 'stopped'. + stop_scope(svc_scope) + + local msg = perform(op.choice( + status_sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if msg and msg.payload then + states[#states + 1] = msg.payload.state + end + + local found = {} + for _, s in ipairs(states) do found[s] = true end + luaunit.assertTrue(found['starting'], "expected 'starting' status") + luaunit.assertTrue(found['running'], "expected 'running' status") + luaunit.assertTrue(found['stopped'], "expected 'stopped' status after cancel") +end + +-- Once the service is running, log calls (via the singleton) are published. +function TestLogServiceLifecycle:test_service_publishes_log_entries() + local root = fibers.current_scope() + local bus = make_bus() + local conn = bus:connect() + + local log_sub = conn:subscribe({ 'logs', '+' }) + local status_sub = conn:subscribe({ 'svc', 'log', 'status' }) + local svc_scope = start_log(bus, root) + + -- Wait for the service to reach 'running' before logging. + while true do + local smsg = perform(op.choice( + status_sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if not smsg then break end + if smsg.payload and smsg.payload.state == 'running' then break end + end + + -- The singleton _conn is now wired by the service; emit a message. + local log = require 'services.log' + log.info('lifecycle publish test') + + -- Drain until we find our specific message (the service may emit its own + -- startup trace first). + local received + for _ = 1, 5 do + local msg = perform(op.choice( + log_sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if not msg then break end + if msg.payload and msg.payload.message and + msg.payload.message:find('lifecycle publish test', 1, true) then + received = msg + break + end + end + + stop_scope(svc_scope) + + luaunit.assertNotNil(received, 'expected log entry to be published by running service') + luaunit.assertEquals(received.topic[2], 'info') + luaunit.assertStrContains(received.payload.message, 'lifecycle publish test') +end + +-- After the service scope is cancelled, _conn must be cleared so subsequent +-- log calls are not published. +function TestLogServiceLifecycle:test_conn_cleared_after_stop() + local root = fibers.current_scope() + local bus = make_bus() + local conn = bus:connect() + + local status_sub = conn:subscribe({ 'svc', 'log', 'status' }) + local svc_scope = start_log(bus, root) + + -- Wait until running. + while true do + local smsg = perform(op.choice( + status_sub:recv_op(), + sleep.sleep_op(1):wrap(function() return nil end) + )) + if not smsg then break end + if smsg.payload and smsg.payload.state == 'running' then break end + end + + stop_scope(svc_scope) + + -- After stop the singleton's _conn must be nil. + local log = require 'services.log' + luaunit.assertNil(log._conn, '_conn must be nil after service stops') +end + +------------------------------------------------------------------------------- +-- Entry point +------------------------------------------------------------------------------- + +if is_entry_point then + fibers.run(function() + os.exit(luaunit.LuaUnit.run()) + end) +end From f29de0fa76986994fe092a2fd1f856f94cb5400e Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Tue, 17 Mar 2026 15:58:52 +0000 Subject: [PATCH 3/6] add log service documentation: outline functionality, levels, and service flow --- docs/specs/log.md | 128 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 128 insertions(+) create mode 100644 docs/specs/log.md diff --git a/docs/specs/log.md b/docs/specs/log.md new file mode 100644 index 00000000..222ed452 --- /dev/null +++ b/docs/specs/log.md @@ -0,0 +1,128 @@ +# Log Service + +## Description + +The log service is a **singleton** that provides structured logging to the rest of the system. It has two modes of operation: + +- **Pre-connection** — log calls write to the console (via `rxilog`) only, with no bus activity. +- **Post-connection** — once `log_service.start()` has wired up a bus connection, every log call also publishes a structured message to `{'logs', }` on the bus so that other services (e.g. a log checker or an uploader) can subscribe to them. + +Because it is a singleton registered in `package.loaded`, any module that calls `require 'services.log'` gets the same instance regardless of load order or scope boundaries. + +## Levels + +The service exposes one method per log level, matching the levels defined in `rxilog`: + +| Method | Bus topic | Severity | +|--------|-----------|----------| +| `log.trace(...)` | `{'logs', 'trace'}` | lowest | +| `log.debug(...)` | `{'logs', 'debug'}` | | +| `log.info(...)` | `{'logs', 'info'}` | | +| `log.warn(...)` | `{'logs', 'warn'}` | | +| `log.error(...)` | `{'logs', 'error'}` | | +| `log.fatal(...)` | `{'logs', 'fatal'}` | highest | + +Each method always writes to the console. Publishing to the bus only happens when `log_service._conn` is set (i.e. after `start()` has run). + +## Bus Messages + +### Log entry (non-retained) + +Topic: `{'logs', }` + +Published for every log call made while the service is running. + +```lua +{ + message = "[LEVEL HH:MM:SS] file.lua:line message text", + timestamp = , -- realtime clock (seconds since epoch) +} +``` + +- `message` is the pre-formatted string produced by `rxilog.format_log_message`, including level, timestamp, source location, and message text. +- `timestamp` is the wall-clock time from `fibers.utils.time.realtime()`. + +### Service status (retained) + +Topic: `{'svc', , 'status'}` + +```lua +{ state = 'starting' | 'running' | 'stopped', ts = } +``` + +Published at each lifecycle transition. `name` defaults to `'log'` but can be overridden via `opts.name`. + +## Initialisation + +`log_service.start(conn, opts)` follows the standard service lifecycle: + +1. Retains `state = 'starting'` on `{'svc', name, 'status'}`. +2. Registers a `finally` block that clears `_conn` and retains `state = 'stopped'` when the scope exits. +3. Sets `log_service._conn = conn` so subsequent log calls publish to the bus. +4. Retains `state = 'running'`. +5. Emits its own first log entry: `log.trace("Log service started")`. +6. Blocks in a `sleep(math.huge)` loop — the service runs until its scope is cancelled. + +## Service Flow + +```mermaid +flowchart TD + St[Start] --> A(Retain status: starting) + A --> B(Register finally block) + B --> C(Set _conn = conn) + C --> D(Retain status: running) + D --> E(log.trace: Log service started) + E --> F{sleep forever} + F -->|scope cancelled| G(finally: clear _conn) + G --> H(Retain status: stopped) + H --> I[End] +``` + +## Architecture + +- The service owns a single long-lived sleep and relies entirely on scope cancellation for shutdown — there is no explicit shutdown message. +- The singleton pattern (`package.loaded["services.log"] = log_service`) means the connection state is global. Only one instance of `start()` should be running at a time. +- Log calls are synchronous: `_conn:publish` is called inline before returning to the caller. There is no batching or queue. +- Source location is captured at call time via `debug.getinfo(2, "Sl")`, so the `message` field always reflects the actual call site, not an internal log helper. +- The `finally` block guarantees `_conn` is cleared even if the scope fails rather than being cancelled cleanly. + +## Tests + +Tests live in `tests/test_log.lua` and are run with: + +```sh +cd tests && luajit test_log.lua +``` + +The entry point wraps `luaunit.LuaUnit.run()` inside `fibers.run()` so every test method can call `perform()` directly. + +### TestLogSingleton + +Unit tests that do not require the scheduler. + +| Test | What it checks | +|------|----------------| +| `test_has_all_log_level_methods` | Singleton exposes a callable method for every `rxilog` level | +| `test_log_without_conn_does_not_error` | Calling log methods with `_conn = nil` does not raise | +| `test_singleton_identity` | `require 'services.log'` twice returns the exact same table | + +### TestLogBusPublish + +Integration tests that wire `_conn` directly without running the full service. Each test operates inside the fiber scheduler and calls `perform()` to receive from the bus. + +| Test | What it checks | +|------|----------------| +| `test_info_publishes_to_bus` | A single `log.info()` produces exactly one `{'logs','info'}` message | +| `test_all_levels_publish_correct_topic` | Each level method publishes on its matching `{'logs', }` topic | +| `test_message_payload_fields` | Payload contains a formatted `message` string and a positive `timestamp` number | +| `test_no_publish_without_connection` | No bus messages are produced when `_conn` is `nil` | + +### TestLogServiceLifecycle + +Full-service tests that start the log service in a child scope via `start_log()` and observe its behaviour over its lifetime. + +| Test | What it checks | +|------|----------------| +| `test_publishes_starting_running_stopped` | Service retains `'starting'`, `'running'`, and `'stopped'` states in order | +| `test_service_publishes_log_entries` | Log calls made after the service reaches `'running'` are delivered to the bus | +| `test_conn_cleared_after_stop` | `log._conn` is `nil` after the service scope is cancelled | From dec399306af9a83e7c95b5f9c7c1f9232fa48fbd Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Wed, 18 Mar 2026 09:41:38 +0000 Subject: [PATCH 4/6] refactor rxilog.lua: reorganize log initialization and enhance error handling --- src/rxilog.lua | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/src/rxilog.lua b/src/rxilog.lua index 47a64ef7..ff71717b 100644 --- a/src/rxilog.lua +++ b/src/rxilog.lua @@ -7,14 +7,14 @@ -- under the terms of the MIT license. See LICENSE for details. -- -local log = { _version = "0.1.0" } - - +local log = {} +log._version = "0.1.0" log.usecolor = true log.outfile = nil log.level = "trace" +---@type {name: string, color: string}[] local modes = { { name = "trace", color = "\27[34m", }, { name = "debug", color = "\27[36m", }, @@ -25,12 +25,16 @@ local modes = { } +---@type table local levels = {} for i, v in ipairs(modes) do levels[v.name] = i end +---@param x number +---@param increment number? +---@return number local round = function(x, increment) increment = increment or 1 x = x / increment @@ -40,6 +44,8 @@ end local _tostring = tostring +---@param ... any +---@return string local tostring = function(...) local t = {} for i = 1, select('#', ...) do @@ -52,6 +58,10 @@ local tostring = function(...) return table.concat(t, " ") end +---@param level string +---@param lineinfo string +---@param msg string +---@return string local format_log_message = function(level, lineinfo, msg) return string.format("[%-6s%s] %s: %s", level:upper(), @@ -63,7 +73,6 @@ end for i, x in ipairs(modes) do local nameupper = x.name:upper() log[x.name] = function(...) - -- Return early if we're below the log level if i < levels[log.level] then return @@ -76,19 +85,21 @@ for i, x in ipairs(modes) do -- Output to console print(string.format("%s[%-6s%s]%s %s: %s", - log.usecolor and x.color or "", - nameupper, - os.date("%H:%M:%S"), - log.usecolor and "\27[0m" or "", - lineinfo, - msg)) + log.usecolor and x.color or "", + nameupper, + os.date("%H:%M:%S"), + log.usecolor and "\27[0m" or "", + lineinfo, + msg)) -- Output to log file if log.outfile then local fp = io.open(log.outfile, "a") - local str = format_log_message(nameupper, lineinfo, msg) - fp:write(str) - fp:close() + if fp then + local str = format_log_message(nameupper, lineinfo, msg) + fp:write(str) + fp:close() + end end end end From 8c87b980a30681d2fd8cab445f401d9e606c77df Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Wed, 18 Mar 2026 09:41:50 +0000 Subject: [PATCH 5/6] update log message format in documentation: clarify structure and example output --- docs/specs/log.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/specs/log.md b/docs/specs/log.md index 222ed452..a110db1f 100644 --- a/docs/specs/log.md +++ b/docs/specs/log.md @@ -34,11 +34,13 @@ Published for every log call made while the service is running. ```lua { - message = "[LEVEL HH:MM:SS] file.lua:line message text", + message = "[INFO Thu Mar 18 14:23:45 2026] src/main.lua:42: Something happened", timestamp = , -- realtime clock (seconds since epoch) } ``` +The format is `[%-6s%s] %s: %s` — level left-padded to 6 chars, then the full `os.date()` string (locale-dependent, e.g. `Thu Mar 18 14:23:45 2026`), then `file.lua:line`, then the message. Note: the file-output timestamp uses `os.date()` with no format argument (full date/time), unlike the console output which uses `os.date("%H:%M:%S")`. + - `message` is the pre-formatted string produced by `rxilog.format_log_message`, including level, timestamp, source location, and message text. - `timestamp` is the wall-clock time from `fibers.utils.time.realtime()`. From 0e7c1ec71d44042e20c4df8032f43c45ce995e44 Mon Sep 17 00:00:00 2001 From: Ryan Name Date: Wed, 18 Mar 2026 09:41:57 +0000 Subject: [PATCH 6/6] refactor log.lua: improve code organization and enhance readability --- src/services/log.lua | 24 ++++++++++++++++-------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/src/services/log.lua b/src/services/log.lua index 91f878ab..58b9135f 100644 --- a/src/services/log.lua +++ b/src/services/log.lua @@ -5,22 +5,29 @@ -- - when conn is set (after start), publishes log entries to {'logs', } -- - publishes service lifecycle status to {'svc', , 'status'} -local rxilog = require 'rxilog' -local runtime = require 'fibers.runtime' -local fibers = require 'fibers' -local sleep = require 'fibers.sleep' -local perform = require 'fibers.performer'.perform +local rxilog = require 'rxilog' +local runtime = require 'fibers.runtime' +local fibers = require 'fibers' +local sleep = require 'fibers.sleep' +local perform = require 'fibers.performer'.perform +local time_utils = require 'fibers.utils.time' local log_service = {} +---@return (string|number)[] local function t(...) return { ... } end +---@return number local function now() return runtime.now() end +---@param conn Connection +---@param name string +---@param state string +---@param extra table? local function publish_status(conn, name, state, extra) local payload = { state = state, ts = now() } if type(extra) == 'table' then @@ -39,7 +46,6 @@ for _, mode in ipairs(rxilog.modes) do if log_service._conn then local info = debug.getinfo(2, "Sl") local lineinfo = info.short_src .. ":" .. info.currentline - local time_utils = require 'fibers.utils.time' log_service._conn:publish(t('logs', level), { message = rxilog.format_log_message(level:upper(), lineinfo, msg), timestamp = time_utils.realtime(), @@ -48,6 +54,8 @@ for _, mode in ipairs(rxilog.modes) do end end +---@param conn Connection +---@param opts {name: string?}? function log_service.start(conn, opts) opts = opts or {} local name = opts.name or 'log' @@ -60,8 +68,8 @@ function log_service.start(conn, opts) rxilog.trace("Log: stopped") end) - log_service._conn = conn - log_service._name = name + log_service._conn = conn + log_service._name = name publish_status(conn, name, 'running') log_service.trace("Log service started")