barnes 0.0.9 → 1.0.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bb1cb2f789371b7caa0a50293ea6c8d37c0b4c1aa2592534c57e755855c691d5
4
- data.tar.gz: 14679f30d7d411d72e1abf22cf94549a3263455d0a220b2a4164f8f65f65cab9
3
+ metadata.gz: a6cb33851e3d78680af4ac681b92eacc508b2b265fa6a2ea263e711b6676d5e6
4
+ data.tar.gz: e0d7596baff290abae04eeae0a10e4ac61a3843a145251ea26b7e22050de8f48
5
5
  SHA512:
6
- metadata.gz: 2e7a031bb6c3174cc480eefb58c6ca0a490056c81db1ed22cace24222ed98f232a6f5c3ae9f8ac7ee8bb988f6d8030612dcc41978308fea190d248bf90a00c9f
7
- data.tar.gz: 598f97d4d7f4ea575bf4bc87b5398eab87aff4a4237ab11dbfa3955a86df75d1b1477a6c7089790b1a5f8d95417789439c8a977556d1a9fd18a51151eef88414
6
+ metadata.gz: 11565b5d7196d00ba28d8d616ba5c3417f6b68851fd20b567432578e14779cf8c99af4a27f816fff708f192999330ede8ee18020af0ba57137e641cf878c7bda
7
+ data.tar.gz: 6d18ba6866a60f201bce1074a2bd5ce5486d697efd195e8191cce518169541484c9fa2806a389691c6894baaf223376bfb9f6811a8462513a6407b9c0f0df104
data/.github/CODEOWNERS CHANGED
@@ -1 +1,3 @@
1
+ #ECCN:Open Source
2
+ #GUSINFO:Heroku - Languages,Heroku Ruby Platform
1
3
  * @heroku/languages
@@ -0,0 +1,15 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: "bundler"
4
+ directory: "/"
5
+ schedule:
6
+ interval: "monthly"
7
+ groups:
8
+ ruby-dependencies:
9
+ update-types:
10
+ - "minor"
11
+ - "patch"
12
+ - package-ecosystem: "github-actions"
13
+ directory: "/"
14
+ schedule:
15
+ interval: "monthly"
@@ -1,14 +1,20 @@
1
1
  name: Check Changelog
2
2
 
3
3
  on:
4
- pull_request:
5
- types: [opened, reopened, edited, synchronize]
4
+ pull_request:
5
+ types: [opened, reopened, labeled, unlabeled, synchronize]
6
+
7
+ permissions:
8
+ contents: read
6
9
 
7
10
  jobs:
8
- build:
11
+ check-changelog:
9
12
  runs-on: ubuntu-latest
13
+ if: (!contains(github.event.pull_request.labels.*.name, 'skip changelog'))
10
14
  steps:
11
- - uses: actions/checkout@v1
12
- - name: Check that CHANGELOG is touched
13
- run: |
14
- cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
15
+ - name: Checkout
16
+ uses: actions/checkout@v6
17
+ - name: Check that CHANGELOG is touched
18
+ run: |
19
+ git fetch origin ${{ github.base_ref }} --depth 1 && \
20
+ git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
@@ -0,0 +1,16 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ jobs:
5
+ test:
6
+ runs-on: ubuntu-latest
7
+ strategy:
8
+ matrix:
9
+ ruby-version: ['4.0', '3.4', '3.3', '3.2', '3.1']
10
+ steps:
11
+ - uses: actions/checkout@v6
12
+ - uses: ruby/setup-ruby@v1
13
+ with:
14
+ ruby-version: ${{ matrix.ruby-version }}
15
+ bundler-cache: true
16
+ - run: bundle exec rake
data/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  ## HEAD (unreleased)
2
2
 
3
+ ## 1.0.0
4
+
5
+ - **Breaking**: Replace StatsD with direct HTTP reporting to HEROKU_METRICS_URL
6
+ - **Breaking**: Remove `statsd:` and `aggregation_period:` parameters from `Barnes.start`
7
+ - **Breaking**: Require Ruby >= 3.1
8
+ - Remove `statsd-ruby` runtime dependency (zero runtime deps)
9
+ - Remove `sample_rate` scaling from instruments
10
+ - Barnes is a no-op when `HEROKU_METRICS_URL` is not set
11
+
12
+ ## 0.1.0
13
+
14
+ - Add GitHub Actions to test against multiple Ruby versions
15
+ - remove MultiJSON in favor of built-in JSON
16
+
3
17
  ## 0.0.9
4
18
 
5
19
  - Handle half-initialize Puma failure on boot (https://github.com/heroku/barnes/pull/37)
data/README.md CHANGED
@@ -1,12 +1,14 @@
1
- ## Barnes - GC Statsd Reporter
1
+ ## Barnes
2
2
 
3
- A fork of [trashed](https://github.com/basecamp/trashed) focused on Ruby metrics for Heroku.
3
+ Ruby runtime metrics for [Heroku Runtime Metrics](https://devcenter.heroku.com/articles/language-runtime-metrics-ruby). Collects GC stats, ObjectSpace counts, and Puma thread/pool metrics and POSTs them directly to `HEROKU_METRICS_URL`.
4
+
5
+ Originally a fork of [trashed](https://github.com/basecamp/trashed).
4
6
 
5
7
  ## Setup
6
8
 
7
- ### Rails 3, 4, 5, and 6
9
+ ### Rails
8
10
 
9
- On Rails 6 (and Rails 3 and 4 and 5), add this to your Gemfile:
11
+ Add this to your Gemfile:
10
12
 
11
13
  ```
12
14
  gem "barnes"
@@ -18,33 +20,46 @@ Then run:
18
20
  $ bundle install
19
21
  ```
20
22
 
21
- ### Non-Rails
23
+ Barnes will start automatically via a Railtie when `HEROKU_METRICS_URL` is set in the environment (this is provided automatically on Heroku dynos).
24
+
25
+ ### Non-Rails (Puma)
22
26
 
23
- Add the gem to the Gemfile
27
+ Add the gem to your Gemfile:
24
28
 
25
29
  ```
26
30
  gem "barnes"
27
31
  ```
28
32
 
29
- Then run:
33
+ Then in your `puma.rb`:
30
34
 
31
- ```
32
- $ bundle install
35
+ ```ruby
36
+ require 'barnes'
37
+
38
+ before_fork do
39
+ Barnes.start
40
+ end
33
41
  ```
34
42
 
35
- In your puma.rb file:
43
+ ## How it works
36
44
 
45
+ Barnes starts a background thread that collects metrics at a configurable interval (default: 10 seconds) and POSTs them as JSON to the URL in `HEROKU_METRICS_URL`.
37
46
 
38
- ```ruby
39
- require 'barnes'
40
- ```
47
+ Barnes is a **silent no-op** when:
48
+
49
+ - `HEROKU_METRICS_URL` is not set (e.g. local development)
50
+ - The dyno is a one-off `run.*` dyno
41
51
 
42
- Then you'll need to start the client with default values:
52
+ No external dependencies or sidecar processes are required.
53
+
54
+ ## Configuration
43
55
 
44
56
  ```ruby
45
- before_fork do
46
- # worker configuration
47
- Barnes.start
48
- end
57
+ Barnes.start(
58
+ interval: 10, # seconds between reports (default: 10)
59
+ panels: [] # custom instrumentation panels (default: built-in ResourceUsage)
60
+ )
49
61
  ```
50
62
 
63
+ ## Requirements
64
+
65
+ - Ruby >= 3.1
data/barnes.gemspec CHANGED
@@ -9,8 +9,8 @@ Gem::Specification.new do |spec|
9
9
  spec.authors = ["schneems"]
10
10
  spec.email = ["richard.schneeman@gmail.com"]
11
11
 
12
- spec.summary = 'Ruby GC stats => StatsD'
13
- spec.description = 'Report GC usage data to StatsD.'
12
+ spec.summary = 'Report Ruby runtime metrics to Heroku'
13
+ spec.description = 'Collect Ruby GC, ObjectSpace, and Puma metrics and report them directly to Heroku Runtime Metrics via HTTP.'
14
14
  spec.homepage = 'https://github.com/heroku/barnes'
15
15
  spec.license = "MIT"
16
16
 
@@ -21,14 +21,15 @@ Gem::Specification.new do |spec|
21
21
  spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
22
22
  spec.require_paths = ["lib"]
23
23
 
24
- spec.required_ruby_version = '>= 2.2.0'
24
+ spec.required_ruby_version = '>= 3.1.0'
25
25
 
26
- spec.add_runtime_dependency 'statsd-ruby', '~> 1.1'
27
- spec.add_runtime_dependency 'multi_json', '~> 1'
26
+ spec.add_runtime_dependency 'json'
27
+ spec.add_runtime_dependency 'logger'
28
+ spec.add_runtime_dependency 'ostruct'
28
29
 
29
30
  spec.add_development_dependency 'rack', '~> 2'
30
31
  spec.add_development_dependency 'rake', '>= 10'
31
32
  spec.add_development_dependency 'minitest', '~> 5.3'
32
- spec.add_development_dependency "puma", '~> 3.12'
33
+ spec.add_development_dependency "puma", '~> 5.6'
33
34
  spec.add_development_dependency "wait_for_it", '~> 0.1'
34
35
  end
@@ -16,12 +16,12 @@ module Barnes
16
16
  end
17
17
 
18
18
  def start!(state)
19
- require 'multi_json'
19
+ require 'json'
20
20
  end
21
21
 
22
22
  def json_stats
23
23
  return {} unless @puma_has_stats
24
- MultiJson.load(::Puma.stats || "{}")
24
+ JSON.load(::Puma.stats || "{}")
25
25
 
26
26
  # Puma loader has not been initialized yet
27
27
  rescue NoMethodError => e
@@ -1,59 +1,20 @@
1
- # A note on GAUGE_COUNTERS.
2
- #
3
- # The sample_rate argument allows for the parameterization
4
- # of instruments that decide to report data as gauges, that
5
- # would typically be reported as counters.
6
- #
7
- # Aggregating counters is typically done simply with the `+`
8
- # operator, which doesn't preserve the number of unique
9
- # reporters that contributed to the count, or allow for one
10
- # to learn the *average* of the counts posted.
11
- #
12
- # A gauge is typically aggregated by simply *replacing* the
13
- # previous value, however, some systems do *more* with gauges
14
- # when aggregating across multiple sources of that gauge, like,
15
- # average, or compute stdev.
16
- #
17
- # This is problematic, however, when a gauge is being used as
18
- # a counter, to preserve the average / stdev computational
19
- # properties from above, because the interval that the gauge
20
- # is being read it, affects the derivative of the increasing
21
- # count. Instead of the derivative over 60s, the derivative is
22
- # taken every 10s, giving us a derivative value that's approximately
23
- # 1/6th of the actual derivative over 60s.
24
- #
25
- # We compensate for this by allowing Instruments to correct for
26
- # this, and ensure that, even though it's an estimate, the data
27
- # is scaled appropriately to the target aggregation interval, not
28
- # just the collection interval.
29
-
30
1
  module Barnes
31
2
  module Instruments
32
3
  class RubyGC
4
+ # Both sets are delta-computed (cur - last), but they land in
5
+ # different reporting buckets. COUNTERS go to `counters` (accumulated
6
+ # totals the receiver can sum). GAUGE_COUNTERS go to `gauges` so each
7
+ # sample is a standalone per-interval value. Keeping them separate also
8
+ # excludes them from the raw-value gauge loop below.
33
9
  COUNTERS = {
34
10
  :count => :'GC.count',
35
11
  :major_gc_count => :'GC.major_count',
36
12
  :minor_gc_count => :'GC.minor_gc_count' }
37
13
 
38
- GAUGE_COUNTERS = {}
39
-
40
- # Detect Ruby 2.1 vs 2.2 GC.stat naming
41
- begin
42
- GC.stat :total_allocated_objects
43
- rescue ArgumentError
44
- GAUGE_COUNTERS.update \
45
- :total_allocated_object => :'GC.total_allocated_objects',
46
- :total_freed_object => :'GC.total_freed_objects'
47
- else
48
- GAUGE_COUNTERS.update \
49
- :total_allocated_objects => :'GC.total_allocated_objects',
50
- :total_freed_objects => :'GC.total_freed_objects'
51
- end
52
-
53
- def initialize(sample_rate)
54
- # see header for an explanation of how this sample_rate is used
55
- @sample_rate = sample_rate
56
- end
14
+ GAUGE_COUNTERS = {
15
+ :total_allocated_objects => :'GC.total_allocated_objects',
16
+ :total_freed_objects => :'GC.total_freed_objects'
17
+ }
57
18
 
58
19
  def start!(state)
59
20
  state[:ruby_gc] = GC.stat
@@ -67,15 +28,10 @@ module Barnes
67
28
  counters[metric] = cur[stat] - last[stat] if cur.include? stat
68
29
  end
69
30
 
70
- # special treatment gauges
71
31
  GAUGE_COUNTERS.each do |stat, metric|
72
- if cur.include? stat
73
- val = cur[stat] - last[stat] if cur.include? stat
74
- gauges[metric] = val * (1/@sample_rate)
75
- end
32
+ gauges[metric] = cur[stat] - last[stat] if cur.include? stat
76
33
  end
77
34
 
78
- # the rest of the gauges
79
35
  cur.each do |k, v|
80
36
  unless GAUGE_COUNTERS.include? k
81
37
  gauges[:"GC.#{k}"] = v
@@ -26,7 +26,6 @@ module Barnes
26
26
  class Stopwatch
27
27
  def initialize(timepiece = Timepiece)
28
28
  @timepiece = timepiece
29
- @has_cpu_time = timepiece.respond_to?(:cpu)
30
29
  end
31
30
 
32
31
  def start!(state)
@@ -36,33 +35,29 @@ module Barnes
36
35
  def instrument!(state, counters, gauges)
37
36
  last = state[:stopwatch]
38
37
  wall_elapsed = @timepiece.wall - last[:wall]
39
- counters[:'Time.wall'] = wall_elapsed
40
-
41
- if @has_cpu_time
42
- cpu_elapsed = @timepiece.cpu - last[:cpu]
43
- idle_elapsed = wall_elapsed - cpu_elapsed
38
+ cpu_elapsed = @timepiece.cpu - last[:cpu]
39
+ idle_elapsed = wall_elapsed - cpu_elapsed
44
40
 
45
- counters[:'Time.cpu'] = cpu_elapsed
46
- counters[:'Time.idle'] = idle_elapsed
41
+ counters[:'Time.wall'] = wall_elapsed
42
+ counters[:'Time.cpu'] = cpu_elapsed
43
+ counters[:'Time.idle'] = idle_elapsed
47
44
 
48
- if wall_elapsed == 0
49
- counters[:'Time.pct.cpu'] = 0
50
- counters[:'Time.pct.idle'] = 0
51
- else
52
- counters[:'Time.pct.cpu'] = 100.0 * cpu_elapsed / wall_elapsed
53
- counters[:'Time.pct.idle'] = 100.0 * idle_elapsed / wall_elapsed
54
- end
45
+ if wall_elapsed == 0
46
+ counters[:'Time.pct.cpu'] = 0
47
+ counters[:'Time.pct.idle'] = 0
48
+ else
49
+ counters[:'Time.pct.cpu'] = 100.0 * cpu_elapsed / wall_elapsed
50
+ counters[:'Time.pct.idle'] = 100.0 * idle_elapsed / wall_elapsed
55
51
  end
56
52
 
57
53
  state[:stopwatch] = current
58
54
  end
59
55
 
60
56
  private def current
61
- state = {
57
+ {
62
58
  :wall => @timepiece.wall,
59
+ :cpu => @timepiece.cpu,
63
60
  }
64
- state[:cpu] = @timepiece.cpu if @has_cpu_time
65
- state
66
61
  end
67
62
  end
68
63
 
@@ -71,17 +66,8 @@ module Barnes
71
66
  ::Time.now.to_f * 1000
72
67
  end
73
68
 
74
- # Ruby 2.1+
75
- if Process.respond_to?(:clock_gettime)
76
- def self.cpu
77
- Process.clock_gettime Process::CLOCK_PROCESS_CPUTIME_ID, :float_millisecond
78
- end
79
-
80
- # ruby-prof installed
81
- elsif defined? RubyProf::Measure::ProcessTime
82
- def self.cpu
83
- RubyProf::Measure::Process.measure * 1000
84
- end
69
+ def self.cpu
70
+ Process.clock_gettime Process::CLOCK_PROCESS_CPUTIME_ID, :float_millisecond
85
71
  end
86
72
  end
87
73
  end
@@ -22,18 +22,14 @@
22
22
  #
23
23
 
24
24
  require 'barnes/consts'
25
+ require 'json'
25
26
 
26
27
  module Barnes
27
- # The periodic class is used to send occasional metrics
28
- # to a reporting instance of `Barnes::Reporter` at a semi-regular
29
- # rate.
30
28
  class Periodic
31
- def initialize(reporter:, sample_rate: 1, debug: false, panels: [])
29
+ def initialize(reporter:, interval: 10, debug: false, panels: [])
32
30
  @reporter = reporter
33
- @reporter.sample_rate = sample_rate
34
31
  @debug = debug
35
- # compute interval based on a 60s reporting phase.
36
- @interval = sample_rate * 60.0
32
+ @interval = interval
37
33
  @panels = panels
38
34
 
39
35
  @thread = Thread.new {
@@ -47,7 +43,6 @@ module Barnes
47
43
  begin
48
44
  sleep @interval
49
45
 
50
- # read the current values
51
46
  env = {
52
47
  STATE => Thread.current[:barnes_state],
53
48
  COUNTERS => {},
@@ -60,6 +55,8 @@ module Barnes
60
55
 
61
56
  puts env.to_json if @debug
62
57
  @reporter.report env
58
+ rescue => e
59
+ $stderr.puts "barnes: error during metrics collection: #{e.class}: #{e.message}"
63
60
  end
64
61
  end
65
62
  }
@@ -25,8 +25,8 @@ require 'rails/railtie'
25
25
 
26
26
  module Barnes
27
27
  # Automatically configures barnes to run with
28
- # rails 3, 4, and 5. Configuration can be changed
29
- # in the application.rb. For example
28
+ # rails. Configuration can be changed
29
+ # in the application.rb. For example:
30
30
  #
31
31
  # module YourApp
32
32
  # class Application < Rails::Application
@@ -34,10 +34,8 @@ module Barnes
34
34
  #
35
35
  class Railtie < ::Rails::Railtie
36
36
  config.barnes = {
37
- interval: DEFAULT_INTERVAL,
38
- aggregation_period: DEFAULT_AGGREGATION_PERIOD,
39
- statsd: DEFAULT_STATSD,
40
- panels: DEFAULT_PANELS,
37
+ interval: DEFAULT_INTERVAL,
38
+ panels: DEFAULT_PANELS,
41
39
  }
42
40
 
43
41
  initializer 'barnes' do |app|
@@ -21,42 +21,92 @@
21
21
  # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
22
  #
23
23
 
24
+ require 'net/http'
25
+ require 'json'
26
+ require 'uri'
27
+
24
28
  module Barnes
25
- # The reporter is used to send stats to the server.
26
- #
27
- # Example:
28
- #
29
- # statsd = Statsd.new('127.0.0.1', "8125")
30
- # reporter = Reporter.new(statsd: , sample_rate: 10)
31
- # reporter.report_statsd('barnes.counters' => {"hello" => 2})
32
29
  class Reporter
33
- attr_accessor :statsd, :sample_rate
30
+ MAX_RETRIES = 3
31
+ HTTP_TIMEOUT = 5
34
32
 
35
- def initialize(statsd: , sample_rate:)
36
- @statsd = statsd
37
- @sample_rate = sample_rate.to_f
33
+ ServerError = Class.new(StandardError)
38
34
 
39
- if @statsd.respond_to?(:easy)
40
- @statsd_method = statsd.method(:easy)
41
- else
42
- @statsd_method = statsd.method(:batch)
43
- end
35
+ def initialize(url:, backoff_sleep: ->(n) { sleep(n) })
36
+ @uri = URI.parse(url)
37
+ @backoff_sleep = backoff_sleep
38
+ @mutex = Mutex.new
39
+ @http = nil
44
40
  end
45
41
 
46
42
  def report(env)
47
- report_statsd env if @statsd
43
+ counters = {}
44
+ env[Barnes::COUNTERS].each { |k, v| counters["Rack.Server.All.#{k}"] = v }
45
+
46
+ gauges = {}
47
+ env[Barnes::GAUGES].each { |k, v| gauges["Rack.Server.All.#{k}"] = v }
48
+
49
+ count = counters.size + gauges.size
50
+ return if count == 0
51
+
52
+ body = JSON.generate(counters: counters, gauges: gauges)
53
+ @mutex.synchronize { post(body, count) }
48
54
  end
49
55
 
50
- def report_statsd(env)
51
- @statsd_method.call do |statsd|
52
- env[Barnes::COUNTERS].each do |metric, value|
53
- statsd.count(:"Rack.Server.All.#{metric}", value, @sample_rate)
54
- end
56
+ private
57
+
58
+ def connection
59
+ return @http if @http&.started?
55
60
 
56
- # for :gauge, use sample rate of 1, since gauges in statsd have no sampling characteristics.
57
- env[Barnes::GAUGES].each do |metric, value|
58
- statsd.gauge(:"Rack.Server.All.#{metric}", value, 1.0)
61
+ @http = Net::HTTP.new(@uri.host, @uri.port)
62
+ @http.use_ssl = @uri.scheme == "https"
63
+ @http.open_timeout = HTTP_TIMEOUT
64
+ @http.read_timeout = HTTP_TIMEOUT
65
+ @http.write_timeout = HTTP_TIMEOUT
66
+ @http.keep_alive_timeout = 30
67
+ @http.start
68
+ @http
69
+ end
70
+
71
+ def post(body, count)
72
+ retries = 0
73
+ pause = 0.1
74
+ timestamp = Time.now.utc.strftime("%Y-%m-%dT%H:%M:%SZ")
75
+
76
+ begin
77
+ request = Net::HTTP::Post.new(@uri)
78
+ request["Content-Type"] = "application/json"
79
+ request["Measurements-Count"] = count.to_s
80
+ request["Measurements-Time"] = timestamp
81
+ request.body = body
82
+
83
+ response = connection.request(request)
84
+
85
+ case response.code.to_i
86
+ when 200..299
87
+ # success
88
+ when 400..499
89
+ $stderr.puts "barnes: metrics POST rejected (#{response.code}): #{response.body}"
90
+ when 500..599
91
+ raise ServerError, "server error #{response.code}"
92
+ end
93
+ rescue ServerError, Net::OpenTimeout, Net::ReadTimeout, Net::WriteTimeout,
94
+ Errno::ECONNREFUSED, Errno::ECONNRESET, Errno::EHOSTUNREACH,
95
+ Errno::EPIPE, SocketError, IOError => e
96
+ @http&.finish rescue nil
97
+ @http = nil
98
+ if retries < MAX_RETRIES
99
+ retries += 1
100
+ @backoff_sleep.call(pause)
101
+ pause *= 2
102
+ retry
103
+ else
104
+ $stderr.puts "barnes: failed to POST metrics after #{MAX_RETRIES} retries: #{e.class}: #{e.message}"
59
105
  end
106
+ rescue => e
107
+ @http&.finish rescue nil
108
+ @http = nil
109
+ $stderr.puts "barnes: unexpected error posting metrics: #{e.class}: #{e.message}"
60
110
  end
61
111
  end
62
112
  end
@@ -25,7 +25,7 @@ require 'barnes/panel'
25
25
 
26
26
  module Barnes
27
27
  class ResourceUsage < Panel
28
- def initialize(sample_rate)
28
+ def initialize
29
29
  super()
30
30
 
31
31
  require 'barnes/instruments/puma_instrument'
@@ -38,28 +38,11 @@ module Barnes
38
38
  require 'barnes/instruments/stopwatch'
39
39
  instrument Barnes::Instruments::Stopwatch.new
40
40
 
41
- if GC.respond_to? :enable_stats
42
- require 'barnes/instruments/ree_gc'
43
- instrument Barnes::Instruments::Ruby18GC.new
44
- end
45
-
46
- # Ruby 1.9+
47
- if ObjectSpace.respond_to? :count_objects
48
- require 'barnes/instruments/object_space_counter'
49
- instrument Barnes::Instruments::ObjectSpaceCounter.new
50
- end
41
+ require 'barnes/instruments/object_space_counter'
42
+ instrument Barnes::Instruments::ObjectSpaceCounter.new
51
43
 
52
- # Ruby 1.9+
53
- if GC.respond_to?(:stat)
54
- require 'barnes/instruments/ruby_gc'
55
- instrument Barnes::Instruments::RubyGC.new(sample_rate)
56
- end
57
-
58
- # Ruby 2.1+ with https://github.com/tmm1/gctools
59
- if defined? GC::OOB
60
- require 'barnes/instruments/gctools_oobgc'
61
- instrument Barnes::Instruments::GctoolsOobgc.new
62
- end
44
+ require 'barnes/instruments/ruby_gc'
45
+ instrument Barnes::Instruments::RubyGC.new
63
46
  end
64
47
  end
65
48
  end
@@ -22,5 +22,5 @@
22
22
  #
23
23
 
24
24
  module Barnes
25
- VERSION = "0.0.9"
25
+ VERSION = "1.0.0"
26
26
  end
data/lib/barnes.rb CHANGED
@@ -22,45 +22,38 @@
22
22
  #
23
23
 
24
24
  module Barnes
25
- DEFAULT_INTERVAL = 10
26
- DEFAULT_AGGREGATION_PERIOD = 60
27
- DEFAULT_STATSD = :default
28
- DEFAULT_PANELS = []
25
+ DEFAULT_INTERVAL = 10
26
+ DEFAULT_PANELS = [].freeze
29
27
 
30
-
31
- # Starts the reporting client
28
+ # Starts the metrics reporting client.
29
+ #
30
+ # Collects Ruby runtime metrics (GC stats, ObjectSpace counts,
31
+ # Puma pool stats) and POSTs them to HEROKU_METRICS_URL.
32
32
  #
33
33
  # Arguments:
34
34
  #
35
- # - interval: How often, in seconds, to instrument and report
36
- # - aggregation_period: The minimal aggregation period in use, in seconds.
37
- # - statsd: The statsd reporter. This should be an instance of statsd-ruby
35
+ # - interval: How often, in seconds, to instrument and report.
38
36
  # - panels: The instrumentation "panels" in use. See `resource_usage.rb` for
39
37
  # an example panel, which is the default if none are provided.
40
- def self.start(interval: DEFAULT_INTERVAL, aggregation_period: DEFAULT_AGGREGATION_PERIOD, statsd: DEFAULT_STATSD, panels: DEFAULT_PANELS)
41
- require 'statsd'
42
- statsd_client = statsd
43
- panels = panels
44
- sample_rate = interval.to_f / aggregation_period.to_f
38
+ def self.start(interval: DEFAULT_INTERVAL, panels: DEFAULT_PANELS)
39
+ return if ENV["DYNO"]&.start_with?("run.")
45
40
 
46
- if statsd_client == :default && ENV["PORT"]
47
- statsd_client = Statsd.new('127.0.0.1', ENV["PORT"])
48
- end
41
+ url = ENV["HEROKU_METRICS_URL"]
42
+ return unless url
49
43
 
50
- if statsd_client && statsd_client != :default
51
- reporter = Barnes::Reporter.new(statsd: statsd_client, sample_rate: sample_rate)
44
+ reporter = Barnes::Reporter.new(url: url)
52
45
 
53
- unless panels.length > 0
54
- panels << Barnes::ResourceUsage.new(sample_rate)
55
- end
56
-
57
- Periodic.new(
58
- reporter: reporter,
59
- sample_rate: sample_rate,
60
- panels: panels,
61
- debug: ENV['BARNES_DEBUG']
62
- )
46
+ panels = panels.dup
47
+ if panels.empty?
48
+ panels << Barnes::ResourceUsage.new
63
49
  end
50
+
51
+ Periodic.new(
52
+ reporter: reporter,
53
+ interval: interval,
54
+ panels: panels,
55
+ debug: ENV['BARNES_DEBUG']
56
+ )
64
57
  end
65
58
  end
66
59
 
metadata CHANGED
@@ -1,43 +1,56 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: barnes
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.9
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
- autorequire:
9
8
  bindir: exe
10
9
  cert_chain: []
11
- date: 2021-03-02 00:00:00.000000000 Z
10
+ date: 1980-01-02 00:00:00.000000000 Z
12
11
  dependencies:
13
12
  - !ruby/object:Gem::Dependency
14
- name: statsd-ruby
13
+ name: json
15
14
  requirement: !ruby/object:Gem::Requirement
16
15
  requirements:
17
- - - "~>"
16
+ - - ">="
18
17
  - !ruby/object:Gem::Version
19
- version: '1.1'
18
+ version: '0'
20
19
  type: :runtime
21
20
  prerelease: false
22
21
  version_requirements: !ruby/object:Gem::Requirement
23
22
  requirements:
24
- - - "~>"
23
+ - - ">="
25
24
  - !ruby/object:Gem::Version
26
- version: '1.1'
25
+ version: '0'
27
26
  - !ruby/object:Gem::Dependency
28
- name: multi_json
27
+ name: logger
29
28
  requirement: !ruby/object:Gem::Requirement
30
29
  requirements:
31
- - - "~>"
30
+ - - ">="
32
31
  - !ruby/object:Gem::Version
33
- version: '1'
32
+ version: '0'
34
33
  type: :runtime
35
34
  prerelease: false
36
35
  version_requirements: !ruby/object:Gem::Requirement
37
36
  requirements:
38
- - - "~>"
37
+ - - ">="
38
+ - !ruby/object:Gem::Version
39
+ version: '0'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ostruct
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - ">="
45
+ - !ruby/object:Gem::Version
46
+ version: '0'
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - ">="
39
52
  - !ruby/object:Gem::Version
40
- version: '1'
53
+ version: '0'
41
54
  - !ruby/object:Gem::Dependency
42
55
  name: rack
43
56
  requirement: !ruby/object:Gem::Requirement
@@ -86,14 +99,14 @@ dependencies:
86
99
  requirements:
87
100
  - - "~>"
88
101
  - !ruby/object:Gem::Version
89
- version: '3.12'
102
+ version: '5.6'
90
103
  type: :development
91
104
  prerelease: false
92
105
  version_requirements: !ruby/object:Gem::Requirement
93
106
  requirements:
94
107
  - - "~>"
95
108
  - !ruby/object:Gem::Version
96
- version: '3.12'
109
+ version: '5.6'
97
110
  - !ruby/object:Gem::Dependency
98
111
  name: wait_for_it
99
112
  requirement: !ruby/object:Gem::Requirement
@@ -108,7 +121,8 @@ dependencies:
108
121
  - - "~>"
109
122
  - !ruby/object:Gem::Version
110
123
  version: '0.1'
111
- description: Report GC usage data to StatsD.
124
+ description: Collect Ruby GC, ObjectSpace, and Puma metrics and report them directly
125
+ to Heroku Runtime Metrics via HTTP.
112
126
  email:
113
127
  - richard.schneeman@gmail.com
114
128
  executables: []
@@ -117,9 +131,10 @@ extra_rdoc_files: []
117
131
  files:
118
132
  - ".codeclimate.yaml"
119
133
  - ".github/CODEOWNERS"
134
+ - ".github/dependabot.yml"
120
135
  - ".github/workflows/check_changelog.yml"
136
+ - ".github/workflows/ci.yml"
121
137
  - ".gitignore"
122
- - ".travis.yml"
123
138
  - CHANGELOG.md
124
139
  - CODE_OF_CONDUCT.md
125
140
  - Gemfile
@@ -132,11 +147,9 @@ files:
132
147
  - init.rb
133
148
  - lib/barnes.rb
134
149
  - lib/barnes/consts.rb
135
- - lib/barnes/instruments/gctools_oobgc.rb
136
150
  - lib/barnes/instruments/object_space_counter.rb
137
151
  - lib/barnes/instruments/puma_instrument.rb
138
152
  - lib/barnes/instruments/puma_stats_value.rb
139
- - lib/barnes/instruments/ree_gc.rb
140
153
  - lib/barnes/instruments/ruby_gc.rb
141
154
  - lib/barnes/instruments/stopwatch.rb
142
155
  - lib/barnes/panel.rb
@@ -149,7 +162,6 @@ homepage: https://github.com/heroku/barnes
149
162
  licenses:
150
163
  - MIT
151
164
  metadata: {}
152
- post_install_message:
153
165
  rdoc_options: []
154
166
  require_paths:
155
167
  - lib
@@ -157,15 +169,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
157
169
  requirements:
158
170
  - - ">="
159
171
  - !ruby/object:Gem::Version
160
- version: 2.2.0
172
+ version: 3.1.0
161
173
  required_rubygems_version: !ruby/object:Gem::Requirement
162
174
  requirements:
163
175
  - - ">="
164
176
  - !ruby/object:Gem::Version
165
177
  version: '0'
166
178
  requirements: []
167
- rubygems_version: 3.2.3
168
- signing_key:
179
+ rubygems_version: 3.6.9
169
180
  specification_version: 4
170
- summary: Ruby GC stats => StatsD
181
+ summary: Report Ruby runtime metrics to Heroku
171
182
  test_files: []
data/.travis.yml DELETED
@@ -1,4 +0,0 @@
1
- language: ruby
2
- rvm:
3
- - 2.5.0
4
- before_install: gem install bundler -v 2.1.0
@@ -1,53 +0,0 @@
1
- # Copyright (c) 2017 Salesforce
2
- # Copyright (c) 2009 37signals, LLC
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
-
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
-
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #
23
-
24
- module Barnes
25
- module Instruments
26
- # Tracks out of band GCs that occurred *since* the last request.
27
- class GctoolsOobgc
28
- def start!(state)
29
- state[:oobgc] = current
30
- end
31
-
32
- def instrument!(state, counters, gauges)
33
- last = state[:oobgc]
34
- cur = state[:oobgc] = current
35
-
36
- counters.update \
37
- :'OOBGC.count' => cur[:count] - last[:count],
38
- :'OOBGC.major_count' => cur[:major] - last[:major],
39
- :'OOBGC.minor_count' => cur[:minor] - last[:minor],
40
- :'OOBGC.sweep_count' => cur[:sweep] - last[:sweep]
41
- end
42
-
43
- private def current
44
- {
45
- :count => GC::OOB.stat(:count).to_i,
46
- :major => GC::OOB.stat(:major).to_i,
47
- :minor => GC::OOB.stat(:minor).to_i,
48
- :sweep => GC::OOB.stat(:sweep).to_i
49
- }
50
- end
51
- end
52
- end
53
- end
@@ -1,59 +0,0 @@
1
- # Copyright (c) 2017 Salesforce
2
- # Copyright (c) 2009 37signals, LLC
3
- #
4
- # Permission is hereby granted, free of charge, to any person obtaining
5
- # a copy of this software and associated documentation files (the
6
- # "Software"), to deal in the Software without restriction, including
7
- # without limitation the rights to use, copy, modify, merge, publish,
8
- # distribute, sublicense, and/or sell copies of the Software, and to
9
- # permit persons to whom the Software is furnished to do so, subject to
10
- # the following conditions:
11
-
12
- # The above copyright notice and this permission notice shall be
13
- # included in all copies or substantial portions of the Software.
14
-
15
- # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
- # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
- # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
- # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
- # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
- # CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
- # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
- #
23
-
24
- module Barnes
25
- module Instruments
26
- class Ruby18GC
27
- def initialize
28
- GC.enable_stats
29
- end
30
-
31
- def start!(state)
32
- state[:ruby18_gc] = current
33
- end
34
-
35
- def instrument!(state, counters, gauges)
36
- last = state[:ruby18_gc]
37
- cur = state[:ruby18_gc] = current
38
-
39
- counters.update \
40
- :'GC.count' => cur[:gc_count] - before[:gc_count],
41
- :'GC.time' => cur[:gc_time] - before[:gc_time],
42
- :'GC.memory' => cur[:gc_memory] - before[:gc_memory],
43
- :'GC.allocated_objects' => cur[:objects] - before[:objects]
44
-
45
- gauges[:'Objects.live'] = ObjectSpace.live_objects
46
- gauges[:'GC.growth'] = GC.growth
47
- end
48
-
49
- private def current
50
- {
51
- :objects => ObjectSpace.allocated_objects,
52
- :gc_count => GC.collections,
53
- :gc_time => GC.time,
54
- :gc_memory => GC.allocated_size
55
- }
56
- end
57
- end
58
- end
59
- end