From 8318653c714029b0c316b083ce86213c62d8b427 Mon Sep 17 00:00:00 2001 From: Kayla Reopelle Date: Thu, 12 Dec 2024 15:51:09 -0800 Subject: [PATCH 1/4] Create superagent configs --- .../agent/configuration/default_source.rb | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/lib/new_relic/agent/configuration/default_source.rb b/lib/new_relic/agent/configuration/default_source.rb index efc3ecc21b..4bc74395fa 100644 --- a/lib/new_relic/agent/configuration/default_source.rb +++ b/lib/new_relic/agent/configuration/default_source.rb @@ -2187,6 +2187,28 @@ def self.notify :transform => DefaultSource.method(:convert_to_constant_list), :description => 'Specify a list of exceptions you do not want the agent to strip when [strip_exception_messages](#strip_exception_messages-enabled) is `true`. Separate exceptions with a comma. For example, `"ImportantException,PreserveMessageException"`.' }, + # Super Agent + :'superagent.fleet_id' => { + :default => 'j2e4a6n0v1', # TODO: set default to nil before release + :public => true, + :type => String, + :allowed_from_server => false, + :description => 'This assigns a fleet id to the language agent. This id is generated by the super agent. If this setting is present, it indicates the agent is running in a super agent/fleet environment and health file(s) will be generated.' + }, + :'superagent.health.delivery_location' => { + :default => 'health/', # TODO: set default to EMPTY_STRING before release + :public => true, + :type => String, + :allowed_from_server => false, + :description => 'A `file:` URI that specifies the fully qualified directory path for health file(s) to be written to. For example: `file:///var/lib/newrelic-super-agent/fleet/agents.d/`. This configuration will be set by the super agent, or one of its components, prior to agent startup.' + }, + :'superagent.health.frequency' => { + :default => 5, + :public => true, + :type => Integer, + :allowed_from_server => false, + :description => 'The interval, in seconds, of how often the health file(s) will be written to. This configuration will be set by the super agent, or one of its components, prior to agent startup.' + }, # Thread profiler :'thread_profiler.enabled' => { :default => DefaultSource.thread_profiler_enabled, From 292f6ae5ae42fa63262d4e981870fb8b5a6da68e Mon Sep 17 00:00:00 2001 From: Kayla Reopelle Date: Thu, 12 Dec 2024 15:51:23 -0800 Subject: [PATCH 2/4] Create HealthCheck class --- lib/new_relic/agent/health_check.rb | 79 ++++++++++++ test/new_relic/agent/health_check_test.rb | 146 ++++++++++++++++++++++ 2 files changed, 225 insertions(+) create mode 100644 lib/new_relic/agent/health_check.rb create mode 100644 test/new_relic/agent/health_check_test.rb diff --git a/lib/new_relic/agent/health_check.rb b/lib/new_relic/agent/health_check.rb new file mode 100644 index 0000000000..ee17d1dd71 --- /dev/null +++ b/lib/new_relic/agent/health_check.rb @@ -0,0 +1,79 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +module NewRelic + module Agent + class HealthCheck + def initialize + # should we pass this in as an arg from the init_plugin method call? + @start_time = nano_time + # if they're configs, is it worth saving them in vars? + @fleet_id = NewRelic::Agent.config[:'superagent.fleet_id'] + @delivery_location = NewRelic::Agent.config[:'superagent.health.delivery_location'] + @frequency = NewRelic::Agent.config[:'superagent.health.frequency'] + # @check? = false + end + + # nope out if no delivery_location? + # seems like something for init_plugin + def validate_delivery_location + end + + # TODO: check health + def health + 'health: true' + end + + # TODO: get valid status + def status + 'status: Agent has shutdown' + end + + # TODO: get actual last error + def last_error + 'last_error: NR-APM-1000' + end + + def start_time + "start_time_unix_nano: #{@start_time}" + end + + def status_time + "status_time_unix_nano: #{nano_time}" + end + + def nano_time + Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond) + end + + def file_name + "health-#{NewRelic::Agent::GuidGenerator.generate_guid(32)}.yml" + end + + def write_file + @path ||= find_or_create_file_path + + File.open("#{@path}/#{file_name}", 'w') do |f| + f.write(contents) # add .to_yaml? + end + end + + def contents + [health, status, last_error, status_time, start_time].join("\n") + end + + # Adapted from AgentLogger + # rescue? + def find_or_create_file_path + for abs_path in [File.expand_path(@delivery_location), + File.expand_path(File.join('', @delivery_location))] do + if File.directory?(abs_path) || (Dir.mkdir(abs_path) rescue nil) + return abs_path[%r{^(.*?)/?$}] + end + end + nil + end + end + end +end diff --git a/test/new_relic/agent/health_check_test.rb b/test/new_relic/agent/health_check_test.rb new file mode 100644 index 0000000000..b94992d500 --- /dev/null +++ b/test/new_relic/agent/health_check_test.rb @@ -0,0 +1,146 @@ +# This file is distributed under New Relic's license terms. +# See https://github.com/newrelic/newrelic-ruby-agent/blob/main/LICENSE for complete details. +# frozen_string_literal: true + +require 'fileutils' +require_relative '../../test_helper' + +class NewRelicHealthCheckTest < Minitest::Test + # example: + # health-bc21b5891f5e44fc9272caef924611a8.yml + # healthy: false + # status: Agent has shutdown + # last_error: NR-APM-1000 + # status_time_unix_nano: 1724953624761000000 + # start_time_unix_nano: 1724953587605000000 + + # maybe delete the file every time? + # def teardown + # FileUtils.rm_rf('health') + # end + + def test_yaml_health_file_written_to_delivery_location + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.directory?('health'), 'Directory not found' + assert File.exist?('health/health-abc123.yml'), 'File not found' + end + end + ensure + FileUtils.rm_rf('health') + end + + # This might be on init... + def test_yaml_health_file_logs_error_when_delivery_location_invalid + end + + def test_yaml_file_generated_if_superagent_fleet_id_present + end + + def test_yaml_file_not_generated_if_superagent_fleet_id_absent + end + + def test_yaml_file_name_has_health_plus_uuid_without_hyphens + health_check = NewRelic::Agent::HealthCheck.new + # ex: health-bc21b5891f5e44fc9272caef924611a8.yml + assert_match /health-(.*){32}\.ya?ml/, health_check.file_name + end + + def test_yaml_health_file_written_on_interval + with_config(:'superagent.health.frequency' => 5) do + + end + end + + def test_agent_logs_errors_if_yaml_health_file_writing_fails + end + + def test_yaml_file_has_health_field + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.readlines('health/health-abc123.yml').grep(/health:/).any? + end + end + ensure + FileUtils.rm_rf('health') + end + + def test_yaml_file_has_status_field + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.readlines('health/health-abc123.yml').grep(/status:/).any? + end + end + ensure + FileUtils.rm_rf('health') + end + + def test_yaml_file_has_last_error_field_when_status_not_healthy + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.readlines('health/health-abc123.yml').grep(/last_error:/).any? + end + end + ensure + FileUtils.rm_rf('health') + end + + def test_yaml_file_does_not_have_last_error_field_when_status_healthy + end + + def test_yaml_file_has_start_time_unix_nano + # TODO - validate timestamp + # TODO - validate timestamp same for every file created by that instance + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.readlines('health/health-abc123.yml').grep(/start_time_unix_nano:/).any? + end + end + ensure + FileUtils.rm_rf('health') + end + + def test_yaml_file_has_status_time_unix_nano + # status_time_unix_nano: + # timestamp present + # timestamp in nanoseconds => milliseconds * 1000000 + with_config(:'superagent.health.delivery_location' => 'health/') do + NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do + health_check = NewRelic::Agent::HealthCheck.new + health_check.write_file + + assert File.readlines('health/health-abc123.yml').grep(/status_time_unix_nano:/).any? + end + end + ensure + FileUtils.rm_rf('health') + end + + def test_yaml_file_fully_regenerated_on_each_interval + end + + def test_unique_health_file_exists_per_process + # puma scenario? + end + + def test_supportability_metric_generated_at_agent_startup + # Supportability/SuperAgent/Health/enabled + end + + ## ADD MORE TESTS FOR ERROR CODE BEHAVIOR +end From 636f47bbd5a2b9aff69057a639edd9505d6622d2 Mon Sep 17 00:00:00 2001 From: Kayla Reopelle Date: Thu, 12 Dec 2024 15:52:00 -0800 Subject: [PATCH 3/4] WIP add health_check to init_plugin --- lib/new_relic/control/instance_methods.rb | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/lib/new_relic/control/instance_methods.rb b/lib/new_relic/control/instance_methods.rb index c64dee0297..dba185751e 100644 --- a/lib/new_relic/control/instance_methods.rb +++ b/lib/new_relic/control/instance_methods.rb @@ -5,6 +5,7 @@ require 'new_relic/agent/null_logger' require 'new_relic/agent/memory_logger' require 'new_relic/agent/agent_logger' +require 'new_relic/agent/health_check' require_relative 'private_instance_methods' @@ -53,7 +54,7 @@ def init_plugin(options = {}) env = determine_env(options) configure_agent(env, options) - + #health_check # Be sure to only create once! RUBY-1020 create_logger(options) @@ -153,6 +154,14 @@ def newrelic_root self.class.newrelic_root end + def health_check + return NewRelic::Agent.logger.debug('superagent.fleet_id not found, skipping health checks') unless NewRelic::Agent.config[:'superagent.fleet_id'] + return NewRelic::Agent.logger.debug('superagent.health.file_destination not found, skipping health checks') unless NewRelic::Agent.config[:'superagent.health.delivery_location'] + + # NewRelic::Agent::HealthCheck.new + # start the loop here? + end + protected def initialize(local_env, config_file_override = nil) From e9ae9958961615d2e344ff3303d2cd8dec50667c Mon Sep 17 00:00:00 2001 From: Kayla Reopelle Date: Thu, 12 Dec 2024 15:58:32 -0800 Subject: [PATCH 4/4] Rubocop --- lib/new_relic/agent/health_check.rb | 5 ++--- lib/new_relic/control/instance_methods.rb | 2 +- test/new_relic/agent/health_check_test.rb | 17 ++++++++--------- 3 files changed, 11 insertions(+), 13 deletions(-) diff --git a/lib/new_relic/agent/health_check.rb b/lib/new_relic/agent/health_check.rb index ee17d1dd71..9f0638be78 100644 --- a/lib/new_relic/agent/health_check.rb +++ b/lib/new_relic/agent/health_check.rb @@ -54,12 +54,11 @@ def file_name def write_file @path ||= find_or_create_file_path - File.open("#{@path}/#{file_name}", 'w') do |f| - f.write(contents) # add .to_yaml? - end + File.write("#{@path}/#{file_name}", contents) end def contents + # TODO: should I add .to_yaml? [health, status, last_error, status_time, start_time].join("\n") end diff --git a/lib/new_relic/control/instance_methods.rb b/lib/new_relic/control/instance_methods.rb index dba185751e..6eddecd071 100644 --- a/lib/new_relic/control/instance_methods.rb +++ b/lib/new_relic/control/instance_methods.rb @@ -54,7 +54,7 @@ def init_plugin(options = {}) env = determine_env(options) configure_agent(env, options) - #health_check + # health_check # Be sure to only create once! RUBY-1020 create_logger(options) diff --git a/test/new_relic/agent/health_check_test.rb b/test/new_relic/agent/health_check_test.rb index b94992d500..2dc788cfcd 100644 --- a/test/new_relic/agent/health_check_test.rb +++ b/test/new_relic/agent/health_check_test.rb @@ -26,7 +26,7 @@ def test_yaml_health_file_written_to_delivery_location health_check.write_file assert File.directory?('health'), 'Directory not found' - assert File.exist?('health/health-abc123.yml'), 'File not found' + assert File.exist?('health/health-abc123.yml'), 'File not found' # rubocop:disable Minitest/AssertPathExists end end ensure @@ -46,12 +46,11 @@ def test_yaml_file_not_generated_if_superagent_fleet_id_absent def test_yaml_file_name_has_health_plus_uuid_without_hyphens health_check = NewRelic::Agent::HealthCheck.new # ex: health-bc21b5891f5e44fc9272caef924611a8.yml - assert_match /health-(.*){32}\.ya?ml/, health_check.file_name + assert_match(/health-(.*){32}\.ya?ml/, health_check.file_name) end def test_yaml_health_file_written_on_interval with_config(:'superagent.health.frequency' => 5) do - end end @@ -64,7 +63,7 @@ def test_yaml_file_has_health_field health_check = NewRelic::Agent::HealthCheck.new health_check.write_file - assert File.readlines('health/health-abc123.yml').grep(/health:/).any? + assert_predicate File.readlines('health/health-abc123.yml').grep(/health:/), :any? end end ensure @@ -77,7 +76,7 @@ def test_yaml_file_has_status_field health_check = NewRelic::Agent::HealthCheck.new health_check.write_file - assert File.readlines('health/health-abc123.yml').grep(/status:/).any? + assert_predicate File.readlines('health/health-abc123.yml').grep(/status:/), :any? end end ensure @@ -90,7 +89,7 @@ def test_yaml_file_has_last_error_field_when_status_not_healthy health_check = NewRelic::Agent::HealthCheck.new health_check.write_file - assert File.readlines('health/health-abc123.yml').grep(/last_error:/).any? + assert_predicate File.readlines('health/health-abc123.yml').grep(/last_error:/), :any? end end ensure @@ -101,14 +100,14 @@ def test_yaml_file_does_not_have_last_error_field_when_status_healthy end def test_yaml_file_has_start_time_unix_nano - # TODO - validate timestamp + # TODO: - validate timestamp # TODO - validate timestamp same for every file created by that instance with_config(:'superagent.health.delivery_location' => 'health/') do NewRelic::Agent::GuidGenerator.stub(:generate_guid, 'abc123') do health_check = NewRelic::Agent::HealthCheck.new health_check.write_file - assert File.readlines('health/health-abc123.yml').grep(/start_time_unix_nano:/).any? + assert_predicate File.readlines('health/health-abc123.yml').grep(/start_time_unix_nano:/), :any? end end ensure @@ -124,7 +123,7 @@ def test_yaml_file_has_status_time_unix_nano health_check = NewRelic::Agent::HealthCheck.new health_check.write_file - assert File.readlines('health/health-abc123.yml').grep(/status_time_unix_nano:/).any? + assert_predicate File.readlines('health/health-abc123.yml').grep(/status_time_unix_nano:/), :any? end end ensure