Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix serverClientIp function that panic on non RouteHandler scope. #9

Merged
merged 1 commit into from
Jan 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading