diff --git a/.changesets/deprecate-heartbeats.md b/.changesets/deprecate-heartbeats.md new file mode 100644 index 000000000..9d93ae2d9 --- /dev/null +++ b/.changesets/deprecate-heartbeats.md @@ -0,0 +1,6 @@ +--- +bump: patch +type: deprecate +--- + +Calls to `Appsignal.heartbeat` and `Appsignal::Heartbeat` will emit a deprecation warning. diff --git a/.changesets/rename-heartbeats-to-cron-check-ins.md b/.changesets/rename-heartbeats-to-cron-check-ins.md new file mode 100644 index 000000000..84448f845 --- /dev/null +++ b/.changesets/rename-heartbeats-to-cron-check-ins.md @@ -0,0 +1,18 @@ +--- +bump: patch +type: change +--- + +Rename heartbeats to cron check-ins. Calls to `Appsignal.heartbeat` and `Appsignal::Heartbeat` should be replaced with calls to `Appsignal::CheckIn.cron` and `Appsignal::CheckIn::Cron`, for example: + +```ruby +# Before +Appsignal.heartbeat("do_something") do + do_something +end + +# After +Appsignal::CheckIn.cron("do_something") do + do_something +end +``` diff --git a/lib/appsignal.rb b/lib/appsignal.rb index c50be94b1..5f71b19c8 100644 --- a/lib/appsignal.rb +++ b/lib/appsignal.rb @@ -6,7 +6,7 @@ require "appsignal/logger" require "appsignal/utils/stdout_and_logger_message" -require "appsignal/helpers/heartbeats" +require "appsignal/helpers/heartbeat" require "appsignal/helpers/instrumentation" require "appsignal/helpers/metrics" @@ -18,7 +18,7 @@ # {Appsignal::Helpers::Metrics}) for ease of use. module Appsignal class << self - include Helpers::Heartbeats + include Helpers::Heartbeat include Helpers::Instrumentation include Helpers::Metrics @@ -461,6 +461,16 @@ def const_missing(name) "Please update the constant name to Appsignal::Probes " \ "in the following file to remove this message.\n#{callers.first}" Appsignal::Probes + when :Heartbeat + unless @heartbeat_constant_deprecation_warning_emitted + callers = caller + Appsignal::Utils::StdoutAndLoggerMessage.warning \ + "The constant Appsignal::Heartbeat has been deprecated. " \ + "Please update the constant name to Appsignal::CheckIn::Cron " \ + "in the following file and elsewhere to remove this message.\n#{callers.first}" + @heartbeat_constant_deprecation_warning_emitted = true + end + Appsignal::CheckIn::Cron else super end @@ -489,4 +499,4 @@ def const_missing(name) require "appsignal/transaction" require "appsignal/version" require "appsignal/transmitter" -require "appsignal/heartbeat" +require "appsignal/check_in" diff --git a/lib/appsignal/check_in.rb b/lib/appsignal/check_in.rb new file mode 100644 index 000000000..69fbaf198 --- /dev/null +++ b/lib/appsignal/check_in.rb @@ -0,0 +1,46 @@ +# frozen_string_literal: true + +module Appsignal + module CheckIn + class << self + # Track cron check-ins. + # + # Track the execution of certain processes by sending a cron check-in. + # + # To track the duration of a piece of code, pass a block to {.cron} + # to report both when the process starts, and when it finishes. + # + # If an exception is raised within the block, the finish event will not + # be reported, triggering a notification about the missing cron check-in. + # The exception will bubble outside of the cron check-in block. + # + # @example Send a cron check-in + # Appsignal::CheckIn.cron("send_invoices") + # + # @example Send a cron check-in with duration + # Appsignal::CheckIn.cron("send_invoices") do + # # your code + # end + # + # @param name [String] name of the cron check-in to report. + # @yield the block to monitor. + # @return [void] + # @since 3.12.7 + # @see https://docs.appsignal.com/check-ins/cron + def cron(name) + cron = Appsignal::CheckIn::Cron.new(:name => name) + output = nil + + if block_given? + cron.start + output = yield + end + + cron.finish + output + end + end + end +end + +require "appsignal/check_in/cron" diff --git a/lib/appsignal/check_in/cron.rb b/lib/appsignal/check_in/cron.rb new file mode 100644 index 000000000..3056fcd92 --- /dev/null +++ b/lib/appsignal/check_in/cron.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +module Appsignal + module CheckIn + class Cron + class << self + # @api private + def transmitter + @transmitter ||= Appsignal::Transmitter.new( + "#{Appsignal.config[:logging_endpoint]}/checkins/cron/json" + ) + end + end + + attr_reader :name, :id + + def initialize(name:) + @name = name + @id = SecureRandom.hex(8) + end + + def start + transmit_event("start") + end + + def finish + transmit_event("finish") + end + + private + + def event(kind) + { + :name => name, + :id => @id, + :kind => kind, + :timestamp => Time.now.utc.to_i + } + end + + def transmit_event(kind) + unless Appsignal.active? + Appsignal.internal_logger.debug( + "AppSignal not active, not transmitting cron check-in event" + ) + return + end + + response = self.class.transmitter.transmit(event(kind)) + + if response.code.to_i >= 200 && response.code.to_i < 300 + Appsignal.internal_logger.debug( + "Transmitted cron check-in `#{name}` (#{id}) #{kind} event" + ) + else + Appsignal.internal_logger.error( + "Failed to transmit cron check-in #{kind} event: status code was #{response.code}" + ) + end + rescue => e + Appsignal.internal_logger.error("Failed to transmit cron check-in #{kind} event: #{e}") + end + end + end +end diff --git a/lib/appsignal/heartbeat.rb b/lib/appsignal/heartbeat.rb deleted file mode 100644 index 1161faeb7..000000000 --- a/lib/appsignal/heartbeat.rb +++ /dev/null @@ -1,59 +0,0 @@ -# frozen_string_literal: true - -module Appsignal - class Heartbeat - class << self - # @api private - def transmitter - @transmitter ||= Appsignal::Transmitter.new( - "#{Appsignal.config[:logging_endpoint]}/heartbeats/json" - ) - end - end - - attr_reader :name, :id - - def initialize(name:) - @name = name - @id = SecureRandom.hex(8) - end - - def start - transmit_event("start") - end - - def finish - transmit_event("finish") - end - - private - - def event(kind) - { - :name => name, - :id => @id, - :kind => kind, - :timestamp => Time.now.utc.to_i - } - end - - def transmit_event(kind) - unless Appsignal.active? - Appsignal.internal_logger.debug("AppSignal not active, not transmitting heartbeat event") - return - end - - response = self.class.transmitter.transmit(event(kind)) - - if response.code.to_i >= 200 && response.code.to_i < 300 - Appsignal.internal_logger.debug("Transmitted heartbeat `#{name}` (#{id}) #{kind} event") - else - Appsignal.internal_logger.error( - "Failed to transmit heartbeat event: status code was #{response.code}" - ) - end - rescue => e - Appsignal.internal_logger.error("Failed to transmit heartbeat event: #{e}") - end - end -end diff --git a/lib/appsignal/helpers/heartbeat.rb b/lib/appsignal/helpers/heartbeat.rb new file mode 100644 index 000000000..bfe00d43b --- /dev/null +++ b/lib/appsignal/helpers/heartbeat.rb @@ -0,0 +1,20 @@ +# frozen_string_literal: true + +module Appsignal + module Helpers + module Heartbeat + # @deprecated Use {Appsignal::CheckIn.cron} instead. + def heartbeat(name, &block) + unless @heartbeat_helper_deprecation_warning_emitted + callers = caller + Appsignal::Utils::StdoutAndLoggerMessage.warning \ + "The helper Appsignal.heartbeat has been deprecated. " \ + "Please update the helper call to Appsignal::CheckIn.cron " \ + "in the following file and elsewhere to remove this message.\n#{callers.first}" + @heartbeat_helper_deprecation_warning_emitted = true + end + Appsignal::CheckIn.cron(name, &block) + end + end + end +end diff --git a/lib/appsignal/helpers/heartbeats.rb b/lib/appsignal/helpers/heartbeats.rb deleted file mode 100644 index 82b2a455e..000000000 --- a/lib/appsignal/helpers/heartbeats.rb +++ /dev/null @@ -1,44 +0,0 @@ -# frozen_string_literal: true - -module Appsignal - module Helpers - module Heartbeats - # Track heartbeats - # - # Track the execution of certain processes by sending a hearbeat. - # - # To track the duration of a piece of code, pass a block to {.heartbeat} - # to report both when the process starts, and when it finishes. - # - # If an exception is raised within the block, the finish event will not - # be reported, triggering a notification about the missing heartbeat. The - # exception will bubble outside of the heartbeat block. - # - # @example Send a heartbeat - # Appsignal.heartbeat("send_invoices") - # - # @example Send a heartbeat with duration - # Appsignal.heartbeat("send_invoices") do - # # your code - # end - # - # @param name [String] name of the heartbeat to report. - # @yield the block to monitor. - # @return [void] - # @since 3.7.0 - # @see https://docs.appsignal.com/heartbeats - def heartbeat(name) - heartbeat = Appsignal::Heartbeat.new(:name => name) - output = nil - - if block_given? - heartbeat.start - output = yield - end - - heartbeat.finish - output - end - end - end -end diff --git a/spec/lib/appsignal/check_in_spec.rb b/spec/lib/appsignal/check_in_spec.rb new file mode 100644 index 000000000..86de7f1ab --- /dev/null +++ b/spec/lib/appsignal/check_in_spec.rb @@ -0,0 +1,294 @@ +describe Appsignal::Heartbeat do + let(:err_stream) { std_stream } + + after do + Appsignal.instance_variable_set(:@heartbeat_constant_deprecation_warning_emitted, false) + end + + it "returns the Cron constant calling the Heartbeat constant" do + silence { expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) } + end + + it "prints a deprecation warning to STDERR" do + capture_std_streams(std_stream, err_stream) do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + + expect(err_stream.read) + .to include("appsignal WARNING: The constant Appsignal::Heartbeat has been deprecated.") + end + + it "does not print a deprecation warning to STDERR more than once" do + capture_std_streams(std_stream, err_stream) do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + + expect(err_stream.read) + .to include("appsignal WARNING: The constant Appsignal::Heartbeat has been deprecated.") + + err_stream.truncate(0) + + capture_std_streams(std_stream, err_stream) do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + + expect(err_stream.read) + .not_to include("appsignal WARNING: The constant Appsignal::Heartbeat has been deprecated.") + end + + it "logs a warning" do + logs = + capture_logs do + silence do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + end + + expect(logs).to contains_log( + :warn, + "The constant Appsignal::Heartbeat has been deprecated." + ) + end + + it "does not log a warning more than once" do + logs = + capture_logs do + silence do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + end + + expect(logs).to contains_log( + :warn, + "The constant Appsignal::Heartbeat has been deprecated." + ) + + logs = + capture_logs do + silence do + expect(Appsignal::Heartbeat).to be(Appsignal::CheckIn::Cron) + end + end + + expect(logs).not_to contains_log( + :warn, + "The constant Appsignal::Heartbeat has been deprecated." + ) + end +end + +describe "Appsignal.heartbeat" do + let(:err_stream) { std_stream } + + before do + Appsignal.instance_variable_set(:@heartbeat_helper_deprecation_warning_emitted, false) + end + + it "should forward the call to Appsignal::CheckIn.cron" do + expect(Appsignal::CheckIn).to receive(:cron).with("heartbeat-name") + expect do + Appsignal.heartbeat("heartbeat-name") + end.not_to raise_error + + block = proc { 42 } + expect(Appsignal::CheckIn).to receive(:cron).with("heartbeat-name") do |&given_block| + expect(given_block).to be(block) + end.and_return("output") + expect(Appsignal.heartbeat("heartbeat-name", &block)).to eq("output") + end + + it "prints a deprecation warning to STDERR" do + capture_std_streams(std_stream, err_stream) do + Appsignal.heartbeat("heartbeat-name") + end + + expect(err_stream.read) + .to include("appsignal WARNING: The helper Appsignal.heartbeat has been deprecated.") + end + + it "does not print a deprecation warning to STDERR more than once" do + capture_std_streams(std_stream, err_stream) do + Appsignal.heartbeat("heartbeat-name") + end + + expect(err_stream.read) + .to include("appsignal WARNING: The helper Appsignal.heartbeat has been deprecated.") + + err_stream.truncate(0) + + capture_std_streams(std_stream, err_stream) do + Appsignal.heartbeat("heartbeat-name") + end + + expect(err_stream.read) + .not_to include("appsignal WARNING: The helper Appsignal.heartbeat has been deprecated.") + end + + it "logs a warning" do + logs = + capture_logs do + silence do + Appsignal.heartbeat("heartbeat-name") + end + end + + expect(logs).to contains_log( + :warn, + "The helper Appsignal.heartbeat has been deprecated." + ) + end + + it "does not log a warning more than once" do + logs = + capture_logs do + silence do + Appsignal.heartbeat("heartbeat-name") + end + end + + expect(logs).to contains_log( + :warn, + "The helper Appsignal.heartbeat has been deprecated." + ) + + logs = + capture_logs do + silence do + Appsignal.heartbeat("heartbeat-name") + end + end + + expect(logs).not_to contains_log( + :warn, + "The helper Appsignal.heartbeat has been deprecated." + ) + end +end + +describe Appsignal::CheckIn::Cron do + let(:config) { project_fixture_config } + let(:cron_checkin) { described_class.new(:name => "cron-checkin-name") } + let(:transmitter) { Appsignal::Transmitter.new("http://cron_checkins/", config) } + + before(:each) do + allow(Appsignal).to receive(:active?).and_return(true) + config.logger = Logger.new(StringIO.new) + allow(Appsignal::CheckIn::Cron).to receive(:transmitter).and_return(transmitter) + end + + describe "when Appsignal is not active" do + it "should not transmit any events" do + allow(Appsignal).to receive(:active?).and_return(false) + expect(transmitter).not_to receive(:transmit) + + cron_checkin.start + cron_checkin.finish + end + end + + describe "#start" do + it "should send a cron check-in start" do + expect(transmitter).to receive(:transmit).with(hash_including( + :name => "cron-checkin-name", + :kind => "start" + )).and_return(Net::HTTPResponse.new(nil, "200", nil)) + + expect(Appsignal.internal_logger).to receive(:debug).with( + "Transmitted cron check-in `cron-checkin-name` (#{cron_checkin.id}) start event" + ) + expect(Appsignal.internal_logger).not_to receive(:error) + + cron_checkin.start + end + + it "should log an error if it fails" do + expect(transmitter).to receive(:transmit).with(hash_including( + :name => "cron-checkin-name", + :kind => "start" + )).and_return(Net::HTTPResponse.new(nil, "499", nil)) + + expect(Appsignal.internal_logger).not_to receive(:debug) + expect(Appsignal.internal_logger).to receive(:error).with( + "Failed to transmit cron check-in start event: status code was 499" + ) + + cron_checkin.start + end + end + + describe "#finish" do + it "should send a cron check-in finish" do + expect(transmitter).to receive(:transmit).with(hash_including( + :name => "cron-checkin-name", + :kind => "finish" + )).and_return(Net::HTTPResponse.new(nil, "200", nil)) + + expect(Appsignal.internal_logger).to receive(:debug).with( + "Transmitted cron check-in `cron-checkin-name` (#{cron_checkin.id}) finish event" + ) + expect(Appsignal.internal_logger).not_to receive(:error) + + cron_checkin.finish + end + + it "should log an error if it fails" do + expect(transmitter).to receive(:transmit).with(hash_including( + :name => "cron-checkin-name", + :kind => "finish" + )).and_return(Net::HTTPResponse.new(nil, "499", nil)) + + expect(Appsignal.internal_logger).not_to receive(:debug) + expect(Appsignal.internal_logger).to receive(:error).with( + "Failed to transmit cron check-in finish event: status code was 499" + ) + + cron_checkin.finish + end + end + + describe ".cron_checkin" do + describe "when a block is given" do + it "should send a cron check-in start and finish and return the block output" do + expect(transmitter).to receive(:transmit).with(hash_including( + :kind => "start", + :name => "cron-checkin-with-block" + )).and_return(nil) + + expect(transmitter).to receive(:transmit).with(hash_including( + :kind => "finish", + :name => "cron-checkin-with-block" + )).and_return(nil) + + output = Appsignal::CheckIn.cron("cron-checkin-with-block") { "output" } + expect(output).to eq("output") + end + + it "should not send a cron check-in finish event when an error is raised" do + expect(transmitter).to receive(:transmit).with(hash_including( + :kind => "start", + :name => "cron-checkin-with-block" + )).and_return(nil) + + expect(transmitter).not_to receive(:transmit).with(hash_including( + :kind => "finish", + :name => "cron-checkin-with-block" + )) + + expect do + Appsignal::CheckIn.cron("cron-checkin-with-block") { raise "error" } + end.to raise_error(RuntimeError, "error") + end + end + + describe "when no block is given" do + it "should only send a cron check-in finish event" do + expect(transmitter).to receive(:transmit).with(hash_including( + :kind => "finish", + :name => "cron-checkin-without-block" + )).and_return(nil) + + Appsignal::CheckIn.cron("cron-checkin-without-block") + end + end + end +end diff --git a/spec/lib/appsignal/heartbeat_spec.rb b/spec/lib/appsignal/heartbeat_spec.rb deleted file mode 100644 index 27e481966..000000000 --- a/spec/lib/appsignal/heartbeat_spec.rb +++ /dev/null @@ -1,127 +0,0 @@ -describe Appsignal::Heartbeat do - let(:config) { project_fixture_config } - let(:heartbeat) { described_class.new(:name => "heartbeat-name") } - let(:transmitter) { Appsignal::Transmitter.new("http://heartbeats/", config) } - - before(:each) do - allow(Appsignal).to receive(:active?).and_return(true) - config.logger = Logger.new(StringIO.new) - allow(Appsignal::Heartbeat).to receive(:transmitter).and_return(transmitter) - end - - describe "when Appsignal is not active" do - it "should not transmit any events" do - allow(Appsignal).to receive(:active?).and_return(false) - expect(transmitter).not_to receive(:transmit) - - heartbeat.start - heartbeat.finish - end - end - - describe "#start" do - it "should send a heartbeat start" do - expect(transmitter).to receive(:transmit).with(hash_including( - :name => "heartbeat-name", - :kind => "start" - )).and_return(Net::HTTPResponse.new(nil, "200", nil)) - - expect(Appsignal.internal_logger).to receive(:debug).with( - "Transmitted heartbeat `heartbeat-name` (#{heartbeat.id}) start event" - ) - expect(Appsignal.internal_logger).not_to receive(:error) - - heartbeat.start - end - - it "should log an error if it fails" do - expect(transmitter).to receive(:transmit).with(hash_including( - :name => "heartbeat-name", - :kind => "start" - )).and_return(Net::HTTPResponse.new(nil, "499", nil)) - - expect(Appsignal.internal_logger).not_to receive(:debug) - expect(Appsignal.internal_logger).to receive(:error).with( - "Failed to transmit heartbeat event: status code was 499" - ) - - heartbeat.start - end - end - - describe "#finish" do - it "should send a heartbeat finish" do - expect(transmitter).to receive(:transmit).with(hash_including( - :name => "heartbeat-name", - :kind => "finish" - )).and_return(Net::HTTPResponse.new(nil, "200", nil)) - - expect(Appsignal.internal_logger).to receive(:debug).with( - "Transmitted heartbeat `heartbeat-name` (#{heartbeat.id}) finish event" - ) - expect(Appsignal.internal_logger).not_to receive(:error) - - heartbeat.finish - end - - it "should log an error if it fails" do - expect(transmitter).to receive(:transmit).with(hash_including( - :name => "heartbeat-name", - :kind => "finish" - )).and_return(Net::HTTPResponse.new(nil, "499", nil)) - - expect(Appsignal.internal_logger).not_to receive(:debug) - expect(Appsignal.internal_logger).to receive(:error).with( - "Failed to transmit heartbeat event: status code was 499" - ) - - heartbeat.finish - end - end - - describe ".heartbeat" do - describe "when a block is given" do - it "should send a heartbeat start and finish and return the block output" do - expect(transmitter).to receive(:transmit).with(hash_including( - :kind => "start", - :name => "heartbeat-with-block" - )).and_return(nil) - - expect(transmitter).to receive(:transmit).with(hash_including( - :kind => "finish", - :name => "heartbeat-with-block" - )).and_return(nil) - - output = Appsignal.heartbeat("heartbeat-with-block") { "output" } - expect(output).to eq("output") - end - - it "should not send a heartbeat finish event when an error is raised" do - expect(transmitter).to receive(:transmit).with(hash_including( - :kind => "start", - :name => "heartbeat-with-block" - )).and_return(nil) - - expect(transmitter).not_to receive(:transmit).with(hash_including( - :kind => "finish", - :name => "heartbeat-with-block" - )) - - expect do - Appsignal.heartbeat("heartbeat-with-block") { raise "error" } - end.to raise_error(RuntimeError, "error") - end - end - - describe "when no block is given" do - it "should only send a heartbeat finish event" do - expect(transmitter).to receive(:transmit).with(hash_including( - :kind => "finish", - :name => "heartbeat-without-block" - )).and_return(nil) - - Appsignal.heartbeat("heartbeat-without-block") - end - end - end -end