Skip to content

Commit

Permalink
implement external round tripper middleware
Browse files Browse the repository at this point in the history
  • Loading branch information
Vincent Landgraf committed Dec 14, 2020
1 parent 8654704 commit caeac41
Show file tree
Hide file tree
Showing 4 changed files with 229 additions and 1 deletion.
143 changes: 143 additions & 0 deletions http/middleware/external_dependency.go
Original file line number Diff line number Diff line change
@@ -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())
}
82 changes: 82 additions & 0 deletions http/middleware/external_dependency_test.go
Original file line number Diff line number Diff line change
@@ -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())
}
3 changes: 3 additions & 0 deletions http/router.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
2 changes: 1 addition & 1 deletion pkg/context/transfer.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down

0 comments on commit caeac41

Please sign in to comment.