rb_falcon_logger 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 0bad75b78e2dc2cc1edc991a6e7fc912937e45252605057c324ce54a630120f2
4
+ data.tar.gz: 250fe6f8ec33ece120659375e8b2869a8046c28bbd31715ba4d9b6c4824e8ada
5
+ SHA512:
6
+ metadata.gz: 70f5902ea03b8c144d6845da7514112602a859c1e8249a84dbb38df012269db05b069aea38bcc35b51bfd1027ed5d1ba2f16cc01057c4c23680c7b24c203aa1d
7
+ data.tar.gz: f6da83bc50a6ccd04deee151da80e138aa3e86189202b2049f58d68fcda181c18fca8f55d4bf7f9447cd6f5aaae78188527f3f6f884a8d648d21feb6e688f6f1
data/LICENSE.txt ADDED
@@ -0,0 +1,23 @@
1
+ MIT License
2
+
3
+ Copyright (c) since 2025: rb-falcon-logger Ruby Gem
4
+ description: fast logger with multiple modes and formats
5
+ author: J. Arrizza email: cppgent0@gmail.com
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in all
15
+ copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
23
+ SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,215 @@
1
+ * website: <https://arrizza.com/rb-falcon-logger.html>
2
+ * installation: see <https://arrizza.com/setup-common.html>
3
+
4
+ ## Summary
5
+
6
+ This is a ruby gem that provides a way to run a fast logger.
7
+ see <https://arrizza.com/python-falcon-logger.html> for a python version
8
+
9
+ ## Sample code
10
+
11
+ see src/logger_tester.rb for a full example
12
+
13
+ Use doit script to run the logger.
14
+
15
+ TODO logger_tester args
16
+ TODO multithreaded_tester
17
+
18
+ ## parameters and functions
19
+
20
+ Use these on the logger creation: ```@log = FalconLogger(path: etc, max_entries: etc, mode: etc.)```
21
+
22
+ * ```path: nil``` : (default) write to stdout
23
+ * ```path: 'path/to/file'``` : write to the given file
24
+ * ```max_entries: 10```: flush the queue when this number of log entries is reached.
25
+ * ```loop_delay: 0.250```: check the queue this often. The longer the delay, the fast it runs.
26
+ * Note: use max_entries: 1 and loop_delay: 0.001, to flush the queue as often as possible
27
+ * ```set_verbose(true)``` - (default) print all log lines to stdout/file.
28
+ * ```set_verbose(false)``` - only print err(), critical(), exception(), and bug() lines
29
+ * ```@log.set_max_dots(num)``` set the number of dots to print before a newline is printed
30
+ * use ```@log.save``` to force the queue to be saved. Generally not recommended.
31
+ * use ```@log.term``` to clear out the queue and gracefully end the background thread
32
+
33
+ There are 3 modes: ```FalconLogger(mode: 'normal')```
34
+
35
+ * nil or 'normal': (default) writes a line to stdout or the file. See below for outputs
36
+ * 'ut' or 'mock': useful in UTs. These do not write to stdout or to a file.
37
+ * The lines are saved in an internal list accessible by: ```@log.ut_lines```.
38
+ * Use ```@log.ut_clear``` or ```@log.ut_lines = []``` to empty it.
39
+ * 'null': is useful when you don't want any output. No stdout, no file, no ut_lines.
40
+
41
+ There are 3 formats: 'elapsed' (default), 'prefix', 'none'
42
+
43
+ * Use ```@log.set_format(fmt)``` to set the format to fmt
44
+
45
+ ## 'elapsed' format outputs
46
+
47
+ These contain:
48
+
49
+ * the number of MM:SS.nnn milliseconds since the last log line
50
+ * if this the first log line, the current date time stamp is automatically printed:
51
+ ```DTS 2025/05/11 16:21:43.170```
52
+ * if an hour goes by since the last DTS, then it is automatically printed
53
+ * a prefix indicating the type of log line error, OK, Debug, etc.
54
+
55
+ ```text
56
+ @log.set_format('elapsed') # the default format
57
+
58
+ # idx = 2
59
+
60
+ # === showing with elapsed time delays
61
+ # === @log.start("#{idx}:", 'start of dts values')
62
+ # === sleep(0.250)
63
+ # === @log.line("#{idx}:", 'start + 250ms')
64
+ # === sleep(0.123)
65
+ # === @log.line("#{idx}:", 'start + 373ms')
66
+ 00:00.000 ==== 2: start of dts values
67
+ 00:00.250 2: start + 250ms
68
+ 00:00.373 2: start + 373ms
69
+
70
+ # === the automatic DTS line when 1 hour has elapsed or at the beginning
71
+ # === @log.start("#{idx}:", 'start')
72
+ DTS 2025/05/21 19:01:11.154
73
+ 00:00.000 ==== 2: start
74
+
75
+ # === @log.line("#{idx}:", 'line')
76
+ 00:00.000 2: line
77
+
78
+ # === @log.highlight("#{idx}:", 'highlight')
79
+ 00:00.000 ---> 2: highlight
80
+
81
+ # === @log.ok("#{idx}:", 'ok')
82
+ 00:00.000 OK 2: ok
83
+
84
+ # === @log.err("#{idx}:", 'err')
85
+ 00:00.000 ERR 2: err
86
+
87
+ # === @log.warn("#{idx}:", 'warn')
88
+ 00:00.000 WARN 2: warn
89
+
90
+ # === @log.bug("#{idx}:", 'bug')
91
+ 00:00.000 BUG 2: bug
92
+
93
+ # === @log.dbg("#{idx}:", 'dbg')
94
+ 00:00.000 DBG 2: dbg
95
+
96
+ # === @log.raw("#{idx}", 'raw', 'line')
97
+ 2 raw line
98
+
99
+ # === @log.output(nil, "#{idx}:", 'output (line nil)')
100
+ # === @log.output(21, "#{idx}:", 'output (line 21)')
101
+ 00:00.000 -- 2: output (line nil)
102
+ 00:00.000 -- 21] 2: output (line 21)
103
+
104
+ #=== lines = ["#{idx}: num_output (line 1)", "#{idx}: num_output (line 2)"]
105
+ #=== @log.num_output(lines)
106
+ 00:00.000 -- 1] 2: num_output (line 1)
107
+ 00:00.000 -- 2] 2: num_output (line 2)
108
+
109
+ # === @log.check(true, "#{idx}:", 'check true')
110
+ # === @log.check(false, "#{idx}:", 'check false')
111
+ 00:00.000 OK 2: check true
112
+ 00:00.000 ERR 2: check false
113
+
114
+ # === lines = ["#{idx}: check_all (line 1)", "#{idx}: check_all (line 2)"]
115
+ # === @log.check_all(true, 'check_all true title', lines)
116
+ 00:00.000 OK check_all true title: true
117
+ 00:00.000 OK - 2: check_all (line 1)
118
+ 00:00.000 OK - 2: check_all (line 2)
119
+
120
+ # === info = { 'key1' => ['val1'] }
121
+ # === @log.json(info, "#{idx}:", 'json', 'list (json/hash)')
122
+ 00:00.000 2: json list (json/hash)
123
+ 00:00.000 > {
124
+ 00:00.000 > "key1": [
125
+ 00:00.000 > "val1"
126
+ 00:00.000 > ]
127
+ 00:00.000 > }
128
+
129
+ #=== val = "\x00\x01\x02\x03\x04\x05\x06\x07\x08\x09\x0A\x0B\x0C\x0D\x0E\x0F\x10\x11"
130
+ #=== @log.hex(val, "#{idx}:", 'hex long')
131
+ 00:00.000 2: hex long
132
+ 00:00.000 0 0x00: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F
133
+ 00:00.000 16 0x10: 10 11
134
+
135
+ # === @log.debug("#{idx}:", 'debug')
136
+ 00:00.000 DBG 2: debug
137
+
138
+ # === @log.info("#{idx}:", 'info')
139
+ 00:00.000 2: info
140
+
141
+ # === @log.warning("#{idx}:", 'warning')
142
+ 00:00.000 WARN 2: warning
143
+
144
+ # === @log.error("#{idx}:", 'error')
145
+ 00:00.000 ERR 2: error
146
+
147
+ # === @log.critical("#{idx}:", 'critical')
148
+ 00:00.000 CRIT 2: critical
149
+
150
+ # === val = 5
151
+ # === begin
152
+ # === val /= 0
153
+ # === rescue ZeroDivisionError => e # <== note: use max_lines: 1 to reduce the amount of output you need
154
+ # === @log.exception(e)
155
+ # === end
156
+ # === @log.line("after excp; val=#{val}")
157
+ 00:00.000 EXCP divided by 0
158
+ 00:00.000 EXCP Backtrace:
159
+ 00:00.000 EXCP <snip>/logger_tester.rb:178:in `/'
160
+ 00:00.000 EXCP <snip>/logger_tester.rb:178:in `_log_exception'
161
+ 00:00.000 EXCP <snip>/logger_tester.rb:88:in `block in run'
162
+
163
+ # === @log.full_dts
164
+ DTS 2025/05/11 16:21:43.170
165
+ ```
166
+
167
+ ## 'prefix' format outputs
168
+
169
+ These are the same as elapsed format, except no elapsed times.
170
+ Use ```./doit normal prefix``` to see an example.
171
+
172
+ ```text
173
+ @log.set_format('prefix')
174
+
175
+ # === @log.ok("#{idx}:", 'ok')
176
+ OK 2: ok
177
+ # === @log.err("#{idx}:", 'err')
178
+ ERR 2: err
179
+ <snip>
180
+ ```
181
+
182
+ ## 'none' format outputs
183
+
184
+ These are the same as elapsed/prefix format, except no elapsed times or prefixes
185
+ Use ```./doit normal none``` to see an example.
186
+
187
+ ```text
188
+ @log.set_format('none')
189
+
190
+ # === @log.ok("#{idx}:", 'ok')
191
+ 2: ok
192
+ # === @log.err("#{idx}:", 'err')
193
+ 2: err
194
+ <snip>
195
+ ```
196
+
197
+ ## dots
198
+
199
+ To put out a series of dots to show progress, use ```@log.dot```.
200
+
201
+ Periodically a newline is printed.
202
+ The default is 25, use ```@log.set_max_dots(nn)``` to change it
203
+
204
+ ## ./doit examples
205
+
206
+ * Use ```./doit file``` to generate a file ```out/sample.log```
207
+ * Use ```./doit null``` to use ```mode: 'null'```, no output, no file generated
208
+ * Use ```./doit ut``` or ```./doit mock``` to use ```mode: 'ut'```. This mode can be used in unit tests.
209
+ * Use ```./doit thread``` to see an example of a multithreaded use of the logger. The default is:
210
+ * 100 client threads
211
+ * each client thread putting in 1000 entries onto the queue.
212
+ * There are random delays between each push() between 0 and 250ms
213
+ * each client thread logs it is putting an entry on the queue using FalconLogger
214
+ * 1 background thread checking that queue, and printing any entries using FalconLogger
215
+ * This takes just over 2m (on my PC)
@@ -0,0 +1,870 @@
1
+ # frozen_string_literal: true
2
+ require 'json'
3
+
4
+ # import atexit
5
+ # import threading
6
+
7
+ # -------------------
8
+ ## Sample gem source
9
+ class FalconLogger
10
+ ## logging formats
11
+ class LogFormat
12
+ include Enumerable
13
+
14
+ ## logging format with elapsed time and prefixes
15
+ ELAPSED = 1
16
+ ## logging format with prefixes only
17
+ PREFIX = 2
18
+ ## logging format with no prefixes or elapsed time
19
+ NONE = 3
20
+ end
21
+
22
+ ## configuration in for bg thread runner
23
+ class RunnerCfg
24
+ ## the maximum entries to hold in the queue before saving to the file
25
+ attr_accessor :max_entries
26
+ ## the maximum number of loops before the queue is emptied
27
+ attr_accessor :max_count
28
+ ## the delay between checking the queue for entries to save
29
+ attr_accessor :loop_delay
30
+
31
+ # -------------------
32
+ def initialize
33
+ @max_entries = 0
34
+ @max_count = 0
35
+ @loop_delay = 0.0
36
+ end
37
+
38
+ # -------------------
39
+ def copy_from(copy_obj)
40
+ @max_entries = copy_obj.max_entries
41
+ @max_count = copy_obj.max_count
42
+ @loop_delay = copy_obj.loop_delay
43
+ end
44
+ end
45
+
46
+ ## holds verbosity
47
+ attr_writer :verbose
48
+
49
+ ## UT only: holds all lines printed
50
+ attr_accessor :ut_lines
51
+
52
+ ###
53
+ ## constructor
54
+ # @param path nil for stdout, or full path to the logger file
55
+ # @param max_entries (optional) maximum number of entries before a flush is done; default 10
56
+ # @param loop_delay (optional) time between checking queue; default 0.250 seconds
57
+ # @param mode (optional) logging mode:
58
+ # nil or "normal": (default) log all lines as set by configuration, format, etc.
59
+ # "ut" or "mock": for UT purposes, saves lines in ut_lines array
60
+ # "null": do no logging; see verbosity for an alternative
61
+ def initialize(path: nil,
62
+ max_entries: 10,
63
+ loop_delay: 0.250,
64
+ mode: nil)
65
+ ## the full path to the log file (if any)
66
+ @path = path
67
+ ## file pointer to destination file or stdout
68
+ @fp = nil
69
+ ## holds the logging mode (nil, normal, ut, etc.
70
+ @logging_mode = mode
71
+ ## holds the function that handles the logging of the current mode and format
72
+ @log_it = nil
73
+
74
+ ## flag to log to stdout or not
75
+ @verbose = true
76
+ ## for UT only
77
+ @ut_mode = false
78
+ ## for UT only
79
+ @ut_lines = []
80
+
81
+ # === log formatting related
82
+
83
+ ## verbosity; if true print all lines, if not print only errors, excp ad bug lines
84
+ @verbose = true
85
+ ## the log display format to use; default: elapsed time + prefixes
86
+ @log_format = LogFormat::ELAPSED
87
+ ## holds the last time a full DTS was written to the log;
88
+ # printed at the beginning and once per hour
89
+ @start_time = Time.at(0.0)
90
+ ## current number of dots printed
91
+ @dots = 0
92
+ ## max number of dots to display
93
+ @max_dots = 25
94
+
95
+ # === runner() related
96
+ ## configuration values used by runner()
97
+ @runner_cfg = nil
98
+ ## backup of runner_cfg
99
+ @backup_cfg = nil
100
+ ## the bg thread used to run
101
+ @thread = nil
102
+ ## the queue used
103
+ @queue = nil
104
+ ## flag to the thread to end the loop
105
+ @finished = false
106
+
107
+ # === log_mode related
108
+ _init_cfg(max_entries, loop_delay)
109
+ if mode.nil? || mode == 'normal'
110
+ @logging_mode = 'normal'
111
+ @log_it = method(:_log_it_normal)
112
+
113
+ # initialize destination file pointer
114
+ @fp = $stdout
115
+ if @path.nil?
116
+ @fp = $stdout
117
+ else
118
+ @fp = File.open(@path, 'w', encoding: 'UTF-8')
119
+ end
120
+ _init_thread
121
+ elsif %w[ut mock].include?(mode)
122
+ _set_log_it_ut_fn
123
+ elsif mode == 'null'
124
+ @log_it = method(:_log_it_null)
125
+ else
126
+ raise(ArgumentError, "Unknown mode: \"#{mode}\", choose \"normal\", \"ut\", \"mock\" or \"null\"")
127
+ end
128
+
129
+ # try ensure at least one save() and thread cleanup is done
130
+ at_exit do
131
+ # uncomment to debug
132
+ # puts('in at_exit:')
133
+ term
134
+ end
135
+ end
136
+
137
+ # --------------------
138
+ ## initialize the configuration variables for the loop
139
+ #
140
+ # @param max_entries the max number of entries in the queue
141
+ # @param loop_delay how long to wait between checks of the queue
142
+ # @return nil
143
+ def _init_cfg(max_entries, loop_delay)
144
+ @runner_cfg = RunnerCfg.new
145
+ set_max_entries(max_entries)
146
+ set_loop_delay(loop_delay)
147
+ @backup_cfg = RunnerCfg.new
148
+ @backup_cfg.copy_from(@runner_cfg)
149
+ end
150
+
151
+ # --------------------
152
+ ## initialize and start the thread
153
+ #
154
+ # @return nil
155
+ def _init_thread
156
+ @queue = Queue.new
157
+ @finished = false
158
+
159
+ @thread = Thread.new do
160
+ _runner
161
+ end
162
+
163
+ # wait for thread to start
164
+ sleep(0.100)
165
+ end
166
+
167
+ # --------------------
168
+ ## UT only; clears ut_lines
169
+ # for backwards compatibility with python version
170
+ #
171
+ # @return nil
172
+ def ut_clear
173
+ @ut_lines = []
174
+ end
175
+
176
+ # --------------------
177
+ # for UT only; set the start_time to the given value
178
+ def ut_start_time(new_time)
179
+ if new_time.is_a?(Float)
180
+ @start_time = Time.at(new_time)
181
+ else
182
+ # assume it's a Time
183
+ @start_time = new_time
184
+ end
185
+ end
186
+
187
+ # --------------------
188
+ ## set verbosity
189
+ # for backwards compatibility with python version
190
+ #
191
+ # @param value (bool) verbosity level
192
+ # @return nil
193
+ def set_verbose(value)
194
+ @verbose = value
195
+ end
196
+
197
+ # --------------------
198
+ ## set log line format.
199
+ #
200
+ # @param form (str) either "elapsed", "prefix", "non" or throws excp
201
+ # @return nil
202
+ def set_format(form)
203
+ if form == 'elapsed'
204
+ @log_format = LogFormat::ELAPSED
205
+ elsif form == 'prefix'
206
+ @log_format = LogFormat::PREFIX
207
+ elsif form == 'none'
208
+ @log_format = LogFormat::NONE
209
+ else
210
+ raise(ArgumentError, "Unknown format: \"#{form}\", choose \"elapsed\", \"prefix\" or \"none\"")
211
+ end
212
+
213
+ _set_log_it_ut_fn if %w[ut mock].include?(@logging_mode)
214
+ end
215
+
216
+ # --------------------
217
+ ## set the log_it function to call based on the current logging format
218
+ #
219
+ # @return nil
220
+ def _set_log_it_ut_fn
221
+ if @log_format == LogFormat::ELAPSED
222
+ @log_it = method(:_log_it_ut_elapsed)
223
+ elsif @log_format == LogFormat::PREFIX
224
+ @log_it = method(:_log_it_ut_prefix)
225
+ elsif @log_format == LogFormat::NONE
226
+ @log_it = method(:_log_it_ut_none)
227
+ else
228
+ # :nocov:
229
+ # coverage: cannot be done in a UT
230
+ puts("BUG in _set_log_it_ut_fn : unhandled log format #{@log_format}")
231
+ # :nocov:
232
+ end
233
+ end
234
+
235
+ # --------------------
236
+ ## set max entries to allow in the queue before printing them
237
+ #
238
+ # @param value (int) number of entries; default: 10
239
+ # @return nil
240
+ def set_max_entries(value)
241
+ @runner_cfg.max_entries = value
242
+ raise(ArgumentError, 'max_entries must be greater than 0') if @runner_cfg.max_entries <= 0
243
+ end
244
+
245
+ # --------------------
246
+ ## set loop delay to check the queue
247
+ #
248
+ # @param loop_delay (float) number of seconds; default: 0.250
249
+ # @return nil
250
+ def set_loop_delay(loop_delay)
251
+ @runner_cfg.loop_delay = loop_delay
252
+ raise(ArgumentError, 'loop_delay must be >= 0.001 seconds') if @runner_cfg.loop_delay < 0.001
253
+
254
+ # print every loop_delay seconds even if less than max_entries are in the queue
255
+ @runner_cfg.max_count = (1 / @runner_cfg.loop_delay).round(1).to_i
256
+ end
257
+
258
+ # --------------------
259
+ ## set how many dots to print on one line before printing a newline
260
+ #
261
+ # @param value (int) number of dots
262
+ # @return nil
263
+ def set_max_dots(value)
264
+ @max_dots = value
265
+ raise(ArgumentError, 'max_dots must be greater than 0') unless @max_dots.positive?
266
+ end
267
+
268
+ # === cleanup functions
269
+
270
+ # --------------------
271
+ ## terminate
272
+ # stop the thread, save any remaining line in the internal queue
273
+ #
274
+ # @return nil
275
+ def term
276
+ begin
277
+ # since this will be called during atexit() handling,
278
+ # stdout and/or file can be closed. Protect against this case.
279
+ _save
280
+ rescue Exception
281
+ # nothing to do
282
+ end
283
+
284
+ @finished = true
285
+ @thread.join(5.0) unless @thread.nil? || !@thread.alive?
286
+ end
287
+
288
+ # --------------------
289
+ ## user can request a save at this point
290
+ #
291
+ # @return nil
292
+ attr_reader :save
293
+
294
+ # === log modes
295
+
296
+ # --------------------
297
+ ## since it's normal mode, put the logging info on the queue.
298
+ #
299
+ # @param info the line info
300
+ # @return nil
301
+ def _log_it_normal(info)
302
+ @queue.push(info)
303
+ end
304
+
305
+ # --------------------
306
+ ## since it's ut mode and no prefixes, add the line to ut_lines
307
+ #
308
+ # @param info the line info
309
+ # @return nil
310
+ def _log_it_ut_none(info)
311
+ return unless info[0] || info[1]
312
+
313
+ # always_print or verbose
314
+ if info[3].nil? && info[4] == [nil]
315
+ line = 'DTS'
316
+ elsif info[3] == '.' && info[4] == [nil]
317
+ line = '.'
318
+ else
319
+ line = info[4].join(' ')
320
+ end
321
+ @ut_lines.append(line)
322
+ end
323
+
324
+ # --------------------
325
+ ## since it's ut mode and prefix mode add the line with prefix to ut_lines
326
+ #
327
+ # @param info the line info
328
+ # @return nil
329
+ def _log_it_ut_prefix(info)
330
+ return unless info[0] || info[1]
331
+
332
+ # always_print or verbose
333
+ prefix = info[3]
334
+ if prefix.nil? && info[4] == [nil]
335
+ line = 'DTS'
336
+ prefix = ' '
337
+ @ut_lines.append(format('%-4s %s', prefix, line))
338
+ elsif info[3] == '.' && info[4] == [nil]
339
+ @ut_lines.append('.')
340
+ elsif prefix.nil? # raw line
341
+ line = info[4].join(' ')
342
+ @ut_lines.append(line)
343
+ else
344
+ line = info[4].join(' ')
345
+ @ut_lines.append(format('%-4s %s', prefix, line))
346
+ end
347
+ end
348
+
349
+ # --------------------
350
+ ## since it's ut mode and elapsed mode add the line with elapsed and prefix to ut_lines
351
+ #
352
+ # @param info the line info
353
+ # @return nil
354
+ def _log_it_ut_elapsed(info)
355
+ return unless info[0] || info[1]
356
+
357
+ # always_print or verbose
358
+ dts = info[2]
359
+
360
+ elapsed = Time.at(dts - @start_time)
361
+ if elapsed.to_f >= 3600.0
362
+ @start_time = Time.now
363
+ elapsed = 0.0
364
+ end
365
+ t_str = _get_elapsed_str(elapsed)
366
+
367
+ prefix = info[3]
368
+ if prefix.nil? && info[4] == [nil]
369
+ line = 'DTS'
370
+ prefix = ' '
371
+ @ut_lines.append(format('%-4s %s', prefix, line))
372
+ elsif info[3] == '.' && info[4] == [nil]
373
+ @ut_lines.append('.')
374
+ elsif prefix.nil? # raw line
375
+ line = info[4].join(' ')
376
+ @ut_lines.append(line)
377
+ else
378
+ line = info[4].join(' ')
379
+ @ut_lines.append(format('%s %-4s %s', t_str, prefix, line))
380
+ end
381
+ end
382
+
383
+ # --------------------
384
+ ## since it's null mode, ignore the line
385
+ #
386
+ # @param info (ignored)
387
+ # @return nil
388
+ def _log_it_null(info)
389
+ # logging ignored
390
+ end
391
+
392
+ # === log lines with prefixes and elapsed times
393
+
394
+ # --------------------
395
+ ## add an item to write the full date-time-stamp to the log
396
+ #
397
+ # @return nil
398
+ def full_dts
399
+ # the nil args/line causes the full dts to display
400
+ @log_it.call([false, @verbose, Time.now, nil, [nil]])
401
+ end
402
+
403
+ # -------------------
404
+ ## write a "start" line with the given message
405
+ #
406
+ # @param args the message to log
407
+ # @return nil
408
+ def start(*args)
409
+ @log_it.call([false, @verbose, Time.now, '====', args].freeze)
410
+ end
411
+
412
+ # --------------------
413
+ ## write a line with the given message
414
+ #
415
+ # @param args the message to log
416
+ # @return nil
417
+ def line(*args)
418
+ @log_it.call([false, @verbose, Time.now, '', args].freeze)
419
+ end
420
+
421
+ # -------------------
422
+ ## write a "warn" line with the given message
423
+ #
424
+ # @param args the message to log
425
+ # @return nil
426
+ def highlight(*args)
427
+ @log_it.call([false, @verbose, Time.now, '--->', args].freeze)
428
+ end
429
+
430
+ # -------------------
431
+ ## write a "ok" line with the given message
432
+ #
433
+ # @param args the message to log
434
+ # @return nil
435
+ def ok(*args)
436
+ @log_it.call([false, @verbose, Time.now, 'OK', args].freeze)
437
+ end
438
+
439
+ # -------------------
440
+ ## write an "err" line with the given message
441
+ #
442
+ # @param args the message to log
443
+ # @return nil
444
+ def err(*args)
445
+ @log_it.call([true, @verbose, Time.now, 'ERR', args].freeze)
446
+ end
447
+
448
+ # -------------------
449
+ ## write a "warn" line with the given message
450
+ #
451
+ # @param args the message to log
452
+ # @return nil
453
+ def warn(*args)
454
+ @log_it.call([false, @verbose, Time.now, 'WARN', args].freeze)
455
+ end
456
+
457
+ # -------------------
458
+ ## write a "bug" line with the given message
459
+ #
460
+ # @param args the message to log
461
+ # @return nil
462
+ def bug(*args)
463
+ @log_it.call([true, @verbose, Time.now, 'BUG', args].freeze)
464
+ end
465
+
466
+ # -------------------
467
+ ## write a "dbg" line with the given message
468
+ #
469
+ # @param args the message to log
470
+ # @return nil
471
+ def dbg(*args)
472
+ @log_it.call([false, @verbose, Time.now, 'DBG', args].freeze)
473
+ end
474
+
475
+ # -------------------
476
+ ## write a raw line (no prefix or elapsed)
477
+ #
478
+ # @param args the message to log
479
+ # @return nil
480
+ def raw(*args)
481
+ @log_it.call([false, @verbose, Time.now, nil, args].freeze)
482
+ end
483
+
484
+ # -------------------
485
+ ## write an output line with the given message
486
+ #
487
+ # @param lineno (optional) the current line number for each line printed
488
+ # @param args the message to write
489
+ # @return nil
490
+ def output(lineno, *args)
491
+ if lineno.nil?
492
+ new_args = [' ']
493
+ else
494
+ new_args = [format('%3d]', lineno)]
495
+ end
496
+ new_args += args
497
+ @log_it.call([false, @verbose, Time.now, ' --', new_args].freeze)
498
+ end
499
+
500
+ # -------------------
501
+ ## write a list of lines using output()
502
+ #
503
+ # @param lines the lines to write
504
+ # @return nil
505
+ def num_output(lines)
506
+ lineno = 0
507
+ lines.each do |line|
508
+ lineno += 1
509
+ output(lineno, line)
510
+ end
511
+ end
512
+
513
+ # --------------------
514
+ ## if ok is true, write an OK line, otherwise an ERR line.
515
+ #
516
+ # @param ok condition indicating ok or err
517
+ # @param args the message to log
518
+ # @return nil
519
+ def check(ok, *args)
520
+ if ok
521
+ ok(args)
522
+ else
523
+ err(args)
524
+ end
525
+ end
526
+
527
+ # --------------------
528
+ ## log a series of messages. Use ok() or err() as appropriate.
529
+ #
530
+ # @param ok the check state
531
+ # @param title the line indicating what the check is about
532
+ # @param lines individual list of lines to print
533
+ # @return nil
534
+ def check_all(ok, title, lines)
535
+ check(ok, "#{title}: #{ok}")
536
+ lines.each do |line|
537
+ check(ok, " - #{line}")
538
+ end
539
+ end
540
+
541
+ # -------------------
542
+ ## add an item to write a 'line' message and a json object to the log
543
+ #
544
+ # @param j_data the json object to write
545
+ # @param args the message to write
546
+ # @return nil
547
+ def json(j_data, *args)
548
+ now = Time.now
549
+ @log_it.call([false, @verbose, now, ' ', args].freeze)
550
+
551
+ j_data = JSON.parse(j_data) if j_data.is_a?(String)
552
+ JSON.pretty_generate(j_data).split("\n").each do |line|
553
+ @log_it.call([false, @verbose, now, ' >', [line]].freeze)
554
+ end
555
+ end
556
+
557
+ # -------------------
558
+ ## add an item to write a 'line' message and a data buffer to the log in hex
559
+ #
560
+ # @param data the data buffer to write; can be a string or a bytes array
561
+ # @param args the message to write
562
+ # @return nil
563
+ def hex(data, *args)
564
+ now = Time.now
565
+ @log_it.call([false, @verbose, now, ' ', args].freeze)
566
+ line = format('%3d 0x%02X:', 0, 0)
567
+ data = data.bytes.to_a if data.is_a?(String)
568
+
569
+ # data.each_char
570
+ col = 0
571
+ data.each_with_index do |ch, i|
572
+ if col >= 16
573
+ @log_it.call([false, @verbose, now, '', [' ', line]].freeze)
574
+ col = 0
575
+ line = format('%3d 0x%02X:', i, i)
576
+ end
577
+
578
+ line += format(' %02X', ch)
579
+ col += 1
580
+ line += ' ' if col == 8
581
+ end
582
+
583
+ # print if there's something left over
584
+ @log_it.call([false, @verbose, now, ' ', [' ', line]].freeze)
585
+ end
586
+
587
+ # --------------------
588
+ ## write a dot to stdout
589
+ #
590
+ # @return nil
591
+ def dot
592
+ @log_it.call([false, @verbose, Time.now, '.', [nil]].freeze)
593
+ end
594
+
595
+ # === (some) compatibility with python logger
596
+
597
+ # -------------------
598
+ ## write a "debug" line with the given message
599
+ #
600
+ # @param args the message to log
601
+ # @return nil
602
+ def debug(*args)
603
+ @log_it.call([false, @verbose, Time.now, 'DBG', args].freeze)
604
+ end
605
+
606
+ # --------------------
607
+ ## write a line with the given message
608
+ #
609
+ # @param args the message to log
610
+ # @return nil
611
+ def info(*args)
612
+ @log_it.call([false, @verbose, Time.now, '', args].freeze)
613
+ end
614
+
615
+ # -------------------
616
+ ## write a "warn" line with the given message
617
+ #
618
+ # @param args the message to log
619
+ # @return nil
620
+ def warning(*args)
621
+ @log_it.call([false, @verbose, Time.now, 'WARN', args].freeze)
622
+ end
623
+
624
+ # -------------------
625
+ ## write a "err" line with the given message
626
+ #
627
+ # @param args the message to log
628
+ # @return nil
629
+ def error(*args)
630
+ @log_it.call([true, @verbose, Time.now, 'ERR', args].freeze)
631
+ end
632
+
633
+ # -------------------
634
+ ## write an "err" line with the given message
635
+ #
636
+ # @param args the message to log
637
+ # @return nil
638
+ def critical(*args)
639
+ @log_it.call([true, @verbose, Time.now, 'CRIT', args].freeze)
640
+ end
641
+
642
+ # --------------------
643
+ ## log an exception
644
+ #
645
+ # @param excp the exception to print
646
+ # @param max_lines the maximum lines to print from the backtrace
647
+ # @return nil
648
+ def exception(excp, max_lines: 3)
649
+ now = Time.now
650
+
651
+ @log_it.call([true, @verbose, now, 'EXCP', [excp.message]].freeze)
652
+ @log_it.call([true, @verbose, now, 'EXCP', ['Backtrace:']].freeze)
653
+ count = 1
654
+ excp.backtrace.each do |line|
655
+ @log_it.call([true, @verbose, now, 'EXCP', [' ', line]].freeze)
656
+ count += 1
657
+ # :nocov:
658
+ # coverage: cannot be done in a UT; minitest adds an unknown number of excp lines
659
+ break if count > max_lines
660
+ # :nocov:
661
+ end
662
+ end
663
+
664
+ private
665
+
666
+ # === logging functions
667
+
668
+ # -------------------
669
+ ## write the given line to stdout
670
+ #
671
+ # @return nil
672
+ def _save
673
+ # if stdout or file is none/closed then nothing to do
674
+ # if @fp.nil?: # pragma: no cover
675
+ # # coverage: can not be replicated
676
+ # # probably redundant to finish but do it anyway
677
+ # @finished = true
678
+ # return
679
+
680
+ count = @queue.size
681
+ while count.positive?
682
+ # in some closing/race conditions, the file may be closed in the middle of a loop
683
+ # note this can be applied to stdout as well.
684
+ # :nocov:
685
+ # coverage: cannot be done in a UT
686
+ break if @fp.closed?
687
+ # :nocov:
688
+
689
+ # get the next tuple: (always_print, verbose, dts, prefix, args)
690
+ tuple = @queue.pop
691
+ always_print = tuple[0]
692
+ verbose = tuple[1]
693
+ dts = tuple[2]
694
+ prefix = tuple[3]
695
+ args = tuple[4]
696
+ count -= 1
697
+
698
+ # uncomment to debug
699
+ # puts("#{always_print} #{verbose} '#{prefix}' #{dts} '#{args}'")
700
+
701
+ # not verbose and ok not to print
702
+ next unless verbose || always_print
703
+
704
+ if args[0].nil? && prefix == '.'
705
+ # dots used to log waiting periods, so okay to call fn
706
+ _handle_dots
707
+ next
708
+ end
709
+
710
+ # at this point, not a dot
711
+
712
+ # last call was a dot, so reset and print a newline ready for the new log line
713
+ if @dots != 0
714
+ @dots = 0
715
+ @fp.write("\n")
716
+ @runner_cfg.copy_from(@backup_cfg)
717
+ end
718
+
719
+ # print the full DTS requested by the user
720
+ if args[0].nil? && prefix.nil?
721
+ # rare request, so okay to call fn
722
+ _handle_full_dts(dts)
723
+ next
724
+ end
725
+
726
+ # print with no prefix or elapsed time
727
+ if @log_format == LogFormat::NONE
728
+ line = args.join(' ')
729
+ @fp.write(line)
730
+ @fp.write("\n")
731
+ next
732
+ end
733
+
734
+ # print with the prefix, but no elapsed time
735
+ if @log_format == LogFormat::PREFIX
736
+ line = args.join(' ')
737
+ if prefix.nil?
738
+ msg = line # raw line
739
+ else
740
+ msg = format('%-4s %s', prefix, line)
741
+ end
742
+ @fp.write(msg)
743
+ @fp.write("\n")
744
+ next
745
+ end
746
+
747
+ # at this point, mode is LogFormat.ELAPSED
748
+
749
+ # approximately once an hour, restart the time period
750
+ elapsed = Time.at(dts - @start_time)
751
+ if elapsed.to_f > 3600.0
752
+ # rare, so okay to call fn
753
+ # display the time at the moment the log line was saved
754
+ _handle_full_dts(dts)
755
+
756
+ # recalc the elapsed time, should be 0
757
+ # add the small value for round-off issues
758
+ elapsed = Time.at(dts - @start_time) # + 0.0001)
759
+ end
760
+
761
+ # print prefix and elapsed time
762
+ line = args.join(' ')
763
+
764
+ # log the line
765
+ if prefix.nil?
766
+ msg = line
767
+ else
768
+ t_str = _get_elapsed_str(elapsed)
769
+ msg = format('%s %-4s %s', t_str, prefix, line)
770
+ end
771
+
772
+ # assume @fp is not closed
773
+ @fp.write(msg)
774
+ @fp.write("\n")
775
+ end
776
+
777
+ # uncomment to debug
778
+ # @fp.close
779
+
780
+ # flush lines to stdout/file; protect with except in case
781
+ begin
782
+ @fp.flush
783
+ rescue IOError
784
+ # coverage: rare case: if stdout/file is closed this will throw an exception
785
+ end
786
+ end
787
+
788
+ # --------------------
789
+ ## If past max_dots, print a newline. Then print a dot.
790
+ #
791
+ # @return none
792
+ def _handle_dots
793
+ if @dots.zero?
794
+ # save delay and count
795
+ @backup_cfg.copy_from(@runner_cfg)
796
+ @runner_cfg.loop_delay = 0.100
797
+ @runner_cfg.max_entries = 1
798
+ @runner_cfg.max_count = 1
799
+ end
800
+
801
+ if @dots >= @max_dots
802
+ @fp.write("\n")
803
+ @dots = 0
804
+ end
805
+
806
+ @fp.write('.')
807
+ @dots += 1
808
+ end
809
+
810
+ # --------------------
811
+ ## print full DTS stamp
812
+ #
813
+ # @param dts the dts of the current log line
814
+ # @return nil
815
+ def _handle_full_dts(dts)
816
+ # restart the timer; user wants the full DTS and elapsed is since that absolute time
817
+ @start_time = dts
818
+
819
+ full_dts = format('%-4s %s', 'DTS', dts.strftime('%4Y/%2m/%2d %2H:%2M:%2S.%3N'))
820
+ full_dts = format('%9s %s', '', full_dts) if @log_format == LogFormat::ELAPSED
821
+ @fp.write(full_dts)
822
+ @fp.write("\n")
823
+ end
824
+
825
+ # --------------------
826
+ ## generate the string of the given elapsed time
827
+ #
828
+ # @param elapsed the elapsed time to format
829
+ # @return the string ("MM.SS.nnn")
830
+ def _get_elapsed_str(elapsed)
831
+ elapsed = Time.at(elapsed) if elapsed.is_a?(Float)
832
+ elapsed.strftime('%2M:%2S.%3N')
833
+ end
834
+
835
+ # --------------------
836
+ ## the thread runner
837
+ # wakes periodically to check if the queue has max_entries or more in it
838
+ # if so, the lines are written to the file
839
+ # if not, it sleeps
840
+ #
841
+ # @return nil
842
+ def _runner
843
+ count = 0
844
+ until @finished
845
+ # sleep until:
846
+ # - there are enough entries in the queue
847
+ # - the max delay is reached
848
+ if @queue.size < @runner_cfg.max_entries && count < @runner_cfg.max_count
849
+ count += 1
850
+ sleep(@runner_cfg.loop_delay)
851
+ next
852
+ end
853
+
854
+ # write out all the current entries
855
+ count = 0
856
+ _save
857
+ end
858
+ rescue Interrupt # handle ctrl-c
859
+ # nothing to do
860
+ ensure
861
+ # save any remaining entries
862
+ _save
863
+
864
+ # close the file if necessary
865
+ if @path && !@fp.nil?
866
+ @fp.close
867
+ @fp = nil
868
+ end
869
+ end
870
+ end
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'gem_source/logger'
data/lib/version.rb ADDED
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+ ## current version
3
+ VERSION = '0.1.0'
metadata ADDED
@@ -0,0 +1,49 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: rb_falcon_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - J. Arrizza
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2025-06-01 00:00:00.000000000 Z
12
+ dependencies: []
13
+ description: fast logger with multiple modes and formats
14
+ email: cppgent0@gmail.com
15
+ executables: []
16
+ extensions: []
17
+ extra_rdoc_files: []
18
+ files:
19
+ - LICENSE.txt
20
+ - README.md
21
+ - lib/gem_source/logger.rb
22
+ - lib/rb_falcon_logger.rb
23
+ - lib/version.rb
24
+ homepage: https://rubygems.org/gems/rb-falcon-logger
25
+ licenses:
26
+ - MIT
27
+ metadata:
28
+ homepage_uri: https://arrizza.com/rb-falcon-logger
29
+ source_code_uri: https://bitbucket.org/arrizza-public/rb-falcon-logger/src/master
30
+ post_install_message:
31
+ rdoc_options: []
32
+ require_paths:
33
+ - lib
34
+ required_ruby_version: !ruby/object:Gem::Requirement
35
+ requirements:
36
+ - - ">="
37
+ - !ruby/object:Gem::Version
38
+ version: 3.2.0
39
+ required_rubygems_version: !ruby/object:Gem::Requirement
40
+ requirements:
41
+ - - ">="
42
+ - !ruby/object:Gem::Version
43
+ version: '0'
44
+ requirements: []
45
+ rubygems_version: 3.4.20
46
+ signing_key:
47
+ specification_version: 4
48
+ summary: fast logger, several modes and formats
49
+ test_files: []