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

Export and refactor har.Logger.Entries #311

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
125 changes: 125 additions & 0 deletions har/entrylist.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
// Copyright 2020 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package har

import (
"container/list"
"sync"
)

// EntryList implements the har.EntryContainer interface for the storage of har.Entry
type EntryList struct {
mu sync.Mutex
items *list.List
}

func NewEntryList() *EntryList {
return &EntryList{
items: list.New(),
}
}

// AddEntry adds an entry to the entry list
func (el *EntryList) AddEntry(entry *Entry) {
el.mu.Lock()
defer el.mu.Unlock()

el.items.PushBack(entry)
}

// Entries returns a slice containing all entries
func (el *EntryList) Entries() []*Entry {
el.mu.Lock()
defer el.mu.Unlock()

es := make([]*Entry, 0, el.items.Len())

for e := el.items.Front(); e != nil; e = e.Next() {
es = append(es, e.Value.(*Entry))
}

return es
}

// RemoveMatches takes a matcher function and returns all entries that return true from the function
func (el *EntryList) RemoveCompleted() []*Entry {
el.mu.Lock()
defer el.mu.Unlock()

es := make([]*Entry, 0, el.items.Len())
var next *list.Element

for e := el.items.Front(); e != nil; e = next {
next = e.Next()

entry := getEntry(e)
if entry.Response != nil {
es = append(es, entry)
el.items.Remove(e)
}
}

return es
}

// RemoveEntry removes and entry from the entry list via the entry's id
func (el *EntryList) RemoveEntry(id string) *Entry {
el.mu.Lock()
defer el.mu.Unlock()

if e, en := el.retrieveElementEntry(id); e != nil {
el.items.Remove(e)

return en
}

return nil
}

// Reset reinitializes the entrylist
func (el *EntryList) Reset() {
el.mu.Lock()
defer el.mu.Unlock()

el.items.Init()
}

// RetrieveEntry returns an entry from the entrylist via the entry's id
func (el *EntryList) RetrieveEntry(id string) *Entry {
el.mu.Lock()
defer el.mu.Unlock()

_, en := el.retrieveElementEntry(id)

return en
}

func getEntry(e *list.Element) *Entry {
if e != nil {
return e.Value.(*Entry)
}

return nil
}

func (el *EntryList) retrieveElementEntry(id string) (*list.Element, *Entry) {
for e := el.items.Front(); e != nil; e = e.Next() {
if en := getEntry(e); en.ID == id {
return e, en
}
}

return nil, nil
}
74 changes: 74 additions & 0 deletions har/entrylist_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2020 Google Inc. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package har

import (
"net/http"
"testing"

"github.com/google/martian/v3"
)

func TestEntryList(t *testing.T) {
ids := make([]string, 3)
urls := make([]string, 3)

logger := NewLogger()

urls[0] = "http://0.example.com/path"
urls[1] = "http://1.example.com/path"
urls[2] = "http://2.example.com/path"

for idx, url := range urls {
req, err := http.NewRequest("GET", url, nil)
if err != nil {
t.Fatalf("http.NewRequest(): got %v, want no error", err)
}

_, remove, err := martian.TestContext(req, nil, nil)
if err != nil {
t.Fatalf("martian.TestContext(): got %v, want no error", err)
}
defer remove()

if err := logger.ModifyRequest(req); err != nil {
t.Fatalf("ModifyRequest(): got %v, want no error", err)
}

ids[idx] = logger.Entries.Entries()[idx].ID
}

for idx, url := range urls {
if got, want := logger.Entries.RetrieveEntry(ids[idx]).Request.URL, url; got != want {
t.Errorf("RetrieveEntry(): got %q, want %q", got, want)
}
}

if got, want := logger.Entries.RemoveEntry(ids[0]).Request.URL, urls[0]; got != want {
t.Errorf("RemoveEntry: got %q, want %q", got, want)
}

if got := logger.Entries.RemoveEntry(ids[0]); got != nil {
t.Errorf("RemoveEntry: should not have retrieve an entry")
}

if got, want := logger.Entries.RetrieveEntry(ids[2]).Request.URL, urls[2]; got != want {
t.Errorf("RemoveEntry got %q, want %q", got, want)
}

if got := logger.Entries.RetrieveEntry(""); got != nil {
t.Errorf("RetrieveEntry: should not have retrieve an entry")
}
}
92 changes: 25 additions & 67 deletions har/har.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import (
"net/http"
"net/url"
"strings"
"sync"
"time"
"unicode/utf8"

Expand All @@ -39,16 +38,24 @@ import (
"github.com/google/martian/v3/proxyutil"
)

// EntryContainer is an interface for the storage of the har entries
type EntryContainer interface {
AddEntry(entry *Entry)
Entries() []*Entry
RemoveCompleted() []*Entry
RemoveEntry(id string) *Entry
Reset()
RetrieveEntry(id string) *Entry
}

// Logger maintains request and response log entries.
type Logger struct {
bodyLogging func(*http.Response) bool
postDataLogging func(*http.Request) bool

creator *Creator

mu sync.Mutex
entries map[string]*Entry
tail *Entry
Entries EntryContainer
}

// HAR is the top level object of a HAR log.
Expand Down Expand Up @@ -91,7 +98,6 @@ type Entry struct {
// Timings describes various phases within request-response round trip. All
// times are specified in milliseconds.
Timings *Timings `json:"timings"`
next *Entry
}

// Request holds data about an individual HTTP request.
Expand Down Expand Up @@ -392,13 +398,19 @@ func NewLogger() *Logger {
Name: "martian proxy",
Version: "2.0.0",
},
entries: make(map[string]*Entry),
Entries: NewEntryList(),
}
l.SetOption(BodyLogging(true))
l.SetOption(PostDataLogging(true))
return l
}

// SetEntries allows the changing of the entry container to another struct which
// implements the EntryContainer interface
func (l *Logger) SetEntries(ec EntryContainer) {
l.Entries = ec
}

// SetOption sets configurable options on the logger.
func (l *Logger) SetOption(opts ...Option) {
for _, opt := range opts {
Expand Down Expand Up @@ -434,19 +446,11 @@ func (l *Logger) RecordRequest(id string, req *http.Request) error {
Timings: &Timings{},
}

l.mu.Lock()
defer l.mu.Unlock()

if _, exists := l.entries[id]; exists {
if l.Entries.RetrieveEntry(id) != nil {
return fmt.Errorf("Duplicate request ID: %s", id)
}
l.entries[id] = entry
if l.tail == nil {
l.tail = entry
}
entry.next = l.tail.next
l.tail.next = entry
l.tail = entry

l.Entries.AddEntry(entry)

return nil
}
Expand Down Expand Up @@ -504,10 +508,7 @@ func (l *Logger) RecordResponse(id string, res *http.Response) error {
return err
}

l.mu.Lock()
defer l.mu.Unlock()

if e, ok := l.entries[id]; ok {
if e := l.Entries.RetrieveEntry(id); e != nil {
e.Response = hres
e.Time = time.Since(e.StartedDateTime).Nanoseconds() / 1000000
}
Expand Down Expand Up @@ -563,53 +564,14 @@ func NewResponse(res *http.Response, withBody bool) (*Response, error) {

// Export returns the in-memory log.
func (l *Logger) Export() *HAR {
l.mu.Lock()
defer l.mu.Unlock()

es := make([]*Entry, 0, len(l.entries))
curr := l.tail
for curr != nil {
curr = curr.next
es = append(es, curr)
if curr == l.tail {
break
}
}
es := l.Entries.Entries()

return l.makeHAR(es)
}

// ExportAndReset returns the in-memory log for completed requests, clearing them.
func (l *Logger) ExportAndReset() *HAR {
l.mu.Lock()
defer l.mu.Unlock()

es := make([]*Entry, 0, len(l.entries))
curr := l.tail
prev := l.tail
var first *Entry
for curr != nil {
curr = curr.next
if curr.Response != nil {
es = append(es, curr)
delete(l.entries, curr.ID)
} else {
if first == nil {
first = curr
}
prev.next = curr
prev = curr
}
if curr == l.tail {
break
}
}
if len(l.entries) == 0 {
l.tail = nil
} else {
l.tail = prev
l.tail.next = first
}
es := l.Entries.RemoveCompleted()

return l.makeHAR(es)
}
Expand All @@ -626,11 +588,7 @@ func (l *Logger) makeHAR(es []*Entry) *HAR {

// Reset clears the in-memory log of entries.
func (l *Logger) Reset() {
l.mu.Lock()
defer l.mu.Unlock()

l.entries = make(map[string]*Entry)
l.tail = nil
l.Entries.Reset()
}

func cookies(cs []*http.Cookie) []Cookie {
Expand Down