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 +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: []
|