appsignal 3.0.4 → 3.0.9

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.
@@ -3,5 +3,4 @@ module Appsignal
3
3
  end
4
4
  end
5
5
 
6
- require "appsignal/probes/puma"
7
6
  require "appsignal/probes/sidekiq"
@@ -8,6 +8,7 @@ module Appsignal
8
8
  # @api private
9
9
  module System
10
10
  LINUX_TARGET = "linux".freeze
11
+ LINUX_ARM_ARCHITECTURE = "aarch64".freeze
11
12
  MUSL_TARGET = "linux-musl".freeze
12
13
  FREEBSD_TARGET = "freebsd".freeze
13
14
  GEM_EXT_PATH = File.expand_path("../../../ext", __FILE__).freeze
@@ -18,15 +19,18 @@ module Appsignal
18
19
 
19
20
  # Detect agent and extension platform build
20
21
  #
21
- # Used by `ext/extconf.rb` to select which build it should download and
22
+ # Used by `ext/*` to select which build it should download and
22
23
  # install.
23
24
  #
24
- # Use `export APPSIGNAL_BUILD_FOR_MUSL=1` if the detection doesn't work
25
- # and to force selection of the musl build.
25
+ # - Use `export APPSIGNAL_BUILD_FOR_MUSL=1` if the detection doesn't work
26
+ # and to force selection of the musl build.
27
+ # - Use `export APPSIGNAL_BUILD_FOR_LINUX_ARM=1` to enable the experimental
28
+ # Linux ARM build.
26
29
  #
27
30
  # @api private
28
31
  # @return [String]
29
32
  def self.agent_platform
33
+ return LINUX_TARGET if force_linux_arm_build?
30
34
  return MUSL_TARGET if force_musl_build?
31
35
 
32
36
  host_os = RbConfig::CONFIG["host_os"].downcase
@@ -53,6 +57,22 @@ module Appsignal
53
57
  local_os
54
58
  end
55
59
 
60
+ # Detect agent and extension architecture build
61
+ #
62
+ # Used by the `ext/*` tasks to select which architecture build it should download and install.
63
+ #
64
+ # - Use `export APPSIGNAL_BUILD_FOR_LINUX_ARM=1` to enable the experimental
65
+ # Linux ARM build.
66
+ #
67
+ # @api private
68
+ # @return [String]
69
+ def self.agent_architecture
70
+ return LINUX_ARM_ARCHITECTURE if force_linux_arm_build?
71
+
72
+ # Fallback on the Ruby
73
+ RbConfig::CONFIG["host_cpu"]
74
+ end
75
+
56
76
  # Returns whether or not the musl build was forced by the user.
57
77
  #
58
78
  # @api private
@@ -60,6 +80,13 @@ module Appsignal
60
80
  %w[true 1].include?(ENV["APPSIGNAL_BUILD_FOR_MUSL"])
61
81
  end
62
82
 
83
+ # Returns whether or not the linux ARM build was selected by the user.
84
+ #
85
+ # @api private
86
+ def self.force_linux_arm_build?
87
+ %w[true 1].include?(ENV["APPSIGNAL_BUILD_FOR_LINUX_ARM"])
88
+ end
89
+
63
90
  # @api private
64
91
  def self.versionify(version)
65
92
  Gem::Version.new(version)
@@ -526,7 +526,7 @@ module Appsignal
526
526
  end
527
527
 
528
528
  def cleaned_backtrace(backtrace)
529
- if defined?(::Rails) && backtrace
529
+ if defined?(::Rails) && Rails.respond_to?(:backtrace_cleaner) && backtrace
530
530
  ::Rails.backtrace_cleaner.clean(backtrace, nil)
531
531
  else
532
532
  backtrace
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "3.0.4".freeze
4
+ VERSION = "3.0.9".freeze
5
5
  end
@@ -1,27 +1,156 @@
1
- APPSIGNAL_PUMA_PLUGIN_LOADED = true
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
2
4
 
3
5
  # AppSignal Puma plugin
4
6
  #
5
- # This plugin ensures the minutely probe thread is started with the Puma
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 = nil)
17
- launcher.events.on_booted do
18
- require "appsignal"
19
- if ::Puma.respond_to?(:stats)
20
- require "appsignal/probes/puma"
21
- Appsignal::Minutely.probes.register :puma, Appsignal::Probes::PumaProbe
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
- Appsignal.start
24
- Appsignal.start_logger
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
@@ -270,9 +270,10 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
270
270
  "build" => {
271
271
  "time" => kind_of(String),
272
272
  "package_path" => File.expand_path("../../../../../", __FILE__),
273
- "architecture" => rbconfig["host_cpu"],
273
+ "architecture" => Appsignal::System.agent_architecture,
274
274
  "target" => Appsignal::System.agent_platform,
275
275
  "musl_override" => false,
276
+ "linux_arm_override" => false,
276
277
  "library_type" => jruby ? "dynamic" : "static",
277
278
  "source" => "remote",
278
279
  "dependencies" => kind_of(Hash),
@@ -301,9 +302,10 @@ describe Appsignal::CLI::Diagnose, :api_stub => true, :send_report => :yes_cli_i
301
302
  " Checksum: verified",
302
303
  "Build details",
303
304
  " Install time: 20",
304
- " Architecture: #{rbconfig["host_cpu"]}",
305
+ " Architecture: #{Appsignal::System.agent_architecture}",
305
306
  " Target: #{Appsignal::System.agent_platform}",
306
307
  " Musl override: false",
308
+ " Linux ARM override: false",
307
309
  " Library type: #{jruby ? "dynamic" : "static"}",
308
310
  " Dependencies: {",
309
311
  " Flags: {",
@@ -0,0 +1,74 @@
1
+ describe Appsignal::Hooks::ExconHook do
2
+ before :context do
3
+ start_agent
4
+ end
5
+
6
+ context "with Excon" do
7
+ before(:context) do
8
+ class Excon
9
+ def self.defaults
10
+ @defaults ||= {}
11
+ end
12
+ end
13
+ Appsignal::Hooks::ExconHook.new.install
14
+ end
15
+ after(:context) { Object.send(:remove_const, :Excon) }
16
+
17
+ describe "#dependencies_present?" do
18
+ subject { described_class.new.dependencies_present? }
19
+
20
+ it { is_expected.to be_truthy }
21
+ end
22
+
23
+ describe "#install" do
24
+ it "adds the AppSignal instrumentor to Excon" do
25
+ expect(Excon.defaults[:instrumentor]).to eql(Appsignal::Integrations::ExconIntegration)
26
+ end
27
+ end
28
+
29
+ describe "instrumentation" do
30
+ let!(:transaction) do
31
+ Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
32
+ end
33
+ around { |example| keep_transactions { example.run } }
34
+
35
+ it "instruments a http request" do
36
+ data = {
37
+ :host => "www.google.com",
38
+ :method => :get,
39
+ :scheme => "http"
40
+ }
41
+ Excon.defaults[:instrumentor].instrument("excon.request", data) {}
42
+
43
+ expect(transaction.to_h["events"]).to include(
44
+ hash_including(
45
+ "name" => "request.excon",
46
+ "title" => "GET http://www.google.com",
47
+ "body" => ""
48
+ )
49
+ )
50
+ end
51
+
52
+ it "instruments a http response" do
53
+ data = { :host => "www.google.com" }
54
+ Excon.defaults[:instrumentor].instrument("excon.response", data) {}
55
+
56
+ expect(transaction.to_h["events"]).to include(
57
+ hash_including(
58
+ "name" => "response.excon",
59
+ "title" => "www.google.com",
60
+ "body" => ""
61
+ )
62
+ )
63
+ end
64
+ end
65
+ end
66
+
67
+ context "without Excon" do
68
+ describe "#dependencies_present?" do
69
+ subject { described_class.new.dependencies_present? }
70
+
71
+ it { is_expected.to be_falsy }
72
+ end
73
+ end
74
+ 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
@@ -33,16 +33,17 @@ describe Appsignal::Hooks::RedisHook do
33
33
 
34
34
  context "instrumentation" do
35
35
  before do
36
+ start_agent
36
37
  # Stub Redis::Client class so that it doesn't perform an actual
37
38
  # Redis query. This class will be included (prepended) with the
38
39
  # AppSignal Redis integration.
39
40
  stub_const("Redis::Client", Class.new do
40
41
  def id
41
- :stub_id
42
+ "stub_id"
42
43
  end
43
44
 
44
- def process(_commands)
45
- :stub_process
45
+ def write(_commands)
46
+ "stub_write"
46
47
  end
47
48
  end)
48
49
  # Load the integration again for the stubbed Redis::Client class.
@@ -50,17 +51,40 @@ describe Appsignal::Hooks::RedisHook do
50
51
  # track if it was installed already or not.
51
52
  Appsignal::Hooks::RedisHook.new.install
52
53
  end
54
+ let!(:transaction) do
55
+ Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
56
+ end
57
+ around { |example| keep_transactions { example.run } }
53
58
 
54
59
  it "instrument a redis call" do
55
- Appsignal::Transaction.create("uuid", Appsignal::Transaction::HTTP_REQUEST, "test")
56
- expect(Appsignal::Transaction.current).to receive(:start_event)
57
- .at_least(:once)
58
- expect(Appsignal::Transaction.current).to receive(:finish_event)
59
- .at_least(:once)
60
- .with("query.redis", :stub_id, "get ?", 0)
60
+ client = Redis::Client.new
61
+ expect(client.write([:get, "key"])).to eql("stub_write")
61
62
 
63
+ transaction_hash = transaction.to_h
64
+ expect(transaction_hash["events"]).to include(
65
+ hash_including(
66
+ "name" => "query.redis",
67
+ "body" => "get ?",
68
+ "title" => "stub_id"
69
+ )
70
+ )
71
+ end
72
+
73
+ it "instrument a redis script call" do
62
74
  client = Redis::Client.new
63
- expect(client.process([[:get, "key"]])).to eql(:stub_process)
75
+ script = "return redis.call('set',KEYS[1],ARGV[1])"
76
+ keys = ["foo"]
77
+ argv = ["bar"]
78
+ expect(client.write([:eval, script, keys.size, keys, argv])).to eql("stub_write")
79
+
80
+ transaction_hash = transaction.to_h
81
+ expect(transaction_hash["events"]).to include(
82
+ hash_including(
83
+ "name" => "query.redis",
84
+ "body" => "#{script} ? ?",
85
+ "title" => "stub_id"
86
+ )
87
+ )
64
88
  end
65
89
  end
66
90
  end