Skip to content

Commit

Permalink
add opentelemetry-jaeger-tracing example
Browse files Browse the repository at this point in the history
  • Loading branch information
imroc committed Aug 9, 2022
1 parent ac9ac4d commit 0ca4e58
Show file tree
Hide file tree
Showing 5 changed files with 727 additions and 0 deletions.
30 changes: 30 additions & 0 deletions examples/opentelemetry-jaeger-tracing/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# opentelemetry-jaeger-tracing

This is a runnable example of req, which uses the built-in tiny github sdk built on req to query and display the information of the specified user.

Best of all, it integrates seamlessly with jaeger tracing and is very easy to extend.

## How to run

First, use `docker` or `podman` to start a test jeager container (see jeager official doc: [ Getting Started](https://www.jaegertracing.io/docs/1.37/getting-started/#all-in-one)).

Then, run example:

```bash
go run .
```
```txt
Please give a github username:
```

Input a github username, e.g. `imroc`:

```bash
$ go run .
Please give a github username: imroc
The moust popular repo of roc (https://imroc.cc) is req, which have 2500 stars
```

Then enter the Jaeger UI with browser (`http://127.0.0.1:16686/`), checkout the tracing details.

Run example again, try to input some username that doesn't exist, and check the error log in Jaeger UI.
188 changes: 188 additions & 0 deletions examples/opentelemetry-jaeger-tracing/github/github.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
package github

import (
"context"
"fmt"
"github.com/imroc/req/v3"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/codes"
"go.opentelemetry.io/otel/trace"
"strconv"
"strings"
)

// Client is the go client for GitHub API.
type Client struct {
*req.Client
}

// NewClient create a GitHub client.
func NewClient() *Client {
c := req.C().
// All GitHub API requests need this header.
SetCommonHeader("Accept", "application/vnd.github.v3+json").
// All GitHub API requests use the same base URL.
SetBaseURL("https://api.github.com").
// EnableDump at the request level in request middleware which dump content into
// memory (not print to stdout), we can record dump content only when unexpected
// exception occurs, it is helpful to troubleshoot problems in production.
OnBeforeRequest(func(c *req.Client, r *req.Request) error {
if r.RetryAttempt > 0 { // Ignore on retry.
return nil
}
r.EnableDump()
return nil
}).
// Unmarshal response body into an APIError struct when status >= 400.
SetCommonError(&APIError{}).
// Handle common exceptions in response middleware.
OnAfterResponse(func(client *req.Client, resp *req.Response) error {
if resp.Err != nil { // Ignore if there is an underlying error.
return nil
}
if err, ok := resp.Error().(*APIError); ok { // Server returns an error message.
// Convert it to human-readable go error.
resp.Err = err
return nil
}
// Corner case: neither an error response nor a success response,
// dump content to help troubleshoot.
if !resp.IsSuccess() {
return fmt.Errorf("bad response, raw dump:\n%s", resp.Dump())
}
return nil
})

return &Client{
Client: c,
}
}

// LoginWithToken login with GitHub personal access token.
// GitHub API doc: https://docs.github.com/en/rest/overview/other-authentication-methods#authenticating-for-saml-sso
func (c *Client) LoginWithToken(token string) *Client {
c.SetCommonHeader("Authorization", "token "+token)
return c
}

// APIError represents the error message that GitHub API returns.
// GitHub API doc: https://docs.github.com/en/rest/overview/resources-in-the-rest-api#client-errors
type APIError struct {
Message string `json:"message"`
DocumentationUrl string `json:"documentation_url,omitempty"`
Errors []struct {
Resource string `json:"resource"`
Field string `json:"field"`
Code string `json:"code"`
} `json:"errors,omitempty"`
}

// Error convert APIError to a human readable error and return.
func (e *APIError) Error() string {
msg := fmt.Sprintf("API error: %s", e.Message)
if e.DocumentationUrl != "" {
return fmt.Sprintf("%s (see doc %s)", msg, e.DocumentationUrl)
}
if len(e.Errors) == 0 {
return msg
}
errs := []string{}
for _, err := range e.Errors {
errs = append(errs, fmt.Sprintf("resource:%s field:%s code:%s", err.Resource, err.Field, err.Code))
}
return fmt.Sprintf("%s (%s)", msg, strings.Join(errs, " | "))
}

// SetDebug enable debug if set to true, disable debug if set to false.
func (c *Client) SetDebug(enable bool) *Client {
if enable {
c.EnableDebugLog()
c.EnableDumpAll()
} else {
c.DisableDebugLog()
c.DisableDumpAll()
}
return c
}

type apiNameType int

const apiNameKey apiNameType = iota

// SetTracer set the tracer of opentelemetry.
func (c *Client) SetTracer(tracer trace.Tracer) {
c.WrapRoundTripFunc(func(rt req.RoundTripper) req.RoundTripFunc {
return func(r *req.Request) (resp *req.Response, err error) {
ctx := r.Context()
spanName := ctx.Value(apiNameKey).(string)
_, span := tracer.Start(r.Context(), spanName)
defer span.End()
span.SetAttributes(
attribute.String("http.url", r.URL.String()),
attribute.String("http.method", r.Method),
attribute.String("http.req.header", r.HeaderToString()),
)
if len(r.Body) > 0 {
span.SetAttributes(
attribute.String("http.req.body", string(r.Body)),
)
}
resp, err = rt.RoundTrip(r)
if err != nil {
span.RecordError(err)
span.SetStatus(codes.Error, err.Error())
return
}
span.SetAttributes(
attribute.Int("http.status_code", resp.StatusCode),
attribute.String("http.resp.header", resp.HeaderToString()),
attribute.String("resp.resp.body", resp.String()),
)
return
}
})
}

func withAPIName(ctx context.Context, name string) context.Context {
if ctx == nil {
ctx = context.Background()
}
return context.WithValue(ctx, apiNameKey, name)
}

type UserProfile struct {
Name string `json:"name"`
Blog string `json:"blog"`
}

// GetUserProfile returns the user profile for the specified user.
// Github API doc: https://docs.github.com/en/rest/users/users#get-a-user
func (c *Client) GetUserProfile(ctx context.Context, username string) (user *UserProfile, err error) {
err = c.Get("/users/{username}").
SetPathParam("username", username).
Do(withAPIName(ctx, "GetUserProfile")).
Into(&user)
return
}

type Repo struct {
Name string `json:"name"`
Star int `json:"stargazers_count"`
}

// ListUserRepo returns a list of public repositories for the specified user
// Github API doc: https://docs.github.com/en/rest/repos/repos#list-repositories-for-a-user
func (c *Client) ListUserRepo(ctx context.Context, username string, page int) (repos []*Repo, err error) {
err = c.Get("/users/{username}/repos").
SetQueryParamsAnyType(map[string]any{
"type": "owner",
"page": strconv.Itoa(page),
"per_page": "100",
"sort": "updated",
"direction": "desc",
}).
SetPathParam("username", username).
Do(withAPIName(ctx, "ListUserRepo")).
Into(&repos)
return
}
38 changes: 38 additions & 0 deletions examples/opentelemetry-jaeger-tracing/go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
module opentelemetry-jaeger-tracing

go 1.18

replace github.com/imroc/req/v3 => ../../

require (
github.com/imroc/req/v3 v3.0.0
go.opentelemetry.io/otel v1.9.0
go.opentelemetry.io/otel/exporters/jaeger v1.9.0
go.opentelemetry.io/otel/sdk v1.9.0
go.opentelemetry.io/otel/trace v1.9.0
)

require (
github.com/cheekybits/genny v1.0.0 // indirect
github.com/fsnotify/fsnotify v1.5.4 // indirect
github.com/go-logr/logr v1.2.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-task/slim-sprig v0.0.0-20210107165309-348f09dbbbc0 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/lucas-clemente/quic-go v0.28.1 // indirect
github.com/marten-seemann/qpack v0.2.1 // indirect
github.com/marten-seemann/qtls-go1-16 v0.1.5 // indirect
github.com/marten-seemann/qtls-go1-17 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-18 v0.1.2 // indirect
github.com/marten-seemann/qtls-go1-19 v0.1.0 // indirect
github.com/nxadm/tail v1.4.8 // indirect
github.com/onsi/ginkgo v1.16.5 // indirect
golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa // indirect
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4 // indirect
golang.org/x/net v0.0.0-20220809012201-f428fae20770 // indirect
golang.org/x/sys v0.0.0-20220808155132-1c4a2a72c664 // indirect
golang.org/x/text v0.3.7 // indirect
golang.org/x/tools v0.1.12 // indirect
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 // indirect
)
Loading

0 comments on commit 0ca4e58

Please sign in to comment.