Skip to content

Commit

Permalink
feat: fix serverClientIp function that panic if non RouteHandler scope.
Browse files Browse the repository at this point in the history
  • Loading branch information
tigerwill90 committed Jan 25, 2025
1 parent fdb4379 commit 7783d1e
Show file tree
Hide file tree
Showing 7 changed files with 141 additions and 72 deletions.
51 changes: 27 additions & 24 deletions foxtrace.go
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package otelfox

import (
"errors"
"fmt"
"github.com/tigerwill90/fox"
"github.com/tigerwill90/otelfox/internal/clientip"
"github.com/tigerwill90/otelfox/internal/semconvutil"
semconv "go.opentelemetry.io/otel/semconv/v1.20.0"
"go.opentelemetry.io/otel/trace"
Expand All @@ -12,40 +14,35 @@ const (
tracerName = "github.com/tigerwill90/otelfox"
)

// Tracer is a Fox middleware that traces HTTP requests using OpenTelemetry.
type Tracer struct {
type middleware struct {
tracer trace.Tracer
cfg *config
service string
}

// New creates a new [Tracer] middleware for the given service.
// Options can be provided to configure the tracer.
func New(service string, opts ...Option) *Tracer {
// Middleware returns middleware that will trace incoming requests.
// The service parameter should describe the name of the (virtual)
// server handling the request.
func Middleware(service string, opts ...Option) fox.MiddlewareFunc {
tracer := createTracer(service, opts...)
return tracer.trace
}

func createTracer(service string, opts ...Option) middleware {
cfg := defaultConfig()
for _, opt := range opts {
opt.apply(cfg)
}

tracer := cfg.provider.Tracer(tracerName, trace.WithInstrumentationVersion(SemVersion()))
return &Tracer{
return middleware{
service: service,
tracer: tracer,
cfg: cfg,
}
}

// Middleware is a convenience function that creates a new [Tracer] middleware instance
// for the specified service and returns the Trace middleware function.
// Options can be provided to configure the tracer.
func Middleware(service string, opts ...Option) fox.MiddlewareFunc {
tracer := New(service, opts...)
return tracer.Trace
}

// Trace is a middleware function that wraps the provided HandlerFunc with tracing capabilities.
// It captures and records HTTP request information using OpenTelemetry.
func (t *Tracer) Trace(next fox.HandlerFunc) fox.HandlerFunc {
func (t middleware) trace(next fox.HandlerFunc) fox.HandlerFunc {
return func(c fox.Context) {

req := c.Request()
Expand Down Expand Up @@ -111,18 +108,24 @@ func (t *Tracer) Trace(next fox.HandlerFunc) fox.HandlerFunc {
}
}

func (t *Tracer) serverClientIP(c fox.Context) string {
if c.Route().ClientIPResolverEnabled() {
ipAddr, err := c.ClientIP()
func (t middleware) serverClientIP(c fox.Context) string {
if t.cfg.resolver != nil {
ipAddr, err := t.cfg.resolver.ClientIP(c)
if err != nil {
return ""
}
return ipAddr.String()
}

ipAddr, err := t.cfg.resolver.ClientIP(c)
if err != nil {
return ""
ipAddr, err := c.ClientIP()
if err == nil {
return ipAddr.String()
}
if errors.Is(err, fox.ErrNoClientIPResolver) {
ipAddr, err = clientip.DefaultResolver.ClientIP(c)
if err == nil {
return ipAddr.String()
}
}
return ipAddr.String()
return ""
}
80 changes: 58 additions & 22 deletions foxtrace_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -51,14 +51,15 @@ func TestPropagationWithGlobalPropagators(t *testing.T) {
ctx, _ = provider.Tracer(tracerName).Start(ctx, "test")
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header))

f, err := fox.New()
f, err := fox.New(
fox.WithMiddleware(Middleware("foobar", WithTracerProvider(provider))),
)
require.NoError(t, err)
mw := New("foobar", WithTracerProvider(provider))
_, err = f.Handle(http.MethodGet, "/user/{id}", mw.Trace(func(c fox.Context) {
_, err = f.Handle(http.MethodGet, "/user/{id}", func(c fox.Context) {
span := trace.SpanFromContext(c.Request().Context())
assert.Equal(t, sc.TraceID(), span.SpanContext().TraceID())
assert.Equal(t, sc.SpanID(), span.SpanContext().SpanID())
}))
})

require.NoError(t, err)
f.ServeHTTP(w, r)
Expand All @@ -80,18 +81,50 @@ func TestPropagationWithCustomPropagators(t *testing.T) {
ctx, _ = provider.Tracer(tracerName).Start(ctx, "test")
b3.Inject(ctx, propagation.HeaderCarrier(r.Header))

f, err := fox.New()
f, err := fox.New(
fox.WithMiddleware(Middleware("foobar", WithTracerProvider(provider), WithPropagators(b3))),
)
require.NoError(t, err)
mw := New("foobar", WithTracerProvider(provider), WithPropagators(b3))
_, err = f.Handle(http.MethodGet, "/user/{id}", mw.Trace(func(c fox.Context) {

_, err = f.Handle(http.MethodGet, "/user/{id}", func(c fox.Context) {
span := trace.SpanFromContext(c.Request().Context())
assert.Equal(t, sc.TraceID(), span.SpanContext().TraceID())
assert.Equal(t, sc.SpanID(), span.SpanContext().SpanID())
}))
})
require.NoError(t, err)
f.ServeHTTP(w, r)
}

func TestWithDefaultClientIPResolver(t *testing.T) {
provider := noop.NewTracerProvider()
otel.SetTextMapPropagator(b3prop.New())
r := httptest.NewRequest("GET", "/foo", nil)
r.Header.Set(fox.HeaderXForwardedFor, "25.13.12.11")
w := httptest.NewRecorder()

ctx := context.Background()
sc := trace.NewSpanContext(trace.SpanContextConfig{
TraceID: trace.TraceID{0x01},
SpanID: trace.SpanID{0x01},
})
ctx = trace.ContextWithRemoteSpanContext(ctx, sc)
ctx, _ = provider.Tracer(tracerName).Start(ctx, "test")
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header))

// Test with the default resolver
f, err := fox.New(
fox.WithMiddleware(Middleware("foobar", WithTracerProvider(provider))),
)
require.NoError(t, err)
_, err = f.Handle(http.MethodGet, "/bar", func(c fox.Context) {
t.Fail()
})
require.NoError(t, err)
assert.NotPanics(t, func() {
f.ServeHTTP(w, r)
})
}

func TestWithSpanAttributes(t *testing.T) {
provider := noop.NewTracerProvider()
otel.SetTextMapPropagator(b3prop.New())
Expand All @@ -108,24 +141,27 @@ func TestWithSpanAttributes(t *testing.T) {
ctx, _ = provider.Tracer(tracerName).Start(ctx, "test")
otel.GetTextMapPropagator().Inject(ctx, propagation.HeaderCarrier(r.Header))

f, err := fox.New()
f, err := fox.New(
fox.WithMiddleware(
Middleware("foobar", WithTracerProvider(provider), WithSpanAttributes(func(c fox.Context) []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 1, 2)
attrs[0] = attribute.String("http.target", r.URL.String())
v := c.Route().Annotation("annotation")
attrs = append(attrs, attribute.KeyValue{
Key: "annotation",
Value: attribute.StringValue(v.(string)),
})
return attrs
})),
),
)
require.NoError(t, err)
mw := New("foobar", WithTracerProvider(provider), WithSpanAttributes(func(c fox.Context) []attribute.KeyValue {
attrs := make([]attribute.KeyValue, 1, 2)
attrs[0] = attribute.String("http.target", r.URL.String())
v := c.Route().Annotation("annotation")
attrs = append(attrs, attribute.KeyValue{
Key: "annotation",
Value: attribute.StringValue(v.(string)),
})
return attrs
}))
_, err = f.Handle(http.MethodGet, "/user/{id}", mw.Trace(func(c fox.Context) {
_, err = f.Handle(http.MethodGet, "/user/{id}", func(c fox.Context) {
span := trace.SpanFromContext(c.Request().Context())
assert.Equal(t, sc.TraceID(), span.SpanContext().TraceID())
assert.Equal(t, sc.SpanID(), span.SpanContext().SpanID())
}), fox.WithAnnotation("annotation", "foobar"))

}, fox.WithAnnotation("annotation", "foobar"))
require.NoError(t, err)

f.ServeHTTP(w, r)
}
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ go 1.23.0

require (
github.com/stretchr/testify v1.10.0
github.com/tigerwill90/fox v0.20.0
github.com/tigerwill90/fox v0.20.1
go.opentelemetry.io/contrib/propagators/b3 v1.34.0
go.opentelemetry.io/otel v1.34.0
go.opentelemetry.io/otel/trace v1.34.0
Expand Down
4 changes: 2 additions & 2 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tigerwill90/fox v0.20.0 h1:bILdreDBwEhEoUdH3w7EL7L9yLkgd/X+oRP2o57iK2I=
github.com/tigerwill90/fox v0.20.0/go.mod h1:j86+yFuBav3kL1V5vSV71RyN5Y5Lqu7zqxk8VG5e+CY=
github.com/tigerwill90/fox v0.20.1 h1:rPQvN0FmyY/YlFgW/UJz5mwmofu1dAbP6DMGIuqCU8A=
github.com/tigerwill90/fox v0.20.1/go.mod h1:j86+yFuBav3kL1V5vSV71RyN5Y5Lqu7zqxk8VG5e+CY=
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
go.opentelemetry.io/contrib/propagators/b3 v1.34.0 h1:9pQdCEvV/6RWQmag94D6rhU+A4rzUhYBEJ8bpscx5p8=
Expand Down
50 changes: 50 additions & 0 deletions internal/clientip/clientip.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package clientip

import (
"github.com/tigerwill90/fox"
"github.com/tigerwill90/fox/clientip"
"net"
)

var DefaultResolver = newChain(
must(clientip.NewLeftmostNonPrivate(clientip.XForwardedForKey, 15)),
must(clientip.NewLeftmostNonPrivate(clientip.ForwardedKey, 15)),
must(clientip.NewSingleIPHeader(fox.HeaderXRealIP)),
must(clientip.NewSingleIPHeader(fox.HeaderCFConnectionIP)),
must(clientip.NewSingleIPHeader(fox.HeaderTrueClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderFastClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAzureClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAppengineRemoteAddr)),
must(clientip.NewSingleIPHeader(fox.HeaderFlyClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAzureSocketIP)),
clientip.NewRemoteAddr(),
)

type chain struct {
resolvers []fox.ClientIPResolver
}

func newChain(resolvers ...fox.ClientIPResolver) chain {
return chain{resolvers: resolvers}
}

// ClientIP try to derive the client IP using this resolver chain.
func (s chain) ClientIP(c fox.Context) (*net.IPAddr, error) {
var lastErr error
for _, sub := range s.resolvers {
ipAddr, err := sub.ClientIP(c)
if err == nil {
return ipAddr, nil
}
lastErr = err
}

return nil, lastErr
}

func must(resolver fox.ClientIPResolver, err error) fox.ClientIPResolver {
if err != nil {
panic(err)
}
return resolver
}
24 changes: 2 additions & 22 deletions option.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package otelfox

import (
"github.com/tigerwill90/fox"
"github.com/tigerwill90/fox/clientip"
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/propagation"
Expand Down Expand Up @@ -50,19 +49,6 @@ func defaultConfig() *config {
carrier: func(r *http.Request) propagation.TextMapCarrier {
return propagation.HeaderCarrier(r.Header)
},
resolver: clientip.NewChain(
must(clientip.NewLeftmostNonPrivate(clientip.XForwardedForKey, 15)),
must(clientip.NewLeftmostNonPrivate(clientip.ForwardedKey, 15)),
must(clientip.NewSingleIPHeader(fox.HeaderCFConnectionIP)),
must(clientip.NewSingleIPHeader(fox.HeaderTrueClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderFastClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAzureClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAzureSocketIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXAppengineRemoteAddr)),
must(clientip.NewSingleIPHeader(fox.HeaderFlyClientIP)),
must(clientip.NewSingleIPHeader(fox.HeaderXRealIP)),
clientip.NewRemoteAddr(),
),
}
}

Expand Down Expand Up @@ -127,18 +113,12 @@ func WithSpanAttributes(fn SpanAttributesFunc) Option {

// WithClientIPResolver sets a custom resolver to determine the client IP address.
// This is for advanced use case, must user should configure the resolver with Fox's router option using
// [fox.WithClientIPResolver].
// [fox.WithClientIPResolver]. Note that setting a resolver here takes priority over any resolver configured
// globally or at the route level in Fox.
func WithClientIPResolver(resolver fox.ClientIPResolver) Option {
return optionFunc(func(c *config) {
if resolver != nil {
c.resolver = resolver
}
})
}

func must(resolver fox.ClientIPResolver, err error) fox.ClientIPResolver {
if err != nil {
panic(err)
}
return resolver
}
2 changes: 1 addition & 1 deletion version.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ package otelfox

import "fmt"

const version = "v0.20.0"
const version = "v0.20.1"

var semver = fmt.Sprintf("semver:%s", version)

Expand Down

0 comments on commit 7783d1e

Please sign in to comment.