cwlogs_io 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: e76cb59bec88c77636194c3a5fe6c45cd14a306cdf2ad8622ba834b591ed7cd0
4
+ data.tar.gz: 11f88c626fff3efc54171baddc89c317746e0ec60d830ac329378744805f0deb
5
+ SHA512:
6
+ metadata.gz: 841c88a803b94f77ea5bbdc6c08517bc00ce0316c26c50c41e9e57d53e6aa666be1bb17433b8571a0ddc5c1f32ab85f02ee3b75715ac2fe337568bfd4e2afc82
7
+ data.tar.gz: 11893c237bd7484ee82fd219a22d1414620151b8072470312df95f28a6033603e6110a32f1380a88b944b5a6cec16709a54c0da6804bef61fccdba19e7f329e5
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.rufo ADDED
@@ -0,0 +1,5 @@
1
+ align_case_when false
2
+ align_chained_calls false
3
+ parens_in_def :yes
4
+ trailing_commas true
5
+ quote_style :single
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ source 'https://rubygems.org'
4
+
5
+ # Specify your gem's dependencies in cwlogs_io.gemspec
6
+ gemspec
7
+
8
+ gem 'rake', '~> 13.0'
9
+
10
+ gem 'rspec', '~> 3.0'
data/Gemfile.lock ADDED
@@ -0,0 +1,55 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ cwlogs_io (0.1.0)
5
+ aws-sdk-cloudwatchlogs (~> 1.69)
6
+ aws-sdk-core (~> 3)
7
+ concurrent-ruby (~> 1.2)
8
+ nokogiri (~> 1.15)
9
+
10
+ GEM
11
+ remote: https://rubygems.org/
12
+ specs:
13
+ aws-eventstream (1.2.0)
14
+ aws-partitions (1.785.0)
15
+ aws-sdk-cloudwatchlogs (1.69.0)
16
+ aws-sdk-core (~> 3, >= 3.177.0)
17
+ aws-sigv4 (~> 1.1)
18
+ aws-sdk-core (3.178.0)
19
+ aws-eventstream (~> 1, >= 1.0.2)
20
+ aws-partitions (~> 1, >= 1.651.0)
21
+ aws-sigv4 (~> 1.5)
22
+ jmespath (~> 1, >= 1.6.1)
23
+ aws-sigv4 (1.6.0)
24
+ aws-eventstream (~> 1, >= 1.0.2)
25
+ concurrent-ruby (1.2.2)
26
+ diff-lcs (1.5.0)
27
+ jmespath (1.6.2)
28
+ nokogiri (1.15.3-x86_64-linux)
29
+ racc (~> 1.4)
30
+ racc (1.7.1)
31
+ rake (13.0.6)
32
+ rspec (3.12.0)
33
+ rspec-core (~> 3.12.0)
34
+ rspec-expectations (~> 3.12.0)
35
+ rspec-mocks (~> 3.12.0)
36
+ rspec-core (3.12.2)
37
+ rspec-support (~> 3.12.0)
38
+ rspec-expectations (3.12.3)
39
+ diff-lcs (>= 1.2.0, < 2.0)
40
+ rspec-support (~> 3.12.0)
41
+ rspec-mocks (3.12.6)
42
+ diff-lcs (>= 1.2.0, < 2.0)
43
+ rspec-support (~> 3.12.0)
44
+ rspec-support (3.12.1)
45
+
46
+ PLATFORMS
47
+ x86_64-linux
48
+
49
+ DEPENDENCIES
50
+ cwlogs_io!
51
+ rake (~> 13.0)
52
+ rspec (~> 3.0)
53
+
54
+ BUNDLED WITH
55
+ 2.3.13
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2023 Hyeonjun Lee
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,43 @@
1
+ # CWlogsIO
2
+
3
+ ruby IO-like streams which is connected to cloudwatch logs.
4
+
5
+ ## Installation
6
+
7
+ Install the gem and add to the application's Gemfile by executing:
8
+
9
+ $ bundle add cwlogs_io
10
+
11
+ If bundler is not being used to manage dependencies, install the gem by executing:
12
+
13
+ $ gem install cwlogs_io
14
+
15
+ ## Usage
16
+
17
+ ```ruby
18
+
19
+ auth = {
20
+ region: 'ap-northeast-2'
21
+ credentials: Aws::InstanceProfileCredentials.new
22
+ }
23
+
24
+ log_group = 'foo'
25
+ log_stream = 'bar'
26
+
27
+ logger = Logger.new(CWlogsIO.new(auth, log_group, log_stream))
28
+ logger.info('hello with CWlogsIO')
29
+ ```
30
+
31
+ ## Development
32
+
33
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
34
+
35
+ 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 the created tag, and push the `.gem` file to [rubygems.org](https://rubygems.org).
36
+
37
+ ## Contributing
38
+
39
+ Bug reports and pull requests are welcome on GitHub at https://github.com/privatenote/cwlogs_io.
40
+
41
+ ## License
42
+
43
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'rspec/core/rake_task'
5
+
6
+ RSpec::Core::RakeTask.new(:spec)
7
+
8
+ task default: :spec
data/cwlogs_io.gemspec ADDED
@@ -0,0 +1,38 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'lib/cwlogs_io/version'
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = 'cwlogs_io'
7
+ spec.version = CWlogsIO::VERSION
8
+ spec.authors = ['Hyeonjun Lee']
9
+ spec.email = ['hyeonjun.lee@privatenote.co.kr']
10
+
11
+ spec.summary = 'IO-like streams for CloudWatch Logs.'
12
+ spec.homepage = 'https://github.com/privatenote/cwlogs_io'
13
+ spec.license = 'MIT'
14
+ spec.required_ruby_version = '>= 2.6.0'
15
+
16
+ spec.metadata['homepage_uri'] = spec.homepage
17
+ spec.metadata['source_code_uri'] = 'https://github.com/privatenote/cwlogs_io'
18
+
19
+ # Specify which files should be added to the gem when it is released.
20
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
21
+ spec.files = Dir.chdir(__dir__) do
22
+ `git ls-files -z`.split("\x0").reject do |f|
23
+ (f == __FILE__) || f.match(%r{\A(?:(?:bin|test|spec|features)/|\.(?:git|travis|circleci)|appveyor)})
24
+ end
25
+ end
26
+ spec.bindir = 'exe'
27
+ spec.executables = spec.files.grep(%r{\Aexe/}) { |f| File.basename(f) }
28
+ spec.require_paths = ['lib']
29
+
30
+ # Uncomment to register a new dependency of your gem
31
+ spec.add_dependency 'aws-sdk-cloudwatchlogs', '~> 1.69'
32
+ spec.add_dependency 'aws-sdk-core', '~> 3'
33
+ spec.add_dependency 'concurrent-ruby', '~> 1.2'
34
+ spec.add_dependency 'nokogiri', '~> 1.15'
35
+
36
+ # For more information and examples about making a new gem, check out our
37
+ # guide at: https://bundler.io/guides/creating_gem.html
38
+ end
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-cloudwatchlogs'
4
+
5
+ module CWlogsIO
6
+ class Client
7
+ def initialize(auth, log_group, log_stream)
8
+ @auth = auth
9
+ @log_group = log_group
10
+ @log_stream = log_stream
11
+ end
12
+
13
+ def create_log_group
14
+ client.create_log_group({ log_group_name: @log_group })
15
+ end
16
+
17
+ def create_log_stream
18
+ client.create_log_stream(
19
+ {
20
+ log_group_name: @log_group,
21
+ log_stream_name: @log_stream,
22
+ }
23
+ )
24
+ end
25
+
26
+ def put_log_events(log_events)
27
+ client.put_log_events(
28
+ {
29
+ log_events: to_params(log_events),
30
+ log_group_name: @log_group,
31
+ log_stream_name: @log_stream,
32
+ }
33
+ )
34
+ end
35
+
36
+ private
37
+
38
+ def client
39
+ @client ||= Aws::CloudWatchLogs::Client.new(
40
+ region: @auth[:region],
41
+ credentials: @auth[:credentials],
42
+ )
43
+ end
44
+
45
+ def to_params(events)
46
+ events.map do |event|
47
+ {
48
+ message: event.message,
49
+ timestamp: (event.timestamp.to_f * 1000).to_i
50
+ }
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,139 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'set'
4
+ require 'singleton'
5
+ require 'concurrent/executor/single_thread_executor'
6
+
7
+ module CWlogsIO
8
+ class HandlerWrapper
9
+ def initialize(handler)
10
+ @handler = handler
11
+ end
12
+
13
+ def self.create(handler_class, *params)
14
+ handler = HandlerWrapper.new(handler_class.new(*params))
15
+ HandlerManager.instance.register(handler)
16
+ handler
17
+ end
18
+
19
+ def close
20
+ @handler.close
21
+ HandlerManager.instance.deregister(self)
22
+ end
23
+
24
+ def method_missing(method, *params)
25
+ @handler.send(method, *params)
26
+ end
27
+
28
+ def respond_to_missing?(method, include_private = false)
29
+ @handler.respond_to?(method, include_private)
30
+ end
31
+ end
32
+
33
+ class HandlerManager
34
+ include Singleton
35
+
36
+ def initialize
37
+ @handlers = Set.new
38
+ end
39
+
40
+ def respawn_all
41
+ @handlers.each(&:respawn)
42
+ end
43
+
44
+ def register(handler)
45
+ @handlers << handler
46
+ end
47
+
48
+ def deregister(handler)
49
+ @handlers.delete(handler)
50
+ end
51
+ end
52
+
53
+ class Handler
54
+ DEFAULT_POLLING_PERIOD_IN_SECONDS = 1
55
+ TERMINATION_TIMEOUT_IN_SECONDS = 5
56
+
57
+ def initialize(logger, polling_period = nil)
58
+ @queue = Queue.new
59
+ @logger = logger
60
+ @polling_period = polling_period || DEFAULT_POLLING_PERIOD_IN_SECONDS
61
+
62
+ initialize_executor
63
+ end
64
+
65
+ def enq(event)
66
+ return if closed?
67
+
68
+ @queue << event
69
+ end
70
+
71
+ def <<(event)
72
+ enq(event)
73
+ end
74
+
75
+ def close
76
+ return if closed?
77
+
78
+ @queue.close
79
+
80
+ shutdown_executor
81
+ end
82
+
83
+ def closed?
84
+ @queue.closed?
85
+ end
86
+
87
+ # should be called in child process, not parent.
88
+ # or, you may lose some events.
89
+ def respawn
90
+ return if closed?
91
+
92
+ @queue.clear
93
+ initialize_executor
94
+ end
95
+
96
+ private
97
+
98
+ def initialize_executor
99
+ @executor = Concurrent::SingleThreadExecutor.new
100
+ @executor.post do
101
+ serve
102
+ end
103
+ end
104
+
105
+ def shutdown_executor
106
+ @executor.shutdown
107
+ @executor.wait_for_termination(TERMINATION_TIMEOUT_IN_SECONDS)
108
+ @executor.kill unless @executor.shutdown?
109
+ end
110
+
111
+ def serve
112
+ @logger.info('ready to process')
113
+ until @queue.closed?
114
+ handle_all
115
+
116
+ sleep @polling_period
117
+ end
118
+
119
+ handle_all unless @queue.empty?
120
+ @logger.info('quitting...')
121
+ rescue Exception => e
122
+ @logger.error(e)
123
+ end
124
+
125
+ def pop_all_events
126
+ result = []
127
+ result << @queue.pop until @queue.empty?
128
+ result
129
+ end
130
+
131
+ def handle_all
132
+ process_events(pop_all_events) unless @queue.empty?
133
+ end
134
+
135
+ def process_events(events)
136
+ raise NotImplementedError
137
+ end
138
+ end
139
+ end
@@ -0,0 +1,100 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'aws-sdk-cloudwatchlogs'
4
+ require 'json/ext'
5
+ require 'logger'
6
+ require_relative 'log_event'
7
+ require_relative 'utils'
8
+ require_relative 'handler'
9
+ require_relative 'client'
10
+
11
+ module CWlogsIO
12
+ class LogEventHandler < Handler
13
+ # https://docs.aws.amazon.com/AmazonCloudWatchLogs/latest/APIReference/API_PutLogEvents.html
14
+ # The maximum batch size is 1,048,576 bytes, and this size is calculated as the sum of all event messages in UTF-8, plus 26 bytes for each log event.
15
+ # 26 bytes is the length of the JSON structure that surrounds the log event information:
16
+ # Each log event can be no larger than 256 KB.
17
+ # The maximum number of log events in a batch is 10,000
18
+ # When there are 100 events in one chunk, the average size of each event can be up to a maximum of 10kB.
19
+ EVENTS_PER_CHUNK = 100
20
+
21
+ def initialize(client, log_group, log_stream, logger, polling_rate = nil)
22
+ super(logger, polling_rate)
23
+ @client = client
24
+ @log_group = log_group
25
+ @log_stream = log_stream
26
+ end
27
+
28
+ def process_events(events)
29
+ events.sort_by!(&:timestamp)
30
+ Utils.chunks(events, EVENTS_PER_CHUNK).each do |event_chunk|
31
+ send_events(event_chunk)
32
+ end
33
+ end
34
+
35
+ private
36
+
37
+ def send_events(events)
38
+ return if events.empty?
39
+
40
+ @client.put_log_events(events)
41
+ rescue StandardError => e
42
+ @logger.error(e)
43
+ end
44
+ end
45
+
46
+ # ruby 'logger' compatible IO-like class.
47
+ # it should implement #write, #close method.
48
+ # https://github.com/ruby/logger/blob/master/lib/logger/log_device.rb
49
+ class IO
50
+ def initialize(auth, log_group, log_stream)
51
+ @auth = auth
52
+ @log_group = log_group
53
+ @log_stream = log_stream
54
+ @handler = HandlerWrapper.create(LogEventHandler, client, log_group, log_stream, logger)
55
+ close if !ensure_log_group || !ensure_log_stream
56
+
57
+ at_exit do
58
+ close
59
+ end
60
+ end
61
+
62
+ def write(message)
63
+ @handler << LogEvent.new(message, Time.now)
64
+ end
65
+
66
+ def close
67
+ @handler.close
68
+ end
69
+
70
+ private
71
+
72
+ def client
73
+ @client ||= Client.new(@auth, @log_group, @log_stream)
74
+ end
75
+
76
+ def ensure_log_group
77
+ client.create_log_group
78
+ true
79
+ rescue Aws::CloudWatchLogs::Errors::ResourceAlreadyExistsException
80
+ true
81
+ rescue Aws::CloudWatchLogs::Errors::ServiceError => e
82
+ logger.error(e)
83
+ false
84
+ end
85
+
86
+ def ensure_log_stream
87
+ client.create_log_stream
88
+ true
89
+ rescue Aws::CloudWatchLogs::Errors::ResourceAlreadyExistsException
90
+ true
91
+ rescue Aws::CloudWatchLogs::Errors::ServiceError => e
92
+ logger.error(e)
93
+ false
94
+ end
95
+
96
+ def logger
97
+ @@logger ||= Logger.new($stdout)
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CWlogsIO
4
+ class LogEvent
5
+ attr_reader :message, :timestamp
6
+
7
+ def initialize(message, timestamp)
8
+ @message = message
9
+ @timestamp = timestamp
10
+ end
11
+ end
12
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CWlogsIO
4
+ module Utils
5
+ def self.chunks(array, size)
6
+ array.each_slice(size)
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module CWlogsIO
4
+ VERSION = '0.1.0'
5
+ end
data/lib/cwlogs_io.rb ADDED
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'cwlogs_io/version'
4
+ require_relative 'cwlogs_io/io'
5
+ require_relative 'hooks'
6
+
7
+ module CWlogsIO
8
+ def self.new(auth, log_group, log_stream)
9
+ CWlogsIO::IO.new(auth, log_group, log_stream)
10
+ end
11
+ end
@@ -0,0 +1,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ if defined?(PhusionPassenger)
4
+ PhusionPassenger.on_event(:starting_worker_process) do |forked|
5
+ CWlogsIO::HandlerManager.instance.respawn_all if forked
6
+ end
7
+ end
data/lib/hooks.rb ADDED
@@ -0,0 +1 @@
1
+ require_relative 'hooks/phusion_passenger'
data/sig/cwlogs_io.rbs ADDED
@@ -0,0 +1,4 @@
1
+ module CwlogsIo
2
+ VERSION: String
3
+ # See the writing guide of rbs: https://github.com/ruby/rbs#guides
4
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: cwlogs_io
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Hyeonjun Lee
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2023-07-24 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-cloudwatchlogs
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '1.69'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '1.69'
27
+ - !ruby/object:Gem::Dependency
28
+ name: aws-sdk-core
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '3'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '3'
41
+ - !ruby/object:Gem::Dependency
42
+ name: concurrent-ruby
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '1.2'
48
+ type: :runtime
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '1.2'
55
+ - !ruby/object:Gem::Dependency
56
+ name: nokogiri
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '1.15'
62
+ type: :runtime
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '1.15'
69
+ description:
70
+ email:
71
+ - hyeonjun.lee@privatenote.co.kr
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".rspec"
77
+ - ".rufo"
78
+ - Gemfile
79
+ - Gemfile.lock
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - cwlogs_io.gemspec
84
+ - lib/cwlogs_io.rb
85
+ - lib/cwlogs_io/client.rb
86
+ - lib/cwlogs_io/handler.rb
87
+ - lib/cwlogs_io/io.rb
88
+ - lib/cwlogs_io/log_event.rb
89
+ - lib/cwlogs_io/utils.rb
90
+ - lib/cwlogs_io/version.rb
91
+ - lib/hooks.rb
92
+ - lib/hooks/phusion_passenger.rb
93
+ - sig/cwlogs_io.rbs
94
+ homepage: https://github.com/privatenote/cwlogs_io
95
+ licenses:
96
+ - MIT
97
+ metadata:
98
+ homepage_uri: https://github.com/privatenote/cwlogs_io
99
+ source_code_uri: https://github.com/privatenote/cwlogs_io
100
+ post_install_message:
101
+ rdoc_options: []
102
+ require_paths:
103
+ - lib
104
+ required_ruby_version: !ruby/object:Gem::Requirement
105
+ requirements:
106
+ - - ">="
107
+ - !ruby/object:Gem::Version
108
+ version: 2.6.0
109
+ required_rubygems_version: !ruby/object:Gem::Requirement
110
+ requirements:
111
+ - - ">="
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ requirements: []
115
+ rubygems_version: 3.3.7
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: IO-like streams for CloudWatch Logs.
119
+ test_files: []