diff --git a/.env-example b/.env-example index 8033766..c033744 100644 --- a/.env-example +++ b/.env-example @@ -9,3 +9,4 @@ ACME_URL=https://acme-staging.api.letsencrypt.org/directory GITHUB_API_HOST=https://api.github.com GITHUB_API_TOKEN=c3c6280f5c5d504a00765fbc598fbf818b90cec7 WEBHOOK_HOST=https://localhost:3000 +SENTRY_URL= diff --git a/apiserver/common/common.go b/apiserver/common/common.go index 7635bc5..3b46e99 100644 --- a/apiserver/common/common.go +++ b/apiserver/common/common.go @@ -15,6 +15,7 @@ var ( GitHubAPIHost = os.Getenv("GITHUB_API_HOST") GitHubAPIToken = os.Getenv("GITHUB_API_TOKEN") WebhookHost = os.Getenv("WEBHOOK_HOST") + SentryURL = os.Getenv("SENTRY_URL") ) func init() { diff --git a/apiserver/controllers/controllers.go b/apiserver/controllers/controllers.go index 9b633cc..800ef91 100644 --- a/apiserver/controllers/controllers.go +++ b/apiserver/controllers/controllers.go @@ -6,6 +6,8 @@ import ( "net/http" "strings" + "github.com/getsentry/raven-go" + "github.com/nitrous-io/rise-server/apiserver/common" "github.com/nitrous-io/rise-server/apiserver/models/oauthtoken" "github.com/nitrous-io/rise-server/apiserver/models/project" "github.com/nitrous-io/rise-server/apiserver/models/user" @@ -21,6 +23,10 @@ const ( CurrentProjectKey = "current_project" ) +func init() { + raven.SetDSN(common.SentryURL) +} + func CurrentToken(c *gin.Context) *oauthtoken.OauthToken { ti, exists := c.Get(CurrentTokenKey) if ti == nil || !exists { @@ -66,16 +72,24 @@ func InternalServerError(c *gin.Context, err error, msg ...string) { errHash string ) + req := c.Request + if err != nil { + appErr := err if len(msg) > 0 { - errMsg = fmt.Sprintf("%s: %s", msg, err.Error()) - } else { - errMsg = err.Error() + errMsg = fmt.Sprintf("%s: %s", msg[0], err.Error()) } + errMsg = appErr.Error() errHash = fmt.Sprintf("%x", sha1.Sum([]byte(errMsg))) - } - req := c.Request + raven.CaptureError(appErr, map[string]string{ + "app": "api-server", + "error_hash": errHash, + "method": req.Method, + "url": req.URL.String(), + "ip": c.ClientIP(), + }) + } fields := log.Fields{ "req": fmt.Sprintf("%s %s", req.Method, req.URL.String()), diff --git a/apiserver/controllers/deployments/deployments.go b/apiserver/controllers/deployments/deployments.go index 0df09a0..2d8eaae 100644 --- a/apiserver/controllers/deployments/deployments.go +++ b/apiserver/controllers/deployments/deployments.go @@ -41,7 +41,7 @@ func Create(c *gin.Context) { db, err := dbconn.DB() if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to get a db connection") + controllers.InternalServerError(c, err, "failed to obtain a db connection") return } @@ -54,7 +54,7 @@ func Create(c *gin.Context) { if proj.ActiveDeploymentID != nil { var prevDepl deployment.Deployment if err := db.Where("id = ?", proj.ActiveDeploymentID).First(&prevDepl).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to fetch a previous deployment") + controllers.InternalServerError(c, err, "failed to fetch a previous deployment") return } @@ -116,20 +116,20 @@ func Create(c *gin.Context) { if part.FormName() == "payload" { ver, err := proj.NextVersion(db) if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to get next deployment version number") + controllers.InternalServerError(c, err, "failed to get next deployment version number") return } depl.Version = ver if err := db.Create(depl).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to create a deployment record in DB") + controllers.InternalServerError(c, err, "failed to create a deployment record in DB") return } br := bufio.NewReader(part) partHead, err := br.Peek(512) if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to get header from payload") + controllers.InternalServerError(c, err, "failed to get header from payload") return } @@ -152,7 +152,7 @@ func Create(c *gin.Context) { hr := hasher.NewReader(br) if err := s3client.Upload(uploadKey, hr, "", "private"); err != nil { - controllers.InternalServerError(c, err, "deployments: failed to upload to S3") + controllers.InternalServerError(c, err, "failed to upload to S3") return } @@ -162,7 +162,7 @@ func Create(c *gin.Context) { UploadedPath: uploadKey, } if err := db.Create(bun).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to create a raw bundle record in DB") + controllers.InternalServerError(c, err, "failed to create a raw bundle record in DB") return } @@ -174,13 +174,13 @@ func Create(c *gin.Context) { case viaCachedBundle: ver, err := proj.NextVersion(db) if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to get next deployment version number") + controllers.InternalServerError(c, err, "failed to get next deployment version number") return } depl.Version = ver if err := db.Create(depl).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to create a deployment record in DB") + controllers.InternalServerError(c, err, "failed to create a deployment record in DB") return } @@ -206,7 +206,7 @@ func Create(c *gin.Context) { }) return } - controllers.InternalServerError(c, err, "deployments: failed to find a raw bundle") + controllers.InternalServerError(c, err, "failed to find a raw bundle") return } depl.RawBundleID = &bun.ID @@ -253,14 +253,14 @@ func Create(c *gin.Context) { ver, err := proj.NextVersion(db) if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to get next deployment version number") + controllers.InternalServerError(c, err, "failed to get next deployment version number") return } depl.TemplateID = &tmpl.ID depl.Version = ver if err := db.Create(depl).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to create a deployment record in DB") + controllers.InternalServerError(c, err, "deployments: failed to create a deployment record in DB") return } @@ -275,7 +275,7 @@ func Create(c *gin.Context) { UploadedPath: bundlePath, } if err := db.Create(bun).Error; err != nil { - controllers.InternalServerError(c, err, "deployments: failed to create a raw bundle record in DB") + controllers.InternalServerError(c, err, "failed to create a raw bundle record in DB") return } @@ -290,7 +290,7 @@ func Create(c *gin.Context) { } if err := depl.UpdateState(db, deployment.StateUploaded); err != nil { - controllers.InternalServerError(c, err, "deployments: failed to update deployment state to be uploaded") + controllers.InternalServerError(c, err, "failed to update deployment state to be uploaded") return } @@ -309,12 +309,12 @@ func Create(c *gin.Context) { } if err != nil { - controllers.InternalServerError(c, err, "deployments: failed to connect to job queue") + controllers.InternalServerError(c, err, "failed to create a job") return } if err := j.Enqueue(); err != nil { - controllers.InternalServerError(c, err, "deployments: failed to enqueue a job") + controllers.InternalServerError(c, err, "failed to enqueue a job") return } @@ -324,7 +324,7 @@ func Create(c *gin.Context) { } if err := depl.UpdateState(db, newState); err != nil { - controllers.InternalServerError(c, err, "deployments: failed to update deployment state to be "+newState) + controllers.InternalServerError(c, err, "failed to update deployment state to be "+newState) return } @@ -366,7 +366,7 @@ func Show(c *gin.Context) { db, err := dbconn.DB() if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to obtain a db connection") return } @@ -380,7 +380,7 @@ func Show(c *gin.Context) { }) return } - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch the deployment from DB") return } @@ -403,7 +403,7 @@ func Download(c *gin.Context) { db, err := dbconn.DB() if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to obtain a db connection") return } @@ -416,7 +416,7 @@ func Download(c *gin.Context) { }) return } - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch the deployment from DB") return } @@ -437,16 +437,16 @@ func Download(c *gin.Context) { }) return } - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch the raw bundle from DB") return } exists, err := s3client.Exists(bun.UploadedPath) if err != nil { - log.Warnf("failed to check existence of %q on S3, err: %v", bun.UploadedPath, err) - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, fmt.Sprintf("failed to check existence of %q on S3", bun.UploadedPath)) return } + if !exists { log.Warnf("deployment raw bundle %q does not exist in S3", bun.UploadedPath) c.JSON(http.StatusGone, gin.H{ @@ -458,8 +458,7 @@ func Download(c *gin.Context) { url, err := s3client.PresignedURL(bun.UploadedPath, presignExpiryDuration) if err != nil { - log.Printf("error generating presigned URL to %q, err: %v", bun.UploadedPath, err) - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, fmt.Sprintf("error generating presigned URL to %q", bun.UploadedPath)) return } @@ -483,13 +482,13 @@ func Rollback(c *gin.Context) { db, err := dbconn.DB() if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to obtain a db connection") return } var currentDepl deployment.Deployment if err := db.First(¤tDepl, *proj.ActiveDeploymentID).Error; err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch an active deployment from DB") return } @@ -497,7 +496,7 @@ func Rollback(c *gin.Context) { if c.PostForm("version") == "" { depl, err = currentDepl.PreviousCompletedDeployment(db) if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch a previous deployment from DB") return } @@ -528,7 +527,7 @@ func Rollback(c *gin.Context) { return } - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch a deployment for specified version from DB") return } @@ -547,17 +546,17 @@ func Rollback(c *gin.Context) { }) if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to create a job") return } if err := j.Enqueue(); err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to enqueue a job") return } if err := depl.UpdateState(db, deployment.StatePendingRollback); err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to update a deployment to be panding rollback") return } @@ -593,13 +592,13 @@ func Index(c *gin.Context) { db, err := dbconn.DB() if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to obtain a db connection") return } depls, err := deployment.CompletedDeployments(db, proj.ID, proj.MaxDeploysKept) if err != nil { - controllers.InternalServerError(c, err) + controllers.InternalServerError(c, err, "failed to fetch completed deployments from DB") return } diff --git a/builder/builder.go b/builder/builder.go index e63f97c..8a5925e 100644 --- a/builder/builder.go +++ b/builder/builder.go @@ -6,6 +6,8 @@ import ( "syscall" "time" + "github.com/getsentry/raven-go" + "github.com/nitrous-io/rise-server/apiserver/common" "github.com/nitrous-io/rise-server/builder/builder" "github.com/nitrous-io/rise-server/pkg/mqconn" "github.com/nitrous-io/rise-server/shared/queues" @@ -15,6 +17,8 @@ import ( ) func main() { + raven.SetDSN(common.SentryURL) + run() os.Exit(1) } @@ -94,6 +98,13 @@ func run() { // failure log.Warnln("Work failed", err, string(d.Body)) + if err != builder.ErrProjectLocked { + raven.CaptureError(err, map[string]string{ + "app": "builder", + "body": string(d.Body), + }) + } + if err == builder.ErrRecordNotFound || err == builder.ErrUnarchiveFailed { if err := d.Ack(false); err != nil { log.WithFields(log.Fields{"queue": queueName}).Warnln("Failed to Ack message:", err) diff --git a/builder/builder/builder.go b/builder/builder/builder.go index 000dd9c..971a870 100644 --- a/builder/builder/builder.go +++ b/builder/builder/builder.go @@ -52,7 +52,7 @@ func init() { var ( S3 filetransfer.FileTransfer = filetransfer.NewS3(s3client.PartSize, s3client.MaxUploadParts) - errUnexpectedState = errors.New("deployment is in unexpected state") + ErrUnexpectedState = errors.New("deployment is in unexpected state") ErrProjectLocked = errors.New("project is locked") ErrOptimizerTimeout = errors.New("Timed out on optimizing assets. This might happen due to too large asset files. We will continue without optimizing your assets.") ErrRecordNotFound = errors.New("project or deployment is deleted") @@ -68,12 +68,12 @@ var ( func Work(data []byte) error { d := &messages.BuildJobData{} if err := json.Unmarshal(data, d); err != nil { - return err + return fmt.Errorf("failed to unmarshal payload %s: %v", string(data), err) } db, err := dbconn.DB() if err != nil { - return err + return fmt.Errorf("failed to obtain a db connection %s: %v", string(data), err) } depl := &deployment.Deployment{} @@ -81,7 +81,7 @@ func Work(data []byte) error { if err == gorm.RecordNotFound { return ErrRecordNotFound } - return err + return fmt.Errorf("failed to fetch a deployment %d: %v", d.DeploymentID, err) } proj := &project.Project{} @@ -89,12 +89,12 @@ func Work(data []byte) error { if err == gorm.RecordNotFound { return ErrRecordNotFound } - return err + return fmt.Errorf("failed to fetch a project %d: %v", depl.ProjectID, err) } acquired, err := proj.Lock(db) if err != nil { - return err + return fmt.Errorf("failed to acquire a project lock: %v", err) } if !acquired { @@ -108,7 +108,7 @@ func Work(data []byte) error { }() if depl.State != deployment.StatePendingBuild { - return errUnexpectedState + return ErrUnexpectedState } // There are 2 possible sources for the bundle (i.e. the files to be @@ -138,7 +138,7 @@ func Work(data []byte) error { f, err := ioutil.TempFile("", prefixID+"-raw-bundle."+archiveFormat) if err != nil { - return err + return fmt.Errorf("failed to create a temp file: %v", err) } defer func() { f.Close() @@ -147,12 +147,12 @@ func Work(data []byte) error { dirName, err := ioutil.TempDir("", prefixID) if err != nil { - return err + return fmt.Errorf("failed to create a temp directory: %v", err) } defer os.RemoveAll(dirName) if err := S3.Download(s3client.BucketRegion, s3client.BucketName, bundlePath, f); err != nil { - return err + return fmt.Errorf("failed to download the bundle %s from S3: %v", bundlePath, err) } if archiveFormat == "tar.gz" { @@ -169,7 +169,7 @@ func Work(data []byte) error { if err == io.EOF { break } - return err + return fmt.Errorf("failed to read the bundle file: %v", err) } if hdr.FileInfo().IsDir() { @@ -178,19 +178,19 @@ func Work(data []byte) error { folderPath := path.Dir(hdr.Name) if err := os.MkdirAll(filepath.Join(dirName, folderPath), 0755); err != nil { - return err + return fmt.Errorf("failed to create a folder %s: %v", folderPath, err) } fileName := path.Clean(hdr.Name) targetFileName := filepath.Join(dirName, fileName) entry, err := os.Create(targetFileName) if err != nil { - return err + return fmt.Errorf("failed to create a file %s: %v", targetFileName, err) } defer entry.Close() if _, err := io.Copy(entry, tr); err != nil { - return err + return fmt.Errorf("failed to write to a file %s: %v", targetFileName, err) } entry.Close() @@ -215,19 +215,19 @@ func Work(data []byte) error { folderPath := path.Dir(file.Name) if err := os.MkdirAll(filepath.Join(dirName, folderPath), 0755); err != nil { - return err + return fmt.Errorf("failed to create a folder %s: %v", folderPath, err) } fileName := path.Clean(file.Name) targetFileName := filepath.Join(dirName, fileName) entry, err := os.Create(targetFileName) if err != nil { - return err + return fmt.Errorf("failed to create a file %s: %v", targetFileName, err) } defer entry.Close() if _, err := io.Copy(entry, rc); err != nil { - return err + return fmt.Errorf("failed to write to a file %s: %v", targetFileName, err) } entry.Close() @@ -236,7 +236,7 @@ func Work(data []byte) error { optimizedBundleArchive, err := ioutil.TempFile("", "optimized-bundle."+archiveFormat) if err != nil { - return err + return fmt.Errorf("failed to create a temp file: %v", err) } defer os.Remove(optimizedBundleArchive.Name()) @@ -250,7 +250,7 @@ func Work(data []byte) error { // Optimize assets domainNames, err := proj.DomainNamesWithProtocol(db) if err != nil { - return err + return fmt.Errorf("failed to get domain names from project %s: %v", proj.Name, err) } output, err := runOptimizer(fmt.Sprintf("%s-%d", prefixID, time.Now().Unix()), dirName, domainNames) @@ -270,41 +270,37 @@ func Work(data []byte) error { } if err := pack(optimizedBundleArchive, dirName, archiveFormat); err != nil { - return err + return fmt.Errorf("failed to compress optimize assets: %v", err) } if err := S3.Upload(s3client.BucketRegion, s3client.BucketName, "deployments/"+prefixID+"/optimized-bundle."+archiveFormat, optimizedBundleArchive, "", "private"); err != nil { - return err + return fmt.Errorf("failed to optimized bundle to S3: %v", err) } } else if err == ErrOptimizerTimeout { - if err := depl.UpdateState(db, deployment.StateBuildFailed); err != nil { - return err - } - nextState = deployment.StateBuildFailed errorMessage := ErrOptimizerTimeout.Error() depl.ErrorMessage = &errorMessage deployJobMsg.UseRawBundle = true } else { - return err + return fmt.Errorf("failed to optimize assets: %v", err) } if err := depl.UpdateState(db, nextState); err != nil { - return err + return fmt.Errorf("failed to update the deployment to be %s: %v", nextState, err) } j, err := job.NewWithJSON(queues.Deploy, &deployJobMsg) if err != nil { - return err + return fmt.Errorf("failed to create a job: %v", err) } if err := j.Enqueue(); err != nil { - return err + return fmt.Errorf("failed to enqueue a job: %v", err) } if err := depl.UpdateState(db, deployment.StatePendingDeploy); err != nil { - return err + return fmt.Errorf("failed to update the deployment to be pending_deploy: %v", err) } return nil diff --git a/deployer/deployer.go b/deployer/deployer.go index 2d8b87b..6e4c694 100644 --- a/deployer/deployer.go +++ b/deployer/deployer.go @@ -6,6 +6,8 @@ import ( "syscall" "time" + "github.com/getsentry/raven-go" + "github.com/nitrous-io/rise-server/apiserver/common" "github.com/nitrous-io/rise-server/deployer/deployer" "github.com/nitrous-io/rise-server/pkg/mqconn" "github.com/nitrous-io/rise-server/shared/queues" @@ -15,6 +17,8 @@ import ( ) func main() { + raven.SetDSN(common.SentryURL) + run() os.Exit(1) } @@ -92,12 +96,20 @@ func run() { for { select { case d := <-msgCh: + log.Infoln("Work started", string(d.Body)) err = deployer.Work(d.Body) if err != nil { // failure log.Warnln("Work failed", err, string(d.Body)) + if err != deployer.ErrProjectLocked { + raven.CaptureError(err, map[string]string{ + "app": "deployer", + "body": string(d.Body), + }) + } + // It does not retry for timeout or record not found error or unarchive failed // because it could retry for long time. if err == deployer.ErrTimeout || diff --git a/deployer/deployer/deployer.go b/deployer/deployer/deployer.go index 943733d..2a1e4d1 100644 --- a/deployer/deployer/deployer.go +++ b/deployer/deployer/deployer.go @@ -36,6 +36,9 @@ import ( ) var ( + S3 filetransfer.FileTransfer = filetransfer.NewS3(s3client.PartSize, s3client.MaxUploadParts) + + ErrUnexpectedState = errors.New("deployment is in unexpected state") ErrProjectLocked = errors.New("project is locked") ErrRecordNotFound = errors.New("project or deployment is deleted") ErrTimeout = errors.New("failed to upload files due to timeout on uploading to s3") @@ -70,21 +73,15 @@ func init() { mimetypes.Register() } -var ( - S3 filetransfer.FileTransfer = filetransfer.NewS3(s3client.PartSize, s3client.MaxUploadParts) - - errUnexpectedState = errors.New("deployment is in unexpected state") -) - func Work(data []byte) error { d := &messages.DeployJobData{} if err := json.Unmarshal(data, d); err != nil { - return err + return fmt.Errorf("failed to unmarshal payload %s: %v", string(data), err) } db, err := dbconn.DB() if err != nil { - return err + return fmt.Errorf("failed to obtain a db connection %s: %v", string(data), err) } depl := &deployment.Deployment{} @@ -92,7 +89,7 @@ func Work(data []byte) error { if err == gorm.RecordNotFound { return ErrRecordNotFound } - return err + return fmt.Errorf("failed to fetch a deployment %d: %v", d.DeploymentID, err) } proj := &project.Project{} @@ -100,12 +97,12 @@ func Work(data []byte) error { if err == gorm.RecordNotFound { return ErrRecordNotFound } - return err + return fmt.Errorf("failed to fetch a project %d: %v", depl.ProjectID, err) } acquired, err := proj.Lock(db) if err != nil { - return err + return fmt.Errorf("failed to acquire a project lock: %v", err) } if !acquired { @@ -120,7 +117,7 @@ func Work(data []byte) error { // Return error if the deployment is in a state that bundle is not uploaded or not prepared for deploying if depl.State == deployment.StateUploaded || depl.State == deployment.StatePendingUpload { - return errUnexpectedState + return ErrUnexpectedState } prefixID := depl.PrefixID() @@ -128,7 +125,7 @@ func Work(data []byte) error { if !d.SkipWebrootUpload { // Disallow re-deploying a deployed project. if depl.State == deployment.StateDeployed { - return errUnexpectedState + return ErrUnexpectedState } archiveFormat := d.ArchiveFormat @@ -153,7 +150,7 @@ func Work(data []byte) error { f, err := ioutil.TempFile("", prefixID+"-optimized-bundle."+archiveFormat) if err != nil { - return err + return fmt.Errorf("failed to create a temp file: %v", err) } defer func() { f.Close() @@ -161,7 +158,7 @@ func Work(data []byte) error { }() if err := S3.Download(s3client.BucketRegion, s3client.BucketName, bundlePath, f); err != nil { - return err + return fmt.Errorf("failed to download the bundle %s from S3: %v", bundlePath, err) } // webroot is a publicly readable directory on S3. @@ -315,7 +312,7 @@ func Work(data []byte) error { var envvars map[string]string if err := json.Unmarshal(depl.JsEnvVars, &envvars); err != nil { - return err + return fmt.Errorf("failed to unmarshal js environment variables %s: %v", string(depl.JsEnvVars), err) } if err := S3.Upload(s3client.BucketRegion, @@ -324,7 +321,7 @@ func Work(data []byte) error { bytes.NewBufferString(fmt.Sprintf(jsenvFormat, depl.JsEnvVars)), "application/javascript", "public-read"); err != nil { - return err + return fmt.Errorf("failed to upload jsenv.js to S3: %v", err) } } @@ -342,12 +339,12 @@ func Work(data []byte) error { }) if err != nil { - return err + return fmt.Errorf("failed to mashal meta.json: %v", err) } domainNames, err := proj.DomainNames(db) if err != nil { - return err + return fmt.Errorf("failed to get domain names: %v", err) } // Upload metadata file for each domain. @@ -355,7 +352,7 @@ func Work(data []byte) error { for _, domain := range domainNames { reader.Seek(0, 0) if err := S3.Upload(s3client.BucketRegion, s3client.BucketName, "domains/"+domain+"/meta.json", reader, "application/json", "public-read"); err != nil { - return err + return fmt.Errorf("failed to upload meta.json to S3: %v", err) } } @@ -364,11 +361,11 @@ func Work(data []byte) error { Domains: domainNames, }) if err != nil { - return err + return fmt.Errorf("failed to create a message for invalidate cache: %v", err) } if err := m.Publish(); err != nil { - return err + return fmt.Errorf("failed to publish invalidate cache message: %v", err) } } @@ -379,18 +376,18 @@ func Work(data []byte) error { defer tx.Rollback() if err := depl.UpdateState(tx, deployment.StateDeployed); err != nil { - return err + return fmt.Errorf("failed to update the deployment to be deployed: %v", err) } if err := tx.Model(project.Project{}).Where("id = ?", proj.ID).Update("active_deployment_id", &depl.ID).Error; err != nil { - return err + return fmt.Errorf("failed to update active deployment: %v", err) } // If project has exceeded its max number of deployments (N), we soft delete // deployments older than the last N deployments. if proj.MaxDeploysKept > 0 { if err := deployment.DeleteExceptLastN(tx, proj.ID, proj.MaxDeploysKept); err != nil { - return err + return fmt.Errorf("failed to purge old deployments: %v", err) } } diff --git a/vendor/github.com/getsentry/raven-go/LICENSE b/vendor/github.com/getsentry/raven-go/LICENSE new file mode 100644 index 0000000..b0301b5 --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/LICENSE @@ -0,0 +1,28 @@ +Copyright (c) 2013 Apollic Software, LLC. All rights reserved. +Copyright (c) 2015 Functional Software, Inc. All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are +met: + + * Redistributions of source code must retain the above copyright +notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above +copyright notice, this list of conditions and the following disclaimer +in the documentation and/or other materials provided with the +distribution. + * Neither the name of Apollic Software, LLC nor the names of its +contributors may be used to endorse or promote products derived from +this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/vendor/github.com/getsentry/raven-go/README.md b/vendor/github.com/getsentry/raven-go/README.md new file mode 100644 index 0000000..553c0a0 --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/README.md @@ -0,0 +1,13 @@ +# raven [![Build Status](https://travis-ci.org/getsentry/raven-go.png?branch=master)](https://travis-ci.org/getsentry/raven-go) + +raven is a Go client for the [Sentry](https://github.com/getsentry/sentry) +event/error logging system. + +- [**API Documentation**](https://godoc.org/github.com/getsentry/raven-go) +- [**Usage and Examples**](https://docs.getsentry.com/hosted/clients/go/) + +## Installation + +```text +go get github.com/getsentry/raven-go +``` diff --git a/vendor/github.com/getsentry/raven-go/client.go b/vendor/github.com/getsentry/raven-go/client.go new file mode 100644 index 0000000..0a5e1e8 --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/client.go @@ -0,0 +1,724 @@ +// Package raven implements a client for the Sentry error logging service. +package raven + +import ( + "bytes" + "compress/zlib" + "crypto/rand" + "encoding/base64" + "encoding/hex" + "encoding/json" + "errors" + "fmt" + "io" + "io/ioutil" + "net/http" + "net/url" + "os" + "runtime" + "strings" + "sync" + "time" +) + +const ( + userAgent = "raven-go/1.0" + timestampFormat = `"2006-01-02T15:04:05.00"` +) + +var ( + ErrPacketDropped = errors.New("raven: packet dropped") + ErrUnableToUnmarshalJSON = errors.New("raven: unable to unmarshal JSON") + ErrMissingUser = errors.New("raven: dsn missing public key and/or password") + ErrMissingPrivateKey = errors.New("raven: dsn missing private key") + ErrMissingProjectID = errors.New("raven: dsn missing project id") +) + +type Severity string + +// http://docs.python.org/2/howto/logging.html#logging-levels +const ( + DEBUG = Severity("debug") + INFO = Severity("info") + WARNING = Severity("warning") + ERROR = Severity("error") + FATAL = Severity("fatal") +) + +type Timestamp time.Time + +func (t Timestamp) MarshalJSON() ([]byte, error) { + return []byte(time.Time(t).UTC().Format(timestampFormat)), nil +} + +func (timestamp *Timestamp) UnmarshalJSON(data []byte) error { + t, err := time.Parse(timestampFormat, string(data)) + if err != nil { + return err + } + + *timestamp = Timestamp(t) + return nil +} + +// An Interface is a Sentry interface that will be serialized as JSON. +// It must implement json.Marshaler or use json struct tags. +type Interface interface { + // The Sentry class name. Example: sentry.interfaces.Stacktrace + Class() string +} + +type Culpriter interface { + Culprit() string +} + +type Transport interface { + Send(url, authHeader string, packet *Packet) error +} + +type outgoingPacket struct { + packet *Packet + ch chan error +} + +type Tag struct { + Key string + Value string +} + +type Tags []Tag + +func (tag *Tag) MarshalJSON() ([]byte, error) { + return json.Marshal([2]string{tag.Key, tag.Value}) +} + +func (t *Tag) UnmarshalJSON(data []byte) error { + var tag [2]string + if err := json.Unmarshal(data, &tag); err != nil { + return err + } + *t = Tag{tag[0], tag[1]} + return nil +} + +func (t *Tags) UnmarshalJSON(data []byte) error { + var tags []Tag + + switch data[0] { + case '[': + // Unmarshal into []Tag + if err := json.Unmarshal(data, &tags); err != nil { + return err + } + case '{': + // Unmarshal into map[string]string + tagMap := make(map[string]string) + if err := json.Unmarshal(data, &tagMap); err != nil { + return err + } + + // Convert to []Tag + for k, v := range tagMap { + tags = append(tags, Tag{k, v}) + } + default: + return ErrUnableToUnmarshalJSON + } + + *t = tags + return nil +} + +// https://docs.getsentry.com/hosted/clientdev/#building-the-json-packet +type Packet struct { + // Required + Message string `json:"message"` + + // Required, set automatically by Client.Send/Report via Packet.Init if blank + EventID string `json:"event_id"` + Project string `json:"project"` + Timestamp Timestamp `json:"timestamp"` + Level Severity `json:"level"` + Logger string `json:"logger"` + + // Optional + Platform string `json:"platform,omitempty"` + Culprit string `json:"culprit,omitempty"` + ServerName string `json:"server_name,omitempty"` + Release string `json:"release,omitempty"` + Environment string `json:"environment,omitempty"` + Tags Tags `json:"tags,omitempty"` + Modules map[string]string `json:"modules,omitempty"` + Fingerprint []string `json:"fingerprint,omitempty"` + Extra map[string]interface{} `json:"extra,omitempty"` + + Interfaces []Interface `json:"-"` +} + +// NewPacket constructs a packet with the specified message and interfaces. +func NewPacket(message string, interfaces ...Interface) *Packet { + extra := map[string]interface{}{ + "runtime.Version": runtime.Version(), + "runtime.NumCPU": runtime.NumCPU(), + "runtime.GOMAXPROCS": runtime.GOMAXPROCS(0), // 0 just returns the current value + "runtime.NumGoroutine": runtime.NumGoroutine(), + } + return &Packet{ + Message: message, + Interfaces: interfaces, + Extra: extra, + } +} + +// Init initializes required fields in a packet. It is typically called by +// Client.Send/Report automatically. +func (packet *Packet) Init(project string) error { + if packet.Project == "" { + packet.Project = project + } + if packet.EventID == "" { + var err error + packet.EventID, err = uuid() + if err != nil { + return err + } + } + if time.Time(packet.Timestamp).IsZero() { + packet.Timestamp = Timestamp(time.Now()) + } + if packet.Level == "" { + packet.Level = ERROR + } + if packet.Logger == "" { + packet.Logger = "root" + } + if packet.ServerName == "" { + packet.ServerName = hostname + } + if packet.Platform == "" { + packet.Platform = "go" + } + + if packet.Culprit == "" { + for _, inter := range packet.Interfaces { + if c, ok := inter.(Culpriter); ok { + packet.Culprit = c.Culprit() + if packet.Culprit != "" { + break + } + } + } + } + + return nil +} + +func (packet *Packet) AddTags(tags map[string]string) { + for k, v := range tags { + packet.Tags = append(packet.Tags, Tag{k, v}) + } +} + +func uuid() (string, error) { + id := make([]byte, 16) + _, err := io.ReadFull(rand.Reader, id) + if err != nil { + return "", err + } + id[6] &= 0x0F // clear version + id[6] |= 0x40 // set version to 4 (random uuid) + id[8] &= 0x3F // clear variant + id[8] |= 0x80 // set to IETF variant + return hex.EncodeToString(id), nil +} + +func (packet *Packet) JSON() []byte { + packetJSON, _ := json.Marshal(packet) + + interfaces := make(map[string]Interface, len(packet.Interfaces)) + for _, inter := range packet.Interfaces { + if inter != nil { + interfaces[inter.Class()] = inter + } + } + + if len(interfaces) > 0 { + interfaceJSON, _ := json.Marshal(interfaces) + packetJSON[len(packetJSON)-1] = ',' + packetJSON = append(packetJSON, interfaceJSON[1:]...) + } + + return packetJSON +} + +type context struct { + user *User + http *Http + tags map[string]string +} + +func (c *context) SetUser(u *User) { c.user = u } +func (c *context) SetHttp(h *Http) { c.http = h } +func (c *context) SetTags(t map[string]string) { + if c.tags == nil { + c.tags = make(map[string]string) + } + for k, v := range t { + c.tags[k] = v + } +} +func (c *context) Clear() { + c.user = nil + c.http = nil + c.tags = nil +} + +// Return a list of interfaces to be used in appending with the rest +func (c *context) interfaces() []Interface { + len, i := 0, 0 + if c.user != nil { + len++ + } + if c.http != nil { + len++ + } + interfaces := make([]Interface, len) + if c.user != nil { + interfaces[i] = c.user + i++ + } + if c.http != nil { + interfaces[i] = c.http + i++ + } + return interfaces +} + +// The maximum number of packets that will be buffered waiting to be delivered. +// Packets will be dropped if the buffer is full. Used by NewClient. +var MaxQueueBuffer = 100 + +func newClient(tags map[string]string) *Client { + client := &Client{ + Transport: &HTTPTransport{}, + Tags: tags, + context: &context{}, + queue: make(chan *outgoingPacket, MaxQueueBuffer), + } + go client.worker() + client.SetDSN(os.Getenv("SENTRY_DSN")) + return client +} + +// New constructs a new Sentry client instance +func New(dsn string) (*Client, error) { + client := newClient(nil) + return client, client.SetDSN(dsn) +} + +// NewWithTags constructs a new Sentry client instance with default tags. +func NewWithTags(dsn string, tags map[string]string) (*Client, error) { + client := newClient(tags) + return client, client.SetDSN(dsn) +} + +// NewClient constructs a Sentry client and spawns a background goroutine to +// handle packets sent by Client.Report. +// +// Deprecated: use New and NewWithTags instead +func NewClient(dsn string, tags map[string]string) (*Client, error) { + client := newClient(tags) + return client, client.SetDSN(dsn) +} + +// Client encapsulates a connection to a Sentry server. It must be initialized +// by calling NewClient. Modification of fields concurrently with Send or after +// calling Report for the first time is not thread-safe. +type Client struct { + Tags map[string]string + + Transport Transport + + // DropHandler is called when a packet is dropped because the buffer is full. + DropHandler func(*Packet) + + // Context that will get appending to all packets + context *context + + mu sync.RWMutex + url string + projectID string + authHeader string + release string + environment string + includePaths []string + queue chan *outgoingPacket + + // A WaitGroup to keep track of all currently in-progress captures + // This is intended to be used with Client.Wait() to assure that + // all messages have been transported before exiting the process. + wg sync.WaitGroup +} + +// Initialize a default *Client instance +var DefaultClient = newClient(nil) + +// SetDSN updates a client with a new DSN. It safe to call after and +// concurrently with calls to Report and Send. +func (client *Client) SetDSN(dsn string) error { + if dsn == "" { + return nil + } + + client.mu.Lock() + defer client.mu.Unlock() + + uri, err := url.Parse(dsn) + if err != nil { + return err + } + + if uri.User == nil { + return ErrMissingUser + } + publicKey := uri.User.Username() + secretKey, ok := uri.User.Password() + if !ok { + return ErrMissingPrivateKey + } + uri.User = nil + + if idx := strings.LastIndex(uri.Path, "/"); idx != -1 { + client.projectID = uri.Path[idx+1:] + uri.Path = uri.Path[:idx+1] + "api/" + client.projectID + "/store/" + } + if client.projectID == "" { + return ErrMissingProjectID + } + + client.url = uri.String() + + client.authHeader = fmt.Sprintf("Sentry sentry_version=4, sentry_key=%s, sentry_secret=%s", publicKey, secretKey) + + return nil +} + +// Sets the DSN for the default *Client instance +func SetDSN(dsn string) error { return DefaultClient.SetDSN(dsn) } + +// SetRelease sets the "release" tag. +func (client *Client) SetRelease(release string) { + client.mu.Lock() + defer client.mu.Unlock() + client.release = release +} + +// SetEnvironment sets the "environment" tag. +func (client *Client) SetEnvironment(environment string) { + client.mu.Lock() + defer client.mu.Unlock() + client.environment = environment +} + +// SetRelease sets the "release" tag on the default *Client +func SetRelease(release string) { DefaultClient.SetRelease(release) } + +// SetEnvironment sets the "environment" tag on the default *Client +func SetEnvironment(environment string) { DefaultClient.SetEnvironment(environment) } + +func (client *Client) worker() { + for outgoingPacket := range client.queue { + + client.mu.RLock() + url, authHeader := client.url, client.authHeader + client.mu.RUnlock() + + outgoingPacket.ch <- client.Transport.Send(url, authHeader, outgoingPacket.packet) + client.wg.Done() + } +} + +// Capture asynchronously delivers a packet to the Sentry server. It is a no-op +// when client is nil. A channel is provided if it is important to check for a +// send's success. +func (client *Client) Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { + if client == nil { + return + } + + // Keep track of all running Captures so that we can wait for them all to finish + // *Must* call client.wg.Done() on any path that indicates that an event was + // finished being acted upon, whether success or failure + client.wg.Add(1) + + ch = make(chan error, 1) + + // Merge capture tags and client tags + packet.AddTags(captureTags) + packet.AddTags(client.Tags) + packet.AddTags(client.context.tags) + + // Initialize any required packet fields + client.mu.RLock() + projectID := client.projectID + release := client.release + environment := client.environment + client.mu.RUnlock() + + err := packet.Init(projectID) + if err != nil { + ch <- err + client.wg.Done() + return + } + + packet.Release = release + packet.Environment = environment + + outgoingPacket := &outgoingPacket{packet, ch} + + select { + case client.queue <- outgoingPacket: + default: + // Send would block, drop the packet + if client.DropHandler != nil { + client.DropHandler(packet) + } + ch <- ErrPacketDropped + client.wg.Done() + } + + return packet.EventID, ch +} + +// Capture asynchronously delivers a packet to the Sentry server with the default *Client. +// It is a no-op when client is nil. A channel is provided if it is important to check for a +// send's success. +func Capture(packet *Packet, captureTags map[string]string) (eventID string, ch chan error) { + return DefaultClient.Capture(packet, captureTags) +} + +// CaptureMessage formats and delivers a string message to the Sentry server. +func (client *Client) CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { + if client == nil { + return "" + } + + packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) + eventID, _ := client.Capture(packet, tags) + + return eventID +} + +// CaptureMessage formats and delivers a string message to the Sentry server with the default *Client +func CaptureMessage(message string, tags map[string]string, interfaces ...Interface) string { + return DefaultClient.CaptureMessage(message, tags, interfaces...) +} + +// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. +func (client *Client) CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { + if client == nil { + return "" + } + + packet := NewPacket(message, append(append(interfaces, client.context.interfaces()...), &Message{message, nil})...) + eventID, ch := client.Capture(packet, tags) + <-ch + + return eventID +} + +// CaptureMessageAndWait is identical to CaptureMessage except it blocks and waits for the message to be sent. +func CaptureMessageAndWait(message string, tags map[string]string, interfaces ...Interface) string { + return DefaultClient.CaptureMessageAndWait(message, tags, interfaces...) +} + +// CaptureErrors formats and delivers an error to the Sentry server. +// Adds a stacktrace to the packet, excluding the call to this method. +func (client *Client) CaptureError(err error, tags map[string]string, interfaces ...Interface) string { + if client == nil { + return "" + } + + packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...) + eventID, _ := client.Capture(packet, tags) + + return eventID +} + +// CaptureErrors formats and delivers an error to the Sentry server using the default *Client. +// Adds a stacktrace to the packet, excluding the call to this method. +func CaptureError(err error, tags map[string]string, interfaces ...Interface) string { + return DefaultClient.CaptureError(err, tags, interfaces...) +} + +// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent +func (client *Client) CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { + if client == nil { + return "" + } + + packet := NewPacket(err.Error(), append(append(interfaces, client.context.interfaces()...), NewException(err, NewStacktrace(1, 3, client.includePaths)))...) + eventID, ch := client.Capture(packet, tags) + <-ch + + return eventID +} + +// CaptureErrorAndWait is identical to CaptureError, except it blocks and assures that the event was sent +func CaptureErrorAndWait(err error, tags map[string]string, interfaces ...Interface) string { + return DefaultClient.CaptureErrorAndWait(err, tags, interfaces...) +} + +// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. +// If an error is captured, both the error and the reported Sentry error ID are returned. +func (client *Client) CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (err interface{}, errorID string) { + // Note: This doesn't need to check for client, because we still want to go through the defer/recover path + // Down the line, Capture will be noop'd, so while this does a _tiny_ bit of overhead constructing the + // *Packet just to be thrown away, this should not be the normal case. Could be refactored to + // be completely noop though if we cared. + defer func() { + var packet *Packet + err = recover() + switch rval := err.(type) { + case nil: + return + case error: + packet = NewPacket(rval.Error(), append(append(interfaces, client.context.interfaces()...), NewException(rval, NewStacktrace(2, 3, client.includePaths)))...) + default: + rvalStr := fmt.Sprint(rval) + packet = NewPacket(rvalStr, append(append(interfaces, client.context.interfaces()...), NewException(errors.New(rvalStr), NewStacktrace(2, 3, client.includePaths)))...) + } + + errorID, _ = client.Capture(packet, tags) + }() + + f() + return +} + +// CapturePanic calls f and then recovers and reports a panic to the Sentry server if it occurs. +// If an error is captured, both the error and the reported Sentry error ID are returned. +func CapturePanic(f func(), tags map[string]string, interfaces ...Interface) (interface{}, string) { + return DefaultClient.CapturePanic(f, tags, interfaces...) +} + +func (client *Client) Close() { + close(client.queue) +} + +func Close() { DefaultClient.Close() } + +// Wait blocks and waits for all events to finish being sent to Sentry server +func (client *Client) Wait() { + client.wg.Wait() +} + +// Wait blocks and waits for all events to finish being sent to Sentry server +func Wait() { DefaultClient.Wait() } + +func (client *Client) URL() string { + client.mu.RLock() + defer client.mu.RUnlock() + + return client.url +} + +func URL() string { return DefaultClient.URL() } + +func (client *Client) ProjectID() string { + client.mu.RLock() + defer client.mu.RUnlock() + + return client.projectID +} + +func ProjectID() string { return DefaultClient.ProjectID() } + +func (client *Client) Release() string { + client.mu.RLock() + defer client.mu.RUnlock() + + return client.release +} + +func Release() string { return DefaultClient.Release() } + +func IncludePaths() []string { return DefaultClient.IncludePaths() } + +func (client *Client) IncludePaths() []string { + client.mu.RLock() + defer client.mu.RUnlock() + + return client.includePaths +} + +func SetIncludePaths(p []string) { DefaultClient.SetIncludePaths(p) } + +func (client *Client) SetIncludePaths(p []string) { + client.mu.Lock() + defer client.mu.Unlock() + + client.includePaths = p +} + +func (c *Client) SetUserContext(u *User) { c.context.SetUser(u) } +func (c *Client) SetHttpContext(h *Http) { c.context.SetHttp(h) } +func (c *Client) SetTagsContext(t map[string]string) { c.context.SetTags(t) } +func (c *Client) ClearContext() { c.context.Clear() } + +func SetUserContext(u *User) { DefaultClient.SetUserContext(u) } +func SetHttpContext(h *Http) { DefaultClient.SetHttpContext(h) } +func SetTagsContext(t map[string]string) { DefaultClient.SetTagsContext(t) } +func ClearContext() { DefaultClient.ClearContext() } + +// HTTPTransport is the default transport, delivering packets to Sentry via the +// HTTP API. +type HTTPTransport struct { + Http http.Client +} + +func (t *HTTPTransport) Send(url, authHeader string, packet *Packet) error { + if url == "" { + return nil + } + + body, contentType := serializedPacket(packet) + req, _ := http.NewRequest("POST", url, body) + req.Header.Set("X-Sentry-Auth", authHeader) + req.Header.Set("User-Agent", userAgent) + req.Header.Set("Content-Type", contentType) + res, err := t.Http.Do(req) + if err != nil { + return err + } + io.Copy(ioutil.Discard, res.Body) + res.Body.Close() + if res.StatusCode != 200 { + return fmt.Errorf("raven: got http status %d", res.StatusCode) + } + return nil +} + +func serializedPacket(packet *Packet) (r io.Reader, contentType string) { + packetJSON := packet.JSON() + + // Only deflate/base64 the packet if it is bigger than 1KB, as there is + // overhead. + if len(packetJSON) > 1000 { + buf := &bytes.Buffer{} + b64 := base64.NewEncoder(base64.StdEncoding, buf) + deflate, _ := zlib.NewWriterLevel(b64, zlib.BestCompression) + deflate.Write(packetJSON) + deflate.Close() + b64.Close() + return buf, "application/octet-stream" + } + return bytes.NewReader(packetJSON), "application/json" +} + +var hostname string + +func init() { + hostname, _ = os.Hostname() +} diff --git a/vendor/github.com/getsentry/raven-go/exception.go b/vendor/github.com/getsentry/raven-go/exception.go new file mode 100644 index 0000000..4160cdb --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/exception.go @@ -0,0 +1,41 @@ +package raven + +import ( + "reflect" + "regexp" +) + +var errorMsgPattern = regexp.MustCompile(`\A(\w+): (.+)\z`) + +func NewException(err error, stacktrace *Stacktrace) *Exception { + msg := err.Error() + ex := &Exception{ + Stacktrace: stacktrace, + Value: msg, + Type: reflect.TypeOf(err).String(), + } + if m := errorMsgPattern.FindStringSubmatch(msg); m != nil { + ex.Module, ex.Value = m[1], m[2] + } + return ex +} + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces +type Exception struct { + // Required + Value string `json:"value"` + + // Optional + Type string `json:"type,omitempty"` + Module string `json:"module,omitempty"` + Stacktrace *Stacktrace `json:"stacktrace,omitempty"` +} + +func (e *Exception) Class() string { return "exception" } + +func (e *Exception) Culprit() string { + if e.Stacktrace == nil { + return "" + } + return e.Stacktrace.Culprit() +} diff --git a/vendor/github.com/getsentry/raven-go/http.go b/vendor/github.com/getsentry/raven-go/http.go new file mode 100644 index 0000000..32107b8 --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/http.go @@ -0,0 +1,84 @@ +package raven + +import ( + "errors" + "fmt" + "net" + "net/http" + "net/url" + "runtime/debug" + "strings" +) + +func NewHttp(req *http.Request) *Http { + proto := "http" + if req.TLS != nil || req.Header.Get("X-Forwarded-Proto") == "https" { + proto = "https" + } + h := &Http{ + Method: req.Method, + Cookies: req.Header.Get("Cookie"), + Query: sanitizeQuery(req.URL.Query()).Encode(), + URL: proto + "://" + req.Host + req.URL.Path, + Headers: make(map[string]string, len(req.Header)), + } + if addr, port, err := net.SplitHostPort(req.RemoteAddr); err == nil { + h.Env = map[string]string{"REMOTE_ADDR": addr, "REMOTE_PORT": port} + } + for k, v := range req.Header { + h.Headers[k] = strings.Join(v, ",") + } + return h +} + +var querySecretFields = []string{"password", "passphrase", "passwd", "secret"} + +func sanitizeQuery(query url.Values) url.Values { + for _, keyword := range querySecretFields { + for field := range query { + if strings.Contains(field, keyword) { + query[field] = []string{"********"} + } + } + } + return query +} + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces +type Http struct { + // Required + URL string `json:"url"` + Method string `json:"method"` + Query string `json:"query_string,omitempty"` + + // Optional + Cookies string `json:"cookies,omitempty"` + Headers map[string]string `json:"headers,omitempty"` + Env map[string]string `json:"env,omitempty"` + + // Must be either a string or map[string]string + Data interface{} `json:"data,omitempty"` +} + +func (h *Http) Class() string { return "request" } + +// Recovery handler to wrap the stdlib net/http Mux. +// Example: +// http.HandleFunc("/", raven.RecoveryHandler(func(w http.ResponseWriter, r *http.Request) { +// ... +// })) +func RecoveryHandler(handler func(http.ResponseWriter, *http.Request)) func(http.ResponseWriter, *http.Request) { + return func(w http.ResponseWriter, r *http.Request) { + defer func() { + if rval := recover(); rval != nil { + debug.PrintStack() + rvalStr := fmt.Sprint(rval) + packet := NewPacket(rvalStr, NewException(errors.New(rvalStr), NewStacktrace(2, 3, nil)), NewHttp(r)) + Capture(packet, nil) + w.WriteHeader(http.StatusInternalServerError) + } + }() + + handler(w, r) + } +} diff --git a/vendor/github.com/getsentry/raven-go/interfaces.go b/vendor/github.com/getsentry/raven-go/interfaces.go new file mode 100644 index 0000000..a05dc3d --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/interfaces.go @@ -0,0 +1,49 @@ +package raven + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#message-interface +type Message struct { + // Required + Message string `json:"message"` + + // Optional + Params []interface{} `json:"params,omitempty"` +} + +func (m *Message) Class() string { return "logentry" } + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#template-interface +type Template struct { + // Required + Filename string `json:"filename"` + Lineno int `json:"lineno"` + ContextLine string `json:"context_line"` + + // Optional + PreContext []string `json:"pre_context,omitempty"` + PostContext []string `json:"post_context,omitempty"` + AbsolutePath string `json:"abs_path,omitempty"` +} + +func (t *Template) Class() string { return "template" } + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces +type User struct { + // All fields are optional + ID string `json:"id,omitempty"` + Username string `json:"username,omitempty"` + Email string `json:"email,omitempty"` + IP string `json:"ip_address,omitempty"` +} + +func (h *User) Class() string { return "user" } + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#context-interfaces +type Query struct { + // Required + Query string `json:"query"` + + // Optional + Engine string `json:"engine,omitempty"` +} + +func (q *Query) Class() string { return "query" } diff --git a/vendor/github.com/getsentry/raven-go/runtests.sh b/vendor/github.com/getsentry/raven-go/runtests.sh new file mode 100755 index 0000000..9ed279c --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/runtests.sh @@ -0,0 +1,4 @@ +#!/bin/bash +go test -race ./... +go test -cover ./... +go test -v ./... diff --git a/vendor/github.com/getsentry/raven-go/stacktrace.go b/vendor/github.com/getsentry/raven-go/stacktrace.go new file mode 100644 index 0000000..81ca3af --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/stacktrace.go @@ -0,0 +1,213 @@ +// Copyright 2011 The Go Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. +// Some code from the runtime/debug package of the Go standard library. + +package raven + +import ( + "bytes" + "go/build" + "io/ioutil" + "path/filepath" + "runtime" + "strings" + "sync" +) + +// https://docs.getsentry.com/hosted/clientdev/interfaces/#failure-interfaces +type Stacktrace struct { + // Required + Frames []*StacktraceFrame `json:"frames"` +} + +func (s *Stacktrace) Class() string { return "stacktrace" } + +func (s *Stacktrace) Culprit() string { + for i := len(s.Frames) - 1; i >= 0; i-- { + frame := s.Frames[i] + if frame.InApp == true && frame.Module != "" && frame.Function != "" { + return frame.Module + "." + frame.Function + } + } + return "" +} + +type StacktraceFrame struct { + // At least one required + Filename string `json:"filename,omitempty"` + Function string `json:"function,omitempty"` + Module string `json:"module,omitempty"` + + // Optional + Lineno int `json:"lineno,omitempty"` + Colno int `json:"colno,omitempty"` + AbsolutePath string `json:"abs_path,omitempty"` + ContextLine string `json:"context_line,omitempty"` + PreContext []string `json:"pre_context,omitempty"` + PostContext []string `json:"post_context,omitempty"` + InApp bool `json:"in_app"` +} + +// Intialize and populate a new stacktrace, skipping skip frames. +// +// context is the number of surrounding lines that should be included for context. +// Setting context to 3 would try to get seven lines. Setting context to -1 returns +// one line with no surrounding context, and 0 returns no context. +// +// appPackagePrefixes is a list of prefixes used to check whether a package should +// be considered "in app". +func NewStacktrace(skip int, context int, appPackagePrefixes []string) *Stacktrace { + var frames []*StacktraceFrame + for i := 1 + skip; ; i++ { + pc, file, line, ok := runtime.Caller(i) + if !ok { + break + } + frame := NewStacktraceFrame(pc, file, line, context, appPackagePrefixes) + if frame != nil { + frames = append(frames, frame) + } + } + // If there are no frames, the entire stacktrace is nil + if len(frames) == 0 { + return nil + } + // Optimize the path where there's only 1 frame + if len(frames) == 1 { + return &Stacktrace{frames} + } + // Sentry wants the frames with the oldest first, so reverse them + for i, j := 0, len(frames)-1; i < j; i, j = i+1, j-1 { + frames[i], frames[j] = frames[j], frames[i] + } + return &Stacktrace{frames} +} + +// Build a single frame using data returned from runtime.Caller. +// +// context is the number of surrounding lines that should be included for context. +// Setting context to 3 would try to get seven lines. Setting context to -1 returns +// one line with no surrounding context, and 0 returns no context. +// +// appPackagePrefixes is a list of prefixes used to check whether a package should +// be considered "in app". +func NewStacktraceFrame(pc uintptr, file string, line, context int, appPackagePrefixes []string) *StacktraceFrame { + frame := &StacktraceFrame{AbsolutePath: file, Filename: trimPath(file), Lineno: line, InApp: false} + frame.Module, frame.Function = functionName(pc) + + // `runtime.goexit` is effectively a placeholder that comes from + // runtime/asm_amd64.s and is meaningless. + if frame.Module == "runtime" && frame.Function == "goexit" { + return nil + } + + if frame.Module == "main" { + frame.InApp = true + } else { + for _, prefix := range appPackagePrefixes { + if strings.HasPrefix(frame.Module, prefix) && !strings.Contains(frame.Module, "vendor") && !strings.Contains(frame.Module, "third_party") { + frame.InApp = true + } + } + } + + if context > 0 { + contextLines, lineIdx := fileContext(file, line, context) + if len(contextLines) > 0 { + for i, line := range contextLines { + switch { + case i < lineIdx: + frame.PreContext = append(frame.PreContext, string(line)) + case i == lineIdx: + frame.ContextLine = string(line) + default: + frame.PostContext = append(frame.PostContext, string(line)) + } + } + } + } else if context == -1 { + contextLine, _ := fileContext(file, line, 0) + if len(contextLine) > 0 { + frame.ContextLine = string(contextLine[0]) + } + } + return frame +} + +// Retrieve the name of the package and function containing the PC. +func functionName(pc uintptr) (pack string, name string) { + fn := runtime.FuncForPC(pc) + if fn == nil { + return + } + name = fn.Name() + // We get this: + // runtime/debug.*T·ptrmethod + // and want this: + // pack = runtime/debug + // name = *T.ptrmethod + if idx := strings.LastIndex(name, "."); idx != -1 { + pack = name[:idx] + name = name[idx+1:] + } + name = strings.Replace(name, "·", ".", -1) + return +} + +var fileCacheLock sync.Mutex +var fileCache = make(map[string][][]byte) + +func fileContext(filename string, line, context int) ([][]byte, int) { + fileCacheLock.Lock() + defer fileCacheLock.Unlock() + lines, ok := fileCache[filename] + if !ok { + data, err := ioutil.ReadFile(filename) + if err != nil { + return nil, 0 + } + lines = bytes.Split(data, []byte{'\n'}) + fileCache[filename] = lines + } + line-- // stack trace lines are 1-indexed + start := line - context + var idx int + if start < 0 { + start = 0 + idx = line + } else { + idx = context + } + end := line + context + 1 + if line >= len(lines) { + return nil, 0 + } + if end > len(lines) { + end = len(lines) + } + return lines[start:end], idx +} + +var trimPaths []string + +// Try to trim the GOROOT or GOPATH prefix off of a filename +func trimPath(filename string) string { + for _, prefix := range trimPaths { + if trimmed := strings.TrimPrefix(filename, prefix); len(trimmed) < len(filename) { + return trimmed + } + } + return filename +} + +func init() { + // Collect all source directories, and make sure they + // end in a trailing "separator" + for _, prefix := range build.Default.SrcDirs() { + if prefix[len(prefix)-1] != filepath.Separator { + prefix += string(filepath.Separator) + } + trimPaths = append(trimPaths, prefix) + } +} diff --git a/vendor/github.com/getsentry/raven-go/writer.go b/vendor/github.com/getsentry/raven-go/writer.go new file mode 100644 index 0000000..61f7a91 --- /dev/null +++ b/vendor/github.com/getsentry/raven-go/writer.go @@ -0,0 +1,20 @@ +package raven + +type Writer struct { + Client *Client + Level Severity + Logger string // Logger name reported to Sentry +} + +// Write formats the byte slice p into a string, and sends a message to +// Sentry at the severity level indicated by the Writer w. +func (w *Writer) Write(p []byte) (int, error) { + message := string(p) + + packet := NewPacket(message, &Message{message, nil}) + packet.Level = w.Level + packet.Logger = w.Logger + w.Client.Capture(packet, nil) + + return len(p), nil +} diff --git a/vendor/vendor.json b/vendor/vendor.json index 081869d..aef1ed8 100644 --- a/vendor/vendor.json +++ b/vendor/vendor.json @@ -139,6 +139,12 @@ "revision": "00224ab4ce9f98ee50882ecc397b0001449d1782", "revisionTime": "2016-05-25T15:20:46Z" }, + { + "checksumSHA1": "GLQq6hvItfmA6NsR0Q9GoOKJK+s=", + "path": "github.com/getsentry/raven-go", + "revision": "c9d3cc542ad199f62c0264286be537f9bce6063c", + "revisionTime": "2016-08-05T00:17:29Z" + }, { "path": "github.com/gin-gonic/gin", "revision": "27f912f5b2c8ee7dd7655d479dee2fc4d41d32d7",