Created
March 11, 2019 17:09
-
-
Save dallasmarlow/e099274642b1dffc0e70b47704025ce5 to your computer and use it in GitHub Desktop.
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-west-2`, `aws ec2 region`) | |
awsRetries = flag.Int(`retries`, 10, ``) | |
cloudwatchMetric = flag.String(`cloudwatchMetric`, `CPUUtilization`, ``) | |
cloudwatchUnit = flag.String(`cloudwatchUnit`, `Percent`, ``) | |
cloudwatchAgg = flag.String(`cloudwatchAgg`, `average`, `metric value aggregator`) | |
metricThreshold = flag.Float64(`threshold`, 80.0, `metric segmentation value`) | |
) | |
type PrscpEc2Instance struct { | |
Id, Type, Role, Env, Name string | |
LaunchTime time.Time | |
CwSummaries map[string]CwSeriesSummary | |
} | |
func newPrscpEc2Instance(ec2Instance *ec2.Instance) PrscpEc2Instance { | |
instance := PrscpEc2Instance{ | |
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": | |
instance.Name = *tag.Value | |
case "env": | |
instance.Env = *tag.Value | |
case "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(-24 * time.Hour)), | |
EndTime: aws.Time(time.Now()), | |
Unit: aws.String(unit), | |
Period: aws.Int64(60), | |
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 ec2Instances(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 prscpEc2Instances( | |
cw *cloudwatch.CloudWatch, | |
cwMetric, cwUnit string, | |
threshold float64, | |
instances []*ec2.Instance) ([]PrscpEc2Instance, error) { | |
var prscpInstances []PrscpEc2Instance | |
for _, instance := range instances { | |
prscpInstance := newPrscpEc2Instance(instance) | |
fmt.Println(`querying for cw metrics:`, prscpInstance.Id) | |
instanceMetrics, err := cw.GetMetricStatistics( | |
newCloudWatchQuery(cwMetric, cwUnit, prscpInstance.Id)) | |
if err != nil { | |
return nil, err | |
} | |
prscpInstance.CwSummaries[cwMetric] = | |
summarizeCloudWatchSeries(instanceMetrics, threshold) | |
prscpInstances = append(prscpInstances, prscpInstance) | |
} | |
return prscpInstances, nil | |
} | |
func groupPrscpInstancesByField( | |
instances []PrscpEc2Instance, | |
fieldFn func(PrscpEc2Instance) string) map[string][]PrscpEc2Instance { | |
groupedInstances := make(map[string][]PrscpEc2Instance) | |
for _, instance := range instances { | |
group := fieldFn(instance) | |
groupedInstances[group] = | |
append(groupedInstances[group], instance) | |
} | |
return groupedInstances | |
} | |
func summarizeGroupedCwSeries( | |
metric string, | |
groupedInstances map[string][]PrscpEc2Instance) 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 := ec2Instances( | |
newEc2Svc(*awsRegion, *awsRetries)) | |
if err != nil { | |
panic(err) | |
} | |
cwSvc := newCloudWatchSvc(*awsRegion, *awsRetries) | |
prscpInstances, err := prscpEc2Instances( | |
cwSvc, | |
*cloudwatchMetric, | |
*cloudwatchUnit, | |
*metricThreshold, | |
instances) | |
if err != nil { | |
panic(err) | |
} | |
renderGroupedAsciiSummary( | |
`Type / Name`, | |
summarizeGroupedCwSeries( | |
*cloudwatchMetric, | |
groupPrscpInstancesByField( | |
prscpInstances, | |
func(i PrscpEc2Instance) string { | |
return strings.Join([]string{i.Type, i.Name}, `/`) | |
}))) | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment