Permalink
Cannot retrieve contributors at this time
Join GitHub today
GitHub is home to over 28 million developers working together to host and review code, manage projects, and build software together.
Sign up
Fetching contributors…
| #!/usr/bin/env ruby | |
| # For use with OpenShift Enterprise 1.0 and 1.1 | |
| # | |
| # This can be used as a script or library on an OpenShift broker host. | |
| # As a script, the default output is for command-line viewing, or choose | |
| # --format tsv for something you can analyze in your favorite spreadsheet, | |
| # or json/xml/yaml to process all the data in your tool of choice. | |
| # Run with the -h flag to view options. | |
| # | |
| # To use as a library, do the following in irb or your ruby script: | |
| # load 'oo-stats' | |
| # stats = OOStats.new | |
| # stats.gather_statistics | |
| # | |
| # ... now stats.results gives you a hash with all the data gathered. | |
| # You can also use the set_option and set_columns methods to | |
| # customize the output you get from stats.display_results. | |
| # | |
| # One serious flaw should be noted: for historical reasons, the data | |
| # coming from the node hosts about gears excludes gears that don't | |
| # have a git repository, e.g. database gears. So you will often see | |
| # a discrepancy in counts from nodes vs Mongo. Also there's no separate | |
| # count for stopped gears vs. idled gears. These problems will be | |
| # remedied with changes to the facts gathered on node hosts. | |
| #-- | |
| # Copyright 2013 Red Hat, Inc. | |
| # | |
| # Licensed under the Apache License, Version 2.0 (the "License"); | |
| # you may not use this file except in compliance with the License. | |
| # You may obtain a copy of the License at | |
| # | |
| # http://www.apache.org/licenses/LICENSE-2.0 | |
| # | |
| # Unless required by applicable law or agreed to in writing, software | |
| # distributed under the License is distributed on an "AS IS" BASIS, | |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | |
| # See the License for the specific language governing permissions and | |
| # limitations under the License. | |
| #++ | |
| require 'rubygems' | |
| require 'time' | |
| class OOStats | |
| # called when OOStats.new is called | |
| def initialize(options = nil) | |
| @options = options || @options || { # default if none given | |
| :wait => 2, | |
| :format => :text, | |
| } | |
| @time = {} | |
| @time[:load_broker_environment] = time_msecs { load_broker_rails_env } | |
| set_initial_columns | |
| end | |
| # Available options are essentially the same as script options | |
| # e.g. set_option :format => :xml | |
| def set_option(options) | |
| @options.merge! options | |
| end | |
| # set the columns displayed for text/tsv reports | |
| def set_columns(columns_for_table_hash) | |
| # text_tableize or tsv_tableize use these. If the column list they get | |
| # is nil (because it wasn't specified) they auto-generate columns for you. | |
| # See set_initial_columns below for example usage. | |
| @columns_for ||= {} | |
| @columns_for.merge! columns_for_table_hash | |
| end | |
| # these are the column lists you get by default | |
| def set_initial_columns | |
| set_columns :profile_summary => | |
| %w{district_count node_count nodes_active nodes_inactive | |
| district_capacity dist_avail_capacity dist_avail_uids total_gears | |
| total_active_gears effective_available_gears lowest_active_capacity_pct | |
| highest_active_capacity_pct avg_active_capacity_pct | |
| total_gears_in_db_records total_apps | |
| }.map{|k| k.to_sym}, | |
| :district_table => | |
| %w{name node_count dist_avail_capacity | |
| total_active_gears effective_available_gears avg_active_capacity_pct | |
| }.map{|k| k.to_sym}, | |
| :district_summary => | |
| %w{profile node_count nodes_active nodes_inactive | |
| district_capacity dist_avail_capacity dist_avail_uids total_gears | |
| total_active_gears effective_available_gears lowest_active_capacity_pct | |
| highest_active_capacity_pct avg_active_capacity_pct | |
| }.map{|k| k.to_sym}, | |
| :node_table => | |
| %w{name total_gears max_gears capacity active_gears max_active_gears active_capacity | |
| }.map{|k| k.to_sym} | |
| end | |
| # use to time an operation in milliseconds | |
| def time_msecs | |
| start_time = (Time.now.to_f * 1000).to_i | |
| yield | |
| return (Time.now.to_f * 1000).to_i - start_time | |
| end | |
| # gather all statistics and analyze | |
| def gather_statistics | |
| # read method comments about the structures they return | |
| @time[:get_node_entries] = time_msecs { @entry_for_node = get_node_entries } | |
| @time[:get_district_entries] = time_msecs { @entry_for_district = get_district_entries } | |
| if @options[:db_stats] # don't gather this data unless requested with --db | |
| @time[:get_db_stats] = time_msecs do | |
| @count_all, @count_for_profile, @count_for_user = get_db_stats | |
| end | |
| end | |
| @time[:summarize_districts] = time_msecs do | |
| @summary_for_district = summarize_districts(@entry_for_district, @entry_for_node) | |
| end | |
| @time[:summarize_profiles] = time_msecs do | |
| @summary_for_profile = summarize_profiles(@summary_for_district, @count_for_profile) | |
| end | |
| @count_all ||= {} # db count may not have occurred | |
| @count_all[:nodes] = @entry_for_node.size | |
| @count_all[:districts] = @entry_for_district.size | |
| @count_all[:profiles] = @summary_for_profile.size | |
| return @time | |
| end | |
| # Bundle up the statistics results in a hash | |
| def results | |
| r = { | |
| :timings_msecs => @time, #timing hash | |
| :node_entries => @entry_for_node.values, #array of node hashes from mcollective | |
| :district_entries => @entry_for_district.values, #array of district hashes from DB | |
| :district_summaries => @summary_for_district.values, #array of district summary hashes | |
| :profile_summaries => @summary_for_profile.values, #array of profile summary hashes | |
| # remember, unless --db option is present, the db is not scanned for apps/gears/carts | |
| # in that case, only data from the nodes and districts are included | |
| :count_all => @count_all, #overall summary hash | |
| # if db counts were gathered, hash of app/gear/cart counts per profile | |
| :db_count_for_profile => @count_for_profile, | |
| # if db counts were gathered, array of users with app/gear counts | |
| :db_count_per_user => @count_for_user ? @count_for_user.values : nil, | |
| } | |
| end | |
| # Print results to stdout according to current options | |
| def display_results | |
| r = results | |
| case @options[:format] | |
| when :yaml | |
| puts r.to_yaml | |
| when :xml | |
| puts r.to_xml | |
| when :json | |
| puts r.to_json | |
| when :tsv | |
| display_results_tsv r | |
| else # :text | |
| display_results_text r | |
| end | |
| nil | |
| end | |
| # Load the broker rails environment so we can leverage its tools | |
| def load_broker_rails_env | |
| begin | |
| require "/var/www/openshift/broker/config/environment" | |
| # Disable analytics for admin scripts | |
| Rails.configuration.analytics[:enabled] = false | |
| # Wait this long for answers from node hosts | |
| Rails.configuration.msg_broker[:rpc_options][:disctimeout] = @options[:wait] | |
| rescue Exception => e | |
| puts <<-"FAIL" | |
| Broker application failed to load; aborting. | |
| The error was: #{e.message} | |
| FAIL | |
| exit 1 | |
| end | |
| end | |
| # get the node statistics by querying the facts on every node | |
| def get_node_entries | |
| entry_for_node = {} | |
| # Which comes out looking like: | |
| # { | |
| # "node1.example.com" => { | |
| # :id => "node1.example.com", # hostname | |
| # :name => "node1", # for short | |
| # :node_profile => "small", # node/gear profile | |
| # :district_uuid => "2dfca730b863428da9af176160138651", | |
| # # or "NONE" if not in a district | |
| # :district_active => true, # node marked as active in district? | |
| # :total_gears => 12, | |
| # :active_gears => 12, | |
| # :inactive_gears => 0, | |
| # # keep in mind below we are actually talking about gears not apps | |
| # :max_apps => 100, | |
| # :capacity => 12.0, # percentage of max_apps consumed | |
| # :max_active_apps => 100, | |
| # :active_capacity => 12.0, # percentage of max_active_apps consumed | |
| # # copies of the max_ settings apps=>gears | |
| # :max_gears => 100, | |
| # :max_active_gears => 100, | |
| # }, | |
| # "node2.example.com" => ... | |
| # } | |
| OpenShift::MCollectiveApplicationContainerProxy.rpc_exec('rpcutil') do |client| | |
| client.inventory do |response| | |
| facts = response[:body][:data][:facts] | |
| host = response[:senderid] | |
| node = {} | |
| # convert from strings to relevant values | |
| %w{node_profile district_uuid}.each {|fact| node[fact.to_sym] = facts[fact]} | |
| node[:district_active] = facts['district_active'] == 'true' ? true : false | |
| %w{max_apps max_active_apps}.each {|fact| node[fact.to_sym] = facts[fact].to_i} | |
| node[:max_gears] = node[:max_apps] | |
| node[:max_active_gears] = node[:max_active_apps] | |
| %w{capacity active_capacity}.each {|fact| node[fact.to_sym] = facts[fact].to_f} | |
| # make necessary calculations | |
| node[:active_gears] = (node[:max_active_apps] * node[:active_capacity] / 100).round | |
| node[:total_gears] = (node[:max_apps] * node[:capacity] / 100).round | |
| node[:inactive_gears] = node[:total_gears] - node[:active_gears] | |
| # record that hash for this node host | |
| node[:id] = host | |
| node[:name] = host.split('.')[0] | |
| entry_for_node[host] = node | |
| end | |
| end | |
| return entry_for_node | |
| end | |
| # get the district definitions from the DB | |
| def get_district_entries | |
| entry_for_district = {} | |
| # Which looks like: | |
| # { | |
| # "2dfca730b863428da9af176160138651" => { | |
| # :profile => "small", # gear profile (aka "size") | |
| # :name => "small_district", #user-friendly name | |
| # :uuid => "2dfca730b863428da9af176160138651", #unique ID | |
| # :nodes => { | |
| # "node1.example.com" => {"active"=>true}, | |
| # "node2.example.com" => {"active"=>true} | |
| # }, | |
| # :district_capacity => 6000, # configured number of gears allowed in this district | |
| # :dist_avail_capacity => 5967, # district_capacity minus gears already allocated | |
| # :dist_avail_uids => 5967, # number of user ids left in the pool | |
| # }, | |
| # "6e5d3ccc0bb1456399687c0be51676f8" => ... | |
| # } | |
| District.find_all.each do |dist| | |
| entry_for_district[dist.uuid] = { | |
| :profile => dist.node_profile, | |
| :name => dist.name, | |
| :uuid => dist.uuid, | |
| :nodes => district_nodes_clone(dist), | |
| :district_capacity => dist.max_capacity, | |
| :dist_avail_capacity => dist.available_capacity, | |
| :dist_avail_uids => dist.available_uids.length, | |
| } | |
| end | |
| return entry_for_district | |
| end | |
| # perform a manual clone such that we don't get BSON entries | |
| def district_nodes_clone(district) | |
| cloned = {} | |
| district.server_identities.each {|id,active| cloned[id] = {"active" => active["active"]}} | |
| return cloned | |
| end | |
| def summarize_districts(entry_for_district, entry_for_node) | |
| # Returned hash looks like: | |
| # { | |
| # "2dfca730b863428da9af176160138651" => { | |
| # :uuid => "2dfca730b863428da9af176160138651", # unique ID for district | |
| # :name => "small_district", # user-friendly name for district | |
| # :profile => "small", # gear profile ("size") for district | |
| # :node_count => 2, # number of nodes responding in the district | |
| # :nodes_active => 1, # number of nodes marked "active" in district | |
| # :nodes_inactive => 1, # number of nodes marked inactive (not open for gear placement) | |
| # # N.B. un-districted nodes are always considered inactive, though they can | |
| # # have gears placed if there are no districts with capacity for the profile. | |
| # | |
| # # the following are *district* capacity numbers: | |
| # :district_capacity => 4000, # configured number of gears allowed in this district | |
| # :dist_avail_capacity => 3967, # district_capacity minus gears already allocated | |
| # :dist_avail_uids => 5967, # number of user ids left in the district uid pool | |
| # # N.B. these are set to 0 for "NONE" districts (undistricted nodes) | |
| # | |
| # # the following are capacity numbers according to responding nodes: | |
| # :total_gears => 27, # gears created on nodes | |
| # :total_active_gears => 27, # gears which are active (not stopped/idled) | |
| # :available_active_gears => 173, # how many more active gears the nodes will support | |
| # # N.B. currently gear numbers are skewed lower than reality because the | |
| # # node only counts the ones that have git repos (e.g. DB gears are excluded) | |
| # :effective_available_gears => 173, # minimum of available_active_gears and dist_avail_capacity | |
| # | |
| # # min/max/average percent usage of active gear capacity on nodes in this district: | |
| # :lowest_active_capacity_pct => 12.0, | |
| # :highest_active_capacity_pct => 15.0, | |
| # :avg_active_capacity_pct => 13.5, | |
| # | |
| # :nodes=> [ array of entry_for_node values that are members of this district ] | |
| # | |
| # :missing_nodes => [ ids of node hosts for this district that did not respond ] | |
| # }, | |
| # "6e5d3ccc0bb1456399687c0be51676f8" => { ... }, | |
| # ... | |
| # } | |
| # these are initial values, will accumulate as we go | |
| starter_stats = Hash[ %w[ | |
| node_count nodes_active nodes_inactive available_active_gears effective_available_gears | |
| total_active_gears total_gears avg_active_capacity_pct | |
| ].collect {|key| [key.to_sym, 0]}] | |
| # may need a unique "NONE" district per profile for nodes that are not in a district | |
| none_district = Hash.new do |h,profile| | |
| h[profile] = { | |
| :name => "(NONE)", | |
| :uuid => "NONE profile=#{profile}", | |
| :profile => profile, | |
| :district_capacity => 0, | |
| :dist_avail_capacity => 0, | |
| :dist_avail_uids => 0, | |
| :nodes => [], | |
| :missing_nodes => {}, | |
| }.merge starter_stats | |
| end | |
| # hash to store the summaries per district | |
| summary_for_district = {} | |
| entry_for_district.each do |uuid,dist| | |
| summary_for_district[uuid] = dist.merge(starter_stats). | |
| merge(:missing_nodes => dist[:nodes].clone, :nodes => []) | |
| end | |
| # We will drive everything according to the nodes that responded. | |
| # There may be some that didn't respond, which won't be included. | |
| entry_for_node.each do |id,node| | |
| sum = summary_for_district[node[:district_uuid]] || | |
| none_district[node[:node_profile]] | |
| sum[:nodes] << node | |
| sum[:missing_nodes].delete id # responded, so not missing | |
| sum[:node_count] += 1 | |
| sum[:nodes_active] += 1 if node[:district_active] | |
| sum[:nodes_inactive] += 1 if !node[:district_active] | |
| sum[:total_active_gears] += node[:active_gears] | |
| sum[:total_gears] += node[:total_gears] | |
| sum[:avg_active_capacity_pct] += node[:active_capacity] | |
| sum[:available_active_gears] += node[:max_active_apps] - node[:active_gears] | |
| end | |
| none_district.values.each {|sum| summary_for_district[sum[:uuid]] = sum} | |
| summary_for_district.each do |uuid,sum| | |
| sum[:avg_active_capacity_pct] /= sum[:node_count] if sum[:node_count] > 0 | |
| sum[:lowest_active_capacity_pct] = sum[:nodes].map{|node| node[:active_capacity]}.min || 0.0 | |
| sum[:highest_active_capacity_pct] = sum[:nodes].map{|node| node[:active_capacity]}.max || 0.0 | |
| sum[:effective_available_gears] = [sum[:available_active_gears], sum[:dist_avail_capacity]].min | |
| # convert :missing nodes to array | |
| sum[:missing_nodes] = sum[:missing_nodes].keys | |
| end | |
| return summary_for_district | |
| end | |
| def summarize_profiles(summary_for_district, count_for_profile) | |
| # Returned hash looks like: (a lot like the district summaries) | |
| # { | |
| # "small" => { | |
| # :profile => "small", | |
| # :districts => [ array of summaries from summary_of_district that have this profile ], | |
| # :district_count => 1, # number of districts with this profile (may include 1 "NONE" district) | |
| # :node_count => 2, # number of nodes responding with this profile | |
| # :nodes_active => 1, # number of nodes marked "active" with this profile | |
| # :nodes_inactive => 1, # number of nodes marked inactive (not open for gear placement) | |
| # # N.B. un-districted nodes are always considered inactive, though they can | |
| # # have gears placed if there are no districts with capacity for the profile. | |
| # :missing_nodes => [ ids of districted node hosts for this profile that did not respond ] | |
| # | |
| # # the following are summarized *district* capacity numbers: | |
| # :district_capacity => 4000, # configured number of gears allowed in districts | |
| # :dist_avail_capacity => 3967, # district_capacity minus gears already allocated | |
| # :dist_avail_uids => 5967, # number of user ids left in the districts' uid pool | |
| # # N.B. these will be 0 for "NONE" districts (undistricted nodes) | |
| # | |
| # # the following are usage/capacity numbers according to responding nodes: | |
| # :total_gears => 27, # gears created on nodes | |
| # :total_active_gears => 27, # gears which are active (not stopped/idled) | |
| # :available_active_gears => 173, # how many more active gears the nodes will support | |
| # # N.B. currently gear numbers are skewed lower than reality because the | |
| # # node only counts the ones that have git repos (e.g. DB gears are excluded) | |
| # :effective_available_gears => 173, # minimum of available_active_gears and dist_avail_capacity | |
| # | |
| # # the following are usage numbers according to the DB, if collected | |
| # :total_db_gears => 27, # gears recorded with this profile in the DB | |
| # :total_apps => 20, # apps recorded with this profile in the DB | |
| # :cartridges => { cartridge counts as in get_db_stats } | |
| # :cartridges_short => { cartridge short name counts as in get_db_stats } | |
| # | |
| # # min/max/average percent usage of active gear capacity on nodes in this profile: | |
| # :lowest_active_capacity_pct => 12.0, | |
| # :highest_active_capacity_pct => 15.0, | |
| # :avg_active_capacity_pct => 13.5, | |
| # }, | |
| # "medium" => {...}, | |
| # ... | |
| # } | |
| # these values will accumulate as we go | |
| starter_stats = Hash[ %w[ | |
| node_count district_count nodes_active nodes_inactive available_active_gears | |
| effective_available_gears total_active_gears total_gears avg_active_capacity_pct | |
| district_capacity dist_avail_capacity dist_avail_uids | |
| ].collect {|key| [key.to_sym, 0]}] | |
| summary_for_profile = Hash.new do |sum,p| | |
| sum[p] = { # auto-created hash per profile | |
| :profile => p, | |
| :districts => [], | |
| :missing_nodes => [], | |
| }.merge starter_stats | |
| end | |
| summary_for_district.each do |uuid,dist| | |
| sum = summary_for_profile[dist[:profile]] | |
| sum[:districts] << dist | |
| sum[:district_count] += 1 | |
| [ :node_count, :nodes_active, :nodes_inactive, :missing_nodes, | |
| :available_active_gears, :total_active_gears, :total_gears, :district_capacity, | |
| :dist_avail_capacity, :dist_avail_uids | |
| ].each {|k| sum[k] += dist[k]} | |
| sum[:avg_active_capacity_pct] += dist[:avg_active_capacity_pct] * dist[:node_count] | |
| end | |
| summary_for_profile.each do |profile,sum| | |
| sum[:avg_active_capacity_pct] /= sum[:node_count] if sum[:node_count] > 0 | |
| sum[:lowest_active_capacity_pct] = sum[:districts].map{|d| d[:lowest_active_capacity_pct]}.min || 0.0 | |
| sum[:highest_active_capacity_pct] = sum[:districts].map{|d| d[:highest_active_capacity_pct]}.max || 0.0 | |
| sum[:effective_available_gears] = [sum[:available_active_gears], sum[:dist_avail_capacity]].min | |
| if count_for_profile | |
| sum[:total_gears_in_db_records] = count_for_profile[profile][:gears] | |
| sum[:total_apps] = count_for_profile[profile][:apps] | |
| sum[:cartridges] = count_for_profile[profile][:cartridges] | |
| sum[:cartridges_short] = count_for_profile[profile][:cartridges_short] | |
| end | |
| end | |
| return summary_for_profile | |
| end | |
| # get statistics from the DB about users/apps/gears/cartridges | |
| def get_db_stats | |
| # initialize the things we will count for the entire installation | |
| count_all = { | |
| :apps => 0, | |
| :gears => 0, | |
| :cartridges => Hash.new {|h,k| h[k] = 0}, | |
| :cartridges_short => Hash.new {|h,k| h[k] = 0}, | |
| :users_with_num_apps => Hash.new {|h,k| h[k] = 0}, | |
| :users_with_num_gears => Hash.new {|h,k| h[k] = 0}, | |
| } | |
| # which ends up looking like: | |
| # { | |
| # :apps => 21, | |
| # :gears => 39, | |
| # :cartridges=> { | |
| # "ruby-1.9" => 5, | |
| # "ruby-1.8" => 7, | |
| # "perl-5.10" => 7, | |
| # "mysql-5.1" => 15, | |
| # "haproxy-1.4" => 12, | |
| # ... | |
| # }, | |
| # :cartridges_short=> { | |
| # "ruby" => 12, | |
| # "perl" => 7, | |
| # "mysql" => 15, | |
| # "haproxy" => 12, | |
| # ... | |
| # }, | |
| # :users_with_num_apps => { | |
| # 1 => 10, # 10 users have 1 app | |
| # 2 => 2, | |
| # 9 => 1, | |
| # ... | |
| # }, | |
| # :users_with_num_gears => { | |
| # 1 => 9, # 9 users have 1 gear | |
| # 2 => 2, | |
| # 4 => 1, | |
| # 11 => 1, | |
| # ... | |
| # }, | |
| # }, | |
| count_for_profile = Hash.new do |hash,profile| | |
| hash[profile] = { | |
| :apps => 0, | |
| :gears => 0, | |
| :cartridges => Hash.new {|h,k| h[k] = 0}, | |
| :cartridges_short => Hash.new {|h,k| h[k] = 0}, | |
| } | |
| end | |
| # counts broken out by profile, which ends up looking like: | |
| # { | |
| # "small" => { hash like above without :users_with_num_* }, | |
| # "medium" => { ... }, | |
| # ... | |
| # } | |
| count_for_user = Hash.new do |hash,user| | |
| hash[user] = Hash.new {|h,k| h[k] = 0} | |
| end | |
| # which ends up looking like: | |
| # { | |
| # "user1" => {:login => "user1", :small_gears =>33, :small_apps =>18}, | |
| # "user2" => {:login => "user2", :medium_apps =>1, :medium_gears =>2, :small_gears =>4, :small_apps =>2} | |
| # ... | |
| # } | |
| # | |
| # this is for the pre-model-refactor DB - will need adjustment after that's done | |
| query = {"apps.group_instances.gears.0" => {"$exists" => true}} | |
| options = {:fields => %w[ | |
| login | |
| apps.node_profile | |
| apps.group_instances.gears.node_profile | |
| apps.group_instances.gears.configured_components | |
| ], | |
| :timeout => false} | |
| OpenShift::DataStore.instance.user_collection.find(query, options) do |mcursor| | |
| mcursor.each do |user| | |
| count_of = count_for_user[user['login']] | |
| count_of[:login] = user['login'] | |
| user['apps'].each do |app| | |
| app_profile = app['node_profile'] | |
| count_of["#{app_profile}_apps".to_sym] += 1 | |
| count_all[:apps] += 1 | |
| count_for_profile[app_profile][:apps] += 1 | |
| count_of[:apps] += 1 | |
| app['group_instances'].each do |group| # group of same-configured gears | |
| group['gears'].each do |gear| | |
| gear_profile = gear['node_profile'] # for now, same as app profile, but... | |
| count_of["#{gear_profile}_gears".to_sym] += 1 | |
| count_of[:gears] += 1 | |
| count_for_profile[gear_profile][:gears] += 1 | |
| count_all[:gears] += 1 | |
| gear['configured_components'].each do |cart_desc| | |
| # e.g. extract from "@@app/cart-mysql-5.1/comp-mysql-server" => "mysql-5.1" | |
| cart = cart_desc.gsub %r{ ^.* \b cart-([^/]+) (/|$) .* $ }x, '\1' | |
| # Now pull the shorter name out without the version number | |
| # e.g. extract from "@@app/cart- mysql - 5.1 /comp-mysql-server" => "mysql" | |
| short = cart_desc.gsub %r{ ^.* \b cart-([-\w]+)-[\d.]+ (/|$) .* $ }x, '\1' | |
| # If either regex doesn't match then we are left with the whole description to ponder. | |
| count_for_profile[gear_profile][:cartridges][cart] += 1 | |
| count_for_profile[gear_profile][:cartridges_short][short] += 1 | |
| count_all[:cartridges][cart] += 1 | |
| count_all[:cartridges_short][short] += 1 | |
| end | |
| end | |
| end | |
| end | |
| count_all[:users_with_num_apps][ count_of[:apps] ] += 1 | |
| count_all[:users_with_num_gears][count_of[:gears]] += 1 | |
| end | |
| end | |
| return count_all, count_for_profile, count_for_user | |
| end | |
| def outline(str) | |
| puts '-' * str.length | |
| puts str | |
| puts '-' * str.length | |
| end | |
| def display_results_text(res=results) | |
| # first, display the per-user usage if available | |
| if @options[:db_stats] | |
| if users = res[:db_count_per_user] | |
| outline "Usage of apps and gears by user:" | |
| # columns are the keys (:small_apps :small_gears etc) with :login at front | |
| text_tableize users, @columns_for[:user_table] | |
| puts "\n\n" | |
| end | |
| if users_for_count = res[:count_all][:users_with_num_apps] | |
| outline "Distribution of apps usage (app count : users) :" | |
| text_legendize users_for_count | |
| puts "\n\n" | |
| end | |
| if users_for_count = res[:count_all][:users_with_num_gears] | |
| outline "Distribution of gears usage (gear count : users) :" | |
| text_legendize users_for_count | |
| puts "\n\n" | |
| end | |
| end | |
| unless @options[:only] | |
| if cartridges = res[:count_all][:cartridges] | |
| # display system-wide cartridge usage | |
| outline "System-wide usage of cartridges:" | |
| cartridges.sort_by {|cart,count| cart}.each { |duple| puts " #{duple[0]} : #{duple[1]}" } | |
| puts "\n\n" | |
| end | |
| # display systems info | |
| case @options[:level] | |
| when :profile | |
| res[:profile_summaries].sort_by {|a| a[:profile]}.each do |profile| | |
| # print summary for that profile | |
| outline "Profile '#{profile[:profile]}' summary:" | |
| text_legendize profile, @columns_for[:profile_summary] | |
| puts "\nDistricts:" | |
| text_tableize profile[:districts].sort_by {|a| a[:name]}, @columns_for[:district_table] | |
| unless profile[:missing_nodes].empty? | |
| puts "\nWARNING: the following districted node(s) in this profile DID NOT respond:" | |
| puts profile[:missing_nodes].join ", " | |
| end | |
| puts "\n\n" | |
| end | |
| when :district | |
| res[:district_summaries].sort_by {|a| a[:profile] + a[:name] }.each do |district| | |
| # print summary for that district | |
| outline "District '#{district[:name]}' summary:" | |
| text_legendize district, @columns_for[:district_summary] | |
| puts "\nNodes:" | |
| text_tableize district[:nodes].sort_by {|a| a[:name]}, @columns_for[:node_table] | |
| unless district[:missing_nodes].empty? | |
| puts "\nWARNING: the following node(s) in this district DID NOT respond:" | |
| puts district[:missing_nodes].join ", " | |
| end | |
| puts "\n\n" | |
| end | |
| else # :node | |
| missing_nodes = [] | |
| res[:district_summaries].sort_by {|a| a[:profile] + a[:name] }.each do |district| | |
| missing_nodes += district[:missing_nodes] | |
| puts "Nodes for district '#{district[:name]}' with profile '#{district[:profile]}':" | |
| text_tableize district[:nodes].sort_by {|a| a[:name]}, @columns_for[:node_table] | |
| puts "\n\n" | |
| end | |
| unless missing_nodes.empty? | |
| puts "\nWARNING: the following districted node(s) DID NOT respond:" | |
| puts missing_nodes.join ", " | |
| puts | |
| end | |
| end | |
| outline "Summary for all systems:" | |
| text_legendize res[:count_all], @columns_for[:count_all_legend] | |
| puts "\n\n" | |
| end | |
| end | |
| # display tab-separated values (spreadsheet friendly) | |
| def display_results_tsv(res=results) | |
| # Right now no column lists have been defined, so all are generated on the fly. | |
| # Use "set_columns" to create column orderings for these if desired | |
| puts "Resource usage summary:" | |
| tsv_legendize res[:count_all], @columns_for[:tsv_db_count_all] | |
| missing_nodes = res[:profile_summaries].inject([]) {|a,p| a += p[:missing_nodes]} | |
| unless missing_nodes.empty? | |
| puts "\n\nWARNING: these districted nodes DID NOT respond:" | |
| missing_nodes.each {|n| puts n} | |
| end | |
| puts "\n\nPer-gear-profile usage summary:" | |
| tsv_tableize res[:profile_summaries].sort_by {|h| h[:profile]}, | |
| @columns_for[:tsv_profile_summaries] | |
| puts "\n\nPer-district usage summary:" | |
| tsv_tableize res[:district_summaries].sort_by {|h| "#{h[:profile]}-#{h[:name]}"}, | |
| @columns_for[:tsv_district_summaries] | |
| puts "\n\nPer-node usage summary:" | |
| tsv_tableize res[:node_entries].sort_by {|h| "#{h[:node_profile]}-#{h[:name]}"}, | |
| @columns_for[:tsv_node_entries] | |
| if res[:db_count_per_user] # --db option specified | |
| puts "\n\nPer-user usage summary:" | |
| tsv_tableize res[:db_count_per_user], @columns_for[:tsv_db_count_for_user] | |
| end | |
| if users_for_count = res[:count_all][:users_with_num_apps] | |
| puts "\n\nDistribution of apps usage (app count : users) :" | |
| tsv_legendize users_for_count | |
| end | |
| if users_for_count = res[:count_all][:users_with_num_gears] | |
| puts "\n\nDistribution of gears usage (gear count : users) :" | |
| tsv_legendize users_for_count | |
| end | |
| if res[:count_all][:cartridges] # --db option specified | |
| puts "\n\nCartridge usage summary:" | |
| tsv_legendize res[:count_all][:cartridges], @columns_for[:tsv_cartridge_legend] | |
| # also break them down by profile | |
| res[:profile_summaries].sort_by {|h| h[:profile]}.each do |profile| | |
| puts "\n\nCartridge usage summary for profile '#{profile[:profile]}':" | |
| tsv_legendize profile[:cartridges], @columns_for[:tsv_cartridge_legend] | |
| end | |
| end | |
| puts "\n\nTimings for gathering this information (milliseconds):" | |
| tsv_legendize res[:timings_msecs], @columns_for[:tsv_timings] | |
| end | |
| def auto_column_list(hashes) | |
| # automatically build the column list using those keys which have "simple" values | |
| cols = {} | |
| hashes.each do |hash| | |
| hash.each {|key,value| cols[key] = true if [String, Symbol, Integer, Fixnum].include? value.class} | |
| end | |
| cols = cols.keys.sort # if you wanted a specific order you wouldn't be here | |
| # OK let's make an exception to bring a few identifier columns to the front | |
| [:name, :profile, :node_profile, :login].each {|col| cols.include?(col) and cols = [col] + (cols - [col])} | |
| return cols | |
| end | |
| def tsv_legendize(hash, order=nil) | |
| order ||= auto_column_list([hash]) | |
| order.each {|key| puts "#{key.to_s.humanize}\t#{hash[key]}" } | |
| end | |
| def tsv_tableize(hashes, cols=nil) | |
| cols ||= auto_column_list(hashes) | |
| # print columns | |
| puts cols.map {|col| col.to_s.humanize}.join "\t" | |
| # print all records | |
| hashes.each {|hash| puts cols.map {|col| hash[col]}.join "\t"} | |
| end | |
| # display data in formatted key:value "legend" | |
| def text_legendize(hash, order=nil) | |
| order ||= auto_column_list([hash]) | |
| humanized = {} | |
| max_size = [] | |
| order.each {|key| max_size << (humanized[key] = key.to_s.humanize).length } | |
| max_size = max_size.max.to_s | |
| order.each {|key| printf " %#{max_size}s : %s\n", humanized[key], hash[key].to_s unless hash[key].nil? } | |
| end | |
| # display data in a nicely formatted table | |
| def text_tableize(rows, cols=nil) | |
| cols ||= auto_column_list(rows) | |
| show_for_col = {} | |
| # figure out how to display each column | |
| cols.each do |col| | |
| name = col.to_s.camelize. | |
| gsub(/Total/, '#'). | |
| gsub(/Available/, 'Avail') | |
| size = (rows.map {|row| row[col].to_s.length} + [name.length]).sort[-1] | |
| show_for_col[col] = {:name => name, :size => size} | |
| end | |
| # print table header and divider | |
| puts cols.map {|col| c=show_for_col[col]; sprintf "%#{c[:size]}s", c[:name] }.join ' ' | |
| puts cols.map {|col| '-' * show_for_col[col][:size] }.join ' ' | |
| # print table contents | |
| rows.each do |row| | |
| puts cols.map {|col| sprintf "%#{show_for_col[col][:size]}s", row[col].to_s}.join ' ' | |
| end | |
| end | |
| end #class OOStats | |
| ############ EXECUTION ########## | |
| # | |
| # If this script is running directly, gather the information and display. | |
| # In a different context (e.g. irb) just load the class and don't run anything. | |
| if __FILE__ == $0 | |
| # | |
| # Options parsing... | |
| # | |
| require 'optparse' | |
| options = { | |
| :wait => 2, | |
| :level => :profile, | |
| :format => :text, | |
| } | |
| optparse = OptionParser.new { |opts| | |
| opts.banner = <<-"USAGE" | |
| #{$0}: Output usage statistics from this installation. | |
| Usage: #{$0} [switches] | |
| Example: #{$0} # standard text display | |
| Example: #{$0} --wait 10 # for slower node responses | |
| Example: #{$0} --format xml | |
| Switches: | |
| USAGE | |
| opts.on('-w','--wait SECONDS', Integer, <<WAIT) { |wait| options[:wait] = wait } | |
| Seconds for broker to wait for node responses (integer, default 2). | |
| \tIf nodes are not responding in time, increase this as needed. | |
| WAIT | |
| opts.on('-d','--db', <<USER) { |x| options[:db_stats] = x } | |
| Gather and include MongoDB usage stats. | |
| \tThis scans all users, apps, and gears in the MongoDB. With thousands | |
| \tof users and applications, this may take seconds or minutes and | |
| \tput noticeable load on MongoDB. | |
| USER | |
| opts.on('-f','--format FORMAT', [:json, :xml, :yaml, :tsv, :text], | |
| 'Choose output format (json, tsv, xml, yaml, default: text)') { |x| options[:format] = x } | |
| opts.on('-l','--level LEVEL', [:profile, :district, :node], <<LEVEL) { |l| options[:level] = l } | |
| For text format, print statistical summary at this level: | |
| \tprofile: profile and district summary statistics (default) | |
| \tdistrict: district summary and node statistics | |
| \tnode: node statistics only | |
| LEVEL | |
| opts.on('-u','--only-user', <<FORMAT) { |x| options[:db_stats] = options[:only] = x } | |
| With text format, show ONLY per-user usage summary (implies --db) | |
| FORMAT | |
| opts.on('-h','--help', 'Print usage') { puts opts; exit 0 } | |
| } | |
| begin | |
| optparse.parse! | |
| rescue OptionParser::InvalidArgument => e | |
| puts e.message | |
| puts "\n" + optparse.to_s | |
| puts "\n" + e.message | |
| exit 1 | |
| end | |
| # | |
| # execution | |
| # | |
| o = OOStats.new(options) | |
| o.gather_statistics | |
| o.display_results | |
| end |