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

ext/har: add HAR logger extension #610

Merged
merged 24 commits into from
Jan 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
97bba32
ext/har: add HAR logger extension
CameronBadman Dec 31, 2024
b30490f
reverted to go.mod and go.sum files
CameronBadman Dec 31, 2024
26a6a75
fixed code issues from pull request 610 other then testing and SaveTo…
CameronBadman Dec 31, 2024
c0ae8a6
added tesify testing and added fruther logs to ParseRequest
CameronBadman Jan 1, 2025
6bf72e0
added testify dependancy
CameronBadman Jan 1, 2025
7439ab3
modernised fillIpaddress func to work for ipv6 addresses
CameronBadman Jan 1, 2025
f1879d2
refactored HAR exporting componant
CameronBadman Jan 1, 2025
79f5602
Merge branch 'elazarl:master' into feature/har-logger-temp
CameronBadman Jan 1, 2025
a6ec1d2
Removed SaveToFile, SaveToFile Tests, AppendEntry, AppendPage, Clear,…
CameronBadman Jan 8, 2025
f67fd6d
removed currentCOunt and lastExport functions to and refactored assoc…
CameronBadman Jan 8, 2025
b136a64
refactored exportLoop to use a timer and interval, refactored logger …
CameronBadman Jan 8, 2025
34201aa
fixed export system to work better and be more readble, switched to f…
CameronBadman Jan 9, 2025
19e41ff
fixed a compilation issue in types and fixed the logger_tests to use …
CameronBadman Jan 9, 2025
28ad3c2
Merge branch 'master' into feature/har-logger
ErikPelli Jan 9, 2025
f24dec9
fixed issues from the pull request including readability issues in th…
CameronBadman Jan 9, 2025
3d7fe4d
Merge branch 'feature/har-logger' of github.com:CameronBadman/goproxy…
CameronBadman Jan 9, 2025
0de338d
updated Interval test to use a larger export Interval
CameronBadman Jan 9, 2025
2ea0b24
added client.Do in testing
CameronBadman Jan 9, 2025
c5b3196
removed uneeded spaces and fixed issues in testing from batching and …
CameronBadman Jan 9, 2025
f7022ef
fixed minor conflict
CameronBadman Jan 9, 2025
2ef471d
removed the entry copies on the ExportFunc and refactored threshold l…
CameronBadman Jan 9, 2025
8483e61
ctx is always not nil
ErikPelli Jan 10, 2025
1346472
Avoid to expose request and response parser functions
ErikPelli Jan 10, 2025
7165004
Update logger.go
ErikPelli Jan 10, 2025
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
7 changes: 7 additions & 0 deletions ext/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,13 @@ go 1.20

require (
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c
github.com/stretchr/testify v1.10.0
golang.org/x/net v0.34.0
golang.org/x/text v0.21.0
)

require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions ext/go.sum
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c h1:yWAGp1CjD1mQGLUsADqPn5s1n2AkGAX33XLDUgoXzyo=
github.com/elazarl/goproxy v0.0.0-20241217120900-7711dfa3811c/go.mod h1:P73liMk9TZCyF9fXG/RyMeSizmATvpvy3ZS61/1eXn4=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/net v0.34.0 h1:Mb7Mrk043xzHgnRM88suvJFwzVrRfHEHJEl5/71CKw0=
golang.org/x/net v0.34.0/go.mod h1:di0qlW3YNM5oh6GqDGQr92MyTozJPmybPK4Ev/Gm31k=
golang.org/x/text v0.21.0 h1:zyQAAkrwaneQ066sspRyJaG9VNi/YJ1NfzcGB3hZ/qo=
golang.org/x/text v0.21.0/go.mod h1:4IBbMaMmOPCJ8SecivzSH54+73PCFmPWxNTLm+vZkEQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
124 changes: 124 additions & 0 deletions ext/har/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package har

import (
"net/http"
"time"

"github.com/elazarl/goproxy"
CameronBadman marked this conversation as resolved.
Show resolved Hide resolved
)

// ExportFunc is a function type that users can implement to handle exported entries
type ExportFunc func([]Entry)

// Logger implements a HAR logging extension for goproxy
type Logger struct {
exportFunc ExportFunc
exportInterval time.Duration
exportThreshold int
dataCh chan Entry
}

// LoggerOption is a function type for configuring the Logger
type LoggerOption func(*Logger)

// WithExportInterval sets the interval for automatic exports
func WithExportInterval(d time.Duration) LoggerOption {
return func(l *Logger) {
l.exportInterval = d
}
}

// WithExportCount sets the number of requests after which to export entries
func WithExportThreshold(threshold int) LoggerOption {
return func(l *Logger) {
l.exportThreshold = threshold
}
}

// NewLogger creates a new HAR logger instance
func NewLogger(exportFunc ExportFunc, opts ...LoggerOption) *Logger {
l := &Logger{
exportFunc: exportFunc,
exportThreshold: 100, // Default threshold
exportInterval: 0, // Default no interval
dataCh: make(chan Entry),
}

// Apply options
for _, opt := range opts {
opt(l)
}

go l.exportLoop()
return l
}
// OnRequest handles incoming HTTP requests
func (l *Logger) OnRequest(req *http.Request, ctx *goproxy.ProxyCtx) (*http.Request, *http.Response) {
ctx.UserData = time.Now()
return req, nil
}

// OnResponse handles HTTP responses
func (l *Logger) OnResponse(resp *http.Response, ctx *goproxy.ProxyCtx) *http.Response {
if resp == nil || ctx.Req == nil || ctx.UserData == nil {
return resp
}
startTime, ok := ctx.UserData.(time.Time)
if !ok {
return resp
}

entry := Entry{
StartedDateTime: startTime,
Time: time.Since(startTime).Milliseconds(),
Request: parseRequest(ctx),
Response: parseResponse(ctx),
Timings: Timings{
Send: 0,
Wait: time.Since(startTime).Milliseconds(),
Receive: 0,
},
}
entry.fillIPAddress(ctx.Req)

l.dataCh <- entry
return resp
}

func (l *Logger) exportLoop() {
var entries []Entry

exportIfNeeded := func() {
if len(entries) > 0 {
go l.exportFunc(entries)
entries = nil
}
}

var tickerC <-chan time.Time
if l.exportInterval > 0 {
ticker := time.NewTicker(l.exportInterval)
defer ticker.Stop()
tickerC = ticker.C
}

for {
select {
case entry, ok := <-l.dataCh:
if !ok {
exportIfNeeded()
return
}
entries = append(entries, entry)
if l.exportThreshold > 0 && len(entries) >= l.exportThreshold {
exportIfNeeded()
}
case <-tickerC:
exportIfNeeded()
}
}
}

CameronBadman marked this conversation as resolved.
Show resolved Hide resolved
func (l *Logger) Stop() {
close(l.dataCh)
}
217 changes: 217 additions & 0 deletions ext/har/logger_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
package har

import (
"context"
"io"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"sync"
"testing"
"time"
"github.com/elazarl/goproxy"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// ConstantHandler is a simple HTTP handler that returns a constant response
type ConstantHandler string

func (h ConstantHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain")
io.WriteString(w, string(h))
}

// createTestProxy sets up a test proxy with a HAR logger
func createTestProxy(logger *Logger) *httptest.Server {
proxy := goproxy.NewProxyHttpServer()
proxy.OnRequest().DoFunc(logger.OnRequest)
proxy.OnResponse().DoFunc(logger.OnResponse)
return httptest.NewServer(proxy)
}

// createProxyClient creates an HTTP client that uses the given proxy
func createProxyClient(proxyURL string) *http.Client {
proxyURLParsed, _ := url.Parse(proxyURL)
tr := &http.Transport{
Proxy: http.ProxyURL(proxyURLParsed),
}
return &http.Client{Transport: tr}
}

func TestHarLoggerBasicFunctionality(t *testing.T) {
testCases := []struct {
name string
method string
body string
contentType string
expectedMethod string
}{
{
name: "GET Request",
method: http.MethodGet,
expectedMethod: http.MethodGet,
},
{
name: "POST Request",
method: http.MethodPost,
body: `{"test":"data"}`,
contentType: "application/json",
expectedMethod: http.MethodPost,
},
}

for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
var wg sync.WaitGroup
wg.Add(1)

var exportedEntries []Entry
exportFunc := func(entries []Entry) {
exportedEntries = append(exportedEntries, entries...)
wg.Done()
}

logger := NewLogger(exportFunc, WithExportThreshold(1)) // Export after each request
defer logger.Stop()

background := httptest.NewServer(ConstantHandler("hello world"))
defer background.Close()

proxyServer := createTestProxy(logger)
defer proxyServer.Close()

client := createProxyClient(proxyServer.URL)

req, err := http.NewRequestWithContext(
context.Background(),
tc.method,
background.URL,
strings.NewReader(tc.body),
)
require.NoError(t, err, "Should create request")

if tc.contentType != "" {
req.Header.Set("Content-Type", tc.contentType)
}

resp, err := client.Do(req)
require.NoError(t, err, "Should send request successfully")
defer resp.Body.Close()

bodyBytes, err := io.ReadAll(resp.Body)
require.NoError(t, err, "Should read response body")

body := string(bodyBytes)
assert.Equal(t, "hello world", body, "Response body should match")

wg.Wait() // Wait for export to complete

assert.Len(t, exportedEntries, 1, "Should have exactly one exported entry")
assert.Equal(t, tc.expectedMethod, exportedEntries[0].Request.Method, "Request method should match")
})
}
}

func TestLoggerThresholdExport(t *testing.T) {
var wg sync.WaitGroup
var exports [][]Entry
var mtx sync.Mutex
wg.Add(3) // Expect 3 exports (3,3,1)

exportFunc := func(entries []Entry) {
mtx.Lock()
exports = append(exports, entries)
mtx.Unlock()

t.Logf("Export occurred with %d entries", len(entries))
wg.Done()
}

threshold := 3
logger := NewLogger(exportFunc, WithExportThreshold(threshold))

background := httptest.NewServer(ConstantHandler("test"))
defer background.Close()
proxyServer := createTestProxy(logger)
defer proxyServer.Close()
client := createProxyClient(proxyServer.URL)

// Send 7 requests
for i := 0; i < 7; i++ {
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
background.URL,
nil,
)
require.NoError(t, err)

resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
}

// Call Stop to trigger final export of remaining entries
logger.Stop()
wg.Wait()

require.Equal(t, 3, len(exports), "should have 3 export batches")

// Count batches by size
batchCounts := make(map[int]int)
for _, batch := range exports {
batchCounts[len(batch)]++
}

// Check batch sizes
assert.Equal(t, 2, batchCounts[threshold], "should have two batches of threshold size")
assert.Equal(t, 1, batchCounts[1], "should have one batch with 1 entry")
}

func TestHarLoggerExportInterval(t *testing.T) {
var wg sync.WaitGroup
var mtx sync.Mutex
var exports [][]Entry
wg.Add(1) // Expect 1 export with all entries

exportFunc := func(entries []Entry) {
mtx.Lock()
exports = append(exports, entries)
mtx.Unlock()

t.Logf("Export occurred with %d entries", len(entries))
wg.Done()
}

logger := NewLogger(exportFunc, WithExportInterval(time.Second))

background := httptest.NewServer(ConstantHandler("test"))
defer background.Close()
proxyServer := createTestProxy(logger)
defer proxyServer.Close()
client := createProxyClient(proxyServer.URL)

// Send 3 requests
for i := 0; i < 3; i++ {
req, err := http.NewRequestWithContext(
context.Background(),
http.MethodGet,
background.URL,
nil,
)
require.NoError(t, err)

resp, err := client.Do(req)
require.NoError(t, err)
resp.Body.Close()
}

wg.Wait()
logger.Stop()

require.Equal(t, 1, len(exports), "should have 1 export batch")
assert.Equal(t, 3, len(exports[0]), "Should have exported 3 entries")
}

Loading
Loading