github.com/mweagle/Sparta@v1.15.0/profile_loop_build.go (about) 1 // +build !lambdabinary 2 3 package sparta 4 5 import ( 6 "fmt" 7 "os" 8 "path" 9 "path/filepath" 10 "sort" 11 "strings" 12 "time" 13 14 survey "github.com/AlecAivazis/survey/v2" 15 "github.com/aws/aws-sdk-go/aws" 16 "github.com/aws/aws-sdk-go/aws/session" 17 "github.com/aws/aws-sdk-go/service/s3" 18 "github.com/aws/aws-sdk-go/service/s3/s3manager" 19 "github.com/google/pprof/driver" 20 "github.com/google/pprof/profile" 21 spartaAWS "github.com/mweagle/Sparta/aws" 22 spartaCF "github.com/mweagle/Sparta/aws/cloudformation" 23 gocf "github.com/mweagle/go-cloudformation" 24 "github.com/pkg/errors" 25 "github.com/sirupsen/logrus" 26 ) 27 28 type userAnswers struct { 29 StackName string `survey:"stackName"` 30 StackInstance string 31 ProfileType string `survey:"profileType"` 32 DownloadNewSnapshots string `survey:"downloadNewSnapshots"` 33 ProfileOptions []string 34 RefreshSnapshots bool 35 } 36 37 func cachedProfileNames() []string { 38 globPattern := filepath.Join(ScratchDirectory, "*.profile") 39 matchingFiles, matchingFilesErr := filepath.Glob(globPattern) 40 if matchingFilesErr != nil { 41 return []string{} 42 } 43 // Just get the base name of the profile... 44 cachedNames := []string{} 45 for _, eachMatch := range matchingFiles { 46 baseName := path.Base(eachMatch) 47 filenameParts := strings.Split(baseName, ".") 48 cachedNames = append(cachedNames, filenameParts[0]) 49 } 50 return cachedNames 51 } 52 53 func askQuestions(userStackName string, stackNameToIDMap map[string]string) (*userAnswers, error) { 54 stackNames := []string{} 55 for eachKey := range stackNameToIDMap { 56 stackNames = append(stackNames, eachKey) 57 } 58 sort.Strings(stackNames) 59 cachedProfiles := cachedProfileNames() 60 sort.Strings(cachedProfiles) 61 62 var qs = []*survey.Question{ 63 { 64 Name: "stackName", 65 Prompt: &survey.Select{ 66 Message: "Which stack would you like to profile:", 67 Options: stackNames, 68 Default: userStackName, 69 }, 70 }, 71 { 72 Name: "profileType", 73 Prompt: &survey.Select{ 74 Message: "What type of profile would you like to view?", 75 Options: profileTypes, 76 Default: profileTypes[0], 77 }, 78 }, 79 } 80 81 // Ask the known questions, figure out if they want to download a new 82 // version of the snapshots... 83 var responses userAnswers 84 responseError := survey.Ask(qs, &responses) 85 if responseError != nil { 86 return nil, responseError 87 } 88 responses.StackInstance = stackNameToIDMap[responses.StackName] 89 90 // Based on the first set, ask whether then want to download a new snapshot 91 cachedProfileExists := strings.Contains(strings.Join(cachedProfiles, " "), responses.ProfileType) 92 93 refreshCacheOptions := []string{} 94 if cachedProfileExists { 95 refreshCacheOptions = append(refreshCacheOptions, "Use cached snapshot") 96 } 97 refreshCacheOptions = append(refreshCacheOptions, "Download new snapshots from S3") 98 var questionsRefresh = []*survey.Question{ 99 { 100 Name: "downloadNewSnapshots", 101 Prompt: &survey.Select{ 102 Message: "What profile snapshot(s) would you like to view?", 103 Options: refreshCacheOptions, 104 Default: refreshCacheOptions[0], 105 }, 106 }, 107 } 108 var refreshAnswers userAnswers 109 refreshQuestionError := survey.Ask(questionsRefresh, &refreshAnswers) 110 if refreshQuestionError != nil { 111 return nil, refreshQuestionError 112 } 113 responses.RefreshSnapshots = (refreshAnswers.DownloadNewSnapshots == "Download new snapshots from S3") 114 115 // Final set of questions regarding heap information 116 // If this is a memory profile, what kind? 117 if responses.ProfileType == "heap" { 118 // the answers will be written to this struct 119 heapAnswers := struct { 120 Type string `survey:"type"` 121 }{} 122 // the questions to ask 123 var heapQuestions = []*survey.Question{ 124 { 125 Name: "type", 126 Prompt: &survey.Select{ 127 Message: "Please select a heap profile type:", 128 Options: []string{"inuse_space", "inuse_objects", "alloc_space", "alloc_objects"}, 129 Default: "inuse_space", 130 }, 131 }, 132 } 133 // perform the questions 134 heapErr := survey.Ask(heapQuestions, &heapAnswers) 135 if heapErr != nil { 136 return nil, heapErr 137 } 138 responses.ProfileOptions = []string{fmt.Sprintf("-%s", heapAnswers.Type)} 139 } 140 return &responses, nil 141 } 142 143 func objectKeysForProfileType(profileType string, 144 stackName string, 145 s3BucketName string, 146 maxCount int64, 147 awsSession *session.Session, 148 logger *logrus.Logger) ([]string, error) { 149 // http://weagle.s3.amazonaws.com/gosparta.io/pprof/SpartaPPropStack/profiles/cpu/cpu.42.profile 150 151 // gosparta.io/pprof/SpartaPPropStack/profiles/cpu/cpu.42.profile 152 // List all these... 153 rootPath := profileSnapshotRootKeypathForType(profileType, stackName) 154 listObjectInput := &s3.ListObjectsInput{ 155 Bucket: aws.String(s3BucketName), 156 // Delimiter: aws.String("/"), 157 Prefix: aws.String(rootPath), 158 MaxKeys: aws.Int64(maxCount), 159 } 160 allItems := []string{} 161 s3Svc := s3.New(awsSession) 162 for { 163 listItemResults, listItemResultsErr := s3Svc.ListObjects(listObjectInput) 164 if listItemResultsErr != nil { 165 return nil, errors.Wrapf(listItemResultsErr, "Attempting to list bucket: %s", s3BucketName) 166 } 167 for _, eachEntry := range listItemResults.Contents { 168 logger.WithFields(logrus.Fields{ 169 "FoundItem": *eachEntry.Key, 170 "Size": *eachEntry.Size, 171 }).Debug("Profile file") 172 } 173 174 for _, eachItem := range listItemResults.Contents { 175 if *eachItem.Size > 0 { 176 allItems = append(allItems, *eachItem.Key) 177 } 178 } 179 if int64(len(allItems)) >= maxCount || listItemResults.NextMarker == nil { 180 return allItems, nil 181 } 182 listObjectInput.Marker = listItemResults.NextMarker 183 } 184 } 185 186 //////////////////////////////////////////////////////////////////////////////// 187 // Type returned from worker pool pulling down S3 snapshots 188 type downloadResult struct { 189 err error 190 localFilePath string 191 } 192 193 func (dr *downloadResult) Error() error { 194 return dr.err 195 } 196 func (dr *downloadResult) Result() interface{} { 197 return dr.localFilePath 198 } 199 200 var _ workResult = (*downloadResult)(nil) 201 202 func downloaderTask(profileType string, 203 stackName string, 204 bucketName string, 205 cacheRootPath string, 206 downloadKey string, 207 s3Service *s3.S3, 208 downloader *s3manager.Downloader, 209 logger *logrus.Logger) taskFunc { 210 211 return func() workResult { 212 downloadInput := &s3.GetObjectInput{ 213 Bucket: aws.String(bucketName), 214 Key: aws.String(downloadKey), 215 } 216 cachedFilename := filepath.Join(cacheRootPath, filepath.Base(downloadKey)) 217 outputFile, outputFileErr := os.Create(cachedFilename) 218 if outputFileErr != nil { 219 return &downloadResult{ 220 err: outputFileErr, 221 } 222 } 223 defer func() { 224 closeErr := outputFile.Close() 225 if closeErr != nil { 226 logger.WithFields(logrus.Fields{ 227 "error": closeErr, 228 }).Warn("Failed to close output file writer") 229 } 230 }() 231 232 _, downloadErr := downloader.Download(outputFile, downloadInput) 233 // If we're all good, delete the one on s3... 234 if downloadErr == nil { 235 deleteObjectInput := &s3.DeleteObjectInput{ 236 Bucket: aws.String(bucketName), 237 Key: aws.String(downloadKey), 238 } 239 _, deleteErr := s3Service.DeleteObject(deleteObjectInput) 240 if deleteErr != nil { 241 logger.WithFields(logrus.Fields{ 242 "Error": deleteErr, 243 }).Warn("Failed to delete S3 profile snapshot") 244 } else { 245 logger.WithFields(logrus.Fields{ 246 "Bucket": bucketName, 247 "Key": downloadKey, 248 }).Debug("Deleted S3 profile") 249 } 250 } 251 return &downloadResult{ 252 err: downloadErr, 253 localFilePath: outputFile.Name(), 254 } 255 } 256 } 257 258 func syncStackProfileSnapshots(profileType string, 259 refreshSnapshots bool, 260 stackName string, 261 stackInstance string, 262 s3BucketName string, 263 awsSession *session.Session, 264 logger *logrus.Logger) ([]string, error) { 265 s3KeyRoot := profileSnapshotRootKeypathForType(profileType, stackName) 266 267 if !refreshSnapshots { 268 cachedProfilePath := cachedAggregatedProfilePath(profileType) 269 // Just used the cached ones... 270 logger.WithFields(logrus.Fields{ 271 "CachedProfile": cachedProfilePath, 272 }).Info("Using cached profiles") 273 274 // Make sure they exist... 275 _, cachedInfoErr := os.Stat(cachedProfilePath) 276 if os.IsNotExist(cachedInfoErr) { 277 return nil, fmt.Errorf("no cache files found for profile type: %s. Please run again and fetch S3 artifacts", profileType) 278 } 279 return []string{cachedProfilePath}, nil 280 } 281 // Rebuild the cache... 282 cacheRoot := cacheDirectoryForProfileType(profileType, stackName) 283 logger.WithFields(logrus.Fields{ 284 "StackName": stackName, 285 "S3Bucket": s3BucketName, 286 "ProfileRootKey": s3KeyRoot, 287 "Type": profileType, 288 "CacheRoot": cacheRoot, 289 }).Info("Refreshing cached profiles") 290 291 removeErr := os.RemoveAll(cacheRoot) 292 if removeErr != nil { 293 return nil, errors.Wrapf(removeErr, "Attempting delete local directory: %s", cacheRoot) 294 } 295 mkdirErr := os.MkdirAll(cacheRoot, os.ModePerm) 296 if nil != mkdirErr { 297 return nil, errors.Wrapf(mkdirErr, "Attempting to create local directory: %s", cacheRoot) 298 } 299 300 // Ok, let's get some user information 301 s3Svc := s3.New(awsSession) 302 downloader := s3manager.NewDownloader(awsSession) 303 downloadKeys, downloadKeysErr := objectKeysForProfileType(profileType, 304 stackName, 305 s3BucketName, 306 1024, 307 awsSession, 308 logger) 309 310 if downloadKeys != nil { 311 return nil, errors.Wrapf(downloadKeysErr, 312 "Failed to determine pprof download keys") 313 } 314 downloadTasks := make([]*workTask, len(downloadKeys)) 315 for index, eachKey := range downloadKeys { 316 taskFunc := downloaderTask(profileType, 317 stackName, 318 s3BucketName, 319 cacheRoot, 320 eachKey, 321 s3Svc, 322 downloader, 323 logger) 324 downloadTasks[index] = newWorkTask(taskFunc) 325 } 326 p := newWorkerPool(downloadTasks, 8) 327 results, runErrors := p.Run() 328 if len(runErrors) > 0 { 329 return nil, fmt.Errorf("errors reported: %#v", runErrors) 330 } 331 332 // Read them all and merge them into a single profile... 333 var accumulatedProfiles []*profile.Profile 334 for _, eachResult := range results { 335 profileFile := eachResult.(string) 336 /* #nosec */ 337 profileInput, profileInputErr := os.Open(profileFile) 338 if profileInputErr != nil { 339 return nil, profileInputErr 340 } 341 parsedProfile, parsedProfileErr := profile.Parse(profileInput) 342 // Ignore broken profiles 343 if parsedProfileErr != nil { 344 logger.WithFields(logrus.Fields{ 345 "Path": eachResult, 346 "Error": parsedProfileErr, 347 }).Warn("Invalid cached profile") 348 } else { 349 logger.WithFields(logrus.Fields{ 350 "Input": profileFile, 351 }).Info("Aggregating profile") 352 accumulatedProfiles = append(accumulatedProfiles, parsedProfile) 353 profileInputCloseErr := profileInput.Close() 354 if profileInputCloseErr != nil { 355 logger.WithFields(logrus.Fields{ 356 "error": profileInputCloseErr, 357 }).Warn("Failed to close profile file writer") 358 } 359 } 360 } 361 logger.WithFields(logrus.Fields{ 362 "ProfileCount": len(accumulatedProfiles), 363 }).Info("Consolidating profiles") 364 365 if len(accumulatedProfiles) <= 0 { 366 return nil, fmt.Errorf("unable to find %s snapshots in s3://%s for profile type: %s", 367 stackName, 368 s3BucketName, 369 profileType) 370 } 371 372 // Great, merge them all 373 consolidatedProfile, consolidatedProfileErr := profile.Merge(accumulatedProfiles) 374 if consolidatedProfileErr != nil { 375 return nil, fmt.Errorf("failed to merge profiles: %s", consolidatedProfileErr.Error()) 376 } 377 // Write it out as the "canonical" path... 378 consolidatedPath := cachedAggregatedProfilePath(profileType) 379 logger.WithFields(logrus.Fields{ 380 "ConsolidatedProfile": consolidatedPath, 381 }).Info("Creating consolidated profile") 382 383 outputFile, outputFileErr := os.Create(consolidatedPath) 384 if outputFileErr != nil { 385 return nil, errors.Wrapf(outputFileErr, 386 "failed to create consolidated file: %s", consolidatedPath) 387 } 388 writeErr := consolidatedProfile.Write(outputFile) 389 if writeErr != nil { 390 return nil, errors.Wrapf(writeErr, 391 "failed to write profile: %s", consolidatedPath) 392 } 393 394 // Delete all the other ones, just return the consolidated one... 395 for _, eachResult := range results { 396 unlinkErr := os.Remove(eachResult.(string)) 397 if unlinkErr != nil { 398 logger.WithFields(logrus.Fields{ 399 "File": consolidatedPath, 400 "Error": unlinkErr, 401 }).Info("Failed to delete file") 402 } 403 outputFileErr := outputFile.Close() 404 if outputFileErr != nil { 405 logger.WithFields(logrus.Fields{ 406 "Error": outputFileErr, 407 }).Info("Failed to close output file") 408 } 409 } 410 return []string{consolidatedPath}, nil 411 } 412 413 // Profile is the interactive command used to pull S3 assets locally into /tmp 414 // and run ppro against the cached profiles 415 func Profile(serviceName string, 416 serviceDescription string, 417 s3BucketName string, 418 httpPort int, 419 logger *logrus.Logger) error { 420 421 awsSession := spartaAWS.NewSession(logger) 422 423 // Get the currently active stacks... 424 // Ref: http://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/using-cfn-describing-stacks.html#w2ab2c15c15c17c11 425 stackSummaries, stackSummariesErr := spartaCF.ListStacks(awsSession, 1024, "CREATE_COMPLETE", 426 "UPDATE_COMPLETE", 427 "UPDATE_ROLLBACK_COMPLETE") 428 429 if stackSummariesErr != nil { 430 return stackSummariesErr 431 } 432 // Get the stack names 433 stackNameToIDMap := make(map[string]string) 434 for _, eachSummary := range stackSummaries { 435 stackNameToIDMap[*eachSummary.StackName] = *eachSummary.StackId 436 } 437 responses, responsesErr := askQuestions(serviceName, stackNameToIDMap) 438 if responsesErr != nil { 439 return responsesErr 440 } 441 442 // What does the user want to view? 443 tempFilePaths, tempFilePathsErr := syncStackProfileSnapshots(responses.ProfileType, 444 responses.RefreshSnapshots, 445 responses.StackName, 446 responses.StackInstance, 447 s3BucketName, 448 awsSession, 449 logger) 450 if tempFilePathsErr != nil { 451 return tempFilePathsErr 452 } 453 // We can't hook the PProf webserver, so put some friendly output 454 logger.Info(fmt.Sprintf("Starting pprof webserver on http://localhost:%d. Enter Ctrl+C to exit.", httpPort)) 455 456 // Startup a server we manage s.t we can gracefully exit.. 457 newArgs := []string{os.Args[0]} 458 newArgs = append(newArgs, responses.ProfileOptions...) 459 newArgs = append(newArgs, "-http", fmt.Sprintf(":%d", httpPort), os.Args[0]) 460 newArgs = append(newArgs, tempFilePaths...) 461 os.Args = newArgs 462 return driver.PProf(&driver.Options{}) 463 } 464 465 // ScheduleProfileLoop installs a profiling loop that pushes profile information 466 // to S3 for local consumption using a `profile` command that wraps 467 // pprof 468 func ScheduleProfileLoop(s3BucketArchive interface{}, 469 snapshotInterval time.Duration, 470 cpuProfileDuration time.Duration, 471 profileNames ...string) { 472 473 // When we're building, we want a template decorator that will be called 474 // by `provision`. This decorator will be responsible for: 475 // ensuring each function has IAM creds (if the role isn't a string) 476 // to write to the profile location and also pushing the 477 // Stack name info as reseved environment variables into the function 478 // execution context so that the AWS lambda version of this function 479 // can quickly lookup the StackName and instance information ... 480 profileDecorator = func(stackName string, info *LambdaAWSInfo, S3Bucket string, logger *logrus.Logger) error { 481 // If we have a role definition, ensure the function has rights to upload 482 // to that bucket, with the limited ARN key 483 logger.WithFields(logrus.Fields{ 484 "Function": info.lambdaFunctionName(), 485 }).Info("Instrumenting function for profiling") 486 487 // The bucket is either a literal or a gocf.StringExpr - which one? 488 var bucketValue gocf.Stringable 489 if s3BucketArchive != nil { 490 bucketValue = spartaCF.DynamicValueToStringExpr(s3BucketArchive) 491 } else { 492 bucketValue = gocf.String(S3Bucket) 493 } 494 495 // 1. Add the env vars to the map 496 if info.Options.Environment == nil { 497 info.Options.Environment = make(map[string]*gocf.StringExpr) 498 } 499 info.Options.Environment[envVarStackName] = gocf.Ref("AWS::StackName").String() 500 info.Options.Environment[envVarStackInstanceID] = gocf.Ref("AWS::StackId").String() 501 info.Options.Environment[envVarProfileBucketName] = bucketValue.String() 502 503 // Update the IAM role... 504 if info.RoleDefinition != nil { 505 arn := gocf.Join("", 506 gocf.String("arn:aws:s3:::"), 507 bucketValue, 508 gocf.String("/"), 509 gocf.String(profileSnapshotRootKeypath(stackName)), 510 gocf.String("/*")) 511 512 info.RoleDefinition.Privileges = append(info.RoleDefinition.Privileges, IAMRolePrivilege{ 513 Actions: []string{"s3:PutObject"}, 514 Resource: arn.String(), 515 }) 516 } 517 return nil 518 } 519 }