blob: 8d696b8354976500e651e955d0a62ab5bbeef20c [file] [log] [blame]
// Copyright 2017 The Go Authors. All rights reserved.
//
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file or at
// https://developers.google.com/open-source/licenses/bsd.
package requestlog
import (
"bytes"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"net/http"
"net/http/httptest"
"strings"
"testing"
"time"
)
func TestFluentdLog(t *testing.T) {
const (
startTime = 1507914000
startTimeNanos = 512
latencySec = 5
latencyNanos = 123456789
endTime = startTime + latencySec
endTimeNanos = startTimeNanos + latencyNanos
)
buf := new(bytes.Buffer)
var logErr error
l := NewFluentdLogger(buf, "mytag", func(e error) { logErr = e })
want := &Entry{
ReceivedTime: time.Unix(startTime, startTimeNanos),
RequestMethod: "POST",
RequestURL: "/foo/bar",
RequestHeaderSize: 456,
RequestBodySize: 123000,
UserAgent: "Chrome proxied through Firefox and Edge",
Referer: "http://www.example.com/",
Proto: "HTTP/1.1",
RemoteIP: "12.34.56.78",
ServerIP: "127.0.0.1",
Status: 404,
ResponseHeaderSize: 555,
ResponseBodySize: 789000,
Latency: latencySec*time.Second + latencyNanos*time.Nanosecond,
}
ent := *want // copy in case Log accidentally mutates
l.Log(&ent)
if logErr != nil {
t.Error("Logger called error callback:", logErr)
}
var got []json.RawMessage
if err := json.Unmarshal(buf.Bytes(), &got); err != nil {
t.Fatal("Unmarshal:", err)
}
if len(got) == 0 {
t.Fatal("Message is empty; want 3 elements")
}
var tag string
if err := json.Unmarshal(got[0], &tag); err != nil {
t.Error("Unmarshal tag:", err)
} else if want := "mytag"; tag != want {
t.Errorf("tag = %q; want %q", tag, want)
}
if len(got) < 2 {
t.Fatal("Message only has 1 element; want 3 elements")
}
var timestamp int64
if err := json.Unmarshal(got[1], &timestamp); err != nil {
t.Error("Unmarshal timestamp:", err)
} else if want := int64(endTime); timestamp != want {
t.Errorf("timestamp = %d; want %d", timestamp, want)
}
if len(got) < 3 {
t.Fatal("Message only has 2 elements; want 3 elements")
}
var r map[string]interface{}
if err := json.Unmarshal(got[2], &r); err != nil {
t.Error("Unmarshal record:", err)
} else {
rr, _ := r["httpRequest"].(map[string]interface{})
if rr == nil {
t.Error("httpRequest does not exist in record or is not a JSON object")
}
if got, want := jsonString(rr, "requestMethod"), ent.RequestMethod; got != want {
t.Errorf("httpRequest.requestMethod = %q; want %q", got, want)
}
if got, want := jsonString(rr, "requestUrl"), ent.RequestURL; got != want {
t.Errorf("httpRequest.requestUrl = %q; want %q", got, want)
}
if got, want := jsonString(rr, "requestSize"), "123456"; got != want {
t.Errorf("httpRequest.requestSize = %q; want %q", got, want)
}
if got, want := jsonNumber(rr, "status"), float64(ent.Status); got != want {
t.Errorf("httpRequest.status = %d; want %d", int64(got), int64(want))
}
if got, want := jsonString(rr, "responseSize"), "789555"; got != want {
t.Errorf("httpRequest.responseSize = %q; want %q", got, want)
}
if got, want := jsonString(rr, "userAgent"), ent.UserAgent; got != want {
t.Errorf("httpRequest.userAgent = %q; want %q", got, want)
}
if got, want := jsonString(rr, "remoteIp"), ent.RemoteIP; got != want {
t.Errorf("httpRequest.remoteIp = %q; want %q", got, want)
}
if got, want := jsonString(rr, "referer"), ent.Referer; got != want {
t.Errorf("httpRequest.referer = %q; want %q", got, want)
}
if got, want := jsonString(rr, "latency"), "5.123456789"; parseLatency(got) != want {
t.Errorf("httpRequest.latency = %q; want %q", got, want+"s")
}
ts, _ := r["timestamp"].(map[string]interface{})
if ts == nil {
t.Error("timestamp does not exist in record or is not a JSON object")
}
if got, want := jsonNumber(ts, "seconds"), float64(endTime); got != want {
t.Errorf("timestamp.seconds = %g; want %g", got, want)
}
if got, want := jsonNumber(ts, "nanos"), float64(endTimeNanos); got != want {
t.Errorf("timestamp.nanos = %g; want %g", got, want)
}
}
if len(got) > 3 {
t.Errorf("Message has %d elements; want 3 elements", len(got))
}
}
func parseLatency(s string) string {
s = strings.TrimSpace(s)
if !strings.HasSuffix(s, "s") {
return ""
}
s = strings.TrimSpace(s[:len(s)-1])
for _, c := range s {
if !(c >= '0' && c <= '9') && c != '.' {
return ""
}
}
return s
}
func jsonString(obj map[string]interface{}, k string) string {
v, _ := obj[k].(string)
return v
}
func jsonNumber(obj map[string]interface{}, k string) float64 {
v, _ := obj[k].(float64)
return v
}
func BenchmarkFluentdLog(b *testing.B) {
ent := &Entry{
ReceivedTime: time.Date(2017, time.October, 13, 17, 0, 0, 512, time.UTC),
RequestMethod: "POST",
RequestURL: "/foo/bar",
RequestHeaderSize: 456,
RequestBodySize: 123000,
UserAgent: "Chrome proxied through Firefox and Edge",
Referer: "http://www.example.com/",
Proto: "HTTP/1.1",
RemoteIP: "12.34.56.78",
ServerIP: "127.0.0.1",
Status: 404,
ResponseHeaderSize: 555,
ResponseBodySize: 789000,
Latency: 5 * time.Second,
}
var buf bytes.Buffer
l := NewFluentdLogger(&buf, "mytag", func(error) {})
l.Log(ent)
b.ReportAllocs()
b.SetBytes(int64(buf.Len()))
buf.Reset()
b.ResetTimer()
l = NewFluentdLogger(ioutil.Discard, "mytag", func(error) {})
for i := 0; i < b.N; i++ {
l.Log(ent)
}
}
func BenchmarkE2E(b *testing.B) {
run := func(b *testing.B, handler http.Handler) {
s := httptest.NewServer(handler)
defer s.Close()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
resp, err := http.Get(s.URL)
if err != nil {
b.Fatal(err)
}
io.Copy(ioutil.Discard, resp.Body)
resp.Body.Close()
}
}
b.Run("Baseline", func(b *testing.B) {
run(b, http.HandlerFunc(benchHandler))
})
b.Run("WithLog", func(b *testing.B) {
l := NewFluentdLogger(ioutil.Discard, "mytag", func(error) {})
run(b, NewHandler(l, http.HandlerFunc(benchHandler)))
})
}
func benchHandler(w http.ResponseWriter, r *http.Request) {
const msg = "Hello, World!"
w.Header().Set("Content-Length", fmt.Sprint(len(msg)))
io.WriteString(w, msg)
}