appsignal 4.0.3-java → 4.0.5-java

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +51 -0
  3. data/ext/agent.rb +27 -27
  4. data/lib/appsignal/check_in/cron.rb +2 -34
  5. data/lib/appsignal/check_in/scheduler.rb +192 -0
  6. data/lib/appsignal/check_in.rb +18 -0
  7. data/lib/appsignal/cli/diagnose.rb +1 -1
  8. data/lib/appsignal/config.rb +7 -0
  9. data/lib/appsignal/hooks/at_exit.rb +3 -1
  10. data/lib/appsignal/hooks/puma.rb +5 -1
  11. data/lib/appsignal/integrations/puma.rb +45 -0
  12. data/lib/appsignal/rack/abstract_middleware.rb +3 -47
  13. data/lib/appsignal/rack/body_wrapper.rb +15 -0
  14. data/lib/appsignal/rack/event_handler.rb +2 -0
  15. data/lib/appsignal/rack/hanami_middleware.rb +5 -1
  16. data/lib/appsignal/rack.rb +68 -0
  17. data/lib/appsignal/transmitter.rb +30 -7
  18. data/lib/appsignal/utils/ndjson.rb +15 -0
  19. data/lib/appsignal/utils.rb +1 -0
  20. data/lib/appsignal/version.rb +1 -1
  21. data/lib/appsignal.rb +1 -0
  22. data/spec/lib/appsignal/check_in/cron_spec.rb +202 -0
  23. data/spec/lib/appsignal/check_in/scheduler_spec.rb +443 -0
  24. data/spec/lib/appsignal/config_spec.rb +13 -0
  25. data/spec/lib/appsignal/environment_spec.rb +1 -1
  26. data/spec/lib/appsignal/hooks/at_exit_spec.rb +22 -0
  27. data/spec/lib/appsignal/hooks/puma_spec.rb +31 -23
  28. data/spec/lib/appsignal/integrations/puma_spec.rb +150 -0
  29. data/spec/lib/appsignal/probes_spec.rb +1 -6
  30. data/spec/lib/appsignal/rack/abstract_middleware_spec.rb +41 -122
  31. data/spec/lib/appsignal/rack/body_wrapper_spec.rb +29 -21
  32. data/spec/lib/appsignal/rack_spec.rb +180 -0
  33. data/spec/lib/appsignal/transmitter_spec.rb +48 -2
  34. data/spec/lib/appsignal_spec.rb +5 -0
  35. data/spec/spec_helper.rb +0 -7
  36. data/spec/support/helpers/config_helpers.rb +2 -1
  37. data/spec/support/helpers/take_at_most_helper.rb +21 -0
  38. data/spec/support/matchers/contains_log.rb +10 -3
  39. data/spec/support/mocks/hash_like.rb +10 -0
  40. data/spec/support/mocks/puma_mock.rb +43 -0
  41. metadata +11 -3
  42. data/spec/lib/appsignal/check_in_spec.rb +0 -136
@@ -19,7 +19,6 @@ module Appsignal
19
19
  @app = app
20
20
  @options = options
21
21
  @request_class = options.fetch(:request_class, ::Rack::Request)
22
- @params_method = options.fetch(:params_method, :params)
23
22
  @instrument_event_name = options.fetch(:instrument_event_name, nil)
24
23
  @report_errors = options.fetch(:report_errors, DEFAULT_ERROR_REPORTING)
25
24
  end
@@ -136,52 +135,9 @@ module Appsignal
136
135
  # Override this method to set metadata after the app is called.
137
136
  # Call `super` to also include the default set metadata.
138
137
  def add_transaction_metadata_after(transaction, request)
139
- transaction.set_metadata("path", request.path)
140
-
141
- request_method = request_method_for(request)
142
- transaction.set_metadata("method", request_method) if request_method
143
-
144
- transaction.add_params { params_for(request) }
145
- transaction.add_session_data { session_data_for(request) }
146
- transaction.add_headers do
147
- request.env if request.respond_to?(:env)
148
- end
149
-
150
- queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
151
- transaction.set_queue_start(queue_start) if queue_start
152
- end
153
-
154
- def params_for(request)
155
- return unless request.respond_to?(@params_method)
156
-
157
- request.send(@params_method)
158
- rescue => error
159
- Appsignal.internal_logger.error(
160
- "Exception while fetching params from '#{@request_class}##{@params_method}': " \
161
- "#{error.class} #{error}"
162
- )
163
- nil
164
- end
165
-
166
- def request_method_for(request)
167
- request.request_method
168
- rescue => error
169
- Appsignal.internal_logger.error(
170
- "Exception while fetching the HTTP request method: #{error.class}: #{error}"
171
- )
172
- nil
173
- end
174
-
175
- def session_data_for(request)
176
- return unless request.respond_to?(:session)
177
-
178
- request.session.to_h
179
- rescue => error
180
- Appsignal.internal_logger.error(
181
- "Exception while fetching session data from '#{@request_class}': " \
182
- "#{error.class} #{error}"
183
- )
184
- nil
138
+ Appsignal::Rack::ApplyRackRequest
139
+ .new(request, @options)
140
+ .apply_to(transaction)
185
141
  end
186
142
 
187
143
  def request_for(env)
@@ -57,6 +57,21 @@ module Appsignal
57
57
  @transaction.set_error(error)
58
58
  raise error
59
59
  end
60
+
61
+ # Return whether the wrapped body responds to the method if this class does not.
62
+ # Based on:
63
+ # https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L16-L24
64
+ def respond_to_missing?(method_name, include_all = false)
65
+ super || @body.respond_to?(method_name, include_all)
66
+ end
67
+
68
+ # Delegate missing methods to the wrapped body.
69
+ # Based on:
70
+ # https://github.com/rack/rack/blob/0ed580bbe3858ffe5d530adf1bdad9ef9c03407c/lib/rack/body_proxy.rb#L44-L61
71
+ def method_missing(method_name, *args, &block)
72
+ @body.__send__(method_name, *args, &block)
73
+ end
74
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
60
75
  end
61
76
 
62
77
  # The standard Rack body wrapper which exposes "each" for iterating
@@ -79,6 +79,8 @@ module Appsignal
79
79
  #
80
80
  # The EventHandler.on_finish callback should be called first, this is
81
81
  # just a fallback if that doesn't get called.
82
+ #
83
+ # One such scenario is when a Puma "lowlevel_error" occurs.
82
84
  Appsignal::Transaction.complete_current!
83
85
  end
84
86
  transaction.start_event
@@ -5,13 +5,17 @@ module Appsignal
5
5
  # @api private
6
6
  class HanamiMiddleware < AbstractMiddleware
7
7
  def initialize(app, options = {})
8
- options[:params_method] ||= :params
8
+ options[:params_method] = nil
9
9
  options[:instrument_event_name] ||= "process_action.hanami"
10
10
  super
11
11
  end
12
12
 
13
13
  private
14
14
 
15
+ def add_transaction_metadata_after(transaction, request)
16
+ transaction.add_params { params_for(request) }
17
+ end
18
+
15
19
  def params_for(request)
16
20
  ::Hanami::Action.params_class.new(request.env).to_h
17
21
  end
@@ -37,5 +37,73 @@ module Appsignal
37
37
  end
38
38
  end
39
39
  end
40
+
41
+ class ApplyRackRequest
42
+ attr_reader :request, :options
43
+
44
+ def initialize(request, options = {})
45
+ @request = request
46
+ @options = options
47
+ @params_method = options.fetch(:params_method, :params)
48
+ end
49
+
50
+ def apply_to(transaction)
51
+ request_path = request.path
52
+ transaction.set_metadata("request_path", request_path)
53
+ # TODO: Remove in next major/minor version
54
+ transaction.set_metadata("path", request_path)
55
+
56
+ request_method = request_method_for(request)
57
+ if request_method
58
+ transaction.set_metadata("request_method", request_method)
59
+ # TODO: Remove in next major/minor version
60
+ transaction.set_metadata("method", request_method)
61
+ end
62
+
63
+ transaction.add_params { params_for(request) }
64
+ transaction.add_session_data { session_data_for(request) }
65
+ transaction.add_headers do
66
+ request.env if request.respond_to?(:env)
67
+ end
68
+
69
+ queue_start = Appsignal::Rack::Utils.queue_start_from(request.env)
70
+ transaction.set_queue_start(queue_start) if queue_start
71
+ end
72
+
73
+ private
74
+
75
+ def params_for(request)
76
+ return if !@params_method || !request.respond_to?(@params_method)
77
+
78
+ request.send(@params_method)
79
+ rescue => error
80
+ Appsignal.internal_logger.error(
81
+ "Exception while fetching params from '#{request.class}##{@params_method}': " \
82
+ "#{error.class} #{error}"
83
+ )
84
+ nil
85
+ end
86
+
87
+ def request_method_for(request)
88
+ request.request_method
89
+ rescue => error
90
+ Appsignal.internal_logger.error(
91
+ "Exception while fetching the HTTP request method: #{error.class}: #{error}"
92
+ )
93
+ nil
94
+ end
95
+
96
+ def session_data_for(request)
97
+ return unless request.respond_to?(:session)
98
+
99
+ request.session.to_h
100
+ rescue => error
101
+ Appsignal.internal_logger.error(
102
+ "Exception while fetching session data from '#{request.class}': " \
103
+ "#{error.class} #{error}"
104
+ )
105
+ nil
106
+ end
107
+ end
40
108
  end
41
109
  end
@@ -9,7 +9,8 @@ require "json"
9
9
  module Appsignal
10
10
  # @api private
11
11
  class Transmitter
12
- CONTENT_TYPE = "application/json; charset=UTF-8"
12
+ JSON_CONTENT_TYPE = "application/json; charset=UTF-8"
13
+ NDJSON_CONTENT_TYPE = "application/x-ndjson; charset=UTF-8"
13
14
 
14
15
  HTTP_ERRORS = [
15
16
  EOFError,
@@ -53,17 +54,39 @@ module Appsignal
53
54
  end
54
55
  end
55
56
 
56
- def transmit(payload)
57
- config.logger.debug "Transmitting payload to #{uri}"
58
- http_client.request(http_post(payload))
57
+ def transmit(payload, format: :json)
58
+ Appsignal.internal_logger.debug "Transmitting payload to #{uri}"
59
+ http_client.request(http_post(payload, :format => format))
59
60
  end
60
61
 
61
62
  private
62
63
 
63
- def http_post(payload)
64
+ def http_post(payload, format: :json)
64
65
  Net::HTTP::Post.new(uri.request_uri).tap do |request|
65
- request["Content-Type"] = CONTENT_TYPE
66
- request.body = Appsignal::Utils::JSON.generate(payload)
66
+ request["Content-Type"] = content_type_for(format)
67
+ request.body = generate_body_for(format, payload)
68
+ end
69
+ end
70
+
71
+ def content_type_for(format)
72
+ case format
73
+ when :json
74
+ JSON_CONTENT_TYPE
75
+ when :ndjson
76
+ NDJSON_CONTENT_TYPE
77
+ else
78
+ raise ArgumentError, "Unknown Content-Type header for format: #{format}"
79
+ end
80
+ end
81
+
82
+ def generate_body_for(format, payload)
83
+ case format
84
+ when :json
85
+ Appsignal::Utils::JSON.generate(payload)
86
+ when :ndjson
87
+ Appsignal::Utils::NDJSON.generate(payload)
88
+ else
89
+ raise ArgumentError, "Unknown body generator for format: #{format}"
67
90
  end
68
91
  end
69
92
 
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module Utils
5
+ class NDJSON
6
+ class << self
7
+ def generate(body)
8
+ body.map do |element|
9
+ Appsignal::Utils::JSON.generate(element)
10
+ end.join("\n")
11
+ end
12
+ end
13
+ end
14
+ end
15
+ end
@@ -12,4 +12,5 @@ require "appsignal/utils/data"
12
12
  require "appsignal/utils/hash_sanitizer"
13
13
  require "appsignal/utils/integration_logger"
14
14
  require "appsignal/utils/json"
15
+ require "appsignal/utils/ndjson"
15
16
  require "appsignal/utils/query_params_sanitizer"
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.0.3"
4
+ VERSION = "4.0.5"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -164,6 +164,7 @@ module Appsignal
164
164
  end
165
165
  Appsignal::Extension.stop
166
166
  Appsignal::Probes.stop
167
+ Appsignal::CheckIn.stop
167
168
  end
168
169
 
169
170
  # Configure the AppSignal Ruby gem using a DSL.
@@ -0,0 +1,202 @@
1
+ describe Appsignal::CheckIn::Cron do
2
+ let(:log_stream) { std_stream }
3
+ let(:logs) { log_contents(log_stream) }
4
+ let(:appsignal_options) { {} }
5
+ let(:config) { project_fixture_config }
6
+ let(:cron_checkin) { described_class.new(:identifier => "cron-checkin-name") }
7
+ let(:transmitter) { Appsignal::Transmitter.new("https://checkin-endpoint.invalid") }
8
+ let(:scheduler) { Appsignal::CheckIn::Scheduler.new }
9
+
10
+ before do
11
+ start_agent(
12
+ :options => appsignal_options,
13
+ :internal_logger => test_logger(log_stream)
14
+ )
15
+ allow(Appsignal::CheckIn).to receive(:scheduler).and_return(scheduler)
16
+ allow(Appsignal::CheckIn).to receive(:transmitter).and_return(transmitter)
17
+ end
18
+ after { stop_scheduler }
19
+
20
+ def stop_scheduler
21
+ scheduler.stop
22
+ end
23
+
24
+ describe "when Appsignal is not active" do
25
+ let(:appsignal_options) { { :active => false } }
26
+
27
+ it "does not transmit any events" do
28
+ expect(Appsignal).to_not be_started
29
+
30
+ cron_checkin.start
31
+ cron_checkin.finish
32
+ stop_scheduler
33
+
34
+ expect(logs).to contains_log(
35
+ :debug,
36
+ /Cannot transmit cron check-in `cron-checkin-name` start event .+: AppSignal is not active/
37
+ )
38
+ expect(logs).to contains_log(
39
+ :debug,
40
+ /Cannot transmit cron check-in `cron-checkin-name` finish event .+: AppSignal is not active/
41
+ )
42
+ end
43
+ end
44
+
45
+ describe "when AppSignal is stopped" do
46
+ it "does not transmit any events" do
47
+ Appsignal.stop
48
+
49
+ cron_checkin.start
50
+ cron_checkin.finish
51
+
52
+ expect(logs).to contains_log(
53
+ :debug,
54
+ "Cannot transmit cron check-in `cron-checkin-name` start event"
55
+ )
56
+ expect(logs).to contains_log(
57
+ :debug,
58
+ "Cannot transmit cron check-in `cron-checkin-name` finish event"
59
+ )
60
+ end
61
+ end
62
+
63
+ describe "#start" do
64
+ it "sends a cron check-in start" do
65
+ cron_checkin.start
66
+
67
+ expect(transmitter).to receive(:transmit).with([hash_including(
68
+ :identifier => "cron-checkin-name",
69
+ :kind => "start",
70
+ :check_in_type => "cron"
71
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "200", nil))
72
+
73
+ scheduler.stop
74
+
75
+ expect(logs).to_not contains_log(:error)
76
+ expect(logs).to contains_log(
77
+ :debug,
78
+ "Scheduling cron check-in `cron-checkin-name` start event"
79
+ )
80
+ expect(logs).to contains_log(
81
+ :debug,
82
+ "Transmitted cron check-in `cron-checkin-name` start event"
83
+ )
84
+ end
85
+
86
+ it "logs an error if it fails" do
87
+ cron_checkin.start
88
+
89
+ expect(transmitter).to receive(:transmit).with([hash_including(
90
+ :identifier => "cron-checkin-name",
91
+ :kind => "start",
92
+ :check_in_type => "cron"
93
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "499", nil))
94
+
95
+ scheduler.stop
96
+
97
+ expect(logs).to contains_log(
98
+ :debug,
99
+ "Scheduling cron check-in `cron-checkin-name` start event"
100
+ )
101
+ expect(logs).to contains_log(
102
+ :error,
103
+ /Failed to transmit cron check-in `cron-checkin-name` start event .+: 499 status code/
104
+ )
105
+ end
106
+ end
107
+
108
+ describe "#finish" do
109
+ it "sends a cron check-in finish" do
110
+ cron_checkin.finish
111
+
112
+ expect(transmitter).to receive(:transmit).with([hash_including(
113
+ :identifier => "cron-checkin-name",
114
+ :kind => "finish",
115
+ :check_in_type => "cron"
116
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "200", nil))
117
+
118
+ scheduler.stop
119
+ expect(logs).to_not contains_log(:error)
120
+ expect(logs).to contains_log(
121
+ :debug,
122
+ "Scheduling cron check-in `cron-checkin-name` finish event"
123
+ )
124
+ expect(logs).to contains_log(
125
+ :debug,
126
+ "Transmitted cron check-in `cron-checkin-name` finish event"
127
+ )
128
+ end
129
+
130
+ it "logs an error if it fails" do
131
+ cron_checkin.finish
132
+
133
+ expect(transmitter).to receive(:transmit).with([hash_including(
134
+ :identifier => "cron-checkin-name",
135
+ :kind => "finish",
136
+ :check_in_type => "cron"
137
+ )], :format => :ndjson).and_return(Net::HTTPResponse.new(nil, "499", nil))
138
+
139
+ scheduler.stop
140
+
141
+ expect(logs).to contains_log(
142
+ :debug,
143
+ "Scheduling cron check-in `cron-checkin-name` finish event"
144
+ )
145
+ expect(logs).to contains_log(
146
+ :error,
147
+ /Failed to transmit cron check-in `cron-checkin-name` finish event .+: 499 status code/
148
+ )
149
+ end
150
+ end
151
+
152
+ describe ".cron" do
153
+ describe "when a block is given" do
154
+ it "sends a cron check-in start and finish and return the block output" do
155
+ expect(scheduler).to receive(:schedule).with(hash_including(
156
+ :kind => "start",
157
+ :identifier => "cron-checkin-with-block",
158
+ :check_in_type => "cron"
159
+ ))
160
+
161
+ expect(scheduler).to receive(:schedule).with(hash_including(
162
+ :kind => "finish",
163
+ :identifier => "cron-checkin-with-block",
164
+ :check_in_type => "cron"
165
+ ))
166
+
167
+ output = Appsignal::CheckIn.cron("cron-checkin-with-block") { "output" }
168
+ expect(output).to eq("output")
169
+ end
170
+
171
+ it "does not send a cron check-in finish event when an error is raised" do
172
+ expect(scheduler).to receive(:schedule).with(hash_including(
173
+ :kind => "start",
174
+ :identifier => "cron-checkin-with-block",
175
+ :check_in_type => "cron"
176
+ ))
177
+
178
+ expect(scheduler).not_to receive(:schedule).with(hash_including(
179
+ :kind => "finish",
180
+ :identifier => "cron-checkin-with-block",
181
+ :check_in_type => "cron"
182
+ ))
183
+
184
+ expect do
185
+ Appsignal::CheckIn.cron("cron-checkin-with-block") { raise "error" }
186
+ end.to raise_error(RuntimeError, "error")
187
+ end
188
+ end
189
+
190
+ describe "when no block is given" do
191
+ it "only sends a cron check-in finish event" do
192
+ expect(scheduler).to receive(:schedule).with(hash_including(
193
+ :kind => "finish",
194
+ :identifier => "cron-checkin-without-block",
195
+ :check_in_type => "cron"
196
+ ))
197
+
198
+ Appsignal::CheckIn.cron("cron-checkin-without-block")
199
+ end
200
+ end
201
+ end
202
+ end