-
-
Notifications
You must be signed in to change notification settings - Fork 1.9k
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
feat: add OTEL tracing span middleware during function proxy #1685
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,10 +1,15 @@ | ||
TAG?=latest | ||
NS?=openfaas | ||
|
||
COMMIT ?= $(shell git rev-parse HEAD) | ||
|
||
.PHONY: build-gateway | ||
build-gateway: | ||
(cd gateway; docker buildx build --platform linux/amd64 -t ${NS}/gateway:latest-dev .) | ||
(cd gateway; docker buildx build --platform linux/amd64 --load -t ${NS}/gateway:${COMMIT} -t ${NS}/gateway:latest-dev .) | ||
|
||
|
||
kind-load: | ||
kind --name of-tracing load docker-image ${NS}/gateway:${COMMIT} | ||
# .PHONY: test-ci | ||
# test-ci: | ||
# ./contrib/ci.sh |
Large diffs are not rendered by default.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,62 @@ | ||
package tracing | ||
|
||
import ( | ||
"fmt" | ||
"log" | ||
"net/http" | ||
"os" | ||
"strings" | ||
|
||
"go.opentelemetry.io/otel" | ||
"go.opentelemetry.io/otel/propagation" | ||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" | ||
"go.opentelemetry.io/otel/trace" | ||
) | ||
|
||
// Middleware returns a http.HandlerFunc that initializes and replaces the OpenTelemetry span for each request. | ||
func Middleware(nameFormatter func(r *http.Request) string, next http.HandlerFunc) http.HandlerFunc { | ||
_, ok := os.LookupEnv("OTEL_EXPORTER") | ||
if !ok { | ||
return next | ||
} | ||
log.Println("configuring proxy tracing middleware") | ||
|
||
propagator := otel.GetTextMapPropagator() | ||
|
||
return func(w http.ResponseWriter, r *http.Request) { | ||
// get the parent span from the request headers | ||
ctx := propagator.Extract(r.Context(), propagation.HeaderCarrier(r.Header)) | ||
opts := []trace.SpanStartOption{ | ||
trace.WithAttributes(semconv.NetAttributesFromHTTPRequest("tcp", r)...), | ||
trace.WithAttributes(semconv.HTTPServerAttributesFromHTTPRequest("gateway", "", r)...), | ||
trace.WithSpanKind(trace.SpanKindServer), | ||
} | ||
|
||
ctx, span := otel.Tracer("Gateway").Start(ctx, nameFormatter(r), opts...) | ||
defer span.End() | ||
|
||
debug(span, "tracing request %q", r.URL.String()) | ||
|
||
r = r.WithContext(ctx) | ||
// set the new span as the parent span in the outgoing request context | ||
// note that this will overwrite the uber-trace-id and traceparent headers | ||
propagator.Inject(ctx, propagation.HeaderCarrier(r.Header)) | ||
next(w, r) | ||
} | ||
} | ||
|
||
// ConstantName geneates the given name for the span based on the request. | ||
func ConstantName(value string) func(*http.Request) string { | ||
return func(r *http.Request) string { | ||
return value | ||
} | ||
} | ||
|
||
func debug(span trace.Span, format string, args ...interface{}) { | ||
value := os.Getenv("OTEL_LOG_LEVEL") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this is the standard env variable for the otel tooling log level, but it isn't fully respected by the go implementation yet. This was helpful for debugging some early issues with the middleware |
||
if strings.ToLower(value) != "debug" { | ||
return | ||
} | ||
|
||
log.Printf("%s, trace_id=%s", fmt.Sprintf(format, args...), span.SpanContext().TraceID()) | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,180 @@ | ||
package tracing | ||
|
||
import ( | ||
"context" | ||
"log" | ||
"os" | ||
"strings" | ||
"time" | ||
|
||
jaegerprop "go.opentelemetry.io/contrib/propagators/jaeger" | ||
"go.opentelemetry.io/otel" | ||
"go.opentelemetry.io/otel/attribute" | ||
"go.opentelemetry.io/otel/exporters/jaeger" | ||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace" | ||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc" | ||
"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp" | ||
"go.opentelemetry.io/otel/exporters/stdout/stdouttrace" | ||
"go.opentelemetry.io/otel/propagation" | ||
"go.opentelemetry.io/otel/sdk/resource" | ||
tracesdk "go.opentelemetry.io/otel/sdk/trace" | ||
semconv "go.opentelemetry.io/otel/semconv/v1.4.0" | ||
) | ||
|
||
type Exporter string | ||
|
||
const ( | ||
JaegerExporter Exporter = "jaeger" | ||
LogExporter Exporter = "log" | ||
OTELExporter Exporter = "otlp" | ||
DisabledExporter Exporter = "disabled" | ||
) | ||
|
||
const ( | ||
otelEnvPropagators = "OTEL_PROPAGATORS" | ||
otelEnvTraceSExporter = "OTEL_TRACES_EXPORTER" | ||
otelEnvExporterLogPrettyPrint = "OTEL_EXPORTER_LOG_PRETTY_PRINT" | ||
otelEnvExporterLogTimestamps = "OTEL_EXPORTER_LOG_TIMESTAMPS" | ||
otelEnvServiceName = "OTEL_SERVICE_NAME" | ||
otelExpOTLPProtocol = "OTEL_EXPORTER_OTLP_PROTOCOL" | ||
) | ||
|
||
type Shutdown func(context.Context) | ||
|
||
// Provider returns an OpenTelemetry TracerProvider configured to use | ||
// the Jaeger exporter that will send spans to the provided url. The returned | ||
// TracerProvider will also use a Resource configured with all the information | ||
// about the application. | ||
func Provider(ctx context.Context, name, version, commit string) (shutdown Shutdown, err error) { | ||
exporter := Exporter(get(otelEnvTraceSExporter, string(DisabledExporter))) | ||
|
||
var exp tracesdk.TracerProviderOption | ||
switch exporter { | ||
case JaegerExporter: | ||
// configure the collector from the env variables, | ||
// OTEL_EXPORTER_JAEGER_ENDPOINT/USER/PASSWORD | ||
// see: https://github.com/open-telemetry/opentelemetry-go/tree/main/exporters/jaeger | ||
j, e := jaeger.New(jaeger.WithCollectorEndpoint()) | ||
exp, err = tracesdk.WithBatcher(j), e | ||
case LogExporter: | ||
w := os.Stdout | ||
opts := []stdouttrace.Option{stdouttrace.WithWriter(w)} | ||
if truthyEnv(otelEnvExporterLogPrettyPrint) { | ||
opts = append(opts, stdouttrace.WithPrettyPrint()) | ||
} | ||
if !truthyEnv(otelEnvExporterLogTimestamps) { | ||
opts = append(opts, stdouttrace.WithoutTimestamps()) | ||
} | ||
|
||
s, e := stdouttrace.New(opts...) | ||
exp, err = tracesdk.WithSyncer(s), e | ||
case OTELExporter: | ||
// find available env variables for configuration | ||
// see: https://github.com/open-telemetry/opentelemetry-go/tree/main/exporters/otlp/otlptrace#environment-variables | ||
kind := get(otelExpOTLPProtocol, "grpc") | ||
|
||
var client otlptrace.Client | ||
switch kind { | ||
case "grpc": | ||
client = otlptracegrpc.NewClient() | ||
case "http": | ||
client = otlptracehttp.NewClient() | ||
} | ||
o, e := otlptrace.New(ctx, client) | ||
exp, err = tracesdk.WithBatcher(o), e | ||
default: | ||
log.Println("tracing disabled") | ||
// We explicitly DO NOT set the global TracerProvider using otel.SetTracerProvider(). | ||
// The unset TracerProvider returns a "non-recording" span, but still passes through context. | ||
// return no-op shutdown function | ||
return func(_ context.Context) {}, nil | ||
} | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
propagators := strings.ToLower(get(otelEnvPropagators, "tracecontext,baggage")) | ||
otel.SetTextMapPropagator( | ||
propagation.NewCompositeTextMapPropagator(withPropagators(propagators)...), | ||
) | ||
|
||
resource, err := resource.New( | ||
context.Background(), | ||
resource.WithFromEnv(), | ||
resource.WithHost(), | ||
resource.WithOS(), | ||
resource.WithTelemetrySDK(), | ||
resource.WithAttributes( | ||
semconv.ServiceVersionKey.String(version), | ||
attribute.String("service.commit", commit), | ||
semconv.ServiceNameKey.String(get(otelEnvServiceName, name)), | ||
), | ||
) | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
provider := tracesdk.NewTracerProvider( | ||
// Always be sure to batch in production. | ||
exp, | ||
tracesdk.WithResource(resource), | ||
tracesdk.WithSampler(tracesdk.AlwaysSample()), | ||
) | ||
|
||
// Register our TracerProvider as the global so any imported | ||
// instrumentation in the future will default to using it. | ||
otel.SetTracerProvider(provider) | ||
|
||
shutdown = func(ctx context.Context) { | ||
// Do not let the application hang forever when it is shutdown. | ||
ctx, cancel := context.WithTimeout(ctx, time.Second*5) | ||
defer cancel() | ||
|
||
err := provider.Shutdown(ctx) | ||
if err != nil { | ||
log.Printf("failed to shutdown tracing provider: %v", err) | ||
} | ||
} | ||
|
||
return shutdown, nil | ||
} | ||
|
||
func truthyEnv(name string) bool { | ||
value, ok := os.LookupEnv(name) | ||
if !ok { | ||
return false | ||
} | ||
|
||
switch value { | ||
case "true", "1", "yes", "on": | ||
return true | ||
default: | ||
return false | ||
} | ||
} | ||
|
||
func get(name, defaultValue string) string { | ||
value, ok := os.LookupEnv(name) | ||
if !ok { | ||
return defaultValue | ||
} | ||
return value | ||
} | ||
|
||
func withPropagators(propagators string) []propagation.TextMapPropagator { | ||
out := []propagation.TextMapPropagator{} | ||
|
||
if strings.Contains(propagators, "tracecontext") { | ||
out = append(out, propagation.TraceContext{}) | ||
} | ||
|
||
if strings.Contains(propagators, "jaeger") { | ||
out = append(out, jaegerprop.Jaeger{}) | ||
} | ||
|
||
if strings.Contains(propagators, "baggage") { | ||
out = append(out, propagation.Baggage{}) | ||
} | ||
|
||
return out | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
package tracing | ||
|
||
import ( | ||
"go.opentelemetry.io/otel/codes" | ||
"go.opentelemetry.io/otel/trace" | ||
) | ||
|
||
func FinishSpan(span trace.Span, err error) { | ||
if span == nil { | ||
return | ||
} | ||
|
||
if err != nil { | ||
span.SetStatus(codes.Error, err.Error()) | ||
span.RecordError(err) | ||
} | ||
span.End() | ||
} |
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this applies the tracing middleware to only the function proxy, we chould also choose to add it to other endpoints though