-
Notifications
You must be signed in to change notification settings - Fork 21
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
implement external round tripper middleware
- Loading branch information
Vincent Landgraf
committed
Dec 14, 2020
1 parent
8654704
commit caeac41
Showing
4 changed files
with
229 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters