appsignal 3.0.6-java → 3.0.7-java
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +11 -0
- data/README.md +9 -3
- data/ext/agent.yml +17 -17
- data/lib/appsignal/config.rb +1 -0
- data/lib/appsignal/hooks/puma.rb +1 -16
- data/lib/appsignal/probes.rb +0 -1
- data/lib/appsignal/version.rb +1 -1
- data/lib/puma/plugin/appsignal.rb +146 -17
- data/spec/lib/appsignal/hooks/puma_spec.rb +0 -46
- data/spec/lib/puma/appsignal_spec.rb +244 -68
- metadata +2 -5
- data/lib/appsignal/probes/puma.rb +0 -61
- data/spec/lib/appsignal/probes/puma_spec.rb +0 -180
    
        checksums.yaml
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 2 | 
             
            SHA256:
         | 
| 3 | 
            -
              metadata.gz:  | 
| 4 | 
            -
              data.tar.gz:  | 
| 3 | 
            +
              metadata.gz: b855b4dfbc358cac71b515acfa9060d0fe285bc166088a2ca8c350e13bb2e026
         | 
| 4 | 
            +
              data.tar.gz: 4ea9db25ed39f99052d8e83a83513a8f75363a028895f15e09e23a18774d633c
         | 
| 5 5 | 
             
            SHA512:
         | 
| 6 | 
            -
              metadata.gz:  | 
| 7 | 
            -
              data.tar.gz:  | 
| 6 | 
            +
              metadata.gz: 7c603ff374341866922386c85f3ed9ee8a842c632fa35c9be60e975afa33f8bb8262f4f7e70fce3e3b4503626825a4425481c5ee3fa259ff8aa6475aca813604
         | 
| 7 | 
            +
              data.tar.gz: f91fcf688355c79d562fabda066473e3942aa1f174f3ce22485a43fe9aac6c530c2200b0f2be0af09a5b2568ca43e9a001d0a0293c5eaea6f989ff83027f709b
         | 
    
        data/CHANGELOG.md
    CHANGED
    
    | @@ -1,5 +1,16 @@ | |
| 1 1 | 
             
            # AppSignal for Ruby gem Changelog
         | 
| 2 2 |  | 
| 3 | 
            +
            ## 3.0.7
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            - [27f9b178](https://github.com/appsignal/appsignal-ruby/commit/27f9b178c20006ee15e69bdf878f3a0c9975b1f4) patch - Bump agent to 6caf6d0. Replaces curl HTTP client and includes various other maintenance updates.
         | 
| 6 | 
            +
            - [665d883a](https://github.com/appsignal/appsignal-ruby/commit/665d883a529e5c14b28e73eeb3ae6410deb3e182) patch - Improve Puma plugin stats collection. Instead of starting the AppSignal gem in the main process we send the stats to the AppSignal agent directly using StatsD. This should improve compatibility with phased restarts. If you use `prune_bundler`, you will need to add AppSignal to the extra `extra_runtime_dependencies` list.
         | 
| 7 | 
            +
              
         | 
| 8 | 
            +
              ```
         | 
| 9 | 
            +
              # config/puma.rb
         | 
| 10 | 
            +
              plugin :appsignal
         | 
| 11 | 
            +
              extra_runtime_dependencies ["appsignal"]
         | 
| 12 | 
            +
              ```
         | 
| 13 | 
            +
             | 
| 3 14 | 
             
            ## 3.0.6
         | 
| 4 15 |  | 
| 5 16 | 
             
            - [d354d79b](https://github.com/appsignal/appsignal-ruby/commit/d354d79b293fd549e66cae60d805d1b1e9e9d2d8) patch - Add Excon integration. Track requests and responses from the Excon gem.
         | 
    
        data/README.md
    CHANGED
    
    | @@ -161,9 +161,13 @@ systems][supported-systems] page. | |
| 161 161 |  | 
| 162 162 | 
             
            Following the process below to release a new version of the Ruby gem.
         | 
| 163 163 |  | 
| 164 | 
            -
            1.  | 
| 165 | 
            -
             | 
| 166 | 
            -
             | 
| 164 | 
            +
            1. Make sure [mono](https://github.com/appsignal/mono/) is installed by
         | 
| 165 | 
            +
               following the installation instructions.
         | 
| 166 | 
            +
            1. Run: `mono publish`
         | 
| 167 | 
            +
               - Mono will automatically bump the version number based on the
         | 
| 168 | 
            +
                 `.changesets/`.
         | 
| 169 | 
            +
               - Mono will automatically update the `CHANGELOG.md` file based on the
         | 
| 170 | 
            +
                 `.changesets/`.
         | 
| 167 171 | 
             
            1. Confirm with your two-factor authentication token for Rubygems.org, _twice_.
         | 
| 168 172 |  | 
| 169 173 | 
             
            ## Development
         | 
| @@ -176,6 +180,8 @@ install all possible dependencies. | |
| 176 180 | 
             
            ```bash
         | 
| 177 181 | 
             
            # Install Bundler
         | 
| 178 182 | 
             
            gem install bundler
         | 
| 183 | 
            +
            # Bootstrap the project
         | 
| 184 | 
            +
            mono bootstrap
         | 
| 179 185 | 
             
            # Install the AppSignal extension and _all_ gems we support.
         | 
| 180 186 | 
             
            bundle exec rake install
         | 
| 181 187 | 
             
            # Only install the AppSignal extension.
         | 
    
        data/ext/agent.yml
    CHANGED
    
    | @@ -1,62 +1,62 @@ | |
| 1 1 | 
             
            ---
         | 
| 2 | 
            -
            version:  | 
| 2 | 
            +
            version: 3ecd06f
         | 
| 3 3 | 
             
            mirrors:
         | 
| 4 4 | 
             
            - https://appsignal-agent-releases.global.ssl.fastly.net
         | 
| 5 5 | 
             
            - https://d135dj0rjqvssy.cloudfront.net
         | 
| 6 6 | 
             
            triples:
         | 
| 7 7 | 
             
              x86_64-darwin:
         | 
| 8 8 | 
             
                static:
         | 
| 9 | 
            -
                  checksum:  | 
| 9 | 
            +
                  checksum: 39839e64832d7964fad51d9acc95374654446ade04d06cbba4d052e4483a8516
         | 
| 10 10 | 
             
                  filename: appsignal-x86_64-darwin-all-static.tar.gz
         | 
| 11 11 | 
             
                dynamic:
         | 
| 12 | 
            -
                  checksum:  | 
| 12 | 
            +
                  checksum: 841b82677aaf553219f689b92b6dc6988278420e2e72dcab28a6c5915a0e4158
         | 
| 13 13 | 
             
                  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
         | 
| 14 14 | 
             
              universal-darwin:
         | 
| 15 15 | 
             
                static:
         | 
| 16 | 
            -
                  checksum:  | 
| 16 | 
            +
                  checksum: 39839e64832d7964fad51d9acc95374654446ade04d06cbba4d052e4483a8516
         | 
| 17 17 | 
             
                  filename: appsignal-x86_64-darwin-all-static.tar.gz
         | 
| 18 18 | 
             
                dynamic:
         | 
| 19 | 
            -
                  checksum:  | 
| 19 | 
            +
                  checksum: 841b82677aaf553219f689b92b6dc6988278420e2e72dcab28a6c5915a0e4158
         | 
| 20 20 | 
             
                  filename: appsignal-x86_64-darwin-all-dynamic.tar.gz
         | 
| 21 21 | 
             
              i686-linux:
         | 
| 22 22 | 
             
                static:
         | 
| 23 | 
            -
                  checksum:  | 
| 23 | 
            +
                  checksum: fdf923153992816c813f938f41feadafbf80d8fa10a785123ee23184945047a0
         | 
| 24 24 | 
             
                  filename: appsignal-i686-linux-all-static.tar.gz
         | 
| 25 25 | 
             
                dynamic:
         | 
| 26 | 
            -
                  checksum:  | 
| 26 | 
            +
                  checksum: d2b8635199a73a1769ee7495acac0cdc69c4f964fef9ec7749fde517d789e1dd
         | 
| 27 27 | 
             
                  filename: appsignal-i686-linux-all-dynamic.tar.gz
         | 
| 28 28 | 
             
              x86-linux:
         | 
| 29 29 | 
             
                static:
         | 
| 30 | 
            -
                  checksum:  | 
| 30 | 
            +
                  checksum: fdf923153992816c813f938f41feadafbf80d8fa10a785123ee23184945047a0
         | 
| 31 31 | 
             
                  filename: appsignal-i686-linux-all-static.tar.gz
         | 
| 32 32 | 
             
                dynamic:
         | 
| 33 | 
            -
                  checksum:  | 
| 33 | 
            +
                  checksum: d2b8635199a73a1769ee7495acac0cdc69c4f964fef9ec7749fde517d789e1dd
         | 
| 34 34 | 
             
                  filename: appsignal-i686-linux-all-dynamic.tar.gz
         | 
| 35 35 | 
             
              x86_64-linux:
         | 
| 36 36 | 
             
                static:
         | 
| 37 | 
            -
                  checksum:  | 
| 37 | 
            +
                  checksum: d81383dedb228f484e1d1e28a76ddc017d788fa62270b6e3f5a55765a4fb89d9
         | 
| 38 38 | 
             
                  filename: appsignal-x86_64-linux-all-static.tar.gz
         | 
| 39 39 | 
             
                dynamic:
         | 
| 40 | 
            -
                  checksum:  | 
| 40 | 
            +
                  checksum: f98948600345ee4726712937b13059fe7145d9b83273fc6efc6e0fd6a255381a
         | 
| 41 41 | 
             
                  filename: appsignal-x86_64-linux-all-dynamic.tar.gz
         | 
| 42 42 | 
             
              x86_64-linux-musl:
         | 
| 43 43 | 
             
                static:
         | 
| 44 | 
            -
                  checksum:  | 
| 44 | 
            +
                  checksum: be2c414da7eb0837f23a8791b369e650aea26afbf9d3be9f530bc2de11fc888f
         | 
| 45 45 | 
             
                  filename: appsignal-x86_64-linux-musl-all-static.tar.gz
         | 
| 46 46 | 
             
                dynamic:
         | 
| 47 | 
            -
                  checksum:  | 
| 47 | 
            +
                  checksum: 3d04b9dcfbfe7696e028f9205dcae08e41bd87d162f7f37d04cc3d4a7442a9ec
         | 
| 48 48 | 
             
                  filename: appsignal-x86_64-linux-musl-all-dynamic.tar.gz
         | 
| 49 49 | 
             
              x86_64-freebsd:
         | 
| 50 50 | 
             
                static:
         | 
| 51 | 
            -
                  checksum:  | 
| 51 | 
            +
                  checksum: fcbfabaa51f9004ed697e9000bfc1a7d71f48f40c04f9c9588a5c8f09cddf0ea
         | 
| 52 52 | 
             
                  filename: appsignal-x86_64-freebsd-all-static.tar.gz
         | 
| 53 53 | 
             
                dynamic:
         | 
| 54 | 
            -
                  checksum:  | 
| 54 | 
            +
                  checksum: ffc261d6d2a799e9bbf1cee3f9134da60cd72cd6b47a72e8c63a5a11e4d0b472
         | 
| 55 55 | 
             
                  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
         | 
| 56 56 | 
             
              amd64-freebsd:
         | 
| 57 57 | 
             
                static:
         | 
| 58 | 
            -
                  checksum:  | 
| 58 | 
            +
                  checksum: fcbfabaa51f9004ed697e9000bfc1a7d71f48f40c04f9c9588a5c8f09cddf0ea
         | 
| 59 59 | 
             
                  filename: appsignal-x86_64-freebsd-all-static.tar.gz
         | 
| 60 60 | 
             
                dynamic:
         | 
| 61 | 
            -
                  checksum:  | 
| 61 | 
            +
                  checksum: ffc261d6d2a799e9bbf1cee3f9134da60cd72cd6b47a72e8c63a5a11e4d0b472
         | 
| 62 62 | 
             
                  filename: appsignal-x86_64-freebsd-all-dynamic.tar.gz
         | 
    
        data/lib/appsignal/config.rb
    CHANGED
    
    | @@ -290,6 +290,7 @@ module Appsignal | |
| 290 290 | 
             
                  ENV["_APPSIGNAL_FILES_WORLD_ACCESSIBLE"]       = config_hash[:files_world_accessible].to_s
         | 
| 291 291 | 
             
                  ENV["_APPSIGNAL_TRANSACTION_DEBUG_MODE"]       = config_hash[:transaction_debug_mode].to_s
         | 
| 292 292 | 
             
                  ENV["_APPSIGNAL_SEND_ENVIRONMENT_METADATA"]    = config_hash[:send_environment_metadata].to_s
         | 
| 293 | 
            +
                  ENV["_APPSIGNAL_ENABLE_STATSD"]                = "true"
         | 
| 293 294 | 
             
                  ENV["_APP_REVISION"]                           = config_hash[:revision].to_s
         | 
| 294 295 | 
             
                end
         | 
| 295 296 |  | 
    
        data/lib/appsignal/hooks/puma.rb
    CHANGED
    
    | @@ -11,23 +11,8 @@ module Appsignal | |
| 11 11 | 
             
                  end
         | 
| 12 12 |  | 
| 13 13 | 
             
                  def install
         | 
| 14 | 
            -
                    if ::Puma.respond_to?(:stats) && !defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)
         | 
| 15 | 
            -
                      # Only install the minutely probe if a user isn't using our Puma
         | 
| 16 | 
            -
                      # plugin, which lives in `lib/puma/appsignal.rb`. This plugin defines
         | 
| 17 | 
            -
                      # the {APPSIGNAL_PUMA_PLUGIN_LOADED} constant.
         | 
| 18 | 
            -
                      #
         | 
| 19 | 
            -
                      # We prefer people use the AppSignal Puma plugin. This fallback is
         | 
| 20 | 
            -
                      # only there when users relied on our *magic* integration.
         | 
| 21 | 
            -
                      #
         | 
| 22 | 
            -
                      # Using the Puma plugin, the minutely probe thread will still run in
         | 
| 23 | 
            -
                      # Puma workers, for other non-Puma probes, but the Puma probe only
         | 
| 24 | 
            -
                      # runs in the Puma main process.
         | 
| 25 | 
            -
                      # For more information:
         | 
| 26 | 
            -
                      # https://docs.appsignal.com/ruby/integrations/puma.html
         | 
| 27 | 
            -
                      Appsignal::Minutely.probes.register :puma, ::Appsignal::Probes::PumaProbe
         | 
| 28 | 
            -
                    end
         | 
| 29 | 
            -
             | 
| 30 14 | 
             
                    return unless defined?(::Puma::Cluster)
         | 
| 15 | 
            +
             | 
| 31 16 | 
             
                    # For clustered mode with multiple workers
         | 
| 32 17 | 
             
                    ::Puma::Cluster.send(:prepend, Module.new do
         | 
| 33 18 | 
             
                      def stop_workers
         | 
    
        data/lib/appsignal/probes.rb
    CHANGED
    
    
    
        data/lib/appsignal/version.rb
    CHANGED
    
    
| @@ -1,27 +1,156 @@ | |
| 1 | 
            -
             | 
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "json"
         | 
| 2 4 |  | 
| 3 5 | 
             
            # AppSignal Puma plugin
         | 
| 4 6 | 
             
            #
         | 
| 5 | 
            -
            # This plugin ensures  | 
| 6 | 
            -
            # minutely probe in the Puma master process.
         | 
| 7 | 
            -
            #
         | 
| 8 | 
            -
            # The constant {APPSIGNAL_PUMA_PLUGIN_LOADED} is here to mark the Plugin as
         | 
| 9 | 
            -
            # loaded by the rest of the AppSignal gem. This ensures that the Puma minutely
         | 
| 10 | 
            -
            # probe is not also started in every Puma workers, which was the old behavior.
         | 
| 11 | 
            -
            # See {Appsignal::Hooks::PumaHook#install} for more information.
         | 
| 7 | 
            +
            # This plugin ensures Puma metrics are sent to the AppSignal agent using StatsD.
         | 
| 12 8 | 
             
            #
         | 
| 13 9 | 
             
            # For even more information:
         | 
| 14 10 | 
             
            # https://docs.appsignal.com/ruby/integrations/puma.html
         | 
| 15 | 
            -
            Puma::Plugin.create do
         | 
| 16 | 
            -
              def start(launcher | 
| 17 | 
            -
                launcher | 
| 18 | 
            -
             | 
| 19 | 
            -
             | 
| 20 | 
            -
             | 
| 21 | 
            -
             | 
| 11 | 
            +
            Puma::Plugin.create do # rubocop:disable Metrics/BlockLength
         | 
| 12 | 
            +
              def start(launcher)
         | 
| 13 | 
            +
                @launcher = launcher
         | 
| 14 | 
            +
                @launcher.events.debug "AppSignal: Puma plugin start."
         | 
| 15 | 
            +
                in_background do
         | 
| 16 | 
            +
                  @launcher.events.debug "AppSignal: Start Puma stats collection loop."
         | 
| 17 | 
            +
                  plugin = AppsignalPumaPlugin.new
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                  loop do
         | 
| 20 | 
            +
                    begin
         | 
| 21 | 
            +
                      # Implement similar behavior to minutely probes.
         | 
| 22 | 
            +
                      # Initial sleep to wait until the app is fully initalized.
         | 
| 23 | 
            +
                      # Then loop every 60 seconds and collect the Puma stats as AppSignal
         | 
| 24 | 
            +
                      # metrics.
         | 
| 25 | 
            +
                      sleep sleep_time
         | 
| 26 | 
            +
             | 
| 27 | 
            +
                      @launcher.events.debug "AppSignal: Collecting Puma stats."
         | 
| 28 | 
            +
                      stats = fetch_puma_stats
         | 
| 29 | 
            +
                      if stats
         | 
| 30 | 
            +
                        plugin.call(stats)
         | 
| 31 | 
            +
                      else
         | 
| 32 | 
            +
                        @launcher.events.log "AppSignal: No Puma stats to report."
         | 
| 33 | 
            +
                      end
         | 
| 34 | 
            +
                    rescue StandardError => error
         | 
| 35 | 
            +
                      log_error "Error while processing metrics.", error
         | 
| 36 | 
            +
                    end
         | 
| 22 37 | 
             
                  end
         | 
| 23 | 
            -
             | 
| 24 | 
            -
             | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              private
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              def sleep_time
         | 
| 44 | 
            +
                60 # seconds
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def log_error(message, error)
         | 
| 48 | 
            +
                @launcher.events.log "AppSignal: #{message}\n" \
         | 
| 49 | 
            +
                  "#{error.class}: #{error.message}\n#{error.backtrace.join("\n")}"
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              def fetch_puma_stats
         | 
| 53 | 
            +
                if Puma.respond_to? :stats_hash # Puma >= 5.0.0
         | 
| 54 | 
            +
                  Puma.stats_hash
         | 
| 55 | 
            +
                elsif Puma.respond_to? :stats # Puma < 5.0.0
         | 
| 56 | 
            +
                  # Puma.stats_hash returns symbolized keys as well
         | 
| 57 | 
            +
                  JSON.parse Puma.stats, :symbolize_names => true
         | 
| 58 | 
            +
                end
         | 
| 59 | 
            +
              rescue StandardError => error
         | 
| 60 | 
            +
                log_error "Error while parsing Puma stats.", error
         | 
| 61 | 
            +
                nil
         | 
| 62 | 
            +
              end
         | 
| 63 | 
            +
            end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
            # AppsignalPumaPlugin
         | 
| 66 | 
            +
            #
         | 
| 67 | 
            +
            # Class to handle the logic of translating the Puma stats to AppSignal metrics.
         | 
| 68 | 
            +
            #
         | 
| 69 | 
            +
            # @api private
         | 
| 70 | 
            +
            class AppsignalPumaPlugin
         | 
| 71 | 
            +
              def initialize
         | 
| 72 | 
            +
                @hostname = fetch_hostname
         | 
| 73 | 
            +
                @statsd = Statsd.new
         | 
| 74 | 
            +
              end
         | 
| 75 | 
            +
             | 
| 76 | 
            +
              def call(stats)
         | 
| 77 | 
            +
                counts = {}
         | 
| 78 | 
            +
                count_keys = [:backlog, :running, :pool_capacity, :max_threads]
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                if stats[:worker_status] # Clustered mode - Multiple workers
         | 
| 81 | 
            +
                  stats[:worker_status].each do |worker|
         | 
| 82 | 
            +
                    stat = worker[:last_status]
         | 
| 83 | 
            +
                    count_keys.each do |key|
         | 
| 84 | 
            +
                      count_if_present counts, key, stat
         | 
| 85 | 
            +
                    end
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
             | 
| 88 | 
            +
                  gauge(:workers, stats[:workers], :type => :count)
         | 
| 89 | 
            +
                  gauge(:workers, stats[:booted_workers], :type => :booted)
         | 
| 90 | 
            +
                  gauge(:workers, stats[:old_workers], :type => :old)
         | 
| 91 | 
            +
                else # Single mode - Single worker
         | 
| 92 | 
            +
                  count_keys.each do |key|
         | 
| 93 | 
            +
                    count_if_present counts, key, stats
         | 
| 94 | 
            +
                  end
         | 
| 95 | 
            +
                end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
         | 
| 98 | 
            +
                gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
         | 
| 99 | 
            +
                gauge(:threads, counts[:running], :type => :running) if counts[:running]
         | 
| 100 | 
            +
                gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
         | 
| 101 | 
            +
              end
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              private
         | 
| 104 | 
            +
             | 
| 105 | 
            +
              attr_reader :hostname
         | 
| 106 | 
            +
             | 
| 107 | 
            +
              def fetch_hostname
         | 
| 108 | 
            +
                # Configure hostname as reported for the Puma metrics with the
         | 
| 109 | 
            +
                # APPSIGNAL_HOSTNAME environment variable.
         | 
| 110 | 
            +
                env_hostname = ENV["APPSIGNAL_HOSTNAME"]
         | 
| 111 | 
            +
                return env_hostname if env_hostname
         | 
| 112 | 
            +
             | 
| 113 | 
            +
                # Auto detect hostname as fallback. May be inaccurate.
         | 
| 114 | 
            +
                Socket.gethostname
         | 
| 115 | 
            +
              end
         | 
| 116 | 
            +
             | 
| 117 | 
            +
              def gauge(field, count, tags = {})
         | 
| 118 | 
            +
                @statsd.gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
         | 
| 119 | 
            +
              end
         | 
| 120 | 
            +
             | 
| 121 | 
            +
              def count_if_present(counts, key, stats)
         | 
| 122 | 
            +
                stat_value = stats[key]
         | 
| 123 | 
            +
                return unless stat_value
         | 
| 124 | 
            +
             | 
| 125 | 
            +
                counts[key] ||= 0
         | 
| 126 | 
            +
                counts[key] += stat_value
         | 
| 127 | 
            +
              end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
              class Statsd
         | 
| 130 | 
            +
                def initialize
         | 
| 131 | 
            +
                  # StatsD server location as configured in AppSignal agent StatsD server.
         | 
| 132 | 
            +
                  @host = "127.0.0.1"
         | 
| 133 | 
            +
                  @port = 8125
         | 
| 134 | 
            +
                end
         | 
| 135 | 
            +
             | 
| 136 | 
            +
                def gauge(metric_name, value, tags)
         | 
| 137 | 
            +
                  send_metric "g", metric_name, value, tags
         | 
| 138 | 
            +
                end
         | 
| 139 | 
            +
             | 
| 140 | 
            +
                private
         | 
| 141 | 
            +
             | 
| 142 | 
            +
                attr_reader :host, :port
         | 
| 143 | 
            +
             | 
| 144 | 
            +
                def send_metric(type, metric_name, metric_value, tags_hash)
         | 
| 145 | 
            +
                  tags = tags_hash.map { |key, value| "#{key}:#{value}" }.join(",")
         | 
| 146 | 
            +
                  data = "#{metric_name}:#{metric_value}|#{type}|##{tags}"
         | 
| 147 | 
            +
             | 
| 148 | 
            +
                  # Open (and close) a new socket every time because we don't know when the
         | 
| 149 | 
            +
                  # plugin will exit and when to cleanly close the socket connection.
         | 
| 150 | 
            +
                  socket = UDPSocket.new
         | 
| 151 | 
            +
                  socket.send(data, 0, host, port)
         | 
| 152 | 
            +
                ensure
         | 
| 153 | 
            +
                  socket && socket.close
         | 
| 25 154 | 
             
                end
         | 
| 26 155 | 
             
              end
         | 
| 27 156 | 
             
            end
         | 
| @@ -35,29 +35,6 @@ describe Appsignal::Hooks::PumaHook do | |
| 35 35 | 
             
                      # Does not error on call
         | 
| 36 36 | 
             
                      Appsignal::Hooks::PumaHook.new.install
         | 
| 37 37 | 
             
                    end
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                    context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
         | 
| 40 | 
            -
                      before do
         | 
| 41 | 
            -
                        # Set in lib/puma/appsignal.rb
         | 
| 42 | 
            -
                        APPSIGNAL_PUMA_PLUGIN_LOADED = true
         | 
| 43 | 
            -
                      end
         | 
| 44 | 
            -
                      after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
         | 
| 45 | 
            -
             | 
| 46 | 
            -
                      it "does not add the Puma minutely probe" do
         | 
| 47 | 
            -
                        Appsignal::Hooks::PumaHook.new.install
         | 
| 48 | 
            -
                        expect(Appsignal::Minutely.probes[:puma]).to be_nil
         | 
| 49 | 
            -
                      end
         | 
| 50 | 
            -
                    end
         | 
| 51 | 
            -
             | 
| 52 | 
            -
                    context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
         | 
| 53 | 
            -
                      it "adds the Puma minutely probe" do
         | 
| 54 | 
            -
                        expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
         | 
| 55 | 
            -
             | 
| 56 | 
            -
                        Appsignal::Hooks::PumaHook.new.install
         | 
| 57 | 
            -
                        probe = Appsignal::Minutely.probes[:puma]
         | 
| 58 | 
            -
                        expect(probe).to eql(Appsignal::Probes::PumaProbe)
         | 
| 59 | 
            -
                      end
         | 
| 60 | 
            -
                    end
         | 
| 61 38 | 
             
                  end
         | 
| 62 39 |  | 
| 63 40 | 
             
                  context "when in clustered mode" do
         | 
| @@ -81,29 +58,6 @@ describe Appsignal::Hooks::PumaHook do | |
| 81 58 | 
             
                      cluster.stop_workers
         | 
| 82 59 | 
             
                      expect(cluster.instance_variable_get(:@called)).to be(true)
         | 
| 83 60 | 
             
                    end
         | 
| 84 | 
            -
             | 
| 85 | 
            -
                    context "with APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
         | 
| 86 | 
            -
                      before do
         | 
| 87 | 
            -
                        # Set in lib/puma/appsignal.rb
         | 
| 88 | 
            -
                        APPSIGNAL_PUMA_PLUGIN_LOADED = true
         | 
| 89 | 
            -
                      end
         | 
| 90 | 
            -
                      after { Object.send :remove_const, :APPSIGNAL_PUMA_PLUGIN_LOADED }
         | 
| 91 | 
            -
             | 
| 92 | 
            -
                      it "does not add the Puma minutely probe" do
         | 
| 93 | 
            -
                        Appsignal::Hooks::PumaHook.new.install
         | 
| 94 | 
            -
                        expect(Appsignal::Minutely.probes[:puma]).to be_nil
         | 
| 95 | 
            -
                      end
         | 
| 96 | 
            -
                    end
         | 
| 97 | 
            -
             | 
| 98 | 
            -
                    context "without APPSIGNAL_PUMA_PLUGIN_LOADED defined" do
         | 
| 99 | 
            -
                      it "adds the Puma minutely probe" do
         | 
| 100 | 
            -
                        expect(defined?(APPSIGNAL_PUMA_PLUGIN_LOADED)).to be_nil
         | 
| 101 | 
            -
             | 
| 102 | 
            -
                        Appsignal::Hooks::PumaHook.new.install
         | 
| 103 | 
            -
                        probe = Appsignal::Minutely.probes[:puma]
         | 
| 104 | 
            -
                        expect(probe).to eql(Appsignal::Probes::PumaProbe)
         | 
| 105 | 
            -
                      end
         | 
| 106 | 
            -
                    end
         | 
| 107 61 | 
             
                  end
         | 
| 108 62 | 
             
                end
         | 
| 109 63 | 
             
              end
         | 
| @@ -1,6 +1,4 @@ | |
| 1 1 | 
             
            RSpec.describe "Puma plugin" do
         | 
| 2 | 
            -
              include WaitForHelper
         | 
| 3 | 
            -
             | 
| 4 2 | 
             
              class MockPumaLauncher
         | 
| 5 3 | 
             
                def events
         | 
| 6 4 | 
             
                  return @events if defined?(@events)
         | 
| @@ -10,110 +8,288 @@ RSpec.describe "Puma plugin" do | |
| 10 8 | 
             
              end
         | 
| 11 9 |  | 
| 12 10 | 
             
              class MockPumaEvents
         | 
| 13 | 
            -
                 | 
| 14 | 
            -
             | 
| 15 | 
            -
             | 
| 11 | 
            +
                attr_reader :logs
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                def initialize
         | 
| 14 | 
            +
                  @logs = []
         | 
| 15 | 
            +
                end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def log(message)
         | 
| 18 | 
            +
                  @logs << [:log, message]
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def debug(message)
         | 
| 22 | 
            +
                  @logs << [:debug, message]
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                def error(message)
         | 
| 26 | 
            +
                  @logs << [:error, message]
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              # StatsD server used for these tests.
         | 
| 31 | 
            +
              # Open a UDPSocket and listen for messages sent by the AppSignal Puma plugin.
         | 
| 32 | 
            +
              class StatsdServer
         | 
| 33 | 
            +
                def start
         | 
| 34 | 
            +
                  stop
         | 
| 35 | 
            +
                  @socket = UDPSocket.new
         | 
| 36 | 
            +
                  @socket.bind("127.0.0.1", 8125)
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                  loop do
         | 
| 39 | 
            +
                    begin
         | 
| 40 | 
            +
                      # Listen for messages and track them on the messages Array.
         | 
| 41 | 
            +
                      packet = @socket.recvfrom(1024)
         | 
| 42 | 
            +
                      track_message packet.first
         | 
| 43 | 
            +
                    rescue Errno::EBADF # rubocop:disable Lint/HandleExceptions
         | 
| 44 | 
            +
                      # Ignore error for JRuby 9.1.17.0 specifically, it doesn't appear to
         | 
| 45 | 
            +
                      # happen on 9.2.18.0. It doesn't break the tests themselves, ignoring
         | 
| 46 | 
            +
                      # this error. It's probably a timing issue where it tries to read
         | 
| 47 | 
            +
                      # from the socket after it's closed.
         | 
| 48 | 
            +
                    end
         | 
| 49 | 
            +
                  end
         | 
| 50 | 
            +
                end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
                def stop
         | 
| 53 | 
            +
                  @socket && @socket.close
         | 
| 54 | 
            +
                ensure
         | 
| 55 | 
            +
                  @socket = nil
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                def messages
         | 
| 59 | 
            +
                  @messages ||= []
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
             | 
| 62 | 
            +
                private
         | 
| 63 | 
            +
             | 
| 64 | 
            +
                def track_message(message)
         | 
| 65 | 
            +
                  @messages_mutex ||= Mutex.new
         | 
| 66 | 
            +
                  @messages_mutex.synchronize { messages << message }
         | 
| 16 67 | 
             
                end
         | 
| 17 68 | 
             
              end
         | 
| 18 69 |  | 
| 19 70 | 
             
              let(:probe) { MockProbe.new }
         | 
| 20 71 | 
             
              let(:launcher) { MockPumaLauncher.new }
         | 
| 72 | 
            +
              let(:hostname) { Socket.gethostname }
         | 
| 73 | 
            +
              let(:expected_default_tags) { { "hostname" => hostname } }
         | 
| 74 | 
            +
              let(:stats_data) { { :backlog => 1 } }
         | 
| 21 75 | 
             
              before do
         | 
| 22 76 | 
             
                module Puma
         | 
| 23 77 | 
             
                  def self.stats
         | 
| 78 | 
            +
                    JSON.dump(@_stats_data)
         | 
| 79 | 
            +
                  end
         | 
| 80 | 
            +
             | 
| 81 | 
            +
                  def self.stats_hash
         | 
| 82 | 
            +
                    @_stats_data
         | 
| 24 83 | 
             
                  end
         | 
| 25 84 |  | 
| 26 | 
            -
                  def self. | 
| 27 | 
            -
                     | 
| 28 | 
            -
                    before = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
         | 
| 29 | 
            -
             | 
| 30 | 
            -
                    # An abbreviated version of what happens in Puma::Cluster#run
         | 
| 31 | 
            -
                    launcher = MockPumaLauncher.new
         | 
| 32 | 
            -
                    plugin = Plugin.plugin.new
         | 
| 33 | 
            -
                    plugin.start(launcher)
         | 
| 34 | 
            -
                    launcher.events.on_booted.call
         | 
| 35 | 
            -
             | 
| 36 | 
            -
                    # Wait for minutely probe thread to finish starting
         | 
| 37 | 
            -
                    sleep 0.005
         | 
| 38 | 
            -
             | 
| 39 | 
            -
                    # Capture any new threads running after application is preloaded.
         | 
| 40 | 
            -
                    # Any threads created during the preloading phase will not be
         | 
| 41 | 
            -
                    # carried over into the forked workers. Puma warns about these
         | 
| 42 | 
            -
                    # but the minutely probe thread should only exist in the main process.
         | 
| 43 | 
            -
                    after = Thread.list.reject { |t| t.thread_variable_get(:fork_safe) }
         | 
| 44 | 
            -
                    $stdout.puts "! WARNING: Detected #{after.size - before.size} Thread(s) started in app boot" if after.size > before.size
         | 
| 85 | 
            +
                  def self._set_stats=(data)
         | 
| 86 | 
            +
                    @_stats_data = data
         | 
| 45 87 | 
             
                  end
         | 
| 46 88 |  | 
| 47 89 | 
             
                  class Plugin
         | 
| 48 90 | 
             
                    class << self
         | 
| 49 | 
            -
                      attr_reader : | 
| 91 | 
            +
                      attr_reader :appsignal_plugin
         | 
| 50 92 |  | 
| 51 93 | 
             
                      def create(&block)
         | 
| 52 | 
            -
                        @ | 
| 53 | 
            -
                        @ | 
| 94 | 
            +
                        @appsignal_plugin = Class.new(::Puma::Plugin)
         | 
| 95 | 
            +
                        @appsignal_plugin.class_eval(&block)
         | 
| 54 96 | 
             
                      end
         | 
| 55 97 | 
             
                    end
         | 
| 56 | 
            -
                  end
         | 
| 57 | 
            -
                end
         | 
| 58 98 |  | 
| 59 | 
            -
             | 
| 60 | 
            -
                ENV["APPSIGNAL_ENABLE_MINUTELY_PROBES"] = "true"
         | 
| 61 | 
            -
                Appsignal.config = project_fixture_config
         | 
| 62 | 
            -
                # Speed up test time
         | 
| 63 | 
            -
                allow(Appsignal::Minutely).to receive(:initial_wait_time).and_return(0.001)
         | 
| 64 | 
            -
                allow(Appsignal::Minutely).to receive(:wait_time).and_return(0.001)
         | 
| 99 | 
            +
                    attr_reader :in_background_block
         | 
| 65 100 |  | 
| 66 | 
            -
             | 
| 101 | 
            +
                    def in_background(&block)
         | 
| 102 | 
            +
                      @in_background_block = block
         | 
| 103 | 
            +
                    end
         | 
| 104 | 
            +
                  end
         | 
| 105 | 
            +
                end
         | 
| 106 | 
            +
                Puma._set_stats = stats_data
         | 
| 67 107 | 
             
                load File.expand_path("../lib/puma/plugin/appsignal.rb", APPSIGNAL_SPEC_DIR)
         | 
| 108 | 
            +
             | 
| 109 | 
            +
                @statsd = StatsdServer.new
         | 
| 110 | 
            +
                @server_thread = Thread.new { @statsd.start }
         | 
| 111 | 
            +
                @server_thread.abort_on_exception = true
         | 
| 68 112 | 
             
              end
         | 
| 69 113 | 
             
              after do
         | 
| 70 | 
            -
                 | 
| 71 | 
            -
             | 
| 72 | 
            -
                Object.send | 
| 114 | 
            +
                @statsd = nil
         | 
| 115 | 
            +
             | 
| 116 | 
            +
                Object.send(:remove_const, :Puma)
         | 
| 117 | 
            +
                Object.send(:remove_const, :AppsignalPumaPlugin)
         | 
| 118 | 
            +
              end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
              def run(plugin)
         | 
| 121 | 
            +
                @client_thread = Thread.new { start_plugin(plugin) }
         | 
| 122 | 
            +
                @client_thread.abort_on_exception = true
         | 
| 123 | 
            +
                sleep 0.03
         | 
| 124 | 
            +
              ensure
         | 
| 125 | 
            +
                stop_all
         | 
| 73 126 | 
             
              end
         | 
| 74 127 |  | 
| 75 | 
            -
               | 
| 76 | 
            -
                 | 
| 77 | 
            -
             | 
| 78 | 
            -
                plugin = Puma::Plugin.plugin.new
         | 
| 79 | 
            -
                expect(launcher.events.on_booted).to be_nil
         | 
| 128 | 
            +
              def appsignal_plugin
         | 
| 129 | 
            +
                Puma::Plugin.appsignal_plugin
         | 
| 130 | 
            +
              end
         | 
| 80 131 |  | 
| 132 | 
            +
              def start_plugin(plugin_class)
         | 
| 133 | 
            +
                plugin = plugin_class.new
         | 
| 134 | 
            +
                # Speed up test by not waiting for 60 seconds initial wait time and loop
         | 
| 135 | 
            +
                # interval.
         | 
| 136 | 
            +
                allow(plugin).to receive(:sleep_time).and_return(0.01)
         | 
| 81 137 | 
             
                plugin.start(launcher)
         | 
| 82 | 
            -
                 | 
| 83 | 
            -
             | 
| 138 | 
            +
                plugin.in_background_block.call
         | 
| 139 | 
            +
              end
         | 
| 140 | 
            +
             | 
| 141 | 
            +
              # Stop all threads in test and stop listening on the UDPSocket
         | 
| 142 | 
            +
              def stop_all
         | 
| 143 | 
            +
                @client_thread.kill if defined?(@client_thread) && @client_thread
         | 
| 144 | 
            +
                @server_thread.kill if defined?(@server_thread) && @server_thread
         | 
| 145 | 
            +
                @statsd.stop if defined?(@statsd) && @statsd
         | 
| 146 | 
            +
                @client_thread = nil
         | 
| 147 | 
            +
                @server_thread = nil
         | 
| 148 | 
            +
              end
         | 
| 149 | 
            +
             | 
| 150 | 
            +
              def logs
         | 
| 151 | 
            +
                launcher.events.logs
         | 
| 152 | 
            +
              end
         | 
| 153 | 
            +
             | 
| 154 | 
            +
              def messages
         | 
| 155 | 
            +
                @statsd.messages.map do |message|
         | 
| 156 | 
            +
                  metric, type, tags_string = message.split("|")
         | 
| 157 | 
            +
                  metric_name, metric_value = metric.split(":")
         | 
| 158 | 
            +
                  tags = {}
         | 
| 159 | 
            +
                  tags_string[1..-1].split(",").each do |tag|
         | 
| 160 | 
            +
                    key, value = tag.split(":")
         | 
| 161 | 
            +
                    tags[key] = value
         | 
| 162 | 
            +
                  end
         | 
| 163 | 
            +
                  {
         | 
| 164 | 
            +
                    :name => metric_name,
         | 
| 165 | 
            +
                    :value => metric_value.to_i,
         | 
| 166 | 
            +
                    :type => type,
         | 
| 167 | 
            +
                    :tags => tags
         | 
| 168 | 
            +
                  }
         | 
| 169 | 
            +
                end
         | 
| 170 | 
            +
              end
         | 
| 171 | 
            +
             | 
| 172 | 
            +
              def expect_gauge(metric_name, metric_value, tags_hash = {})
         | 
| 173 | 
            +
                expect(messages).to include(
         | 
| 174 | 
            +
                  :name => "puma_#{metric_name}",
         | 
| 175 | 
            +
                  :value => metric_value,
         | 
| 176 | 
            +
                  :type => "g",
         | 
| 177 | 
            +
                  :tags => expected_default_tags.merge(tags_hash)
         | 
| 178 | 
            +
                )
         | 
| 179 | 
            +
              end
         | 
| 180 | 
            +
             | 
| 181 | 
            +
              context "with multiple worker stats" do
         | 
| 182 | 
            +
                let(:stats_data) do
         | 
| 183 | 
            +
                  {
         | 
| 184 | 
            +
                    :workers => 2,
         | 
| 185 | 
            +
                    :booted_workers => 2,
         | 
| 186 | 
            +
                    :old_workers => 0,
         | 
| 187 | 
            +
                    :worker_status => [
         | 
| 188 | 
            +
                      {
         | 
| 189 | 
            +
                        :last_status => {
         | 
| 190 | 
            +
                          :backlog => 0,
         | 
| 191 | 
            +
                          :running => 5,
         | 
| 192 | 
            +
                          :pool_capacity => 5,
         | 
| 193 | 
            +
                          :max_threads => 5
         | 
| 194 | 
            +
                        }
         | 
| 195 | 
            +
                      },
         | 
| 196 | 
            +
                      {
         | 
| 197 | 
            +
                        :last_status => {
         | 
| 198 | 
            +
                          :backlog => 0,
         | 
| 199 | 
            +
                          :running => 5,
         | 
| 200 | 
            +
                          :pool_capacity => 5,
         | 
| 201 | 
            +
                          :max_threads => 5
         | 
| 202 | 
            +
                        }
         | 
| 203 | 
            +
                      }
         | 
| 204 | 
            +
                    ]
         | 
| 205 | 
            +
                  }
         | 
| 206 | 
            +
                end
         | 
| 207 | 
            +
             | 
| 208 | 
            +
                it "collects puma stats as guage metrics with the (summed) worker metrics" do
         | 
| 209 | 
            +
                  run(appsignal_plugin)
         | 
| 210 | 
            +
             | 
| 211 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 212 | 
            +
                  expect_gauge(:workers, 2, "type" => "count")
         | 
| 213 | 
            +
                  expect_gauge(:workers, 2, "type" => "booted")
         | 
| 214 | 
            +
                  expect_gauge(:workers, 0, "type" => "old")
         | 
| 215 | 
            +
                  expect_gauge(:connection_backlog, 0)
         | 
| 216 | 
            +
                  expect_gauge(:pool_capacity, 10)
         | 
| 217 | 
            +
                  expect_gauge(:threads, 10, "type" => "running")
         | 
| 218 | 
            +
                  expect_gauge(:threads, 10, "type" => "max")
         | 
| 219 | 
            +
                end
         | 
| 220 | 
            +
              end
         | 
| 221 | 
            +
             | 
| 222 | 
            +
              context "with single worker stats" do
         | 
| 223 | 
            +
                let(:stats_data) do
         | 
| 224 | 
            +
                  {
         | 
| 225 | 
            +
                    :backlog => 0,
         | 
| 226 | 
            +
                    :running => 5,
         | 
| 227 | 
            +
                    :pool_capacity => 5,
         | 
| 228 | 
            +
                    :max_threads => 5
         | 
| 229 | 
            +
                  }
         | 
| 230 | 
            +
                end
         | 
| 231 | 
            +
             | 
| 232 | 
            +
                it "calls `puma_gauge` with the (summed) worker metrics" do
         | 
| 233 | 
            +
                  run(appsignal_plugin)
         | 
| 234 | 
            +
             | 
| 235 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 236 | 
            +
                  expect_gauge(:connection_backlog, 0)
         | 
| 237 | 
            +
                  expect_gauge(:pool_capacity, 5)
         | 
| 238 | 
            +
                  expect_gauge(:threads, 5, "type" => "running")
         | 
| 239 | 
            +
                  expect_gauge(:threads, 5, "type" => "max")
         | 
| 240 | 
            +
                end
         | 
| 241 | 
            +
              end
         | 
| 84 242 |  | 
| 85 | 
            -
             | 
| 86 | 
            -
                 | 
| 243 | 
            +
              context "when using APPSIGNAL_HOSTNAME" do
         | 
| 244 | 
            +
                let(:hostname) { "my-host-name" }
         | 
| 245 | 
            +
                before { ENV["APPSIGNAL_HOSTNAME"] = hostname }
         | 
| 246 | 
            +
                after { ENV.delete("APPSIGNAL_HOSTNAME") }
         | 
| 87 247 |  | 
| 88 | 
            -
                 | 
| 89 | 
            -
             | 
| 248 | 
            +
                it "reports the APPSIGNAL_HOSTNAME as the hostname tag value" do
         | 
| 249 | 
            +
                  run(appsignal_plugin)
         | 
| 250 | 
            +
             | 
| 251 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 252 | 
            +
                  expect_gauge(:connection_backlog, 1)
         | 
| 253 | 
            +
                end
         | 
| 90 254 | 
             
              end
         | 
| 91 255 |  | 
| 92 | 
            -
               | 
| 93 | 
            -
                 | 
| 94 | 
            -
             | 
| 256 | 
            +
              context "without Puma.stats_hash" do
         | 
| 257 | 
            +
                before do
         | 
| 258 | 
            +
                  Puma.singleton_class.send(:remove_method, :stats_hash)
         | 
| 259 | 
            +
                end
         | 
| 260 | 
            +
             | 
| 261 | 
            +
                it "fetches metrics from Puma.stats instead" do
         | 
| 262 | 
            +
                  run(appsignal_plugin)
         | 
| 95 263 |  | 
| 96 | 
            -
             | 
| 264 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 265 | 
            +
                  expect(logs).to_not include([kind_of(Symbol), "AppSignal: No Puma stats to report."])
         | 
| 266 | 
            +
                  expect_gauge(:connection_backlog, 1)
         | 
| 267 | 
            +
                end
         | 
| 97 268 | 
             
              end
         | 
| 98 269 |  | 
| 99 | 
            -
              context "without Puma.stats" do
         | 
| 100 | 
            -
                before  | 
| 270 | 
            +
              context "without Puma.stats and Puma.stats_hash" do
         | 
| 271 | 
            +
                before do
         | 
| 272 | 
            +
                  Puma.singleton_class.send(:remove_method, :stats)
         | 
| 273 | 
            +
                  Puma.singleton_class.send(:remove_method, :stats_hash)
         | 
| 274 | 
            +
                end
         | 
| 101 275 |  | 
| 102 | 
            -
                it "does not  | 
| 103 | 
            -
                   | 
| 104 | 
            -
                  expect(Appsignal::Minutely.probes[:puma]).to be_nil
         | 
| 105 | 
            -
                  plugin = Puma::Plugin.plugin.new
         | 
| 106 | 
            -
                  expect(launcher.events.on_booted).to be_nil
         | 
| 276 | 
            +
                it "does not fetch metrics" do
         | 
| 277 | 
            +
                  run(appsignal_plugin)
         | 
| 107 278 |  | 
| 108 | 
            -
                   | 
| 109 | 
            -
                  expect( | 
| 110 | 
            -
                  expect( | 
| 279 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 280 | 
            +
                  expect(logs).to include([:log, "AppSignal: No Puma stats to report."])
         | 
| 281 | 
            +
                  expect(messages).to be_empty
         | 
| 282 | 
            +
                end
         | 
| 283 | 
            +
              end
         | 
| 111 284 |  | 
| 112 | 
            -
             | 
| 113 | 
            -
             | 
| 285 | 
            +
              context "without running StatsD server" do
         | 
| 286 | 
            +
                it "does nothing" do
         | 
| 287 | 
            +
                  Appsignal.stop
         | 
| 288 | 
            +
                  stop_all
         | 
| 289 | 
            +
                  run(appsignal_plugin)
         | 
| 114 290 |  | 
| 115 | 
            -
                   | 
| 116 | 
            -
                   | 
| 291 | 
            +
                  expect(logs).to_not include([:error, kind_of(String)])
         | 
| 292 | 
            +
                  expect(messages).to be_empty
         | 
| 117 293 | 
             
                end
         | 
| 118 294 | 
             
              end
         | 
| 119 295 | 
             
            end
         | 
    
        metadata
    CHANGED
    
    | @@ -1,7 +1,7 @@ | |
| 1 1 | 
             
            --- !ruby/object:Gem::Specification
         | 
| 2 2 | 
             
            name: appsignal
         | 
| 3 3 | 
             
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            -
              version: 3.0. | 
| 4 | 
            +
              version: 3.0.7
         | 
| 5 5 | 
             
            platform: java
         | 
| 6 6 | 
             
            authors:
         | 
| 7 7 | 
             
            - Robert Beekman
         | 
| @@ -10,7 +10,7 @@ authors: | |
| 10 10 | 
             
            autorequire:
         | 
| 11 11 | 
             
            bindir: bin
         | 
| 12 12 | 
             
            cert_chain: []
         | 
| 13 | 
            -
            date: 2021- | 
| 13 | 
            +
            date: 2021-06-15 00:00:00.000000000 Z
         | 
| 14 14 | 
             
            dependencies:
         | 
| 15 15 | 
             
            - !ruby/object:Gem::Dependency
         | 
| 16 16 | 
             
              name: rack
         | 
| @@ -263,7 +263,6 @@ files: | |
| 263 263 | 
             
            - lib/appsignal/marker.rb
         | 
| 264 264 | 
             
            - lib/appsignal/minutely.rb
         | 
| 265 265 | 
             
            - lib/appsignal/probes.rb
         | 
| 266 | 
            -
            - lib/appsignal/probes/puma.rb
         | 
| 267 266 | 
             
            - lib/appsignal/probes/sidekiq.rb
         | 
| 268 267 | 
             
            - lib/appsignal/rack/generic_instrumentation.rb
         | 
| 269 268 | 
             
            - lib/appsignal/rack/rails_instrumentation.rb
         | 
| @@ -349,7 +348,6 @@ files: | |
| 349 348 | 
             
            - spec/lib/appsignal/logger_spec.rb
         | 
| 350 349 | 
             
            - spec/lib/appsignal/marker_spec.rb
         | 
| 351 350 | 
             
            - spec/lib/appsignal/minutely_spec.rb
         | 
| 352 | 
            -
            - spec/lib/appsignal/probes/puma_spec.rb
         | 
| 353 351 | 
             
            - spec/lib/appsignal/probes/sidekiq_spec.rb
         | 
| 354 352 | 
             
            - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
         | 
| 355 353 | 
             
            - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
         | 
| @@ -498,7 +496,6 @@ test_files: | |
| 498 496 | 
             
            - spec/lib/appsignal/logger_spec.rb
         | 
| 499 497 | 
             
            - spec/lib/appsignal/marker_spec.rb
         | 
| 500 498 | 
             
            - spec/lib/appsignal/minutely_spec.rb
         | 
| 501 | 
            -
            - spec/lib/appsignal/probes/puma_spec.rb
         | 
| 502 499 | 
             
            - spec/lib/appsignal/probes/sidekiq_spec.rb
         | 
| 503 500 | 
             
            - spec/lib/appsignal/rack/generic_instrumentation_spec.rb
         | 
| 504 501 | 
             
            - spec/lib/appsignal/rack/rails_instrumentation_spec.rb
         | 
| @@ -1,61 +0,0 @@ | |
| 1 | 
            -
            module Appsignal
         | 
| 2 | 
            -
              module Probes
         | 
| 3 | 
            -
                class PumaProbe
         | 
| 4 | 
            -
                  def initialize
         | 
| 5 | 
            -
                    @hostname = Appsignal.config[:hostname] || Socket.gethostname
         | 
| 6 | 
            -
                  end
         | 
| 7 | 
            -
             | 
| 8 | 
            -
                  # @api private
         | 
| 9 | 
            -
                  def call
         | 
| 10 | 
            -
                    puma_stats = fetch_puma_stats
         | 
| 11 | 
            -
                    return unless puma_stats
         | 
| 12 | 
            -
             | 
| 13 | 
            -
                    stats = JSON.parse puma_stats, :symbolize_names => true
         | 
| 14 | 
            -
                    counts = {}
         | 
| 15 | 
            -
                    count_keys = [:backlog, :running, :pool_capacity, :max_threads]
         | 
| 16 | 
            -
             | 
| 17 | 
            -
                    if stats[:worker_status] # Multiple workers
         | 
| 18 | 
            -
                      stats[:worker_status].each do |worker|
         | 
| 19 | 
            -
                        stat = worker[:last_status]
         | 
| 20 | 
            -
                        count_keys.each do |key|
         | 
| 21 | 
            -
                          count_if_present counts, key, stat
         | 
| 22 | 
            -
                        end
         | 
| 23 | 
            -
                      end
         | 
| 24 | 
            -
             | 
| 25 | 
            -
                      gauge(:workers, stats[:workers], :type => :count)
         | 
| 26 | 
            -
                      gauge(:workers, stats[:booted_workers], :type => :booted)
         | 
| 27 | 
            -
                      gauge(:workers, stats[:old_workers], :type => :old)
         | 
| 28 | 
            -
                    else # Single worker
         | 
| 29 | 
            -
                      count_keys.each do |key|
         | 
| 30 | 
            -
                        count_if_present counts, key, stats
         | 
| 31 | 
            -
                      end
         | 
| 32 | 
            -
                    end
         | 
| 33 | 
            -
             | 
| 34 | 
            -
                    gauge(:connection_backlog, counts[:backlog]) if counts[:backlog]
         | 
| 35 | 
            -
                    gauge(:pool_capacity, counts[:pool_capacity]) if counts[:pool_capacity]
         | 
| 36 | 
            -
                    gauge(:threads, counts[:running], :type => :running) if counts[:running]
         | 
| 37 | 
            -
                    gauge(:threads, counts[:max_threads], :type => :max) if counts[:max_threads]
         | 
| 38 | 
            -
                  end
         | 
| 39 | 
            -
             | 
| 40 | 
            -
                  private
         | 
| 41 | 
            -
             | 
| 42 | 
            -
                  attr_reader :hostname
         | 
| 43 | 
            -
             | 
| 44 | 
            -
                  def gauge(field, count, tags = {})
         | 
| 45 | 
            -
                    Appsignal.set_gauge("puma_#{field}", count, tags.merge(:hostname => hostname))
         | 
| 46 | 
            -
                  end
         | 
| 47 | 
            -
             | 
| 48 | 
            -
                  def count_if_present(counts, key, stats)
         | 
| 49 | 
            -
                    stat_value = stats[key]
         | 
| 50 | 
            -
                    return unless stat_value
         | 
| 51 | 
            -
                    counts[key] ||= 0
         | 
| 52 | 
            -
                    counts[key] += stat_value
         | 
| 53 | 
            -
                  end
         | 
| 54 | 
            -
             | 
| 55 | 
            -
                  def fetch_puma_stats
         | 
| 56 | 
            -
                    ::Puma.stats
         | 
| 57 | 
            -
                  rescue NoMethodError # rubocop:disable Lint/HandleExceptions
         | 
| 58 | 
            -
                  end
         | 
| 59 | 
            -
                end
         | 
| 60 | 
            -
              end
         | 
| 61 | 
            -
            end
         | 
| @@ -1,180 +0,0 @@ | |
| 1 | 
            -
            require "appsignal/probes/puma"
         | 
| 2 | 
            -
             | 
| 3 | 
            -
            describe Appsignal::Probes::PumaProbe do
         | 
| 4 | 
            -
              before(:context) do
         | 
| 5 | 
            -
                Appsignal.config = project_fixture_config
         | 
| 6 | 
            -
              end
         | 
| 7 | 
            -
              after(:context) do
         | 
| 8 | 
            -
                Appsignal.config = nil
         | 
| 9 | 
            -
              end
         | 
| 10 | 
            -
             | 
| 11 | 
            -
              let(:probe) { described_class.new }
         | 
| 12 | 
            -
             | 
| 13 | 
            -
              describe "hostname" do
         | 
| 14 | 
            -
                it "returns the socket hostname" do
         | 
| 15 | 
            -
                  expect(probe.send(:hostname)).to eql(Socket.gethostname)
         | 
| 16 | 
            -
                end
         | 
| 17 | 
            -
             | 
| 18 | 
            -
                context "with overridden hostname" do
         | 
| 19 | 
            -
                  around do |sample|
         | 
| 20 | 
            -
                    Appsignal.config[:hostname] = "frontend1"
         | 
| 21 | 
            -
                    sample.run
         | 
| 22 | 
            -
                    Appsignal.config[:hostname] = nil
         | 
| 23 | 
            -
                  end
         | 
| 24 | 
            -
                  it "returns the configured host" do
         | 
| 25 | 
            -
                    expect(probe.send(:hostname)).to eql("frontend1")
         | 
| 26 | 
            -
                  end
         | 
| 27 | 
            -
                end
         | 
| 28 | 
            -
              end
         | 
| 29 | 
            -
             | 
| 30 | 
            -
              describe "#call" do
         | 
| 31 | 
            -
                let(:expected_default_tags) { { :hostname => Socket.gethostname } }
         | 
| 32 | 
            -
             | 
| 33 | 
            -
                context "with multiple worker stats" do
         | 
| 34 | 
            -
                  before(:context) do
         | 
| 35 | 
            -
                    class Puma
         | 
| 36 | 
            -
                      def self.stats
         | 
| 37 | 
            -
                        {
         | 
| 38 | 
            -
                          "workers" => 2,
         | 
| 39 | 
            -
                          "booted_workers" => 2,
         | 
| 40 | 
            -
                          "old_workers" => 0,
         | 
| 41 | 
            -
                          "worker_status" => [
         | 
| 42 | 
            -
                            {
         | 
| 43 | 
            -
                              "last_status" => {
         | 
| 44 | 
            -
                                "backlog" => 0,
         | 
| 45 | 
            -
                                "running" => 5,
         | 
| 46 | 
            -
                                "pool_capacity" => 5,
         | 
| 47 | 
            -
                                "max_threads" => 5
         | 
| 48 | 
            -
                              }
         | 
| 49 | 
            -
                            },
         | 
| 50 | 
            -
                            {
         | 
| 51 | 
            -
                              "last_status" => {
         | 
| 52 | 
            -
                                "backlog" => 0,
         | 
| 53 | 
            -
                                "running" => 5,
         | 
| 54 | 
            -
                                "pool_capacity" => 5,
         | 
| 55 | 
            -
                                "max_threads" => 5
         | 
| 56 | 
            -
                              }
         | 
| 57 | 
            -
                            }
         | 
| 58 | 
            -
                          ]
         | 
| 59 | 
            -
                        }.to_json
         | 
| 60 | 
            -
                      end
         | 
| 61 | 
            -
                    end
         | 
| 62 | 
            -
                  end
         | 
| 63 | 
            -
                  after(:context) { Object.send(:remove_const, :Puma) }
         | 
| 64 | 
            -
             | 
| 65 | 
            -
                  it "calls `puma_gauge` with the (summed) worker metrics" do
         | 
| 66 | 
            -
                    expect_gauge(:workers, 2, :type => :count)
         | 
| 67 | 
            -
                    expect_gauge(:workers, 2, :type => :booted)
         | 
| 68 | 
            -
                    expect_gauge(:workers, 0, :type => :old)
         | 
| 69 | 
            -
             | 
| 70 | 
            -
                    expect_gauge(:connection_backlog, 0)
         | 
| 71 | 
            -
                    expect_gauge(:pool_capacity, 10)
         | 
| 72 | 
            -
                    expect_gauge(:threads, 10, :type => :running)
         | 
| 73 | 
            -
                    expect_gauge(:threads, 10, :type => :max)
         | 
| 74 | 
            -
             | 
| 75 | 
            -
                    probe.call
         | 
| 76 | 
            -
                  end
         | 
| 77 | 
            -
                end
         | 
| 78 | 
            -
             | 
| 79 | 
            -
                context "with single worker stats" do
         | 
| 80 | 
            -
                  before(:context) do
         | 
| 81 | 
            -
                    class Puma
         | 
| 82 | 
            -
                      def self.stats
         | 
| 83 | 
            -
                        {
         | 
| 84 | 
            -
                          "backlog" => 0,
         | 
| 85 | 
            -
                          "running" => 5,
         | 
| 86 | 
            -
                          "pool_capacity" => 5,
         | 
| 87 | 
            -
                          "max_threads" => 5
         | 
| 88 | 
            -
                        }.to_json
         | 
| 89 | 
            -
                      end
         | 
| 90 | 
            -
                    end
         | 
| 91 | 
            -
                  end
         | 
| 92 | 
            -
                  after(:context) { Object.send(:remove_const, :Puma) }
         | 
| 93 | 
            -
             | 
| 94 | 
            -
                  it "calls `puma_gauge` with the (summed) worker metrics" do
         | 
| 95 | 
            -
                    expect_gauge(:connection_backlog, 0)
         | 
| 96 | 
            -
                    expect_gauge(:pool_capacity, 5)
         | 
| 97 | 
            -
                    expect_gauge(:threads, 5, :type => :running)
         | 
| 98 | 
            -
                    expect_gauge(:threads, 5, :type => :max)
         | 
| 99 | 
            -
                    probe.call
         | 
| 100 | 
            -
                  end
         | 
| 101 | 
            -
                end
         | 
| 102 | 
            -
             | 
| 103 | 
            -
                context "without stats" do
         | 
| 104 | 
            -
                  before(:context) do
         | 
| 105 | 
            -
                    class Puma
         | 
| 106 | 
            -
                      def self.stats
         | 
| 107 | 
            -
                      end
         | 
| 108 | 
            -
                    end
         | 
| 109 | 
            -
                  end
         | 
| 110 | 
            -
                  after(:context) { Object.send(:remove_const, :Puma) }
         | 
| 111 | 
            -
             | 
| 112 | 
            -
                  context "when it returns nil" do
         | 
| 113 | 
            -
                    it "does not track metrics" do
         | 
| 114 | 
            -
                      expect(probe).to_not receive(:puma_gauge)
         | 
| 115 | 
            -
                      probe.call
         | 
| 116 | 
            -
                    end
         | 
| 117 | 
            -
                  end
         | 
| 118 | 
            -
             | 
| 119 | 
            -
                  # Puma.stats raises a NoMethodError on a nil object on the first call.
         | 
| 120 | 
            -
                  context "when it returns a NoMethodError on the first call" do
         | 
| 121 | 
            -
                    let(:log) { StringIO.new }
         | 
| 122 | 
            -
             | 
| 123 | 
            -
                    it "ignores the first call and tracks the second call" do
         | 
| 124 | 
            -
                      use_logger_with log do
         | 
| 125 | 
            -
                        expect(Puma).to receive(:stats)
         | 
| 126 | 
            -
                          .and_raise(NoMethodError.new("undefined method `stats' for nil:NilClass"))
         | 
| 127 | 
            -
                        probe.call
         | 
| 128 | 
            -
             | 
| 129 | 
            -
                        expect(Puma).to receive(:stats).and_return({
         | 
| 130 | 
            -
                          "backlog" => 1,
         | 
| 131 | 
            -
                          "running" => 5,
         | 
| 132 | 
            -
                          "pool_capacity" => 4,
         | 
| 133 | 
            -
                          "max_threads" => 6
         | 
| 134 | 
            -
                        }.to_json)
         | 
| 135 | 
            -
             | 
| 136 | 
            -
                        expect_gauge(:connection_backlog, 1)
         | 
| 137 | 
            -
                        expect_gauge(:pool_capacity, 4)
         | 
| 138 | 
            -
                        expect_gauge(:threads, 5, :type => :running)
         | 
| 139 | 
            -
                        expect_gauge(:threads, 6, :type => :max)
         | 
| 140 | 
            -
                        probe.call
         | 
| 141 | 
            -
                      end
         | 
| 142 | 
            -
             | 
| 143 | 
            -
                      expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
         | 
| 144 | 
            -
                    end
         | 
| 145 | 
            -
                  end
         | 
| 146 | 
            -
             | 
| 147 | 
            -
                  context "when it does not have a complete stats payload" do
         | 
| 148 | 
            -
                    let(:log) { StringIO.new }
         | 
| 149 | 
            -
             | 
| 150 | 
            -
                    it "tracks whatever metrics we do have" do
         | 
| 151 | 
            -
                      use_logger_with log do
         | 
| 152 | 
            -
                        expect(Puma).to receive(:stats).and_return({
         | 
| 153 | 
            -
                          "backlog" => 1,
         | 
| 154 | 
            -
                          "running" => 5
         | 
| 155 | 
            -
                        }.to_json)
         | 
| 156 | 
            -
             | 
| 157 | 
            -
                        expect_gauge(:connection_backlog, 1)
         | 
| 158 | 
            -
                        expect_no_gauge(:pool_capacity)
         | 
| 159 | 
            -
                        expect_gauge(:threads, 5, :type => :running)
         | 
| 160 | 
            -
                        expect_no_gauge(:threads, :type => :max)
         | 
| 161 | 
            -
                        probe.call
         | 
| 162 | 
            -
                      end
         | 
| 163 | 
            -
             | 
| 164 | 
            -
                      expect(log_contents(log)).to_not contains_log(:error, "Error in minutely probe 'puma'")
         | 
| 165 | 
            -
                    end
         | 
| 166 | 
            -
                  end
         | 
| 167 | 
            -
                end
         | 
| 168 | 
            -
             | 
| 169 | 
            -
                def expect_gauge(key, value, tags = {})
         | 
| 170 | 
            -
                  expect(Appsignal).to receive(:set_gauge)
         | 
| 171 | 
            -
                    .with("puma_#{key}", value, expected_default_tags.merge(tags))
         | 
| 172 | 
            -
                    .and_call_original
         | 
| 173 | 
            -
                end
         | 
| 174 | 
            -
             | 
| 175 | 
            -
                def expect_no_gauge(key, tags = {})
         | 
| 176 | 
            -
                  expect(Appsignal).to_not receive(:set_gauge)
         | 
| 177 | 
            -
                    .with("puma_#{key}", anything, tags)
         | 
| 178 | 
            -
                end
         | 
| 179 | 
            -
              end
         | 
| 180 | 
            -
            end
         |