logger 1.3.0 → 1.5.1
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 +4 -4
- data/lib/logger/errors.rb +9 -0
- data/lib/logger/formatter.rb +37 -0
- data/lib/logger/log_device.rb +205 -0
- data/lib/logger/period.rb +47 -0
- data/lib/logger/severity.rb +19 -0
- data/lib/logger/version.rb +5 -0
- data/lib/logger.rb +52 -316
- data/logger.gemspec +13 -13
- metadata +44 -30
- data/.gitignore +0 -8
- data/.travis.yml +0 -6
- data/Gemfile +0 -6
- data/LICENSE.txt +0 -22
- data/README.md +0 -39
- data/Rakefile +0 -10
- data/bin/console +0 -14
- data/bin/setup +0 -8
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 55dfc9db8783f8e0bce997c8f6c162b10e4602bcbe968354bb8f4a0f3a6c1764
|
4
|
+
data.tar.gz: '08e336951520fc4cc5ab2553ec563509a769a20fb461f47d4e0f2b561745783e'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 001ea8a77429868cfe3e438b41a1d100ee0d5c1bba2d74685fd031add97efae2bb0f31b4d889a84a954f01dfcc96624512e488bb08da4703e33bc38432c3e606
|
7
|
+
data.tar.gz: 747d62f6bf313cb3a120afcae4179feb47bbe816d40895793ccc719ca87bb4c1798c0cb72ac39862dbcbacc6468e59606d2d5600e395607562f0c0a58bbf3f6f
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Logger
|
4
|
+
# Default formatter for log messages.
|
5
|
+
class Formatter
|
6
|
+
Format = "%s, [%s #%d] %5s -- %s: %s\n"
|
7
|
+
DatetimeFormat = "%Y-%m-%dT%H:%M:%S.%6N"
|
8
|
+
|
9
|
+
attr_accessor :datetime_format
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
@datetime_format = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def call(severity, time, progname, msg)
|
16
|
+
Format % [severity[0..0], format_datetime(time), Process.pid, severity, progname,
|
17
|
+
msg2str(msg)]
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def format_datetime(time)
|
23
|
+
time.strftime(@datetime_format || DatetimeFormat)
|
24
|
+
end
|
25
|
+
|
26
|
+
def msg2str(msg)
|
27
|
+
case msg
|
28
|
+
when ::String
|
29
|
+
msg
|
30
|
+
when ::Exception
|
31
|
+
"#{ msg.message } (#{ msg.class })\n#{ msg.backtrace.join("\n") if msg.backtrace }"
|
32
|
+
else
|
33
|
+
msg.inspect
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,205 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative 'period'
|
4
|
+
|
5
|
+
class Logger
|
6
|
+
# Device used for logging messages.
|
7
|
+
class LogDevice
|
8
|
+
include Period
|
9
|
+
|
10
|
+
attr_reader :dev
|
11
|
+
attr_reader :filename
|
12
|
+
include MonitorMixin
|
13
|
+
|
14
|
+
def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false)
|
15
|
+
@dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
|
16
|
+
@binmode = binmode
|
17
|
+
mon_initialize
|
18
|
+
set_dev(log)
|
19
|
+
if @filename
|
20
|
+
@shift_age = shift_age || 7
|
21
|
+
@shift_size = shift_size || 1048576
|
22
|
+
@shift_period_suffix = shift_period_suffix || '%Y%m%d'
|
23
|
+
|
24
|
+
unless @shift_age.is_a?(Integer)
|
25
|
+
base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
|
26
|
+
@next_rotate_time = next_rotate_time(base_time, @shift_age)
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def write(message)
|
32
|
+
begin
|
33
|
+
synchronize do
|
34
|
+
if @shift_age and @dev.respond_to?(:stat)
|
35
|
+
begin
|
36
|
+
check_shift_log
|
37
|
+
rescue
|
38
|
+
warn("log shifting failed. #{$!}")
|
39
|
+
end
|
40
|
+
end
|
41
|
+
begin
|
42
|
+
@dev.write(message)
|
43
|
+
rescue
|
44
|
+
warn("log writing failed. #{$!}")
|
45
|
+
end
|
46
|
+
end
|
47
|
+
rescue Exception => ignored
|
48
|
+
warn("log writing failed. #{ignored}")
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def close
|
53
|
+
begin
|
54
|
+
synchronize do
|
55
|
+
@dev.close rescue nil
|
56
|
+
end
|
57
|
+
rescue Exception
|
58
|
+
@dev.close rescue nil
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
def reopen(log = nil)
|
63
|
+
# reopen the same filename if no argument, do nothing for IO
|
64
|
+
log ||= @filename if @filename
|
65
|
+
if log
|
66
|
+
synchronize do
|
67
|
+
if @filename and @dev
|
68
|
+
@dev.close rescue nil # close only file opened by Logger
|
69
|
+
@filename = nil
|
70
|
+
end
|
71
|
+
set_dev(log)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
self
|
75
|
+
end
|
76
|
+
|
77
|
+
private
|
78
|
+
|
79
|
+
def set_dev(log)
|
80
|
+
if log.respond_to?(:write) and log.respond_to?(:close)
|
81
|
+
@dev = log
|
82
|
+
if log.respond_to?(:path)
|
83
|
+
@filename = log.path
|
84
|
+
end
|
85
|
+
else
|
86
|
+
@dev = open_logfile(log)
|
87
|
+
@dev.sync = true
|
88
|
+
@dev.binmode if @binmode
|
89
|
+
@filename = log
|
90
|
+
end
|
91
|
+
end
|
92
|
+
|
93
|
+
def open_logfile(filename)
|
94
|
+
begin
|
95
|
+
File.open(filename, (File::WRONLY | File::APPEND))
|
96
|
+
rescue Errno::ENOENT
|
97
|
+
create_logfile(filename)
|
98
|
+
end
|
99
|
+
end
|
100
|
+
|
101
|
+
def create_logfile(filename)
|
102
|
+
begin
|
103
|
+
logdev = File.open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
|
104
|
+
logdev.flock(File::LOCK_EX)
|
105
|
+
logdev.sync = true
|
106
|
+
logdev.binmode if @binmode
|
107
|
+
add_log_header(logdev)
|
108
|
+
logdev.flock(File::LOCK_UN)
|
109
|
+
rescue Errno::EEXIST
|
110
|
+
# file is created by another process
|
111
|
+
logdev = open_logfile(filename)
|
112
|
+
logdev.sync = true
|
113
|
+
end
|
114
|
+
logdev
|
115
|
+
end
|
116
|
+
|
117
|
+
def add_log_header(file)
|
118
|
+
file.write(
|
119
|
+
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
|
120
|
+
) if file.size == 0
|
121
|
+
end
|
122
|
+
|
123
|
+
def check_shift_log
|
124
|
+
if @shift_age.is_a?(Integer)
|
125
|
+
# Note: always returns false if '0'.
|
126
|
+
if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
|
127
|
+
lock_shift_log { shift_log_age }
|
128
|
+
end
|
129
|
+
else
|
130
|
+
now = Time.now
|
131
|
+
if now >= @next_rotate_time
|
132
|
+
@next_rotate_time = next_rotate_time(now, @shift_age)
|
133
|
+
lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
|
134
|
+
end
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
if /mswin|mingw|cygwin/ =~ RbConfig::CONFIG['host_os']
|
139
|
+
def lock_shift_log
|
140
|
+
yield
|
141
|
+
end
|
142
|
+
else
|
143
|
+
def lock_shift_log
|
144
|
+
retry_limit = 8
|
145
|
+
retry_sleep = 0.1
|
146
|
+
begin
|
147
|
+
File.open(@filename, File::WRONLY | File::APPEND) do |lock|
|
148
|
+
lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
|
149
|
+
if File.identical?(@filename, lock) and File.identical?(lock, @dev)
|
150
|
+
yield # log shifting
|
151
|
+
else
|
152
|
+
# log shifted by another process (i-node before locking and i-node after locking are different)
|
153
|
+
@dev.close rescue nil
|
154
|
+
@dev = open_logfile(@filename)
|
155
|
+
@dev.sync = true
|
156
|
+
end
|
157
|
+
end
|
158
|
+
rescue Errno::ENOENT
|
159
|
+
# @filename file would not exist right after #rename and before #create_logfile
|
160
|
+
if retry_limit <= 0
|
161
|
+
warn("log rotation inter-process lock failed. #{$!}")
|
162
|
+
else
|
163
|
+
sleep retry_sleep
|
164
|
+
retry_limit -= 1
|
165
|
+
retry_sleep *= 2
|
166
|
+
retry
|
167
|
+
end
|
168
|
+
end
|
169
|
+
rescue
|
170
|
+
warn("log rotation inter-process lock failed. #{$!}")
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def shift_log_age
|
175
|
+
(@shift_age-3).downto(0) do |i|
|
176
|
+
if FileTest.exist?("#{@filename}.#{i}")
|
177
|
+
File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
|
178
|
+
end
|
179
|
+
end
|
180
|
+
@dev.close rescue nil
|
181
|
+
File.rename("#{@filename}", "#{@filename}.0")
|
182
|
+
@dev = create_logfile(@filename)
|
183
|
+
return true
|
184
|
+
end
|
185
|
+
|
186
|
+
def shift_log_period(period_end)
|
187
|
+
suffix = period_end.strftime(@shift_period_suffix)
|
188
|
+
age_file = "#{@filename}.#{suffix}"
|
189
|
+
if FileTest.exist?(age_file)
|
190
|
+
# try to avoid filename crash caused by Timestamp change.
|
191
|
+
idx = 0
|
192
|
+
# .99 can be overridden; avoid too much file search with 'loop do'
|
193
|
+
while idx < 100
|
194
|
+
idx += 1
|
195
|
+
age_file = "#{@filename}.#{suffix}.#{idx}"
|
196
|
+
break unless FileTest.exist?(age_file)
|
197
|
+
end
|
198
|
+
end
|
199
|
+
@dev.close rescue nil
|
200
|
+
File.rename("#{@filename}", age_file)
|
201
|
+
@dev = create_logfile(@filename)
|
202
|
+
return true
|
203
|
+
end
|
204
|
+
end
|
205
|
+
end
|
@@ -0,0 +1,47 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Logger
|
4
|
+
module Period
|
5
|
+
module_function
|
6
|
+
|
7
|
+
SiD = 24 * 60 * 60
|
8
|
+
|
9
|
+
def next_rotate_time(now, shift_age)
|
10
|
+
case shift_age
|
11
|
+
when 'daily'
|
12
|
+
t = Time.mktime(now.year, now.month, now.mday) + SiD
|
13
|
+
when 'weekly'
|
14
|
+
t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
|
15
|
+
when 'monthly'
|
16
|
+
t = Time.mktime(now.year, now.month, 1) + SiD * 32
|
17
|
+
return Time.mktime(t.year, t.month, 1)
|
18
|
+
when 'now', 'everytime'
|
19
|
+
return now
|
20
|
+
else
|
21
|
+
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
|
22
|
+
end
|
23
|
+
if t.hour.nonzero? or t.min.nonzero? or t.sec.nonzero?
|
24
|
+
hour = t.hour
|
25
|
+
t = Time.mktime(t.year, t.month, t.mday)
|
26
|
+
t += SiD if hour > 12
|
27
|
+
end
|
28
|
+
t
|
29
|
+
end
|
30
|
+
|
31
|
+
def previous_period_end(now, shift_age)
|
32
|
+
case shift_age
|
33
|
+
when 'daily'
|
34
|
+
t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
|
35
|
+
when 'weekly'
|
36
|
+
t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
|
37
|
+
when 'monthly'
|
38
|
+
t = Time.mktime(now.year, now.month, 1) - SiD / 2
|
39
|
+
when 'now', 'everytime'
|
40
|
+
return now
|
41
|
+
else
|
42
|
+
raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
|
43
|
+
end
|
44
|
+
Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Logger
|
4
|
+
# Logging severity.
|
5
|
+
module Severity
|
6
|
+
# Low-level information, mostly for developers.
|
7
|
+
DEBUG = 0
|
8
|
+
# Generic (useful) information about system operation.
|
9
|
+
INFO = 1
|
10
|
+
# A warning.
|
11
|
+
WARN = 2
|
12
|
+
# A handleable error condition.
|
13
|
+
ERROR = 3
|
14
|
+
# An unhandleable error that results in a program crash.
|
15
|
+
FATAL = 4
|
16
|
+
# An unknown message that should always be logged.
|
17
|
+
UNKNOWN = 5
|
18
|
+
end
|
19
|
+
end
|
data/lib/logger.rb
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
# frozen_string_literal:
|
1
|
+
# frozen_string_literal: true
|
2
2
|
# logger.rb - simple logging utility
|
3
3
|
# Copyright (C) 2000-2003, 2005, 2008, 2011 NAKAMURA, Hiroshi <nahi@ruby-lang.org>.
|
4
4
|
#
|
@@ -11,6 +11,13 @@
|
|
11
11
|
# A simple system for logging messages. See Logger for more documentation.
|
12
12
|
|
13
13
|
require 'monitor'
|
14
|
+
require 'rbconfig'
|
15
|
+
|
16
|
+
require_relative 'logger/version'
|
17
|
+
require_relative 'logger/formatter'
|
18
|
+
require_relative 'logger/log_device'
|
19
|
+
require_relative 'logger/severity'
|
20
|
+
require_relative 'logger/errors'
|
14
21
|
|
15
22
|
# == Description
|
16
23
|
#
|
@@ -224,7 +231,6 @@ require 'monitor'
|
|
224
231
|
# })
|
225
232
|
#
|
226
233
|
class Logger
|
227
|
-
VERSION = "1.3.0"
|
228
234
|
_, name, rev = %w$Id$
|
229
235
|
if name
|
230
236
|
name = name.chomp(",v")
|
@@ -232,29 +238,8 @@ class Logger
|
|
232
238
|
name = File.basename(__FILE__)
|
233
239
|
end
|
234
240
|
rev ||= "v#{VERSION}"
|
235
|
-
ProgName = "#{name}/#{rev}"
|
236
|
-
|
237
|
-
class Error < RuntimeError # :nodoc:
|
238
|
-
end
|
239
|
-
# not used after 1.2.7. just for compat.
|
240
|
-
class ShiftingError < Error # :nodoc:
|
241
|
-
end
|
241
|
+
ProgName = "#{name}/#{rev}"
|
242
242
|
|
243
|
-
# Logging severity.
|
244
|
-
module Severity
|
245
|
-
# Low-level information, mostly for developers.
|
246
|
-
DEBUG = 0
|
247
|
-
# Generic (useful) information about system operation.
|
248
|
-
INFO = 1
|
249
|
-
# A warning.
|
250
|
-
WARN = 2
|
251
|
-
# A handleable error condition.
|
252
|
-
ERROR = 3
|
253
|
-
# An unhandleable error that results in a program crash.
|
254
|
-
FATAL = 4
|
255
|
-
# An unknown message that should always be logged.
|
256
|
-
UNKNOWN = 5
|
257
|
-
end
|
258
243
|
include Severity
|
259
244
|
|
260
245
|
# Logging severity threshold (e.g. <tt>Logger::INFO</tt>).
|
@@ -318,25 +303,40 @@ class Logger
|
|
318
303
|
alias sev_threshold level
|
319
304
|
alias sev_threshold= level=
|
320
305
|
|
321
|
-
# Returns +true+
|
306
|
+
# Returns +true+ if and only if the current severity level allows for the printing of
|
322
307
|
# +DEBUG+ messages.
|
323
|
-
def debug?;
|
308
|
+
def debug?; level <= DEBUG; end
|
309
|
+
|
310
|
+
# Sets the severity to DEBUG.
|
311
|
+
def debug!; self.level = DEBUG; end
|
324
312
|
|
325
|
-
# Returns +true+
|
313
|
+
# Returns +true+ if and only if the current severity level allows for the printing of
|
326
314
|
# +INFO+ messages.
|
327
|
-
def info?;
|
315
|
+
def info?; level <= INFO; end
|
328
316
|
|
329
|
-
#
|
317
|
+
# Sets the severity to INFO.
|
318
|
+
def info!; self.level = INFO; end
|
319
|
+
|
320
|
+
# Returns +true+ if and only if the current severity level allows for the printing of
|
330
321
|
# +WARN+ messages.
|
331
|
-
def warn?;
|
322
|
+
def warn?; level <= WARN; end
|
323
|
+
|
324
|
+
# Sets the severity to WARN.
|
325
|
+
def warn!; self.level = WARN; end
|
332
326
|
|
333
|
-
# Returns +true+
|
327
|
+
# Returns +true+ if and only if the current severity level allows for the printing of
|
334
328
|
# +ERROR+ messages.
|
335
|
-
def error?;
|
329
|
+
def error?; level <= ERROR; end
|
330
|
+
|
331
|
+
# Sets the severity to ERROR.
|
332
|
+
def error!; self.level = ERROR; end
|
336
333
|
|
337
|
-
# Returns +true+
|
334
|
+
# Returns +true+ if and only if the current severity level allows for the printing of
|
338
335
|
# +FATAL+ messages.
|
339
|
-
def fatal?;
|
336
|
+
def fatal?; level <= FATAL; end
|
337
|
+
|
338
|
+
# Sets the severity to FATAL.
|
339
|
+
def fatal!; self.level = FATAL; end
|
340
340
|
|
341
341
|
#
|
342
342
|
# :call-seq:
|
@@ -350,14 +350,16 @@ class Logger
|
|
350
350
|
# === Args
|
351
351
|
#
|
352
352
|
# +logdev+::
|
353
|
-
# The log device. This is a filename (String)
|
354
|
-
# +STDOUT+, +STDERR+, or an open file)
|
353
|
+
# The log device. This is a filename (String), IO object (typically
|
354
|
+
# +STDOUT+, +STDERR+, or an open file), +nil+ (it writes nothing) or
|
355
|
+
# +File::NULL+ (same as +nil+).
|
355
356
|
# +shift_age+::
|
356
357
|
# Number of old log files to keep, *or* frequency of rotation (+daily+,
|
357
|
-
# +weekly+ or +monthly+). Default value is 0
|
358
|
+
# +weekly+ or +monthly+). Default value is 0, which disables log file
|
359
|
+
# rotation.
|
358
360
|
# +shift_size+::
|
359
|
-
# Maximum logfile size in bytes (only applies when +shift_age+ is a
|
360
|
-
# Defaults to +1048576+ (1MB).
|
361
|
+
# Maximum logfile size in bytes (only applies when +shift_age+ is a positive
|
362
|
+
# Integer). Defaults to +1048576+ (1MB).
|
361
363
|
# +level+::
|
362
364
|
# Logging severity threshold. Default values is Logger::DEBUG.
|
363
365
|
# +progname+::
|
@@ -366,6 +368,8 @@ class Logger
|
|
366
368
|
# Logging formatter. Default values is an instance of Logger::Formatter.
|
367
369
|
# +datetime_format+::
|
368
370
|
# Date and time format. Default value is '%Y-%m-%d %H:%M:%S'.
|
371
|
+
# +binmode+::
|
372
|
+
# Use binary mode on the log device. Default value is false.
|
369
373
|
# +shift_period_suffix+::
|
370
374
|
# The log file suffix format for +daily+, +weekly+ or +monthly+ rotation.
|
371
375
|
# Default is '%Y%m%d'.
|
@@ -376,17 +380,18 @@ class Logger
|
|
376
380
|
#
|
377
381
|
def initialize(logdev, shift_age = 0, shift_size = 1048576, level: DEBUG,
|
378
382
|
progname: nil, formatter: nil, datetime_format: nil,
|
379
|
-
shift_period_suffix: '%Y%m%d')
|
383
|
+
binmode: false, shift_period_suffix: '%Y%m%d')
|
380
384
|
self.level = level
|
381
385
|
self.progname = progname
|
382
386
|
@default_formatter = Formatter.new
|
383
387
|
self.datetime_format = datetime_format
|
384
388
|
self.formatter = formatter
|
385
389
|
@logdev = nil
|
386
|
-
if logdev
|
387
|
-
@logdev = LogDevice.new(logdev, :
|
388
|
-
:
|
389
|
-
:
|
390
|
+
if logdev && logdev != File::NULL
|
391
|
+
@logdev = LogDevice.new(logdev, shift_age: shift_age,
|
392
|
+
shift_size: shift_size,
|
393
|
+
shift_period_suffix: shift_period_suffix,
|
394
|
+
binmode: binmode)
|
390
395
|
end
|
391
396
|
end
|
392
397
|
|
@@ -407,7 +412,7 @@ class Logger
|
|
407
412
|
# Reopen a log device.
|
408
413
|
#
|
409
414
|
def reopen(logdev = nil)
|
410
|
-
@logdev
|
415
|
+
@logdev&.reopen(logdev)
|
411
416
|
self
|
412
417
|
end
|
413
418
|
|
@@ -454,7 +459,7 @@ class Logger
|
|
454
459
|
#
|
455
460
|
def add(severity, message = nil, progname = nil)
|
456
461
|
severity ||= UNKNOWN
|
457
|
-
if @logdev.nil? or severity <
|
462
|
+
if @logdev.nil? or severity < level
|
458
463
|
return true
|
459
464
|
end
|
460
465
|
if progname.nil?
|
@@ -572,7 +577,7 @@ class Logger
|
|
572
577
|
private
|
573
578
|
|
574
579
|
# Severity label for logging (max 5 chars).
|
575
|
-
SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY).
|
580
|
+
SEV_LABEL = %w(DEBUG INFO WARN ERROR FATAL ANY).freeze
|
576
581
|
|
577
582
|
def format_severity(severity)
|
578
583
|
SEV_LABEL[severity] || 'ANY'
|
@@ -581,273 +586,4 @@ private
|
|
581
586
|
def format_message(severity, datetime, progname, msg)
|
582
587
|
(@formatter || @default_formatter).call(severity, datetime, progname, msg)
|
583
588
|
end
|
584
|
-
|
585
|
-
|
586
|
-
# Default formatter for log messages.
|
587
|
-
class Formatter
|
588
|
-
Format = "%s, [%s#%d] %5s -- %s: %s\n".freeze
|
589
|
-
|
590
|
-
attr_accessor :datetime_format
|
591
|
-
|
592
|
-
def initialize
|
593
|
-
@datetime_format = nil
|
594
|
-
end
|
595
|
-
|
596
|
-
def call(severity, time, progname, msg)
|
597
|
-
Format % [severity[0..0], format_datetime(time), $$, severity, progname,
|
598
|
-
msg2str(msg)]
|
599
|
-
end
|
600
|
-
|
601
|
-
private
|
602
|
-
|
603
|
-
def format_datetime(time)
|
604
|
-
time.strftime(@datetime_format || "%Y-%m-%dT%H:%M:%S.%6N ".freeze)
|
605
|
-
end
|
606
|
-
|
607
|
-
def msg2str(msg)
|
608
|
-
case msg
|
609
|
-
when ::String
|
610
|
-
msg
|
611
|
-
when ::Exception
|
612
|
-
"#{ msg.message } (#{ msg.class })\n" <<
|
613
|
-
(msg.backtrace || []).join("\n")
|
614
|
-
else
|
615
|
-
msg.inspect
|
616
|
-
end
|
617
|
-
end
|
618
|
-
end
|
619
|
-
|
620
|
-
module Period
|
621
|
-
module_function
|
622
|
-
|
623
|
-
SiD = 24 * 60 * 60
|
624
|
-
|
625
|
-
def next_rotate_time(now, shift_age)
|
626
|
-
case shift_age
|
627
|
-
when 'daily'
|
628
|
-
t = Time.mktime(now.year, now.month, now.mday) + SiD
|
629
|
-
when 'weekly'
|
630
|
-
t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
|
631
|
-
when 'monthly'
|
632
|
-
t = Time.mktime(now.year, now.month, 1) + SiD * 32
|
633
|
-
return Time.mktime(t.year, t.month, 1)
|
634
|
-
else
|
635
|
-
return now
|
636
|
-
end
|
637
|
-
if t.hour.nonzero? or t.min.nonzero? or t.sec.nonzero?
|
638
|
-
hour = t.hour
|
639
|
-
t = Time.mktime(t.year, t.month, t.mday)
|
640
|
-
t += SiD if hour > 12
|
641
|
-
end
|
642
|
-
t
|
643
|
-
end
|
644
|
-
|
645
|
-
def previous_period_end(now, shift_age)
|
646
|
-
case shift_age
|
647
|
-
when 'daily'
|
648
|
-
t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
|
649
|
-
when 'weekly'
|
650
|
-
t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
|
651
|
-
when 'monthly'
|
652
|
-
t = Time.mktime(now.year, now.month, 1) - SiD / 2
|
653
|
-
else
|
654
|
-
return now
|
655
|
-
end
|
656
|
-
Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
|
657
|
-
end
|
658
|
-
end
|
659
|
-
|
660
|
-
# Device used for logging messages.
|
661
|
-
class LogDevice
|
662
|
-
include Period
|
663
|
-
|
664
|
-
attr_reader :dev
|
665
|
-
attr_reader :filename
|
666
|
-
include MonitorMixin
|
667
|
-
|
668
|
-
def initialize(log = nil, shift_age: nil, shift_size: nil, shift_period_suffix: nil)
|
669
|
-
@dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
|
670
|
-
mon_initialize
|
671
|
-
set_dev(log)
|
672
|
-
if @filename
|
673
|
-
@shift_age = shift_age || 7
|
674
|
-
@shift_size = shift_size || 1048576
|
675
|
-
@shift_period_suffix = shift_period_suffix || '%Y%m%d'
|
676
|
-
|
677
|
-
unless @shift_age.is_a?(Integer)
|
678
|
-
base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
|
679
|
-
@next_rotate_time = next_rotate_time(base_time, @shift_age)
|
680
|
-
end
|
681
|
-
end
|
682
|
-
end
|
683
|
-
|
684
|
-
def write(message)
|
685
|
-
begin
|
686
|
-
synchronize do
|
687
|
-
if @shift_age and @dev.respond_to?(:stat)
|
688
|
-
begin
|
689
|
-
check_shift_log
|
690
|
-
rescue
|
691
|
-
warn("log shifting failed. #{$!}")
|
692
|
-
end
|
693
|
-
end
|
694
|
-
begin
|
695
|
-
@dev.write(message)
|
696
|
-
rescue
|
697
|
-
warn("log writing failed. #{$!}")
|
698
|
-
end
|
699
|
-
end
|
700
|
-
rescue Exception => ignored
|
701
|
-
warn("log writing failed. #{ignored}")
|
702
|
-
end
|
703
|
-
end
|
704
|
-
|
705
|
-
def close
|
706
|
-
begin
|
707
|
-
synchronize do
|
708
|
-
@dev.close rescue nil
|
709
|
-
end
|
710
|
-
rescue Exception
|
711
|
-
@dev.close rescue nil
|
712
|
-
end
|
713
|
-
end
|
714
|
-
|
715
|
-
def reopen(log = nil)
|
716
|
-
# reopen the same filename if no argument, do nothing for IO
|
717
|
-
log ||= @filename if @filename
|
718
|
-
if log
|
719
|
-
synchronize do
|
720
|
-
if @filename and @dev
|
721
|
-
@dev.close rescue nil # close only file opened by Logger
|
722
|
-
@filename = nil
|
723
|
-
end
|
724
|
-
set_dev(log)
|
725
|
-
end
|
726
|
-
end
|
727
|
-
self
|
728
|
-
end
|
729
|
-
|
730
|
-
private
|
731
|
-
|
732
|
-
def set_dev(log)
|
733
|
-
if log.respond_to?(:write) and log.respond_to?(:close)
|
734
|
-
@dev = log
|
735
|
-
else
|
736
|
-
@dev = open_logfile(log)
|
737
|
-
@dev.sync = true
|
738
|
-
@filename = log
|
739
|
-
end
|
740
|
-
end
|
741
|
-
|
742
|
-
def open_logfile(filename)
|
743
|
-
begin
|
744
|
-
File.open(filename, (File::WRONLY | File::APPEND))
|
745
|
-
rescue Errno::ENOENT
|
746
|
-
create_logfile(filename)
|
747
|
-
end
|
748
|
-
end
|
749
|
-
|
750
|
-
def create_logfile(filename)
|
751
|
-
begin
|
752
|
-
logdev = File.open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
|
753
|
-
logdev.flock(File::LOCK_EX)
|
754
|
-
logdev.sync = true
|
755
|
-
add_log_header(logdev)
|
756
|
-
logdev.flock(File::LOCK_UN)
|
757
|
-
rescue Errno::EEXIST
|
758
|
-
# file is created by another process
|
759
|
-
logdev = open_logfile(filename)
|
760
|
-
logdev.sync = true
|
761
|
-
end
|
762
|
-
logdev
|
763
|
-
end
|
764
|
-
|
765
|
-
def add_log_header(file)
|
766
|
-
file.write(
|
767
|
-
"# Logfile created on %s by %s\n" % [Time.now.to_s, Logger::ProgName]
|
768
|
-
) if file.size == 0
|
769
|
-
end
|
770
|
-
|
771
|
-
def check_shift_log
|
772
|
-
if @shift_age.is_a?(Integer)
|
773
|
-
# Note: always returns false if '0'.
|
774
|
-
if @filename && (@shift_age > 0) && (@dev.stat.size > @shift_size)
|
775
|
-
lock_shift_log { shift_log_age }
|
776
|
-
end
|
777
|
-
else
|
778
|
-
now = Time.now
|
779
|
-
if now >= @next_rotate_time
|
780
|
-
@next_rotate_time = next_rotate_time(now, @shift_age)
|
781
|
-
lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
|
782
|
-
end
|
783
|
-
end
|
784
|
-
end
|
785
|
-
|
786
|
-
if /mswin|mingw/ =~ RUBY_PLATFORM
|
787
|
-
def lock_shift_log
|
788
|
-
yield
|
789
|
-
end
|
790
|
-
else
|
791
|
-
def lock_shift_log
|
792
|
-
retry_limit = 8
|
793
|
-
retry_sleep = 0.1
|
794
|
-
begin
|
795
|
-
File.open(@filename, File::WRONLY | File::APPEND) do |lock|
|
796
|
-
lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
|
797
|
-
if File.identical?(@filename, lock) and File.identical?(lock, @dev)
|
798
|
-
yield # log shifting
|
799
|
-
else
|
800
|
-
# log shifted by another process (i-node before locking and i-node after locking are different)
|
801
|
-
@dev.close rescue nil
|
802
|
-
@dev = open_logfile(@filename)
|
803
|
-
@dev.sync = true
|
804
|
-
end
|
805
|
-
end
|
806
|
-
rescue Errno::ENOENT
|
807
|
-
# @filename file would not exist right after #rename and before #create_logfile
|
808
|
-
if retry_limit <= 0
|
809
|
-
warn("log rotation inter-process lock failed. #{$!}")
|
810
|
-
else
|
811
|
-
sleep retry_sleep
|
812
|
-
retry_limit -= 1
|
813
|
-
retry_sleep *= 2
|
814
|
-
retry
|
815
|
-
end
|
816
|
-
end
|
817
|
-
rescue
|
818
|
-
warn("log rotation inter-process lock failed. #{$!}")
|
819
|
-
end
|
820
|
-
end
|
821
|
-
|
822
|
-
def shift_log_age
|
823
|
-
(@shift_age-3).downto(0) do |i|
|
824
|
-
if FileTest.exist?("#{@filename}.#{i}")
|
825
|
-
File.rename("#{@filename}.#{i}", "#{@filename}.#{i+1}")
|
826
|
-
end
|
827
|
-
end
|
828
|
-
@dev.close rescue nil
|
829
|
-
File.rename("#{@filename}", "#{@filename}.0")
|
830
|
-
@dev = create_logfile(@filename)
|
831
|
-
return true
|
832
|
-
end
|
833
|
-
|
834
|
-
def shift_log_period(period_end)
|
835
|
-
suffix = period_end.strftime(@shift_period_suffix)
|
836
|
-
age_file = "#{@filename}.#{suffix}"
|
837
|
-
if FileTest.exist?(age_file)
|
838
|
-
# try to avoid filename crash caused by Timestamp change.
|
839
|
-
idx = 0
|
840
|
-
# .99 can be overridden; avoid too much file search with 'loop do'
|
841
|
-
while idx < 100
|
842
|
-
idx += 1
|
843
|
-
age_file = "#{@filename}.#{suffix}.#{idx}"
|
844
|
-
break unless FileTest.exist?(age_file)
|
845
|
-
end
|
846
|
-
end
|
847
|
-
@dev.close rescue nil
|
848
|
-
File.rename("#{@filename}", age_file)
|
849
|
-
@dev = create_logfile(@filename)
|
850
|
-
return true
|
851
|
-
end
|
852
|
-
end
|
853
589
|
end
|
data/logger.gemspec
CHANGED
@@ -1,27 +1,27 @@
|
|
1
1
|
begin
|
2
|
-
require_relative "lib/logger"
|
3
|
-
rescue LoadError
|
4
|
-
|
5
|
-
require_relative "logger"
|
2
|
+
require_relative "lib/logger/version"
|
3
|
+
rescue LoadError # Fallback to load version file in ruby core repository
|
4
|
+
require_relative "version"
|
6
5
|
end
|
7
6
|
|
8
7
|
Gem::Specification.new do |spec|
|
9
8
|
spec.name = "logger"
|
10
9
|
spec.version = Logger::VERSION
|
11
|
-
spec.authors = ["SHIBATA Hiroshi"]
|
12
|
-
spec.email = ["hsbt@ruby-lang.org"]
|
10
|
+
spec.authors = ["Naotoshi Seo", "SHIBATA Hiroshi"]
|
11
|
+
spec.email = ["sonots@gmail.com", "hsbt@ruby-lang.org"]
|
13
12
|
|
14
13
|
spec.summary = %q{Provides a simple logging utility for outputting messages.}
|
15
14
|
spec.description = %q{Provides a simple logging utility for outputting messages.}
|
16
15
|
spec.homepage = "https://github.com/ruby/logger"
|
17
|
-
spec.
|
16
|
+
spec.licenses = ["Ruby", "BSD-2-Clause"]
|
18
17
|
|
19
|
-
spec.files =
|
20
|
-
spec.bindir = "exe"
|
21
|
-
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
|
18
|
+
spec.files = Dir.glob("lib/**/*.rb") + ["logger.gemspec"]
|
22
19
|
spec.require_paths = ["lib"]
|
23
20
|
|
24
|
-
spec.
|
25
|
-
|
26
|
-
spec.add_development_dependency "
|
21
|
+
spec.required_ruby_version = ">= 2.3.0"
|
22
|
+
|
23
|
+
spec.add_development_dependency "bundler", ">= 0"
|
24
|
+
spec.add_development_dependency "rake", ">= 12.3.3"
|
25
|
+
spec.add_development_dependency "test-unit"
|
26
|
+
spec.add_development_dependency "rdoc"
|
27
27
|
end
|
metadata
CHANGED
@@ -1,79 +1,94 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: logger
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 1.
|
4
|
+
version: 1.5.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
|
+
- Naotoshi Seo
|
7
8
|
- SHIBATA Hiroshi
|
8
|
-
autorequire:
|
9
|
-
bindir:
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
10
11
|
cert_chain: []
|
11
|
-
date:
|
12
|
+
date: 2022-03-17 00:00:00.000000000 Z
|
12
13
|
dependencies:
|
13
14
|
- !ruby/object:Gem::Dependency
|
14
15
|
name: bundler
|
15
16
|
requirement: !ruby/object:Gem::Requirement
|
16
17
|
requirements:
|
17
|
-
- - "
|
18
|
+
- - ">="
|
18
19
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
20
|
+
version: '0'
|
20
21
|
type: :development
|
21
22
|
prerelease: false
|
22
23
|
version_requirements: !ruby/object:Gem::Requirement
|
23
24
|
requirements:
|
24
|
-
- - "
|
25
|
+
- - ">="
|
25
26
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
27
|
+
version: '0'
|
27
28
|
- !ruby/object:Gem::Dependency
|
28
29
|
name: rake
|
29
30
|
requirement: !ruby/object:Gem::Requirement
|
30
31
|
requirements:
|
31
|
-
- - "
|
32
|
+
- - ">="
|
32
33
|
- !ruby/object:Gem::Version
|
33
|
-
version:
|
34
|
+
version: 12.3.3
|
34
35
|
type: :development
|
35
36
|
prerelease: false
|
36
37
|
version_requirements: !ruby/object:Gem::Requirement
|
37
38
|
requirements:
|
38
|
-
- - "
|
39
|
+
- - ">="
|
39
40
|
- !ruby/object:Gem::Version
|
40
|
-
version:
|
41
|
+
version: 12.3.3
|
41
42
|
- !ruby/object:Gem::Dependency
|
42
|
-
name:
|
43
|
+
name: test-unit
|
43
44
|
requirement: !ruby/object:Gem::Requirement
|
44
45
|
requirements:
|
45
|
-
- - "
|
46
|
+
- - ">="
|
46
47
|
- !ruby/object:Gem::Version
|
47
|
-
version: '
|
48
|
+
version: '0'
|
48
49
|
type: :development
|
49
50
|
prerelease: false
|
50
51
|
version_requirements: !ruby/object:Gem::Requirement
|
51
52
|
requirements:
|
52
|
-
- - "
|
53
|
+
- - ">="
|
53
54
|
- !ruby/object:Gem::Version
|
54
|
-
version: '
|
55
|
+
version: '0'
|
56
|
+
- !ruby/object:Gem::Dependency
|
57
|
+
name: rdoc
|
58
|
+
requirement: !ruby/object:Gem::Requirement
|
59
|
+
requirements:
|
60
|
+
- - ">="
|
61
|
+
- !ruby/object:Gem::Version
|
62
|
+
version: '0'
|
63
|
+
type: :development
|
64
|
+
prerelease: false
|
65
|
+
version_requirements: !ruby/object:Gem::Requirement
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
version: '0'
|
55
70
|
description: Provides a simple logging utility for outputting messages.
|
56
71
|
email:
|
72
|
+
- sonots@gmail.com
|
57
73
|
- hsbt@ruby-lang.org
|
58
74
|
executables: []
|
59
75
|
extensions: []
|
60
76
|
extra_rdoc_files: []
|
61
77
|
files:
|
62
|
-
- ".gitignore"
|
63
|
-
- ".travis.yml"
|
64
|
-
- Gemfile
|
65
|
-
- LICENSE.txt
|
66
|
-
- README.md
|
67
|
-
- Rakefile
|
68
|
-
- bin/console
|
69
|
-
- bin/setup
|
70
78
|
- lib/logger.rb
|
79
|
+
- lib/logger/errors.rb
|
80
|
+
- lib/logger/formatter.rb
|
81
|
+
- lib/logger/log_device.rb
|
82
|
+
- lib/logger/period.rb
|
83
|
+
- lib/logger/severity.rb
|
84
|
+
- lib/logger/version.rb
|
71
85
|
- logger.gemspec
|
72
86
|
homepage: https://github.com/ruby/logger
|
73
87
|
licenses:
|
88
|
+
- Ruby
|
74
89
|
- BSD-2-Clause
|
75
90
|
metadata: {}
|
76
|
-
post_install_message:
|
91
|
+
post_install_message:
|
77
92
|
rdoc_options: []
|
78
93
|
require_paths:
|
79
94
|
- lib
|
@@ -81,16 +96,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
81
96
|
requirements:
|
82
97
|
- - ">="
|
83
98
|
- !ruby/object:Gem::Version
|
84
|
-
version:
|
99
|
+
version: 2.3.0
|
85
100
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
86
101
|
requirements:
|
87
102
|
- - ">="
|
88
103
|
- !ruby/object:Gem::Version
|
89
104
|
version: '0'
|
90
105
|
requirements: []
|
91
|
-
|
92
|
-
|
93
|
-
signing_key:
|
106
|
+
rubygems_version: 3.4.0.dev
|
107
|
+
signing_key:
|
94
108
|
specification_version: 4
|
95
109
|
summary: Provides a simple logging utility for outputting messages.
|
96
110
|
test_files: []
|
data/.gitignore
DELETED
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/LICENSE.txt
DELETED
@@ -1,22 +0,0 @@
|
|
1
|
-
Copyright (C) 1993-2013 Yukihiro Matsumoto. All rights reserved.
|
2
|
-
|
3
|
-
Redistribution and use in source and binary forms, with or without
|
4
|
-
modification, are permitted provided that the following conditions
|
5
|
-
are met:
|
6
|
-
1. Redistributions of source code must retain the above copyright
|
7
|
-
notice, this list of conditions and the following disclaimer.
|
8
|
-
2. Redistributions in binary form must reproduce the above copyright
|
9
|
-
notice, this list of conditions and the following disclaimer in the
|
10
|
-
documentation and/or other materials provided with the distribution.
|
11
|
-
|
12
|
-
THIS SOFTWARE IS PROVIDED BY THE AUTHOR AND CONTRIBUTORS ``AS IS'' AND
|
13
|
-
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
14
|
-
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
15
|
-
ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR OR CONTRIBUTORS BE LIABLE
|
16
|
-
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
17
|
-
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
18
|
-
OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
19
|
-
HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
|
20
|
-
LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY
|
21
|
-
OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF
|
22
|
-
SUCH DAMAGE.
|
data/README.md
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
# Logger
|
2
|
-
|
3
|
-
Welcome to your new gem! In this directory, you'll find the files you need to be able to package up your Ruby library into a gem. Put your Ruby code in the file `lib/logger`. To experiment with that code, run `bin/console` for an interactive prompt.
|
4
|
-
|
5
|
-
TODO: Delete this and the text above, and describe your gem
|
6
|
-
|
7
|
-
## Installation
|
8
|
-
|
9
|
-
Add this line to your application's Gemfile:
|
10
|
-
|
11
|
-
```ruby
|
12
|
-
gem 'logger'
|
13
|
-
```
|
14
|
-
|
15
|
-
And then execute:
|
16
|
-
|
17
|
-
$ bundle
|
18
|
-
|
19
|
-
Or install it yourself as:
|
20
|
-
|
21
|
-
$ gem install logger
|
22
|
-
|
23
|
-
## Usage
|
24
|
-
|
25
|
-
TODO: Write usage instructions here
|
26
|
-
|
27
|
-
## Development
|
28
|
-
|
29
|
-
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
30
|
-
|
31
|
-
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
32
|
-
|
33
|
-
## Contributing
|
34
|
-
|
35
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/ruby/logger.
|
36
|
-
|
37
|
-
## License
|
38
|
-
|
39
|
-
The gem is available as open source under the terms of the [BSD-2-Clause](LICENSE.txt).
|
data/Rakefile
DELETED
data/bin/console
DELETED
@@ -1,14 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
|
3
|
-
require "bundler/setup"
|
4
|
-
require "logger"
|
5
|
-
|
6
|
-
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
-
# with your gem easier. You can also use a different console, if you like.
|
8
|
-
|
9
|
-
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
-
# require "pry"
|
11
|
-
# Pry.start
|
12
|
-
|
13
|
-
require "irb"
|
14
|
-
IRB.start(__FILE__)
|