Multi-layer routing
Co-authored-by: Romain <rtribotte@users.noreply.github.com>
This commit is contained in:
parent
8392503df7
commit
d6598f370c
37 changed files with 2834 additions and 37 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
96
pkg/middlewares/accesslog/field_middleware_test.go
Normal file
96
pkg/middlewares/accesslog/field_middleware_test.go
Normal 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)
|
||||
}
|
||||
|
|
@ -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()
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue