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": "## ![Sparta](https://s3-us-west-2.amazonaws.com/weagle-sparta-public/cloudwatch/SpartaHelmet32.png) { "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": "## ![Sparta](https://mweagle.github.io/SpartaPublicResources/sparta/SpartaHelmet32.png) { "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  }