appsignal 4.0.9 → 4.1.1

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e98be4df445350c51cb3b1d3543300257051dda917d8433a4503c9eda77d2cd6
4
- data.tar.gz: bca5d410c1a2fb91812d2cc63b19a04b0db6cafd18c368a1ece930915bbf0a66
3
+ metadata.gz: fd929ef03ad3a9ceec274cc57e036614e3b2dfb76be026b8c0b878a35a5ce654
4
+ data.tar.gz: ef77980f54715f5b4a05ee218df8a0e51b3f867643ca793ef790dda3f324e05c
5
5
  SHA512:
6
- metadata.gz: ca0b4443929b282883b1d74f0092526bb69b3c223a6a60af1de372cf6af2acde569e57ada8b67b8aaac6645ada48b2c253ec595c1aec72bd5c812d03b577e953
7
- data.tar.gz: 5e3d0d285a6ebf6a7b8beae27fdaceec6365aa016fa1181e1572f42054fced494771eac6fd62434efb5cd4dae7ee7ca542aa1b6ac5d680139ec7d24dfd238c55
6
+ metadata.gz: 0a6ad722785e28515a5d716df4dbf4aad9bc134718ebc2354cd4bcd3fda75c923cd1266f249e2d019cdc459ca43fc0ca2559cc4733859a05546a809f1e5f5410
7
+ data.tar.gz: 0cf285924ec8dd63cbcdb89a0354f76dfdc86d6737dc32c80f2538ae136b8653763b67a3507238db866751869d7c17f817eb2e766b4b7860584f4618be550d58
data/CHANGELOG.md CHANGED
@@ -1,5 +1,50 @@
1
1
  # AppSignal for Ruby gem Changelog
2
2
 
3
+ ## 4.1.1
4
+
5
+ _Published on 2024-09-28._
6
+
7
+ ### Changed
8
+
9
+ - Add the `reported_by` tag to errors reported by the Rails error reporter so the source of the error is easier to identify. (patch [ff98ed67](https://github.com/appsignal/appsignal-ruby/commit/ff98ed677bf30242c51261bf7e44c9b6ba2f33ac))
10
+
11
+ ### Fixed
12
+
13
+ - Fix no AppSignal internal logs being logged from Capistrano tasks. (patch [089d0325](https://github.com/appsignal/appsignal-ruby/commit/089d03251c3dc8b83658a4ebfade51ab6bed1771))
14
+ - Report all the config options set via `Appsignal.config` in the DSL config source in the diagnose report. Previously, it would only report the options from the last time `Appsignal.configure` was called. (patch [27b9aff7](https://github.com/appsignal/appsignal-ruby/commit/27b9aff7776646dfef6c55fa589024a71052e70b))
15
+ - Fix 'no implicit conversion of Pathname into String' error when parsing backtrace lines of error causes in Rails apps. (patch [b767f269](https://github.com/appsignal/appsignal-ruby/commit/b767f269c41cb7625d6869d8e8acb9b288292d19))
16
+
17
+ ## 4.1.0
18
+
19
+ _Published on 2024-09-26._
20
+
21
+ ### Added
22
+
23
+ - Add support for heartbeat check-ins.
24
+
25
+ Use the `Appsignal::CheckIn.heartbeat` method to send a single heartbeat check-in event from your application. This can be used, for example, in your application's main loop:
26
+
27
+ ```ruby
28
+ loop do
29
+ Appsignal::CheckIn.heartbeat("job_processor")
30
+ process_job
31
+ end
32
+ ```
33
+
34
+ Heartbeats are deduplicated and sent asynchronously, without blocking the current thread. Regardless of how often the `.heartbeat` method is called, at most one heartbeat with the same identifier will be sent every ten seconds.
35
+
36
+ Pass `continuous: true` as the second argument to send heartbeats continuously during the entire lifetime of the current process. This can be used, for example, after your application has finished its boot process:
37
+
38
+ ```ruby
39
+ def main
40
+ start_app
41
+ Appsignal::CheckIn.heartbeat("my_app", continuous: true)
42
+ end
43
+ ```
44
+
45
+ (minor [7ae7152c](https://github.com/appsignal/appsignal-ruby/commit/7ae7152cddae7c257e9d62d3bf2433cce1f4287d))
46
+ - Include the first backtrace line from error causes to show where each cause originated in the interface. (patch [496b035a](https://github.com/appsignal/appsignal-ruby/commit/496b035a3510dbb6dc47c7c59172f488ec55c986))
47
+
3
48
  ## 4.0.9
4
49
 
5
50
  _Published on 2024-09-17._
@@ -22,13 +22,11 @@ module Appsignal
22
22
  private
23
23
 
24
24
  def event(kind)
25
- {
25
+ Event.cron(
26
26
  :identifier => @identifier,
27
27
  :digest => @digest,
28
- :kind => kind,
29
- :timestamp => Time.now.utc.to_i,
30
- :check_in_type => "cron"
31
- }
28
+ :kind => kind
29
+ )
32
30
  end
33
31
  end
34
32
  end
@@ -0,0 +1,72 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Appsignal
4
+ module CheckIn
5
+ # @api private
6
+ class Event
7
+ class << self
8
+ def new(check_in_type:, identifier:, digest: nil, kind: nil)
9
+ {
10
+ :identifier => identifier,
11
+ :digest => digest,
12
+ :kind => kind,
13
+ :timestamp => Time.now.utc.to_i,
14
+ :check_in_type => check_in_type
15
+ }.compact
16
+ end
17
+
18
+ def cron(identifier:, digest:, kind:)
19
+ new(
20
+ :check_in_type => "cron",
21
+ :identifier => identifier,
22
+ :digest => digest,
23
+ :kind => kind
24
+ )
25
+ end
26
+
27
+ def heartbeat(identifier:)
28
+ new(
29
+ :check_in_type => "heartbeat",
30
+ :identifier => identifier
31
+ )
32
+ end
33
+
34
+ def redundant?(event, other)
35
+ return false if
36
+ other[:check_in_type] != event[:check_in_type] ||
37
+ other[:identifier] != event[:identifier]
38
+
39
+ return false if event[:check_in_type] == "cron" && (
40
+ other[:digest] != event[:digest] ||
41
+ other[:kind] != event[:kind]
42
+ )
43
+
44
+ return false if
45
+ event[:check_in_type] != "cron" &&
46
+ event[:check_in_type] != "heartbeat"
47
+
48
+ true
49
+ end
50
+
51
+ def describe(events)
52
+ if events.empty?
53
+ # This shouldn't happen.
54
+ "no check-in events"
55
+ elsif events.length > 1
56
+ "#{events.length} check-in events"
57
+ else
58
+ event = events.first
59
+ if event[:check_in_type] == "cron"
60
+ "cron check-in `#{event[:identifier] || "unknown"}` " \
61
+ "#{event[:kind] || "unknown"} event (digest #{event[:digest] || "unknown"})"
62
+ elsif event[:check_in_type] == "heartbeat"
63
+ "heartbeat check-in `#{event[:identifier] || "unknown"}` event"
64
+ else
65
+ "unknown check-in event"
66
+ end
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
72
+ end
@@ -29,7 +29,7 @@ module Appsignal
29
29
  def schedule(event)
30
30
  unless Appsignal.active?
31
31
  Appsignal.internal_logger.debug(
32
- "Cannot transmit #{describe([event])}: AppSignal is not active"
32
+ "Cannot transmit #{Event.describe([event])}: AppSignal is not active"
33
33
  )
34
34
  return
35
35
  end
@@ -37,7 +37,7 @@ module Appsignal
37
37
  @mutex.synchronize do
38
38
  if @queue.closed?
39
39
  Appsignal.internal_logger.debug(
40
- "Cannot transmit #{describe([event])}: AppSignal is stopped"
40
+ "Cannot transmit #{Event.describe([event])}: AppSignal is stopped"
41
41
  )
42
42
  return
43
43
  end
@@ -48,7 +48,7 @@ module Appsignal
48
48
  start_waker(INITIAL_DEBOUNCE_SECONDS) if @waker.nil?
49
49
 
50
50
  Appsignal.internal_logger.debug(
51
- "Scheduling #{describe([event])} to be transmitted"
51
+ "Scheduling #{Event.describe([event])} to be transmitted"
52
52
  )
53
53
 
54
54
  # Make sure to start the thread after an event has been added.
@@ -92,7 +92,7 @@ module Appsignal
92
92
  end
93
93
 
94
94
  def transmit(events)
95
- description = describe(events)
95
+ description = Event.describe(events)
96
96
 
97
97
  begin
98
98
  response = CheckIn.transmitter.transmit(events, :format => :ndjson)
@@ -110,42 +110,18 @@ module Appsignal
110
110
  end
111
111
  end
112
112
 
113
- def describe(events)
114
- if events.empty?
115
- # This shouldn't happen.
116
- "no check-in events"
117
- elsif events.length > 1
118
- "#{events.length} check-in events"
119
- else
120
- event = events.first
121
- if event[:check_in_type] == "cron"
122
- "cron check-in `#{event[:identifier] || "unknown"}` " \
123
- "#{event[:kind] || "unknown"} event (digest #{event[:digest] || "unknown"})" \
124
- else
125
- "unknown check-in event"
126
- end
127
- end
128
- end
129
-
130
113
  # Must be called from within a `@mutex.synchronize` block.
131
114
  def add_event(event)
132
115
  # Remove redundant events, keeping the newly added one, which
133
116
  # should be the one with the most recent timestamp.
134
- if event[:check_in_type] == "cron"
135
- # Remove any existing cron check-in event with the same identifier,
136
- # digest and kind as the one we're adding.
137
- @events.reject! do |existing_event|
138
- next unless existing_event[:identifier] == event[:identifier] &&
139
- existing_event[:digest] == event[:digest] &&
140
- existing_event[:kind] == event[:kind] &&
141
- existing_event[:check_in_type] == "cron"
117
+ @events.reject! do |existing_event|
118
+ next unless Event.redundant?(event, existing_event)
142
119
 
143
- Appsignal.internal_logger.debug(
144
- "Replacing previously scheduled #{describe([existing_event])}"
145
- )
120
+ Appsignal.internal_logger.debug(
121
+ "Replacing previously scheduled #{Event.describe([existing_event])}"
122
+ )
146
123
 
147
- true
148
- end
124
+ true
149
125
  end
150
126
 
151
127
  @events << event
@@ -2,10 +2,21 @@
2
2
 
3
3
  module Appsignal
4
4
  module CheckIn
5
+ HEARTBEAT_CONTINUOUS_INTERVAL_SECONDS = 30
5
6
  class << self
7
+ # @api private
8
+ def continuous_heartbeats
9
+ @continuous_heartbeats ||= []
10
+ end
11
+
12
+ # @api private
13
+ def kill_continuous_heartbeats
14
+ continuous_heartbeats.each(&:kill)
15
+ end
16
+
6
17
  # Track cron check-ins.
7
18
  #
8
- # Track the execution of certain processes by sending a cron check-in.
19
+ # Track the execution of scheduled processes by sending a cron check-in.
9
20
  #
10
21
  # To track the duration of a piece of code, pass a block to {.cron}
11
22
  # to report both when the process starts, and when it finishes.
@@ -40,6 +51,37 @@ module Appsignal
40
51
  output
41
52
  end
42
53
 
54
+ # Track heartbeat check-ins.
55
+ #
56
+ # Track the execution of long-lived processes by sending a heartbeat
57
+ # check-in.
58
+ #
59
+ # @example Send a heartbeat check-in
60
+ # Appsignal::CheckIn.heartbeat("main_loop")
61
+ #
62
+ # @param identifier [String] identifier of the heartbeat check-in to report.
63
+ # @param continuous [Boolean] whether the heartbeats should be sent continuously
64
+ # during the lifetime of the process. Defaults to `false`.
65
+ # @yield the block to monitor.
66
+ # @return [void]
67
+ # @since 4.1.0
68
+ # @see https://docs.appsignal.com/check-ins/heartbeat
69
+ def heartbeat(identifier, continuous: false)
70
+ if continuous
71
+ continuous_heartbeats << Thread.new do
72
+ loop do
73
+ heartbeat(identifier)
74
+ sleep HEARTBEAT_CONTINUOUS_INTERVAL_SECONDS
75
+ end
76
+ end
77
+
78
+ return
79
+ end
80
+
81
+ event = Event.heartbeat(:identifier => identifier)
82
+ scheduler.schedule(event)
83
+ end
84
+
43
85
  # @api private
44
86
  def transmitter
45
87
  @transmitter ||= Transmitter.new(
@@ -60,5 +102,6 @@ module Appsignal
60
102
  end
61
103
  end
62
104
 
105
+ require "appsignal/check_in/event"
63
106
  require "appsignal/check_in/scheduler"
64
107
  require "appsignal/check_in/cron"
@@ -180,36 +180,19 @@ module Appsignal
180
180
  "APPSIGNAL_CPU_COUNT" => :cpu_count
181
181
  }.freeze
182
182
 
183
- # @attribute [r] system_config
184
- # Config detected on the system level.
185
- # Used in diagnose report.
186
- # @api private
187
- # @return [Hash]
188
- # @!attribute [r] file_config
189
- # Config loaded from `config/appsignal.yml` config file.
190
- # Used in diagnose report.
191
- # @api private
192
- # @return [Hash]
193
- # @!attribute [r] env_config
194
- # Config loaded from the system environment.
195
- # Used in diagnose report.
196
- # @api private
197
- # @return [Hash]
198
- # @!attribute [r] config_hash
199
- # Config used by the AppSignal gem.
200
- # Combined Hash of the {system_config}, {file_config}, {env_config}
201
- # attributes.
202
- # @see #[]
203
- # @see #[]=
204
- # @api private
205
- # @return [Hash]
183
+ # @api private
184
+ attr_reader :root_path, :env, :config_hash
206
185
 
186
+ # List of config option sources. If a config option was set by a source,
187
+ # it's listed in that source's config options hash.
188
+ #
189
+ # These options are merged as the config is initialized.
190
+ # Their values cannot be changed after the config is initialized.
191
+ #
192
+ # Used by the diagnose report to list which value was read from which source.
207
193
  # @api private
208
- attr_accessor :root_path, :env, :config_hash
209
194
  attr_reader :system_config, :loaders_config, :initial_config, :file_config,
210
195
  :env_config, :override_config, :dsl_config
211
- # @api private
212
- attr_accessor :logger
213
196
 
214
197
  # Initialize a new configuration object for AppSignal.
215
198
  #
@@ -217,9 +200,6 @@ module Appsignal
217
200
  # @param env [String] The environment to load when AppSignal is started. It
218
201
  # will look for an environment with this name in the `config/appsignal.yml`
219
202
  # config file.
220
- # @param logger [Logger] The logger to use for the AppSignal gem. This is
221
- # used by the configuration class only. Default:
222
- # {Appsignal.internal_logger}. See also {Appsignal.start}.
223
203
  #
224
204
  # @api private
225
205
  # @see https://docs.appsignal.com/ruby/configuration/
@@ -230,13 +210,11 @@ module Appsignal
230
210
  # How to integrate AppSignal manually
231
211
  def initialize(
232
212
  root_path,
233
- env,
234
- logger = Appsignal.internal_logger
213
+ env
235
214
  )
236
- @root_path = root_path
215
+ @root_path = root_path.to_s
237
216
  @config_file_error = false
238
217
  @config_file = config_file
239
- @logger = logger
240
218
  @valid = false
241
219
 
242
220
  @env = env.to_s
@@ -402,7 +380,7 @@ module Appsignal
402
380
 
403
381
  # @api private
404
382
  def merge_dsl_options(options)
405
- @dsl_config = options
383
+ @dsl_config.merge!(options)
406
384
  merge(options)
407
385
  end
408
386
 
@@ -426,7 +404,7 @@ module Appsignal
426
404
  push_api_key = config_hash[:push_api_key] || ""
427
405
  if push_api_key.strip.empty?
428
406
  @valid = false
429
- @logger.error "Push API key not set after loading config"
407
+ logger.error "Push API key not set after loading config"
430
408
  else
431
409
  @valid = true
432
410
  end
@@ -445,6 +423,10 @@ module Appsignal
445
423
 
446
424
  private
447
425
 
426
+ def logger
427
+ Appsignal.internal_logger
428
+ end
429
+
448
430
  def config_file
449
431
  @config_file ||=
450
432
  root_path.nil? ? nil : File.join(root_path, "config", "appsignal.yml")
@@ -546,7 +528,7 @@ module Appsignal
546
528
 
547
529
  def merge(new_config)
548
530
  new_config.each do |key, value|
549
- @logger.debug("Config key '#{key}' is being overwritten") unless config_hash[key].nil?
531
+ logger.debug("Config key '#{key}' is being overwritten") unless config_hash[key].nil?
550
532
  config_hash[key] = value
551
533
  end
552
534
  end
@@ -10,12 +10,12 @@ namespace :appsignal do
10
10
 
11
11
  appsignal_config = Appsignal::Config.new(
12
12
  Dir.pwd,
13
- appsignal_env,
14
- Logger.new(StringIO.new)
13
+ appsignal_env
15
14
  ).tap do |c|
16
15
  c.merge_dsl_options(fetch(:appsignal_config, {}))
17
16
  c.validate
18
17
  end
18
+ Appsignal._start_logger
19
19
 
20
20
  if appsignal_config&.active?
21
21
  marker_data = {
@@ -18,12 +18,12 @@ module Appsignal
18
18
 
19
19
  appsignal_config = Appsignal::Config.new(
20
20
  ENV.fetch("PWD", nil),
21
- env,
22
- Appsignal::Utils::IntegrationLogger.new(StringIO.new)
21
+ env
23
22
  ).tap do |c|
24
23
  c.merge_dsl_options(fetch(:appsignal_config, {}))
25
24
  c.validate
26
25
  end
26
+ Appsignal._start_logger
27
27
 
28
28
  if appsignal_config&.active?
29
29
  marker_data = {
@@ -94,6 +94,7 @@ module Appsignal
94
94
  transaction.set_action(action_name) if action_name
95
95
  transaction.add_custom_data(custom_data) if custom_data
96
96
 
97
+ tags[:reported_by] = :rails_error_reporter
97
98
  tags[:severity] = severity
98
99
  tags[:source] = source.to_s if source
99
100
  transaction.add_tags(tags)
@@ -627,7 +627,8 @@ module Appsignal
627
627
  causes_sample_data = causes.map do |e|
628
628
  {
629
629
  :name => e.class.name,
630
- :message => cleaned_error_message(e)
630
+ :message => cleaned_error_message(e),
631
+ :first_line => first_formatted_backtrace_line(e)
631
632
  }
632
633
  end
633
634
 
@@ -639,6 +640,36 @@ module Appsignal
639
640
  )
640
641
  end
641
642
 
643
+ BACKTRACE_REGEX =
644
+ %r{(?<gem>[\w-]+ \(.+\) )?(?<path>:?/?\w+?.+?):(?<line>:?\d+)(?<group>:in `(?<method>.+)')?$}.freeze # rubocop:disable Layout/LineLength
645
+
646
+ def first_formatted_backtrace_line(error)
647
+ backtrace = cleaned_backtrace(error.backtrace)
648
+ first_line = backtrace&.first
649
+ return unless first_line
650
+
651
+ captures = BACKTRACE_REGEX.match(first_line)
652
+ return unless captures
653
+
654
+ captures.named_captures
655
+ .merge("original" => first_line)
656
+ .tap do |c|
657
+ config = Appsignal.config
658
+ c.delete("group") # Unused key, only for easier matching
659
+ # Strip of whitespace at the end of the gem name
660
+ c["gem"] = c["gem"]&.strip
661
+ # Strip the app path from the path if present
662
+ root_path = config.root_path
663
+ if c["path"].start_with?(root_path)
664
+ c["path"].delete_prefix!(root_path)
665
+ # Relative paths shouldn't start with a slash
666
+ c["path"].delete_prefix!("/")
667
+ end
668
+ # Add revision for linking to the repository from the UI
669
+ c["revision"] = config[:revision]
670
+ end
671
+ end
672
+
642
673
  def set_sample_data(key, data)
643
674
  return unless key && data
644
675
 
@@ -107,7 +107,7 @@ module Appsignal
107
107
  if ca_file && File.exist?(ca_file) && File.readable?(ca_file)
108
108
  http.ca_file = ca_file
109
109
  else
110
- config.logger.warn "Ignoring non-existing or unreadable " \
110
+ Appsignal.internal_logger.warn "Ignoring non-existing or unreadable " \
111
111
  "`ca_file_path`: #{ca_file}"
112
112
  end
113
113
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Appsignal
4
- VERSION = "4.0.9"
4
+ VERSION = "4.1.1"
5
5
  end
data/lib/appsignal.rb CHANGED
@@ -244,8 +244,7 @@ module Appsignal
244
244
  else
245
245
  @config = Config.new(
246
246
  root_path || Config.determine_root_path,
247
- Config.determine_env(env),
248
- Appsignal.internal_logger
247
+ Config.determine_env(env)
249
248
  )
250
249
  end
251
250
 
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: appsignal
3
3
  version: !ruby/object:Gem::Version
4
- version: 4.0.9
4
+ version: 4.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Robert Beekman
@@ -10,7 +10,7 @@ authors:
10
10
  autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2024-09-17 00:00:00.000000000 Z
13
+ date: 2024-09-28 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: logger
@@ -167,6 +167,7 @@ files:
167
167
  - lib/appsignal/capistrano.rb
168
168
  - lib/appsignal/check_in.rb
169
169
  - lib/appsignal/check_in/cron.rb
170
+ - lib/appsignal/check_in/event.rb
170
171
  - lib/appsignal/check_in/scheduler.rb
171
172
  - lib/appsignal/cli.rb
172
173
  - lib/appsignal/cli/demo.rb