logtail_test.go 9.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408
  1. // Copyright (c) Tailscale Inc & AUTHORS
  2. // SPDX-License-Identifier: BSD-3-Clause
  3. package logtail
  4. import (
  5. "bytes"
  6. "context"
  7. "encoding/json"
  8. "io"
  9. "net/http"
  10. "net/http/httptest"
  11. "strings"
  12. "testing"
  13. "time"
  14. "tailscale.com/tstest"
  15. "tailscale.com/tstime"
  16. )
  17. func TestFastShutdown(t *testing.T) {
  18. ctx, cancel := context.WithCancel(context.Background())
  19. cancel()
  20. testServ := httptest.NewServer(http.HandlerFunc(
  21. func(w http.ResponseWriter, r *http.Request) {}))
  22. defer testServ.Close()
  23. l := NewLogger(Config{
  24. BaseURL: testServ.URL,
  25. }, t.Logf)
  26. err := l.Shutdown(ctx)
  27. if err != nil {
  28. t.Error(err)
  29. }
  30. }
  31. // maximum number of times a test will call l.Write()
  32. const logLines = 3
  33. type LogtailTestServer struct {
  34. srv *httptest.Server // Log server
  35. uploaded chan []byte
  36. }
  37. func NewLogtailTestHarness(t *testing.T) (*LogtailTestServer, *Logger) {
  38. ts := LogtailTestServer{}
  39. // max channel backlog = 1 "started" + #logLines x "log line" + 1 "closed"
  40. ts.uploaded = make(chan []byte, 2+logLines)
  41. ts.srv = httptest.NewServer(http.HandlerFunc(
  42. func(w http.ResponseWriter, r *http.Request) {
  43. body, err := io.ReadAll(r.Body)
  44. if err != nil {
  45. t.Error("failed to read HTTP request")
  46. }
  47. ts.uploaded <- body
  48. }))
  49. t.Cleanup(ts.srv.Close)
  50. l := NewLogger(Config{BaseURL: ts.srv.URL}, t.Logf)
  51. // There is always an initial "logtail started" message
  52. body := <-ts.uploaded
  53. if !strings.Contains(string(body), "started") {
  54. t.Errorf("unknown start logging statement: %q", string(body))
  55. }
  56. return &ts, l
  57. }
  58. func TestDrainPendingMessages(t *testing.T) {
  59. ts, l := NewLogtailTestHarness(t)
  60. for i := 0; i < logLines; i++ {
  61. l.Write([]byte("log line"))
  62. }
  63. // all of the "log line" messages usually arrive at once, but poll if needed.
  64. body := ""
  65. for i := 0; i <= logLines; i++ {
  66. body += string(<-ts.uploaded)
  67. count := strings.Count(body, "log line")
  68. if count == logLines {
  69. break
  70. }
  71. // if we never find count == logLines, the test will eventually time out.
  72. }
  73. err := l.Shutdown(context.Background())
  74. if err != nil {
  75. t.Error(err)
  76. }
  77. }
  78. func TestEncodeAndUploadMessages(t *testing.T) {
  79. ts, l := NewLogtailTestHarness(t)
  80. tests := []struct {
  81. name string
  82. log string
  83. want string
  84. }{
  85. {
  86. "plain text",
  87. "log line",
  88. "log line",
  89. },
  90. {
  91. "simple JSON",
  92. `{"text": "log line"}`,
  93. "log line",
  94. },
  95. }
  96. for _, tt := range tests {
  97. io.WriteString(l, tt.log)
  98. body := <-ts.uploaded
  99. data := unmarshalOne(t, body)
  100. got := data["text"]
  101. if got != tt.want {
  102. t.Errorf("%s: got %q; want %q", tt.name, got.(string), tt.want)
  103. }
  104. ltail, ok := data["logtail"]
  105. if ok {
  106. logtailmap := ltail.(map[string]any)
  107. _, ok = logtailmap["client_time"]
  108. if !ok {
  109. t.Errorf("%s: no client_time present", tt.name)
  110. }
  111. } else {
  112. t.Errorf("%s: no logtail map present", tt.name)
  113. }
  114. }
  115. err := l.Shutdown(context.Background())
  116. if err != nil {
  117. t.Error(err)
  118. }
  119. }
  120. func TestEncodeSpecialCases(t *testing.T) {
  121. ts, l := NewLogtailTestHarness(t)
  122. // -------------------------------------------------------------------------
  123. // JSON log message already contains a logtail field.
  124. io.WriteString(l, `{"logtail": "LOGTAIL", "text": "text"}`)
  125. body := <-ts.uploaded
  126. data := unmarshalOne(t, body)
  127. errorHasLogtail, ok := data["error_has_logtail"]
  128. if ok {
  129. if errorHasLogtail != "LOGTAIL" {
  130. t.Errorf("error_has_logtail: got:%q; want:%q",
  131. errorHasLogtail, "LOGTAIL")
  132. }
  133. } else {
  134. t.Errorf("no error_has_logtail field: %v", data)
  135. }
  136. // -------------------------------------------------------------------------
  137. // special characters
  138. io.WriteString(l, "\b\f\n\r\t"+`"\`)
  139. bodytext := string(<-ts.uploaded)
  140. // json.Unmarshal would unescape the characters, we have to look at the encoded text
  141. escaped := strings.Contains(bodytext, `\b\f\n\r\t\"\`)
  142. if !escaped {
  143. t.Errorf("special characters got %s", bodytext)
  144. }
  145. // -------------------------------------------------------------------------
  146. // skipClientTime to omit the logtail metadata
  147. l.skipClientTime = true
  148. io.WriteString(l, "text")
  149. body = <-ts.uploaded
  150. data = unmarshalOne(t, body)
  151. _, ok = data["logtail"]
  152. if ok {
  153. t.Errorf("skipClientTime: unexpected logtail map present: %v", data)
  154. }
  155. // -------------------------------------------------------------------------
  156. // lowMem + long string
  157. l.skipClientTime = false
  158. l.lowMem = true
  159. longStr := strings.Repeat("0", 5120)
  160. io.WriteString(l, longStr)
  161. body = <-ts.uploaded
  162. data = unmarshalOne(t, body)
  163. text, ok := data["text"]
  164. if !ok {
  165. t.Errorf("lowMem: no text %v", data)
  166. }
  167. if n := len(text.(string)); n > 4500 {
  168. t.Errorf("lowMem: got %d chars; want <4500 chars", n)
  169. }
  170. // -------------------------------------------------------------------------
  171. err := l.Shutdown(context.Background())
  172. if err != nil {
  173. t.Error(err)
  174. }
  175. }
  176. var sink []byte
  177. func TestLoggerEncodeTextAllocs(t *testing.T) {
  178. lg := &Logger{clock: tstime.StdClock{}}
  179. inBuf := []byte("some text to encode")
  180. procID := uint32(0x24d32ee9)
  181. procSequence := uint64(0x12346)
  182. err := tstest.MinAllocsPerRun(t, 1, func() {
  183. sink = lg.encodeText(inBuf, false, procID, procSequence, 0)
  184. })
  185. if err != nil {
  186. t.Fatal(err)
  187. }
  188. }
  189. func TestLoggerWriteLength(t *testing.T) {
  190. lg := &Logger{
  191. clock: tstime.StdClock{},
  192. buffer: NewMemoryBuffer(1024),
  193. }
  194. inBuf := []byte("some text to encode")
  195. n, err := lg.Write(inBuf)
  196. if err != nil {
  197. t.Error(err)
  198. }
  199. if n != len(inBuf) {
  200. t.Errorf("logger.Write wrote %d bytes, expected %d", n, len(inBuf))
  201. }
  202. }
  203. func TestParseAndRemoveLogLevel(t *testing.T) {
  204. tests := []struct {
  205. log string
  206. wantLevel int
  207. wantLog string
  208. }{
  209. {
  210. "no level",
  211. 0,
  212. "no level",
  213. },
  214. {
  215. "[v1] level 1",
  216. 1,
  217. "level 1",
  218. },
  219. {
  220. "level 1 [v1] ",
  221. 1,
  222. "level 1 ",
  223. },
  224. {
  225. "[v2] level 2",
  226. 2,
  227. "level 2",
  228. },
  229. {
  230. "level [v2] 2",
  231. 2,
  232. "level 2",
  233. },
  234. {
  235. "[v3] no level 3",
  236. 0,
  237. "[v3] no level 3",
  238. },
  239. {
  240. "some ignored text then [v\x00JSON]5{\"foo\":1234}",
  241. 5,
  242. `{"foo":1234}`,
  243. },
  244. }
  245. for _, tt := range tests {
  246. gotLevel, gotLog := parseAndRemoveLogLevel([]byte(tt.log))
  247. if gotLevel != tt.wantLevel {
  248. t.Errorf("parseAndRemoveLogLevel(%q): got:%d; want %d",
  249. tt.log, gotLevel, tt.wantLevel)
  250. }
  251. if string(gotLog) != tt.wantLog {
  252. t.Errorf("parseAndRemoveLogLevel(%q): got:%q; want %q",
  253. tt.log, gotLog, tt.wantLog)
  254. }
  255. }
  256. }
  257. func unmarshalOne(t *testing.T, body []byte) map[string]any {
  258. t.Helper()
  259. var entries []map[string]any
  260. err := json.Unmarshal(body, &entries)
  261. if err != nil {
  262. t.Error(err)
  263. }
  264. if len(entries) != 1 {
  265. t.Fatalf("expected one entry, got %d", len(entries))
  266. }
  267. return entries[0]
  268. }
  269. func TestEncodeTextTruncation(t *testing.T) {
  270. lg := &Logger{clock: tstime.StdClock{}, lowMem: true}
  271. in := bytes.Repeat([]byte("a"), 5120)
  272. b := lg.encodeText(in, true, 0, 0, 0)
  273. got := string(b)
  274. want := `{"text": "` + strings.Repeat("a", 4096) + `…+1024"}` + "\n"
  275. if got != want {
  276. t.Errorf("got:\n%qwant:\n%q\n", got, want)
  277. }
  278. }
  279. type simpleMemBuf struct {
  280. Buffer
  281. buf bytes.Buffer
  282. }
  283. func (b *simpleMemBuf) Write(p []byte) (n int, err error) { return b.buf.Write(p) }
  284. func TestEncode(t *testing.T) {
  285. tests := []struct {
  286. in string
  287. want string
  288. }{
  289. {
  290. "normal",
  291. `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "text": "normal"}` + "\n",
  292. },
  293. {
  294. "and a [v1] level one",
  295. `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":1,"text": "and a level one"}` + "\n",
  296. },
  297. {
  298. "[v2] some verbose two",
  299. `{"logtail": {"client_time": "1970-01-01T00:02:03.000000456Z","proc_id": 7,"proc_seq": 1}, "v":2,"text": "some verbose two"}` + "\n",
  300. },
  301. {
  302. "{}",
  303. `{"logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
  304. },
  305. {
  306. `{"foo":"bar"}`,
  307. `{"foo":"bar","logtail":{"client_time":"1970-01-01T00:02:03.000000456Z","proc_id":7,"proc_seq":1}}` + "\n",
  308. },
  309. {
  310. "foo: [v\x00JSON]0{\"foo\":1}",
  311. "{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1}}\n",
  312. },
  313. {
  314. "foo: [v\x00JSON]2{\"foo\":1}",
  315. "{\"foo\":1,\"logtail\":{\"client_time\":\"1970-01-01T00:02:03.000000456Z\",\"proc_id\":7,\"proc_seq\":1},\"v\":2}\n",
  316. },
  317. }
  318. for _, tt := range tests {
  319. buf := new(simpleMemBuf)
  320. lg := &Logger{
  321. clock: tstest.NewClock(tstest.ClockOpts{Start: time.Unix(123, 456).UTC()}),
  322. buffer: buf,
  323. procID: 7,
  324. procSequence: 1,
  325. }
  326. io.WriteString(lg, tt.in)
  327. got := buf.buf.String()
  328. if got != tt.want {
  329. t.Errorf("for %q,\n got: %#q\nwant: %#q\n", tt.in, got, tt.want)
  330. }
  331. if err := json.Compact(new(bytes.Buffer), buf.buf.Bytes()); err != nil {
  332. t.Errorf("invalid output JSON for %q: %s", tt.in, got)
  333. }
  334. }
  335. }
  336. // Test that even if Logger.Write modifies the input buffer, we still return the
  337. // length of the input buffer, not what we shrank it down to. Otherwise the
  338. // caller will think we did a short write, violating the io.Writer contract.
  339. func TestLoggerWriteResult(t *testing.T) {
  340. buf := NewMemoryBuffer(100)
  341. lg := &Logger{
  342. clock: tstest.NewClock(tstest.ClockOpts{Start: time.Unix(123, 0)}),
  343. buffer: buf,
  344. }
  345. const in = "[v1] foo"
  346. n, err := lg.Write([]byte(in))
  347. if err != nil {
  348. t.Fatal(err)
  349. }
  350. if got, want := n, len(in); got != want {
  351. t.Errorf("Write = %v; want %v", got, want)
  352. }
  353. back, err := buf.TryReadLine()
  354. if err != nil {
  355. t.Fatal(err)
  356. }
  357. if got, want := string(back), `{"logtail": {"client_time": "1970-01-01T00:02:03Z"}, "v":1,"text": "foo"}`+"\n"; got != want {
  358. t.Errorf("mismatch.\n got: %#q\nwant: %#q", back, want)
  359. }
  360. }