| // 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], ×tamp); 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) | |
| } |