Skip to content

Commit

Permalink
Merge pull request #70 from uknth/dep-upgrade
Browse files Browse the repository at this point in the history
Better filter support in http/transport
  • Loading branch information
uknth authored Dec 24, 2023
2 parents 2c71351 + 7b6c458 commit c54ad9e
Show file tree
Hide file tree
Showing 13 changed files with 297 additions and 263 deletions.
4 changes: 2 additions & 2 deletions transport/http/errorEncoder.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ type ErrorEncoder func(context.Context, error, net_http.ResponseWriter)
// by the request
func WithErrorEncoder(fn ErrorEncoder) TransportOption {
return func(tr *Transport) {
tr.options = append(
tr.options, NewErrorEncoderHandlerOptions(fn),
tr.handlerOptions = append(
tr.handlerOptions, NewErrorEncoderHandlerOptions(fn),
)
}
}
Expand Down
95 changes: 47 additions & 48 deletions transport/http/filter_metrics.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ import (
"github.com/unbxd/go-base/v2/metrics"
"go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/metric"
"go.opentelemetry.io/otel/trace"
)

type SpanNameFormatter func(operation string, r *http.Request) string

func defaultSpanNameFormatter(operation string, r *http.Request) string {
func DefaultSpanNameFormatter(operation string, r *http.Request) string {
// we will only get this if chi is the router
var (
sb strings.Builder
Expand All @@ -35,7 +34,7 @@ func defaultSpanNameFormatter(operation string, r *http.Request) string {
return sb.String()
}

// OpenTelemetryFilterForDefaultMux uses OpenTelemetry to publish events
// OpenTelemetryFilter uses OpenTelemetry to publish events
// There are multiple providers for OpenTelemetry that can be used
// A simple example of using this filter is by just setting this up in the
// filter chain and in the application, set the provider
Expand All @@ -52,14 +51,13 @@ func defaultSpanNameFormatter(operation string, r *http.Request) string {
// defer provider.Shutdown()
// otel.SetTracerProvider(provider)
// }
func OpenTelemetryFilterForDefaultMux(
func OpenTelemetryFilter(
namespace string,
provider OpenTelemetryProvider,
tags []KeyValue,
meterProvider metric.MeterProvider,
traceProvider trace.TracerProvider,
filters ...otelhttp.Filter,
filters ...OpenTelemetryRequestFilter,
) Filter {
formatter := defaultSpanNameFormatter
formatter := DefaultSpanNameFormatter
attribs := make([]attribute.KeyValue, 0)

for _, kv := range tags {
Expand All @@ -69,7 +67,7 @@ func OpenTelemetryFilterForDefaultMux(
options := []otelhttp.Option{}

for _, fn := range filters {
options = append(options, otelhttp.WithFilter(fn))
options = append(options, otelhttp.WithFilter(otelhttp.Filter(fn)))
}

options = append(options, []otelhttp.Option{
Expand All @@ -78,8 +76,8 @@ func OpenTelemetryFilterForDefaultMux(
trace.WithNewRoot(),
trace.WithAttributes(attribs...),
),
otelhttp.WithMeterProvider(meterProvider),
otelhttp.WithTracerProvider(traceProvider),
otelhttp.WithMeterProvider(provider),
otelhttp.WithTracerProvider(provider),
}...)

// this is slightly in-efficient that we are double wrapping
Expand All @@ -98,67 +96,68 @@ func OpenTelemetryFilterForDefaultMux(

type MetricsNameFormatter func(namespace string, r *http.Request) string

func CustomMetricsForDefaultMuxFilter(namespace string, provider metrics.Provider, formatter MetricsNameFormatter, tagss ...KeyValue) Filter {
var (
counters = make(map[string]metrics.Counter)
histograms = make(map[string]metrics.Histogram)
tt = []string{}
)
type MetricsTagGenerator func(rw WrapResponseWriter, req *http.Request) []KeyValue

for _, kv := range tagss {
tt = append(tt, kv.Key)
tt = append(tt, kv.Value)
func DefaultMetricsNameFormatter(namespace string, r *http.Request) string {
rcx := chi.RouteContext(r.Context())
if rcx == nil {
return namespace + ".not-chi"
}

if formatter == nil {
formatter = func(namespace string, r *http.Request) string {
rcx := chi.RouteContext(r.Context())
if rcx == nil {
return namespace + ".not-chi"
}
var sb strings.Builder

var sb strings.Builder
rpt := rcx.RoutePattern()

rpt := rcx.RoutePattern()
sb.WriteString(namespace)
sb.WriteRune('.')
sb.WriteString(strings.ReplaceAll(rpt, "/", "_"))

sb.WriteString(namespace)
sb.WriteRune('.')
sb.WriteString(strings.ReplaceAll(rpt, "/", "_"))
return sb.String()
}

return sb.String()
}
func CustomMetricsFilter(
namespace string,
provider metrics.Provider,
formatter MetricsNameFormatter,
tagsGenerators ...MetricsTagGenerator,
) Filter {
var (
// counters = make(map[string]metrics.Counter)
histograms = make(map[string]metrics.Histogram)
)

if formatter == nil {
formatter = DefaultMetricsNameFormatter
}

return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
defer func() {
label := formatter(namespace, r)

tags := make([]string, len(tt))
copy(tags, tt)
var (
label = formatter(namespace, r)
tags = make(keyValues, 0)
)

// method
tags = append(tags, "method:"+r.Method)
tags = append(tags, []KeyValue{
{"method", r.Method},
}...)

// status code
if rw, ok := w.(WrapResponseWriter); ok {
tags = append(tags, "status_code:"+strconv.Itoa(rw.Status()))
tags = append(
tags,
KeyValue{"status_code", strconv.Itoa(rw.Status())},
)
}

c, ok := counters[label]
if !ok {
c = provider.NewCounter(label, 1)
counters[label] = c
}

c.With(tags...).Add(1)

h, ok := histograms[label]
if !ok {
h = provider.NewHistogram(label, 1)
}

h.With(tags...).Observe(float64(time.Since(start).Milliseconds()))
h.With(tags.tags()...).Observe(float64(time.Since(start).Milliseconds()))
}()

next.ServeHTTP(w, r)
Expand Down
6 changes: 3 additions & 3 deletions transport/http/filter_panic.go
Original file line number Diff line number Diff line change
Expand Up @@ -187,7 +187,7 @@ func WithoutStack() RecoveryOption {
return func(r *recovery) { r.returnStack = false; r.stackOthers = false }
}

func NewRecovery(
func newRecovery(
logger log.Logger,
options ...RecoveryOption,
) *recovery {
Expand All @@ -210,8 +210,8 @@ func NewRecovery(
return r
}

func PanicRecoveryFilter(logger log.Logger, options ...RecoveryOption) Filter {
recovery := NewRecovery(logger, options...)
func panicRecoveryFilter(logger log.Logger, options ...RecoveryOption) Filter {
recovery := newRecovery(logger, options...)

return func(next http.Handler) http.Handler {
return http.HandlerFunc(recovery.HandlerFunc(next))
Expand Down
51 changes: 35 additions & 16 deletions transport/http/filter_trace.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,25 @@ import (
"github.com/unbxd/go-base/v2/log"
)

func nc(val interface{}) string {
if val == nil {
return "nil"
}

st, ok := val.(string)
if !ok {
return "notstr"
}
return st
}

type TraceLogFieldsGen func(rw WrapResponseWriter, req *http.Request) []log.Field

// TraceLoggingFilter supersedes `NewTraceLoggerFinalizerHandlerOption` as this
// is more closer to the end of request handling phase.
// This reads most of the properties from Context and writes log line for loggers
// to consume.
func TraceLoggingFilter(logger log.Logger) Filter {
func TraceLoggingFilter(logger log.Logger, fieldGenerators ...TraceLogFieldsGen) Filter {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
Expand All @@ -29,27 +43,32 @@ func TraceLoggingFilter(logger log.Logger) Filter {
panic(msg)
}

fields = append(fields, log.Int("status", ww.Status()))

ctx := r.Context()
for k, ck := range map[string]ContextKey{
"method": ContextKeyRequestMethod,
"proto": ContextKeyRequestProto,
"host": ContextKeyRequestHost,
"remoteAddr": ContextKeyRequestRemoteAddr,
"xForwardedFor": ContextKeyRequestXForwardedFor,
"requestId": ContextKeyRequestXRequestID,
"req.method": ContextKeyRequestMethod,
"req.uri": ContextKeyRequestURI,
"req.path": ContextKeyRequestPath,
"req.host": ContextKeyRequestHost,
"req.remote_addr": ContextKeyRequestRemoteAddr,
"req.xfor": ContextKeyRequestXForwardedFor,
"req.ref": ContextKeyRequestReferer,
"req.id": ContextKeyRequestXRequestID,
"req.hdr.accept": ContextKeyRequestAccept,
"res.size": ContextKeyResponseSize,
} {
val := ctx.Value(ck)
if val != nil {
str := val.(string)
fields = append(fields, log.String(k, nc(ctx.Value(ck))))
}

fields = append(fields, log.String(k, str))
}
fields = append(fields, log.Int("status", ww.Status()))

for _, fg := range fieldGenerators {
fields = append(fields, fg(ww, r)...)
}

fields = append(fields, log.String("latencys", time.Since(start).String()))
fields = append(fields, log.Int64("latency", time.Since(start).Milliseconds()))
end := time.Since(start)

fields = append(fields, log.String("latencys", end.String()))
fields = append(fields, log.Int64("latency", end.Milliseconds()))

// trace is same as info here
logger.Info(r.URL.RequestURI(), fields...)
Expand Down
79 changes: 0 additions & 79 deletions transport/http/filters.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,82 +13,3 @@ func Chain(inner http.Handler, filters ...Filter) http.Handler {
}
return filters[0](Chain(inner, filters[1:]...))
}

// serverNameFilter is simple filter to set custom 'server' header for response
func serverNameFilter(name string, version string) Filter {
sn := name + "-" + version
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("server", sn)
next.ServeHTTP(w, r)
})
}
}

// closerFilter is builtin that wraps filter chain
func closerFilter() Filter {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
r.Body.Close()
r.Close = true
}()

next.ServeHTTP(w, r)
})
}
}

// decorateContextFilter decorates the http.Request.Context() with
// details about the http Request
// List of keys can be found in http.go
func decorateContextFilter() Filter {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(
w http.ResponseWriter,
r *http.Request,
) {
ctx := r.Context()

ctx = decorateContext(ctx, r)

r = r.WithContext(ctx)

next.ServeHTTP(w, r)
})
}
}

// NoopFilter doesn't do anything
func NoopFilter() Filter {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
next.ServeHTTP(w, r)
})
}
}

func heartbeatFilter(name string, heartbeats []string) Filter {
paths := make(map[string]struct{}, len(heartbeats))
for _, hb := range heartbeats {
paths[hb] = struct{}{}
}

message := name + " :: Ah, ha, ha, ha, stayin' alive, stayin' alive!"

return func(next http.Handler) http.Handler {
return http.HandlerFunc(
func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodGet {
_, ok := paths[r.URL.Path]
if ok {
w.Header().Set("Content-Type", "text/plain")
w.WriteHeader(http.StatusOK)
_, _ = w.Write([]byte(message))
return
}
}
next.ServeHTTP(w, r)
})
}
}
Loading

0 comments on commit c54ad9e

Please sign in to comment.