github.com/mweagle/Sparta@v1.15.0/decorator/dashboard.go (about) 1 package decorator 2 3 import ( 4 "bytes" 5 "regexp" 6 "text/template" 7 8 "github.com/aws/aws-sdk-go/aws/session" 9 sparta "github.com/mweagle/Sparta" 10 spartaCF "github.com/mweagle/Sparta/aws/cloudformation" 11 gocf "github.com/mweagle/go-cloudformation" 12 "github.com/sirupsen/logrus" 13 ) 14 15 const ( 16 // OutputDashboardURL is the keyname used in the CloudFormation Output 17 // that stores the CloudWatch Dashboard URL 18 // @enum OutputKey 19 OutputDashboardURL = "CloudWatchDashboardURL" 20 ) 21 22 const ( 23 headerWidthUnits = 9 24 headerHeightUnits = 6 25 metricsPerRow = 3 26 metricWidthUnits = 6 27 metricHeightUnits = 6 28 ) 29 30 // widgetExtents represents the extents of various containers in the generated 31 // dashboard 32 type widgetExtents struct { 33 HeaderWidthUnits int 34 HeaderHeightUnits int 35 MetricWidthUnits int 36 MetricHeightUnits int 37 MetricsPerRow int 38 } 39 40 // LambdaTemplateData is the mapping of Sparta public LambdaAWSInfo together 41 // with the CloudFormationResource name this resource uses 42 type LambdaTemplateData struct { 43 LambdaAWSInfo *sparta.LambdaAWSInfo 44 ResourceName string 45 } 46 47 // DashboardTemplateData is the object supplied to the dashboard template 48 // to generate the resulting dashboard 49 type DashboardTemplateData struct { 50 // The list of lambda functions 51 LambdaFunctions []*LambdaTemplateData 52 // SpartaVersion is the Sparta library used to provision this service 53 SpartaVersion string 54 // SpartaGitHash is the commit hash of this version of the library 55 SpartaGitHash string 56 TimeSeriesPeriod int 57 Extents widgetExtents 58 } 59 60 // The default dashboard template 61 var dashboardTemplate = ` 62 { 63 "widgets": [ 64 { 65 "type": "text", 66 "x": 0, 67 "y": 0, 68 "width": << .Extents.HeaderWidthUnits >>, 69 "height": << .Extents.HeaderHeightUnits >>, 70 "properties": { 71 "markdown": "##  { "Ref" : "AWS::StackName" } Summary\n 72 * ☁️ [CloudFormation Stack](https://{ "Ref" : "AWS::Region" }.console.aws.amazon.com/cloudformation/home?region={ "Ref" : "AWS::Region" }#/stack/detail?stackId={"Ref" : "AWS::StackId"})\n 73 * ☢️ [XRay](https://{ "Ref" : "AWS::Region" }.console.aws.amazon.com/xray/home?region={ "Ref" : "AWS::Region" }#/service-map)\n 74 * **Lambda Count** : << len .LambdaFunctions >>\n 75 * **Sparta Version** : << .SpartaVersion >> ( [<< .SpartaGitHash >>](https://github.com/mweagle/Sparta/commit/<< .SpartaGitHash >>) )\n 76 * 🔗 [Sparta Documentation](https://gosparta.io)\n" 77 } 78 }, 79 { 80 "type": "text", 81 "x": << .Extents.HeaderWidthUnits >>, 82 "y": 0, 83 "width": << .Extents.HeaderWidthUnits >>, 84 "height": << .Extents.HeaderHeightUnits >>, 85 "properties": { 86 "markdown": "##  { "Ref" : "AWS::StackName" } Logs\n 87 <<range $index, $eachLambda := .LambdaFunctions>> 88 * 🔎 [{ "Ref" : "<< $eachLambda.ResourceName >>" }](https://{ "Ref" : "AWS::Region" }.console.aws.amazon.com/cloudwatch/home?region={ "Ref" : "AWS::Region" }#logStream:group=/aws/lambda/{ "Ref" : "<< $eachLambda.ResourceName >>" })\n 89 <<end>>" 90 } 91 }<<range $index, $eachLambda := .LambdaFunctions>>, 92 { 93 "type": "metric", 94 "x": <<widgetX $index >>, 95 "y": <<widgetY $index >>, 96 "width": << $.Extents.MetricWidthUnits >>, 97 "height": << $.Extents.MetricHeightUnits >>, 98 "properties": { 99 "view": "timeSeries", 100 "stacked": false, 101 "metrics": [ 102 [ "AWS/Lambda", "Invocations", "FunctionName", "{ "Ref" : "<< $eachLambda.ResourceName >>" }", { "stat": "Sum" }], 103 [ ".", "Errors", ".", ".", { "stat": "Sum" }], 104 [ ".", "Throttles", ".", ".", { "stat": "Sum" } ] 105 ], 106 "region": "{ "Ref" : "AWS::Region" }", 107 "period": << $.TimeSeriesPeriod >>, 108 "title": "λ: { "Ref" : "<< $eachLambda.ResourceName >>" }" 109 } 110 }<<end>> 111 ] 112 } 113 ` 114 115 var templateFuncMap = template.FuncMap{ 116 // The name "inc" is what the function will be called in the template text. 117 "widgetX": func(lambdaIndex int) int { 118 return metricWidthUnits * (lambdaIndex % metricsPerRow) 119 }, 120 "widgetY": func(lambdaIndex int) int { 121 xRow := 1 122 xRow += (int)((float64)(lambdaIndex % metricsPerRow)) 123 // That's the row 124 return headerHeightUnits + (xRow * metricHeightUnits) 125 }, 126 } 127 128 // DashboardDecorator returns a ServiceDecoratorHook function that 129 // can be attached the workflow to create a dashboard 130 func DashboardDecorator(lambdaAWSInfo []*sparta.LambdaAWSInfo, 131 timeSeriesPeriod int) sparta.ServiceDecoratorHookFunc { 132 return func(context map[string]interface{}, 133 serviceName string, 134 cfTemplate *gocf.Template, 135 S3Bucket string, 136 S3Key string, 137 buildID string, 138 awsSession *session.Session, 139 noop bool, 140 logger *logrus.Logger) error { 141 142 lambdaFunctions := make([]*LambdaTemplateData, len(lambdaAWSInfo)) 143 for index, eachLambda := range lambdaAWSInfo { 144 lambdaFunctions[index] = &LambdaTemplateData{ 145 LambdaAWSInfo: eachLambda, 146 ResourceName: eachLambda.LogicalResourceName(), 147 } 148 } 149 dashboardTemplateData := &DashboardTemplateData{ 150 SpartaVersion: sparta.SpartaVersion, 151 SpartaGitHash: sparta.SpartaGitHash, 152 LambdaFunctions: lambdaFunctions, 153 TimeSeriesPeriod: timeSeriesPeriod, 154 Extents: widgetExtents{ 155 HeaderWidthUnits: headerWidthUnits, 156 HeaderHeightUnits: headerHeightUnits, 157 MetricWidthUnits: metricWidthUnits, 158 MetricHeightUnits: metricHeightUnits, 159 MetricsPerRow: metricsPerRow, 160 }, 161 } 162 163 dashboardTmpl, dashboardTmplErr := template.New("dashboard"). 164 Delims("<<", ">>"). 165 Funcs(templateFuncMap). 166 Parse(dashboardTemplate) 167 if nil != dashboardTmplErr { 168 return dashboardTmplErr 169 } 170 var templateResults bytes.Buffer 171 evalResultErr := dashboardTmpl.Execute(&templateResults, dashboardTemplateData) 172 if nil != evalResultErr { 173 return evalResultErr 174 } 175 176 // Raw template output 177 logger.WithFields(logrus.Fields{ 178 "Dashboard": templateResults.String(), 179 }).Debug("CloudWatch Dashboard template result") 180 181 // Replace any multiline backtick newlines with nothing, since otherwise 182 // the Fn::Joined JSON will be malformed 183 reReplace, reReplaceErr := regexp.Compile("\n") 184 if nil != reReplaceErr { 185 return reReplaceErr 186 } 187 escapedBytes := reReplace.ReplaceAll(templateResults.Bytes(), []byte("")) 188 logger.WithFields(logrus.Fields{ 189 "Dashboard": string(escapedBytes), 190 }).Debug("CloudWatch Dashboard post cleanup") 191 192 // Super, now parse this into an Fn::Join representation 193 // so that we can get inline expansion of the AWS pseudo params 194 templateReader := bytes.NewReader(escapedBytes) 195 templateExpr, templateExprErr := spartaCF.ConvertToTemplateExpression(templateReader, nil) 196 if nil != templateExprErr { 197 return templateExprErr 198 } 199 200 dashboardResource := gocf.CloudWatchDashboard{} 201 dashboardResource.DashboardBody = templateExpr 202 dashboardResource.DashboardName = gocf.String(serviceName) 203 dashboardName := sparta.CloudFormationResourceName("Dashboard", "Dashboard") 204 cfTemplate.AddResource(dashboardName, &dashboardResource) 205 206 // Add the output 207 cfTemplate.Outputs[OutputDashboardURL] = &gocf.Output{ 208 Description: "CloudWatch Dashboard URL", 209 Value: gocf.Join("", 210 gocf.String("https://"), 211 gocf.Ref("AWS::Region"), 212 gocf.String(".console.aws.amazon.com/cloudwatch/home?region="), 213 gocf.Ref("AWS::Region"), 214 gocf.String("#dashboards:name="), 215 gocf.Ref(dashboardName)), 216 } 217 return nil 218 } 219 }