Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixes and refactors trace v2 #288

Merged
merged 4 commits into from
Dec 1, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions gokitmiddlewares/loggingmiddleware/loggercontext.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/arquivei/foundationkit/request"
"github.com/arquivei/foundationkit/trace"
tracev2 "github.com/arquivei/foundationkit/trace/v2"
"github.com/rs/zerolog"
"github.com/rs/zerolog/log"
)
Expand Down Expand Up @@ -36,13 +37,17 @@ func enrichLoggerContext(ctx context.Context, l *zerolog.Logger, c Config, req i
if rid := request.GetIDFromContext(ctx); !rid.IsEmpty() {
zctx = zctx.EmbedObject(request.GetIDFromContext(ctx))
} else {
log.Warn().Msg("Request doesn't have a Request ID! Did you forget to use trackingmiddleware on the transport layer?")
log.Warn().Msg("[foundationkit:loggingmiddleware] Request doesn't have a Request ID! Did you forget to use trackingmiddleware on the transport layer?")
}

if t := trace.GetFromContext(ctx); !trace.IDIsEmpty(t.ID) {
zctx = zctx.EmbedObject(trace.GetFromContext(ctx))
if t := tracev2.GetTraceInfoFromContext(ctx); traceInfoIsEmpty(t) {
zctx = zctx.EmbedObject(t)
} else {
log.Warn().Msg("Request doesn't have a trace! Did you forget to use trackingmiddleware on the transport layer?")
if t := trace.GetFromContext(ctx); !trace.IDIsEmpty(t.ID) {
zctx = zctx.EmbedObject(trace.GetFromContext(ctx))
} else {
log.Warn().Msg("[foundationkit:loggingmiddleware] Request doesn't have a trace! Did you forget to use trackingmiddleware on the transport layer?")
}
}

return zctx.Str("endpoint_name", c.Name)
Expand All @@ -57,3 +62,7 @@ func enrichLoggerAfterResponse(l *zerolog.Logger, c Config, begin time.Time, res
return zctx.Dur("endpoint_took", time.Since(begin))
})
}

func traceInfoIsEmpty(t tracev2.TraceInfo) bool {
return t.ID != "00000000000000000000000000000000"
}
41 changes: 40 additions & 1 deletion httpmiddlewares/trackingmiddleware/middleware.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,26 @@ import (

"github.com/arquivei/foundationkit/request"
"github.com/arquivei/foundationkit/trace"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
otrace "go.opentelemetry.io/otel/trace"
)

// New instantiates a new tracking middleware wrapping the @next handler.
func New(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
ctx = request.WithNewID(ctx)
ctx = trace.WithTrace(ctx, trace.GetFromHTTPRequest(r))
t := trace.GetFromHTTPRequest(r)
ctx = trace.WithTrace(ctx, t)

// We fetch trace id from context because WithTrace
// will initialize a trace if it is empty.
t = trace.GetFromContext(ctx)
translateTraceV1ToTraceV2Headers(t, r)

ctx = otel.GetTextMapPropagator().Extract(ctx, propagation.HeaderCarrier(r.Header))
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(w.Header()))

request.SetInHTTPResponse(request.GetIDFromContext(ctx), w)
trace.SetInHTTPResponse(trace.GetFromContext(ctx), w)
Expand All @@ -21,3 +33,30 @@ func New(next http.Handler) http.Handler {
next.ServeHTTP(w, r)
})
}

func translateTraceV1ToTraceV2Headers(tv1 trace.Trace, r *http.Request) {
if r.Header.Get("traceparent") != "" {
return
}

if trace.IDIsEmpty(tv1.ID) {
return
}

tv2, err := otrace.TraceIDFromHex(tv1.ID.String())
if err != nil {
return
}

// Because we don't have a valid span id, lets fake one using the
// beginning of the trace id.
sp := otrace.SpanID(tv2[0:16])

// For now, the probability is being handled as a boolean. Anything
// higher than zero will be sampled.
p := "00"
if tv1.ProbabilitySample != nil && *tv1.ProbabilitySample == 1 {
p = "01"
}
r.Header.Set("traceparent", "00-"+tv2.String()+"-"+sp.String()+"-"+p)
}
26 changes: 26 additions & 0 deletions request/http.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ import (
"net/http"

"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)

const (
Expand Down Expand Up @@ -68,3 +70,27 @@ func SetInHTTPResponse(id ID, response http.ResponseWriter) {

response.Header().Set(HTTPHeaderID, id.String())
}

// HTTPMiddleware returns an http middleware that adds a request id
// to the context of the request and the same id in the header of the
// http response. If there is an active trace span, the request id is
// also registred as an attribute 'request.id'. It's importante that
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "importante" -> "important"

// this middleware comes after the trace middleware.
func HTTPMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
id := newID()

ctx := r.Context()
ctx = WithID(ctx, id)
r = r.WithContext(ctx)

SetInHTTPResponse(id, w)

if span := trace.SpanFromContext(ctx); span.IsRecording() {
span.SetAttributes(attribute.String("request.id", id.String()))
}

next.ServeHTTP(w, r)
})
}
9 changes: 6 additions & 3 deletions trace/v2/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@ This lib provides a `Config` struct and this section will explain how to properl
### Provider

1. `stackdriver` to export the trace to stackdriver
2. Empty string to not export the trace, but the trace will be created
2. `otlp` to export using OpenTelemetry Protocol.
3. Empty string to not export the trace, but the trace will be created

### Probability sample

Expand Down Expand Up @@ -74,7 +75,7 @@ ctx, span := trace.Start(ctx, "SPAN-NAME")
defer span.End()
```

Recovering trace informations and logging it
Recovering trace information and logging it

```golang
type TraceInfo struct {
Expand All @@ -88,6 +89,8 @@ t := trace.GetTraceInfoFromContext(ctx)
log.Ctx(ctx).Info().EmbedObject(t).Msg("Hello")
```

Refer to `examples/` directory for a working example.

## Propagating the trace

### API
Expand Down Expand Up @@ -138,4 +141,4 @@ func encodeResponse(

### Workers

[WIP]
For exporting the trace you can use `trace.ToMap` and `trace.FromMap` to export the necessary information to a `map[string]string` that could be marshaled into messages or used as message metadata if you broker supports it.
78 changes: 56 additions & 22 deletions trace/v2/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,58 +2,92 @@ package trace

import (
"errors"
"fmt"
"strings"

"github.com/arquivei/foundationkit/app"
"github.com/rs/zerolog/log"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/propagation"
"go.opentelemetry.io/otel/sdk/resource"
"go.opentelemetry.io/otel/sdk/trace"
semconv "go.opentelemetry.io/otel/semconv/v1.21.0"
)

// Config represents all trace's configuration
type Config struct {
Exporter string `default:""`
ProbabilitySample float64 `default:"0"`
ServiceName string
ServiceVersion string
Exporter string
ProbabilitySample float64
Stackdriver StackdriverConfig
OTLP OTLPConfig
}

// Setup use Config to setup an trace exporter and returns a shutdown handler
func Setup(c Config) app.ShutdownFunc {
exporter, err := newExporter(c)

if err != nil {
log.Fatal().Err(err).Msg("Failed to create trace exporter")
}

res, err := newResource(c.ServiceName, c.ServiceVersion)
if err != nil {
log.Fatal().Err(err).Msg("Failed to create trace resource")
}

tp := newTraceProvider(res, c.ProbabilitySample, exporter)

otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(newPropagator())

return tp.ForceFlush
}

func newResource(serviceName, serviceVersion string) (*resource.Resource, error) {
return resource.Merge(resource.Default(),
resource.NewWithAttributes(semconv.SchemaURL,
semconv.ServiceName(serviceName),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are serviceName and serviceVersion required? If they are, does resource return a good error message? If not, a custom error message would make it quick to someone who is using fkit without deep knowledge of this source code to know which configurations are missing

semconv.ServiceVersion(serviceVersion),
))
}

func newPropagator() propagation.TextMapPropagator {
return propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
)
}

func newTraceProvider(res *resource.Resource, prob float64, exporter trace.SpanExporter) *trace.TracerProvider {
return trace.NewTracerProvider(
trace.WithResource(res),
trace.WithSampler(
trace.ParentBased(trace.TraceIDRatioBased(prob)),
),
trace.WithBatcher(exporter),
)
}

func newExporter(c Config) (trace.SpanExporter, error) {
var exporter trace.SpanExporter
var err error

exporterName := strings.ToLower(c.Exporter)
switch exporterName {
switch exporterName := strings.ToLower(c.Exporter); exporterName {
case "stackdriver":
exporter, err = newStackdriverExporter(c.Stackdriver)
case "otlp":
exporter, err = newOTLPExporter(c.OTLP)
case "":
// No trace will be exported, but will be created
default:
err = errors.New("invalid exporter")
err = errors.New("invalid exporter name: " + exporterName)
}

if err != nil {
log.Fatal().Str("exporter", exporterName).Err(err).Msg("Failed to create trace exporter")
return nil, fmt.Errorf("creating trace exporter: %w", err)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: here it could use errors.Errorf in order to avoid importing fmt, as errors is already imported

}

tp := trace.NewTracerProvider(
trace.WithSampler(
trace.ParentBased(trace.TraceIDRatioBased(c.ProbabilitySample)),
),
trace.WithBatcher(exporter),
)

otel.SetTracerProvider(tp)
otel.SetTextMapPropagator(
propagation.NewCompositeTextMapPropagator(
propagation.TraceContext{},
propagation.Baggage{},
),
)

return tp.ForceFlush
return exporter, nil
}
12 changes: 2 additions & 10 deletions trace/v2/examples/Makefile
Original file line number Diff line number Diff line change
@@ -1,15 +1,7 @@
.PHONY: send send-%

send:
$(eval URL := http://localhost:8686/ping/v1)
@if [ -f "$(R)" ] ; then \
cat $(R) | curl -v -H 'Content-Type: application/json' -d @- "$(URL)"; \
else \
echo "Invalid request file '$(R)'" > /dev/stderr; \
fi

send-without-trace: R ?= docs/requests/3-5000.json
send-without-trace: send
docker compose run sender

run:
GOOGLE_APPLICATION_CREDENTIALS=gcp-credentials.json go run cmd/api/*.go
docker compose run api
28 changes: 26 additions & 2 deletions trace/v2/examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,30 @@

## API

This example will create an API (ping) that configures the trace, creates a span and propagates to another API (pong). The ping API is configured to use it self as pong API.
This example will create an API (ping) that configures the trace, creates a span and propagates to another API (pong). The ping API is configured to use itself as pong API.

The whole example runs in docker with a [jaeger](https://www.jaegertracing.io/) collecting the traces.

To run the example, run `make run` from this directory to create the API.

Wait for the API to start serving requests. It will print the message `Application main loop starting now!` on the logs.

After that, send a request using `make send`. You can change the parameters of the request on the `docker-compose.yaml` file.

Them visit http://localhost:16686/ to see the traces.

## Example V1 compatibility

In the file `trace/v2/examples/cmd/api/resources.go` we apply the new middlewares. But if you want to test with the old `trackingmiddleware.New`, comment the trace and request middlewares and uncomment the trackingmiddleware.

``` go
r.Use(
// This is deprecated. Used when we can't ditch trace v1.
// trackingmiddleware.New,
// This is the preferred way
trace.MuxHTTPMiddleware(""),
request.HTTPMiddleware,
enrichloggingmiddleware.New,
)
```

To run the example, go to examples folder and run `make run` to create the API. After that, send a request using `make send-without-trace`.
12 changes: 12 additions & 0 deletions trace/v2/examples/cmd/api/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
FROM golang:1.21-alpine

WORKDIR /usr/src/app

# pre-copy/cache go.mod for pre-downloading dependencies and only redownloading them in subsequent builds if they change
COPY go.mod go.sum ./
RUN go mod download && go mod verify

COPY . .
RUN go build -v -o /usr/local/bin/app ./trace/v2/examples/cmd/api

CMD ["app"]
4 changes: 3 additions & 1 deletion trace/v2/examples/cmd/api/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"time"

"github.com/arquivei/foundationkit/log"
tracev1 "github.com/arquivei/foundationkit/trace"
"github.com/arquivei/foundationkit/trace/v2"
)

Expand All @@ -24,5 +25,6 @@ var config struct {
}
}

Trace trace.Config
Trace trace.Config
TraceV1 tracev1.Config
}
2 changes: 2 additions & 0 deletions trace/v2/examples/cmd/api/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ const (

func main() {
app.SetupConfig(&config)
config.Trace.ServiceName = "ping-pong"
config.Trace.ServiceVersion = version
ctx := fklog.SetupLoggerWithContext(context.Background(), config.Log, version)

log.Ctx(ctx).Info().Str("config", fklog.Flatten(config)).Msg("Configuration")
Expand Down
Loading
Loading