From caeac41edcf0507f8250e7adcfba0f01d9e1402f Mon Sep 17 00:00:00 2001 From: Vincent Landgraf Date: Mon, 14 Dec 2020 14:00:13 +0100 Subject: [PATCH] implement external round tripper middleware --- http/middleware/external_dependency.go | 143 ++++++++++++++++++++ http/middleware/external_dependency_test.go | 82 +++++++++++ http/router.go | 3 + pkg/context/transfer.go | 2 +- 4 files changed, 229 insertions(+), 1 deletion(-) create mode 100644 http/middleware/external_dependency.go create mode 100644 http/middleware/external_dependency_test.go diff --git a/http/middleware/external_dependency.go b/http/middleware/external_dependency.go new file mode 100644 index 000000000..2b039ac37 --- /dev/null +++ b/http/middleware/external_dependency.go @@ -0,0 +1,143 @@ +// Copyright © 2020 by PACE Telematics GmbH. All rights reserved. +// Created at 2020/12/14 by Vincent Landgraf + +package middleware + +import ( + "bytes" + "context" + "fmt" + "net/http" + "strconv" + "strings" + "sync" + "time" + + "github.com/pace/bricks/maintenance/log" +) + +// depFormat is the format of a single dependency report +const depFormat = "%s:%d" + +// ExternalDependencyHeaderName name of the HTTP header that is used for reporting +const ExternalDependencyHeaderName = "External-Dependencies" + +// ExternalDependency middleware to report external dependencies +func ExternalDependency(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + var edc ExternalDependencyContext + edw := externalDependencyWriter{ + ResponseWriter: w, + edc: &edc, + } + r = r.WithContext(ContextWithExternalDependency(r.Context(), &edc)) + next.ServeHTTP(&edw, r) + }) +} + +func AddExternalDependency(ctx context.Context, name string, dur time.Duration) { + ec := ExternalDependencyContextFromContext(ctx) + if ec == nil { + log.Ctx(ctx).Warn().Msgf("can't add external dependency %q with %s, because context is missing", name, dur) + return + } + ec.AddDependency(name, dur) +} + +type externalDependencyWriter struct { + http.ResponseWriter + header bool + edc *ExternalDependencyContext +} + +// addHeader adds the external dependency header if not done already +func (w *externalDependencyWriter) addHeader() { + if !w.header { + if len(w.edc.dependencies) > 0 { + w.ResponseWriter.Header().Add(ExternalDependencyHeaderName, w.edc.String()) + } + w.header = true + } +} + +func (w *externalDependencyWriter) Write(data []byte) (int, error) { + w.addHeader() + return w.ResponseWriter.Write(data) +} + +func (w *externalDependencyWriter) WriteHeader(statusCode int) { + w.addHeader() + w.ResponseWriter.WriteHeader(statusCode) +} + +// ContextWithExternalDependency creates a contex with the external provided dependencies +func ContextWithExternalDependency(ctx context.Context, edc *ExternalDependencyContext) context.Context { + return context.WithValue(ctx, (*ExternalDependencyContext)(nil), edc) +} + +// ExternalDependencyContextFromContext returns the external dependencies context or nil +func ExternalDependencyContextFromContext(ctx context.Context) *ExternalDependencyContext { + if v := ctx.Value((*ExternalDependencyContext)(nil)); v != nil { + return v.(*ExternalDependencyContext) + } + return nil +} + +// ExternalDependencyContext contains all dependencies that where seed +// during the request livecycle +type ExternalDependencyContext struct { + mu sync.RWMutex + dependencies []externalDependency +} + +func (c *ExternalDependencyContext) AddDependency(name string, duration time.Duration) { + c.mu.Lock() + c.dependencies = append(c.dependencies, externalDependency{ + Name: name, + Duration: duration, + }) + c.mu.Unlock() +} + +// String formats all external dependencies +func (c *ExternalDependencyContext) String() string { + var buf bytes.Buffer + sep := len(c.dependencies) - 1 + for _, dep := range c.dependencies { + buf.WriteString(dep.String()) + if sep > 0 { + buf.WriteByte(',') + sep-- + } + } + return buf.String() +} + +// Parse a external dependency value +func (c *ExternalDependencyContext) Parse(s string) { + values := strings.Split(s, ",") + for _, value := range values { + index := strings.IndexByte(value, ':') + if index == -1 { + continue // ignore the invalid values + } + dur, err := strconv.ParseInt(value[index+1:], 10, 64) + if err != nil { + continue // ignore the invalid values + } + + c.AddDependency(value[:index], time.Millisecond*time.Duration(dur)) + } +} + +// externalDependency represents one external dependency that +// was involved in the process to creating a response +type externalDependency struct { + Name string // canonical name of the source + Duration time.Duration // time spend with the external dependency +} + +// String returns a formated single external dependency +func (r externalDependency) String() string { + return fmt.Sprintf(depFormat, r.Name, r.Duration.Milliseconds()) +} diff --git a/http/middleware/external_dependency_test.go b/http/middleware/external_dependency_test.go new file mode 100644 index 000000000..5c7e27df7 --- /dev/null +++ b/http/middleware/external_dependency_test.go @@ -0,0 +1,82 @@ +// Copyright © 2020 by PACE Telematics GmbH. All rights reserved. +// Created at 2020/12/14 by Vincent Landgraf + +package middleware + +import ( + "context" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func Test_ExternalDependency_Middleare(t *testing.T) { + AddExternalDependency(context.TODO(), "test", time.Second) // should not fail + t.Run("empty set", func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + h := ExternalDependency(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + h.ServeHTTP(rec, req) + assert.Nil(t, rec.HeaderMap[ExternalDependencyHeaderName]) + }) + t.Run("one dependency set", func(t *testing.T) { + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/", nil) + + h := ExternalDependency(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + AddExternalDependency(r.Context(), "test", time.Second) + w.Write(nil) // nolint: errcheck + })) + h.ServeHTTP(rec, req) + assert.Equal(t, rec.HeaderMap[ExternalDependencyHeaderName][0], "test:1000") + }) +} + +func Test_ExternalDependencyContext_String(t *testing.T) { + var edc ExternalDependencyContext + + // empty + assert.Empty(t, edc.String()) + + // one dependency + edc.AddDependency("test1", time.Millisecond) + assert.EqualValues(t, "test1:1", edc.String()) + + // multiple dependencies + edc.AddDependency("test2", time.Nanosecond) + assert.EqualValues(t, "test1:1,test2:0", edc.String()) + + edc.AddDependency("test3", time.Second) + assert.EqualValues(t, "test1:1,test2:0,test3:1000", edc.String()) + + edc.AddDependency("test4", time.Millisecond*123) + assert.EqualValues(t, "test1:1,test2:0,test3:1000,test4:123", edc.String()) +} + +func Test_ExternalDependencyContext_Parse(t *testing.T) { + var edc ExternalDependencyContext + + // empty + assert.Empty(t, edc.String()) + + // one dependency + edc.Parse("test1:1") + assert.EqualValues(t, "test1:1", edc.String()) + + // ignore invalid lines + edc.Parse("error") + assert.EqualValues(t, "test1:1", edc.String()) + + // multiple dependencies + edc.Parse("test2:0") + assert.EqualValues(t, "test1:1,test2:0", edc.String()) + + edc.Parse("test3:1000,test4:123") + assert.EqualValues(t, "test1:1,test2:0,test3:1000,test4:123", edc.String()) +} diff --git a/http/router.go b/http/router.go index f6cc29995..b3d65f14e 100644 --- a/http/router.go +++ b/http/router.go @@ -43,6 +43,9 @@ func Router() *mux.Router { r.Use(locale.Handler()) + // report use of external dependencies + r.Use(middleware.ExternalDependency) + // makes some infos about the request accessable from the context r.Use(middleware.RequestInContext) diff --git a/pkg/context/transfer.go b/pkg/context/transfer.go index 318bf37b1..e66c427b0 100644 --- a/pkg/context/transfer.go +++ b/pkg/context/transfer.go @@ -3,7 +3,7 @@ package context import ( "context" - "github.com/pace/bricks/http" + http "github.com/pace/bricks/http/middleware" "github.com/pace/bricks/http/oauth2" "github.com/pace/bricks/locale" "github.com/pace/bricks/maintenance/errors"