diff --git a/config/dev.yml b/config/dev.yml index 944cb11..87d8ebc 100644 --- a/config/dev.yml +++ b/config/dev.yml @@ -77,6 +77,8 @@ cors_debug: true # exporter: "zipkin" # endpoint: "http://zipkin:9411/api/v2/spans" # sample: 0.2 +# include_query: false +# include_params: false auth: # Can be 'rails' or 'jwt' diff --git a/core/internal/psql/query.go b/core/internal/psql/query.go index 4c7a3a5..dd96d34 100644 --- a/core/internal/psql/query.go +++ b/core/internal/psql/query.go @@ -17,10 +17,6 @@ const ( closeBlock = 500 ) -var ( - ErrAllTablesSkipped = errors.New("all tables skipped. cannot render query") -) - type Variables map[string]json.RawMessage type Config struct { @@ -92,30 +88,35 @@ func (co *Compiler) compileQuery(qc *qcode.QCode, w io.Writer, vars Variables) ( io.WriteString(c.w, `SELECT jsonb_build_object(`) for _, id := range qc.Roots { - root := &qc.Selects[id] - if root.SkipRender || len(root.Cols) == 0 { - continue - } - - st.Push(root.ID + closeBlock) - st.Push(root.ID) - if i != 0 { io.WriteString(c.w, `, `) } - c.renderRootSelect(root) + root := &qc.Selects[id] + + if root.SkipRender || len(root.Cols) == 0 { + squoted(c.w, root.FieldName) + io.WriteString(c.w, `, `) + io.WriteString(c.w, `NULL`) + + } else { + st.Push(root.ID + closeBlock) + st.Push(root.ID) + c.renderRootSelect(root) + } + i++ } - io.WriteString(c.w, `) as "__root" FROM `) - - if i == 0 { - return 0, ErrAllTablesSkipped - } - var ignored uint32 + if st.Len() != 0 { + io.WriteString(c.w, `) as "__root" FROM `) + } else { + io.WriteString(c.w, `) as "__root"`) + return ignored, nil + } + for { if st.Len() == 0 { break diff --git a/core/internal/qcode/qcode.go b/core/internal/qcode/qcode.go index 4a6ca21..2add02d 100644 --- a/core/internal/qcode/qcode.go +++ b/core/internal/qcode/qcode.go @@ -333,59 +333,82 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { } trv := com.getRole(role, field.Name) + skipRender := false - switch action { - case QTQuery: - if trv.query.block { - continue + if trv != nil { + switch action { + case QTQuery: + if trv.query.block { + skipRender = true + } + + case QTInsert: + if trv.insert.block { + return fmt.Errorf("insert blocked: %s", field.Name) + } + + case QTUpdate: + if trv.update.block { + return fmt.Errorf("update blocked: %s", field.Name) + } + + case QTDelete: + if trv.delete.block { + return fmt.Errorf("delete blocked: %s", field.Name) + } } - case QTInsert: - if trv.insert.block { - return fmt.Errorf("insert blocked: %s", field.Name) - } - - case QTUpdate: - if trv.update.block { - return fmt.Errorf("update blocked: %s", field.Name) - } - - case QTDelete: - if trv.delete.block { - return fmt.Errorf("delete blocked: %s", field.Name) - } + } else if role == "anon" { + skipRender = true } selects = append(selects, Select{ - ID: id, - ParentID: parentID, - Name: field.Name, - Children: make([]int32, 0, 5), - Allowed: trv.allowedColumns(action), - Functions: true, + ID: id, + ParentID: parentID, + Name: field.Name, + SkipRender: skipRender, }) s := &selects[(len(selects) - 1)] - switch action { - case QTQuery: - s.Functions = !trv.query.disable.funcs - s.Paging.Limit = trv.query.limit - - case QTInsert: - s.PresetMap = trv.insert.psmap - s.PresetList = trv.insert.pslist - - case QTUpdate: - s.PresetMap = trv.update.psmap - s.PresetList = trv.update.pslist - } - if len(field.Alias) != 0 { s.FieldName = field.Alias } else { s.FieldName = s.Name } + if s.ParentID == -1 { + qc.Roots = append(qc.Roots, s.ID) + } else { + p := &selects[s.ParentID] + p.Children = append(p.Children, s.ID) + } + + if skipRender { + id++ + continue + } + + s.Children = make([]int32, 0, 5) + s.Functions = true + + if trv != nil { + s.Allowed = trv.allowedColumns(action) + + switch action { + case QTQuery: + s.Functions = !trv.query.disable.funcs + s.Paging.Limit = trv.query.limit + + case QTInsert: + s.PresetMap = trv.insert.psmap + s.PresetList = trv.insert.pslist + + case QTUpdate: + s.PresetMap = trv.update.psmap + s.PresetList = trv.update.pslist + } + } + err := com.compileArgs(qc, s, field.Args, role) if err != nil { return err @@ -394,13 +417,6 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { // Order is important AddFilters must come after compileArgs com.AddFilters(qc, s, role) - if s.ParentID == -1 { - qc.Roots = append(qc.Roots, s.ID) - } else { - p := &selects[s.ParentID] - p.Children = append(p.Children, s.ID) - } - s.Cols = make([]Column, 0, len(field.Children)) action = QTQuery @@ -440,14 +456,10 @@ func (com *Compiler) compileQuery(qc *QCode, op *Operation, role string) error { func (com *Compiler) AddFilters(qc *QCode, sel *Select, role string) { var fil *Exp - var nu bool // user required (or not) in this filter + var nu bool // need user_id (or not) in this filter if trv, ok := com.tr[role][sel.Name]; ok { fil, nu = trv.filter(qc.Type) - - } else if com.db && role == "anon" { - // Tables not defined under the anon role will not be rendered - sel.SkipRender = true } if fil == nil { @@ -838,14 +850,17 @@ func (com *Compiler) compileArgAfterBefore(sel *Select, arg *Arg, pt PagingType) return nil, false } -var zeroTrv = &trval{} +// var zeroTrv = &trval{} func (com *Compiler) getRole(role, field string) *trval { if trv, ok := com.tr[role][field]; ok { return trv - } else { - return zeroTrv } + + return nil + // } else { + // return zeroTrv + // } } func AddFilter(sel *Select, fil *Exp) { diff --git a/core/prepare.go b/core/prepare.go index 6635e8a..e54d68b 100644 --- a/core/prepare.go +++ b/core/prepare.go @@ -11,7 +11,6 @@ import ( "strings" "github.com/dosco/super-graph/core/internal/allow" - "github.com/dosco/super-graph/core/internal/psql" "github.com/dosco/super-graph/core/internal/qcode" "github.com/valyala/fasttemplate" ) @@ -103,9 +102,6 @@ func (sg *SuperGraph) prepareStmt(item allow.Item) error { // logger.Debug().Msgf("Prepared statement 'query %s' (anon)", item.Name) stmts2, err := sg.buildRoleStmt(qb, vars, "anon") - if err == psql.ErrAllTablesSkipped { - return nil - } if err != nil { return err } @@ -121,9 +117,6 @@ func (sg *SuperGraph) prepareStmt(item allow.Item) error { // logger.Debug().Msgf("Prepared statement 'mutation %s' (%s)", item.Name, role.Name) stmts, err := sg.buildRoleStmt(qb, vars, role.Name) - if err == psql.ErrAllTablesSkipped { - continue - } if err != nil { return err } diff --git a/internal/serv/api.go b/internal/serv/api.go index 2408085..1ef7538 100644 --- a/internal/serv/api.go +++ b/internal/serv/api.go @@ -62,9 +62,11 @@ type Serv struct { } Tracing struct { - Exporter string - Endpoint string - Sample string + Exporter string + Endpoint string + Sample string + IncludeQuery bool `mapstructure:"include_query"` + IncludeParams bool `mapstructure:"include_params"` } } diff --git a/internal/serv/http.go b/internal/serv/http.go index bd12ab0..acf32f8 100644 --- a/internal/serv/http.go +++ b/internal/serv/http.go @@ -10,6 +10,8 @@ import ( "github.com/dosco/super-graph/core" "github.com/dosco/super-graph/internal/serv/internal/auth" "github.com/rs/cors" + "go.opencensus.io/plugin/ochttp" + "go.opencensus.io/trace" "go.uber.org/zap" ) @@ -44,7 +46,7 @@ func apiV1Handler() http.Handler { AllowCredentials: true, Debug: conf.DebugCORS, }) - h = c.Handler(h) + return c.Handler(h) } return h @@ -78,6 +80,22 @@ func apiV1(w http.ResponseWriter, r *http.Request) { doLog := true res, err := sg.GraphQL(ct, req.Query, req.Vars) + if conf.telemetryEnabled() { + span := trace.FromContext(ct) + + span.AddAttributes( + trace.StringAttribute("operation", res.OperationName()), + trace.StringAttribute("query_name", res.QueryName()), + trace.StringAttribute("role", res.Role()), + ) + + if err != nil { + span.AddAttributes(trace.StringAttribute("error", err.Error())) + } + + ochttp.SetRoute(ct, apiRoute) + } + if !conf.Production && res.QueryName() == introspectionQuery { doLog = false } diff --git a/internal/serv/init.go b/internal/serv/init.go index ef2abc8..3bd23a8 100644 --- a/internal/serv/init.go +++ b/internal/serv/init.go @@ -215,7 +215,20 @@ func initDB(c *Config, useDB, useTelemetry bool) (*sql.DB, error) { // } if useTelemetry && conf.telemetryEnabled() { - driverName, err = ocsql.Register(driverName, ocsql.WithAllTraceOptions(), ocsql.WithInstanceName(conf.AppName)) + opts := ocsql.TraceOptions{ + AllowRoot: false, + Ping: true, + RowsNext: true, + RowsClose: true, + RowsAffected: true, + LastInsertID: true, + Query: conf.Telemetry.Tracing.IncludeQuery, + QueryParams: conf.Telemetry.Tracing.IncludeParams, + } + opt := ocsql.WithOptions(opts) + name := ocsql.WithInstanceName(conf.AppName) + + driverName, err = ocsql.Register(driverName, opt, name) if err != nil { return nil, fmt.Errorf("unable to register ocsql driver: %v", err) } diff --git a/internal/serv/serv.go b/internal/serv/serv.go index 643734b..6f3f3d1 100644 --- a/internal/serv/serv.go +++ b/internal/serv/serv.go @@ -13,6 +13,11 @@ import ( rice "github.com/GeertJohan/go.rice" "github.com/NYTimes/gziphandler" "github.com/dosco/super-graph/internal/serv/internal/auth" + "go.opencensus.io/plugin/ochttp" +) + +var ( + apiRoute string = "/api/v1/graphql" ) func initWatcher() { @@ -76,6 +81,10 @@ func startHTTP() { MaxHeaderBytes: 1 << 20, } + if conf.telemetryEnabled() { + srv.Handler = &ochttp.Handler{Handler: routes} + } + idleConnsClosed := make(chan struct{}) go func() { sigint := make(chan os.Signal, 1) @@ -114,8 +123,6 @@ func routeHandler() (http.Handler, error) { return mux, nil } - apiRoute := "/api/v1/graphql" - if len(conf.APIPath) != 0 { apiRoute = path.Join("/", conf.APIPath, "/v1/graphql") } @@ -178,6 +185,10 @@ func setActionRoutes(routes map[string]http.Handler) error { routes[p] = fn } + if conf.telemetryEnabled() { + routes[p] = ochttp.WithRouteTag(routes[p], p) + } + if err != nil { return err } diff --git a/internal/serv/telemetry.go b/internal/serv/telemetry.go index 2551a1c..8bb7bdd 100644 --- a/internal/serv/telemetry.go +++ b/internal/serv/telemetry.go @@ -103,7 +103,9 @@ func enableObservability(mux *http.ServeMux) (func(), error) { } if err != nil { - return nil, fmt.Errorf("ERR OpenCensus: %s: %v", conf.Telemetry.Tracing, err) + return nil, fmt.Errorf("ERR OpenCensus: %s: %v", + conf.Telemetry.Tracing.Exporter, + err) } if tex != nil {