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 +7 -0
- data/.gitignore +5 -0
- data/AsCombinedMetrics.gemspec +22 -0
- data/CHANGES.md +2 -0
- data/LICENSE +21 -0
- data/README.md +152 -0
- data/bin/as-combined-metrics +23 -0
- data/combinedMetrics-sample.yml +56 -0
- data/lib/as-combined-metrics/aws.rb +42 -0
- data/lib/as-combined-metrics/cli.rb +89 -0
- data/lib/as-combined-metrics/cloudformation.rb +46 -0
- data/lib/as-combined-metrics/cloudwatch.rb +70 -0
- data/lib/as-combined-metrics/config.rb +64 -0
- data/lib/as-combined-metrics/logger.rb +24 -0
- data/lib/as-combined-metrics/poller.rb +51 -0
- data/lib/as-combined-metrics/stats.rb +98 -0
- data/lib/as-combined-metrics/utils.rb +6 -0
- data/lib/as-combined-metrics.rb +8 -0
- metadata +124 -0
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,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
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
|
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: []
|