diff --git a/go.mod b/go.mod index f3a0187..f1ca71c 100644 --- a/go.mod +++ b/go.mod @@ -7,9 +7,9 @@ toolchain go1.21.4 require ( cloud.google.com/go/logging v1.8.1 contrib.go.opencensus.io/exporter/stackdriver v0.13.14 - github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0 github.com/go-kit/kit v0.13.0 github.com/go-kit/log v0.2.1 + github.com/go-logr/zerologr v1.2.3 github.com/gorilla/mux v1.8.1 github.com/hamba/avro/v2 v2.18.0 github.com/oklog/ulid/v2 v2.1.0 @@ -34,7 +34,6 @@ require ( cloud.google.com/go/longrunning v0.5.1 // indirect cloud.google.com/go/monitoring v1.16.0 // indirect cloud.google.com/go/trace v1.10.1 // indirect - github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.45.0 // indirect github.com/aws/aws-sdk-go v1.45.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.8.0 // indirect diff --git a/go.sum b/go.sum index d033628..e5c9f94 100644 --- a/go.sum +++ b/go.sum @@ -18,12 +18,6 @@ cloud.google.com/go/trace v1.10.1/go.mod h1:gbtL94KE5AJLH3y+WVpfWILmqgc6dXcqgNXd contrib.go.opencensus.io/exporter/stackdriver v0.13.14 h1:zBakwHardp9Jcb8sQHcHpXy/0+JIb1M8KjigCJzx7+4= contrib.go.opencensus.io/exporter/stackdriver v0.13.14/go.mod h1:5pSSGY0Bhuk7waTHuDf4aQ8D2DrhgETRo9fy6k3Xlzc= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0 h1:OEgjQy1rH4Fbn5IpuI9d0uhLl+j6DkDvh9Q2Ucd6GK8= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace v1.21.0/go.mod h1:EUfJ8lb3pjD8VasPPwqIvG2XVCE6DOT8tY5tcwbWA+A= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.45.0 h1:/BF7rO6PYcmFoyJrq6HA3LqQpFSQei9aNuO1fvV3OqU= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/cloudmock v0.45.0/go.mod h1:WntFIMzxcU+PMBuekFc34UOsEZ9sP+vsnBYTyaNBkOs= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.45.0 h1:o/Nf55GfyLwGDaHkVAkRGgBXeExce73L6N9w2PZTB3k= -github.com/GoogleCloudPlatform/opentelemetry-operations-go/internal/resourcemapping v0.45.0/go.mod h1:qkFPtMouQjW5ugdHIOthiTbweVHUTqbS0Qsu55KqXks= github.com/VividCortex/gohistogram v1.0.0 h1:6+hBz+qvs0JOrrNhhmR7lFxo5sINxBCGXrdtl/UvroE= github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= github.com/aws/aws-sdk-go v1.45.5 h1:bxilnhv9FngUgdPNJmOIv2bk+2sP0dpqX3e4olhWcGM= @@ -71,6 +65,8 @@ github.com/go-logr/logr v1.3.0 h1:2y3SDp0ZXuc6/cjLSZ+Q3ir+QB9T/iG5yYRXqsagWSY= github.com/go-logr/logr v1.3.0/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-logr/zerologr v1.2.3 h1:up5N9vcH9Xck3jJkXzgyOxozT14R47IyDODz8LM1KSs= +github.com/go-logr/zerologr v1.2.3/go.mod h1:BxwGo7y5zgSHYR1BjbnHPyF/5ZjVKfKxAZANVu6E8Ho= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= github.com/golang/glog v1.1.2 h1:DVjP2PbBOzHyzA+dn3WhHIq4NdVu3Q+pvivFICf/7fo= diff --git a/gokitmiddlewares/loggingmiddleware/loggercontext.go b/gokitmiddlewares/loggingmiddleware/loggercontext.go index f2d1676..5d2d21d 100644 --- a/gokitmiddlewares/loggingmiddleware/loggercontext.go +++ b/gokitmiddlewares/loggingmiddleware/loggercontext.go @@ -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" ) @@ -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) @@ -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" +} diff --git a/httpmiddlewares/trackingmiddleware/middleware.go b/httpmiddlewares/trackingmiddleware/middleware.go index 2c5fb64..5770f50 100644 --- a/httpmiddlewares/trackingmiddleware/middleware.go +++ b/httpmiddlewares/trackingmiddleware/middleware.go @@ -5,6 +5,9 @@ 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. @@ -12,7 +15,16 @@ 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) @@ -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) +} diff --git a/request/http.go b/request/http.go index 4ecbc19..b84128e 100644 --- a/request/http.go +++ b/request/http.go @@ -5,6 +5,8 @@ import ( "net/http" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel/attribute" + "go.opentelemetry.io/otel/trace" ) const ( @@ -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 registered as an attribute 'request.id'. It's important that +// 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) + }) +} diff --git a/trace/v2/README.md b/trace/v2/README.md deleted file mode 100644 index 1ede576..0000000 --- a/trace/v2/README.md +++ /dev/null @@ -1,141 +0,0 @@ -# Trace v2 - -This guide will help you to setup and use the trace/v2 lib. We have a usage example in `examples` folder. - -## Configuring - -This lib provides a `Config` struct and this section will explain how to properly configure. - -### Provider - -1. `stackdriver` to export the trace to stackdriver -2. Empty string to not export the trace, but the trace will be created - -### Probability sample - -1. `0` will not sample or export (if provider was set) -2. Float between 0 and 1 will use the number as a probability for sampling and export (if provider was set) -3. `1` will sample and export (if provider was set) - -### Stackdriver - -Information to connect in stackdriver - -## Setting up and using the lib - -Using in your config.go file - -```golang -var config struct { - // (...) - - Trace trace.Config -} -``` - -Creating a setup method - -```golang -func setupTrace() { - traceShutdown := trace.Setup(config.Trace) - app.RegisterShutdownHandler( - &app.ShutdownHandler{ - Name: "opentelemetry_trace", - Priority: app.ShutdownPriority(shutdownPriorityTrace), - Handler: traceShutdown, - Policy: app.ErrorPolicyAbort, - }) -} -``` - -Setting the trace up AFTER create a new App in main.go - -```golang - -func main() { - app.SetupConfig(&config) - - // (...) - - if err := app.NewDefaultApp(ctx); err != nil { - log.Ctx(ctx).Fatal().Err(err).Msg("Failed to create app") - } - - setupTrace() - - // (...) -} -``` - -Starting a span - -```golang -ctx, span := trace.Start(ctx, "SPAN-NAME") -defer span.End() -``` - -Recovering trace informations and logging it - -```golang -type TraceInfo struct { - ID string - IsSampled bool -} -``` - -```golang -t := trace.GetTraceInfoFromContext(ctx) -log.Ctx(ctx).Info().EmbedObject(t).Msg("Hello") -``` - -## Propagating the trace - -### API - -Using in transport layer - -```golang -func MakeHTTPHandler(e endpoint.Endpoint) http.Handler { - // (...) - - r := mux.NewRouter() - r.Use(trace.MuxHTTPMiddleware("SERVER-NAME")) - - //(...) - return r -} -``` - -Using in a HTTP request - -```golang -request, err := http.NewRequestWithContext( - ctx, - "POST", - url, - bytes.NewReader(body), -) -if err != nil { - return "", errors.E(op, err) -} - -trace.SetTraceInRequest(request) -``` - -Using in a HTTP response - -```golang -func encodeResponse( - ctx context.Context, - w http.ResponseWriter, - r interface{}, -) error { - w.Header().Set("Content-Type", "application/json; charset=utf-8") - trace.SetTraceInResponse(ctx, w) - return json.NewEncoder(w).Encode(r) -} -``` - -### Workers - -[WIP] \ No newline at end of file diff --git a/trace/v2/config.go b/trace/v2/config.go index 397b809..482b387 100644 --- a/trace/v2/config.go +++ b/trace/v2/config.go @@ -1,59 +1,75 @@ package trace import ( - "errors" - "strings" + "context" + "os" "github.com/arquivei/foundationkit/app" + + "github.com/go-logr/zerologr" "github.com/rs/zerolog/log" "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/sdk/trace" ) -// Config represents all trace's configuration -type Config struct { - Exporter string `default:""` - ProbabilitySample float64 `default:"0"` - Stackdriver StackdriverConfig - OTLP OTLPConfig -} - // Setup use Config to setup an trace exporter and returns a shutdown handler -func Setup(c Config) app.ShutdownFunc { - var exporter trace.SpanExporter - var err error - - exporterName := strings.ToLower(c.Exporter) - switch 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") - } +func Setup(ctx context.Context) app.ShutdownFunc { + lintOtelEnvVariables() + + // Set the OpenTelemetry to use foundation's kit default logger. + otel.SetLogger(zerologr.New(&log.Logger)) + otel.SetErrorHandler(otel.ErrorHandlerFunc(func(err error) { + log.Warn().Err(err).Msg("[foundationkit:trace/v2] OpenTelemetry raised an error!") + })) + + exporter, err := otlptracehttp.New(ctx) if err != nil { - log.Fatal().Str("exporter", exporterName).Err(err).Msg("Failed to create trace exporter") + log.Fatal().Err(err).Msg("[foundationkit:trace/v2] Failed to create trace exporter") } - tp := trace.NewTracerProvider( - trace.WithSampler( - trace.ParentBased(trace.TraceIDRatioBased(c.ProbabilitySample)), - ), - trace.WithBatcher(exporter), - ) + tp := trace.NewTracerProvider(trace.WithBatcher(exporter)) otel.SetTracerProvider(tp) - otel.SetTextMapPropagator( - propagation.NewCompositeTextMapPropagator( - propagation.TraceContext{}, - propagation.Baggage{}, - ), - ) + otel.SetTextMapPropagator(newPropagator()) return tp.ForceFlush } + +func newPropagator() propagation.TextMapPropagator { + return propagation.NewCompositeTextMapPropagator( + propagation.TraceContext{}, + propagation.Baggage{}, + ) +} + +// lintOtelEnvVariables logs a warning if an important variable is empty +// If any of these variables are empty, the OpenTelemetry SDK may not +// work as expected. This will not break the code, but could lead +// to loses of traces. +// +// Linted variables: +// - OTEL_SERVICE_NAME: Because defaults to 'unkown_service' and this says nothing about the service. +// - OTEL_EXPORTER_OTLP_ENDPOINT: because it defaults to localhost and could lose exported traces. +// +// Variables that are probably OK being empty: +// - OTEL_TRACES_SAMPLER_ARG - It defaults to "1.0" +// - OTEL_TRACES_SAMPLER - Ut defaults to "parentbased_always_on" +// +// Other variables don't seems to make too much of a difference. +func lintOtelEnvVariables() { + for _, env := range []string{ + "OTEL_SERVICE_NAME", + "OTEL_EXPORTER_OTLP_ENDPOINT", + } { + lintEnvVariable(env) + } +} + +func lintEnvVariable(env string) { + if os.Getenv(env) == "" { + log.Warn().Str("env", env).Msg("[foundationkit:trace/v2] An OpenTelemetry environment variable is empty but it probably shouldn't be. OpenTelemetry will use it's default, but this can lead to traces not being exported or recorded with a wrong name. Please read the docs at https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/.") + } +} diff --git a/trace/v2/doc.go b/trace/v2/doc.go new file mode 100644 index 0000000..422c5c4 --- /dev/null +++ b/trace/v2/doc.go @@ -0,0 +1,134 @@ +/* +# Trace v2 + +This guide will help you to setup and use the trace/v2 lib. We have a usage example in `examples` folder. + +## Configuring + +This lib provides a `Config` struct and this section will explain how to properly configure. + +All configuration is done via environment variables for the OpenTelemetry SDK. Please refer to +https://opentelemetry.io/docs/specs/otel/configuration/sdk-environment-variables/ for more information. + +## Setting up and using the lib + +# Call trace.Setup to initialize the OpenTemetry SDK + +```golang + +traceShutdown := trace.Setup(ctx) +app.RegisterShutdownHandler( + + &app.ShutdownHandler{ + Name: "opentelemetry_trace", + Priority: app.ShutdownPriority(shutdownPriorityTrace), + Handler: traceShutdown, + Policy: app.ErrorPolicyAbort, + }) + +``` + +# Setting the trace up AFTER create a new App in main.go + +```golang + + func main() { + app.SetupConfig(&config) + + // (...) + + if err := app.NewDefaultApp(ctx); err != nil { + log.Ctx(ctx).Fatal().Err(err).Msg("Failed to create app") + } + + setupTrace() + + // (...) + } + +``` + +# Starting a span + +```golang +ctx, span := trace.Start(ctx, "SPAN-NAME") +defer span.End() +``` + +# Recovering trace information and logging it + +```golang + + type TraceInfo struct { + ID string + IsSampled bool + } + +``` + +```golang +t := trace.GetTraceInfoFromContext(ctx) +log.Ctx(ctx).Info().EmbedObject(t).Msg("Hello") +``` + +Refer to `examples/` directory for a working example. + +## Propagating the trace + +### API + +# Using in transport layer + +Use the middleware to automatically handle traces from HTTP Requests. + +```golang + + func MakeHTTPHandler(e endpoint.Endpoint) http.Handler { + // (...) + + r := mux.NewRouter() + r.Use(trace.MuxHTTPMiddleware("SERVER-NAME")) + + //(...) + return r + } + +``` + +The "SERVER-NAME" is optional and OpenTelemetry will default to the host's IP. + +# Using with go-kit endpoints + +```golang + + e := endpoint.Chain( + trace.EndpointMiddleware("my-endpoint"), + loggingmiddleware.MustNew(loggingConfig), + )(srv)) + +``` + +# Using in a HTTP request + +```golang +request, err := http.NewRequestWithContext( + + ctx, + "POST", + url, + bytes.NewReader(body), + +) + + if err != nil { + return "", errors.E(op, err) + } + +trace.SetTraceInRequest(request) +``` + +### Workers + +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. +*/ +package trace // import "github.com/arquivei/foundationkit/trace/v2" diff --git a/trace/v2/examples/Makefile b/trace/v2/examples/Makefile index 051ae2a..f0a3764 100644 --- a/trace/v2/examples/Makefile +++ b/trace/v2/examples/Makefile @@ -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 \ No newline at end of file + docker compose run api diff --git a/trace/v2/examples/README.md b/trace/v2/examples/README.md index 90c3831..46d5bb6 100644 --- a/trace/v2/examples/README.md +++ b/trace/v2/examples/README.md @@ -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`. diff --git a/trace/v2/examples/cmd/api/config.go b/trace/v2/examples/cmd/api/config.go index 01d9973..777b46c 100644 --- a/trace/v2/examples/cmd/api/config.go +++ b/trace/v2/examples/cmd/api/config.go @@ -4,7 +4,7 @@ import ( "time" "github.com/arquivei/foundationkit/log" - "github.com/arquivei/foundationkit/trace/v2" + tracev1 "github.com/arquivei/foundationkit/trace" ) var config struct { @@ -24,5 +24,5 @@ var config struct { } } - Trace trace.Config + TraceV1 tracev1.Config } diff --git a/trace/v2/examples/cmd/api/main.go b/trace/v2/examples/cmd/api/main.go index 6df4e23..ed79b01 100644 --- a/trace/v2/examples/cmd/api/main.go +++ b/trace/v2/examples/cmd/api/main.go @@ -18,6 +18,7 @@ const ( func main() { app.SetupConfig(&config) + ctx := fklog.SetupLoggerWithContext(context.Background(), config.Log, version) log.Ctx(ctx).Info().Str("config", fklog.Flatten(config)).Msg("Configuration") diff --git a/trace/v2/examples/cmd/api/resources.go b/trace/v2/examples/cmd/api/resources.go index e93a742..bc5a4d9 100644 --- a/trace/v2/examples/cmd/api/resources.go +++ b/trace/v2/examples/cmd/api/resources.go @@ -1,11 +1,14 @@ package main import ( + "context" "net/http" "github.com/arquivei/foundationkit/app" "github.com/arquivei/foundationkit/gokitmiddlewares/loggingmiddleware" "github.com/arquivei/foundationkit/httpmiddlewares/enrichloggingmiddleware" + "github.com/arquivei/foundationkit/request" + tracev1 "github.com/arquivei/foundationkit/trace" "github.com/arquivei/foundationkit/trace/v2" "github.com/arquivei/foundationkit/trace/v2/examples/services/ping" "github.com/arquivei/foundationkit/trace/v2/examples/services/ping/apiping" @@ -16,7 +19,11 @@ import ( ) func setupTrace() { - traceShutdown := trace.Setup(config.Trace) + // This is only to show that v1 and v2 can coexist + // This is not recommended in production. + tracev1.SetupTrace(config.TraceV1) + + traceShutdown := trace.Setup(context.Background()) app.RegisterShutdownHandler( &app.ShutdownHandler{ Name: "opentelemetry_trace", @@ -35,6 +42,7 @@ func getEndpoint() endpoint.Endpoint { ) pingEndpoint := endpoint.Chain( + trace.EndpointMiddleware("ping-pong-endpoint"), loggingmiddleware.MustNew(loggingConfig), )(apiping.MakeAPIPingEndpoint( ping.NewService(pongAdapter), @@ -46,9 +54,16 @@ func getEndpoint() endpoint.Endpoint { func getHTTPServer() *http.Server { r := mux.NewRouter() - r.PathPrefix("/ping/").Handler(apiping.MakeHTTPHandler(getEndpoint())) + 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, + ) - r.Use(enrichloggingmiddleware.New) + r.PathPrefix("/ping/").Handler(apiping.MakeHTTPHandler(getEndpoint())) httpAddr := ":" + config.HTTP.Port httpServer := &http.Server{Addr: httpAddr, Handler: r} diff --git a/trace/v2/examples/docker-compose.yaml b/trace/v2/examples/docker-compose.yaml new file mode 100644 index 0000000..250f6de --- /dev/null +++ b/trace/v2/examples/docker-compose.yaml @@ -0,0 +1,57 @@ +services: + api: + image: golang:1.21-alpine + container_name: trace_example_api + depends_on: + - jaeger + working_dir: /go/src + volumes: + - ../../../:/go/src:ro + # Uncomment this line and the ones at the bottom + # if you want to enable caching go modules. + # - go-mod-cache:/go/pkg/mod + environment: + LOG_LEVEL: trace + LOG_HUMAN: "true" + HTTP_PORT: "80" + PONG_HTTP_URL: "http://localhost/ping/v1" + PONG_HTTP_TIMEOUT: "10s" + + # Or official OpenTelemetry variables + # This allow for more control and features. + OTEL_SERVICE_NAME: "ping-pong-srv" + OTEL_RESOURCE_ATTRIBUTES: "service.namespace=example,service.instance.id=trace_example_api" + OTEL_EXPORTER_OTLP_ENDPOINT: "http://jaeger:4318" + OTEL_TRACES_SAMPLER: "parentbased_traceidratio" + OTEL_TRACES_SAMPLER_ARG: "0.5" + + # TraceV1 is only used for compatibility demonstration + TRACEV1_EXPORTER: "" + TRACEV1_PROBABILITYSAMPLE: "1" + entrypoint: + - go + - run + - ./trace/v2/examples/cmd/api + + # sender sends a single request to the API. + sender: + image: curlimages/curl + depends_on: + - api + command: + - "http://api/ping/v1" + - "-vsd" + - '{"num": 3, "sleep": 500}' + + # jaeger: A tool for collection and inspecting tracing. This is the all-in-one container. + # http://localhost:16686/ + # https://www.jaegertracing.io/docs/1.6/getting-started/ + jaeger: + image: jaegertracing/all-in-one:latest + ports: + - 16686:16686 + environment: + COLLECTOR_OTLP_ENABLED: "true" +# Uncomment the following lines if you want to cache modules +# volumes: +# go-mod-cache: diff --git a/trace/v2/examples/services/ping/apiping/entity_http_request.go b/trace/v2/examples/services/ping/apiping/entity_http_request.go index 066e9e9..f598b09 100644 --- a/trace/v2/examples/services/ping/apiping/entity_http_request.go +++ b/trace/v2/examples/services/ping/apiping/entity_http_request.go @@ -1,9 +1,21 @@ package apiping -import "time" +import ( + "time" + + "go.opentelemetry.io/otel/attribute" +) // Request Request type Request struct { Num int `json:"num"` Sleep time.Duration `json:"sleep"` } + +// TraceAttributes will inject attributes num and sleep into the trace span. +func (r Request) TraceAttributes() []attribute.KeyValue { + return []attribute.KeyValue{ + attribute.Int("request.num", r.Num), + attribute.Float64("request.sleep.seconds", r.Sleep.Seconds()), + } +} diff --git a/trace/v2/examples/services/ping/apiping/http.go b/trace/v2/examples/services/ping/apiping/http.go index 02e7f38..aef99ce 100644 --- a/trace/v2/examples/services/ping/apiping/http.go +++ b/trace/v2/examples/services/ping/apiping/http.go @@ -6,7 +6,6 @@ import ( "net/http" "github.com/arquivei/foundationkit/errors" - "github.com/arquivei/foundationkit/trace/v2" "github.com/go-kit/kit/endpoint" kithttp "github.com/go-kit/kit/transport/http" @@ -27,7 +26,7 @@ func MakeHTTPHandler(e endpoint.Endpoint) http.Handler { ) r := mux.NewRouter() - r.Use(trace.MuxHTTPMiddleware("ga-trace-go-api")) + r.Handle("/ping/v1", httpHandler).Methods("POST") return r @@ -48,7 +47,6 @@ func encodeResponse( r interface{}, ) error { w.Header().Set("Content-Type", "application/json; charset=utf-8") - trace.SetTraceInResponse(ctx, w) return json.NewEncoder(w).Encode(r) } diff --git a/trace/v2/examples/services/ping/implping/adapter_pong_http.go b/trace/v2/examples/services/ping/implping/adapter_pong_http.go index ea9c31d..5e374d6 100644 --- a/trace/v2/examples/services/ping/implping/adapter_pong_http.go +++ b/trace/v2/examples/services/ping/implping/adapter_pong_http.go @@ -12,7 +12,7 @@ import ( "github.com/arquivei/foundationkit/trace/v2/examples/services/ping" ) -type adapterPongHttp struct { +type adapterPongHTTP struct { client *http.Client url string } @@ -27,19 +27,20 @@ func NewHTTPPongAdapter( client *http.Client, url string, ) ping.PongGateway { - - return &adapterPongHttp{ + return &adapterPongHTTP{ client: client, url: url, } } -func (a *adapterPongHttp) Pong( +func (a *adapterPongHTTP) Pong( ctx context.Context, num int, sleep time.Duration, ) (string, error) { const op = errors.Op("implping.adapterPongHttp.Pong") + ctx, span := trace.Start(ctx, "implping.adapterPongHttp.Pong") + defer span.End() body, err := json.Marshal(pongRequestResponse{ Num: num, @@ -61,14 +62,12 @@ func (a *adapterPongHttp) Pong( trace.SetTraceInRequest(request) - response, err := a.client.Do(request) + httpResponse, err := a.client.Do(request) if err != nil { return "", errors.E(op, err) } - defer response.Body.Close() - - buf := new(bytes.Buffer) - buf.ReadFrom(response.Body) + defer httpResponse.Body.Close() - return buf.String(), nil + var response string + return response, json.NewDecoder(httpResponse.Body).Decode(&response) } diff --git a/trace/v2/examples/services/ping/services.go b/trace/v2/examples/services/ping/services.go index 120430e..dbdbd99 100644 --- a/trace/v2/examples/services/ping/services.go +++ b/trace/v2/examples/services/ping/services.go @@ -2,12 +2,12 @@ package ping import ( "context" + "fmt" "time" - "github.com/arquivei/foundationkit/trace/v2" - "github.com/arquivei/foundationkit/errors" - "github.com/rs/zerolog/log" + "github.com/arquivei/foundationkit/trace/v2" + "go.opentelemetry.io/otel/attribute" ) // Service Service @@ -29,16 +29,24 @@ func NewService(pongGateway PongGateway) Service { func (s *service) Ping(ctx context.Context, req Request) (string, error) { const op = errors.Op("ping.service.Ping") - ctx, span := trace.Start(ctx, "ping-service") + ctx, span := trace.Start(ctx, "ping.service.Ping") defer span.End() - t := trace.GetTraceInfoFromContext(ctx) - defer log.Ctx(ctx).Info().EmbedObject(t).Msg("Just for check trace info") + if req.Num < 0 { + return "", fmt.Errorf("negative number received: %d", req.Num) + } time.Sleep(req.Sleep * time.Millisecond) + pingpong := getPingPong(req.Num) + + span.SetAttributes( + attribute.KeyValue{Key: "req.num", Value: attribute.IntValue(req.Num)}, + attribute.KeyValue{Key: "ping.pong", Value: attribute.StringValue(pingpong)}, + ) + if req.Num == 0 { - return "ping", nil + return pingpong, nil } pong, err := s.pongGateway.Pong(ctx, req.Num-1, req.Sleep) @@ -46,5 +54,12 @@ func (s *service) Ping(ctx context.Context, req Request) (string, error) { return "", errors.E(op, err) } - return "ping " + pong, nil + return pingpong + "-" + pong, nil +} + +func getPingPong(n int) string { + if n%2 == 1 { + return "ping" + } + return "pong" } diff --git a/trace/v2/exporter_otlp.go b/trace/v2/exporter_otlp.go deleted file mode 100644 index fbd18ab..0000000 --- a/trace/v2/exporter_otlp.go +++ /dev/null @@ -1,56 +0,0 @@ -package trace - -import ( - "context" - - "github.com/rs/zerolog/log" - "go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" - "go.opentelemetry.io/otel/sdk/trace" -) - -// OTLPConfig allows for configuring the OpenTelemery Protocol. -type OTLPConfig struct { - // Endpoint allows one to set the address of the collector - // endpoint that the driver will use to send spans. - // It is a string in the form :. - // If unset, it will instead try to use the default endpoint - // from package otlptracehttp (at the time of this writing - // the default is localhost:4318). - // Note that the endpoint must not contain any URL path. - Endpoint string - // Compression tells the driver to compress the sent data. - // Possible values are: - // - "" (empty string) or "none": No compression - // - "gzip": GZIP compression. - Compression string - // Insecure tells the driver to connect to the collector using the - // HTTP scheme, instead of HTTPS. - Insecure bool -} - -func (c *OTLPConfig) Options() []otlptracehttp.Option { - opts := make([]otlptracehttp.Option, 0, 10) - - if c.Endpoint != "" { - opts = append(opts, otlptracehttp.WithEndpoint(c.Endpoint)) - } - - switch c.Compression { - case "", "none": - opts = append(opts, otlptracehttp.WithCompression(otlptracehttp.NoCompression)) - case "gzip": - opts = append(opts, otlptracehttp.WithCompression(otlptracehttp.GzipCompression)) - default: - log.Fatal().Msgf("Invalid OpenTelemetry trace exporter compression: %s", c.Compression) - } - - if c.Insecure { - opts = append(opts, otlptracehttp.WithInsecure()) - } - - return opts -} - -func newOTLPExporter(c OTLPConfig) (trace.SpanExporter, error) { - return otlptracehttp.New(context.Background(), c.Options()...) -} diff --git a/trace/v2/exporter_stackdriver.go b/trace/v2/exporter_stackdriver.go deleted file mode 100644 index 3c9539f..0000000 --- a/trace/v2/exporter_stackdriver.go +++ /dev/null @@ -1,17 +0,0 @@ -package trace - -import ( - stackdriverexporter "github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/trace" - "go.opentelemetry.io/otel/sdk/trace" -) - -// StackdriverConfig contains all the configuration of the Starkdriver exporter. -type StackdriverConfig struct { - ProjectID string -} - -func newStackdriverExporter(c StackdriverConfig) (trace.SpanExporter, error) { - return stackdriverexporter.New( - stackdriverexporter.WithProjectID(c.ProjectID), - ) -} diff --git a/trace/v2/gokit.go b/trace/v2/gokit.go new file mode 100644 index 0000000..0536792 --- /dev/null +++ b/trace/v2/gokit.go @@ -0,0 +1,50 @@ +package trace + +import ( + "context" + + "github.com/arquivei/foundationkit/request" + "github.com/go-kit/kit/endpoint" + "go.opentelemetry.io/otel/attribute" +) + +// EndpointMiddleware returns a new gokit endpoint.Middleware that wraps +// the next Endpoint with a span with the given name. The RequestID, if +// present, is injected as a tag and if the next Endpoint returns an +// error, it is registered in the span. +func EndpointMiddleware(name string) endpoint.Middleware { + return func(next endpoint.Endpoint) endpoint.Endpoint { + return func(ctx context.Context, req interface{}) (resp interface{}, err error) { + ctx, span := Start(ctx, name) + + requestID := request.GetIDFromContext(ctx) + if !requestID.IsEmpty() { + span.SetAttributes(attribute.String("request.id", requestID.String())) + } + + if ta, ok := req.(TraceAttributer); ok { + span.SetAttributes(ta.TraceAttributes()...) + } + + resp, err = next(ctx, req) + if err != nil { + span.RecordError(err) + } + + if ta, ok := resp.(TraceAttributer); ok { + span.SetAttributes(ta.TraceAttributes()...) + } + + span.End() + + return resp, err + } + } +} + +// TraceAttributer is a interface for returning OpenTelemetry span attributes. +// If the Request or Response (or both) implement this interface the EndpointMiddleware +// will set theses attributes on the span. +type TraceAttributer interface { + TraceAttributes() []attribute.KeyValue +} diff --git a/trace/v2/http.go b/trace/v2/http.go index 351d735..4f6ae8d 100644 --- a/trace/v2/http.go +++ b/trace/v2/http.go @@ -13,6 +13,8 @@ import ( // MuxHTTPMiddleware sets up a handler to start tracing the incoming // requests. The service parameter should describe the name of the // (virtual) server handling the request. +// This will be set in the tag 'net.host.name'. It defaults to the +// ip. func MuxHTTPMiddleware(service string) mux.MiddlewareFunc { return otelmux.Middleware(service) } diff --git a/trace/v2/span.go b/trace/v2/span.go index 68113f2..5b99908 100644 --- a/trace/v2/span.go +++ b/trace/v2/span.go @@ -7,20 +7,8 @@ import ( "go.opentelemetry.io/otel/trace" ) -// Span is the individual component of a trace. It represents a single named -// and timed operation of a workflow that is traced -type Span struct { - span trace.Span -} - -// End stops an span sampling -func (s Span) End() { - s.span.End() -} - // Start creates a new span. If other spans were created using @ctx, this method // will bind them all -func Start(ctx context.Context, name string) (context.Context, Span) { - ctx, span := otel.Tracer("").Start(ctx, name) - return withTraceInfo(ctx, span), Span{span} +func Start(ctx context.Context, name string) (context.Context, trace.Span) { + return otel.Tracer("").Start(ctx, name) } diff --git a/trace/v2/traceinfo.go b/trace/v2/traceinfo.go index 90efa67..15e1125 100644 --- a/trace/v2/traceinfo.go +++ b/trace/v2/traceinfo.go @@ -4,15 +4,11 @@ import ( "context" "github.com/rs/zerolog/log" + "go.opentelemetry.io/otel" + "go.opentelemetry.io/otel/propagation" "go.opentelemetry.io/otel/trace" ) -type contextKeyType int - -const ( - contextKeyTraceInfo contextKeyType = iota -) - // TraceInfo carries the trace informations type TraceInfo struct { ID string @@ -22,7 +18,13 @@ type TraceInfo struct { // GetTraceInfoFromContext returns a TraceInfo from the context @ctx or logs // if there is no TraceInfo in context func GetTraceInfoFromContext(ctx context.Context) TraceInfo { - if t, ok := ctx.Value(contextKeyTraceInfo).(TraceInfo); ok { + sc := trace.SpanContextFromContext(ctx) + t := TraceInfo{ + ID: sc.TraceID().String(), + IsSampled: sc.IsSampled(), + } + + if t.ID != "" { return t } log.Warn(). @@ -31,15 +33,16 @@ func GetTraceInfoFromContext(ctx context.Context) TraceInfo { return TraceInfo{} } -func withTraceInfo(ctx context.Context, s trace.Span) context.Context { - if v := ctx.Value(contextKeyTraceInfo); v != nil { - return ctx - } - - traceInfo := TraceInfo{ - ID: s.SpanContext().TraceID().String(), - IsSampled: s.SpanContext().IsSampled(), - } +// ToMap extracts the current trace from the context and put it on a map. +// This can be used to serialize the trace context on json messages. +func ToMap(ctx context.Context) map[string]string { + m := map[string]string{} + otel.GetTextMapPropagator().Inject(ctx, propagation.MapCarrier(m)) + return m +} - return context.WithValue(ctx, contextKeyTraceInfo, traceInfo) +// FromMap injects the trace context from the map into the context. This is the +// inverse of ToMap and could be used to extract trace context form json messages. +func FromMap(ctx context.Context, m map[string]string) context.Context { + return otel.GetTextMapPropagator().Extract(ctx, propagation.MapCarrier(m)) }