Provide Log Body in OTEL access Log

This commit is contained in:
Tom Moulard 2025-07-24 11:52:04 +02:00 committed by GitHub
parent c0edcc09bb
commit 5d85e6d088
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 151 additions and 93 deletions

View File

@ -364,7 +364,10 @@ func (h *Handler) logTheRoundTrip(ctx context.Context, logDataTable *LogData) {
totalDuration := time.Now().UTC().Sub(core[StartUTC].(time.Time)) totalDuration := time.Now().UTC().Sub(core[StartUTC].(time.Time))
core[Duration] = totalDuration core[Duration] = totalDuration
if h.keepAccessLog(status, retryAttempts, totalDuration) { if !h.keepAccessLog(status, retryAttempts, totalDuration) {
return
}
size := logDataTable.DownstreamResponse.size size := logDataTable.DownstreamResponse.size
core[DownstreamContentSize] = size core[DownstreamContentSize] = size
if original, ok := core[OriginContentSize]; ok { if original, ok := core[OriginContentSize]; ok {
@ -393,10 +396,24 @@ func (h *Handler) logTheRoundTrip(ctx context.Context, logDataTable *LogData) {
h.mu.Lock() h.mu.Lock()
defer h.mu.Unlock() defer h.mu.Unlock()
h.logger.WithContext(ctx).WithFields(fields).Println()
entry := h.logger.WithContext(ctx).WithFields(fields)
var message string
if h.config.OTLP != nil {
// If the logger is configured to use OpenTelemetry,
// we compute the log body with the formatter.
mBytes, err := h.logger.Formatter.Format(entry)
if err != nil {
message = fmt.Sprintf("Failed to format access log entry: %v", err)
} else {
message = string(mBytes)
} }
} }
entry.Println(message)
}
func (h *Handler) redactHeaders(headers http.Header, fields logrus.Fields, prefix string) { func (h *Handler) redactHeaders(headers http.Header, fields logrus.Fields, prefix string) {
for k := range headers { for k := range headers {
v := h.config.Fields.KeepHeader(k) v := h.config.Fields.KeepHeader(k)

View File

@ -56,7 +56,38 @@ var (
testStart = time.Now() testStart = time.Now()
) )
func TestOTelAccessLog(t *testing.T) { func TestOTelAccessLogWithBody(t *testing.T) {
testCases := []struct {
desc string
format string
bodyCheckFn func(*testing.T, string)
}{
{
desc: "Common format with log body",
format: CommonFormat,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For common format, verify the body contains the CLF formatted string
assert.Regexp(t, `"body":{"stringValue":".*- /health -.*200.*"}`, log)
},
},
{
desc: "JSON format with log body",
format: JSONFormat,
bodyCheckFn: func(t *testing.T, log string) {
t.Helper()
// For JSON format, verify the body contains the JSON formatted string
assert.Regexp(t, `"body":{"stringValue":".*DownstreamStatus.*:200.*"}`, log)
},
},
}
for _, test := range testCases {
t.Run(test.desc, func(t *testing.T) {
t.Parallel()
logCh := make(chan string) logCh := make(chan string)
collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { collector := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
gzr, err := gzip.NewReader(r.Body) gzr, err := gzip.NewReader(r.Body)
@ -77,6 +108,7 @@ func TestOTelAccessLog(t *testing.T) {
t.Cleanup(collector.Close) t.Cleanup(collector.Close)
config := &types.AccessLog{ config := &types.AccessLog{
Format: test.format,
OTLP: &types.OTelLog{ OTLP: &types.OTelLog{
ServiceName: "test", ServiceName: "test",
ResourceAttributes: map[string]string{"resource": "attribute"}, ResourceAttributes: map[string]string{"resource": "attribute"},
@ -95,7 +127,7 @@ func TestOTelAccessLog(t *testing.T) {
req := &http.Request{ req := &http.Request{
Header: map[string][]string{}, Header: map[string][]string{},
URL: &url.URL{ URL: &url.URL{
Path: testPath, Path: "/health",
}, },
} }
ctx := trace.ContextWithSpanContext(t.Context(), trace.NewSpanContext(trace.SpanContextConfig{ ctx := trace.ContextWithSpanContext(t.Context(), trace.NewSpanContext(trace.SpanContextConfig{
@ -126,10 +158,19 @@ func TestOTelAccessLog(t *testing.T) {
t.Error("AccessLog not exported") t.Error("AccessLog not exported")
case log := <-logCh: case log := <-logCh:
// Verify basic OTLP structure
assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log) assert.Regexp(t, `{"key":"resource","value":{"stringValue":"attribute"}}`, log)
assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log) assert.Regexp(t, `{"key":"service.name","value":{"stringValue":"test"}}`, log)
assert.Regexp(t, `{"key":"DownstreamStatus","value":{"intValue":"200"}}`, log) assert.Regexp(t, `{"key":"DownstreamStatus","value":{"intValue":"200"}}`, log)
assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log) assert.Regexp(t, `"traceId":"01020304050607080000000000000000","spanId":"0102030405060708"`, log)
// Most importantly, verify the log body is populated (not empty)
assert.NotRegexp(t, `"body":{"stringValue":""}`, log, "Log body should not be empty when OTLP is configured")
// Run format-specific body checks
test.bodyCheckFn(t, log)
}
})
} }
} }