class Chef::DataCollector::Reporter

Chef::DataCollector::Reporter

Provides an event handler that can be registered to report on Chef run data. Unlike the existing Chef::ResourceReporter event handler, the DataCollector handler is not tied to a Chef Server / Chef Reporting and exports its data through a webhook-like mechanism to a configured endpoint.

Attributes

all_resource_reports[R]
current_resource_report[R]
deprecations[R]
enabled[R]
error_descriptions[R]
exception[R]
expanded_run_list[R]
http[R]
run_context[R]
run_status[R]
status[R]

Public Class Methods

new() click to toggle source
# File lib/chef/data_collector.rb, line 109
def initialize
  validate_data_collector_server_url!
  validate_data_collector_output_locations! if data_collector_output_locations
  @all_resource_reports    = []
  @current_resource_loaded = nil
  @error_descriptions      = {}
  @expanded_run_list       = {}
  @deprecations            = Set.new
  @enabled                 = true

  @http = setup_http_client(data_collector_server_url)
  if data_collector_output_locations
    @http_output_locations = setup_http_output_locations if data_collector_output_locations[:urls]
  end
end

Public Instance Methods

converge_complete() click to toggle source

see Chef::EventDispatch::Base#converge_complete At the end of the converge, we add any unprocessed resources to our report list.

# File lib/chef/data_collector.rb, line 166
def converge_complete
  detect_unprocessed_resources
end
converge_failed(exception) click to toggle source

see Chef::EventDispatch::Base#converge_failed At the end of the converge, we add any unprocessed resources to our report list

# File lib/chef/data_collector.rb, line 173
def converge_failed(exception)
  detect_unprocessed_resources
end
converge_start(run_context) click to toggle source

see Chef::EventDispatch::Base#converge_start Upon receipt, we stash the #run_context for use at the end of the run in order to determine what resource+action combinations have not yet fired so we can report on unprocessed resources.

# File lib/chef/data_collector.rb, line 159
def converge_start(run_context)
  @run_context = run_context
end
cookbook_resolution_failed(expanded_run_list, exception) click to toggle source

see Chef::EventDispatch::Base#cookbook_resolution_failed The run error text is updated with the output of the appropriate formatter.

# File lib/chef/data_collector.rb, line 262
def cookbook_resolution_failed(expanded_run_list, exception)
  update_error_description(
    Formatters::ErrorMapper.cookbook_resolution_failed(
      expanded_run_list,
      exception
    ).for_json
  )
end
cookbook_sync_failed(cookbooks, exception) click to toggle source

see Chef::EventDispatch::Base#cookbook_sync_failed The run error text is updated with the output of the appropriate formatter.

# File lib/chef/data_collector.rb, line 274
def cookbook_sync_failed(cookbooks, exception)
  update_error_description(
    Formatters::ErrorMapper.cookbook_sync_failed(
      cookbooks,
      exception
    ).for_json
  )
end
deprecation(message, location = caller(2..2)[0]) click to toggle source

see Chef::EventDispatch::Base#deprecation Append a received deprecation to the list of deprecations

# File lib/chef/data_collector.rb, line 285
def deprecation(message, location = caller(2..2)[0])
  add_deprecation(message.message, message.url, location)
end
resource_completed(new_resource) click to toggle source

see Chef::EventDispatch::Base#resource_completed Mark the ResourceReport instance as finished (for timing details). This marks the end of this resource during this run.

# File lib/chef/data_collector.rb, line 232
def resource_completed(new_resource)
  if current_resource_report && !nested_resource?(new_resource)
    current_resource_report.finish
    add_resource_report(current_resource_report)
    clear_current_resource_report
  end
end
resource_current_state_loaded(new_resource, action, current_resource) click to toggle source

see Chef::EventDispatch::Base#resource_current_state_loaded Create a new ResourceReport instance that we'll use to track the state of this resource during the run. Nested resources are ignored as they are assumed to be an inline resource of a custom resource, and we only care about tracking top-level resources.

# File lib/chef/data_collector.rb, line 182
def resource_current_state_loaded(new_resource, action, current_resource)
  return if nested_resource?(new_resource)
  initialize_resource_report_if_needed(new_resource, action, current_resource)
end
resource_failed(new_resource, action, exception) click to toggle source

see Chef::EventDispatch::Base#resource_failed Flag the current ResourceReport as failed and supply the exception as long as it's a top-level resource, and update the run error text with the proper Formatter.

# File lib/chef/data_collector.rb, line 217
def resource_failed(new_resource, action, exception)
  initialize_resource_report_if_needed(new_resource, action)
  current_resource_report.failed(exception) unless nested_resource?(new_resource)
  update_error_description(
    Formatters::ErrorMapper.resource_failed(
      new_resource,
      action,
      exception
    ).for_json
  )
end
resource_skipped(new_resource, action, conditional) click to toggle source

see Chef::EventDispatch::Base#resource_skipped If this is a top-level resource, we create a ResourceReport instance (because a skipped resource does not trigger the #resource_current_state_loaded event), and flag it as skipped.

# File lib/chef/data_collector.rb, line 198
def resource_skipped(new_resource, action, conditional)
  return if nested_resource?(new_resource)

  initialize_resource_report_if_needed(new_resource, action)
  current_resource_report.skipped(conditional)
end
resource_up_to_date(new_resource, action) click to toggle source

see Chef::EventDispatch::Base#resource_up_to_date Mark our ResourceReport status accordingly

# File lib/chef/data_collector.rb, line 189
def resource_up_to_date(new_resource, action)
  initialize_resource_report_if_needed(new_resource, action)
  current_resource_report.up_to_date unless nested_resource?(new_resource)
end
resource_updated(new_resource, action) click to toggle source

see Chef::EventDispatch::Base#resource_updated Flag the current ResourceReport instance as updated (as long as it's a top-level resource).

# File lib/chef/data_collector.rb, line 208
def resource_updated(new_resource, action)
  initialize_resource_report_if_needed(new_resource, action)
  current_resource_report.updated unless nested_resource?(new_resource)
end
run_completed(node) click to toggle source

see Chef::EventDispatch::Base#run_completed Upon receipt, we will send our run completion message to the configured DataCollector endpoint.

# File lib/chef/data_collector.rb, line 145
def run_completed(node)
  send_run_completion(status: "success")
end
run_failed(exception) click to toggle source

see Chef::EventDispatch::Base#run_failed

# File lib/chef/data_collector.rb, line 150
def run_failed(exception)
  send_run_completion(status: "failure")
end
run_list_expand_failed(node, exception) click to toggle source

see Chef::EventDispatch::Base#run_list_expand_failed The run error text is updated with the output of the appropriate formatter.

# File lib/chef/data_collector.rb, line 250
def run_list_expand_failed(node, exception)
  update_error_description(
    Formatters::ErrorMapper.run_list_expand_failed(
      node,
      exception
    ).for_json
  )
end
run_list_expanded(run_list_expansion) click to toggle source

see Chef::EventDispatch::Base#run_list_expanded The expanded run list is stored for later use by the #run_completed event and message.

# File lib/chef/data_collector.rb, line 243
def run_list_expanded(run_list_expansion)
  @expanded_run_list = run_list_expansion
end
run_started(current_run_status) click to toggle source

see Chef::EventDispatch::Base#run_started Upon receipt, we will send our run start message to the configured DataCollector endpoint. Depending on whether the user has configured raise_on_failure, if we cannot send the message, we will either disable the DataCollector Reporter for the duration of this run, or we'll raise an exception.

# File lib/chef/data_collector.rb, line 132
def run_started(current_run_status)
  update_run_status(current_run_status)

  message = Chef::DataCollector::Messages.run_start_message(current_run_status)
  disable_reporter_on_error do
    send_to_data_collector(message)
  end
  send_to_output_locations(message) if data_collector_output_locations
end

Private Instance Methods

add_deprecation(message, url, location) click to toggle source
# File lib/chef/data_collector.rb, line 451
def add_deprecation(message, url, location)
  @deprecations << { message: message, url: url, location: location }
end
add_resource_report(resource_report) click to toggle source
# File lib/chef/data_collector.rb, line 427
def add_resource_report(resource_report)
  @all_resource_reports << OpenStruct.new(
    resource: resource_report.new_resource,
    action: resource_report.action,
    report_data: resource_report.to_hash
  )
end
clear_current_resource_report() click to toggle source
# File lib/chef/data_collector.rb, line 468
def clear_current_resource_report
  @current_resource_report = nil
end
create_resource_report(new_resource, action, current_resource = nil) click to toggle source
# File lib/chef/data_collector.rb, line 460
def create_resource_report(new_resource, action, current_resource = nil)
  Chef::DataCollector::ResourceReport.new(
    new_resource,
    action,
    current_resource
  )
end
data_collector_accessible?() click to toggle source
# File lib/chef/data_collector.rb, line 439
def data_collector_accessible?
  @enabled
end
data_collector_output_locations() click to toggle source
# File lib/chef/data_collector.rb, line 419
def data_collector_output_locations
  Chef::Config[:data_collector][:output_locations]
end
data_collector_server_url() click to toggle source
# File lib/chef/data_collector.rb, line 415
def data_collector_server_url
  Chef::Config[:data_collector][:server_url]
end
data_collector_token() click to toggle source
# File lib/chef/data_collector.rb, line 423
def data_collector_token
  Chef::Config[:data_collector][:token]
end
detect_unprocessed_resources() click to toggle source
# File lib/chef/data_collector.rb, line 472
def detect_unprocessed_resources
  # create a Hash (for performance reasons, rather than an Array) containing all
  # resource+action combinations from the Resource Collection
  #
  # We use the object ID instead of the resource itself in the Hash key because
  # we currently allow users to create a property called "hash" which creates
  # a #hash instance method on the resource. Ruby expects that to be a Fixnum,
  # so bad things happen when adding an object to an Array or a Hash if it's not.
  collection_resources = {}
  run_context.resource_collection.all_resources.each do |resource|
    Array(resource.action).each do |action|
      collection_resources[[resource.__id__, action]] = resource
    end
  end

  # Delete from the Hash any resource+action combination we have
  # already processed.
  all_resource_reports.each do |report|
    collection_resources.delete([report.resource.__id__, report.action])
  end

  # The items remaining in the Hash are unprocessed resource+actions,
  # so we'll create new resource reports for them which default to
  # a state of "unprocessed".
  collection_resources.each do |key, resource|
    # The Hash key is an array of the Resource's object ID and the action.
    # We need to pluck out the action.
    add_resource_report(create_resource_report(resource, key[1]))
  end
end
disable_data_collector_reporter() click to toggle source
# File lib/chef/data_collector.rb, line 435
def disable_data_collector_reporter
  @enabled = false
end
disable_reporter_on_error() { || ... } click to toggle source

Yields to the passed-in block (which is expected to be some interaction with the DataCollector endpoint). If some communication failure occurs, either disable any future communications to the DataCollector endpoint, or raise an exception (if the user has set Chef::Config.data_collector.raise_on_failure to true.)

@param block [Proc] A ruby block to run. Ignored if a command is given.

# File lib/chef/data_collector.rb, line 319
def disable_reporter_on_error
  yield
rescue Timeout::Error, Errno::EINVAL, Errno::ECONNRESET,
       Errno::ECONNREFUSED, EOFError, Net::HTTPBadResponse,
       Net::HTTPHeaderSyntaxError, Net::ProtocolError, OpenSSL::SSL::SSLError,
       Errno::EHOSTDOWN => e
  # Do not disable data collector reporter if additional output_locations have been specified
  disable_data_collector_reporter unless data_collector_output_locations
  code = if e.respond_to?(:response) && e.response.code
           e.response.code.to_s
         else
           "Exception Code Empty"
         end

  msg = "Error while reporting run start to Data Collector. " \
        "URL: #{data_collector_server_url} " \
        "Exception: #{code} -- #{e.message} "

  if Chef::Config[:data_collector][:raise_on_failure]
    Chef::Log.error(msg)
    raise
  else
    # Make the message non-scary for folks who don't have automate:
    msg << " (This is normal if you do not have Chef Automate)"
    Chef::Log.info(msg)
  end
end
handle_output_location(type, loc, message) click to toggle source
# File lib/chef/data_collector.rb, line 360
def handle_output_location(type, loc, message)
  type == :urls ? send_to_http_location(loc, message) : send_to_file_location(loc, message)
end
handle_type(type, loc) click to toggle source
# File lib/chef/data_collector.rb, line 544
def handle_type(type, loc)
  type == :urls ? validate_and_return_uri(loc) : validate_and_create_file(loc)
end
headers() click to toggle source
# File lib/chef/data_collector.rb, line 404
def headers
  headers = { "Content-Type" => "application/json" }

  unless data_collector_token.nil?
    headers["x-data-collector-token"] = data_collector_token
    headers["x-data-collector-auth"]  = "version=1.0"
  end

  headers
end
initialize_resource_report_if_needed(new_resource, action, current_resource = nil) click to toggle source
# File lib/chef/data_collector.rb, line 455
def initialize_resource_report_if_needed(new_resource, action, current_resource = nil)
  return unless current_resource_report.nil?
  @current_resource_report = create_resource_report(new_resource, action, current_resource)
end
nested_resource?(new_resource) click to toggle source

If we are getting messages about a resource while we are in the middle of another resource's update, we assume that the nested resource is just the implementation of a provider, and we want to hide it from the reporting output.

# File lib/chef/data_collector.rb, line 507
def nested_resource?(new_resource)
  @current_resource_report && @current_resource_report.new_resource != new_resource
end
send_run_completion(opts) click to toggle source

Send any messages to the DataCollector endpoint that are necessary to indicate the run has completed. Currently, two messages are sent:

  • An “action” message with the node object indicating it's been updated

  • An “run_converge” (i.e. RunEnd) message with details about the run, what resources were modified/up-to-date/skipped, etc.

@param opts [Hash] Additional details about the run, such as its success/failure.

# File lib/chef/data_collector.rb, line 384
def send_run_completion(opts)
  # If run_status is nil we probably failed before the client triggered
  # the run_started callback. In this case we'll skip updating because
  # we have nothing to report.
  return unless run_status

  message = Chef::DataCollector::Messages.run_end_message(
             run_status: run_status,
             expanded_run_list: expanded_run_list,
             resources: all_resource_reports,
             status: opts[:status],
             error_descriptions: error_descriptions,
             deprecations: deprecations.to_a
            )
  disable_reporter_on_error do
    send_to_data_collector(message)
  end
  send_to_output_locations(message) if data_collector_output_locations
end
send_to_data_collector(message) click to toggle source
# File lib/chef/data_collector.rb, line 347
def send_to_data_collector(message)
  return unless data_collector_accessible?
  http.post(nil, message, headers) if data_collector_server_url
end
send_to_file_location(file_name, message) click to toggle source
# File lib/chef/data_collector.rb, line 364
def send_to_file_location(file_name, message)
  open(file_name, "a") { |f| f.puts message }
end
send_to_http_location(http_url, message) click to toggle source
# File lib/chef/data_collector.rb, line 368
def send_to_http_location(http_url, message)
  @http_output_locations[http_url].post(nil, message, headers) if @http_output_locations[http_url]
rescue
  Chef::Log.trace("Data collector failed to send to URL location #{http_url}. Please check your configured data_collector.output_locations")
end
send_to_output_locations(message) click to toggle source
# File lib/chef/data_collector.rb, line 352
def send_to_output_locations(message)
  data_collector_output_locations.each do |type, location_list|
    location_list.each do |l|
      handle_output_location(type, l, message)
    end
  end
end
setup_http_client(url) click to toggle source

Selects the type of HTTP client to use based on whether we are using token-based or signed header authentication. Token authentication is intended to be used primarily for Chef Solo in which case no signing key will be available (in which case `Chef::ServerAPI.new()` would raise an exception.

# File lib/chef/data_collector.rb, line 296
def setup_http_client(url)
  if data_collector_token.nil?
    Chef::ServerAPI.new(url, validate_utf8: false)
  else
    Chef::HTTP::SimpleJSON.new(url, validate_utf8: false)
  end
end
setup_http_output_locations() click to toggle source
# File lib/chef/data_collector.rb, line 304
def setup_http_output_locations
  Chef::Config[:data_collector][:output_locations][:urls].each_with_object({}) do |location_url, http_output_locations|
    http_output_locations[location_url] = setup_http_client(location_url)
  end
end
update_error_description(discription_hash) click to toggle source
# File lib/chef/data_collector.rb, line 447
def update_error_description(discription_hash)
  @error_descriptions = discription_hash
end
update_run_status(run_status) click to toggle source
# File lib/chef/data_collector.rb, line 443
def update_run_status(run_status)
  @run_status = run_status
end
validate_and_create_file(file) click to toggle source
# File lib/chef/data_collector.rb, line 517
def validate_and_create_file(file)
  send_to_file_location(file, "")
  true
# Rescue exceptions raised by the file path being non-existent or not writeable and re-raise them to the user
# with clearer explanatory text.
rescue Errno::ENOENT
  raise Chef::Exceptions::ConfigurationError,
        "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which is a non existent file path."
rescue Errno::EACCES
  raise Chef::Exceptions::ConfigurationError,
        "Chef::Config[:data_collector][:output_locations][:files] contains the location #{file}, which cannnot be written to by Chef."
end
validate_and_return_uri(uri) click to toggle source
# File lib/chef/data_collector.rb, line 511
def validate_and_return_uri(uri)
  URI(uri)
rescue URI::InvalidURIError
  nil
end
validate_data_collector_output_locations!() click to toggle source
# File lib/chef/data_collector.rb, line 548
def validate_data_collector_output_locations!
  if data_collector_output_locations.empty?
    raise Chef::Exceptions::ConfigurationError,
          "Chef::Config[:data_collector][:output_locations] is empty. Please supply an hash of valid URLs and / or local file paths."
  end

  data_collector_output_locations.each do |type, locations|
    locations.each do |l|
      unless handle_type(type, l)
        raise Chef::Exceptions::ConfigurationError,
                "Chef::Config[:data_collector][:output_locations] contains the location #{l} which is not valid."
      end
    end
  end
end
validate_data_collector_server_url!() click to toggle source
# File lib/chef/data_collector.rb, line 530
def validate_data_collector_server_url!
  unless !data_collector_server_url && data_collector_output_locations
    uri = validate_and_return_uri(data_collector_server_url)
    unless uri
      raise Chef::Exceptions::ConfigurationError, "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is not a valid URI."
    end

    if uri.host.nil?
      raise Chef::Exceptions::ConfigurationError,
        "Chef::Config[:data_collector][:server_url] (#{data_collector_server_url}) is a URI with no host. Please supply a valid URL."
    end
  end
end