appsignal 4.0.3-java → 4.0.5-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.
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