appsignal 3.0.6 → 3.0.10

Sign up to get free protection for your applications and to get access to all the features.
@@ -105,7 +105,6 @@ module Appsignal
105
105
  paths_report = Paths.new
106
106
  data[:paths] = paths_report.report
107
107
  print_paths_section(paths_report)
108
- print_empty_line
109
108
 
110
109
  transmit_report_to_appsignal if send_report_to_appsignal?(options)
111
110
  end
@@ -327,6 +326,7 @@ module Appsignal
327
326
  puts "AppSignal library"
328
327
  data_section :library do
329
328
  save :language, "ruby"
329
+ puts_value "Language", "Ruby"
330
330
  puts_and_save :package_version, "Gem version", Appsignal::VERSION
331
331
  puts_and_save :agent_version, "Agent version", Appsignal::Extension.agent_version
332
332
  puts_and_save :extension_loaded, "Extension loaded", Appsignal.extension_loaded
@@ -399,6 +399,7 @@ module Appsignal
399
399
  puts " Architecture: #{report["architecture"]}"
400
400
  puts " Target: #{report["target"]}"
401
401
  puts " Musl override: #{report["musl_override"]}"
402
+ puts " Linux ARM override: #{report["linux_arm_override"]}"
402
403
  puts " Library type: #{report["library_type"]}"
403
404
  puts " Source: #{report["source"]}" if report["source"] != "remote"
404
405
  puts " Dependencies: #{report["dependencies"]}"
@@ -416,7 +417,7 @@ module Appsignal
416
417
  rbconfig = RbConfig::CONFIG
417
418
  puts "Host information"
418
419
  data_section :host do
419
- puts_and_save :architecture, "Architecture", rbconfig["host_cpu"]
420
+ puts_and_save :architecture, "Architecture", Appsignal::System.agent_architecture
420
421
 
421
422
  os_label = os = rbconfig["host_os"]
422
423
  os_label = "#{os} (Microsoft Windows is not supported.)" if Gem.win_platform?
@@ -573,9 +574,13 @@ module Appsignal
573
574
  "(file: #{ownership[:user]}:#{ownership[:uid]}, " \
574
575
  "process: #{process_user[:user]}:#{process_user[:uid]})"
575
576
  puts_value "Ownership?", owner, :level => 2
576
- return unless path.key?(:content)
577
- puts " Contents (last 10 lines):"
578
- puts path[:content].last(10)
577
+
578
+ if path.key?(:content)
579
+ puts " Contents (last 10 lines):"
580
+ puts path[:content].last(10)
581
+ else
582
+ print_empty_line
583
+ end
579
584
  end
580
585
 
581
586
  def print_empty_line
@@ -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
 
@@ -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
@@ -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.6".freeze
4
+ VERSION = "3.0.10".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
@@ -0,0 +1,18 @@
1
+ #!/bin/bash
2
+
3
+ set -eu
4
+
5
+ mkdir -p $HOME/bin
6
+ cache_key=v1-lintje-$LINTJE_VERSION
7
+ cache restore $cache_key
8
+
9
+ # File exists and is executable
10
+ if [ -x "$HOME/bin/lintje" ]; then
11
+ echo "Restored Lintje $LINTJE_VERSION from cache"
12
+ else
13
+ echo "Downloading Lintje $LINTJE_VERSION"
14
+ curl -L \
15
+ https://github.com/tombruijn/lintje/releases/download/v$LINTJE_VERSION/x86_64-unknown-linux-gnu.tar.gz | \
16
+ tar -xz --directory $HOME/bin
17
+ cache store $cache_key $HOME/bin/lintje
18
+ fi
@@ -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: {",
@@ -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