# 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 'socket' require 'net/https' require 'net/http' require 'logger' require 'zlib' require 'stringio' require 'new_relic/constants' require 'new_relic/traced_thread' require 'new_relic/coerce' require 'new_relic/agent/autostart' require 'new_relic/agent/harvester' require 'new_relic/agent/hostname' require 'new_relic/agent/new_relic_service' require 'new_relic/agent/pipe_service' require 'new_relic/agent/configuration/manager' require 'new_relic/agent/database' require 'new_relic/agent/instrumentation/resque/helper' require 'new_relic/agent/commands/agent_command_router' require 'new_relic/agent/event_listener' require 'new_relic/agent/distributed_tracing' require 'new_relic/agent/monitors' require 'new_relic/agent/transaction_event_recorder' require 'new_relic/agent/custom_event_aggregator' require 'new_relic/agent/span_event_aggregator' require 'new_relic/agent/log_event_aggregator' require 'new_relic/agent/sampler_collection' require 'new_relic/agent/javascript_instrumentor' require 'new_relic/agent/vm/monotonic_gc_profiler' require 'new_relic/agent/utilization_data' require 'new_relic/environment_report' require 'new_relic/agent/attribute_filter' require 'new_relic/agent/adaptive_sampler' require 'new_relic/agent/connect/request_builder' require 'new_relic/agent/connect/response_handler' require 'new_relic/agent/agent_helpers/connect' require 'new_relic/agent/agent_helpers/harvest' require 'new_relic/agent/agent_helpers/start_worker_thread' require 'new_relic/agent/agent_helpers/startup' require 'new_relic/agent/agent_helpers/special_startup' require 'new_relic/agent/agent_helpers/shutdown' require 'new_relic/agent/agent_helpers/transmit' module NewRelic module Agent # The agent is a singleton that is instantiated when the plugin is # activated. It collects performance data from ruby applications # in realtime as the application runs, and periodically sends that # data to the NewRelic server. class Agent def self.config ::NewRelic::Agent.config end include NewRelic::Agent::AgentHelpers::Connect include NewRelic::Agent::AgentHelpers::Harvest include NewRelic::Agent::AgentHelpers::StartWorkerThread include NewRelic::Agent::AgentHelpers::SpecialStartup include NewRelic::Agent::AgentHelpers::Startup include NewRelic::Agent::AgentHelpers::Shutdown include NewRelic::Agent::AgentHelpers::Transmit def initialize init_basics init_components init_event_handlers setup_attribute_filter end private def init_basics @started = false @event_loop = nil @worker_thread = nil @connect_state = :pending @connect_attempts = 0 @waited_on_connect = nil @connected_pid = nil @wait_on_connect_mutex = Mutex.new @after_fork_lock = Mutex.new @wait_on_connect_condition = ConditionVariable.new end def init_components @service = NewRelicService.new @events = EventListener.new @stats_engine = StatsEngine.new @transaction_sampler = TransactionSampler.new @sql_sampler = SqlSampler.new @transaction_rules = RulesEngine.new @monotonic_gc_profiler = VM::MonotonicGCProfiler.new @adaptive_sampler = AdaptiveSampler.new(Agent.config[:sampling_target], Agent.config[:sampling_target_period_in_seconds]) end def init_event_handlers @agent_command_router = Commands::AgentCommandRouter.new(@events) @monitors = Monitors.new(@events) @error_collector = ErrorCollector.new(@events) @harvest_samplers = SamplerCollection.new(@events) @javascript_instrumentor = JavaScriptInstrumentor.new(@events) @harvester = Harvester.new(@events) @transaction_event_recorder = TransactionEventRecorder.new(@events) @custom_event_aggregator = CustomEventAggregator.new(@events) @span_event_aggregator = SpanEventAggregator.new(@events) @log_event_aggregator = LogEventAggregator.new(@events) end def setup_attribute_filter refresh_attribute_filter @events.subscribe(:initial_configuration_complete) do refresh_attribute_filter end end public def refresh_attribute_filter @attribute_filter = AttributeFilter.new(Agent.config) end # contains all the class-level methods for NewRelic::Agent::Agent module ClassMethods # Should only be called by NewRelic::Control - returns a # memoized singleton instance of the agent, creating one if needed def instance @instance ||= self.new end end # Holds all the methods defined on NewRelic::Agent::Agent # instances module InstanceMethods # the statistics engine that holds all the timeslice data attr_reader :stats_engine # the transaction sampler that handles recording transactions attr_reader :transaction_sampler attr_reader :sql_sampler # error collector is a simple collection of recorded errors attr_reader :error_collector # whether we should record raw, obfuscated, or no sql attr_reader :record_sql # builder for JS agent scripts to inject attr_reader :javascript_instrumentor # cross application tracing ids and encoding attr_reader :cross_process_id # service for communicating with collector attr_reader :service # Global events dispatcher. This will provides our primary mechanism # for agent-wide events, such as finishing configuration, error notification # and request before/after from Rack. attr_reader :events # listens and responds to events that need to process headers # for synthetics and distributed tracing attr_reader :monitors # Transaction and metric renaming rules as provided by the # collector on connect. The former are applied during txns, # the latter during harvest. attr_accessor :transaction_rules # GC::Profiler.total_time is not monotonic so we wrap it. attr_reader :monotonic_gc_profiler attr_reader :custom_event_aggregator attr_reader :span_event_aggregator attr_reader :log_event_aggregator attr_reader :transaction_event_recorder attr_reader :attribute_filter attr_reader :adaptive_sampler def transaction_event_aggregator @transaction_event_recorder.transaction_event_aggregator end def synthetics_event_aggregator @transaction_event_recorder.synthetics_event_aggregator end def agent_id=(agent_id) @service.agent_id = agent_id end # This method should be called in a forked process after a fork. # It assumes the parent process initialized the agent, but does # not assume the agent started. # # The call is idempotent, but not reentrant. # # * It clears any metrics carried over from the parent process # * Restarts the sampler thread if necessary # * Initiates a new agent run and worker loop unless that was done # in the parent process and +:force_reconnect+ is not true # # Options: # * :force_reconnect => true to force the spawned process to # establish a new connection, such as when forking a long running process. # The default is false--it will only connect to the server if the parent # had not connected. # * :keep_retrying => false if we try to initiate a new # connection, this tells me to only try it once so this method returns # quickly if there is some kind of latency with the server. def after_fork(options = {}) return unless needs_after_fork_work? ::NewRelic::Agent.logger.debug("Starting the worker thread in #{Process.pid} (parent #{Process.ppid}) after forking.") channel_id = options[:report_to_channel] install_pipe_service(channel_id) if channel_id # Clear out locks and stats left over from parent process reset_objects_with_locks drop_buffered_data setup_and_start_agent(options) end def needs_after_fork_work? needs_restart = false @after_fork_lock.synchronize do needs_restart = @harvester.needs_restart? @harvester.mark_started end return false if !needs_restart || !Agent.config[:agent_enabled] || !Agent.config[:monitor_mode] || disconnected? || !control.security_settings_valid? true end def install_pipe_service(channel_id) @service = PipeService.new(channel_id) if connected? @connected_pid = Process.pid else ::NewRelic::Agent.logger.debug("Child process #{Process.pid} not reporting to non-connected parent (process #{Process.ppid}).") @service.shutdown disconnect end end def revert_to_default_configuration Agent.config.remove_config_type(:manual) Agent.config.remove_config_type(:server) end def trap_signals_for_litespeed # if litespeed, then ignore all future SIGUSR1 - it's # litespeed trying to shut us down if Agent.config[:dispatcher] == :litespeed Signal.trap('SIGUSR1', 'IGNORE') Signal.trap('SIGTERM', 'IGNORE') end end # Sets a thread local variable as to whether we should or # should not record sql in the current thread. Returns the # previous value, if there is one def set_record_sql(should_record) # THREAD_LOCAL_ACCESS state = Tracer.state prev = state.record_sql state.record_sql = should_record prev.nil? || prev end # Push flag indicating whether we should be tracing in this # thread. This uses a stack which allows us to disable tracing # children of a transaction without affecting the tracing of # the whole transaction def push_trace_execution_flag(should_trace = false) # THREAD_LOCAL_ACCESS Tracer.state.push_traced(should_trace) end # Pop the current trace execution status. Restore trace execution status # to what it was before we pushed the current flag. def pop_trace_execution_flag # THREAD_LOCAL_ACCESS Tracer.state.pop_traced end # Clear out the metric data, errors, and transaction traces, etc. def drop_buffered_data @stats_engine.reset! @error_collector.drop_buffered_data @transaction_sampler.reset! @transaction_event_recorder.drop_buffered_data @custom_event_aggregator.reset! @span_event_aggregator.reset! @log_event_aggregator.reset! @sql_sampler.reset! if Agent.config[:clear_transaction_state_after_fork] Tracer.clear_state end end # Clear out state for any objects that we know lock from our parents # This is necessary for cases where we're in a forked child and Ruby # might be holding locks for background thread that aren't there anymore. def reset_objects_with_locks @stats_engine = StatsEngine.new end def flush_pipe_data if connected? && @service.is_a?(PipeService) transmit_data_types end end private # A shorthand for NewRelic::Control.instance def control NewRelic::Control.instance end def container_for_endpoint(endpoint) case endpoint when :metric_data then @stats_engine when :transaction_sample_data then @transaction_sampler when :error_data then @error_collector.error_trace_aggregator when :error_event_data then @error_collector.error_event_aggregator when :analytic_event_data then transaction_event_aggregator when :custom_event_data then @custom_event_aggregator when :span_event_data then span_event_aggregator when :sql_trace_data then @sql_sampler when :log_event_data then @log_event_aggregator end end def merge_data_for_endpoint(endpoint, data) if data && !data.empty? container = container_for_endpoint(endpoint) if container.respond_to?(:has_metadata?) && container.has_metadata? container_for_endpoint(endpoint).merge!(data, false) else container_for_endpoint(endpoint).merge!(data) end end rescue => e NewRelic::Agent.logger.error("Error while merging #{endpoint} data from child: ", e) end public :merge_data_for_endpoint end extend ClassMethods include InstanceMethods end end end