-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
base: master
Are you sure you want to change the base?
Changes from all commits
97bba32
b30490f
26a6a75
c0ae8a6
6bf72e0
7439ab3
f1879d2
79f5602
3ee7b1e
2088b02
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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= |
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 | ||
currentCount int | ||
lastExport time.Time | ||
Comment on lines
+23
to
+24
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.