class CFLightAPIWorker
Constants
- ENVIRONMENT_VARIABLES_WHITELIST
Public Class Methods
new()
click to toggle source
# File lib/cf_light_api/worker.rb, line 19 def initialize @logger = Logger.new(STDOUT) if ENV['DEBUG'] @logger.level = Logger::DEBUG else @logger.level = Logger::INFO end @logger.formatter = proc do |severity, datetime, progname, msg| "#{datetime} [cf_light_api:worker]: #{msg}\n" end ['CF_API', 'CF_USER', 'CF_PASSWORD'].each do |env| unless ENV[env] @logger.info "Error: please set the '#{env}' environment variable." exit 1 end end # If either of the Graphite settings are set, verify that they are both set, or exit with an error. CF_ENV_NAME is used # to prefix the Graphite key, to allow filtering by environment if you run more than one. if ENV['GRAPHITE_HOST'] or ENV['GRAPHITE_PORT'] ['GRAPHITE_HOST', 'GRAPHITE_PORT', 'CF_ENV_NAME'].each do |env| unless ENV[env] @logger.info "Error: please also set the '#{env}' environment variable to enable exporting to Graphite." exit 1 end end end update_interval = (ENV['UPDATE_INTERVAL'] || '5m').to_s # If you change the default '5m' here, also remember to change the default age validity in sinatra/cf_light_api.rb:31 update_timeout = (ENV['UPDATE_TIMEOUT'] || '5m').to_s @update_threads = (ENV['UPDATE_THREADS'] || 1).to_i @lock_manager = Redlock::Client.new([ENV['REDIS_URI']]) @scheduler = Rufus::Scheduler.new @logger.info "Update interval: '#{update_interval}'" @logger.info "Update timeout: '#{update_timeout}'" @logger.info "Update threads: '#{@update_threads}'" if ENV['GRAPHITE_HOST'] and ENV['GRAPHITE_PORT'] @logger.info "Graphite server: #{ENV['GRAPHITE_HOST']}:#{ENV['GRAPHITE_PORT']}" else @logger.info 'Graphite server: Disabled' end @scheduler.every update_interval, :first_in => '5s', :overlap => false, :timeout => update_timeout do update_cf_data end end
Public Instance Methods
cf_rest(path, method='GET')
click to toggle source
# File lib/cf_light_api/worker.rb, line 73 def cf_rest(path, method='GET') @logger.debug "Making #{method} request for #{path}..." resources = [] options = {:accept => :json} response = @cf_client.base.rest_client.request(method, path, options)[1][:body] begin response = JSON.parse(response) if response['error_code'] raise CFResponseError.new("Code #{response['code']}, #{response['error_code']} - #{response['description']}") end rescue Rufus::Scheduler::TimeoutError => e raise e rescue JSON::ParserError => e @logger.error "Error parsing JSON response from #{method} #{path}: #{e.message}" @logger.trace e.backtrace @logger.trace response raise e rescue CFoundry => e @logger.error "CFoundry error making #{method} #{path}: #{e.message}" @logger.trace e.backtrace @logger.trace response raise e rescue CFResponseError => e @logger.error "CF API returned a response with an error document for #{method} #{path}: #{e.message}" @logger.trace e.backtrace @logger.trace response raise e rescue StandardError => e @logger.error "General error making #{method} #{path}: #{e.message}" @logger.trace e.backtrace @logger.trace response raise e end # Some endpoints return a 'resources' array, others are flat, depending on the path. if response['resources'] resources << response['resources'] else resources << response end # Handle the pagination by recursing over myself until we get a response which doesn't contain a 'next_url' # at which point all the resources are returned up the stack and flattened. resources << cf_rest(response['next_url'], method) unless response['next_url'] == nil resources.flatten end
filtered_environment_variables(env_vars)
click to toggle source
# File lib/cf_light_api/worker.rb, line 237 def filtered_environment_variables env_vars if ENVIRONMENT_VARIABLES_WHITELIST.any? return ENVIRONMENT_VARIABLES_WHITELIST.inject({}) do |filtered, key| filtered[key] = env_vars[key] if env_vars[key] filtered end else return env_vars end end
find_domain_for_route(route)
click to toggle source
# File lib/cf_light_api/worker.rb, line 186 def find_domain_for_route route return @domains.find{|a_domain| a_domain['metadata']['guid'] == route['entity']['domain_guid']} end
format_duration(elapsed_seconds)
click to toggle source
# File lib/cf_light_api/worker.rb, line 170 def format_duration(elapsed_seconds) seconds = elapsed_seconds % 60 minutes = (elapsed_seconds / 60) % 60 hours = elapsed_seconds / (60 * 60) format("%02d hrs, %02d mins, %02d secs", hours, minutes, seconds) end
format_orgs(orgs)
click to toggle source
# File lib/cf_light_api/worker.rb, line 249 def format_orgs orgs return orgs.map do |org| quota = @quotas.find{|a_quota| a_quota['metadata']['guid'] == org['entity']['quota_definition_guid']} quota = { :total_services => quota['entity']['total_services'], :total_routes => quota['entity']['total_routes'], :memory_limit => quota['entity']['memory_limit'] * 1024 * 1024 } send_org_quota_data_to_graphite(org['entity']['name'], quota) if @graphite { :guid => org['metadata']['guid'], :name => org['entity']['name'], :quota => quota } end end
format_routes_for_app(app)
click to toggle source
# File lib/cf_light_api/worker.rb, line 190 def format_routes_for_app app # The app object passed in here should contain a "routes" attribute, fetched as part of the original request to CF (with inline-relation gathering enabled) # and it will look something like this: # "routes"=> # [{"metadata"=>{"guid"=>"afea5690-fb93-451a-9610-2d524d36e35f", "url"=>"/v2/routes/afea5690-fb93-451a-9610-2d524d36e35f", "created_at"=>"2015-03-11T12:20:22Z", "updated_at"=>"2015-03-11T12:20:22Z"}, # "entity"=> # {"host"=>"hostname_here", # "path"=>"", # "domain_guid"=>"f13e6864-537e-41bb-b46c-f3810dbf7c84", # "space_guid"=>"c0af44b8-8b51-4db5-927e-ccad2e6dab54", # "service_instance_guid"=>nil, # "port"=>nil, # "domain_url"=>"/v2/shared_domains/f13e6864-537e-41bb-b46c-f3810dbf7c84", # "space_url"=>"/v2/spaces/c0af44b8-8b51-4db5-927e-ccad2e6dab54", # "apps_url"=>"/v2/routes/afea5690-fb93-451a-9610-2d524d36e35f/apps", # "route_mappings_url"=>"/v2/routes/afea5690-fb93-451a-9610-2d524d36e35f/route_mappings"}}, # If we don't receive that child attribute, (perhaps the app was being staged or didn't have any routes yet) we make another request to CF to try # and fetch them before giving up and just returning an empty array. routes = [] if app['entity']['routes'] == nil # We have no routes data inlined with the app entity, so let's try to retrieve them directly from CF routes = cf_rest(app['entity']['routes_url']) else # Routes were already retrieved as an inline-relation, so just use those... routes = app['entity']['routes'] end routes.collect do |route| host = route['entity']['host'] path = route['entity']['path'] domain = find_domain_for_route(route) if domain == nil # The domain doesn't exist, this could be due to a race condition, so let's update the list and try again update_domains() domain = find_domain_for_route(route) if domain == nil # If we can't determine the domain associated with this route, raise an error as we can't guarantee the state is correct here, # it shouldn't be possible to get a route back from CF with a domain GUID that doesn't exist, as that route would be invalid. raise "Unable to find domain #{route['entity']['domain_guid']} for route #{route['metadata']['guid']}." end end "#{host}.#{domain['entity']['name']}#{path}" end end
get_buildpacks_by_guid()
click to toggle source
# File lib/cf_light_api/worker.rb, line 181 def get_buildpacks_by_guid buildpacks = cf_rest('/v2/buildpacks?results-per-page=100') buildpacks_by_guid = buildpacks.map { |buildpack| [buildpack['metadata']['guid'], buildpack] }.to_h end
get_client(cf_api=ENV['CF_API'], cf_user=ENV['CF_USER'], cf_password=ENV['CF_PASSWORD'])
click to toggle source
# File lib/cf_light_api/worker.rb, line 123 def get_client(cf_api=ENV['CF_API'], cf_user=ENV['CF_USER'], cf_password=ENV['CF_PASSWORD']) client = CFoundry::Client.get(cf_api) client.login({:username => cf_user, :password => cf_password}) client end
get_v1_base_data(app)
click to toggle source
# File lib/cf_light_api/worker.rb, line 269 def get_v1_base_data app # Format the app data in the expected format for the /v1/apps endpoint, to remain compatible. # Find the org for this app, using the org GUID from the space. Relationship: Apps belong to spaces, and spaces belong to orgs. space = @spaces.find{|a_space| a_space['metadata']['guid'] == app['entity']['space_guid']} org = @orgs.find{|an_org| an_org['metadata']['guid'] == space['entity']['organization_guid']} { :buildpack => app['entity']['buildpack'], :data_from => Time.now.to_i, :diego => app['entity']['diego'], :docker => app['entity']['docker_image'] ? true : false, :docker_image => app['entity']['docker_image'], :guid => app['metadata']['guid'], :last_uploaded => app['metadata']['updated_at'] ? DateTime.parse(app['metadata']['updated_at']).strftime('%Y-%m-%d %T %z') : nil, :name => app['entity']['name'], :org => org['entity']['name'], :space => space['entity']['name'], :stack => app['entity']['stack']['entity']['name'], :state => app['entity']['state'], :instances => [], :routes => [] } end
put_in_redis(key, data)
click to toggle source
# File lib/cf_light_api/worker.rb, line 166 def put_in_redis(key, data) REDIS.set key, data.to_json end
send_cf_light_api_update_time_to_graphite(seconds)
click to toggle source
# File lib/cf_light_api/worker.rb, line 160 def send_cf_light_api_update_time_to_graphite seconds graphite_key = "cf_light_api.#{ENV['CF_ENV_NAME']}.update_duration" @logger.info "Exporting CF Light API update time to Graphite, path #{graphite_key} => #{seconds.round}" @graphite.metrics "#{graphite_key}" => seconds.round end
send_instance_usage_data_to_graphite(instance_stats, org, space, app_name)
click to toggle source
# File lib/cf_light_api/worker.rb, line 129 def send_instance_usage_data_to_graphite(instance_stats, org, space, app_name) sanitised_app_name = app_name.gsub ".", "_" # Some apps have dots in the app name which breaks the Graphite key path instance_stats.each_with_index do |instance_data, index| graphite_base_key = "cf_apps.#{ENV['CF_ENV_NAME']}.#{org}.#{space}.#{sanitised_app_name}.#{index}" @logger.debug " Exporting app instance \##{index} usage statistics to Graphite, path '#{graphite_base_key}'" # Quota data ['mem_quota', 'disk_quota'].each do |key| @logger.trace "#{graphite_base_key}.#{key} => #{instance_data['stats'][key]}" @graphite.metrics "#{graphite_base_key}.#{key}" => instance_data['stats'][key] end # Usage data ['mem', 'disk', 'cpu'].each do |key| @logger.trace "#{graphite_base_key}.#{key} => #{instance_data['stats']['usage'][key]}" @graphite.metrics "#{graphite_base_key}.#{key}" => instance_data['stats']['usage'][key] end end end
send_org_quota_data_to_graphite(org_name, quota)
click to toggle source
# File lib/cf_light_api/worker.rb, line 150 def send_org_quota_data_to_graphite(org_name, quota) graphite_base_key = "cf_orgs.#{ENV['CF_ENV_NAME']}.#{org_name}" @logger.debug " Exporting org quota statistics to Graphite, path '#{graphite_base_key}'" quota.keys.each do |key| @logger.trace "#{graphite_base_key}.quota.#{key} => #{quota[key]}" @graphite.metrics "#{graphite_base_key}.quota.#{key}" => quota[key] end end
update_cf_data()
click to toggle source
# File lib/cf_light_api/worker.rb, line 294 def update_cf_data @cf_client = nil @graphite = GraphiteAPI.new(graphite: "#{ENV['GRAPHITE_HOST']}:#{ENV['GRAPHITE_PORT']}") if ENV['GRAPHITE_HOST'] and ENV['GRAPHITE_PORT'] and ENV['CF_ENV_NAME'] begin @lock_manager.lock("#{ENV['REDIS_KEY_PREFIX']}:lock", 5*60*1000) do |lock| if lock start_time = Time.now @logger.info "Updating data..." @cf_client = get_client() # Ensure we have a fresh auth token... @cf_info = cf_rest('/v2/info').first @apps = cf_rest('/v2/apps?results-per-page=100&inline-relations-depth=1&include-relations=routes,stack') @spaces = cf_rest('/v2/spaces?results-per-page=100') @buildpacks = get_buildpacks_by_guid() # Sets @buildpacks to a map of buildpack resources indexed by guid update_domains() # Sets @domain by hitting the CF API # Orgs @orgs = cf_rest('/v2/organizations?results-per-page=100') @quotas = cf_rest('/v2/quota_definitions?results-per-page=100') formatted_orgs = format_orgs @orgs v1_data = [] v2_data = [] Parallel.each(@apps, :in_threads => @update_threads) do |app| begin # Formats the base data compatible with the v1 endpoint v1_document = get_v1_base_data(app) # New format base data for the v2 endpoint v2_document = app['entity'].dup v2_document['environment_json'] = {} # The environment JSON will have been duplicated from the app entity, so we need to blank it here, as it will be re-populated later if EXPOSE_ENVIRONMENT_VARIABLES is true. v2_document['created_at'] = app['metadata']['created_at'] v2_document['updated_at'] = app['metadata']['updated_at'] v2_document['guid'] = app['metadata']['guid'] v2_document['instances'] = [] v2_document['routes'] = [] v2_document['meta'] = { 'error' => false } # Add buildpack_name as a top level string attribute and looks it up using its guid when the buildpack field is null buildpack_name = app['entity']['buildpack'] buildpack_guid = app['entity']['detected_buildpack_guid'] v2_document['buildpack_name'] = if @buildpacks.has_key?(buildpack_guid) and (buildpack_name.nil? or buildpack_name.empty?) @buildpacks[buildpack_guid]['entity']['name'] else buildpack_name end # Add space, stack and org names as a top level string attribute for ease of use: v2_document['stack'] = app['entity']['stack']['entity']['name'] # Get the org name from the app's space - relationship: an app belongs to a space, and a space belongs to an org. space = @spaces.find{|a_space| a_space['metadata']['guid'] == app['entity']['space_guid']} org = @orgs.find{|an_org| an_org['metadata']['guid'] == space['entity']['organization_guid']} v2_document['space'] = space['entity']['name'] v1_document['org'] = org['entity']['name'] v2_document['org'] = org['entity']['name'] # Gather and filter environment variable JSON if the feature is enabled: if ENV['EXPOSE_ENVIRONMENT_VARIABLES'] == 'true' then env_vars = filtered_environment_variables( app['entity']['environment_json'] ) v1_document['environment_variables'] = env_vars v2_document['environment_json'] = env_vars end routes = format_routes_for_app(app) v1_document['routes'] = routes v2_document['routes'] = routes # Try to gather app instance stats, unless the app is stopped... unless app['entity']['state'] == 'STOPPED' response = cf_rest("/v2/apps/#{app['metadata']['guid']}/stats") instances = response.first.map{|key,value|value} v1_document['instances'] = instances v2_document['instances'] = instances end # We consider an app to be "running" if there is at least one app instance available with a state of "RUNNING" running = false running_instances = [] if v2_document['instances'].any? running_instances = v2_document['instances'].select{|instance| instance['state'] == 'RUNNING'} running = true if running_instances.any? end v1_document['running'] = running v2_document['running'] = running if @graphite if running_instances.any? send_instance_usage_data_to_graphite(running_instances, v2_document['org'], v2_document['space'], v2_document['name']) end end rescue Rufus::Scheduler::TimeoutError => e raise e rescue CFoundry, CFResponseError, StandardError => e v1_document['running'] = "error" v1_document['error'] = "#{e.message}" v2_document['meta'] = {} v2_document['meta']['error'] = true v2_document['meta']['type'] = e.class v2_document['meta']['message'] = e.message v2_document['meta']['backtrace'] = e.backtrace end v1_data << v1_document v2_data << v2_document end # Sanity check - do we have the expected quantity of data? This shouldn't happen as the `parallel` gem should handle # sharing and modifying variables for us when using threads. if @apps.count != v1_data.count or @apps.count != v2_data.count raise "V1 and V2 app counts don't match after processing!" end put_in_redis "#{ENV['REDIS_KEY_PREFIX']}:info", @cf_info put_in_redis "#{ENV['REDIS_KEY_PREFIX']}:orgs", formatted_orgs put_in_redis "#{ENV['REDIS_KEY_PREFIX']}:apps", v1_data put_in_redis "#{ENV['REDIS_KEY_PREFIX']}:apps:v2", v2_data put_in_redis "#{ENV['REDIS_KEY_PREFIX']}:last_updated", {:last_updated => Time.now} elapsed_seconds = Time.now.to_f - start_time.to_f send_cf_light_api_update_time_to_graphite(elapsed_seconds) if @graphite @logger.info "Update completed in #{format_duration(elapsed_seconds)}..." @lock_manager.unlock(lock) @cf_client.logout else @logger.info "Update already running in another instance!" end end rescue Rufus::Scheduler::TimeoutError Parallel::Kill @cf_client.logout @logger.info 'Data update took too long and was aborted, waiting for the lock to expire before trying again...' send_cf_light_api_update_time_to_graphite(0) if @graphite rescue StandardError => e @logger.info "Unable to complete update due to #{e.class}: #{e.message}" @logger.error e.backtrace end end
update_domains()
click to toggle source
# File lib/cf_light_api/worker.rb, line 177 def update_domains @domains = cf_rest('/v2/domains?results-per-page=100') end