Skip to content

Commit f8a1c10

Browse files
Oleg Chaplashkinylobankov
authored andcommitted
Add logging to unified file
We present a new functionality for logging. > How it worked before Having written a simple test with two servers (bob and frank) we would like to see the output of each of them and also what is going on in the test itself. For example: g.test_with_bob_and_frank = function() g.bob:exec(function() require('log').info('Hi, Frank!') end) g.frank:exec(function() require('log').info('Hi, Bob!') end) require('log').info('Hi, Bob and Frank!') end In order to see their greetings, we had to look over each server log file: $ cat /tmp/t/${BOB_VARDIR}/bob.log ... 2023-12-19 18:34:26.305 [84739] main/109/main I> Hi, Frank! $ cat /tmp/t/${FRANK_VARDIR}/frank.log ... 2023-12-19 18:34:26.306 [84752] main/109/main I> Hi, Bob! And there was no way to see the "Hi, Bob and Frank!" log message at all. > How it works now Now, if we provide the `-l, --log` parameter with the file path, we will see the following contents in the specified log file: $ ./bin/luatest -c -v -l run.log <test> && cat run.log ... bob | 2023-12-19 18:39:40.858 [85021] main/109/main I> Hi, Frank! frank | 2023-12-19 18:39:40.859 [85034] main/109/main I> Hi, Bob! luatest | 2023-12-19 18:39:40.860 [85034] main/109/main I> Hi, Bob and Frank! > What's under the hood The solution is based on the existing OutputBeautifier module logic: it can already read data from standard output streams (stdout/stderr) and print it to luatest stdout. When we run luatest (this is Tarantool process), we read stderr stream and hijack it. All logs of this process will be written to stderr and intercepted by the OutputBeautifier fiber. The fiber processes them and writes to our specified file for all logs and then to the luatest stdout stream. To save the log file for each server separately (this can be useful for research) we use the standard `tee` command. In the case of the server, we configure logging as follows: ' | tee ${TARANTOOL_WORKDIR}/${TARANTOOL_ALIAS}.log' While the server is running, all logs will be redirected to the `tee` command. So it will be written to the server log file and stdout stream. Then all data from stdout will be intercepted by the OutputBeautifier fiber that will write it to the specified log file. > What's new in administration New options: `-l, --log PATH` - set the path to the unified log file. `--runner-log-prefix NAME` - set the log prefix for luatest runner, 'luatest' by default. Improved options: `-v` - increase output verbosity (as it was before) and set `INFO` log level for luatest runner. `-vv` - increase log verbosity to `VERBOSE` level for luatest runnner. `-vvv` - increase log verbosity to `DEBUG` level for luatest runnner. Closes #324
1 parent 6009417 commit f8a1c10

File tree

5 files changed

+154
-11
lines changed

5 files changed

+154
-11
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
# Changelog
22

3+
## Unreleased
4+
5+
- Add logging to unified file (gh-324).
6+
37
## 1.0.1
48

59
- Fixed incorrect Unix domain socket path length check (gh-341).

luatest/output_beautifier.lua

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,29 +59,41 @@ end
5959
-- @string object.prefix String to prefix each output line with.
6060
-- @string[opt] object.color Color name for prefix.
6161
-- @string[opt] object.color_code Color code for prefix.
62+
-- @boolean[opt] object.runner Mark OutputBeautifier object is created for runner process.
6263
-- @return input object.
6364
function OutputBeautifier:new(object)
64-
checks('table', {prefix = 'string', color = '?string', color_code = '?string'})
65+
checks('table', {prefix = 'string', color = '?string', color_code = '?string', runner = '?boolean'})
6566
return self:from(object)
6667
end
6768

6869
function OutputBeautifier.mt:initialize()
6970
self.color_code = self.color_code or
7071
self.class.COLOR_BY_NAME[self.color] or
7172
OutputBeautifier:next_color_code()
72-
self.pipes = {stdout = ffi_io.create_pipe(), stderr = ffi_io.create_pipe()}
73+
self.pipes = {stderr = ffi_io.create_pipe()}
74+
if not self.runner then
75+
self.pipes.stdout = ffi_io.create_pipe()
76+
end
7377
self.stderr = ''
78+
self.enable_capture = nil
7479
end
7580

7681
-- Replace standard output descriptors with pipes.
82+
-- Stdout descriptor of the runner process will not be replaced
83+
-- because it is used for displaying all captured data from other processes.
7784
function OutputBeautifier.mt:hijack_output()
78-
ffi_io.dup2_io(self.pipes.stdout[1], io.stdout)
85+
if not self.runner then
86+
ffi_io.dup2_io(self.pipes.stdout[1], io.stdout)
87+
end
7988
ffi_io.dup2_io(self.pipes.stderr[1], io.stderr)
8089
end
8190

8291
-- Start fibers that reads from pipes and prints formatted output.
8392
-- Pass `track_pid` option to automatically stop forwarder once process is finished.
8493
function OutputBeautifier.mt:enable(options)
94+
if options and options.enable_capture ~= nil then
95+
self.enable_capture = options.enable_capture == false
96+
end
8597
if self.fibers then
8698
return
8799
end
@@ -140,8 +152,10 @@ end
140152
-- Every line with log level mark (` X> `) changes the color for all the following
141153
-- lines until the next one with the mark.
142154
function OutputBeautifier.mt:run(fd, pipe)
143-
local prefix = self.color_code .. self.prefix .. ' | '
155+
local prefix = self.prefix .. ' | '
156+
local colored_prefix = self.color_code .. prefix
144157
local line_color_code = self.class.RESET_TERM
158+
local log_file = rawget(_G, 'log_file')
145159
while fiber.testcancel() or true do
146160
self:process_fd_output(fd, function(chunks)
147161
local raw_lines = table.concat(chunks)
@@ -154,7 +168,16 @@ function OutputBeautifier.mt:run(fd, pipe)
154168
end
155169
for _, line in pairs(lines) do
156170
line_color_code = self:color_for_line(line) or line_color_code
157-
io.stdout:write(table.concat({prefix, line_color_code, line, self.class.RESET_TERM, '\n'}))
171+
if not self.runner or self.enable_capture then
172+
io.stdout:write(
173+
table.concat(
174+
{colored_prefix, line_color_code, line, self.class.RESET_TERM,'\n'}
175+
)
176+
)
177+
end
178+
if log_file ~= nil then
179+
log_file.fh:write(table.concat({prefix, line, '\n'}))
180+
end
158181
fiber.yield()
159182
end
160183
end)

luatest/runner.lua

Lines changed: 62 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
local clock = require('clock')
66
local fio = require('fio')
7+
local log = require('log')
78

89
local assertions = require('luatest.assertions')
910
local capturing = require('luatest.capturing')
@@ -16,6 +17,7 @@ local Server = require('luatest.server')
1617
local sorted_pairs = require('luatest.sorted_pairs')
1718
local TestInstance = require('luatest.test_instance')
1819
local utils = require('luatest.utils')
20+
local OutputBeautifier = require('luatest.output_beautifier')
1921

2022
local ROCK_VERSION = require('luatest.VERSION')
2123

@@ -56,6 +58,50 @@ function Runner.run(args, options)
5658
end
5759
options = utils.merge(options.luatest.configure(), Runner.parse_cmd_line(args), options)
5860

61+
local log_prefix = options.log_prefix or 'luatest'
62+
local log_cfg = string.format("%s.log", log_prefix)
63+
64+
if options.log_file then
65+
-- Save the file descriptor as a global variable to use it
66+
-- in the `output_beautifier` module: this module is connected to the
67+
-- the `server` module. We cannot link the `server` module to the `runner`
68+
-- module to explicitly give this parameter.
69+
local fh = fio.open(options.log_file, {'O_CREAT', 'O_WRONLY'}, tonumber('640', 8))
70+
rawset(_G, 'log_file', {fh = fh})
71+
72+
local output_beautifier = OutputBeautifier:new({prefix = log_prefix, runner = true})
73+
output_beautifier:enable({enable_capture = options.enable_capture})
74+
output_beautifier:hijack_output()
75+
76+
-- `tee` copy logs to file and also to standard output, but we need
77+
-- process all captured data through the OutputBeatifier object.
78+
-- Data will be redirected back to stderr of the current process.
79+
-- `/dev/fd/2` is a symlink to `/proc/self/fd`, where `/proc/self` is
80+
-- a symlink to `/proc/<PID>`. So `/dev/fd/2` is equal to `/proc/<PID>/fd/2`
81+
-- and it means "stderr of the current process".
82+
log_cfg = string.format("| tee %s > /dev/fd/2", log_cfg)
83+
end
84+
-- Logging cannot be initialized without configuring the `box` engine
85+
-- on a version less than 2.5.1 (see more details at [1]). Otherwise,
86+
-- this causes the `attempt to call field 'cfg' (a nil value)` error,
87+
-- so there are the following limitations:
88+
-- 1. There is no `luatest.log` file (but logs are still available
89+
-- in stdout and in the `run.log` file);
90+
-- 2. All logs from luatest are non-formatted and look like:
91+
--
92+
-- luatest | My log message
93+
--
94+
-- [1]: https://github.com/tarantool/tarantool/issues/689
95+
if utils.version_current_ge_than(2, 5, 1) then
96+
-- Initialize logging for luatest runner.
97+
-- The log format will be as follows:
98+
-- YYYY-MM-DD HH:MM:SS.ZZZ [ID] main/.../luatest I> ...
99+
log.cfg{
100+
log = log_cfg,
101+
level = options.log_level or 5,
102+
}
103+
end
104+
59105
if options.help then
60106
print(Runner.USAGE)
61107
return 0
@@ -85,12 +131,17 @@ Positional arguments:
85131
Options:
86132
-h, --help: Print this help
87133
--version: Print version information
88-
-v, --verbose: Increase verbosity
134+
-v, --verbose: Increase output verbosity for luatest runnner
135+
-vv, Increase log verbosity to VERBOSE level for luatest runnner
136+
-vvv, Increase log verbosity to DEBUG level for luatest runnner
89137
-q, --quiet: Set verbosity to minimum
90138
-c Disable capture
91139
-b Print full backtrace (don't remove luatest frames)
92140
-e, --error: Stop on first error
93141
-f, --failure: Stop on first failure or error
142+
-l, --log PATH: Path to the unified log file
143+
--runner-log-prefix NAME:
144+
Log prefix for luatest runner, 'luatest' by default
94145
--shuffle VALUE: Set execution order:
95146
- group[:seed] - shuffle tests within group
96147
- all[:seed] - shuffle all tests
@@ -135,6 +186,12 @@ function Runner.parse_cmd_line(args)
135186
result.version = true
136187
elseif arg == '--verbose' or arg == '-v' then
137188
result.verbosity = GenericOutput.VERBOSITY.VERBOSE
189+
elseif arg == '-vv' then
190+
result.verbosity = GenericOutput.VERBOSITY.VERBOSE
191+
result.log_level = 6 -- verbose
192+
elseif arg == '-vvv' then
193+
result.verbosity = GenericOutput.VERBOSITY.VERBOSE
194+
result.log_level = 7 -- debug
138195
elseif arg == '--quiet' or arg == '-q' then
139196
result.verbosity = GenericOutput.VERBOSITY.QUIET
140197
elseif arg == '--fail-fast' or arg == '-f' then
@@ -145,6 +202,10 @@ function Runner.parse_cmd_line(args)
145202
if seed then
146203
result.seed = tonumber(seed) or error('Invalid seed value')
147204
end
205+
elseif arg == '-l' or arg == '--log' then
206+
result.log_file = tostring(next_arg()) or error('Invalid log file path')
207+
elseif arg == '--runner-log-prefix' then
208+
result.log_prefix = tostring(next_arg())
148209
elseif arg == '--seed' then
149210
result.seed = tonumber(next_arg()) or error('Invalid seed value')
150211
elseif arg == '--output' or arg == '-o' then

luatest/server.lua

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,10 @@ function Server:initialize()
212212

213213
local prefix = fio.pathjoin(Server.vardir, 'artifacts', self.rs_id or '')
214214
self.artifacts = fio.pathjoin(prefix, self.id)
215+
216+
if rawget(_G, 'log_file') ~= nil then
217+
self.unified_log_enabled = true
218+
end
215219
end
216220

217221
-- Create a table with env variables based on the constructor params.
@@ -223,6 +227,7 @@ end
223227
-- * `TARANTOOL_ALIAS`
224228
-- * `TARANTOOL_HTTP_PORT`
225229
-- * `TARANTOOL_BOX_CFG`
230+
-- * `TARANTOOL_UNIFIED_LOG_ENABLED`
226231
--
227232
-- @return table
228233
function Server:build_env()
@@ -235,6 +240,9 @@ function Server:build_env()
235240
if self.box_cfg ~= nil then
236241
res.TARANTOOL_BOX_CFG = json.encode(self.box_cfg)
237242
end
243+
if self.unified_log_enabled then
244+
res.TARANTOOL_UNIFIED_LOG_ENABLED = tostring(self.unified_log_enabled)
245+
end
238246
return res
239247
end
240248

@@ -743,7 +751,16 @@ end
743751
function Server:grep_log(pattern, bytes_num, opts)
744752
local options = opts or {}
745753
local reset = options.reset or true
746-
local filename = options.filename or self:exec(function() return box.cfg.log end)
754+
755+
-- `box.cfg.log` can contain not only the path to the log file.
756+
-- When unified logging mode is on, `box.cfg.log` is as follows:
757+
--
758+
-- | tee ${TARANTOOL_WORKDIR}/${TARANTOOL_ALIAS}.log
759+
--
760+
-- Therefore, we set `_G.box_cfg_log_file` in server_instance.lua which
761+
-- contains the log file path: ${TARANTOOL_WORKDIR}/${TARANTOOL_ALIAS}.log.
762+
local filename = options.filename or self:exec(function()
763+
return rawget(_G, 'box_cfg_log_file') or box.cfg.log end)
747764
local file = fio.open(filename, {'O_RDONLY', 'O_NONBLOCK'})
748765

749766
local function fail(msg)

luatest/server_instance.lua

Lines changed: 42 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,55 @@
11
local fio = require('fio')
22
local fun = require('fun')
33
local json = require('json')
4+
local log = require('log')
45

56
local TIMEOUT_INFINITY = 500 * 365 * 86400
67

8+
local function log_cfg()
9+
-- `log.cfg{}` is available since 2.5.1 version only. See more
10+
-- details at https://github.com/tarantool/tarantool/issues/689.
11+
if log.cfg ~= nil then
12+
-- Logging may be initialized before `box.cfg{}` call:
13+
--
14+
-- server:new({
15+
-- env = {['TARANTOOL_RUN_BEFORE_BOX_CFG'] = [[
16+
-- require('log').cfg{ log = <custom_log_file> }
17+
-- ]]})
18+
--
19+
-- This causes the `Can't set option 'log' dynamically` error,
20+
-- so we need to return the old log file path.
21+
if log.cfg.log ~= nil then
22+
return log.cfg.log
23+
end
24+
end
25+
local log_file = fio.pathjoin(
26+
os.getenv('TARANTOOL_WORKDIR'),
27+
os.getenv('TARANTOOL_ALIAS') .. '.log'
28+
)
29+
-- When `box.cfg.log` is called, we may get a string like
30+
--
31+
-- | tee ${TARANTOOL_WORKDIR}/${TARANTOOL_ALIAS}.log
32+
--
33+
-- Some tests or functions (e.g. Server:grep_log) may request the
34+
-- log file path, so we save it to a global variable. Thus it can
35+
-- be obtained by `rawget(_G, 'box_cfg_log_file')`.
36+
rawset(_G, 'box_cfg_log_file', log_file)
37+
38+
local unified_log_enabled = os.getenv('TARANTOOL_UNIFIED_LOG_ENABLED')
39+
if unified_log_enabled then
40+
-- Redirect the data stream to two sources at once:
41+
-- to the standard stream (stdout) and to the file
42+
-- ${TARANTOOL_WORKDIR}/${TARANTOOL_ALIAS}.log.
43+
return string.format('| tee %s', log_file)
44+
end
45+
return log_file
46+
end
47+
748
local function default_cfg()
849
return {
950
work_dir = os.getenv('TARANTOOL_WORKDIR'),
1051
listen = os.getenv('TARANTOOL_LISTEN'),
11-
log = fio.pathjoin(
12-
os.getenv('TARANTOOL_WORKDIR'),
13-
os.getenv('TARANTOOL_ALIAS') .. '.log'
14-
),
52+
log = log_cfg()
1553
}
1654
end
1755

0 commit comments

Comments
 (0)