as-combined-metrics 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 6db99eeeb56b6107fd4f3e98aee2043f3addb23d
4
+ data.tar.gz: 84ad3a6ce0b4bd2840c30829045769e2fa95c6cd
5
+ SHA512:
6
+ metadata.gz: 25583029dc0e54184705bb798f9649bd73c04c2f2d0bc9136b9e7ecd177a3a9dd199785386731bb75b89ba5c49158704ecabfae722fdf619127588fc0d785e4e
7
+ data.tar.gz: ed23784918bf5407ea352a9c5cd591ddc6f28c0796e270f2d396b5f2e279695347c7ed042b45a9b1d177a19071659a6e7688dddbecf0037234cd53e539f620bd
data/.gitignore ADDED
@@ -0,0 +1,5 @@
1
+ .DS_Store
2
+ *.gem
3
+ *.conf
4
+ combinedMetrics.yml
5
+ *.bkpgit pu
@@ -0,0 +1,22 @@
1
+ $:.push File.expand_path("../lib", __FILE__)
2
+ $:.push File.expand_path("../lib/as-combined-metrics", __FILE__)
3
+
4
+ require File.expand_path('../lib/as-combined-metrics', __FILE__)
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = "as-combined-metrics"
8
+ s.version = AsCombinedMetrics::VERSION
9
+ s.authors = ["Ami Mahloof"]
10
+ s.email = "ami.mahloof@gmail.com"
11
+ s.homepage = "https://github.com/innovia/as-combined-metrics"
12
+ s.summary = "Submit custom AWS CloudWatch metric that combines several other thresholds for scale in or out"
13
+ s.description = "AWS custom combined metric CloudWatch tool"
14
+ s.required_rubygems_version = ">= 1.3.6"
15
+ s.files = `git ls-files`.split($\).reject{|n| n =~ %r[png|gif\z]}.reject{|n| n =~ %r[^(test|spec|features)/]}
16
+ s.add_runtime_dependency 'thor', '~> 0.19', '>= 0.19.1'
17
+ s.add_runtime_dependency 'aws-sdk', '~> 2.0.45', '>= 2.0.45'
18
+ s.add_runtime_dependency 'activesupport', '~> 4.2.3', '>= 4.2.3'
19
+ s.executables = s.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
20
+ s.extra_rdoc_files = ['README.md', 'LICENSE']
21
+ s.license = 'MIT'
22
+ end
data/CHANGES.md ADDED
@@ -0,0 +1,2 @@
1
+ 1.0.0
2
+ initial release
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2015-2016 Glide Talk, LTD.
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in
13
+ all copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
21
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,152 @@
1
+ AWS Autoscale Combined Metrics
2
+ ==============================
3
+
4
+ Beta - Not Production Ready!
5
+
6
+ AWS currently (July 2015) does not evalute other policies during a cooldown peroid
7
+
8
+ meanning if CPU policy was triggered and scaling acitivty happened the other policies for Network / Memory etc.. would not be evaluated until the cooldown period for the first policy passed
9
+
10
+ this gem will push a custom metric to cloudwatch:
11
+
12
+ it will combine the metrics you want to check and only if all of them are out of the treshold you provided
13
+
14
+ scale in if *all* metrics are in range send a 1 (ok to scale in)
15
+ scale out if *any* metrics are in range send a 1 (ok to scale out)
16
+
17
+ How it works
18
+ ------------
19
+
20
+ There's a yml config file in which you specify either a cloudformation stack name and resource (i.e AppServerGroup)
21
+ in this config you specify the metrics tresholds and comparison operator to evalute (kind of alrams in CloudWatch)
22
+
23
+ you can specify sectaions for ScaleOut and ScaleIn and their related metrics in the config file
24
+
25
+ ````yml
26
+ ScaleOut:
27
+ - metric_name: CPUUtilization
28
+ namespace: AWS/EC2
29
+ statistics:
30
+ - Maximum
31
+ unit: Percent
32
+ threshold: 20
33
+ comparison_operator: <=
34
+ aggregate_as_group: true
35
+ ````
36
+
37
+ you can specify aggregate_as_group: true in the config and it will aggregate based on the statistics you passed across ec2 instances
38
+
39
+ Note:
40
+ AWS is already agregating the instances metrics on the AutoScaleGroupName dimension for you (so if you are pushing a custom metrics you shold be pushing it to the autoscale group diemension)
41
+
42
+
43
+ if the autoscale group needs to be extracted from the stack name you specify (useful for discovery uisng cloudformation)
44
+ ````yml
45
+ cloudformation:
46
+ enabled: true
47
+ stack_name: STACK_NAME_X
48
+ logical_resource_ids:
49
+ - CloudFormation_RESOURCE_ID
50
+ ````
51
+
52
+ The app will combined the results of all metrics into a true / flase array
53
+ for ScaleOut events it will check if any element in the array is true and will publish a custom metric (i.e ScaleOut_CPUUtilization_NetworkIn ) under the combined_metrics custom name space in CloudWatch on the AutoScale Group dimension you have specified in the config
54
+
55
+ you can then set a single alarm and a single policy to perform your scale activity if itsthe value is 1 (O.K to scale in/out)
56
+
57
+
58
+ you run the file through the as-combined-metrics app (a backup of the config will be created)
59
+
60
+ ````bash
61
+ as-combined-metrics -f path_to/combinedMetrics.yml
62
+ ````
63
+
64
+ options:
65
+ --------
66
+ ````bash
67
+ r, [--region=REGION] # AWS Region
68
+ # Default: us-east-1
69
+ l, [--log-level=LOG_LEVEL] # Log level
70
+ # Default: INFO
71
+ f, [--config-file=CONFIG_FILE] # Metrics config file location
72
+ [--scalein-only=SCALEIN_ONLY] # gather combined metrics for scale in only
73
+ [--scaleout-only=SCALEOUT_ONLY] # gather combined metrics for scale out only
74
+ p, [--period=N] # Metric datapoint last x minutes
75
+ # Default: 300
76
+ t, [--timeout=N] # Timeout (seconds) for fetching autoscale group name
77
+ # Default: 120
78
+ o, [--once], [--no-once] # no loop - run once
79
+ d, [--dryrun], [--no-dryrun] # do not submit metric to CloudWatch
80
+ i, [--interval=N] # interval to check metrics
81
+ # Default: 30
82
+ ````
83
+
84
+ Required privileges
85
+ -------------------
86
+
87
+ * cloudwatch:GetMetricStatistics
88
+ * cloudwatch:PutMetricData
89
+ * autoscaling:DescribeAutoScalingGroups
90
+ * cloudformation:DescribeStackResources
91
+ * cloudformation:DescribeStacks
92
+
93
+ policy sample
94
+ ````json
95
+ {
96
+ "Version": "2012-10-17",
97
+ "Statement": [
98
+ {
99
+ "Sid": "Stmt1436799375000",
100
+ "Effect": "Allow",
101
+ "Action": [
102
+ "cloudwatch:GetMetricStatistics",
103
+ "cloudwatch:PutMetricData"
104
+ ],
105
+ "Resource": [
106
+ "*"
107
+ ]
108
+ },
109
+ {
110
+ "Sid": "Stmt1436799403000",
111
+ "Effect": "Allow",
112
+ "Action": [
113
+ "autoscaling:DescribeAutoScalingGroups"
114
+ ],
115
+ "Resource": [
116
+ "*"
117
+ ]
118
+ },
119
+ {
120
+ "Sid": "Stmt1436799495000",
121
+ "Effect": "Allow",
122
+ "Action": [
123
+ "cloudformation:DescribeStackResources",
124
+ "cloudformation:DescribeStacks"
125
+ ],
126
+ "Resource": [
127
+ "*"
128
+ ]
129
+ }
130
+ ]
131
+ }
132
+ ````
133
+
134
+ Credentials are loaded automatically from the following locations (AWS-SDK handled):
135
+
136
+ * ENV['AWS_ACCESS_KEY_ID'] and ENV['AWS_SECRET_ACCESS_KEY']
137
+ * Aws.config[:credentials]
138
+ * Shared credentials file, ~/.aws/credentials
139
+ * EC2 Instance profile
140
+
141
+
142
+ Contributing
143
+ -------------
144
+ 1. Fork it
145
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
146
+ 3. Commit your changes (`git commit -am 'Add some feature'`)
147
+ 4. Push to the branch (`git push origin my-new-feature`)
148
+ 5. Create new Pull Request
149
+
150
+ License
151
+ -------
152
+ aws-cobined-metrics is released under [MIT License](http://www.opensource.org/licenses/MIT)
@@ -0,0 +1,23 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
3
+ require 'thor'
4
+ require 'as-combined-metrics'
5
+ require 'active_support/hash_with_indifferent_access'
6
+
7
+ def help
8
+ AsCombinedMetrics::Cli.help(Thor::Base.shell.new)
9
+ end
10
+
11
+
12
+ begin
13
+ ENV["THOR_DEBUG"] = "1"
14
+ AsCombinedMetrics::Cli.start
15
+ rescue Thor::RequiredArgumentMissingError => e
16
+ puts "\e[31mMissing Arguments: #{e}\e[0m\n\n"
17
+ help
18
+ rescue Thor::InvocationError => e
19
+ puts "\e[31m#{e.to_s.gsub(/Usage:.+"/, '').chomp} but there's no such option\e[0m\n\n"
20
+ help
21
+ end
22
+
23
+
@@ -0,0 +1,56 @@
1
+ ---
2
+ cloudformation:
3
+ enabled: true
4
+ stack_name: STACK_NAME_X
5
+ logical_resource_ids:
6
+ - CloudFormation_RESOURCE_ID
7
+
8
+ autoscale_group_name: GROUP_NAME-X
9
+
10
+ ScaleIn:
11
+ - metric_name: CPUUtilization
12
+ namespace: AWS/EC2
13
+ statistics:
14
+ - Maximum
15
+ unit: Percent
16
+ threshold: 20
17
+ comparison_operator: <=
18
+ - metric_name: NetworkIn
19
+ namespace: AWS/EC2
20
+ unit: Bytes
21
+ threshold: 188743680
22
+ statistics:
23
+ - Maximum
24
+ comparison_operator: <=
25
+ - metric_name: memory_usage.heapUsed.max
26
+ namespace: node_cluster
27
+ statistics:
28
+ - Average
29
+ unit: Bytes
30
+ threshold: 7864320
31
+ comparison_operator: <=
32
+ aggregate_as_group: true
33
+
34
+
35
+ ScaleOut:
36
+ - metric_name: CPUUtilization
37
+ namespace: AWS/EC2
38
+ statistics:
39
+ - Maximum
40
+ unit: Percent
41
+ threshold: 20
42
+ comparison_operator: <=
43
+ - metric_name: NetworkIn
44
+ namespace: AWS/EC2
45
+ unit: Bytes
46
+ threshold: 188743680
47
+ statistics:
48
+ - Maximum
49
+ comparison_operator: <=
50
+ - metric_name: memory_usage.heapUsed.max
51
+ namespace: node_cluster
52
+ statistics:
53
+ - Average
54
+ unit: Bytes
55
+ threshold: 7864320
56
+ comparison_operator: <=
@@ -0,0 +1,42 @@
1
+ module AsCombinedMetrics::Cli::Aws
2
+ def init_aws_sdk
3
+ logger.progname = "#{Module.nesting.first.to_s} init_aws_sdk"
4
+ logger.info { set_color "Initializing AWS SDK", :white }
5
+
6
+ Aws.config.update({region: options[:region]})
7
+
8
+ # add max retries
9
+ @cw = Aws::CloudWatch::Client.new()
10
+ @cfm = Aws::CloudFormation::Client.new()
11
+ @as = Aws::AutoScaling::Client.new()
12
+ end
13
+
14
+ def validate_as_group(autoscale_group_name)
15
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
16
+ logger.info { set_color "Validating #{autoscale_group_name} exists...", :white }
17
+
18
+ response = @as.describe_auto_scaling_groups({auto_scaling_group_names: [autoscale_group_name], max_records: 1})
19
+
20
+ if response.auto_scaling_groups.empty? || autoscale_group_name != response.auto_scaling_groups[0].auto_scaling_group_name
21
+ logger.fatal { set_color "Can't find AutoScale group #{autoscale_group_name} on ec2 - exiting now", :red }
22
+ exit 1
23
+ else
24
+ logger.info { set_color "O.K", :green }
25
+ end
26
+ end
27
+
28
+ def verify_stack_name
29
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
30
+
31
+ if ENV['STACK_NAME']
32
+ logger.info { set_color "STACK_NAME Env variable was found [ #{ENV['STACK_NAME']} ]", :cyan }
33
+ @stack_name = ENV['STACK_NAME']
34
+ elsif @config["cloudformation"]["stack_name"]
35
+ logger.info { set_color "STACK_NAME was found in the config file #{@config["cloudformation"]["stack_name"]}", :cyan }
36
+ @stack_name = @config["cloudformation"]["stack_name"]
37
+ else
38
+ logger.fatal { set_color "verify_stack_name_env Env variable STACK_NAME was not found or does not have a value, exiting now...", :red }
39
+ exit 1
40
+ end
41
+ end
42
+ end
@@ -0,0 +1,89 @@
1
+ # encoding: utf-8
2
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
3
+
4
+ require 'thor'
5
+ require 'aws-sdk'
6
+ require 'yaml'
7
+ require 'pp'
8
+ require 'time'
9
+
10
+ class AsCombinedMetrics::Cli < Thor
11
+ autoload :Logging, 'as-combined-metrics/logger'
12
+ autoload :Config, 'as-combined-metrics/config'
13
+ autoload :Poller, 'as-combined-metrics/poller'
14
+ autoload :Aws, 'as-combined-metrics/aws'
15
+ autoload :Utils, 'as-combined-metrics/utils'
16
+ autoload :Stats, 'as-combined-metrics/stats'
17
+ autoload :CloudWatch, 'as-combined-metrics/cloudwatch'
18
+ autoload :CloudFormation, 'as-combined-metrics/cloudformation'
19
+
20
+ default_task :start
21
+
22
+ class_option :region, :desc => 'AWS Region', :default => 'us-east-1', :aliases => 'r', :type => :string
23
+ class_option :log_level, :desc => 'Log level', :default => 'INFO', :aliases => 'l', :type => :string
24
+ class_option :config_file, :desc => 'Metrics config file location', :aliases => 'f', :type => :string
25
+ class_option :scalein_only, :desc => 'gather combined metrics for scale in only', :default => false
26
+ class_option :scaleout_only, :desc => 'gather combined metrics for scale out only', :default => false
27
+ class_option :period, :desc => 'Metric datapoint last x minutes', :default => 300, :aliases => 'p', :type => :numeric
28
+ class_option :timeout, :desc => 'Timeout (seconds) for fetching autoscale group name', :default => 120, :aliases => 't', :type => :numeric
29
+ class_option :once, :desc => "no loop - run once", :default => false, :aliases => 'o', :type => :boolean
30
+ class_option :dryrun, :desc => "do not submit metric to CloudWatch", :default => false, :aliases => 'd', :type => :boolean
31
+ class_option :interval, :desc => 'interval to check metrics', :default => 30, :aliases => 'i', :type => :numeric
32
+ desc "start", "combine metrics"
33
+ def start
34
+ %w(INT TERM USR1 USR2 TTIN).each do |sig|
35
+ trap sig do
36
+ puts "Got Singnal #{sig} Exiting..."
37
+ exit 0
38
+ end
39
+ end
40
+
41
+ raise Thor::RequiredArgumentMissingError, 'missing config file location [-f / --config-file]' if options[:config_file].nil?
42
+
43
+ extend Logging
44
+ extend Utils
45
+ extend Config
46
+ extend Stats
47
+ extend Aws
48
+ extend CloudFormation
49
+ extend CloudWatch
50
+ extend Poller
51
+
52
+ logger.info { set_color "Dry Run - will not published metrics to CloudWatch", :yellow } if options[:dryrun]
53
+ logger.info "Starting Combined Metrics on #{options[:region]} region"
54
+ init_aws_sdk
55
+ load_config
56
+ poll(options[:interval])
57
+ <<-INFO
58
+ 1) in this loop (poll) we overwrite a hash of combined metrics as follow:
59
+
60
+ for every metric in the config file use fetch_metric(metric) to get it's current reading
61
+
62
+ if metric has a key of aggregate_as_group,
63
+ get all instances of the AutoScale group and for each instance fetch metric
64
+ then push the result to an array, from that array get the average result and push it to the combined_metrics hash
65
+
66
+ if one of the metrics fails to get datapoints (result => empty datapoints set) then set the result to -1
67
+ the check_combined_metrics will see that the value is -1 and automatically false the result
68
+
69
+
70
+ Combined metrics:
71
+ { "CPUUtilization" => {"measure"=>7.08, "threshold"=>30, "comparison_operator"=>">="},
72
+ "NetworkIn" => {"measure"=>43765341.0, "threshold"=>943718400, "comparison_operator"=>">="},
73
+ "memory_usage.heapUsed.average"=>{"measure"=> 67844551, "threshold"=>600000, "comparison_operator"=>">="}
74
+ }
75
+
76
+ 2) we then call check_combined_metrics to check if the measure is under or above the threshold and the result (true/false) to array
77
+
78
+ 3) if all elements of that array are true we set the value of self.combined_metric_value to 1 (Scale OK) else we set it to 0 (Do not Scale)
79
+
80
+ 4) last step -> publish that value to CloudWatch
81
+ INFO
82
+ end
83
+
84
+ map ["-v", "--version"] => :version
85
+ desc "version", "version"
86
+ def version
87
+ say AsCombinedMetrics::ABOUT, color = :green
88
+ end
89
+ end
@@ -0,0 +1,46 @@
1
+ module AsCombinedMetrics::Cli::CloudFormation
2
+ def describe_stack(logical_resource_id)
3
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
4
+
5
+ logger.info { set_color "Getting the full autoscale group name for the resource => #{logical_resource_id}", :white }
6
+ counter = 1
7
+ @autoscale_group_name = nil
8
+ @print_log = true
9
+
10
+ until @autoscale_group_name
11
+ begin
12
+ stack_info = @cfm.describe_stack_resource({
13
+ :stack_name => @stack_name,
14
+ :logical_resource_id => logical_resource_id
15
+ })
16
+
17
+ if !stack_info.nil?
18
+ @autoscale_group_name = stack_info.stack_resource_detail.physical_resource_id
19
+ @config["autoscale_group_name"] = @autoscale_group_name
20
+ File.open(options[:config_file], "w") { |f| f.write(@config.to_yaml) }
21
+ logger.info { set_color "AutoScale Group Name: #{@autoscale_group_name}", :cyan }
22
+ end
23
+ rescue Exception => e
24
+ if e.to_s.match (/Stack with name\s\S+\sdoes not exist/)
25
+ logger.fatal { set_color "Stack name was not found", :red }
26
+ exit 1
27
+ end
28
+
29
+ if @print_log
30
+ logger.error { set_color "Unable to find the resource - #{e}", :yellow }
31
+ logger.info { "Will retry every 5 seconds up to maximum of #{options[:timeout]} seconds" }
32
+ end
33
+
34
+ interval = 5
35
+ if counter < options[:timeout].to_i
36
+ counter += interval
37
+ @print_log = false
38
+ sleep interval
39
+ else
40
+ logger.fatal { set_color "Timeout Error - Unable to get the full AutoScale group name for the stack #{@stack_name} for #{options[:timeout]} seconds", :red }
41
+ exit 1
42
+ end
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,70 @@
1
+ module AsCombinedMetrics::Cli::CloudWatch
2
+
3
+ def set_default_options(hash, member)
4
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
5
+ if hash[member] && !hash[member].nil?
6
+ default = hash[member]
7
+ else
8
+ default = self.config["default_options"][member.to_s.gsub("_", "-")] if !self.config["default_options"][member.to_s.gsub("_", "-")].nil?
9
+ end
10
+ end
11
+
12
+ def fetch_metric(metric)
13
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
14
+
15
+ # Read from CloudWatch
16
+ data = {
17
+ :start_time => (Time.now.utc - options[:period]).iso8601,
18
+ :end_time => (Time.now.utc).iso8601,
19
+ :period => options[:period]
20
+ }
21
+
22
+ # # removing the keys that are not used in fetch metric
23
+ metric = metric.merge(data).reject { |k,v| [:comparison_operator, :threshold, :aggregate_as_group].include? k }
24
+
25
+ logger.info { set_color "Polling data for metric: #{metric[:metric_name]}", :white }
26
+ logger.info { "Metric info: #{metric}" }
27
+
28
+ begin
29
+ result = @cw.get_metric_statistics(metric).to_hash
30
+ rescue Exception => e
31
+ logger.error { set_color "An error occured #{e}, SDK will retry", :red }
32
+ end
33
+
34
+ if result[:datapoints].empty?
35
+ logger.info { set_color "No datapoints found for #{metric[:metric_name]} - Will publish a value of 0 (Don't DownScale) to CloudWatch", :red }
36
+ logger.info { "Result: #{result}" }
37
+ return -1
38
+ else
39
+ datapoint = result[:datapoints][0][metric[:statistics][0].downcase.to_sym]
40
+ logger.info { set_color "Result: #{datapoint} #{result[:datapoints][0][:unit]}", :white }
41
+ datapoint
42
+ end
43
+ end
44
+
45
+ def publish_metric(mode, combined_metric_value)
46
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
47
+
48
+ cw_options = {
49
+ namespace: 'combined_metrics',
50
+ metric_data: [
51
+ metric_name: combined_metrics_name(mode),
52
+ dimensions: [{
53
+ name: 'AutoScalingGroupName',
54
+ value: @config[:autoscale_group_name]
55
+ }],
56
+ value: combined_metric_value,
57
+ unit: 'None'
58
+ ]
59
+ }
60
+ logger.info { set_color "Options to be sent to cloudwatch: #{cw_options}", :white }
61
+
62
+ begin
63
+ logger.info { set_color "Publishing Combined Metrics to CloudWatch...", :white }
64
+ @cw.put_metric_data(cw_options)
65
+ rescue Exception => e
66
+ logger.error { set_color "An error occured #{e}, SDK will retry up to 3 times", :red }
67
+ end
68
+ end
69
+
70
+ end
@@ -0,0 +1,64 @@
1
+ require 'digest/sha1'
2
+
3
+ class ConfigError < StandardError
4
+ end
5
+
6
+ module AsCombinedMetrics::Cli::Config
7
+ def load_config
8
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
9
+ raise ConfigError, "Config file not found" if !File.exists?(options[:config_file])
10
+
11
+ sha1_config = Digest::SHA1.hexdigest(File.read(options[:config_file]))
12
+
13
+ if File.exists?("#{options[:config_file]}.bkp")
14
+ sha1_config_bkp = Digest::SHA1.hexdigest(File.read("#{options[:config_file]}.bkp"))
15
+ else
16
+ sha1_config_bkp = nil
17
+ end
18
+
19
+ if sha1_config != sha1_config_bkp
20
+ logger.info { set_color "Backing up original config file to #{options[:config_file]}.bkp", :white }
21
+ FileUtils.cp(options[:config_file], "#{options[:config_file]}.bkp")
22
+ else
23
+ logger.info {set_color "Skipping backup because the backup is identical to the config file", :white }
24
+ end
25
+
26
+ begin
27
+ logger.info { set_color "Loading config file (#{options[:config_file]})", :white }
28
+ @config = YAML.load_file(options[:config_file])
29
+
30
+ # Symbolize keys recursively for config
31
+ @config.deep_symbolize_keys!
32
+ logger.debug "config: #{@config}"
33
+
34
+ if options[:scalein_only] && !@config.has_key?(:ScaleIn)
35
+ raise ConfigError, "Could not find the proper mode for ScaleIn in the config file"
36
+ elsif options[:scaleout_only] && !@config.has_key?(:ScaleOut)
37
+ raise ConfigError, "Could not find the proper mode for ScaleOut in the config file"
38
+ elsif !@config.has_key?(:ScaleIn) || !@config.has_key?(:ScaleOut)
39
+ raise ConfigError, "Could not find the proper mode for ScaleIn or ScaleOut in the config file"
40
+ end
41
+
42
+ rescue Exception, ConfigError => e
43
+ logger.fatal {set_color "Error reading config file: #{e}", :red}
44
+ puts e.backtrace
45
+ exit 1
46
+ end
47
+ process
48
+ end
49
+
50
+ def process
51
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
52
+
53
+ if @config.has_key?(:autoscale_group_name)
54
+ validate_as_group(@config[:autoscale_group_name])
55
+ elsif @config[:cloudformation][:enabled]
56
+ verify_stack_name
57
+
58
+ @config[:cloudformation][:logical_resource_ids].each do | resource |
59
+ logger.debug "logical_resource_id: #{resource}"
60
+ describe_stack(resource)
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,24 @@
1
+ require 'logger'
2
+
3
+ module AsCombinedMetrics::Cli::Logging
4
+ def logger
5
+ @logger ||= AsCombinedMetrics::Cli::Logging.logger_for(self.class.name, options[:log_level])
6
+ end
7
+
8
+ # Use a hash class-ivar to cache a unique Logger per class:
9
+ @loggers = {}
10
+
11
+ class << self
12
+ def logger_for(class_name, level)
13
+ @loggers[class_name] ||= configure_logger_for(level)
14
+ end
15
+
16
+ def configure_logger_for(level)
17
+ logger = Logger.new(STDOUT)
18
+ logger.level = Object.const_get("Logger::#{level.upcase}")
19
+ logger
20
+ end
21
+ end
22
+ end
23
+
24
+
@@ -0,0 +1,51 @@
1
+ module AsCombinedMetrics::Cli::Poller
2
+ def poll(interval)
3
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
4
+ @combined_metrics = {}
5
+
6
+ if options[:scalein_only]
7
+ modes = [:ScaleIn]
8
+ elsif options[:scaleout_only]
9
+ modes = [:ScaleOut]
10
+ else
11
+ modes = [:ScaleIn, :ScaleOut]
12
+ end
13
+
14
+ loop do
15
+ modes.each do |mode|
16
+ logger.info "Polling metrics for #{mode}"
17
+
18
+ @config[mode].each do |metric|
19
+ logger.debug "Getting stats for metric #{metric}"
20
+ @combined_metrics[metric[:metric_name]] ||= {}
21
+
22
+ if metric.has_key?(:aggregate_as_group)
23
+ logger.debug "Aggregating autoscale group metrics"
24
+ @combined_metrics[metric[:metric_name]][:measure] = aggregate_instances_per_as_group(metric)
25
+ else
26
+ metric[:dimensions] = [{ name: "AutoScalingGroupName", value: @config[:autoscale_group_name]}]
27
+ @combined_metrics[metric[:metric_name]][:measure] = fetch_metric(metric) unless @combined_metrics[metric[:metric_name]].has_key?(:measure)
28
+ end
29
+
30
+ @combined_metrics[metric[:metric_name]][:threshold] = metric[:threshold]
31
+ @combined_metrics[metric[:metric_name]][:comparison_operator] = metric[:comparison_operator]
32
+ end
33
+
34
+ logger.info { set_color "Combined metrics attributes for #{mode}: #{@combined_metrics}", :cyan }
35
+ combined_metric_value = check_combined_metrics(mode)
36
+
37
+ logger.info { set_color "Combined metrics value: #{combined_metric_value} => [0 = Do Not #{mode}, 1 = OK To #{mode}]" }
38
+
39
+ publish_metric(mode, combined_metric_value) unless options[:dryrun]
40
+ end
41
+
42
+ if options[:once]
43
+ logger.info { set_color "Ran Once - Exiting now..." }
44
+ exit 0
45
+ else
46
+ logger.info { set_color "Next metrics check in #{interval} seconds..." }
47
+ sleep interval
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,98 @@
1
+ module AsCombinedMetrics::Cli::Stats
2
+ def check_combined_metrics(mode)
3
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
4
+
5
+ logger.info { set_color "Checking combined metrics for #{mode}:", :white }
6
+
7
+ # prepare an array to hold a true / false result for each metric
8
+ results_array = []
9
+
10
+ # checking each metric by its comparison operator for true / flase
11
+ @combined_metrics.each do |name, values|
12
+ # The line below checks => measure comparison operator value (a <= b) without the use of eval! (it turn the string into evalutation)
13
+ # measure.method(b['comparison_operator']).(threshold)
14
+ if values[:measure] == -1
15
+ evaluation = false
16
+ else
17
+ evaluation = values[:measure].method(values[:comparison_operator]).(values[:threshold])
18
+ end
19
+ results_array << evaluation
20
+ logger.info { set_color "Results for combined metrics for #{name} with #{values}: #{results_array.last}", :magenta }
21
+ end
22
+
23
+ # a short hand if statement => if all array elements true set combined_metric_value to 1 else to 0
24
+ case mode
25
+ when :ScaleIn then
26
+ results_array.all? ? combined_metric_value = 1 : combined_metric_value = 0
27
+ when :ScaleOut then
28
+ results_array.any? ? combined_metric_value = 1 : combined_metric_value = 0
29
+ else
30
+ logger.fatal { set_color "Could not find mode #{mode} - check the config file, exiting now...", :red }
31
+ exit
32
+ end
33
+
34
+ logger.info { set_color "Combined results of #{results_array} for #{mode} is: #{combined_metric_value}", :yellow, :bold }
35
+
36
+ return combined_metric_value
37
+ end
38
+
39
+ def aggregate_instances_per_as_group(metric)
40
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
41
+ logger.info { set_color "Aggregating metrics accross instances for autoscale group: #{@config[:autoscale_group_name]}" , :white}
42
+ begin
43
+
44
+ instances = @as.describe_auto_scaling_groups({auto_scaling_group_names: [@config[:autoscale_group_name]], max_records: 1}).auto_scaling_groups.first.instances.collect {|i| i[:instance_id]}
45
+
46
+ logger.info { set_color "Found #{instances.size} Instances for autoscale group: #{@config['autoscale_group_name']}", :magenta }
47
+
48
+ instances_aggregated_data = []
49
+
50
+ metric[:dimensions] = [{name: "InstanceId"}]
51
+
52
+ instances.each do |instance_id|
53
+ metric[:dimensions][0][:value] = instance_id
54
+
55
+ logger.info {"aggregated metric: #{metric}"}
56
+ metric_result = fetch_metric(metric)
57
+
58
+ logger.info { set_color "Metric result: #{metric_result}", :bold }
59
+
60
+ if metric_result == -1
61
+ instances_aggregated_data << metric_result
62
+ break
63
+ return # check removal of break
64
+ else
65
+ instances_aggregated_data << metric_result
66
+ end
67
+ end
68
+
69
+ logger.info { "Instances_aggregated_data: #{instances_aggregated_data}" }
70
+
71
+ if !instances_aggregated_data.empty?
72
+ case metric[:statistics][0].downcase
73
+ when 'maximum'
74
+ logger.info { set_color "Instances_aggregated_data [Maximum]: #{instances_aggregated_data.max}", :yellow }
75
+ instances_aggregated_data.max
76
+ when 'minimum'
77
+ logger.info { set_color "Instances_aggregated_data [Minimum]: #{instances_aggregated_data.min}", :yellow }
78
+ instances_aggregated_data.min
79
+ else
80
+ avg_array(instances_aggregated_data)
81
+ end
82
+ else
83
+ # Do not scale down - we don't have all the metric data to scale down
84
+ end
85
+ rescue Exception => e
86
+ logger.info { set_color "Error aggregating metrics #{e.message}", :red }
87
+ end
88
+ end
89
+
90
+ def avg_array(data)
91
+ logger.progname = "#{Module.nesting.first.to_s} #{__method__}"
92
+
93
+ avg = (data.inject {|sum, i| sum + i }) / data.size
94
+ self.logger.info { set_color "Instances_aggregated_data [Average]: #{avg.round}", :white }
95
+ avg.round
96
+ end
97
+
98
+ end
@@ -0,0 +1,6 @@
1
+ module AsCombinedMetrics::Cli::Utils
2
+ def combined_metrics_name(mode)
3
+ # combined the metrics name for each mode with underscores
4
+ "#{mode}_#{@config[mode].map { |m| m[:metric_name].gsub(/\.|-/, '_')}.join('_')}"
5
+ end
6
+ end
@@ -0,0 +1,8 @@
1
+ module AsCombinedMetrics
2
+ VERSION = "1.0.0"
3
+ ABOUT = "as-combined-metrics v#{VERSION} (c) #{Time.now.strftime("2015-%Y")} @innovia"
4
+
5
+ $:.unshift File.expand_path(File.join(File.dirname(__FILE__), %w[.. lib]))
6
+
7
+ autoload :Cli, 'as-combined-metrics/cli'
8
+ end
metadata ADDED
@@ -0,0 +1,124 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: as-combined-metrics
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ platform: ruby
6
+ authors:
7
+ - Ami Mahloof
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2015-07-14 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ~>
18
+ - !ruby/object:Gem::Version
19
+ version: '0.19'
20
+ - - '>='
21
+ - !ruby/object:Gem::Version
22
+ version: 0.19.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: '0.19'
30
+ - - '>='
31
+ - !ruby/object:Gem::Version
32
+ version: 0.19.1
33
+ - !ruby/object:Gem::Dependency
34
+ name: aws-sdk
35
+ requirement: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - ~>
38
+ - !ruby/object:Gem::Version
39
+ version: 2.0.45
40
+ - - '>='
41
+ - !ruby/object:Gem::Version
42
+ version: 2.0.45
43
+ type: :runtime
44
+ prerelease: false
45
+ version_requirements: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ~>
48
+ - !ruby/object:Gem::Version
49
+ version: 2.0.45
50
+ - - '>='
51
+ - !ruby/object:Gem::Version
52
+ version: 2.0.45
53
+ - !ruby/object:Gem::Dependency
54
+ name: activesupport
55
+ requirement: !ruby/object:Gem::Requirement
56
+ requirements:
57
+ - - ~>
58
+ - !ruby/object:Gem::Version
59
+ version: 4.2.3
60
+ - - '>='
61
+ - !ruby/object:Gem::Version
62
+ version: 4.2.3
63
+ type: :runtime
64
+ prerelease: false
65
+ version_requirements: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 4.2.3
70
+ - - '>='
71
+ - !ruby/object:Gem::Version
72
+ version: 4.2.3
73
+ description: AWS custom combined metric CloudWatch tool
74
+ email: ami.mahloof@gmail.com
75
+ executables:
76
+ - as-combined-metrics
77
+ extensions: []
78
+ extra_rdoc_files:
79
+ - README.md
80
+ - LICENSE
81
+ files:
82
+ - .gitignore
83
+ - AsCombinedMetrics.gemspec
84
+ - CHANGES.md
85
+ - LICENSE
86
+ - README.md
87
+ - bin/as-combined-metrics
88
+ - combinedMetrics-sample.yml
89
+ - lib/as-combined-metrics.rb
90
+ - lib/as-combined-metrics/aws.rb
91
+ - lib/as-combined-metrics/cli.rb
92
+ - lib/as-combined-metrics/cloudformation.rb
93
+ - lib/as-combined-metrics/cloudwatch.rb
94
+ - lib/as-combined-metrics/config.rb
95
+ - lib/as-combined-metrics/logger.rb
96
+ - lib/as-combined-metrics/poller.rb
97
+ - lib/as-combined-metrics/stats.rb
98
+ - lib/as-combined-metrics/utils.rb
99
+ homepage: https://github.com/innovia/as-combined-metrics
100
+ licenses:
101
+ - MIT
102
+ metadata: {}
103
+ post_install_message:
104
+ rdoc_options: []
105
+ require_paths:
106
+ - lib
107
+ required_ruby_version: !ruby/object:Gem::Requirement
108
+ requirements:
109
+ - - '>='
110
+ - !ruby/object:Gem::Version
111
+ version: '0'
112
+ required_rubygems_version: !ruby/object:Gem::Requirement
113
+ requirements:
114
+ - - '>='
115
+ - !ruby/object:Gem::Version
116
+ version: 1.3.6
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 2.4.2
120
+ signing_key:
121
+ specification_version: 4
122
+ summary: Submit custom AWS CloudWatch metric that combines several other thresholds
123
+ for scale in or out
124
+ test_files: []