From ed24cb41332a0ec8c87f9e987daedf31c0dae5eb Mon Sep 17 00:00:00 2001 From: ispiroglu Date: Wed, 25 Sep 2024 00:00:24 +0300 Subject: [PATCH] docs: add client documentation --- docs/modules/client.md | 149 ++++++++++++++++++ example/client-server/client/client.go | 4 +- .../client-server/client/client_wrapper.go | 30 ++++ example/client-server/client/error_decoder.go | 15 ++ modules/client/driver.go | 1 + modules/common/ctxvaluer/valuer.go | 5 +- 6 files changed, 201 insertions(+), 3 deletions(-) create mode 100644 docs/modules/client.md create mode 100644 example/client-server/client/client_wrapper.go create mode 100644 example/client-server/client/error_decoder.go diff --git a/docs/modules/client.md b/docs/modules/client.md new file mode 100644 index 0000000..dee6747 --- /dev/null +++ b/docs/modules/client.md @@ -0,0 +1,149 @@ +# Client + +The client module enables your application to interact with other web services over HTTP using the [Resty](https://github.com/go-resty/resty) library. + +This module simplifies the process of creating an HTTP client. It reads the base URL and timeout configurations from a config file. To create a client, you need to add the module to your application as follows: + +```go + + app := chaki.New() + + app.Use( + // ... + client.Module(), + // ... + ) + + //... +``` + +To create a client, you can use the following code: + +```go +type exampleClient struct { + *client.Base +} + +func newClient(f *client.Factory) *exampleClient { + return &exampleClient{ + Base: f.Get("example-client"), + } +} +``` + +the `example-client` name should match the name in the config file to configure the client from the config file: +```yaml +client: + example-client: + baseUrl: "http://super-duper-client-url.com" + timeout: 500ms +``` + +To create a request, you should use the following code. This ensures that tracing and other metadata are used on the request as all metadata is under context. + +```go +func (cl *exampleClient) SendHello(ctx context.Context) (string, error) { + resp := &response.Response[string]{} + + request := cl.Request(ctx) // this gives you an *resty.Request, to work with. + + if _, err := request. + SetResult(resp). + Get("/hello"); err != nil { + return "", err + } + + return resp.Data, nil +} +``` + +If you want to log every outgoing request and incoming response, you can simply set `logging` key to `true` on config. +```yaml + client: + example-client: + baseUrl: "http://super-duper-client-url.com" + timeout: 500ms + logging: true +``` +--- +## Error Handler + +By default, Chaki provides a built-in error handler to encapsulate incoming errors. The source code can be found in `modules/client/errors.go`. To avoid log chaos, error cases are not logged by default. + +To access the details of the errors, you can cast the error type into `GenericClientError` type as follows: +```go + + _, err := cl.SendSomeRequest() + genericError := client.GenericClientError{} + errors.As(err, genericError) + logger.From(ctx).Error(genericError.ClientName) + +``` + +### Providing error handler +You can provide a custom error handler to handle errors in a more specific way. The error handler function should accept a `context.Context` and a `*resty.Response` as parameters. +```go +func newClient(f *client.Factory) *exampleClient { + return &exampleClient{ + Base: f.Get("example-client", client.WithErrDecoder(customErrorDecoder)), + } +} + +func customErrorDecoder(_ context.Context, res *resty.Response) error { + if res.StatusCode() == 404 { + return fmt.Errorf("not found") + } + return nil +} +``` + +--- + +## Wrappers + +You can add wrappers to clients to extend their functionality. Chaki provides a default wrapper that adds the following headers to requests if the corresponding values are present in the context: +```go + CorrelationIDKey = "x-correlationId" + ExecutorUserKey = "x-executor-user" + AgentNameKey = "x-agentname" + OwnerKey = "x-owner" +``` + +### Providing an wrapper + +You can wrap the existing client as follows. +```go + + +type user struct { + publicUsername string + publicTag string +} + +func HeaderWrapper() client.DriverWrapper { + return func(restyClient *resty.Client) *resty.Client { + return restyClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { + ctx := r.Context() + + h := map[string]string{} + + if v := ctx.Value("user"); v != nil { + user := v.(user) + h["publicUsername"] = user.publicUsername + h["publicTag"] = user.publicTag + } + + r.SetHeaders(h) + return nil + }) + } +} + +func newClient(f *client.Factory) *exampleClient { + return &exampleClient{ + Base: f.Get("example-client", + client.WithDriverWrappers(HeaderWrapper())), + } +} + +``` diff --git a/example/client-server/client/client.go b/example/client-server/client/client.go index dcc2fb4..578ee58 100644 --- a/example/client-server/client/client.go +++ b/example/client-server/client/client.go @@ -14,7 +14,9 @@ type exampleClient struct { func newClient(f *client.Factory) *exampleClient { return &exampleClient{ - Base: f.Get("example-client"), + Base: f.Get("example-client", + client.WithErrDecoder(customErrorDecoder), + client.WithDriverWrappers(HeaderWrapper())), } } diff --git a/example/client-server/client/client_wrapper.go b/example/client-server/client/client_wrapper.go new file mode 100644 index 0000000..a40873f --- /dev/null +++ b/example/client-server/client/client_wrapper.go @@ -0,0 +1,30 @@ +package main + +import ( + "github.com/Trendyol/chaki/modules/client" + "github.com/go-resty/resty/v2" +) + +type user struct { + publicUsername string + publicTag string +} + +func HeaderWrapper() client.DriverWrapper { + return func(restyClient *resty.Client) *resty.Client { + return restyClient.OnBeforeRequest(func(c *resty.Client, r *resty.Request) error { + ctx := r.Context() + + h := map[string]string{} + + if v := ctx.Value("user"); v != nil { + user := v.(user) + h["publicUsername"] = user.publicUsername + h["publicTag"] = user.publicTag + } + + r.SetHeaders(h) + return nil + }) + } +} diff --git a/example/client-server/client/error_decoder.go b/example/client-server/client/error_decoder.go new file mode 100644 index 0000000..bd19de7 --- /dev/null +++ b/example/client-server/client/error_decoder.go @@ -0,0 +1,15 @@ +package main + +import ( + "context" + "fmt" + + "github.com/go-resty/resty/v2" +) + +func customErrorDecoder(_ context.Context, res *resty.Response) error { + if res.StatusCode() == 404 { + return fmt.Errorf("not found") + } + return nil +} diff --git a/modules/client/driver.go b/modules/client/driver.go index 84dd113..bffb8a4 100644 --- a/modules/client/driver.go +++ b/modules/client/driver.go @@ -20,6 +20,7 @@ func newDriverBuilder(cfg *config.Config) *driverBuilder { d := resty.New(). SetBaseURL(cfg.GetString("baseurl")). SetTimeout(cfg.GetDuration("timeout")). + // Debug mode provides a logging, but it's not in the same format with our logger. SetDebug(cfg.GetBool("debug")) return &driverBuilder{ diff --git a/modules/common/ctxvaluer/valuer.go b/modules/common/ctxvaluer/valuer.go index 8f8023a..97b1a96 100644 --- a/modules/common/ctxvaluer/valuer.go +++ b/modules/common/ctxvaluer/valuer.go @@ -11,10 +11,11 @@ import ( const ( CorrelationIDKey = "x-correlationId" ExecutorUserKey = "x-executor-user" - TraceIDKey = "trace-id" - SpanIDKey = "span-id" AgentNameKey = "x-agentname" OwnerKey = "x-owner" + + TraceIDKey = "trace-id" + SpanIDKey = "span-id" ) var (