| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- // Copyright (c) Tailscale Inc & AUTHORS
- // SPDX-License-Identifier: BSD-3-Clause
- package logtail
- import (
- "bytes"
- "context"
- "encoding/json"
- "io"
- "net/http"
- "net/http/httptest"
- "strings"
- "testing"
- "time"
- "tailscale.com/tstest"
- "tailscale.com/tstime"
- )
- func TestFastShutdown(t *testing.T) {
- ctx, cancel := context.WithCancel(context.Background())
- cancel()
- testServ := httptest.NewServer(http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {}))
- defer testServ.Close()
- l := NewLogger(Config{
- BaseURL: testServ.URL,
- }, t.Logf)
- err := l.Shutdown(ctx)
- if err != nil {
- t.Error(err)
- }
- }
- // maximum number of times a test will call l.Write()
- const logLines = 3
- type LogtailTestServer struct {
- srv *httptest.Server // Log server
- uploaded chan []byte
- }
- func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
- ts := LogtailTestServer{}
- // max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
- ts.uploaded = make(chan []byte, 2+logLines)
- ts.srv = httptest.NewServer(http.HandlerFunc(
- func(w http.ResponseWriter, r *http.Request) {
- body, err := io.ReadAll(r.Body)
- if err != nil {
- t.Error("failed to read HTTP request")
- }
- ts.uploaded <- body
- }))
- t.Cleanup(ts.srv.Close)
- l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
- // There is always an initial "logtail started" message
- body := <-ts.uploaded
- if !strings.Contains(string(body), "started") {
- t.Errorf("unknown start logging statement: %q", string(body))
- }
- return &ts, l
- }
- func TestDrainPendingMessages(t *testing.T) {
- ts, l := NewLogtailTestHarness(t)
- for i := 0; i < logLines; i++ {
- l.Write([]byte("log line"))
- }
- // all of the "log line" messages usually arrive at once, but poll if needed.
- body := ""
- for i := 0; i <= logLines; i++ {
- body += string(<-ts.uploaded)
- count := strings.Count(body, "log line")
- if count == logLines {
- break
- }
- // if we never find count == logLines, the test will eventually time out.
- }
- err := l.Shutdown(context.Background())
- if err != nil {
- t.Error(err)
- }
- }
- func TestEncodeAndUploadMessages(t *testing.T) {
- ts, l := NewLogtailTestHarness(t)
- tests := []struct {
- name string
- log string
- want string
- }{
- {
- "plain text",
- "log line",
- "log line",
- },
- {
- "simple JSON",
- `{"text": "log line"}`,
- "log line",
- },
- }
- for _, tt := range tests {
- io.WriteString(l, tt.log)
- body := <-ts.uploaded
- data := unmarshalOne(t, body)
- got := data["text"]
- if got != tt.want {
- t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
- }
- ltail, ok := data["logtail"]
- if ok {
- logtailmap := ltail.(map[string]any)
- _, ok = logtailmap["client_time"]
- if !ok {
- t.Errorf("%s: no client_time present", tt.name)
- }
- } else {
- t.Errorf("%s: no logtail map present", tt.name)
- }
- }
- err := l.Shutdown(context.Background())
- if err != nil {
- t.Error(err)
- }
- }
- func TestEncodeSpecialCases(t *testing.T) {
- ts, l := NewLogtailTestHarness(t)
- // -------------------------------------------------------------------------
- // JSON log message already contains a logtail field.
- io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
- body := <-ts.uploaded
- data := unmarshalOne(t, body)
- errorHasLogtail, ok := data["error_has_logtail"]
- if ok {
- if errorHasLogtail != "LOGTAIL" {
- t.Errorf("error_has_logtail: got:%q; want:%q",
- errorHasLogtail, "LOGTAIL")
- }
- } else {
- t.Errorf("no error_has_logtail field: %v", data)
- }
- // -------------------------------------------------------------------------
- // special characters
- io.WriteString(l, "\b\f\n\r\t"+`"\`)
- bodytext := string(<-ts.uploaded)
- // json.Unmarshal would unescape the characters, we have to look at the encoded text
- escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
- if !escaped {
- t.Errorf("special characters got %s", bodytext)
- }
- // -------------------------------------------------------------------------
- // skipClientTime to omit the logtail metadata
- l.skipClientTime = true
- io.WriteString(l, "text")
- body = <-ts.uploaded
- data = unmarshalOne(t, body)
- _, ok = data["logtail"]
- if ok {
- t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
- }
- // -------------------------------------------------------------------------
- // lowMem + long string
- l.skipClientTime = false
- l.lowMem = true
- longStr := strings.Repeat("0", 5120)
- io.WriteString(l, longStr)
- body = <-ts.uploaded
- data = unmarshalOne(t, body)
- text, ok := data["text"]
- if !ok {
- t.Errorf("lowMem: no text %v", data)
- }
- if n := len(text.(string)); n > 4500 {
- t.Errorf("lowMem: got %d chars; want <4500 chars", n)
- }
- // -------------------------------------------------------------------------
- err := l.Shutdown(context.Background())
- if err != nil {
- t.Error(err)
- }
- }
- var sink []byte
- func TestLoggerEncodeTextAllocs(t *testing.T) {
- lg := &Logger{clock: tstime.StdClock{}}
- inBuf := []byte("some text to encode")
- procID := uint32(0x24d32ee9)
- procSequence := uint64(0x12346)
- err := tstest.MinAllocsPerRun(t, 1, func() {
- sink = lg.encodeText(inBuf, false, procID, procSequence, 0)
- })
- if err != nil {
- t.Fatal(err)
- }
- }
- func TestLoggerWriteLength(t *testing.T) {
- lg := &Logger{
- clock: tstime.StdClock{},
- buffer: NewMemoryBuffer(1024),
- }
- inBuf := []byte("some text to encode")
- n, err := lg.Write(inBuf)
- if err != nil {
- t.Error(err)
- }
- if n != len(inBuf) {
- t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
- }
- }
- func TestParseAndRemoveLogLevel(t *testing.T) {
- tests := []struct {
- log string
- wantLevel int
- wantLog string
- }{
- {
- "no level",
- 0,
- "no level",
- },
- {
- "[v1] level 1",
- 1,
- "level 1",
- },
- {
- "level 1 [v1] ",
- 1,
- "level 1 ",
- },
- {
- "[v2] level 2",
- 2,
- "level 2",
- },
- {
- "level [v2] 2",
- 2,
- "level 2",
- },
- {
- "[v3] no level 3",
- 0,
- "[v3] no level 3",
- },
- {
- "some ignored text then [v\x00JSON]5{\"foo\":1234}",
- 5,
- `{"foo":1234}`,
- },
- }
- for _, tt := range tests {
- gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
- if gotLevel != tt.wantLevel {
- t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
- tt.log, gotLevel, tt.wantLevel)
- }
- if string(gotLog) != tt.wantLog {
- t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
- tt.log, gotLog, tt.wantLog)
- }
- }
- }
- func unmarshalOne(t *testing.T, body []byte) map[string]any {
- t.Helper()
- var entries []map[string]any
- err := json.Unmarshal(body, &entries)
- if err != nil {
- t.Error(err)
- }
- if len(entries) != 1 {
- t.Fatalf("expected one entry, got %d", len(entries))
- }
- return entries[0]
- }
- func TestEncodeTextTruncation(t *testing.T) {
- lg := &Logger{clock: tstime.StdClock{}, lowMem: true}
- in := bytes.Repeat([]byte("a"), 5120)
- b := lg.encodeText(in, true, 0, 0, 0)
- got := string(b)
- want := `{"text": "` + strings.Repeat("a", 4096) + `…+1024"}` + "\n"
- if got != want {
- t.Errorf("got:\n%qwant:\n%q\n", got, want)
- }
- }
- type simpleMemBuf struct {
- Buffer
- buf bytes.Buffer
- }
- func (b *simpleMemBuf) Write(p []byte) (n int, err error) { return b.buf.Write(p) }
- func TestEncode(t *testing.T) {
- tests := []struct {
- in string
- want string
- }{
- {
- "normal",
- `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "text": "normal"}` + "\n",
- },
- {
- "and a [v1] level one",
- `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":1,"text": "and a level one"}` + "\n",
- },
- {
- "[v2] some verbose two",
- `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":2,"text": "some verbose two"}` + "\n",
- },
- {
- "{}",
- `{"logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
- },
- {
- `{"foo":"bar"}`,
- `{"foo":"bar","logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
- },
- {
- "foo: [v\x00JSON]0{\"foo\":1}",
- "{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1}}\n",
- },
- {
- "foo: [v\x00JSON]2{\"foo\":1}",
- "{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1},\"v\":2}\n",
- },
- }
- for _, tt := range tests {
- buf := new(simpleMemBuf)
- lg := &Logger{
- clock: tstest.NewClock(tstest.ClockOpts{Start: time.Unix(123, 456).UTC()}),
- buffer: buf,
- procID: 7,
- procSequence: 1,
- }
- io.WriteString(lg, tt.in)
- got := buf.buf.String()
- if got != tt.want {
- t.Errorf("for %q,\n got: %#q\nwant: %#q\n", tt.in, got, tt.want)
- }
- if err := json.Compact(new(bytes.Buffer), buf.buf.Bytes()); err != nil {
- t.Errorf("invalid output JSON for %q: %s", tt.in, got)
- }
- }
- }
- // Test that even if Logger.Write modifies the input buffer, we still return the
- // length of the input buffer, not what we shrank it down to. Otherwise the
- // caller will think we did a short write, violating the io.Writer contract.
- func TestLoggerWriteResult(t *testing.T) {
- buf := NewMemoryBuffer(100)
- lg := &Logger{
- clock: tstest.NewClock(tstest.ClockOpts{Start: time.Unix(123, 0)}),
- buffer: buf,
- }
- const in = "[v1] foo"
- n, err := lg.Write([]byte(in))
- if err != nil {
- t.Fatal(err)
- }
- if got, want := n, len(in); got != want {
- t.Errorf("Write = %v; want %v", got, want)
- }
- back, err := buf.TryReadLine()
- if err != nil {
- t.Fatal(err)
- }
- if got, want := string(back), `{"logtail": {"client_time": "1970-01-01T00:02:03Z"}, "v":1,"text": "foo"}`+"\n"; got != want {
- t.Errorf("mismatch.\n got: %#q\nwant: %#q", back, want)
- }
- }
|