Last active
November 21, 2017 17:18
-
-
Save dallasmarlow/c7a9a6ba369fb4d8c23d73d60540291a to your computer and use it in GitHub Desktop.
ec2 cpu utilization report
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
package main | |
import ( | |
"flag" | |
"fmt" | |
"math" | |
"os" | |
"path" | |
"sort" | |
"strconv" | |
"strings" | |
"time" | |
"github.com/aws/aws-sdk-go/aws" | |
"github.com/aws/aws-sdk-go/aws/session" | |
"github.com/aws/aws-sdk-go/service/cloudwatch" | |
"github.com/aws/aws-sdk-go/service/ec2" | |
"github.com/olekukonko/tablewriter" | |
histogram "github.com/vividcortex/gohistogram" | |
) | |
var ( | |
awsRegion = flag.String(`region`, `us-east-1`, `aws ec2 region`) | |
awsRetries = flag.Int(`retries`, 10, ``) | |
cloudwatchMetric = flag.String(`cloudwatchMetric`, `CPUUtilization`, ``) | |
cloudwatchUnit = flag.String(`cloudwatchUnit`, `Percent`, ``) | |
cloudwatchPeroid = flag.Int64(`cloudwatchPeroid`, 60, ``) | |
cloudwatchAgg = flag.String(`cloudwatchAgg`, `average`, `metric value aggregator`) | |
metricThreshold = flag.Float64(`threshold`, 80.0, `metric segmentation value`) | |
metricInterval = flag.Duration(`interval`, 24*time.Hour, `duration of time to subtract from current time`) | |
) | |
type Ec2Instance struct { | |
Id, Type, Role, Env, Name string | |
LaunchTime time.Time | |
CwSummaries map[string]CwSeriesSummary | |
} | |
func newEc2Instance(ec2Instance *ec2.Instance) Ec2Instance { | |
instance := Ec2Instance{ | |
Id: *ec2Instance.InstanceId, | |
Type: *ec2Instance.InstanceType, | |
LaunchTime: *ec2Instance.LaunchTime, | |
CwSummaries: make(map[string]CwSeriesSummary), | |
} | |
for _, tag := range ec2Instance.Tags { | |
if tag == nil { | |
continue | |
} | |
switch *tag.Key { | |
case "Name", "name": | |
instance.Name = *tag.Value | |
case "Env", "env": | |
instance.Env = *tag.Value | |
case "Role", "role": | |
instance.Role = *tag.Value | |
} | |
} | |
if instance.Role == `` && | |
ec2Instance.IamInstanceProfile != nil && | |
*ec2Instance.IamInstanceProfile.Arn != `` { | |
instance.Role = path.Base(*ec2Instance.IamInstanceProfile.Arn) | |
} | |
if instance.Name == `` { | |
instance.Name = *ec2Instance.PublicDnsName | |
} | |
return instance | |
} | |
func newAwsCfg(region string, retries int) *aws.Config { | |
return &aws.Config{ | |
Region: aws.String(region), | |
MaxRetries: aws.Int(retries)} | |
} | |
func newEc2Svc(region string, retries int) *ec2.EC2 { | |
return ec2.New(session.New(), newAwsCfg(region, retries)) | |
} | |
func newCloudWatchSvc(region string, retries int) *cloudwatch.CloudWatch { | |
return cloudwatch.New(session.New(), newAwsCfg(region, retries)) | |
} | |
func newCloudWatchQuery(metric, unit string, instanceId string) *cloudwatch.GetMetricStatisticsInput { | |
return &cloudwatch.GetMetricStatisticsInput{ | |
Namespace: aws.String("AWS/EC2"), | |
MetricName: aws.String(metric), | |
StartTime: aws.Time(time.Now().Add(-*metricInterval)), | |
EndTime: aws.Time(time.Now()), | |
Unit: aws.String(unit), | |
Period: aws.Int64(*cloudwatchPeroid), | |
Statistics: []*string{aws.String(parseCloudwatchAgg(*cloudwatchAgg))}, | |
Dimensions: []*cloudwatch.Dimension{ | |
{Name: aws.String("InstanceId"), Value: aws.String(instanceId)}, | |
}, | |
} | |
} | |
type CwSeriesSummary struct { | |
NumDatapoints, ThresholdDatapoints int | |
Sum, Mean, Q95, StdDev float64 | |
} | |
func (s CwSeriesSummary) ThresholdPercentage() float64 { | |
return float64(s.ThresholdDatapoints) / float64(s.NumDatapoints) | |
} | |
type GroupedCwSeriesSummary struct { | |
NumInstances int | |
CwSeriesSummary | |
} | |
func summarizeCloudWatchSeries(series *cloudwatch.GetMetricStatisticsOutput, threshold float64) CwSeriesSummary { | |
summary := CwSeriesSummary{NumDatapoints: len(series.Datapoints)} | |
hist := histogram.NewHistogram(20) | |
for _, datapoint := range series.Datapoints { | |
var value float64 | |
switch parseCloudwatchAgg(*cloudwatchAgg) { | |
case `Average`: | |
value = *datapoint.Average | |
case `Maximum`: | |
value = *datapoint.Maximum | |
case `Minimum`: | |
value = *datapoint.Minimum | |
case `Sum`: | |
value = *datapoint.Sum | |
} | |
hist.Add(value) | |
summary.Sum += value | |
if value > threshold { | |
summary.ThresholdDatapoints += 1 | |
} | |
} | |
summary.Mean = hist.Mean() | |
summary.Q95 = hist.Quantile(0.95) | |
summary.StdDev = math.Sqrt(hist.Variance()) | |
return summary | |
} | |
func queryInstances(ec2Svc *ec2.EC2) ([]*ec2.Instance, error) { | |
var instances []*ec2.Instance | |
var token *string | |
for { | |
resp, err := ec2Svc.DescribeInstances( | |
&ec2.DescribeInstancesInput{ | |
NextToken: token, | |
Filters: []*ec2.Filter{ | |
{Name: aws.String(`instance-state-name`), Values: []*string{aws.String(`running`)}}}}) | |
// {Name: aws.String(`tag:role`), Values: []*string{aws.String(*awsRole)}}}}) | |
if err != nil { | |
return nil, err | |
} | |
for _, resv := range resp.Reservations { | |
instances = append(instances, resv.Instances...) | |
} | |
if resp.NextToken != nil { | |
token = resp.NextToken | |
} else { | |
break | |
} | |
} | |
return instances, nil | |
} | |
func processInstances( | |
cw *cloudwatch.CloudWatch, | |
cwMetric, cwUnit string, | |
threshold float64, | |
instances []*ec2.Instance) ([]Ec2Instance, error) { | |
var ec2Instances []Ec2Instance | |
for _, instance := range instances { | |
ec2Instance := newEc2Instance(instance) | |
fmt.Println(`querying for cw metrics:`, ec2Instance.Id) | |
instanceMetrics, err := cw.GetMetricStatistics( | |
newCloudWatchQuery(cwMetric, cwUnit, ec2Instance.Id)) | |
if err != nil { | |
return nil, err | |
} | |
ec2Instance.CwSummaries[cwMetric] = | |
summarizeCloudWatchSeries(instanceMetrics, threshold) | |
ec2Instances = append(ec2Instances, ec2Instance) | |
} | |
return ec2Instances, nil | |
} | |
func groupInstancesByField( | |
instances []Ec2Instance, | |
fieldFn func(Ec2Instance) string) map[string][]Ec2Instance { | |
groupedInstances := make(map[string][]Ec2Instance) | |
for _, instance := range instances { | |
group := fieldFn(instance) | |
groupedInstances[group] = | |
append(groupedInstances[group], instance) | |
} | |
return groupedInstances | |
} | |
func summarizeGroupedCwSeries( | |
metric string, | |
groupedInstances map[string][]Ec2Instance) map[string]GroupedCwSeriesSummary { | |
summaries := make(map[string]GroupedCwSeriesSummary) | |
for group, instances := range groupedInstances { | |
summary := GroupedCwSeriesSummary{NumInstances: len(instances)} | |
var totalMean, totalQ95, totalStdDev float64 | |
for _, instance := range instances { | |
summary.NumDatapoints += instance.CwSummaries[metric].NumDatapoints | |
summary.ThresholdDatapoints += instance.CwSummaries[metric].ThresholdDatapoints | |
summary.Sum += instance.CwSummaries[metric].Sum | |
totalMean += instance.CwSummaries[metric].Mean | |
totalQ95 += instance.CwSummaries[metric].Q95 | |
totalStdDev += instance.CwSummaries[metric].StdDev | |
} | |
summary.Mean = totalMean / float64(summary.NumInstances) | |
summary.Q95 = totalQ95 / float64(summary.NumInstances) | |
summary.StdDev = totalStdDev / float64(summary.NumInstances) | |
summaries[group] = summary | |
} | |
return summaries | |
} | |
func renderGroupedAsciiSummary(groupField string, summaries map[string]GroupedCwSeriesSummary) { | |
table := tablewriter.NewWriter(os.Stdout) | |
if *cloudwatchUnit != `Percent` { | |
table.SetHeader( | |
[]string{groupField, `Instances`, `Sum`, `Mean`, `Q95`, `StdDev`, `Threshold`, `Raw`}) | |
} else { | |
table.SetHeader( | |
[]string{groupField, `Instances`, `Mean`, `Q95`, `StdDev`, `Threshold`, `Raw`}) | |
} | |
floatToString := func(n float64) string { | |
return strconv.FormatFloat(n, 'f', 0, 64) | |
} | |
var groups []string | |
for group := range summaries { | |
groups = append(groups, group) | |
} | |
sort.Strings(groups) | |
for _, group := range groups { | |
summary, pres := summaries[group] | |
if pres { | |
if *cloudwatchUnit != `Percent` { | |
table.Append([]string{ | |
group, | |
strconv.Itoa(summary.NumInstances), | |
floatToString(summary.Sum), | |
floatToString(summary.Mean), | |
floatToString(summary.Q95), | |
floatToString(summary.StdDev), | |
strconv.Itoa(summary.ThresholdDatapoints), | |
strconv.Itoa(summary.NumDatapoints)}) | |
} else { | |
table.Append([]string{ | |
group, | |
strconv.Itoa(summary.NumInstances), | |
floatToString(summary.Mean), | |
floatToString(summary.Q95), | |
floatToString(summary.StdDev), | |
strconv.Itoa(summary.ThresholdDatapoints), | |
strconv.Itoa(summary.NumDatapoints)}) | |
} | |
} | |
} | |
fmt.Println( | |
time.Now().UTC(), | |
` -- `, | |
strings.Join([]string{ | |
*awsRegion, | |
*cloudwatchMetric, | |
*cloudwatchUnit, | |
parseCloudwatchAgg(*cloudwatchAgg), | |
floatToString(*metricThreshold)}, ` / `)) | |
table.Render() | |
} | |
func parseCloudwatchAgg(value string) string { | |
switch strings.ToLower(value) { | |
case `max`, `maximum`: | |
return `Maximum` | |
case `min`, `minimum`: | |
return `Minimum` | |
case `sum`: | |
return `Sum` | |
} | |
return `Average` | |
} | |
func main() { | |
flag.Parse() | |
instances, err := queryInstances( | |
newEc2Svc(*awsRegion, *awsRetries)) | |
if err != nil { | |
panic(err) | |
} | |
cwSvc := newCloudWatchSvc(*awsRegion, *awsRetries) | |
ec2Instances, err := processInstances( | |
cwSvc, | |
*cloudwatchMetric, | |
*cloudwatchUnit, | |
*metricThreshold, | |
instances) | |
if err != nil { | |
panic(err) | |
} | |
renderGroupedAsciiSummary( | |
`Type / Role`, | |
summarizeGroupedCwSeries( | |
*cloudwatchMetric, | |
groupInstancesByField( | |
ec2Instances, | |
func(i Ec2Instance) string { | |
return strings.Join([]string{i.Type, i.Role}, `/`) | |
}))) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment