puma-cloudwatch 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: fd707d05af913eccd60a30e1a17fde3c458c4fe89edb102ff7a29b483d65ac9d
4
+ data.tar.gz: 58f7896483eec60cde81ea9615d80fccf3dd63e001f80c63968f016fb0c10081
5
+ SHA512:
6
+ metadata.gz: '08add2c6446a55ce039e8dfe8d0e86f01fd05b61b63cbc3e71830757034fd9ae220ad79a6a79536b416137c48adf92eafb74eb0c455e81f1f286830c1dc1c24a'
7
+ data.tar.gz: d30b0f1d6e88f6923aec9067c3731ff7610d48a51d450320195612998dc476456119e5be861de4d58c1e6522f1f0bc6459616810d1f2af90d03b79818eac2fcd
data/.gitignore ADDED
@@ -0,0 +1,13 @@
1
+ /.bundle/
2
+ /.yardoc
3
+ /_yardoc/
4
+ /coverage/
5
+ /doc/
6
+ /pkg/
7
+ /spec/reports/
8
+ /tmp/
9
+
10
+ # rspec failure tracking
11
+ .rspec_status
12
+
13
+ /Gemfile.lock
data/.rspec ADDED
@@ -0,0 +1,3 @@
1
+ --format documentation
2
+ --color
3
+ --require spec_helper
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ sudo: false
3
+ language: ruby
4
+ cache: bundler
5
+ rvm:
6
+ - 2.5.6
7
+ before_install: gem install bundler -v 2.0.2
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source "https://rubygems.org"
2
+
3
+ # Specify your gem's dependencies in puma_cloudwatch.gemspec
4
+ gemspec
data/LICENSE.txt ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2019 Tung Nguyen
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,66 @@
1
+ # Puma Cloudwatch Puma
2
+
3
+ A [puma](https://puma.io) plugin that sends puma stats to CloudWatch.
4
+
5
+ ## Usage
6
+
7
+ You should configure the `PUMA_CLOUDWATCH_DIMENSION_VALUE` env variable to include your application name.
8
+ For example, if you're application is named "myapp", this would be a good value to use:
9
+
10
+ PUMA_CLOUDWATCH_DIMENSION_VALUE=myapp-puma
11
+
12
+ Then you can get metrics for your `myapp-puma` app. List of metrics:
13
+
14
+ * pool_capacity: the number of requests that the server is capable of taking right now.
15
+ * max_threads: preconfigured maximum number of worker threads.
16
+ * running: the number of running threads (spawned threads) for any Puma worker.
17
+ * backlog: the number of connections in that worker's "todo" set waiting for a worker thread.
18
+
19
+ The `pool_capacity` metric is important. It shows how busy the server is getting before it reaches capacity.
20
+
21
+ ### Environment Variables
22
+
23
+ The plugin's settings can be controlled with environmental variables:
24
+
25
+ Env Var | Description | Default Value
26
+ --- | --- | ---
27
+ PUMA\_CLOUDWATCH\_NAMESPACE | CloudWatch metric namespace | WebServer
28
+ PUMA\_CLOUDWATCH\_DIMENSION\_NAME | CloudWatch metric dimension name | App
29
+ PUMA\_CLOUDWATCH\_DIMENSION\_VALUE | CloudWatch metric dimension value | puma
30
+ PUMA\_CLOUDWATCH\_FREQUENCY | How often to send data to CloudWatch in seconds. | 60
31
+ PUMA\_CLOUDWATCH\_NOOP | When set, the plugin prints out the metrics instead of sending them to cloudwatch. | (unset)
32
+
33
+ ### Sum and Frequency
34
+
35
+ If you leave the `PUMA_CLOUDWATCH_FREQUENCY` at its default of 60 seconds and graph out the `pool_capacity` capacity with a 1-minute period resolution, then the CloudWatch Sum statistic is useful. It shows how busy all the `myapp-puma` servers are. Particularly, the `pool_capacity` shows available capacity, and `pool_threads` shows the total threads.
36
+
37
+ **Important**: If you change the CloudWatch send frequency, then Sum statistic must be normalized. For example, let's say you use `PUMA_CLOUDWATCH_FREQUENCY=30`. Then puma-cloudwatch will send data every 30s. However, if the chart is still using a 1-minute period, then the Sum statistic would "double". Capacity has not doubled, puma-cloudwatch is just sending twice as much data. To normalize the Sum, set the time period resolution to match the frequency. In this case: 30 seconds.
38
+
39
+ If you use the Average statistic, then you don't have to worry about normalizing. Average already inherently normalized.
40
+
41
+ ## Installation
42
+
43
+ Add this line to your application's Gemfile:
44
+
45
+ ```ruby
46
+ gem 'puma-cloudwatch'
47
+ ```
48
+
49
+ And then execute:
50
+
51
+ $ bundle
52
+
53
+ In your `config/puma.rb`
54
+
55
+ Add these 2 lines your `config/puma.rb`:
56
+
57
+ ```ruby
58
+ activate_control_app
59
+ plugin :cloudwatch
60
+ ```
61
+
62
+ It activates the puma control rack application, and enables the puma-cloudwatch plugin to send metrics.
63
+
64
+ ## Contributing
65
+
66
+ Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/puma_cloudwatch.
data/Rakefile ADDED
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
data/bin/console ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require "bundler/setup"
4
+ require "puma_cloudwatch"
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,7 @@
1
+ require 'puma-cloudwatch'
2
+
3
+ Puma::Plugin.create do
4
+ def start(launcher)
5
+ PumaCloudwatch::Metrics.start_sending(launcher)
6
+ end
7
+ end
@@ -0,0 +1 @@
1
+ require_relative "puma_cloudwatch"
@@ -0,0 +1,39 @@
1
+ require "json"
2
+ require "socket"
3
+
4
+ class PumaCloudwatch::Metrics
5
+ class Fetcher
6
+ def initialize(options={})
7
+ @control_url = options[:control_url]
8
+ @control_auth_token = options[:control_auth_token]
9
+ end
10
+
11
+ def call
12
+ body = with_retries do
13
+ read_socket
14
+ end
15
+ JSON.parse(body.split("\n").last) # stats
16
+ end
17
+
18
+ private
19
+ def read_socket
20
+ Socket.unix(@control_url.gsub('unix://', '')) do |socket|
21
+ socket.print("GET /stats?token=#{@control_auth_token} HTTP/1.0\r\n\r\n")
22
+ socket.read
23
+ end
24
+ end
25
+
26
+ def with_retries
27
+ retries, max_attempts = 0, 10
28
+ yield
29
+ rescue Errno::ENOENT => e
30
+ retries += 1
31
+ if retries > max_attempts
32
+ raise e
33
+ end
34
+ puts "retries #{retries} #{e.class} #{e.message}" if ENV['PUMA_CLOUDWATCH_SOCKET_RETRY']
35
+ sleep 1
36
+ retry
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,32 @@
1
+ class PumaCloudwatch::Metrics
2
+ class Looper
3
+ def self.run(options)
4
+ new(options).run
5
+ end
6
+
7
+ def initialize(options)
8
+ @options = options
9
+ @control_url = options[:control_url]
10
+ @control_auth_token = options[:control_auth_token]
11
+ @frequency = Integer(ENV['PUMA_CLOUDWATCH_FREQUENCY'] || 60)
12
+ end
13
+
14
+ def run
15
+ raise StandardError, "Puma control app is not activated" if @control_url == nil
16
+ puts "Sending metrics to CloudWatch..."
17
+ Thread.new do
18
+ perform
19
+ end
20
+ end
21
+
22
+ def perform
23
+ puts "puma-cloudwatch plugin: Will send data every #{@frequency} seconds."
24
+ loop do
25
+ stats = Fetcher.new(@options).call
26
+ results = Parser.new(stats).call
27
+ Sender.new(results).call
28
+ sleep @frequency
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,44 @@
1
+ class PumaCloudwatch::Metrics
2
+ class Parser
3
+ METRICS = [:backlog, :running, :pool_capacity, :max_threads]
4
+
5
+ def initialize(data)
6
+ @data = data
7
+ end
8
+
9
+ def call
10
+ Array.new.tap { |result| parse(@data, result) }
11
+ end
12
+
13
+ private
14
+ # Build this structure:
15
+ #
16
+ # [{:backlog=>[0, 0],
17
+ # :running=>[0, 0],
18
+ # :pool_capacity=>[16, 16],
19
+ # :max_threads=>[16, 16]}]
20
+ #
21
+ def parse(stats, result)
22
+ item = Hash.new([])
23
+
24
+ clustered = stats.key?("worker_status")
25
+ if clustered
26
+ statuses = stats["worker_status"].map { |s| s["last_status"] } # last_status: Array with worker stats
27
+ statuses.each do |status|
28
+ METRICS.each do |metric|
29
+ count = status[metric.to_s]
30
+ item[metric] += [count] if count
31
+ end
32
+ end
33
+ else # single mode
34
+ METRICS.each do |metric|
35
+ count = stats[metric.to_s]
36
+ item[metric] += [count] if count
37
+ end
38
+ end
39
+
40
+ result << item unless item.empty?
41
+ result
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,94 @@
1
+ require "aws-sdk-cloudwatch"
2
+
3
+ # It probably makes sense to configure PUMA_CLOUDWATCH_DIMENSION_VALUE to include your application name.
4
+ # For example if you're application is named "myapp", this would be a good value to use:
5
+ #
6
+ # PUMA_CLOUDWATCH_DIMENSION_VALUE=myapp-puma
7
+ #
8
+ # Then you can get all the metrics for the pool_capacity for your myapp-puma app.
9
+ #
10
+ # Summing the metric tells you the total available pool_capacity for the myapp-puma app.
11
+ #
12
+ class PumaCloudwatch::Metrics
13
+ class Sender
14
+ def initialize(metrics)
15
+ @metrics = metrics
16
+ @namespace = ENV['PUMA_CLOUDWATCH_NAMESPACE'] || "WebServer"
17
+ @dimension_name = ENV['PUMA_CLOUDWATCH_DIMENSION_NAME'] || "App"
18
+ @dimension_value = ENV['PUMA_CLOUDWATCH_DIMENSION_VALUE'] || "puma" # IE: myapp-puma
19
+ end
20
+
21
+ def call
22
+ put_metric_data(
23
+ namespace: @namespace,
24
+ metric_data: metric_data,
25
+ )
26
+ end
27
+
28
+ # Input @metrics example:
29
+ #
30
+ # [{:backlog=>[0, 0],
31
+ # :running=>[0, 0],
32
+ # :pool_capacity=>[16, 16],
33
+ # :max_threads=>[16, 16]}]
34
+ #
35
+ # Output example:
36
+ #
37
+ # [{:metric_name=>"backlog",
38
+ # :statistic_values=>{:sample_count=>2, :sum=>0, :minimum=>0, :maximum=>0}},
39
+ # {:metric_name=>"running",
40
+ # :statistic_values=>{:sample_count=>2, :sum=>0, :minimum=>0, :maximum=>0}},
41
+ # {:metric_name=>"pool_capacity",
42
+ # :statistic_values=>{:sample_count=>2, :sum=>32, :minimum=>16, :maximum=>16}},
43
+ # {:metric_name=>"max_threads",
44
+ # :statistic_values=>{:sample_count=>2, :sum=>32, :minimum=>16, :maximum=>16}}]
45
+ #
46
+ # Resources:
47
+ # pool_capcity and max_threads are the important metrics
48
+ # https://dev.to/amplifr/monitoring-puma-web-server-with-prometheus-and-grafana-5b5o
49
+ #
50
+ def metric_data
51
+ data = []
52
+ @metrics.each do |metric|
53
+ metric.each do |name, values|
54
+ data << {
55
+ metric_name: name.to_s,
56
+ dimensions: dimensions,
57
+ statistic_values: {
58
+ sample_count: values.length,
59
+ sum: values.sum,
60
+ minimum: values.min,
61
+ maximum: values.max
62
+ }
63
+ }
64
+ end
65
+ end
66
+ data
67
+ end
68
+
69
+ def dimensions
70
+ [
71
+ name: @dimension_name,
72
+ value: @dimension_value
73
+ ]
74
+ end
75
+
76
+ private
77
+ def put_metric_data(params)
78
+ if noop?
79
+ puts "NOOP: sending data to cloudwatch:"
80
+ pp params
81
+ else
82
+ cloudwatch.put_metric_data(params)
83
+ end
84
+ end
85
+
86
+ def noop?
87
+ !ENV["PUMA_CLOUDWATCH_NOOP"].nil?
88
+ end
89
+
90
+ def cloudwatch
91
+ @cloudwatch ||= Aws::CloudWatch::Client.new
92
+ end
93
+ end
94
+ end
@@ -0,0 +1,20 @@
1
+ module PumaCloudwatch
2
+ class Metrics
3
+ autoload :Fetcher, "puma_cloudwatch/metrics/fetcher"
4
+ autoload :Looper, "puma_cloudwatch/metrics/looper"
5
+ autoload :Parser, "puma_cloudwatch/metrics/parser"
6
+ autoload :Sender, "puma_cloudwatch/metrics/sender"
7
+
8
+ def self.start_sending(launcher)
9
+ new(launcher).start_sending
10
+ end
11
+
12
+ def initialize(launcher)
13
+ @launcher = launcher
14
+ end
15
+
16
+ def start_sending
17
+ Looper.run(@launcher.options)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,3 @@
1
+ module PumaCloudwatch
2
+ VERSION = "0.1.0"
3
+ end
@@ -0,0 +1,7 @@
1
+ require "puma_cloudwatch/version"
2
+
3
+ module PumaCloudwatch
4
+ class Error < StandardError; end
5
+
6
+ autoload :Metrics, "puma_cloudwatch/metrics"
7
+ end
@@ -0,0 +1,31 @@
1
+ lib = File.expand_path("lib", __dir__)
2
+ $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
+ require "puma_cloudwatch/version"
4
+
5
+ Gem::Specification.new do |spec|
6
+ spec.name = "puma-cloudwatch"
7
+ spec.version = PumaCloudwatch::VERSION
8
+ spec.authors = ["Tung Nguyen"]
9
+ spec.email = ["tongueroo@gmail.com"]
10
+
11
+ spec.summary = "Puma plugin sends puma stats to CloudWatch"
12
+ spec.homepage = "https://github.com/tongueroo/puma-cloudwatch"
13
+ spec.license = "MIT"
14
+
15
+ spec.metadata["homepage_uri"] = spec.homepage
16
+
17
+ # Specify which files should be added to the gem when it is released.
18
+ # The `git ls-files -z` loads the files in the RubyGem that have been added into git.
19
+ spec.files = Dir.chdir(File.expand_path('..', __FILE__)) do
20
+ `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
21
+ end
22
+ spec.bindir = "exe"
23
+ spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
24
+ spec.require_paths = ["lib"]
25
+
26
+ spec.add_dependency "aws-sdk-cloudwatch"
27
+
28
+ spec.add_development_dependency "bundler", "~> 2.0"
29
+ spec.add_development_dependency "rake", "~> 10.0"
30
+ spec.add_development_dependency "rspec", "~> 3.0"
31
+ end
metadata ADDED
@@ -0,0 +1,119 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: puma-cloudwatch
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - Tung Nguyen
8
+ autorequire:
9
+ bindir: exe
10
+ cert_chain: []
11
+ date: 2019-09-17 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: aws-sdk-cloudwatch
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: bundler
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '2.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '2.0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - "~>"
46
+ - !ruby/object:Gem::Version
47
+ version: '10.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - "~>"
53
+ - !ruby/object:Gem::Version
54
+ version: '10.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - "~>"
60
+ - !ruby/object:Gem::Version
61
+ version: '3.0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - "~>"
67
+ - !ruby/object:Gem::Version
68
+ version: '3.0'
69
+ description:
70
+ email:
71
+ - tongueroo@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - ".gitignore"
77
+ - ".rspec"
78
+ - ".travis.yml"
79
+ - Gemfile
80
+ - LICENSE.txt
81
+ - README.md
82
+ - Rakefile
83
+ - bin/console
84
+ - bin/setup
85
+ - lib/puma-cloudwatch.rb
86
+ - lib/puma/plugin/cloudwatch.rb
87
+ - lib/puma_cloudwatch.rb
88
+ - lib/puma_cloudwatch/metrics.rb
89
+ - lib/puma_cloudwatch/metrics/fetcher.rb
90
+ - lib/puma_cloudwatch/metrics/looper.rb
91
+ - lib/puma_cloudwatch/metrics/parser.rb
92
+ - lib/puma_cloudwatch/metrics/sender.rb
93
+ - lib/puma_cloudwatch/version.rb
94
+ - puma-cloudwatch.gemspec
95
+ homepage: https://github.com/tongueroo/puma-cloudwatch
96
+ licenses:
97
+ - MIT
98
+ metadata:
99
+ homepage_uri: https://github.com/tongueroo/puma-cloudwatch
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: '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.0.6
116
+ signing_key:
117
+ specification_version: 4
118
+ summary: Puma plugin sends puma stats to CloudWatch
119
+ test_files: []