as-combined-metrics 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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: []