Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Generate call stack for sdk error and preserve stack trace #3310

Merged
merged 8 commits into from
Oct 5, 2018
2 changes: 2 additions & 0 deletions cli/cdsctl/admin.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ func admin() *cobra.Command {
adminPlatformModels,
adminPlugins,
adminBroadcasts,
adminErrors,
usr,
group,
worker,
Expand All @@ -36,5 +37,6 @@ func admin() *cobra.Command {
adminPlugins,
adminPluginsAction,
adminBroadcasts,
adminErrors,
})
}
42 changes: 42 additions & 0 deletions cli/cdsctl/admin_errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package main

import (
"fmt"

"github.com/ovh/cds/cli"
"github.com/spf13/cobra"
)

var (
adminErrorsCmd = cli.Command{
Name: "errors",
Short: "Manage CDS errors",
}

adminErrors = cli.NewCommand(adminErrorsCmd, nil,
[]*cobra.Command{
cli.NewCommand(adminErrorsGetCmd, adminErrorsGetFunc, nil),
})
)

var adminErrorsGetCmd = cli.Command{
Name: "get",
Short: "Get CDS error",
Args: []cli.Arg{
{Name: "uuid"},
},
}

func adminErrorsGetFunc(v cli.Values) error {
res, err := client.MonErrorsGet(v.GetString("uuid"))
if err != nil {
return err
}

fmt.Printf("Message: %s\n", res.Message)
if res.StackTrace != "" {
fmt.Printf("Stack trace:\n%s", res.StackTrace)
}

return nil
}
21 changes: 14 additions & 7 deletions cli/cobra.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ import (
"github.com/olekukonko/tablewriter"
"github.com/spf13/cobra"
"gopkg.in/yaml.v2"

"github.com/ovh/cds/sdk"
)

// ShellMode will os.Exit if false, display only exit code if true
Expand All @@ -27,18 +29,23 @@ func ExitOnError(err error, helpFunc ...func() error) {
return
}

if e, ok := err.(*Error); ok {
code := 50 // default error code

switch e := err.(type) {
case sdk.Error:
fmt.Printf("Error(%s): %s\n", e.UUID, e.Message)
case *Error:
code = e.Code
fmt.Println("Error:", e.Error())
for _, f := range helpFunc {
f()
}
OSExit(e.Code)
default:
fmt.Println("Error:", err.Error())
}
fmt.Println("Error:", err.Error())

for _, f := range helpFunc {
f()
}
OSExit(50)

OSExit(code)
}

// OSExit will os.Exit if ShellMode is false, display only exit code if true
Expand Down
4 changes: 4 additions & 0 deletions engine/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,10 @@ type Configuration struct {
} `toml:"status" comment:"###########################\n CDS Status Settings \n Documentation: https://ovh.github.io/cds/hosting/monitoring/ \n##########################" json:"status"`
DefaultOS string `toml:"defaultOS" default:"linux" comment:"if no model and os/arch is specified in your job's requirements then spawn worker on this operating system (example: freebsd, linux, windows)" json:"defaultOS"`
DefaultArch string `toml:"defaultArch" default:"amd64" comment:"if no model and no os/arch is specified in your job's requirements then spawn worker on this architecture (example: amd64, arm, 386)" json:"defaultArch"`
Graylog struct {
AccessToken string `toml:"accessToken" json:"-"`
URL string `toml:"url" comment:"Example: http://localhost:9000" json:"url"`
} `toml:"graylog" json:"graylog"`
}

// ProviderConfiguration is the piece of configuration for each provider authentication
Expand Down
1 change: 1 addition & 0 deletions engine/api/api_routes.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ func (api *API) InitRouter() {
r.Handle("/mon/building/{hash}", r.GET(api.getPipelineBuildingCommitHandler))
r.Handle("/mon/metrics", r.GET(api.getMetricsHandler, Auth(false)))
r.Handle("/mon/stats", r.GET(observability.StatsHandler, Auth(false)))
r.Handle("/mon/errors/{uuid}", r.GET(api.getErrorHandler, NeedAdmin(true)))

r.Handle("/ui/navbar", r.GET(api.getNavbarHandler))
r.Handle("/ui/project/{key}/application/{permApplicationName}/overview", r.GET(api.getApplicationOverviewHandler))
Expand Down
6 changes: 3 additions & 3 deletions engine/api/application_import.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,9 +71,9 @@ func (api *API) postApplicationImportHandler() service.Handler {
myError, ok := globalError.(sdk.Error)
if ok {
log.Warning("postApplicationImportHandler> Unable to import application %s : %v", eapp.Name, myError)
msgTranslated, _ := sdk.ProcessError(myError, r.Header.Get("Accept-Language"))
msgListString = append(msgListString, msgTranslated)
return service.WriteJSON(w, msgListString, myError.Status)
sdkErr := sdk.ExtractHTTPError(myError, r.Header.Get("Accept-Language"))
msgListString = append(msgListString, sdkErr.Message)
return service.WriteJSON(w, msgListString, sdkErr.Status)
}
return sdk.WrapError(globalError, "postApplicationImportHandler> Unable import application %s", eapp.Name)
}
Expand Down
5 changes: 2 additions & 3 deletions engine/api/ascode_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,10 +37,9 @@ func writeError(w *http.Response, err error) (*http.Response, error) {
body := new(bytes.Buffer)
enc := json.NewEncoder(body)
w.Body = ioutil.NopCloser(body)
msg, errProcessed := sdk.ProcessError(err, "")
sdkErr := sdk.Error{Message: msg}
sdkErr := sdk.ExtractHTTPError(err, "")
enc.Encode(sdkErr)
w.StatusCode = errProcessed.Status
w.StatusCode = sdkErr.Status
return w, sdkErr
}

Expand Down
75 changes: 75 additions & 0 deletions engine/api/error.go
Original file line number Diff line number Diff line change
@@ -1 +1,76 @@
package api

import (
"context"
"encoding/json"
"fmt"
"io/ioutil"
"net/http"
"time"

"github.com/gorilla/mux"

"github.com/ovh/cds/sdk"
"github.com/ovh/cds/engine/service"
)

type graylogResponse struct {
TotalResult int `json:"total_results"`
Messages []struct {
Message map[string]interface{} `json:"message"`
} `json:"messages"`
}

func (api *API) getErrorHandler() service.Handler {
return func(ctx context.Context, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
uuid := vars["uuid"]

if api.Config.Graylog.URL == "" || api.Config.Graylog.AccessToken == "" {
return sdk.ErrNotImplemented
}

req, err := http.NewRequest("GET", fmt.Sprintf("%s/api/search/universal/absolute", api.Config.Graylog.URL), nil)
if err != nil {
return sdk.WrapError(err, "invalid given Graylog url")
}

q := req.URL.Query()
q.Add("query", fmt.Sprintf("error_uuid:%s", uuid))
q.Add("from", "1970-01-01 00:00:00.000")
q.Add("to", time.Now().Format("2006-01-02 15:04:05"))
q.Add("limit", "1")
req.URL.RawQuery = q.Encode()

req.SetBasicAuth(api.Config.Graylog.AccessToken, "token")
resp, err := http.DefaultClient.Do(req)
if err != nil {
return sdk.WrapError(err, "cannot send query to Graylog")
}

defer resp.Body.Close()
body, err := ioutil.ReadAll(resp.Body)
if err != nil {
return sdk.WrapError(err, "cannot read response from Graylog")
}

var res graylogResponse
if err := json.Unmarshal(body, &res); err != nil {
return sdk.WrapError(err, "cannot unmarshal response from Graylog")
}

if res.TotalResult < 1 {
return sdk.ErrNotFound
}

e := sdk.Error{
UUID: res.Messages[0].Message["error_uuid"].(string),
Message: res.Messages[0].Message["message"].(string),
}
if st, ok := res.Messages[0].Message["stack_trace"]; ok {
e.StackTrace = st.(string)
}

return service.WriteJSON(w, e, http.StatusOK)
}
}
6 changes: 3 additions & 3 deletions engine/api/services/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ func All(db gorp.SqlExecutor) ([]sdk.Service, error) {
if err == sdk.ErrNotFound {
return nil, nil
}
return nil, sdk.WrapError(err, "All> Unable to find all services")
return nil, sdk.WrapError(err, "Unable to find all services")
}
return services, nil
}
Expand All @@ -95,13 +95,13 @@ func findAll(db gorp.SqlExecutor, query string, args ...interface{}) ([]sdk.Serv
if err == sql.ErrNoRows {
return nil, sdk.ErrNotFound
}
return nil, sdk.WrapError(err, "findAll> no service found")
return nil, sdk.WithStack(err)
}
ss := make([]sdk.Service, len(sdbs))
for i := 0; i < len(sdbs); i++ {
s := &sdbs[i]
if err := s.PostGet(db); err != nil {
return nil, sdk.WrapError(err, "findAll> postGet on srv id:%d name:%s type:%s lastHeartbeat:%v", s.ID, s.Name, s.Type, s.LastHeartbeat)
return nil, sdk.WrapError(err, "postGet on srv id:%d name:%s type:%s lastHeartbeat:%v", s.ID, s.Name, s.Type, s.LastHeartbeat)
}
ss[i] = sdk.Service(sdbs[i])
}
Expand Down
2 changes: 1 addition & 1 deletion engine/api/status.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func (api *API) statusHandler() service.Handler {

srvs, err := services.All(api.mustDB())
if err != nil {
return sdk.WrapError(err, "statusHandler> error on q.All()")
return err
}

mStatus := api.computeGlobalStatus(srvs)
Expand Down
3 changes: 1 addition & 2 deletions engine/api/workflow/dao.go
Original file line number Diff line number Diff line change
Expand Up @@ -1043,8 +1043,7 @@ func Push(ctx context.Context, db *gorp.DbMap, store cache.Store, proj *sdk.Proj
wf, msgList, err := ParseAndImport(ctx, tx, store, proj, &wrkflw, u, ImportOptions{DryRun: dryRun, Force: true})
if err != nil {
log.Error("Push> Unable to import workflow: %v", err)
err = sdk.SetError(err, "unable to import workflow %s", wrkflw.Name)
return nil, nil, sdk.WrapError(err, "Push> %v ", err)
return nil, nil, sdk.WrapError(err, "Push> unable to import workflow %s", wrkflw.Name)
}

// TODO workflow as code, manage derivation workflow
Expand Down
29 changes: 21 additions & 8 deletions engine/service/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,9 @@ import (
"strconv"
"time"

"github.com/pborman/uuid"
"github.com/sirupsen/logrus"

"github.com/ovh/cds/sdk"
"github.com/ovh/cds/sdk/cdsclient"
"github.com/ovh/cds/sdk/log"
Expand Down Expand Up @@ -88,17 +91,27 @@ func WriteProcessTime(w http.ResponseWriter) {
// WriteError is a helper function to return error in a language the called understand
func WriteError(w http.ResponseWriter, r *http.Request, err error) {
al := r.Header.Get("Accept-Language")
msg, errProcessed := sdk.ProcessError(err, al)
sdkErr := sdk.Error{Message: msg}
httpErr := sdk.ExtractHTTPError(err, al)
httpErr.UUID = uuid.NewUUID().String()
isErrWithStack := sdk.IsErrorWithStack(err)

entry := logrus.WithField("method", r.Method).
WithField("request_uri", r.RequestURI).
WithField("status", httpErr.Status).
WithField("error_uuid", httpErr.UUID)
if isErrWithStack {
entry = entry.WithField("stack_trace", fmt.Sprintf("%+v", err))
}

// ErrAlreadyTaken and ErrWorkerModelAlreadyBooked are not useful to log in warning
if sdk.ErrorIs(errProcessed, sdk.ErrAlreadyTaken) ||
sdk.ErrorIs(errProcessed, sdk.ErrWorkerModelAlreadyBooked) ||
sdk.ErrorIs(errProcessed, sdk.ErrJobAlreadyBooked) || r.URL.Path == "/user/me" {
log.Debug("%-7s | %-4d | %s \t %s", r.Method, errProcessed.Status, r.RequestURI, err)
if sdk.ErrorIs(httpErr, sdk.ErrAlreadyTaken) ||
sdk.ErrorIs(httpErr, sdk.ErrWorkerModelAlreadyBooked) ||
sdk.ErrorIs(httpErr, sdk.ErrJobAlreadyBooked) || r.URL.Path == "/user/me" {
entry.Debugf("%s", err)
} else {
log.Warning("%-7s | %-4d | %s \t %s", r.Method, errProcessed.Status, r.RequestURI, err)
entry.Warningf("%s", err)
}

_ = WriteJSON(w, sdkErr, errProcessed.Status)
// safely ignore error returned by WriteJSON
_ = WriteJSON(w, httpErr, httpErr.Status)
}
8 changes: 4 additions & 4 deletions engine/worker/cmd_cache.go
Original file line number Diff line number Diff line change
Expand Up @@ -378,22 +378,22 @@ func (wk *currentWorker) cachePullHandler(w http.ResponseWriter, r *http.Request
case tar.TypeReg, tar.TypeLink:
f, err := os.OpenFile(target, os.O_CREATE|os.O_RDWR, os.FileMode(header.Mode))
if err != nil {
err = sdk.Error{
sdkErr := sdk.Error{
Message: "worker cache pull > Unable to open file : " + err.Error(),
Status: http.StatusInternalServerError,
}
writeJSON(w, err, http.StatusInternalServerError)
writeJSON(w, sdkErr, sdkErr.Status)
return
}

// copy over contents
if _, err := io.Copy(f, tr); err != nil {
_ = f.Close()
err = sdk.Error{
sdkErr := sdk.Error{
Message: "worker cache pull > Cannot copy content file : " + err.Error(),
Status: http.StatusInternalServerError,
}
writeJSON(w, err, http.StatusInternalServerError)
writeJSON(w, sdkErr, sdkErr.Status)
return
}

Expand Down
5 changes: 2 additions & 3 deletions engine/worker/cmd_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,7 +86,6 @@ func writeJSON(w http.ResponseWriter, data interface{}, status int) {

func writeError(w http.ResponseWriter, r *http.Request, err error) {
al := r.Header.Get("Accept-Language")
msg, sdkError := sdk.ProcessError(err, al)
sdkErr := sdk.Error{Message: msg}
writeJSON(w, sdkErr, sdkError.Status)
sdkErr := sdk.ExtractHTTPError(err, al)
writeJSON(w, sdkErr, sdkErr.Status)
}
17 changes: 17 additions & 0 deletions sdk/cdsclient/client_mon.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package cdsclient

import (
"encoding/json"
"fmt"

"github.com/ovh/cds/sdk"
)

Expand All @@ -27,3 +30,17 @@ func (c *client) MonDBMigrate() ([]sdk.MonDBMigrate, error) {
}
return monDBMigrate, nil
}

func (c *client) MonErrorsGet(uuid string) (*sdk.Error, error) {
res, _, _, err := c.Request("GET", fmt.Sprintf("/mon/errors/%s", uuid), nil)
if err != nil {
return nil, err
}

var sdkError sdk.Error
if err := json.Unmarshal(res, &sdkError); err != nil {
return nil, err
}

return &sdkError, nil
}
Loading