appsignal 2.10.7-java → 2.11.0.alpha.2-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/.semaphore/semaphore.yml +45 -53
- data/CHANGELOG.md +18 -0
- data/build_matrix.yml +13 -6
- data/ext/agent.yml +19 -19
- data/ext/appsignal_extension.c +10 -1
- data/ext/base.rb +15 -4
- data/gemfiles/padrino.gemfile +2 -2
- data/lib/appsignal.rb +21 -1
- data/lib/appsignal/capistrano.rb +2 -0
- data/lib/appsignal/config.rb +6 -2
- data/lib/appsignal/environment.rb +126 -0
- data/lib/appsignal/extension/jruby.rb +10 -0
- data/lib/appsignal/hooks/net_http.rb +2 -0
- data/lib/appsignal/hooks/puma.rb +2 -58
- data/lib/appsignal/hooks/redis.rb +2 -0
- data/lib/appsignal/hooks/sequel.rb +2 -0
- data/lib/appsignal/hooks/sidekiq.rb +2 -99
- data/lib/appsignal/integrations/delayed_job_plugin.rb +16 -3
- data/lib/appsignal/integrations/object.rb +4 -0
- data/lib/appsignal/integrations/resque_active_job.rb +12 -4
- data/lib/appsignal/probes/puma.rb +61 -0
- data/lib/appsignal/probes/sidekiq.rb +102 -0
- data/lib/appsignal/rack/js_exception_catcher.rb +5 -2
- data/lib/appsignal/transaction.rb +22 -7
- data/lib/appsignal/version.rb +1 -1
- data/lib/puma/plugin/appsignal.rb +2 -1
- data/spec/lib/appsignal/cli/diagnose_spec.rb +2 -1
- data/spec/lib/appsignal/config_spec.rb +6 -1
- data/spec/lib/appsignal/environment_spec.rb +167 -0
- data/spec/lib/appsignal/hooks/delayed_job_spec.rb +198 -166
- data/spec/lib/appsignal/hooks/puma_spec.rb +2 -181
- data/spec/lib/appsignal/hooks/sidekiq_spec.rb +256 -462
- data/spec/lib/appsignal/integrations/padrino_spec.rb +1 -1
- data/spec/lib/appsignal/integrations/resque_active_job_spec.rb +55 -13
- data/spec/lib/appsignal/probes/puma_spec.rb +180 -0
- data/spec/lib/appsignal/probes/sidekiq_spec.rb +201 -0
- data/spec/lib/appsignal/rack/js_exception_catcher_spec.rb +9 -4
- data/spec/lib/appsignal/transaction_spec.rb +30 -13
- data/spec/lib/appsignal_spec.rb +22 -0
- data/spec/lib/puma/appsignal_spec.rb +1 -1
- data/spec/support/helpers/dependency_helper.rb +5 -0
- data/spec/support/helpers/env_helpers.rb +1 -1
- data/spec/support/helpers/environment_metdata_helper.rb +16 -0
- data/spec/support/stubs/sidekiq/api.rb +1 -1
- metadata +19 -8
@@ -142,7 +142,7 @@ if DependencyHelper.padrino_present?
|
|
142
142
|
expect_a_transaction_to_be_created
|
143
143
|
# Uses path for action name
|
144
144
|
expect(transaction).to receive(:set_action_if_nil).with("PadrinoTestApp#unknown")
|
145
|
-
expect(response).to match_response(404, "
|
145
|
+
expect(response).to match_response(404, "GET /404")
|
146
146
|
end
|
147
147
|
end
|
148
148
|
|
@@ -1,21 +1,18 @@
|
|
1
|
-
if DependencyHelper.
|
1
|
+
if DependencyHelper.active_job_present?
|
2
2
|
require "active_job"
|
3
|
+
require File.expand_path("lib/appsignal/integrations/resque_active_job.rb")
|
4
|
+
|
5
|
+
class TestActiveJob < ActiveJob::Base
|
6
|
+
include Appsignal::Integrations::ResqueActiveJobPlugin
|
7
|
+
|
8
|
+
def perform(_)
|
9
|
+
end
|
10
|
+
end
|
3
11
|
|
4
12
|
describe Appsignal::Integrations::ResqueActiveJobPlugin do
|
5
|
-
let(:file) { File.expand_path("lib/appsignal/integrations/resque_active_job.rb") }
|
6
13
|
let(:args) { "argument" }
|
7
14
|
let(:job) { TestActiveJob.new(args) }
|
8
|
-
before
|
9
|
-
load file
|
10
|
-
start_agent
|
11
|
-
|
12
|
-
class TestActiveJob < ActiveJob::Base
|
13
|
-
include Appsignal::Integrations::ResqueActiveJobPlugin
|
14
|
-
|
15
|
-
def perform(_)
|
16
|
-
end
|
17
|
-
end
|
18
|
-
end
|
15
|
+
before { start_agent }
|
19
16
|
|
20
17
|
def perform
|
21
18
|
keep_transactions do
|
@@ -141,5 +138,50 @@ if DependencyHelper.resque_present? && DependencyHelper.active_job_present?
|
|
141
138
|
end
|
142
139
|
end
|
143
140
|
end
|
141
|
+
|
142
|
+
context "without queue time" do
|
143
|
+
it "does not add queue time to transaction" do
|
144
|
+
# TODO: Not available in transaction.to_h yet.
|
145
|
+
# https://github.com/appsignal/appsignal-agent/issues/293
|
146
|
+
expect(Appsignal).to receive(:monitor_single_transaction).with(
|
147
|
+
"perform_job.resque",
|
148
|
+
a_hash_including(:queue_start => nil)
|
149
|
+
).and_call_original
|
150
|
+
|
151
|
+
perform
|
152
|
+
expect(last_transaction.to_h).to include(
|
153
|
+
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
154
|
+
"action" => "TestActiveJob#perform",
|
155
|
+
"events" => [
|
156
|
+
hash_including("name" => "perform_job.resque")
|
157
|
+
]
|
158
|
+
)
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
if DependencyHelper.rails6_present?
|
163
|
+
context "with queue time" do
|
164
|
+
it "adds queue time to transction" do
|
165
|
+
queue_start = "2017-01-01 10:01:00UTC"
|
166
|
+
queue_start_time = Time.parse(queue_start)
|
167
|
+
# TODO: Not available in transaction.to_h yet.
|
168
|
+
# https://github.com/appsignal/appsignal-agent/issues/293
|
169
|
+
expect(Appsignal).to receive(:monitor_single_transaction).with(
|
170
|
+
"perform_job.resque",
|
171
|
+
a_hash_including(:queue_start => queue_start_time)
|
172
|
+
).and_call_original
|
173
|
+
job.enqueued_at = queue_start
|
174
|
+
|
175
|
+
perform
|
176
|
+
expect(last_transaction.to_h).to include(
|
177
|
+
"namespace" => Appsignal::Transaction::BACKGROUND_JOB,
|
178
|
+
"action" => "TestActiveJob#perform",
|
179
|
+
"events" => [
|
180
|
+
hash_including("name" => "perform_job.resque")
|
181
|
+
]
|
182
|
+
)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
144
186
|
end
|
145
187
|
end
|
@@ -0,0 +1,180 @@
|
|
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
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require "appsignal/probes/sidekiq"
|
2
|
+
|
3
|
+
describe Appsignal::Probes::SidekiqProbe do
|
4
|
+
describe "#call" do
|
5
|
+
let(:probe) { described_class.new }
|
6
|
+
let(:redis_hostname) { "localhost" }
|
7
|
+
let(:expected_default_tags) { { :hostname => "localhost" } }
|
8
|
+
before do
|
9
|
+
Appsignal.config = project_fixture_config
|
10
|
+
module Sidekiq
|
11
|
+
def self.redis_info
|
12
|
+
{
|
13
|
+
"connected_clients" => 2,
|
14
|
+
"used_memory" => 1024,
|
15
|
+
"used_memory_rss" => 512
|
16
|
+
}
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.redis
|
20
|
+
yield Client.new
|
21
|
+
end
|
22
|
+
|
23
|
+
class Client
|
24
|
+
def connection
|
25
|
+
{ :host => "localhost" }
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Stats
|
30
|
+
class << self
|
31
|
+
attr_reader :calls
|
32
|
+
|
33
|
+
def count_call
|
34
|
+
@calls ||= -1
|
35
|
+
@calls += 1
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def workers_size
|
40
|
+
# First method called, so count it towards a call
|
41
|
+
self.class.count_call
|
42
|
+
24
|
43
|
+
end
|
44
|
+
|
45
|
+
def processes_size
|
46
|
+
25
|
47
|
+
end
|
48
|
+
|
49
|
+
# Return two different values for two separate calls.
|
50
|
+
# This allows us to test the delta of the value send as a gauge.
|
51
|
+
def processed
|
52
|
+
[10, 15][self.class.calls]
|
53
|
+
end
|
54
|
+
|
55
|
+
# Return two different values for two separate calls.
|
56
|
+
# This allows us to test the delta of the value send as a gauge.
|
57
|
+
def failed
|
58
|
+
[10, 13][self.class.calls]
|
59
|
+
end
|
60
|
+
|
61
|
+
def retry_size
|
62
|
+
12
|
63
|
+
end
|
64
|
+
|
65
|
+
# Return two different values for two separate calls.
|
66
|
+
# This allows us to test the delta of the value send as a gauge.
|
67
|
+
def dead_size
|
68
|
+
[10, 12][self.class.calls]
|
69
|
+
end
|
70
|
+
|
71
|
+
def scheduled_size
|
72
|
+
14
|
73
|
+
end
|
74
|
+
|
75
|
+
def enqueued
|
76
|
+
15
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
class Queue
|
81
|
+
Queue = Struct.new(:name, :size, :latency)
|
82
|
+
|
83
|
+
def self.all
|
84
|
+
[
|
85
|
+
Queue.new("default", 10, 12),
|
86
|
+
Queue.new("critical", 1, 2)
|
87
|
+
]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
92
|
+
after { Object.send(:remove_const, "Sidekiq") }
|
93
|
+
|
94
|
+
describe ".dependencies_present?" do
|
95
|
+
before do
|
96
|
+
class Redis; end
|
97
|
+
Redis.const_set(:VERSION, version)
|
98
|
+
end
|
99
|
+
after { Object.send(:remove_const, "Redis") }
|
100
|
+
|
101
|
+
context "when Redis version is < 3.3.5" do
|
102
|
+
let(:version) { "3.3.4" }
|
103
|
+
|
104
|
+
it "does not start probe" do
|
105
|
+
expect(described_class.dependencies_present?).to be_falsy
|
106
|
+
end
|
107
|
+
end
|
108
|
+
|
109
|
+
context "when Redis version is >= 3.3.5" do
|
110
|
+
let(:version) { "3.3.5" }
|
111
|
+
|
112
|
+
it "does not start probe" do
|
113
|
+
expect(described_class.dependencies_present?).to be_truthy
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
it "loads Sidekiq::API" do
|
119
|
+
expect(defined?(Sidekiq::API)).to be_falsy
|
120
|
+
probe
|
121
|
+
expect(defined?(Sidekiq::API)).to be_truthy
|
122
|
+
end
|
123
|
+
|
124
|
+
it "logs config on initialize" do
|
125
|
+
log = capture_logs { probe }
|
126
|
+
expect(log).to contains_log(:debug, "Initializing Sidekiq probe\n")
|
127
|
+
end
|
128
|
+
|
129
|
+
it "logs used hostname on call once" do
|
130
|
+
log = capture_logs { probe.call }
|
131
|
+
expect(log).to contains_log(
|
132
|
+
:debug,
|
133
|
+
%(Sidekiq probe: Using Redis server hostname "localhost" as hostname)
|
134
|
+
)
|
135
|
+
log = capture_logs { probe.call }
|
136
|
+
# Match more logs with incompelete message
|
137
|
+
expect(log).to_not contains_log(:debug, %(Sidekiq probe: ))
|
138
|
+
end
|
139
|
+
|
140
|
+
it "collects custom metrics" do
|
141
|
+
expect_gauge("worker_count", 24).twice
|
142
|
+
expect_gauge("process_count", 25).twice
|
143
|
+
expect_gauge("connection_count", 2).twice
|
144
|
+
expect_gauge("memory_usage", 1024).twice
|
145
|
+
expect_gauge("memory_usage_rss", 512).twice
|
146
|
+
expect_gauge("job_count", 5, :status => :processed) # Gauge delta
|
147
|
+
expect_gauge("job_count", 3, :status => :failed) # Gauge delta
|
148
|
+
expect_gauge("job_count", 12, :status => :retry_queue).twice
|
149
|
+
expect_gauge("job_count", 2, :status => :died) # Gauge delta
|
150
|
+
expect_gauge("job_count", 14, :status => :scheduled).twice
|
151
|
+
expect_gauge("job_count", 15, :status => :enqueued).twice
|
152
|
+
expect_gauge("queue_length", 10, :queue => "default").twice
|
153
|
+
expect_gauge("queue_latency", 12_000, :queue => "default").twice
|
154
|
+
expect_gauge("queue_length", 1, :queue => "critical").twice
|
155
|
+
expect_gauge("queue_latency", 2_000, :queue => "critical").twice
|
156
|
+
# Call probe twice so we can calculate the delta for some gauge values
|
157
|
+
probe.call
|
158
|
+
probe.call
|
159
|
+
end
|
160
|
+
|
161
|
+
context "when `redis_info` is not defined" do
|
162
|
+
before do
|
163
|
+
allow(Sidekiq).to receive(:respond_to?).with(:redis_info).and_return(false)
|
164
|
+
end
|
165
|
+
|
166
|
+
it "does not collect redis metrics" do
|
167
|
+
expect_gauge("connection_count", 2).never
|
168
|
+
expect_gauge("memory_usage", 1024).never
|
169
|
+
expect_gauge("memory_usage_rss", 512).never
|
170
|
+
probe.call
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
context "when hostname is configured for probe" do
|
175
|
+
let(:redis_hostname) { "my_redis_server" }
|
176
|
+
let(:probe) { described_class.new(:hostname => redis_hostname) }
|
177
|
+
|
178
|
+
it "uses the redis hostname for the hostname tag" do
|
179
|
+
allow(Appsignal).to receive(:set_gauge).and_call_original
|
180
|
+
log = capture_logs { probe }
|
181
|
+
expect(log).to contains_log(
|
182
|
+
:debug,
|
183
|
+
%(Initializing Sidekiq probe with config: {:hostname=>"#{redis_hostname}"})
|
184
|
+
)
|
185
|
+
log = capture_logs { probe.call }
|
186
|
+
expect(log).to contains_log(
|
187
|
+
:debug,
|
188
|
+
"Sidekiq probe: Using hostname config option #{redis_hostname.inspect} as hostname"
|
189
|
+
)
|
190
|
+
expect(Appsignal).to have_received(:set_gauge)
|
191
|
+
.with(anything, anything, :hostname => redis_hostname).at_least(:once)
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
def expect_gauge(key, value, tags = {})
|
196
|
+
expect(Appsignal).to receive(:set_gauge)
|
197
|
+
.with("sidekiq_#{key}", value, expected_default_tags.merge(tags))
|
198
|
+
.and_call_original
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|