mirror of
https://github.com/coder/coder.git
synced 2026-09-24 15:04:27 +08:00
feat: tracing improvements (#4988)
This commit is contained in:
+13
-3
@@ -1,6 +1,8 @@
|
||||
package httpmw
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -11,9 +13,13 @@ import (
|
||||
|
||||
func Logger(log slog.Logger) func(next http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
return http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
|
||||
start := time.Now()
|
||||
sw := &tracing.StatusWriter{ResponseWriter: w}
|
||||
|
||||
sw, ok := rw.(*tracing.StatusWriter)
|
||||
if !ok {
|
||||
panic(fmt.Sprintf("ResponseWriter not a *tracing.StatusWriter; got %T", rw))
|
||||
}
|
||||
|
||||
httplog := log.With(
|
||||
slog.F("host", httpapi.RequestHost(r)),
|
||||
@@ -51,7 +57,11 @@ func Logger(log slog.Logger) func(next http.Handler) http.Handler {
|
||||
logLevelFn = httplog.Warn
|
||||
}
|
||||
|
||||
logLevelFn(r.Context(), r.Method)
|
||||
// We already capture most of this information in the span (minus
|
||||
// the response body which we don't want to capture anyways).
|
||||
tracing.RunWithoutSpan(r.Context(), func(ctx context.Context) {
|
||||
logLevelFn(ctx, r.Method)
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
package patternmatcher
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/xerrors"
|
||||
)
|
||||
|
||||
// RoutePatterns provides a method to generate a regex which will match a URL
|
||||
// path against a collection of patterns. If any of the patterns match the path,
|
||||
// the regex will return a successful match.
|
||||
//
|
||||
// Multiple patterns can be provided and they are matched in order. Example:
|
||||
// - /api/* matches /api/1 but not /api or /api/1/2
|
||||
// - /api/*/2 matches /api/1/2 but not /api/2 /api/1
|
||||
// - /api/** matches /api/1, /api/1/2, /api/1/2/3 but not /api
|
||||
// - /api/**/3 matches /api/1/2, /api/1/2/3 but not /api, /api/1 or /api/1/2
|
||||
//
|
||||
// All patterns support an optional trailing slash.
|
||||
type RoutePatterns []string
|
||||
|
||||
func (rp RoutePatterns) MustCompile() *regexp.Regexp {
|
||||
re, err := rp.Compile()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return re
|
||||
}
|
||||
|
||||
func (rp RoutePatterns) Compile() (*regexp.Regexp, error) {
|
||||
patterns := make([]string, len(rp))
|
||||
for i, p := range rp {
|
||||
p = strings.ReplaceAll(p, "**", ".+")
|
||||
p = strings.ReplaceAll(p, "*", "[^/]+")
|
||||
if !strings.HasSuffix(p, "/") {
|
||||
p += "/?"
|
||||
}
|
||||
patterns[i] = p
|
||||
}
|
||||
|
||||
pattern := fmt.Sprintf("^(%s)$", strings.Join(patterns, "|"))
|
||||
re, err := regexp.Compile(pattern)
|
||||
if err != nil {
|
||||
return nil, xerrors.Errorf("compile regex %q: %w", pattern, err)
|
||||
}
|
||||
|
||||
return re, nil
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
package patternmatcher_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/coder/coder/coderd/httpmw/patternmatcher"
|
||||
)
|
||||
|
||||
func Test_RoutePatterns(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
patterns []string
|
||||
errContains string
|
||||
output string
|
||||
}{
|
||||
{
|
||||
name: "Empty",
|
||||
patterns: []string{},
|
||||
output: "^()$",
|
||||
},
|
||||
{
|
||||
name: "Single",
|
||||
patterns: []string{
|
||||
"/api",
|
||||
},
|
||||
output: "^(/api/?)$",
|
||||
},
|
||||
{
|
||||
name: "TrailingSlash",
|
||||
patterns: []string{
|
||||
"/api/",
|
||||
},
|
||||
output: "^(/api/)$",
|
||||
},
|
||||
{
|
||||
name: "Multiple",
|
||||
patterns: []string{
|
||||
"/api",
|
||||
"/api2",
|
||||
},
|
||||
output: "^(/api/?|/api2/?)$",
|
||||
},
|
||||
{
|
||||
name: "Star",
|
||||
patterns: []string{
|
||||
"/api/*",
|
||||
},
|
||||
output: "^(/api/[^/]+/?)$",
|
||||
},
|
||||
{
|
||||
name: "StarStar",
|
||||
patterns: []string{
|
||||
"/api/**",
|
||||
},
|
||||
output: "^(/api/.+/?)$",
|
||||
},
|
||||
{
|
||||
name: "TelemetryPatterns",
|
||||
patterns: []string{
|
||||
"/api",
|
||||
"/api/**",
|
||||
"/@*/*/apps/**",
|
||||
"/%40*/*/apps/**",
|
||||
"/gitauth/*/callback",
|
||||
},
|
||||
output: "^(/api/?|/api/.+/?|/@[^/]+/[^/]+/apps/.+/?|/%40[^/]+/[^/]+/apps/.+/?|/gitauth/[^/]+/callback/?)$",
|
||||
},
|
||||
{
|
||||
name: "Slash",
|
||||
patterns: []string{
|
||||
"/",
|
||||
},
|
||||
output: "^(/)$",
|
||||
},
|
||||
{
|
||||
name: "SlashStar",
|
||||
patterns: []string{
|
||||
"/*",
|
||||
},
|
||||
output: "^(/[^/]+/?)$",
|
||||
},
|
||||
{
|
||||
name: "SlashStarStar",
|
||||
patterns: []string{
|
||||
"/**",
|
||||
},
|
||||
output: "^(/.+/?)$",
|
||||
},
|
||||
{
|
||||
name: "SlashSlash",
|
||||
patterns: []string{
|
||||
"//",
|
||||
"/api//v1",
|
||||
},
|
||||
output: "^(//|/api//v1/?)$",
|
||||
},
|
||||
{
|
||||
name: "Invalid",
|
||||
patterns: []string{
|
||||
"/api(",
|
||||
},
|
||||
errContains: "compile regex",
|
||||
},
|
||||
}
|
||||
|
||||
for _, c := range cases {
|
||||
c := c
|
||||
t.Run(c.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rp := patternmatcher.RoutePatterns(c.patterns)
|
||||
re, err := rp.Compile()
|
||||
if c.errContains != "" {
|
||||
require.Error(t, err)
|
||||
require.ErrorContains(t, err, c.errContains)
|
||||
|
||||
require.Panics(t, func() {
|
||||
_ = rp.MustCompile()
|
||||
})
|
||||
} else {
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, c.output, re.String())
|
||||
|
||||
require.NotPanics(t, func() {
|
||||
re := rp.MustCompile()
|
||||
require.Equal(t, c.output, re.String())
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user