I'm very new to Ruby and I'm having some difficulties with a seemingly simple problem.
Code is here...
https://github.com/sensu/sensu-community-plugins/blob/master/plugins/graphite/check-stats.rb
...but I've included a full copy of the current source at the end, because it may change as new versions are submitted to Github.
It's a Sensu plugin. It collects data from Graphite via an HTTP request. Stores the reply in body, which is then JSON.parse() into data.
For each metric in data, it collects datapoints, and performs an average on the datapoints. If average is higher than certain thresholds (options -w or -c), it throws a warning or a critical.
Sometimes the Graphite store is a bit behind times. The most recent data point may be missing from some metrics. When that happens, the data point is nil.
The problem is, nil is counted as zero when computing average(datapoints). This artificially lowers the average, sometimes to the effect that the plugin doesn't trigger when it should.
What's the best way to eliminate the nil values from the calculation of average?
Ideally, the elimination of the nils should happen in such a way that, if all data points are nil, then it should trigger the datapoints.empty condition. Basically, kill all the nils before they reach "unless datapoints.empty?" because if all are nil then we don't actually have any data points.
Or somehow metric.collect{} should skip the nil values.
I've tried to use .compact but that didn't seem to make a difference (probably I've used it wrong).
This is the current version of the code:
#!/usr/bin/env ruby
#
# Checks metrics in graphite, averaged over a period of time.
#
# The fired sensu event will only be critical if a stat is
# above the critical threshold. Otherwise, the event will be warning,
# if a stat is above the warning threshold.
#
# Multiple stats will be checked if * are used
# in the "target" query.
#
# Author: Alan Smith (alan#asmith.me)
# Date: 08/28/2014
#
require 'rubygems' if RUBY_VERSION < '1.9.0'
require 'json'
require 'net/http'
require 'sensu-plugin/check/cli'
class CheckGraphiteStat < Sensu::Plugin::Check::CLI
option :host,
:short => "-h HOST",
:long => "--host HOST",
:description => "graphite hostname",
:proc => proc {|p| p.to_s },
:default => "graphite"
option :period,
:short => "-p PERIOD",
:long => "--period PERIOD",
:description => "The period back in time to extract from Graphite. Use -24hours, -2days, -15mins, etc, same format as in Graphite",
:proc => proc {|p| p.to_s },
:required => true
option :target,
:short => "-t TARGET",
:long => "--target TARGET",
:description => "The graphite metric name. Can include * to query multiple metrics",
:proc => proc {|p| p.to_s },
:required => true
option :warn,
:short => "-w WARN",
:long => "--warn WARN",
:description => "Warning level",
:proc => proc {|p| p.to_f },
:required => false
option :crit,
:short => "-c Crit",
:long => "--crit CRIT",
:description => "Critical level",
:proc => proc {|p| p.to_f },
:required => false
def average(a)
total = 0
a.to_a.each {|i| total += i.to_f}
total / a.length
end
def danger(metric)
datapoints = metric['datapoints'].collect {|p| p[0].to_f}
unless datapoints.empty?
avg = average(datapoints)
if !config[:crit].nil? && avg > config[:crit]
return [2, "#{metric['target']} is #{avg}"]
elsif !config[:warn].nil? && avg > config[:warn]
return [1, "#{metric['target']} is #{avg}"]
end
end
[0, nil]
end
def run
body =
begin
uri = URI("http://#{config[:host]}/render?format=json&target=#{config[:target]}&from=#{config[:period]}")
res = Net::HTTP.get_response(uri)
res.body
rescue Exception => e
warning "Failed to query graphite: #{e.inspect}"
end
status = 0
message = ''
data =
begin
JSON.parse(body)
rescue
[]
end
unknown "No data from graphite" if data.empty?
data.each do |metric|
s, msg = danger(metric)
message += "#{msg} " unless s == 0
status = s unless s < status
end
if status == 2
critical message
elsif status == 1
warning message
end
ok
end
end
Well, if you want to eliminate nils before doing collect, you can do
metric['datapoints'].reject { |p| p.nil? }.collect {|p| p[0].to_f}
instead of
metric['datapoints'].collect {|p| p[0].to_f}
BTW, you average can also be rewritten as
def average(a)
a.reduce(0,:+)/a.size
end
You can use Array#compact which does exactly that:
["a", nil, "b", nil, "c", nil].compact
#=> [ "a", "b", "c" ]
http://ruby-doc.org/core-2.1.3/Array.html#method-i-compact
Related
I am trying to figure out how to add a couple of additional search parameters to the request using the adwords api ruby gem. Here's the example I'm trying to work with. Unfortunately the documentation lacks any instruction on how to add the additional params and Google's API docs are cryptic.
Any help would be greatly appreciated!
#!/usr/bin/env ruby
# Encoding: utf-8
#
# Author:: api.dklimkin#gmail.com (Danial Klimkin)
#
# Copyright:: Copyright 2011, Google Inc. All Rights Reserved.
#
# License:: 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.
#
# This example retrieves keywords that are related to a given keyword.
#
# Tags: TargetingIdeaService.get
require 'adwords_api'
require 'json'
def get_keyword_ideas(_keywords)
# AdwordsApi::Api will read a config file from ENV['HOME']/adwords_api.yml
# when called without parameters.
adwords = AdwordsApi::Api.new
# To enable logging of SOAP requests, set the log_level value to 'DEBUG' in
# the configuration file or provide your own logger:
# adwords.logger = Logger.new('adwords_xml.log')
targeting_idea_srv = adwords.service(:TargetingIdeaService, API_VERSION)
# Construct selector object.
selector = {
:idea_type => 'KEYWORD',
:request_type => 'STATS', # 'IDEAS',
:requested_attribute_types =>
['KEYWORD_TEXT', 'SEARCH_VOLUME', 'TARGETED_MONTHLY_SEARCHES', 'COMPETITION', 'AVERAGE_CPC'],
:search_parameters => [
{
# The 'xsi_type' field allows you to specify the xsi:type of the object
# being created. It's only necessary when you must provide an explicit
# type that the client library can't infer.
:xsi_type => 'RelatedToQuerySearchParameter',
:queries => _keywords
},
{
# Language setting (optional).
# The ID can be found in the documentation:
# https://developers.google.com/adwords/api/docs/appendix/languagecodes
# Only one LanguageSearchParameter is allowed per request.
:xsi_type => 'LanguageSearchParameter',
:languages => [{:id => 1000}]
},
{
:xsi_type => 'LocationSearchParameter',
:locations => [{:id => 21177}] # Utah
}
],
:paging => {
:start_index => 0,
:number_results => PAGE_SIZE
}
}
# Define initial values.
offset = 0
results = []
begin
# Perform request.
page = targeting_idea_srv.get(selector)
results += page[:entries] if page and page[:entries]
# Prepare next page request.
offset += PAGE_SIZE
selector[:paging][:start_index] = offset
end while offset < page[:total_num_entries]
puts results.to_json
# Display results.
results.each do |result|
data = result[:data]
keyword = data['KEYWORD_TEXT'][:value]
puts "----------------\nFound keyword with text '%s'" % keyword
targeted_monthly_searches = data['TARGETED_MONTHLY_SEARCHES'][:value]
if targeted_monthly_searches
puts "\tWith targeted monthly searches: #{targeted_monthly_searches.to_json}"
end
average_monthly_searches = data['SEARCH_VOLUME'][:value]
if average_monthly_searches
puts "\tand average monthly search volume: %d" % average_monthly_searches
end
end
# puts "Total keywords related to '%s': %d." % [keyword_text, results.length]
end
if __FILE__ == $0
API_VERSION = :v201502
PAGE_SIZE = 100
begin
keywords = [
'car insurance logan utah',
'logan utah insurance',
'logan insurance',
'insurance logan utah',
'insurance logan',
'logan ut insurance',
'north logan ut insurance',
'car insurance logan',
'car insurance logan ut',
'insurance logan ut',
'logan car insurance',
'nibley insurance'
]
get_keyword_ideas(keywords)
# Authorization error.
rescue AdsCommon::Errors::OAuth2VerificationRequired => e
puts "Authorization credentials are not valid. Edit adwords_api.yml for " +
"OAuth2 client ID and secret and run misc/setup_oauth2.rb example " +
"to retrieve and store OAuth2 tokens."
puts "See this wiki page for more details:\n\n " +
'http://code.google.com/p/google-api-ads-ruby/wiki/OAuth2'
# HTTP errors.
rescue AdsCommon::Errors::HttpError => e
puts "HTTP Error: %s" % e
# API errors.
rescue AdwordsApi::Errors::ApiException => e
puts "Message: %s" % e.message
puts 'Errors:'
e.errors.each_with_index do |error, index|
puts "\tError [%d]:" % (index + 1)
error.each do |field, value|
puts "\t\t%s: %s" % [field, value]
end
end
end
end
I found the answer. Here's the corrected selector hash:
selector = {
:idea_type => 'KEYWORD',
:request_type => 'STATS', # 'IDEAS',
:requested_attribute_types =>
['KEYWORD_TEXT', 'SEARCH_VOLUME', 'TARGETED_MONTHLY_SEARCHES', 'COMPETITION', 'AVERAGE_CPC'],
:search_parameters => [
{
# The 'xsi_type' field allows you to specify the xsi:type of the object
# being created. It's only necessary when you must provide an explicit
# type that the client library can't infer.
:xsi_type => 'RelatedToQuerySearchParameter',
:queries => _keywords
},
{
# Language setting (optional).
# The ID can be found in the documentation:
# https://developers.google.com/adwords/api/docs/appendix/languagecodes
# Only one LanguageSearchParameter is allowed per request.
:xsi_type => 'LanguageSearchParameter',
:languages => [{:id => 1000}]
},
{
:xsi_type => 'LocationSearchParameter',
:locations => [{:id => 21177}] # Utah
},
{
:xsi_type => 'NetworkSearchParameter',
:network_setting => {
:target_google_search => true,
:target_search_network => false,
:target_content_network => false
}
},
{
:xsi_type => 'ExcludedKeywordSearchParameter',
:keywords => [
{text: 'uk', match_type: 'BROAD'},
{text: 'canada', match_type: 'BROAD'}
]
},
],
:paging => {
:start_index => 0,
:number_results => PAGE_SIZE
}
}
Suppose I have a file in the following format.
date|time|account
2010-01-01|07:00:00|A1
2010-01-01|07:00:01|A2
....
Suppose I have the following function.
def ReadLongFile(longFile)
CSV.foreach(longFile, :headers => true, :col_sep => '|') do |row|
p row.to_hash
end
end
I like this function because it allows me to store each line as a hash where the header entries are the keys, and the line entries are the corresponding values. However, what is the most efficient way to modify it such that I can verify the header contains the correct entries? I was considering two options. First, I could open it another function and check the first line. Second, I could check within the function, but it would perform the check each iteration.
I would suggest using the CSV::header_row function to perform the check, and raising an error if it's not what you expect. Something like:
def ReadLongFile(longFile)
CSV.foreach(longFile, :headers => true, :return_headers => true, :col_sep => '|') do |row|
if row.header_row? then
raise ArgumentError, "Bad headers" unless header_sane?(row)
end
# Otherwise do the processing
end
end
Your implemenation of header_sane? will perform the validation that you need to ensure the file is what you expect it to be. Your calling code can rescue the ArgumentError if it can recover from it, or just let it fail :-)
Note: Updated to reflect error noted in the comments below. Be sure to set the :return_headers option when calling CSV::foreach.
If you are worried about the minimal overhead of calling header_row? for each of the row entries, you can construct a CSV instance and use shift to manually check the first row before continuing. For instance:
def ReadLongFile(longFile)
File.open(longFile) do |file|
reader = CSV.new(file, {:col_sep => '|', :headers => true, :return_headers => true})
header_row = reader.shift
raise ArgumentError, "Bad file headers" unless header_sane?(header_row)
reader.each do |row|
p row
end
end
end
Implemented as above, the following behavior holds true:
[4] pry(main)> def header_sane? row
[4] pry(main)* true
[4] pry(main)* end
=> nil
[5] pry(main)> ReadLongFile("file.csv")
#<CSV::Row "date":"2010-01-01" "time":"07:00:00" "account":"A1">
#<CSV::Row "date":"2010-01-01" "time":"07:00:01" "account":"A2">
=> nil
[6] pry(main)> def header_sane? row
[6] pry(main)* false
[6] pry(main)* end
=> nil
[7] pry(main)> ReadLongFile("file.csv")
ArgumentError: Bad file headers
from (pry):7:in `block in ReadLongFile'
I am writing a small ruby app using sinatra and have a text input for input that I then convert to a flat using the .to_f method. However if the input is empty the .to_f still converts the empty string to a 0 value.
I would like it to be checked so if the input is blank/empty it does not attempt to convert it to a number.
Below is the code I have so far, I have tried adding .empty? to the end but it throws a method error.
weight = Weight.create(
:amount => params[:amount].to_f,
:user_id => current_user.id,
:created_at => Time.now
)
You have two basic options. The first is to use the ternary operator, and give a default value when the string is empty. The basic template is:
(params[:amount].empty?) ? <EMPTY EXPRESSION> : <NOT EMPTY EXPRESSION>
For example, to return nil when params[:amount] is empty:
weight = Weight.create(
:amount => (params[:amount].empty?) ? nil : params[:amount].to_f,
:user_id => current_user.id,
:created_at => Time.now
)
The second is to use Ruby's logical operators. The basic template is:
params[:amount].empty? && <EMPTY EXPRESSION> || <NOT EMPTY EXPRESSION>
For example, to raise an exception when params[:amount] is empty:
weight = Weight.create(
:amount => params[:amount].empty? && \
(raise ArgumentError.new('Bad :amount')) || params[:amount].to_f
:user_id => current_user.id,
:created_at => Time.now
)
Both ways can return nil or raise the exception. The choice is largely stylistic.
This is a more Java/EE way of doing things than is stricly necessary, but I find that parameter validation is such a common thing that it helps to define the functionality in one place and then just reuse it.
class ParamsExtractor
def get_float_parameter(params,key)
params[key] && !(params[key].nil? || params[key].to_s.strip == '') ? params[key].to_f : 0.0
end
end
weight = Weight.create(
:amount => ParamsExtractor.get_float_parameter(params, :amount),
:user_id => current_user.id,
:created_at => Time.now
)
There are additional things you can do (modules etc) but this is clear and easilly testable via RSpec
x = '' => ""
x.to_f unless x.empty? => nil
x = '1' => "1"
x.to_f unless x.empty? => 1.0
I would like to extract some information from a string in Ruby by only reading the String once (O(n) time complexity).
Here is an example:
The string looks like this: -location here -time 7:30pm -activity biking
I have a Ruby object I want to populate with this info. All the keywords are known, and they are all optional.
def ActivityInfo
_attr_reader_ :location, :time, :activity
def initialize(str)
#location, #time, #activity = DEFAULT_LOCATION, DEFAULT_TIME, DEFAULT_ACTIVITY
# Here is how I was planning on implementing this
current_string = ""
next_parameter = nil # A reference to keep track of which parameter the current string is refering to
words = str.split
while !str.empty?
word = str.shift
case word
when "-location"
if !next_parameter.nil?
next_parameter.parameter = current_string # Set the parameter value to the current_string
current_string = ""
else
next_parameter = #location
when "-time"
if !next_parameter.nil?
next_parameter.parameter = current_string
current_string = ""
else
next_parameter = #time
when "-activity"
if !next_parameter.nil?
next_parameter.parameter = current_string
current_string = ""
else
next_parameter = #time
else
if !current_string.empty?
current_string += " "
end
current_string += word
end
end
end
end
So basically I just don't know how to make a variable be the reference of another variable or method, so that I can then set it to a specific value. Or maybe there is just another more efficient way to achieve this?
Thanks!
The string looks suspiciously like a command-line, and there are some good Ruby modules to parse those, such as optparse.
Assuming it's not, here's a quick way to parse the commands in your sample into a hash:
cmd = '-location here -time 7:30pm -activity biking'
Hash[*cmd.scan(/-(\w+) (\S+)/).flatten]
Which results in:
{
"location" => "here",
"time" => "7:30pm",
"activity" => "biking"
}
Expanding it a bit farther:
class ActivityInfo
def initialize(h)
#location = h['location']
#time = h['time' ]
#activity = h['activity']
end
end
act = ActivityInfo.new(Hash[*cmd.scan(/-(\w+) (\S+)/).flatten])
Which sets act to an instance of ActivityInfo looking like:
#<ActivityInfo:0x101142df8
#activity = "biking",
#location = "here",
#time = "7:30pm"
>
--
The OP asked how to deal with situations where the commands are not flagged with - or are multiple words. These are equivalent, but I prefer the first stylistically:
irb(main):003:0> cmd.scan(/-((?:location|time|activity)) \s+ (\S+)/x)
[
[0] [
[0] "location",
[1] "here"
],
[1] [
[0] "time",
[1] "7:30pm"
],
[2] [
[0] "activity",
[1] "biking"
]
]
irb(main):004:0> cmd.scan(/-(location|time|activity) \s+ (\S+)/x)
[
[0] [
[0] "location",
[1] "here"
],
[1] [
[0] "time",
[1] "7:30pm"
],
[2] [
[0] "activity",
[1] "biking"
]
]
If the commands are multiple words, such as "at location":
irb(main):009:0> cmd = '-at location here -time 7:30pm -activity biking'
"-at location here -time 7:30pm -activity biking"
irb(main):010:0>
irb(main):011:0* cmd.scan(/-((?:at \s location|time|activity)) \s+ (\S+)/x)
[
[0] [
[0] "at location",
[1] "here"
],
[1] [
[0] "time",
[1] "7:30pm"
],
[2] [
[0] "activity",
[1] "biking"
]
]
If you need even more flexibility look at Ruby's strscan module. You can use that to tear apart a string and find the commands and their parameters.
Convert String to Options Hash
If you just want easy access to your flags and their values, you can split your string into a hash where each flag is a key. For example:
options = Hash[ str.scan /-(\w+)\s+(\S+)/ ]
=> {"location"=>"here", "time"=>"7:30pm", "activity"=>"biking"}
You can then reference values directly (e.g. options['location']) or iterate through your hash in key/value pairs. For example:
options.each_pair { |k, v| puts "%s %s" % [k, v] }
A Dash of Metaprogramming
Okay, this is serious over-engineering, but I spent a little extra time on this question because I found it interesting. I'm not claiming the following is useful; I'm just saying it was fun for me to do.
If you want to parse your option flags and and dynamically create a set of attribute readers and set some instance variables without having to define each flag or variable separately, you can do this with a dash of metaprogramming.
# Set attribute readers and instance variables dynamically
# using Kernel#instance_eval.
class ActivityInfo
def initialize(str)
options = Hash[ str.scan /-(\w+)\s+(\S+)/ ]
options.each_pair do |k, v|
self.class.instance_eval { attr_reader k.to_sym }
instance_variable_set("##{k}", v)
end
end
end
ActivityInfo.new '-location here -time 7:30pm -activity biking'
=> #<ActivityInfo:0x00000001b49398
#activity="biking",
#location="here",
#time="7:30pm">
Honestly, I think setting your variables explicitly from an options hash such as:
#activity = options['activity']`
will convey your intent more clearly (and be more readable), but it's always good to have alternatives. Your mileage may vary.
Why reinvent the wheel when Thor can do the heavy lifting for you?
class ActivityInfo < Thor
desc "record", "record details of your activity"
method_option :location, :type => :string, :aliases => "-l", :required => true
method_option :time, :type => :datetime, :aliases => "-t", :required => true
method_option :activity, :type => :string, :aliases => "-a", :required => true
def record
location = options[:location]
time = options[:time]
activity = options[:activity]
# record details of the activity
end
end
The options will be parse for you based on the datatype you specified. You can invoke it programmatically:
task = ActivityInfo.new([], {location: 'NYC', time: Time.now, activity: 'Chilling out'})
task.record
Or from command line: thor activity_info:record -l NYC -t "2012-06-23 02:30:00" -a "Chilling out"
Given the following code,
How would you refactor this so that the method search_word has access to issueid?
I would say that changing the function search_word so it accepts 3 arguments or making issueid an instance variable (#issueid) could be considered as an example of bad practices, but honestly I cannot find any other solution. If there's no solution aside from this, would you mind explaining the reason why there's no other solution?
Please bear in mind that it is a Ruby on Rails model.
def search_type_of_relation_in_text(issueid, type_of_causality)
relation_ocurrences = Array.new
keywords_list = {
:C => ['cause', 'causes'],
:I => ['prevent', 'inhibitors'],
:P => ['type','supersets'],
:E => ['effect', 'effects'],
:R => ['reduce', 'inhibited'],
:S => ['example', 'subsets']
}[type_of_causality.to_sym]
for keyword in keywords_list
relation_ocurrences + search_word(keyword, relation_type)
end
return relation_ocurrences
end
def search_word(keyword, relation_type)
relation_ocurrences = Array.new
#buffer.search('//p[text()*= "'+keyword+'"]/a').each { |relation|
relation_suggestion_url = 'http://en.wikipedia.org'+relation.attributes['href']
relation_suggestion_title = URI.unescape(relation.attributes['href'].gsub("_" , " ").gsub(/[\w\W]*\/wiki\//, ""))
if not #current_suggested[relation_type].include?(relation_suggestion_url)
if #accepted[relation_type].include?(relation_suggestion_url)
relation_ocurrences << {:title => relation_suggestion_title, :wiki_url => relation_suggestion_url, :causality => type_of_causality, :status => "A", :issue_id => issueid}
else
relation_ocurrences << {:title => relation_suggestion_title, :wiki_url => relation_suggestion_url, :causality => type_of_causality, :status => "N", :issue_id => issueid}
end
end
}
end
If you need additional context, pass it through as an additional argument. That's how it's supposed to work.
Setting #-type instance variables to pass context is bad form as you've identified.
There's a number of Ruby conventions you seem to be unaware of:
Instead of Array.new just use [ ], and instead of Hash.new use { }.
Use a case statement or a constant instead of defining a Hash and then retrieving only one of the elements, discarding the remainder.
Avoid using return unless strictly necessary, as the last operation is always returned by default.
Use array.each do |item| instead of for item in array
Use do ... end instead of { ... } for multi-line blocks, where the curly brace version is generally reserved for one-liners. Avoids confusion with hash declarations.
Try and avoid duplicating large chunks of code when the differences are minor. For instance, declare a temporary variable, conditionally manipulate it, then store it instead of defining multiple independent variables.
With that in mind, here's a reworking of it:
KEYWORDS = {
:C => ['cause', 'causes'],
:I => ['prevent', 'inhibitors'],
:P => ['type','supersets'],
:E => ['effect', 'effects'],
:R => ['reduce', 'inhibited'],
:S => ['example', 'subsets']
}
def search_type_of_relation_in_text(issue_id, type_of_causality)
KEYWORDS[type_of_causality.to_sym].collect do |keyword|
search_word(keyword, relation_type, issue_id)
end
end
def search_word(keyword, relation_type, issue_id)
relation_occurrences = [ ]
#buffer.search(%Q{//p[text()*= "#{keyword}'"]/a}).each do |relation|
relation_suggestion_url = "http://en.wikipedia.org#{relation.attributes['href']}"
relation_suggestion_title = URI.unescape(relation.attributes['href'].gsub("_" , " ").gsub(/[\w\W]*\/wiki\//, ""))
if (!#current_suggested[relation_type].include?(relation_suggestion_url))
occurrence = {
:title => relation_suggestion_title,
:wiki_url => relation_suggestion_url,
:causality => type_of_causality,
:issue_id => issue_id
}
occurrence[:status] =
if (#accepted[relation_type].include?(relation_suggestion_url))
'A'
else
'N'
end
relation_ocurrences << occurrence
end
end
relation_occurrences
end