From e1d3bb9055151591277cd14d5d732f84df53f51d Mon Sep 17 00:00:00 2001 From: Vikram Rangnekar Date: Mon, 13 May 2019 00:05:08 -0400 Subject: [PATCH] Add tracing for API stitching --- config/dev.yml | 14 ++--- config/prod.yml | 12 +++++ docs/.vuepress/public/tracing.png | Bin 0 -> 43271 bytes docs/guide.md | 48 +++++++++++------ jsn/filter.go | 23 +++++--- jsn/get.go | 7 +++ jsn/json_test.go | 24 +++++++-- jsn/replace.go | 6 +++ jsn/strip.go | 6 +++ psql/psql.go | 2 +- psql/psql_test.go | 8 +-- serv/core.go | 86 +++++++++++++++++------------- 12 files changed, 160 insertions(+), 76 deletions(-) create mode 100644 docs/.vuepress/public/tracing.png diff --git a/config/dev.yml b/config/dev.yml index 5261e24..2fe6357 100644 --- a/config/dev.yml +++ b/config/dev.yml @@ -110,14 +110,14 @@ database: remotes: - name: payments id: stripe_id - path: data - pass_headers: - - cookie - - host - # set_headers: - # - name: authorize - # value: Bearer 1234567890 url: http://rails_app:3000/stripe/$id + path: data + # pass_headers: + # - cookie + # - host + set_headers: + - name: Authorization + value: Bearer - # You can create new fields that have a # real db table backing them diff --git a/config/prod.yml b/config/prod.yml index 0e8ad2a..1ddcb9e 100644 --- a/config/prod.yml +++ b/config/prod.yml @@ -105,6 +105,18 @@ database: # even defaults.filter filter: none + # remotes: + # - name: payments + # id: stripe_id + # url: http://rails_app:3000/stripe/$id + # path: data + # # pass_headers: + # # - cookie + # # - host + # set_headers: + # - name: Authorization + # value: Bearer + - # You can create new fields that have a # real db table backing them name: me diff --git a/docs/.vuepress/public/tracing.png b/docs/.vuepress/public/tracing.png new file mode 100644 index 0000000000000000000000000000000000000000..cfb268bc42cdc47d86b40bf6f15151cb094df86c GIT binary patch literal 43271 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 literal 0 HcmV?d00001 diff --git a/docs/guide.md b/docs/guide.md index d671333..b496596 100644 --- a/docs/guide.md +++ b/docs/guide.md @@ -347,7 +347,7 @@ class AddSearchColumn < ActiveRecord::Migration[5.1] end ``` -## Stitching in REST APIs +## API Stitching It often happens that after fetching some data from the DB we need to call another API to fetch some more data and all this combined into a single JSON response. @@ -355,27 +355,26 @@ For example you need to list the last 3 payments made by a user. You will first Similiarly you might also have the need to fetch the users last tweet and include that too. Super Graph can handle this for you using it's `API Stitching` feature. -### API Stitching configuration +### Stripe API example The configuration is self explanatory. A `payments` field has been added under the `customers` table. This field is added to the `remotes` subsection that defines fields associated with `customers` that are remote and not real database columns. The `id` parameter maps a column from the `customers` table to the `$id` variable. In this case it maps `$id` to the `customer_id` column. ```yaml - tables: - - name: customers - - remotes: - - name: payments - id: customer_id - path: data - pass_headers: - - cookie - - host - # set_headers: - # - name: authorize - # value: Bearer 1234567890 - url: http://rails_app:3000/stripe/$id +tables: + - name: customers + remotes: + - name: payments + id: stripe_id + url: http://rails_app:3000/stripe/$id + path: data + # pass_headers: + # - cookie + # - host + set_headers: + - name: Authorization + value: Bearer ``` #### How do I make use of this? @@ -416,6 +415,11 @@ And voila here is the result. You get all of this advanced and honestly complex ... ``` +Even tracing data is availble in the Super Graph web UI if tracing is enabled in the +config. By default it is for development. + +![Query Tracing](/tracing.png "Super Graph Web UI Query Tracing") + ## Authentication You can only have one type of auth enabled. You can either pick Rails or JWT. @@ -610,6 +614,18 @@ database: # even defaults.filter filter: none + remotes: + - name: payments + id: stripe_id + url: http://rails_app:3000/stripe/$id + path: data + # pass_headers: + # - cookie + # - host + set_headers: + - name: Authorization + value: Bearer + - # You can create new fields that have a # real db table backing them name: me diff --git a/jsn/filter.go b/jsn/filter.go index 1a9f658..c55097a 100644 --- a/jsn/filter.go +++ b/jsn/filter.go @@ -8,7 +8,6 @@ import ( func Filter(w *bytes.Buffer, b []byte, keys []string) error { var err error - kmap := make(map[uint64]struct{}, len(keys)) for i := range keys { @@ -39,8 +38,7 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error { } } - switch { - case state == expectKey: + if state == expectKey { switch b[i] { case '[': if !isList { @@ -53,16 +51,19 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error { } else { _, err = w.Write([]byte("},{")) } - item++ field = 0 - case '"': - state = expectKeyClose - s = i - i++ + item++ } if err != nil { return err } + } + + switch { + case state == expectKey && b[i] == '"': + state = expectKeyClose + s = i + case state == expectKeyClose && b[i] == '"': state = expectColon k = b[(s + 1):i] @@ -105,6 +106,12 @@ func Filter(w *bytes.Buffer, b []byte, keys []string) error { case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'): e = i + + case state == expectValue && b[i] == 'n': + state = expectNull + + case state == expectNull && b[i] == 'l': + e = i } if e != 0 { diff --git a/jsn/get.go b/jsn/get.go index bc7d3a3..584ce84 100644 --- a/jsn/get.go +++ b/jsn/get.go @@ -10,6 +10,7 @@ const ( expectColon expectValue expectString + expectNull expectListClose expectObjClose expectBoolClose @@ -114,6 +115,12 @@ func Get(b []byte, keys [][]byte) []Field { case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'): e = i + + case state == expectValue && b[i] == 'n': + state = expectNull + + case state == expectNull && b[i] == 'l': + e = i } if e != 0 { diff --git a/jsn/json_test.go b/jsn/json_test.go index 1b939c2..ba545e6 100644 --- a/jsn/json_test.go +++ b/jsn/json_test.go @@ -81,7 +81,8 @@ var ( "id": 11, "full_name": "Arden Koss", "email": "cristobalankunding@howewelch.org", - "__twitter_id": "2048666903444506956" + "__twitter_id": "2048666903444506956", + "something": null }, { "id": 12, @@ -106,6 +107,7 @@ var ( "full_name": "Sidney Stroman", "email": "user0@demo.com", "__twitter_id": "2048666903444506956", + "something": null, "embed": { "id": 8, "full_name": "Caroll Orn Sr.", @@ -137,7 +139,7 @@ var ( "__twitter_id": "2048666903444506956", "embed": { "id": 8, - "full_name": "Caroll Orn Sr.", + "full_name": null, "email": "joannarau@hegmann.io", "__twitter_id": "ABC123" } @@ -216,7 +218,7 @@ func TestValue(t *testing.T) { } } -func TestFilter(t *testing.T) { +func TestFilter1(t *testing.T) { var b bytes.Buffer Filter(&b, []byte(input2), []string{"id", "full_name", "embed"}) @@ -226,6 +228,20 @@ func TestFilter(t *testing.T) { t.Error("Does not match expected json") } } + +func TestFilter2(t *testing.T) { + value := `[{"id":1,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":100,"amount_refunded":0,"date":"01/01/2019","application":null,"billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}, {"id":2,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":0,"date":"02/18/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}},{"id":3,"customer_id":"cus_2TbMGf3cl0","object":"charge","amount":150,"amount_refunded":50,"date":"03/21/2019","billing_details":{"address":"1 Infinity Drive","zipcode":"94024"}}]` + + var b bytes.Buffer + Filter(&b, []byte(value), []string{"id"}) + + expected := `[{"id":1},{"id":2},{"id":3}]` + + if b.String() != expected { + t.Error("Does not match expected json") + } +} + func TestStrip(t *testing.T) { path1 := [][]byte{[]byte("data"), []byte("users")} value1 := Strip([]byte(input3), path1) @@ -266,7 +282,7 @@ func TestReplace(t *testing.T) { "__twitter_id": "2048666903444506956", "embed": { "id": 8, - "full_name": "Caroll Orn Sr.", + "full_name": null, "email": "joannarau@hegmann.io", "some_list":[{"id":1,"embed":{"id":8}},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13}] } diff --git a/jsn/replace.go b/jsn/replace.go index a1216c3..43389a5 100644 --- a/jsn/replace.go +++ b/jsn/replace.go @@ -96,6 +96,12 @@ func Replace(w *bytes.Buffer, b []byte, from, to []Field) error { case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'): e = i + + case state == expectValue && b[i] == 'n': + state = expectNull + + case state == expectNull && b[i] == 'l': + e = i } if e != 0 { diff --git a/jsn/strip.go b/jsn/strip.go index 6427636..3b66fd0 100644 --- a/jsn/strip.go +++ b/jsn/strip.go @@ -80,6 +80,12 @@ func Strip(b []byte, path [][]byte) []byte { case state == expectBoolClose && (b[i] == 'e' || b[i] == 'E'): e = i + + case state == expectValue && b[i] == 'n': + state = expectNull + + case state == expectNull && b[i] == 'l': + e = i } if e != 0 { diff --git a/psql/psql.go b/psql/psql.go index 3748e04..211ac1d 100644 --- a/psql/psql.go +++ b/psql/psql.go @@ -661,7 +661,7 @@ func renderOrderBy(w io.Writer, sel *qcode.Select) error { case qcode.OrderDescNullsLast: fmt.Fprintf(w, `%s_%d.ob.%s DESC NULLS LAST`, sel.Table, sel.ID, ob.Col) default: - return fmt.Errorf("13: unexpected value %v (%t)", ob.Order, ob.Order) + return fmt.Errorf("13: unexpected value %v", ob.Order) } } return nil diff --git a/psql/psql_test.go b/psql/psql_test.go index b22fdb4..4c8d295 100644 --- a/psql/psql_test.go +++ b/psql/psql_test.go @@ -262,7 +262,7 @@ func oneToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("users"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email", "products_1.join"."products" AS "products") AS "sel_0")) AS "users" FROM (SELECT "users"."email", "users"."id" FROM "users" WHERE ((("users"."id") = ('{{user_id}}'))) LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "sel_1")) AS "products" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "products_1") AS "products_1.join" ON ('true') LIMIT ('20') :: integer) AS "users_0") AS "done_1337";` + sql := `SELECT json_object_agg('users', users) FROM (SELECT coalesce(json_agg("users"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "users_0"."email" AS "email", "products_1_join"."products" AS "products") AS "sel_0")) AS "users" FROM (SELECT "users"."email", "users"."id" FROM "users" WHERE ((("users"."id") = ('{{user_id}}'))) LIMIT ('20') :: integer) AS "users_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name", "products_1"."price" AS "price") AS "sel_1")) AS "products" FROM (SELECT "products"."name", "products"."price" FROM "products" WHERE ((("products"."user_id") = ("users_0"."id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "products_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "users_0") AS "done_1337";` resSQL, err := compileGQLToPSQL(gql) if err != nil { @@ -285,7 +285,7 @@ func belongsTo(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1.join"."users" AS "users") AS "sel_0")) AS "products" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("users"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "users_1"."email" AS "email") AS "sel_1")) AS "users" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "users_1") AS "users_1.join" ON ('true') LIMIT ('20') :: integer) AS "products_0") AS "done_1337";` + sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "products_0"."price" AS "price", "users_1_join"."users" AS "users") AS "sel_0")) AS "products" FROM (SELECT "products"."name", "products"."price", "products"."user_id" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("users"), '[]') AS "users" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "users_1"."email" AS "email") AS "sel_1")) AS "users" FROM (SELECT "users"."email" FROM "users" WHERE ((("users"."id") = ("products_0"."user_id"))) LIMIT ('20') :: integer) AS "users_1" LIMIT ('20') :: integer) AS "users_1") AS "users_1_join" ON ('true') LIMIT ('20') :: integer) AS "products_0") AS "done_1337";` resSQL, err := compileGQLToPSQL(gql) if err != nil { @@ -308,7 +308,7 @@ func manyToMany(t *testing.T) { } }` - sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "customers_1.join"."customers" AS "customers") AS "sel_0")) AS "products" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("customers"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "sel_1")) AS "customers" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "customers_1") AS "customers_1.join" ON ('true') LIMIT ('20') :: integer) AS "products_0") AS "done_1337";` + sql := `SELECT json_object_agg('products', products) FROM (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "products_0"."name" AS "name", "customers_1_join"."customers" AS "customers") AS "sel_0")) AS "products" FROM (SELECT "products"."name", "products"."id" FROM "products" WHERE ((("products"."price") > (0)) AND (("products"."price") < (8))) LIMIT ('20') :: integer) AS "products_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("customers"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "customers_1"."email" AS "email", "customers_1"."full_name" AS "full_name") AS "sel_1")) AS "customers" FROM (SELECT "customers"."email", "customers"."full_name" FROM "customers" LEFT OUTER JOIN "purchases" ON (("purchases"."product_id") = ("products_0"."id")) WHERE ((("customers"."id") = ("purchases"."customer_id"))) LIMIT ('20') :: integer) AS "customers_1" LIMIT ('20') :: integer) AS "customers_1") AS "customers_1_join" ON ('true') LIMIT ('20') :: integer) AS "products_0") AS "done_1337";` resSQL, err := compileGQLToPSQL(gql) if err != nil { @@ -331,7 +331,7 @@ func manyToManyReverse(t *testing.T) { } }` - sql := `SELECT json_object_agg('customers', customers) FROM (SELECT coalesce(json_agg("customers"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1.join"."products" AS "products") AS "sel_0")) AS "customers" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name") AS "sel_1")) AS "products" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "products_1") AS "products_1.join" ON ('true') LIMIT ('20') :: integer) AS "customers_0") AS "done_1337";` + sql := `SELECT json_object_agg('customers', customers) FROM (SELECT coalesce(json_agg("customers"), '[]') AS "customers" FROM (SELECT row_to_json((SELECT "sel_0" FROM (SELECT "customers_0"."email" AS "email", "customers_0"."full_name" AS "full_name", "products_1_join"."products" AS "products") AS "sel_0")) AS "customers" FROM (SELECT "customers"."email", "customers"."full_name", "customers"."id" FROM "customers" LIMIT ('20') :: integer) AS "customers_0" LEFT OUTER JOIN LATERAL (SELECT coalesce(json_agg("products"), '[]') AS "products" FROM (SELECT row_to_json((SELECT "sel_1" FROM (SELECT "products_1"."name" AS "name") AS "sel_1")) AS "products" FROM (SELECT "products"."name" FROM "products" LEFT OUTER JOIN "purchases" ON (("purchases"."customer_id") = ("customers_0"."id")) WHERE ((("products"."id") = ("purchases"."product_id"))) LIMIT ('20') :: integer) AS "products_1" LIMIT ('20') :: integer) AS "products_1") AS "products_1_join" ON ('true') LIMIT ('20') :: integer) AS "customers_0") AS "done_1337";` resSQL, err := compileGQLToPSQL(gql) if err != nil { diff --git a/serv/core.go b/serv/core.go index 6076cdb..e8e4f58 100644 --- a/serv/core.go +++ b/serv/core.go @@ -92,24 +92,31 @@ func (c *coreContext) handleReq(w io.Writer, req *http.Request) error { continue } + st := time.Now() + b, err := r.Fn(req, id) if err != nil { return err } + if conf.EnableTracing { + c.addTrace(s, st) + } + if len(r.Path) != 0 { b = jsn.Strip(b, r.Path) } - fils := []string{} - for i := range s.Cols { - fils = append(fils, s.Cols[i].Name) - } - var ob bytes.Buffer - if err = jsn.Filter(&ob, b, fils); err != nil { - return err + if len(s.Cols) != 0 { + err = jsn.Filter(&ob, b, colsToList(s.Cols)) + if err != nil { + return err + } + + } else { + ob.WriteString("null") } f := jsn.Field{[]byte(s.FieldName), ob.Bytes()} @@ -188,8 +195,8 @@ func (c *coreContext) resolveSQL(qc *qcode.QCode, vars variables) ( return nil, 0, err } - if conf.EnableTracing { - c.res.Extensions = &extensions{newTrace(st, time.Now(), qc)} + if conf.EnableTracing && len(qc.Query.Selects) != 0 { + c.addTrace(&qc.Query.Selects[0], st) } return []byte(root), skipped, nil @@ -200,6 +207,34 @@ func (c *coreContext) render(w io.Writer, data []byte) error { return json.NewEncoder(w).Encode(c.res) } +func (c *coreContext) addTrace(sel *qcode.Select, st time.Time) { + et := time.Now() + du := et.Sub(st) + + if c.res.Extensions == nil { + c.res.Extensions = &extensions{&trace{ + Version: 1, + StartTime: st, + Execution: execution{}, + }} + } + + c.res.Extensions.Tracing.EndTime = et + c.res.Extensions.Tracing.Duration = du + + tr := resolver{ + Path: []string{sel.Table}, + ParentType: "Query", + FieldName: sel.Table, + ReturnType: "object", + StartOffset: 1, + Duration: du, + } + + c.res.Extensions.Tracing.Execution.Resolvers = + append(c.res.Extensions.Tracing.Execution.Resolvers, tr) +} + func parentFieldIds(h *xxhash.Digest, sel []qcode.Select, skipped uint32) ( [][]byte, map[uint64]*qcode.Select) { @@ -251,32 +286,11 @@ func authCheck(ctx *coreContext) bool { return (ctx.Value(userIDKey) != nil) } -func newTrace(st, et time.Time, qc *qcode.QCode) *trace { - if len(qc.Query.Selects) == 0 { - return nil +func colsToList(cols []qcode.Column) []string { + var f []string + + for i := range cols { + f = append(f, cols[i].Name) } - - du := et.Sub(et) - sel := qc.Query.Selects[0] - - t := &trace{ - Version: 1, - StartTime: st, - EndTime: et, - Duration: du, - Execution: execution{ - []resolver{ - resolver{ - Path: []string{sel.Table}, - ParentType: "Query", - FieldName: sel.Table, - ReturnType: "object", - StartOffset: 1, - Duration: du, - }, - }, - }, - } - - return t + return f }