Skip to content
This repository has been archived by the owner on Mar 18, 2022. It is now read-only.

Commit

Permalink
add optional request stats from httptrace
Browse files Browse the repository at this point in the history
  • Loading branch information
mkungla committed Feb 6, 2022
1 parent ce15f05 commit 8c8832d
Show file tree
Hide file tree
Showing 9 changed files with 395 additions and 188 deletions.
60 changes: 32 additions & 28 deletions block.go
Original file line number Diff line number Diff line change
Expand Up @@ -87,93 +87,97 @@ type (
)

// GetBlocks returns summarised details about all blocks (paginated - latest first).
func (c *Client) GetBlocks(ctx context.Context) (*BlocksResponse, error) {
rsp, err := c.GET(ctx, "/blocks")
func (c *Client) GetBlocks(ctx context.Context) (res *BlocksResponse, err error) {
res = &BlocksResponse{}
rsp, err := c.request(ctx, &res.Response, "GET", nil, "/blocks")
if err != nil {
return nil, err
res.applyError(nil, err)
return
}
res := &BlocksResponse{}
res.setStatus(rsp)

body, err := readResponseBody(rsp)
if err != nil {
return nil, err
res.applyError(body, err)
return
}
if err := json.Unmarshal(body, &res.Blocks); err != nil {
if err = json.Unmarshal(body, &res.Blocks); err != nil {
res.applyError(body, err)
return res, nil
return
}

return res, nil
res.ready()
return
}

// GetBlockInfo returns detailed information about a specific block.
func (c *Client) GetBlockInfo(ctx context.Context, hash BlockHash) (*BlockInfoResponse, error) {
func (c *Client) GetBlockInfo(ctx context.Context, hash BlockHash) (res *BlockInfoResponse, err error) {
res = &BlockInfoResponse{}
params := url.Values{}
params.Set("_block_hash", string(hash))

rsp, err := c.GET(ctx, "/block_info", params)
rsp, err := c.request(ctx, &res.Response, "GET", nil, "/block_info", params)
if err != nil {
return nil, err
return
}
res := &BlockInfoResponse{}

res.setStatus(rsp)
body, err := readResponseBody(rsp)
if err != nil {
return nil, err
res.applyError(nil, err)
return
}

blockpl := []Block{}

if err := json.Unmarshal(body, &blockpl); err != nil {
if err = json.Unmarshal(body, &blockpl); err != nil {
res.applyError(body, err)
return res, nil
return
}

if rsp.StatusCode != http.StatusOK {
res.applyError(body, err)
return res, nil
return
}
if len(blockpl) == 1 {
res.Block = &blockpl[0]
}
res.ready()
return res, nil
}

// GetBlockTxs returns a list of all transactions included in a provided block.
func (c *Client) GetBlockTxs(ctx context.Context, hash BlockHash) (*BlockTxsResponse, error) {
func (c *Client) GetBlockTxs(ctx context.Context, hash BlockHash) (res *BlockTxsResponse, err error) {
res = &BlockTxsResponse{}
params := url.Values{}
params.Set("_block_hash", string(hash))

rsp, err := c.GET(ctx, "/block_txs", params)
rsp, err := c.request(ctx, &res.Response, "GET", nil, "/block_txs", params)
if err != nil {
res.applyError(nil, err)
return nil, err
}
res := &BlockTxsResponse{}

res.setStatus(rsp)
body, err := readResponseBody(rsp)
if err != nil {
return nil, err
res.applyError(body, err)
return
}

blockTxs := []struct {
Hash TxHash `json:"tx_hash"`
}{}

if err := json.Unmarshal(body, &blockTxs); err != nil {
if err = json.Unmarshal(body, &blockTxs); err != nil {
res.applyError(body, err)
return res, nil
return
}

if rsp.StatusCode != http.StatusOK {
res.applyError(body, err)
return res, nil
return
}
if len(blockTxs) > 0 {
for _, tx := range blockTxs {
res.Txs = append(res.Txs, tx.Hash)
}
}
res.ready()
return res, nil
}
104 changes: 96 additions & 8 deletions client.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,68 @@ package koios

import (
"context"
"crypto/tls"
"fmt"
"io"
"net/http"
"net/http/httptrace"
"net/url"
"strings"
"time"
)

// GET sends api http get request to provided relative path with query params
// HEAD sends api http HEAD request to provided relative path with query params
// and returns an HTTP response.
func (c *Client) HEAD(ctx context.Context, path string, query ...url.Values) (*http.Response, error) {
return c.request(ctx, nil, "HEAD", nil, path, query...)
}

// POST sends api http POST request to provided relative path with query params
// and returns an HTTP response. When using POST method you are expected
// to handle the response according to net/http.Do documentation.
// e.g. Caller should close resp.Body when done reading from it.
func (c *Client) POST(ctx context.Context, body io.Reader, path string, query ...url.Values) (*http.Response, error) {
return c.request(ctx, nil, "POST", body, path, query...)
}

// GET sends api http GET request to provided relative path with query params
// and returns an HTTP response. When using GET method you are expected
// to handle the response according to net/http.Do documentation.
// e.g. Caller should close resp.Body when done reading from it.
func (c *Client) GET(ctx context.Context, path string, query ...url.Values) (*http.Response, error) {
return c.request(ctx, "GET", nil, path, query...)
return c.request(ctx, nil, "GET", nil, path, query...)
}

func (c *Client) request(
ctx context.Context,
res *Response,
m string,
body io.Reader,
p string,
query ...url.Values) (*http.Response, error) {
var (
requrl *url.URL
requrl string
)

p = strings.TrimLeft(p, "/")
c.mux.RLock()
switch len(query) {
case 0:
requrl = c.url.ResolveReference(&url.URL{Path: p})
requrl = c.url.ResolveReference(&url.URL{Path: p}).String()
case 1:
requrl = c.url.ResolveReference(&url.URL{Path: p, RawQuery: query[0].Encode()})
requrl = c.url.ResolveReference(&url.URL{Path: p, RawQuery: query[0].Encode()}).String()
default:
c.mux.RUnlock()
return nil, fmt.Errorf("%w: got %d", ErrURLValuesLenght, len(query))
err := fmt.Errorf("%w: got %d", ErrURLValuesLenght, len(query))
if res != nil {
res.applyError(nil, err)
}
return nil, err
}
if res != nil {
res.RequestURL = requrl
}

c.mux.RUnlock()

// optain lock to update last ts and total
Expand All @@ -72,12 +97,75 @@ func (c *Client) request(
// Release client so that other requests can use it.
c.mux.Unlock()

req, err := http.NewRequestWithContext(ctx, strings.ToUpper(m), requrl.String(), body)
req, err := http.NewRequestWithContext(ctx, strings.ToUpper(m), requrl, body)
if err != nil {
if res != nil {
res.applyError(nil, err)
}
return nil, err
}

if res != nil && c.reqStatsEnabled {
return c.requestWithStats(req, res)
}

rsp, err := c.client.Do(req)
if err != nil {
if res != nil {
res.applyError(nil, err)
}
return nil, err
}

if res != nil {
res.applyRsp(rsp)
}
return rsp, nil
}

func (c *Client) requestWithStats(req *http.Request, res *Response) (*http.Response, error) {
res.Stats = &RequestStats{}
var dns, tlshs, connect time.Time
trace := &httptrace.ClientTrace{
DNSStart: func(dsi httptrace.DNSStartInfo) {
dns = time.Now().UTC()
},
DNSDone: func(ddi httptrace.DNSDoneInfo) {
res.Stats.DNSLookupDur = time.Since(dns)
},
TLSHandshakeStart: func() {
tlshs = time.Now().UTC()
},
TLSHandshakeDone: func(cs tls.ConnectionState, err error) {
if err != nil {
res.applyError(nil, err)
}
res.Stats.TLSHSDur = time.Since(tlshs)
},
ConnectStart: func(network, addr string) {
connect = time.Now().UTC()
},
ConnectDone: func(network, addr string, err error) {
if err != nil {
res.applyError(nil, err)
}
res.Stats.ESTCXNDur = time.Since(connect)
},
GotFirstResponseByte: func() {
res.Stats.TTFB = time.Since(res.Stats.ReqStartedAt)
},
}
req = req.WithContext(httptrace.WithClientTrace(req.Context(), trace))

res.Stats.ReqStartedAt = time.Now().UTC()
rsp, err := c.client.Transport.RoundTrip(req)
if err != nil {
res.applyError(nil, err)
return nil, err
}

return c.client.Do(req)
res.applyRsp(rsp)
return rsp, nil
}

// BaseURL returns currently used base url e.g. https://api.koios.rest/api/v0
Expand Down
26 changes: 23 additions & 3 deletions cmd/koios-rest/cmd-general.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ package main
import (
"context"
"errors"
"fmt"
"io/ioutil"

"github.com/howijd/koios-rest-go-client"
Expand All @@ -28,9 +29,9 @@ import (
func addGeneralCommands(app *cli.App, api *koios.Client) {
app.Commands = append(app.Commands, []*cli.Command{
{
Name: "get",
Usage: "Send get request to API endpoint",
Hidden: true,
Name: "get",
Usage: "get issues a GET request to the specified API endpoint",
Category: "UTILS",
Action: func(ctx *cli.Context) error {
endpoint := ctx.Args().Get(0)
if len(endpoint) == 0 {
Expand All @@ -44,5 +45,24 @@ func addGeneralCommands(app *cli.App, api *koios.Client) {
return nil
},
},
{
Name: "head",
Usage: "head issues a HEAD request to the specified API endpoint",
Category: "UTILS",
Action: func(ctx *cli.Context) error {
endpoint := ctx.Args().Get(0)
if len(endpoint) == 0 {
return errors.New("provide endpoint as argument e.g. /tip")
}
res, err := api.HEAD(context.Background(), endpoint)
handleErr(err)
if res.Body != nil {
res.Body.Close()
}
fmt.Println(res.Request.URL.String())
fmt.Println(res.Status)
return nil
},
},
}...)
}
6 changes: 6 additions & 0 deletions cmd/koios-rest/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ func main() {
handleErr(koios.Schema(c.String("schema"))(api))
handleErr(koios.RateLimit(uint8(c.Uint("rate-limit")))(api))
handleErr(koios.Origin(c.String("origin"))(api))
handleErr(koios.CollectRequestsStats(c.Bool("enable-req-stats"))(api))
return nil
},
}
Expand Down Expand Up @@ -137,5 +138,10 @@ func globalFlags() []cli.Flag {
Usage: "Ugly prints response json strings directly without calling json pretty.",
Value: false,
},
&cli.BoolFlag{
Name: "enable-req-stats",
Usage: "Enable request stats.",
Value: false,
},
}
}
Loading

0 comments on commit 8c8832d

Please sign in to comment.