Files
kubeshark/api/pkg/tap/har_writer.go
nimrod-up9 b03134919e Reduce delay between tap and UI - Skip dump to file (#26)
* Pass HARs between tap and api via channel.

* Fixed make docker commad.

* Various fixes.

* Added .DS_Store to .gitignore.

* Parse flags in Mizu main instead of in tap_output.go.

* Use channel to pass HAR by default instead of files.
2021-05-03 14:50:28 +03:00

223 lines
5.4 KiB
Go

package tap
import (
"encoding/json"
"errors"
"fmt"
"net/http"
"os"
"path/filepath"
"time"
"github.com/google/martian/har"
)
const readPermission = 0644
const tempFilenamePrefix = "har_writer"
type PairChanItem struct {
Request *http.Request
RequestTime time.Time
Response *http.Response
ResponseTime time.Time
}
func openNewHarFile(filename string) *HarFile {
file, err := os.OpenFile(filename, os.O_APPEND|os.O_CREATE|os.O_WRONLY, readPermission)
if err != nil {
panic(fmt.Sprintf("Failed to open output file: %s (%v,%+v)", err, err, err))
}
harFile := HarFile{file: file, entryCount: 0}
harFile.writeHeader()
return &harFile
}
type HarFile struct {
file *os.File
entryCount int
}
func NewEntry(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time) (*har.Entry, error) {
// TODO: quick fix until TRA-3212 is implemented
if request.URL == nil || request.Method == "" {
return nil, errors.New("Invalid request")
}
harRequest, err := har.NewRequest(request, true)
if err != nil {
SilentError("convert-request-to-har", "Failed converting request to HAR %s (%v,%+v)\n", err, err, err)
return nil, errors.New("Failed converting request to HAR")
}
// Martian copies http.Request.URL.String() to har.Request.URL.
// According to the spec, the URL field needs to be the absolute URL.
harRequest.URL = fmt.Sprintf("http://%s%s", request.Host, request.URL)
harResponse, err := har.NewResponse(response, true)
if err != nil {
SilentError("convert-response-to-har", "Failed converting response to HAR %s (%v,%+v)\n", err, err, err)
return nil, errors.New("Failed converting response to HAR")
}
totalTime := responseTime.Sub(requestTime).Round(time.Millisecond).Milliseconds()
if totalTime < 1 {
totalTime = 1
}
harEntry := har.Entry{
StartedDateTime: time.Now().UTC(),
Time: totalTime,
Request: harRequest,
Response: harResponse,
Cache: &har.Cache{},
Timings: &har.Timings{
Send: -1,
Wait: -1,
Receive: totalTime,
},
}
return &harEntry, nil
}
func (f *HarFile) WriteEntry(harEntry *har.Entry) {
harEntryJson, err := json.Marshal(harEntry)
if err != nil {
SilentError("har-entry-marshal", "Failed converting har entry object to JSON%s (%v,%+v)\n", err, err, err)
return
}
var separator string
if f.GetEntryCount() > 0 {
separator = ","
} else {
separator = ""
}
harEntryString := append([]byte(separator), harEntryJson...)
if _, err := f.file.Write(harEntryString); err != nil {
panic(fmt.Sprintf("Failed to write to output file: %s (%v,%+v)", err, err, err))
}
f.entryCount++
}
func (f *HarFile) GetEntryCount() int {
return f.entryCount
}
func (f *HarFile) Close() {
f.writeTrailer()
err := f.file.Close()
if err != nil {
panic(fmt.Sprintf("Failed to close output file: %s (%v,%+v)", err, err, err))
}
}
func (f*HarFile) writeHeader() {
header := []byte(`{"log": {"version": "1.2", "creator": {"name": "Mizu", "version": "0.0.1"}, "entries": [`)
if _, err := f.file.Write(header); err != nil {
panic(fmt.Sprintf("Failed to write header to output file: %s (%v,%+v)", err, err, err))
}
}
func (f*HarFile) writeTrailer() {
trailer := []byte("]}}")
if _, err := f.file.Write(trailer); err != nil {
panic(fmt.Sprintf("Failed to write trailer to output file: %s (%v,%+v)", err, err, err))
}
}
func NewHarWriter(outputDir string, maxEntries int) *HarWriter {
return &HarWriter{
OutputDirPath: outputDir,
MaxEntries: maxEntries,
PairChan: make(chan *PairChanItem),
OutChan: make(chan *har.Entry, 1000),
currentFile: nil,
done: make(chan bool),
}
}
type HarWriter struct {
OutputDirPath string
MaxEntries int
PairChan chan *PairChanItem
OutChan chan *har.Entry
currentFile *HarFile
done chan bool
}
func (hw *HarWriter) WritePair(request *http.Request, requestTime time.Time, response *http.Response, responseTime time.Time) {
hw.PairChan <- &PairChanItem{
Request: request,
RequestTime: requestTime,
Response: response,
ResponseTime: responseTime,
}
}
func (hw *HarWriter) Start() {
if hw.OutputDirPath != "" {
if err := os.MkdirAll(hw.OutputDirPath, os.ModePerm); err != nil {
panic(fmt.Sprintf("Failed to create output directory: %s (%v,%+v)", err, err, err))
}
}
go func() {
for pair := range hw.PairChan {
harEntry, err := NewEntry(pair.Request, pair.RequestTime, pair.Response, pair.ResponseTime)
if err != nil {
continue
}
if hw.OutputDirPath != "" {
if hw.currentFile == nil {
hw.openNewFile()
}
hw.currentFile.WriteEntry(harEntry)
if hw.currentFile.GetEntryCount() >= hw.MaxEntries {
hw.closeFile()
}
} else {
hw.OutChan <- harEntry
}
}
if hw.currentFile != nil {
hw.closeFile()
}
hw.done <- true
} ()
}
func (hw *HarWriter) Stop() {
close(hw.PairChan)
<-hw.done
}
func (hw *HarWriter) openNewFile() {
filename := filepath.Join(os.TempDir(), fmt.Sprintf("%s_%d", tempFilenamePrefix, time.Now().UnixNano()))
hw.currentFile = openNewHarFile(filename)
}
func (hw *HarWriter) closeFile() {
hw.currentFile.Close()
tmpFilename := hw.currentFile.file.Name()
hw.currentFile = nil
filename := buildFilename(hw.OutputDirPath, time.Now())
os.Rename(tmpFilename, filename)
}
func buildFilename(dir string, t time.Time) string {
// (epoch time in nanoseconds)__(YYYY_Month_DD__hh-mm-ss).har
filename := fmt.Sprintf("%d__%s.har", t.UnixNano(), t.Format("2006_Jan_02__15-04-05"))
return filepath.Join(dir, filename)
}