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

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
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.33.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.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
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=
185 changes: 185 additions & 0 deletions ext/har/logger.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package har

import (
"encoding/json"
"net/http"
"os"
"sync"
"time"
"github.com/elazarl/goproxy"
)

// 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 {
mu sync.Mutex
entries []Entry
captureContent bool
exportFunc ExportFunc
exportInterval time.Duration
exportCount int
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
exportCount int
exportThreshold int

currentCount int
lastExport time.Time
Comment on lines +23 to +24
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove these variables, they are useless

stopChan chan struct{}
}

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

// WithExportInterval sets the time interval for exporting entries
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 WithExportCount(count int) LoggerOption {
return func(l *Logger) {
l.exportCount = count
}
}

// NewLogger creates a new HAR logger instance
func NewLogger(exportFunc ExportFunc, opts ...LoggerOption) *Logger {
l := &Logger{
entries: make([]Entry, 0),
captureContent: true,
exportFunc: exportFunc,
stopChan: make(chan struct{}),
}

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) {
if ctx != nil {
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 == 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.Req, l.captureContent),
Response: ParseResponse(resp, l.captureContent),
Timings: Timings{
Send: 0,
Wait: time.Since(startTime).Milliseconds(),
Receive: 0,
},
}
entry.fillIPAddress(ctx.Req)

l.mu.Lock()
l.entries = append(l.entries, entry)
l.currentCount++
l.mu.Unlock()

return resp
}

func (l *Logger) exportLoop() {
ticker := time.NewTicker(100 * time.Millisecond) // Check frequently
defer ticker.Stop()

for {
select {
case <-ticker.C:
l.checkAndExport()
case <-l.stopChan:
return
}
}
}

func (l *Logger) checkAndExport() {
l.mu.Lock()
defer l.mu.Unlock()

shouldExport := false
if l.exportCount > 0 && l.currentCount >= l.exportCount {
shouldExport = true
} else if l.exportInterval > 0 && time.Since(l.lastExport) >= l.exportInterval {
shouldExport = true
}

if shouldExport && len(l.entries) > 0 {
l.exportFunc(l.entries)
l.entries = make([]Entry, 0)
l.currentCount = 0
l.lastExport = time.Now()
}
}

// Stop stops the export loop
func (l *Logger) Stop() {
close(l.stopChan)
}

// SaveToFile writes the current HAR log to a file
func (l *Logger) SaveToFile(filename string) error {
l.mu.Lock()
defer l.mu.Unlock()
file, err := os.Create(filename)
if err != nil {
return err
}
defer file.Close()

har := &Har{
Log: Log{
Version: "1.2",
Creator: Creator{
Name: "GoProxy",
Version: "1.0",
},
Entries: l.entries,
},
}

jsonData, err := json.Marshal(har)
if err != nil {
return err
}

_, err = file.Write(jsonData)
return err
}
Comment on lines +141 to +168
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete this function, it's unused


// Clear resets the HAR log
func (l *Logger) Clear() {
l.mu.Lock()
defer l.mu.Unlock()
l.entries = make([]Entry, 0)
l.currentCount = 0
}

// GetEntries returns a copy of the current HAR entries
func (l *Logger) GetEntries() []Entry {
l.mu.Lock()
defer l.mu.Unlock()
entries := make([]Entry, len(l.entries))
copy(entries, l.entries)
return entries
}
Comment on lines +171 to +185
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Delete these two functions, user will handle entries only inside its custom handler

Loading