testa_logger 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: ff209242c287b9a4143614c9375371889e72eab6bebb81efb5ab23202f461293
4
+ data.tar.gz: cde5a35fbaa3c1ba3bb4d5e3b10a29c4185906116bfcc6b1c46202e27a3eb007
5
+ SHA512:
6
+ metadata.gz: a8272501fb9046b72e231420fba9b345a29e51cbe6a234dfed7de0db812de5698897caa03271ac4bfd58bf5a61e093e5354540dff45db8760ec97ca39e712602
7
+ data.tar.gz: 1dbc91b8244fd5c9414b203815daf345d9c09378231626be98a0199c6791c9bbbba97605503febbd662a0a4025cb77c3f77bdb6c62b29dc9e5622e1a837fc791
data/.gitignore ADDED
@@ -0,0 +1,8 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
data/.rubocop.yml ADDED
@@ -0,0 +1,4 @@
1
+ # we have to have per project rubcop files (IDE limitation),
2
+ # but we want only one config across all projects
3
+ # you can override any global cops here, in this file
4
+ inherit_from: ../.rubocop.yml
@@ -0,0 +1,74 @@
1
+ # Contributor Covenant Code of Conduct
2
+
3
+ ## Our Pledge
4
+
5
+ In the interest of fostering an open and welcoming environment, we as
6
+ contributors and maintainers pledge to making participation in our project and
7
+ our community a harassment-free experience for everyone, regardless of age, body
8
+ size, disability, ethnicity, gender identity and expression, level of experience,
9
+ nationality, personal appearance, race, religion, or sexual identity and
10
+ orientation.
11
+
12
+ ## Our Standards
13
+
14
+ Examples of behavior that contributes to creating a positive environment
15
+ include:
16
+
17
+ * Using welcoming and inclusive language
18
+ * Being respectful of differing viewpoints and experiences
19
+ * Gracefully accepting constructive criticism
20
+ * Focusing on what is best for the community
21
+ * Showing empathy towards other community members
22
+
23
+ Examples of unacceptable behavior by participants include:
24
+
25
+ * The use of sexualized language or imagery and unwelcome sexual attention or
26
+ advances
27
+ * Trolling, insulting/derogatory comments, and personal or political attacks
28
+ * Public or private harassment
29
+ * Publishing others' private information, such as a physical or electronic
30
+ address, without explicit permission
31
+ * Other conduct which could reasonably be considered inappropriate in a
32
+ professional setting
33
+
34
+ ## Our Responsibilities
35
+
36
+ Project maintainers are responsible for clarifying the standards of acceptable
37
+ behavior and are expected to take appropriate and fair corrective action in
38
+ response to any instances of unacceptable behavior.
39
+
40
+ Project maintainers have the right and responsibility to remove, edit, or
41
+ reject comments, commits, code, wiki edits, issues, and other contributions
42
+ that are not aligned to this Code of Conduct, or to ban temporarily or
43
+ permanently any contributor for other behaviors that they deem inappropriate,
44
+ threatening, offensive, or harmful.
45
+
46
+ ## Scope
47
+
48
+ This Code of Conduct applies both within project spaces and in public spaces
49
+ when an individual is representing the project or its community. Examples of
50
+ representing a project or community include using an official project e-mail
51
+ address, posting via an official social media account, or acting as an appointed
52
+ representative at an online or offline event. Representation of a project may be
53
+ further defined and clarified by project maintainers.
54
+
55
+ ## Enforcement
56
+
57
+ Instances of abusive, harassing, or otherwise unacceptable behavior may be
58
+ reported by contacting the project team at karlo.razumovic@gmail.com. All
59
+ complaints will be reviewed and investigated and will result in a response that
60
+ is deemed necessary and appropriate to the circumstances. The project team is
61
+ obligated to maintain confidentiality with regard to the reporter of an incident.
62
+ Further details of specific enforcement policies may be posted separately.
63
+
64
+ Project maintainers who do not follow or enforce the Code of Conduct in good
65
+ faith may face temporary or permanent repercussions as determined by other
66
+ members of the project's leadership.
67
+
68
+ ## Attribution
69
+
70
+ This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
71
+ available at [https://contributor-covenant.org/version/1/4][version]
72
+
73
+ [homepage]: https://contributor-covenant.org
74
+ [version]: https://contributor-covenant.org/version/1/4/
data/Gemfile ADDED
@@ -0,0 +1,9 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in testa_logger.gemspec
4
+ gemspec
5
+
6
+ gem "minitest", "~> 5.0"
7
+ gem "rake", "~> 12.0"
8
+ gem "rubocop", "~> 1.26.0", require: false
9
+ gem "rubocop-rails", require: false
data/Gemfile.lock ADDED
@@ -0,0 +1,88 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ testa_logger (0.1.0)
5
+ activesupport
6
+ awesome_print
7
+ aws-sdk-s3
8
+ concurrent-ruby
9
+ json
10
+ ostruct
11
+ securerandom
12
+
13
+ GEM
14
+ remote: https://rubygems.org/
15
+ specs:
16
+ activesupport (6.1.6.1)
17
+ concurrent-ruby (~> 1.0, >= 1.0.2)
18
+ i18n (>= 1.6, < 2)
19
+ minitest (>= 5.1)
20
+ tzinfo (~> 2.0)
21
+ zeitwerk (~> 2.3)
22
+ ast (2.4.2)
23
+ awesome_print (1.9.2)
24
+ aws-eventstream (1.2.0)
25
+ aws-partitions (1.614.0)
26
+ aws-sdk-core (3.131.5)
27
+ aws-eventstream (~> 1, >= 1.0.2)
28
+ aws-partitions (~> 1, >= 1.525.0)
29
+ aws-sigv4 (~> 1.1)
30
+ jmespath (~> 1, >= 1.6.1)
31
+ aws-sdk-kms (1.58.0)
32
+ aws-sdk-core (~> 3, >= 3.127.0)
33
+ aws-sigv4 (~> 1.1)
34
+ aws-sdk-s3 (1.114.0)
35
+ aws-sdk-core (~> 3, >= 3.127.0)
36
+ aws-sdk-kms (~> 1)
37
+ aws-sigv4 (~> 1.4)
38
+ aws-sigv4 (1.5.1)
39
+ aws-eventstream (~> 1, >= 1.0.2)
40
+ concurrent-ruby (1.1.10)
41
+ i18n (1.12.0)
42
+ concurrent-ruby (~> 1.0)
43
+ jmespath (1.6.1)
44
+ json (2.6.2)
45
+ minitest (5.16.2)
46
+ ostruct (0.2.0)
47
+ parallel (1.22.1)
48
+ parser (3.1.2.1)
49
+ ast (~> 2.4.1)
50
+ rack (2.2.4)
51
+ rainbow (3.1.1)
52
+ rake (12.3.3)
53
+ regexp_parser (2.5.0)
54
+ rexml (3.2.5)
55
+ rubocop (1.26.1)
56
+ parallel (~> 1.10)
57
+ parser (>= 3.1.0.0)
58
+ rainbow (>= 2.2.2, < 4.0)
59
+ regexp_parser (>= 1.8, < 3.0)
60
+ rexml
61
+ rubocop-ast (>= 1.16.0, < 2.0)
62
+ ruby-progressbar (~> 1.7)
63
+ unicode-display_width (>= 1.4.0, < 3.0)
64
+ rubocop-ast (1.21.0)
65
+ parser (>= 3.1.1.0)
66
+ rubocop-rails (2.15.2)
67
+ activesupport (>= 4.2.0)
68
+ rack (>= 1.1)
69
+ rubocop (>= 1.7.0, < 2.0)
70
+ ruby-progressbar (1.11.0)
71
+ securerandom (0.2.0)
72
+ tzinfo (2.0.5)
73
+ concurrent-ruby (~> 1.0)
74
+ unicode-display_width (2.2.0)
75
+ zeitwerk (2.6.0)
76
+
77
+ PLATFORMS
78
+ x86_64-linux
79
+
80
+ DEPENDENCIES
81
+ minitest (~> 5.0)
82
+ rake (~> 12.0)
83
+ rubocop (~> 1.26.0)
84
+ rubocop-rails
85
+ testa_logger!
86
+
87
+ BUNDLED WITH
88
+ 2.3.14
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2022 karlo.razumovic
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,44 @@
1
+ # TestaLogger
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/testa_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 'testa_logger'
13
+ ```
14
+
15
+ And then execute:
16
+
17
+ $ bundle install
18
+
19
+ Or install it yourself as:
20
+
21
+ $ gem install testa_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/[USERNAME]/testa_logger. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [code of conduct](https://github.com/[USERNAME]/testa_logger/blob/master/CODE_OF_CONDUCT.md).
36
+
37
+
38
+ ## License
39
+
40
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
41
+
42
+ ## Code of Conduct
43
+
44
+ Everyone interacting in the TestaLogger project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the [code of conduct](https://github.com/[USERNAME]/testa_logger/blob/master/CODE_OF_CONDUCT.md).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require "bundler/gem_tasks"
2
+ require "rake/testtask"
3
+
4
+ Rake::TestTask.new(:test) do |t|
5
+ t.libs << "test"
6
+ t.libs << "lib"
7
+ t.test_files = FileList["test/**/*_test.rb"]
8
+ end
9
+
10
+ task :default => :test
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "testa_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__)
data/bin/setup ADDED
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
7
+
8
+ # Do any other automated setup that you need to do here
@@ -0,0 +1,4 @@
1
+ module TestaLogger
2
+ class IoPersistenceError < StandardError
3
+ end
4
+ end
@@ -0,0 +1,49 @@
1
+ # frozen_string_literal: true
2
+
3
+ module TestaLogger
4
+ class LogDevice
5
+ module Period
6
+ module_function
7
+
8
+ SiD = 24 * 60 * 60
9
+
10
+ def next_rotate_time(now, shift_age)
11
+ case shift_age
12
+ when "daily"
13
+ t = Time.mktime(now.year, now.month, now.mday) + SiD
14
+ when "weekly"
15
+ t = Time.mktime(now.year, now.month, now.mday) + SiD * (7 - now.wday)
16
+ when "monthly"
17
+ t = Time.mktime(now.year, now.month, 1) + SiD * 32
18
+ return Time.mktime(t.year, t.month, 1)
19
+ when "now", "everytime"
20
+ return now
21
+ else
22
+ raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
23
+ end
24
+ if t.hour.nonzero? || t.min.nonzero? || t.sec.nonzero?
25
+ hour = t.hour
26
+ t = Time.mktime(t.year, t.month, t.mday)
27
+ t += SiD if hour > 12
28
+ end
29
+ t
30
+ end
31
+
32
+ def previous_period_end(now, shift_age)
33
+ case shift_age
34
+ when "daily"
35
+ t = Time.mktime(now.year, now.month, now.mday) - SiD / 2
36
+ when "weekly"
37
+ t = Time.mktime(now.year, now.month, now.mday) - (SiD * now.wday + SiD / 2)
38
+ when "monthly"
39
+ t = Time.mktime(now.year, now.month, 1) - SiD / 2
40
+ when "now", "everytime"
41
+ return now
42
+ else
43
+ raise ArgumentError, "invalid :shift_age #{shift_age.inspect}, should be daily, weekly, monthly, or everytime"
44
+ end
45
+ Time.mktime(t.year, t.month, t.mday, 23, 59, 59)
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,198 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "log_device/period"
4
+
5
+ module TestaLogger
6
+ # Device used for logging messages.
7
+ class LogDevice
8
+ include Period
9
+ include MonitorMixin
10
+
11
+ attr_reader :dev, :filename
12
+
13
+ def initialize(log, shift_age: nil, shift_size: nil, shift_period_suffix: nil, binmode: false, before_shift: nil)
14
+ @dev = @filename = @shift_age = @shift_size = @shift_period_suffix = nil
15
+ @before_shift = before_shift
16
+ @binmode = binmode
17
+ mon_initialize
18
+ set_dev(log)
19
+
20
+ return unless @filename
21
+
22
+ @shift_age = shift_age || 7
23
+ @shift_size = shift_size || 1_048_576
24
+ @shift_period_suffix = shift_period_suffix || "%Y%m%d"
25
+
26
+ return if @shift_age.is_a?(Integer)
27
+
28
+ base_time = @dev.respond_to?(:stat) ? @dev.stat.mtime : Time.now
29
+ @next_rotate_time = next_rotate_time(base_time, @shift_age)
30
+ end
31
+
32
+ def write(message)
33
+ synchronize do
34
+ if @shift_age && @dev.respond_to?(:stat)
35
+ begin
36
+ check_shift_log
37
+ rescue StandardError => e
38
+ warn("log shifting failed. #{e}")
39
+ end
40
+ end
41
+ begin
42
+ @dev.write(message)
43
+ rescue StandardError => e
44
+ warn("log writing failed. #{e}")
45
+ end
46
+ end
47
+ rescue SystemExit
48
+ exit 1
49
+ rescue Exception => e
50
+ warn("log writing failed. #{e}")
51
+ end
52
+
53
+ def close
54
+ synchronize do
55
+ @dev.close rescue nil
56
+ end
57
+ rescue SystemExit
58
+ exit 1
59
+ rescue Exception
60
+ @dev.close rescue nil
61
+ end
62
+
63
+ def reopen(log = nil)
64
+ # reopen the same filename if no argument, do nothing for IO
65
+ log ||= @filename if @filename
66
+ if log
67
+ synchronize do
68
+ if @filename && @dev
69
+ @dev.close rescue nil # close only file opened by Logger
70
+ @filename = nil
71
+ end
72
+ set_dev(log)
73
+ end
74
+ end
75
+ self
76
+ end
77
+
78
+ private
79
+
80
+ def set_dev(log)
81
+ if log.respond_to?(:write) && log.respond_to?(:close)
82
+ @dev = log
83
+ @filename = log.path if log.respond_to?(:path)
84
+ else
85
+ @dev = open_logfile(log)
86
+ @dev.sync = true
87
+ @dev.binmode if @binmode
88
+ @filename = log
89
+ end
90
+ end
91
+
92
+ def open_logfile(filename)
93
+ File.open(filename, (File::WRONLY | File::APPEND))
94
+ rescue Errno::ENOENT
95
+ create_logfile(filename)
96
+ end
97
+
98
+ def create_logfile(filename)
99
+ begin
100
+ logdev = File.open(filename, (File::WRONLY | File::APPEND | File::CREAT | File::EXCL))
101
+ logdev.flock(File::LOCK_EX)
102
+ logdev.sync = true
103
+ logdev.binmode if @binmode
104
+ add_log_header(logdev)
105
+ logdev.flock(File::LOCK_UN)
106
+ rescue Errno::EEXIST
107
+ # file is created by another process
108
+ logdev = open_logfile(filename)
109
+ logdev.sync = true
110
+ end
111
+ logdev
112
+ end
113
+
114
+ def add_log_header(file)
115
+ file.write(format("# Logfile created on %s\n", Time.now.to_s)) if file.size.zero?
116
+ end
117
+
118
+ def check_shift_log
119
+ if @shift_age.is_a?(Integer)
120
+ # NOTE: always returns false if '0'.
121
+ lock_shift_log { shift_log_age } if @filename && @shift_age.positive? && (@dev.stat.size > @shift_size)
122
+ else
123
+ now = Time.now
124
+ if now >= @next_rotate_time
125
+ @next_rotate_time = next_rotate_time(now, @shift_age)
126
+ lock_shift_log { shift_log_period(previous_period_end(now, @shift_age)) }
127
+ end
128
+ end
129
+ end
130
+
131
+ if /mswin|mingw/ =~ RUBY_PLATFORM
132
+ def lock_shift_log
133
+ yield
134
+ end
135
+ else
136
+ def lock_shift_log
137
+ retry_limit = 8
138
+ retry_sleep = 0.1
139
+ begin
140
+ File.open(@filename, File::WRONLY | File::APPEND) do |lock|
141
+ lock.flock(File::LOCK_EX) # inter-process locking. will be unlocked at closing file
142
+ if File.identical?(@filename, lock) && File.identical?(lock, @dev)
143
+ yield # log shifting
144
+ else
145
+ # log shifted by another process (i-node before locking and i-node after locking are different)
146
+ @dev.close rescue nil
147
+ @dev = open_logfile(@filename)
148
+ @dev.sync = true
149
+ end
150
+ end
151
+ rescue Errno::ENOENT => e
152
+ # @filename file would not exist right after #rename and before #create_logfile
153
+ if retry_limit <= 0
154
+ warn("log rotation inter-process lock failed. #{e}")
155
+ else
156
+ sleep retry_sleep
157
+ retry_limit -= 1
158
+ retry_sleep *= 2
159
+ retry
160
+ end
161
+ end
162
+ rescue StandardError => e
163
+ warn("log rotation inter-process lock failed. #{e}")
164
+ end
165
+ end
166
+
167
+ def shift_log_age
168
+ (@shift_age - 3).downto(0) do |i|
169
+ File.rename("#{@filename}.#{i}", "#{@filename}.#{i + 1}") if FileTest.exist?("#{@filename}.#{i}")
170
+ end
171
+ @dev.close rescue nil
172
+ @before_shift&.call(@filename.to_s)
173
+ File.rename(@filename.to_s, "#{@filename}.0")
174
+ @dev = create_logfile(@filename)
175
+ true
176
+ end
177
+
178
+ def shift_log_period(period_end)
179
+ suffix = period_end.strftime(@shift_period_suffix)
180
+ age_file = "#{@filename}.#{suffix}"
181
+ if FileTest.exist?(age_file)
182
+ # try to avoid filename crash caused by Timestamp change.
183
+ idx = 0
184
+ # .99 can be overridden; avoid too much file search with 'loop do'
185
+ while idx < 100
186
+ idx += 1
187
+ age_file = "#{@filename}.#{suffix}.#{idx}"
188
+ break unless FileTest.exist?(age_file)
189
+ end
190
+ end
191
+ @dev.close rescue nil
192
+ @before_shift&.call(@filename.to_s)
193
+ File.rename(@filename.to_s, age_file)
194
+ @dev = create_logfile(@filename)
195
+ true
196
+ end
197
+ end
198
+ end
@@ -0,0 +1,52 @@
1
+ module TestaLogger
2
+ class Logger
3
+ class Dispatcher
4
+ def initialize(faye_url, faye_token, app, group, subgroup = nil)
5
+ @uri = URI.parse(faye_url)
6
+ @faye_token = faye_token
7
+ @channel = "/logs/live/#{app}/#{group}"
8
+ @channel += "/#{subgroup}" unless subgroup.nil?
9
+ @outbox = Concurrent::Array.new
10
+ run_dispatch_thread
11
+ at_exit { dispatch }
12
+ end
13
+
14
+ def push(level, time, tag, formatted_text)
15
+ data = {
16
+ level: level,
17
+ time: time,
18
+ tag: tag,
19
+ formatted_text: formatted_text,
20
+ }
21
+ @outbox << data
22
+ end
23
+
24
+ def run_dispatch_thread
25
+ Thread.new do
26
+ max_delay = 0.5
27
+ max_buffer = 20
28
+ last_sent = Time.now.to_f
29
+ loop do
30
+ sleep 0.1
31
+ next unless (last_sent + max_delay < Time.now.to_f) || @outbox.count > max_buffer
32
+
33
+ last_sent = Time.now.to_f
34
+ next if @outbox.count.zero?
35
+
36
+ dispatch
37
+ end
38
+ end
39
+ end
40
+
41
+ def dispatch
42
+ message = {
43
+ channel: @channel,
44
+ data: { logs: @outbox },
45
+ ext: { auth_token: @faye_token },
46
+ }
47
+ Net::HTTP.post_form(@uri, message: message.to_json)
48
+ @outbox.clear
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,65 @@
1
+ module TestaLogger
2
+ class Logger
3
+ module Persistence
4
+ def self.extended(base)
5
+ require "aws-sdk-s3"
6
+ base.init_s3_client
7
+ end
8
+
9
+ def init_s3_client
10
+ Aws.config.update(
11
+ region: options.s3_creds[:region],
12
+ credentials: Aws::Credentials.new(options.s3_creds[:access_key_id], options.s3_creds[:secret_access_key])
13
+ )
14
+ @s3 = Aws::S3::Client.new
15
+ at_exit { persist rescue false }
16
+ end
17
+
18
+ def persist_with_record(record, attachment_name)
19
+ raise IoPersistenceError if @log_device.is_a?(IO)
20
+
21
+ begin
22
+ # stop writing into log file with it is being attached, otherwise checksum integrity will fail.
23
+ write_thread["stop"] = true
24
+ record.send(attachment_name).attach(io: File.open(options.filepath), filename: "#{attachment_name}.log")
25
+ rescue StandardError => e
26
+ error(TAG, e)
27
+ raise
28
+ ensure
29
+ write_thread["stop"] = false
30
+ write_thread.run
31
+ end
32
+ end
33
+
34
+ def persist
35
+ return if @s3.nil?
36
+
37
+ begin
38
+ write_thread["stop"] = true
39
+
40
+ key = "logs/#{app}/#{group}"
41
+ key += "/#{subgroup}" unless subgroup.nil?
42
+
43
+ extension = File.extname(options.filepath)
44
+ filename = File.basename(options.filepath, extension)
45
+ time_string = Time.now.strftime("%H_%M_%S__%d_%m_%Y")
46
+ key += "/#{filename}_#{time_string}#{extension}"
47
+
48
+ # stop writing into log file with it is being attached, otherwise checksum integrity will fail.
49
+ response = @s3.put_object(
50
+ bucket: options.s3_creds[:bucket],
51
+ key: key,
52
+ body: IO.read(options.filepath)
53
+ )
54
+ error(TAG, "Failed to persist log file #{options.filepath}. Response: #{response.body}") unless response.etag
55
+ rescue StandardError => e
56
+ error(TAG, e)
57
+ raise
58
+ ensure
59
+ write_thread["stop"] = false
60
+ write_thread.run
61
+ end
62
+ end
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,194 @@
1
+ require_relative "logger/persistence"
2
+ require_relative "logger/dispatcher"
3
+
4
+ module TestaLogger
5
+ class Logger
6
+ TAG = "TestaLogger"
7
+
8
+ # @return [String]
9
+ attr_accessor :app, :group, :subgroup
10
+
11
+ # @return [OpenStruct]
12
+ attr_accessor :options
13
+
14
+ # @return [Thread]
15
+ attr_accessor :write_thread
16
+
17
+ # Low-level information, mostly for developers.
18
+ DEBUG = 0
19
+ # Generic (useful) information about system operation.
20
+ INFO = 1
21
+ # A warning.
22
+ WARN = 2
23
+ # A handleable error condition.
24
+ ERROR = 3
25
+ # An unhandleable error that results in a program crash.
26
+ FATAL = 4
27
+ # An unknown message that should always be logged.
28
+ UNKNOWN = 5
29
+
30
+ def initialize(app, group, subgroup = nil, options = {})
31
+ @app = app
32
+ @group = group
33
+ @subgroup = subgroup
34
+ handle_options(options)
35
+ create_log_file
36
+ setup_dispatcher
37
+ start_write_thread
38
+ self.extend Persistence if @options.persist
39
+ end
40
+
41
+ def handle_options(options)
42
+ options.deep_symbolize_keys!
43
+ @options = Logger.default_options
44
+ @options.shift_age = options[:shift_age] unless options[:shift_age].nil?
45
+ @options.level = options[:level] unless options[:level].nil?
46
+ @options.formatter = options[:formatter] unless options[:formatter].nil?
47
+ @options.live = options[:live] unless options[:live].nil?
48
+ @options.filepath = File.expand_path(options[:filepath]) unless options[:filepath].nil?
49
+ @options.tag_length = options[:tag_length] unless options[:tag_length].nil?
50
+ @options.persist = options[:persist] unless options[:persist].nil?
51
+ @options.faye_url = options[:faye_url] unless options[:faye_url].nil?
52
+ @options.faye_token = options[:faye_token] unless options[:faye_token].nil?
53
+ @options.s3_creds = options[:s3_creds].deep_symbolize_keys unless options[:s3_creds].empty?
54
+ end
55
+
56
+ def create_log_file
57
+ options.filepath = "/tmp/#{SecureRandom.uuid}.log" if options.filepath.nil?
58
+ FileUtils.mkdir_p(File.dirname(options.filepath))
59
+ before_shift = options.persist ? proc { persist } : nil
60
+ @log_device = LogDevice.new(options.filepath,
61
+ shift_age: options.shift_age,
62
+ shift_size: 1_048_576,
63
+ shift_period_suffix: "%d%m%Y",
64
+ binmode: false,
65
+ before_shift: before_shift)
66
+ end
67
+
68
+ def setup_dispatcher
69
+ @dispatcher = Dispatcher.new(
70
+ options.faye_url,
71
+ options.faye_token,
72
+ app,
73
+ group,
74
+ subgroup
75
+ )
76
+ end
77
+
78
+ def start_write_thread
79
+ # we must use this queue in order to be able to collect logs in trap context
80
+ @queue = Queue.new
81
+ @write_thread = Thread.new do
82
+ loop do
83
+ text = @queue.pop
84
+ Thread.stop if Thread.current["stop"]
85
+ @log_device.write(text)
86
+ end
87
+ end
88
+ end
89
+
90
+ def debug(tag, *args, &block)
91
+ add_log_to_queue(DEBUG, tag, args, &block)
92
+ end
93
+
94
+ def info(tag, *args, &block)
95
+ add_log_to_queue(INFO, tag, args, &block)
96
+ end
97
+
98
+ def warn(tag, *args, &block)
99
+ add_log_to_queue(WARN, tag, args, &block)
100
+ end
101
+
102
+ def error(tag, *args, &block)
103
+ add_log_to_queue(ERROR, tag, args, &block)
104
+ end
105
+
106
+ def fatal(tag, *args, &block)
107
+ add_log_to_queue(FATAL, tag, args, &block)
108
+ end
109
+
110
+ def unknown(tag, *args, &block)
111
+ add_log_to_queue(UNKNOWN, tag, args, &block)
112
+ end
113
+
114
+ class << self
115
+ def default_options
116
+ OpenStruct.new(
117
+ shift_age: "daily",
118
+ level: DEBUG,
119
+ formatter: default_formatter,
120
+ live: false,
121
+ filepath: nil,
122
+ tag_length: 13,
123
+ persist: true, # requires aws credentials set in env variables
124
+ faye_url: ENV["WEB_SOCKET_URL"],
125
+ faye_token: ENV["FAYE_TOKEN"],
126
+ s3_creds: {
127
+ region: ENV["AWS_REGION"],
128
+ access_key_id: ENV["AWS_ACCESS_KEY_ID"],
129
+ secret_access_key: ENV["AWS_SECRET_ACCESS_KEY"],
130
+ bucket_name: ENV["S3_BUCKET_NAME"],
131
+ }
132
+ )
133
+ end
134
+
135
+ def default_formatter
136
+ proc do |severity, datetime, _, msg|
137
+ "[#{datetime.strftime('%Y-%m-%d %H:%M:%S.%L')}] #{format('%-5.5s', severity)} -- : #{msg}\n"
138
+ end
139
+ end
140
+ end
141
+
142
+ private
143
+
144
+ def add_log_to_queue(level, tag, args, &block)
145
+ return if level < options.level
146
+
147
+ time = Time.now
148
+ text = create_log_string(tag, args, &block)
149
+ formatted_severity = format_severity(level)
150
+ formatted_text = format_message(formatted_severity, time, "", text)
151
+
152
+ @queue << formatted_text
153
+ @dispatcher.push(level, time, tag, formatted_text) if options.live
154
+ end
155
+
156
+ def create_log_string(tag, args, &block)
157
+ if args.count.zero?
158
+ if block.nil?
159
+ "[NO-TAG]: #{tag}"
160
+ else
161
+ "[#{padded_tag(tag)}]: #{args_to_string(block.call)}"
162
+ end
163
+ else
164
+ "[#{padded_tag(tag)}]: #{args_to_string(args)}"
165
+ end
166
+ end
167
+
168
+ def padded_tag(tag)
169
+ format("%-#{options.tag_length}.#{options.tag_length}s", tag)
170
+ end
171
+
172
+ def args_to_string(args)
173
+ args = [args] unless args.respond_to?(:map)
174
+ args.map do |arg|
175
+ if arg.instance_of?(String)
176
+ arg
177
+ else
178
+ arg.ai(limit: 1000, plain: true)
179
+ end
180
+ end.join("\n")
181
+ end
182
+
183
+ # Severity label for logging (max 5 chars).
184
+ SEV_LABEL = %w[DEBUG INFO WARN ERROR FATAL UNKNOWN].freeze
185
+
186
+ def format_severity(severity)
187
+ SEV_LABEL[severity] || "UNKNOWN"
188
+ end
189
+
190
+ def format_message(severity, datetime, progname, msg)
191
+ options.formatter.call(severity, datetime, progname, msg)
192
+ end
193
+ end
194
+ end
@@ -0,0 +1,3 @@
1
+ module TestaLogger
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,29 @@
1
+ require_relative "testa_logger/version"
2
+ require_relative "testa_logger/log_device"
3
+ require_relative "testa_logger/logger"
4
+ require_relative "testa_logger/io_persistence_error"
5
+ require "active_support"
6
+ require "active_support/core_ext/hash"
7
+ require "ostruct"
8
+ require "awesome_print"
9
+ require "concurrent-ruby"
10
+ require "json"
11
+ require "uri"
12
+ require "net/http"
13
+
14
+ module TestaLogger
15
+ # @param [String] app name of the application, i.e. rails, ws, dj, filewatcher
16
+ # @param [String] group category under the application, i.e. in rails (branch_version_job, import_job)
17
+ # @param [String, nil] subgroup optional, sub category under group, i.e. in dj group play (scenario_25, group_2)
18
+ # @param [Hash] options
19
+ def self.new(app, group, subgroup = nil, options = {})
20
+ Logger.new(app, group, subgroup, options)
21
+ end
22
+
23
+ # Patch exception logging
24
+ class ::Exception
25
+ def ai(options = {})
26
+ "Message: #{self.message}\nClass: #{self.class.name}\nBacktrace:\n#{self.backtrace.ai(options)}"
27
+ end
28
+ end
29
+ end
data/logger.iml ADDED
@@ -0,0 +1,75 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <module type="RUBY_MODULE" version="4">
3
+ <component name="NewModuleRootManager" inherit-compiler-output="true">
4
+ <exclude-output />
5
+ <content url="file://$MODULE_DIR$">
6
+ <sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
7
+ </content>
8
+ <orderEntry type="jdk" jdkName="RVM: ruby-2.7.6" jdkType="RUBY_SDK" />
9
+ <orderEntry type="sourceFolder" forTests="false" />
10
+ <orderEntry type="library" scope="PROVIDED" name="activesupport (v6.1.6.1, RVM: ruby-2.7.6) [gem]" level="application" />
11
+ <orderEntry type="library" scope="PROVIDED" name="ast (v2.4.2, RVM: ruby-2.7.6) [gem]" level="application" />
12
+ <orderEntry type="library" scope="PROVIDED" name="awesome_print (v1.9.2, RVM: ruby-2.7.6) [gem]" level="application" />
13
+ <orderEntry type="library" scope="PROVIDED" name="aws-eventstream (v1.2.0, RVM: ruby-2.7.6) [gem]" level="application" />
14
+ <orderEntry type="library" scope="PROVIDED" name="aws-partitions (v1.614.0, RVM: ruby-2.7.6) [gem]" level="application" />
15
+ <orderEntry type="library" scope="PROVIDED" name="aws-sdk-core (v3.131.5, RVM: ruby-2.7.6) [gem]" level="application" />
16
+ <orderEntry type="library" scope="PROVIDED" name="aws-sdk-kms (v1.58.0, RVM: ruby-2.7.6) [gem]" level="application" />
17
+ <orderEntry type="library" scope="PROVIDED" name="aws-sdk-s3 (v1.114.0, RVM: ruby-2.7.6) [gem]" level="application" />
18
+ <orderEntry type="library" scope="PROVIDED" name="aws-sigv4 (v1.5.1, RVM: ruby-2.7.6) [gem]" level="application" />
19
+ <orderEntry type="library" scope="PROVIDED" name="bundler (v2.3.14, RVM: ruby-2.7.6) [gem]" level="application" />
20
+ <orderEntry type="library" scope="PROVIDED" name="concurrent-ruby (v1.1.10, RVM: ruby-2.7.6) [gem]" level="application" />
21
+ <orderEntry type="library" scope="PROVIDED" name="i18n (v1.12.0, RVM: ruby-2.7.6) [gem]" level="application" />
22
+ <orderEntry type="library" scope="PROVIDED" name="jmespath (v1.6.1, RVM: ruby-2.7.6) [gem]" level="application" />
23
+ <orderEntry type="library" scope="PROVIDED" name="json (v2.6.2, RVM: ruby-2.7.6) [gem]" level="application" />
24
+ <orderEntry type="library" scope="PROVIDED" name="minitest (v5.16.2, RVM: ruby-2.7.6) [gem]" level="application" />
25
+ <orderEntry type="library" scope="PROVIDED" name="ostruct (v0.2.0, RVM: ruby-2.7.6) [gem]" level="application" />
26
+ <orderEntry type="library" scope="PROVIDED" name="parallel (v1.22.1, RVM: ruby-2.7.6) [gem]" level="application" />
27
+ <orderEntry type="library" scope="PROVIDED" name="parser (v3.1.2.1, RVM: ruby-2.7.6) [gem]" level="application" />
28
+ <orderEntry type="library" scope="PROVIDED" name="rack (v2.2.4, RVM: ruby-2.7.6) [gem]" level="application" />
29
+ <orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, RVM: ruby-2.7.6) [gem]" level="application" />
30
+ <orderEntry type="library" scope="PROVIDED" name="rake (v12.3.3, RVM: ruby-2.7.6) [gem]" level="application" />
31
+ <orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.5.0, RVM: ruby-2.7.6) [gem]" level="application" />
32
+ <orderEntry type="library" scope="PROVIDED" name="rexml (v3.2.5, RVM: ruby-2.7.6) [gem]" level="application" />
33
+ <orderEntry type="library" scope="PROVIDED" name="rubocop (v1.26.1, RVM: ruby-2.7.6) [gem]" level="application" />
34
+ <orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.21.0, RVM: ruby-2.7.6) [gem]" level="application" />
35
+ <orderEntry type="library" scope="PROVIDED" name="rubocop-rails (v2.15.2, RVM: ruby-2.7.6) [gem]" level="application" />
36
+ <orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.11.0, RVM: ruby-2.7.6) [gem]" level="application" />
37
+ <orderEntry type="library" scope="PROVIDED" name="securerandom (v0.2.0, RVM: ruby-2.7.6) [gem]" level="application" />
38
+ <orderEntry type="library" scope="PROVIDED" name="tzinfo (v2.0.5, RVM: ruby-2.7.6) [gem]" level="application" />
39
+ <orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v2.2.0, RVM: ruby-2.7.6) [gem]" level="application" />
40
+ <orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.6.0, RVM: ruby-2.7.6) [gem]" level="application" />
41
+ </component>
42
+ <component name="RakeTasksCache">
43
+ <option name="myRootTask">
44
+ <RakeTaskImpl id="rake">
45
+ <subtasks>
46
+ <RakeTaskImpl description="Build testa_logger-0.1.0.gem into the pkg directory" fullCommand="build" id="build" />
47
+ <RakeTaskImpl id="build">
48
+ <subtasks>
49
+ <RakeTaskImpl description="Generate SHA512 checksum if testa_logger-0.1.0.gem into the checksums directory" fullCommand="build:checksum" id="checksum" />
50
+ </subtasks>
51
+ </RakeTaskImpl>
52
+ <RakeTaskImpl description="Remove any temporary products" fullCommand="clean" id="clean" />
53
+ <RakeTaskImpl description="Remove any generated files" fullCommand="clobber" id="clobber" />
54
+ <RakeTaskImpl description="Build and install testa_logger-0.1.0.gem into system gems" fullCommand="install" id="install" />
55
+ <RakeTaskImpl id="install">
56
+ <subtasks>
57
+ <RakeTaskImpl description="Build and install testa_logger-0.1.0.gem into system gems without network access" fullCommand="install:local" id="local" />
58
+ </subtasks>
59
+ </RakeTaskImpl>
60
+ <RakeTaskImpl description="Create tag v0.1.0 and build and push testa_logger-0.1.0.gem to rubygems.org" fullCommand="release[remote]" id="release[remote]" />
61
+ <RakeTaskImpl description="Run tests" fullCommand="test" id="test" />
62
+ <RakeTaskImpl description="" fullCommand="default" id="default" />
63
+ <RakeTaskImpl description="" fullCommand="release" id="release" />
64
+ <RakeTaskImpl id="release">
65
+ <subtasks>
66
+ <RakeTaskImpl description="" fullCommand="release:guard_clean" id="guard_clean" />
67
+ <RakeTaskImpl description="" fullCommand="release:rubygem_push" id="rubygem_push" />
68
+ <RakeTaskImpl description="" fullCommand="release:source_control_push" id="source_control_push" />
69
+ </subtasks>
70
+ </RakeTaskImpl>
71
+ </subtasks>
72
+ </RakeTaskImpl>
73
+ </option>
74
+ </component>
75
+ </module>
@@ -0,0 +1,37 @@
1
+ require_relative 'lib/testa_logger/version'
2
+
3
+ Gem::Specification.new do |spec|
4
+ spec.name = "testa_logger"
5
+ spec.version = TestaLogger::VERSION
6
+ spec.authors = ["karlo.razumovic"]
7
+ spec.email = ["karlo.razumovic@gmail.com"]
8
+
9
+ spec.summary = %q{Customized logger}
10
+ spec.description = %q{Customized logger that can log while in trap context and broadcast logs to websocket}
11
+ # spec.homepage = ""
12
+ spec.license = "MIT"
13
+ spec.required_ruby_version = Gem::Requirement.new(">= 2.3.0")
14
+
15
+ # spec.metadata["allowed_push_host"] = "TODO: Set to 'http://mygemserver.com'"
16
+
17
+ # spec.metadata["homepage_uri"] = spec.homepage
18
+ # spec.metadata["source_code_uri"] = "TODO: Put your gem's public repo URL here."
19
+ # spec.metadata["changelog_uri"] = "TODO: Put your gem's CHANGELOG.md URL here."
20
+
21
+ spec.add_runtime_dependency "activesupport"
22
+ spec.add_runtime_dependency "awesome_print"
23
+ spec.add_runtime_dependency "aws-sdk-s3"
24
+ spec.add_runtime_dependency "concurrent-ruby"
25
+ spec.add_runtime_dependency "json"
26
+ spec.add_runtime_dependency "ostruct"
27
+ spec.add_runtime_dependency "securerandom"
28
+
29
+ # Specify which files should be added to the gem when it is released.
30
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
31
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
32
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
33
+ end
34
+ spec.bindir = "exe"
35
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
36
+ spec.require_paths = ["lib"]
37
+ end
metadata ADDED
@@ -0,0 +1,162 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: testa_logger
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - karlo.razumovic
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2022-08-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: activesupport
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: awesome_print
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: aws-sdk-s3
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: concurrent-ruby
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ - !ruby/object:Gem::Dependency
70
+ name: json
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '0'
76
+ type: :runtime
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '0'
83
+ - !ruby/object:Gem::Dependency
84
+ name: ostruct
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0'
90
+ type: :runtime
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0'
97
+ - !ruby/object:Gem::Dependency
98
+ name: securerandom
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '0'
104
+ type: :runtime
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '0'
111
+ description: Customized logger that can log while in trap context and broadcast logs
112
+ to websocket
113
+ email:
114
+ - karlo.razumovic@gmail.com
115
+ executables: []
116
+ extensions: []
117
+ extra_rdoc_files: []
118
+ files:
119
+ - ".gitignore"
120
+ - ".rubocop.yml"
121
+ - CODE_OF_CONDUCT.md
122
+ - Gemfile
123
+ - Gemfile.lock
124
+ - LICENSE.txt
125
+ - README.md
126
+ - Rakefile
127
+ - bin/console
128
+ - bin/setup
129
+ - lib/testa_logger.rb
130
+ - lib/testa_logger/io_persistence_error.rb
131
+ - lib/testa_logger/log_device.rb
132
+ - lib/testa_logger/log_device/period.rb
133
+ - lib/testa_logger/logger.rb
134
+ - lib/testa_logger/logger/dispatcher.rb
135
+ - lib/testa_logger/logger/persistence.rb
136
+ - lib/testa_logger/version.rb
137
+ - logger.iml
138
+ - testa_logger.gemspec
139
+ homepage:
140
+ licenses:
141
+ - MIT
142
+ metadata: {}
143
+ post_install_message:
144
+ rdoc_options: []
145
+ require_paths:
146
+ - lib
147
+ required_ruby_version: !ruby/object:Gem::Requirement
148
+ requirements:
149
+ - - ">="
150
+ - !ruby/object:Gem::Version
151
+ version: 2.3.0
152
+ required_rubygems_version: !ruby/object:Gem::Requirement
153
+ requirements:
154
+ - - ">="
155
+ - !ruby/object:Gem::Version
156
+ version: '0'
157
+ requirements: []
158
+ rubygems_version: 3.1.6
159
+ signing_key:
160
+ specification_version: 4
161
+ summary: Customized logger
162
+ test_files: []