# Copyright (c) 2022 Contrast Security, Inc. See https://www.contrastsecurity.com/enduser-terms-0317a for more details. # frozen_string_literal: true require 'contrast/config/env_variables' require 'contrast/components/logger' require 'contrast/utils/telemetry_client' require 'contrast/agent/worker_thread' require 'contrast/utils/telemetry' require 'contrast/agent/telemetry/events/exceptions/telemetry_exceptions' require 'contrast/agent/telemetry/events/exceptions/telemetry_exceptions_report' module Contrast module Agent module Telemetry # This class will initialize and hold everything needed for the telemetry class Base < WorkerThread include Contrast::Components::Logger::InstanceMethods include Contrast::Agent::Telemetry::TelemetryExceptionReport # this is where we will send the data from the agents URL = 'https://telemetry.ruby.contrastsecurity.com/' # Suggested timeout after each send is to be 3 hours (10800 seconds) SUGGESTED_TIMEOUT = 10_800 class << self include Contrast::Components::Logger::InstanceMethods include Contrast::Config::EnvVariables def application_id Contrast::Utils::Telemetry::Identifier.application_id end def instance_id Contrast::Utils::Telemetry::Identifier.instance_id end def enabled? @_enabled = telemetry_enabled? if @_enabled.nil? @_enabled end private def telemetry_enabled? opt_out_telemetry = return_value(:telemetry_opt_outs).to_s return false if opt_out_telemetry.casecmp?('true') || opt_out_telemetry == '1' # In case of connection error, do not create the background thread or queue, # as if the opt-out env var was set @_client = Contrast::Utils::TelemetryClient.new ip_opt_out_telemetry = @_client.initialize_connection(URL) if ip_opt_out_telemetry.nil? logger.warn("Connection was not established properly!!! \n Telemetry reporting will be disabled!") return false end true end end def client @_client ||= Contrast::Utils::TelemetryClient.new end def connection @_connection ||= client.initialize_connection(URL) end def error_messages @_error_messages ||= [] end def attempt_to_start? unless cs__class.enabled? logger.warn('Telemetry service is disabled!') return false end logger.debug('Attempting to start telemetry thread') unless running? true end def start_thread! return if running? # It is recommended that implementations send a single payload of # general metrics every 3 hours, starting from implementation startup. @_thread = Contrast::Agent::Thread.new do logger.debug('Starting background telemetry thread.') loop do next unless client && connection # Start pushing exceptions to queue for reporting. push_exceptions until queue.empty? event = queue.pop begin logger.debug('This is the current processed event', event) sleep_time = request_with_response(event) if sleep_time sleep(sleep_time) logger.debug('Retrying to process event', event) retry_sleep_time = request_with_response(event) sleep(retry_sleep_time) unless retry_sleep_time.nil? end rescue StandardError => e logger.error('Could not send message to service from telemetry queue.', e) stop! end end sleep(SUGGESTED_TIMEOUT) end end end def send_event event if ::Contrast::AGENT.disabled? logger.warn('Attempted to queue event with Agent disabled', caller: caller, event: event) return end return unless cs__class.enabled? logger.debug('Enqueued event for sending', event_type: event.cs__class) queue << event if event end def delete_queue! @_queue&.clear @_queue&.close @_queue = nil end def stop! return unless running? @_enabled = false delete_queue! super end def request_with_response event res = client.send_request(event, connection) client.handle_response(res) end private def queue @_queue ||= Queue.new end end end end end