1
0
Fork 0

Multi-layer routing

Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
Simon Delicata 2025-10-22 11:58:05 +02:00 committed by GitHub
parent 8392503df7
commit d6598f370c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 2834 additions and 37 deletions

View file

@ -2,6 +2,7 @@ package accesslog
import (
"net/http"
"strings"
"time"
"github.com/rs/zerolog/log"
@ -76,3 +77,37 @@ func InitServiceFields(rw http.ResponseWriter, req *http.Request, next http.Hand
next.ServeHTTP(rw, req)
}
const separator = " -> "
// ConcatFieldHandler concatenates field values instead of overriding them.
type ConcatFieldHandler struct {
next http.Handler
name string
value string
}
// NewConcatFieldHandler creates a ConcatField handler that concatenates values.
func NewConcatFieldHandler(next http.Handler, name, value string) http.Handler {
return &ConcatFieldHandler{next: next, name: name, value: value}
}
func (c *ConcatFieldHandler) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
table := GetLogData(req)
if table == nil {
c.next.ServeHTTP(rw, req)
return
}
// Check if field already exists and concatenate if so
if existingValue, exists := table.Core[c.name]; exists && existingValue != nil {
if existingStr, ok := existingValue.(string); ok && strings.TrimSpace(existingStr) != "" {
table.Core[c.name] = existingStr + separator + c.value
c.next.ServeHTTP(rw, req)
return
}
}
table.Core[c.name] = c.value
c.next.ServeHTTP(rw, req)
}

View file

@ -0,0 +1,96 @@
package accesslog
import (
"context"
"net/http"
"net/http/httptest"
"testing"
"github.com/stretchr/testify/assert"
)
func TestConcatFieldHandler_ServeHTTP(t *testing.T) {
testCases := []struct {
desc string
existingValue interface{}
newValue string
expectedResult string
}{
{
desc: "first router - no existing value",
existingValue: nil,
newValue: "router1",
expectedResult: "router1",
},
{
desc: "second router - concatenate with existing string",
existingValue: "router1",
newValue: "router2",
expectedResult: "router1 -> router2",
},
{
desc: "third router - concatenate with existing chain",
existingValue: "router1 -> router2",
newValue: "router3",
expectedResult: "router1 -> router2 -> router3",
},
{
desc: "empty existing value - treat as first",
existingValue: " ",
newValue: "router1",
expectedResult: "router1",
},
{
desc: "non-string existing value - replace with new value",
existingValue: 123,
newValue: "router1",
expectedResult: "router1",
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
logData := &LogData{
Core: CoreLogData{},
}
if test.existingValue != nil {
logData.Core[RouterName] = test.existingValue
}
req := httptest.NewRequest(http.MethodGet, "/test", nil)
req = req.WithContext(context.WithValue(req.Context(), DataTableKey, logData))
handler := NewConcatFieldHandler(nextHandler, RouterName, test.newValue)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
assert.Equal(t, test.expectedResult, logData.Core[RouterName])
assert.Equal(t, http.StatusOK, rec.Code)
})
}
}
func TestConcatFieldHandler_ServeHTTP_NoLogData(t *testing.T) {
nextHandlerCalled := false
nextHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
nextHandlerCalled = true
w.WriteHeader(http.StatusOK)
})
handler := NewConcatFieldHandler(nextHandler, RouterName, "router1")
// Create request without LogData in context.
req := httptest.NewRequest(http.MethodGet, "/test", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
// Verify next handler was called and no panic occurred.
assert.True(t, nextHandlerCalled)
assert.Equal(t, http.StatusOK, rec.Code)
}

View file

@ -1180,6 +1180,83 @@ func logWriterTestHandlerFunc(rw http.ResponseWriter, r *http.Request) {
rw.WriteHeader(testStatus)
}
func TestConcatFieldHandler_LoggerIntegration(t *testing.T) {
logFilePath := filepath.Join(t.TempDir(), "access.log")
config := &otypes.AccessLog{FilePath: logFilePath, Format: CommonFormat}
logger, err := NewHandler(t.Context(), config)
require.NoError(t, err)
t.Cleanup(func() {
err := logger.Close()
require.NoError(t, err)
})
req := &http.Request{
Header: map[string][]string{
"User-Agent": {testUserAgent},
"Referer": {testReferer},
},
Proto: testProto,
Host: testHostname,
Method: testMethod,
RemoteAddr: fmt.Sprintf("%s:%d", testHostname, testPort),
URL: &url.URL{
User: url.UserPassword(testUsername, ""),
Path: testPath,
},
Body: io.NopCloser(bytes.NewReader([]byte("testdata"))),
}
chain := alice.New()
chain = chain.Append(capture.Wrap)
// Injection of the observability variables in the request context.
chain = chain.Append(func(next http.Handler) (http.Handler, error) {
return observability.WithObservabilityHandler(next, observability.Observability{
AccessLogsEnabled: true,
}), nil
})
chain = chain.Append(logger.AliceConstructor())
// Simulate multi-layer routing with concatenated router names
var handler http.Handler = http.HandlerFunc(func(rw http.ResponseWriter, r *http.Request) {
logData := GetLogData(r)
if logData != nil {
logData.Core[ServiceURL] = testServiceName
logData.Core[OriginStatus] = testStatus
logData.Core[OriginContentSize] = testContentSize
logData.Core[RetryAttempts] = testRetryAttempts
logData.Core[StartUTC] = testStart.UTC()
logData.Core[StartLocal] = testStart.Local()
}
rw.WriteHeader(testStatus)
if _, err := rw.Write([]byte(testContent)); err != nil {
http.Error(rw, err.Error(), http.StatusInternalServerError)
}
})
// Create chain of ConcatFieldHandlers to simulate multi-layer routing
handler = NewConcatFieldHandler(handler, RouterName, "child-router")
handler = NewConcatFieldHandler(handler, RouterName, "parent-router")
handler = NewConcatFieldHandler(handler, RouterName, "root-router")
finalHandler, err := chain.Then(handler)
require.NoError(t, err)
recorder := httptest.NewRecorder()
finalHandler.ServeHTTP(recorder, req)
logData, err := os.ReadFile(logFilePath)
require.NoError(t, err)
result, err := ParseAccessLog(string(logData))
require.NoError(t, err)
expectedRouterName := "\"root-router -> parent-router -> child-router\""
assert.Equal(t, expectedRouterName, result[RouterName])
}
func doLoggingWithAbortedStream(t *testing.T, config *otypes.AccessLog) {
t.Helper()