formatter_pretty_test.go 7.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345
  1. package logger
  2. import (
  3. "bytes"
  4. "errors"
  5. "log/slog"
  6. "strings"
  7. "testing"
  8. "time"
  9. "github.com/imgproxy/imgproxy/v3/testutil"
  10. "github.com/stretchr/testify/suite"
  11. )
  12. type FormatterPrettyTestSuite struct {
  13. testutil.LazySuite
  14. buf testutil.LazyObj[*bytes.Buffer]
  15. config testutil.LazyObj[*Config]
  16. handler testutil.LazyObj[*Handler]
  17. logger testutil.LazyObj[*slog.Logger]
  18. }
  19. func (s *FormatterPrettyTestSuite) SetupTest() {
  20. s.buf, _ = testutil.NewLazySuiteObj(
  21. s,
  22. func() (*bytes.Buffer, error) {
  23. return new(bytes.Buffer), nil
  24. },
  25. )
  26. s.config, _ = testutil.NewLazySuiteObj(
  27. s,
  28. func() (*Config, error) {
  29. cfg := NewDefaultConfig()
  30. cfg.Format = FormatPretty
  31. return &cfg, nil
  32. },
  33. )
  34. s.handler, _ = testutil.NewLazySuiteObj(
  35. s,
  36. func() (*Handler, error) {
  37. return NewHandler(s.buf(), s.config()), nil
  38. },
  39. )
  40. s.logger, _ = testutil.NewLazySuiteObj(
  41. s,
  42. func() (*slog.Logger, error) {
  43. return slog.New(s.handler()), nil
  44. },
  45. )
  46. }
  47. func (s *FormatterPrettyTestSuite) SetupSubTest() {
  48. s.ResetLazyObjects()
  49. }
  50. func (s *FormatterPrettyTestSuite) removeColorCodes() {
  51. p := s.buf().Bytes()
  52. q := p[:0]
  53. inEscape := false
  54. for i := 0; i < len(p); i++ {
  55. switch {
  56. case p[i] == '\x1b':
  57. // Skip ANSI escape codes
  58. inEscape = true
  59. case inEscape && p[i] == 'm':
  60. inEscape = false
  61. case !inEscape:
  62. q = append(q, p[i])
  63. }
  64. }
  65. s.buf().Truncate(len(q))
  66. }
  67. func (s *FormatterPrettyTestSuite) checkNextEntry(lvl, msg string) {
  68. // Remove color codes from the log entry,
  69. // we're not going to test coloring
  70. s.removeColorCodes()
  71. // Pretty level names are always 3 characters long
  72. s.Require().Len(lvl, 3)
  73. str, err := s.buf().ReadString('\n')
  74. s.Require().NoError(err)
  75. const timeLen = 19
  76. const lvlLen = 3 + 4 // +4 for the space and brackets
  77. const prefixLen = timeLen + lvlLen
  78. s.Require().GreaterOrEqual(len(str), prefixLen)
  79. timePart := str[:timeLen]
  80. levelPart := str[timeLen:prefixLen]
  81. now := time.Now()
  82. t, err := time.ParseInLocation(time.DateTime, timePart, now.Location())
  83. s.Require().NoError(err)
  84. s.Require().WithinDuration(time.Now(), t, time.Minute)
  85. s.Equal(" ["+lvl+"] ", levelPart)
  86. // Check the message
  87. s.Equal(msg+"\n", str[prefixLen:])
  88. }
  89. func (s *FormatterPrettyTestSuite) TestLevel() {
  90. type testEntry struct {
  91. level slog.Level
  92. levelName string
  93. message string
  94. }
  95. testEntries := []testEntry{
  96. {level: slog.LevelDebug, levelName: "DBG", message: "Debug message"},
  97. {level: slog.LevelInfo, levelName: "INF", message: "Info message"},
  98. {level: slog.LevelWarn, levelName: "WRN", message: "Warning message"},
  99. {level: slog.LevelError, levelName: "ERR", message: "Error message"},
  100. {level: LevelCritical, levelName: "CRT", message: "Critical message"},
  101. }
  102. testCases := []struct {
  103. level slog.Level
  104. entries []testEntry
  105. }{
  106. {level: slog.LevelDebug, entries: testEntries},
  107. {level: slog.LevelInfo, entries: testEntries[1:]},
  108. {level: slog.LevelWarn, entries: testEntries[2:]},
  109. {level: slog.LevelError, entries: testEntries[3:]},
  110. {level: LevelCritical, entries: testEntries[4:]},
  111. }
  112. for _, tc := range testCases {
  113. s.Run(tc.level.String(), func() {
  114. s.config().Level = tc.level
  115. for _, entry := range testEntries {
  116. s.logger().Log(s.T().Context(), entry.level, entry.message)
  117. }
  118. for _, entry := range tc.entries {
  119. s.checkNextEntry(entry.levelName, entry.message)
  120. }
  121. })
  122. }
  123. }
  124. func (s *FormatterPrettyTestSuite) TestAttributes() {
  125. s.logger().Info(
  126. "Test message",
  127. slog.String("string", "value"),
  128. slog.Int("int", -100),
  129. slog.Uint64("uint64", 200),
  130. slog.Float64("float64", 3.14),
  131. slog.Bool("bool", true),
  132. slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
  133. slog.Duration("duration", time.Minute),
  134. slog.Any("err", errors.New("error value")),
  135. slog.Any("any", struct {
  136. Field1 string
  137. Field2 int
  138. }{"value", 42}),
  139. )
  140. s.checkNextEntry(
  141. "INF",
  142. strings.Join([]string{
  143. "Test message",
  144. "string=value",
  145. "int=-100",
  146. "uint64=200",
  147. "float64=3.14",
  148. "bool=true",
  149. `time="1984-01-02 03:04:05"`,
  150. "duration=1m0s",
  151. `err="error value"`,
  152. `any="{Field1:value Field2:42}"`,
  153. }, " "),
  154. )
  155. }
  156. func (s *FormatterPrettyTestSuite) TestGroups() {
  157. s.Run("LastGroupNotEmpty", func() {
  158. s.logger().
  159. With(
  160. slog.String("string", "value"),
  161. slog.Int("int", -100),
  162. ).
  163. WithGroup("group1").
  164. With(
  165. slog.Uint64("uint64", 200),
  166. slog.Float64("float64", 3.14),
  167. ).
  168. WithGroup("group2").
  169. With(slog.Group("group3",
  170. slog.Bool("bool", true),
  171. slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
  172. )).
  173. With(slog.Group("empty_group")).
  174. WithGroup("group4").
  175. Info(
  176. "Test message",
  177. slog.Duration("duration", time.Minute),
  178. slog.Any("any", struct {
  179. Field1 string
  180. Field2 int
  181. }{"value", 42}),
  182. )
  183. s.checkNextEntry(
  184. "INF",
  185. strings.Join([]string{
  186. "Test message",
  187. "string=value",
  188. "int=-100",
  189. "group1={",
  190. "uint64=200",
  191. "float64=3.14",
  192. "group2={",
  193. "group3={",
  194. "bool=true",
  195. `time="1984-01-02 03:04:05"`,
  196. "}",
  197. "group4={",
  198. "duration=1m0s",
  199. `any="{Field1:value Field2:42}"`,
  200. "}",
  201. "}",
  202. "}",
  203. }, " "),
  204. )
  205. })
  206. s.Run("LastGroupsEmpty", func() {
  207. s.logger().
  208. With(
  209. slog.String("string", "value"),
  210. slog.Int("int", -100),
  211. ).
  212. WithGroup("group1").
  213. With(
  214. slog.Uint64("uint64", 200),
  215. slog.Float64("float64", 3.14),
  216. ).
  217. WithGroup("group2").
  218. With(slog.Group("group3",
  219. slog.Bool("bool", true),
  220. slog.Time("time", time.Date(1984, 1, 2, 3, 4, 5, 6, time.UTC)),
  221. )).
  222. With(slog.Group("empty_group")).
  223. WithGroup("group4").
  224. WithGroup("group5").
  225. Info("Test message")
  226. s.checkNextEntry(
  227. "INF",
  228. strings.Join([]string{
  229. "Test message",
  230. "string=value",
  231. "int=-100",
  232. "group1={",
  233. "uint64=200",
  234. "float64=3.14",
  235. "group2={",
  236. "group3={",
  237. "bool=true",
  238. `time="1984-01-02 03:04:05"`,
  239. "}",
  240. "}",
  241. "}",
  242. }, " "),
  243. )
  244. })
  245. }
  246. func (s *FormatterPrettyTestSuite) TestQuoting() {
  247. s.logger().Info(
  248. "Test message",
  249. "key", "value",
  250. "key with spaces", "value with spaces",
  251. `key"with"quotes`, `value"with"quotes`,
  252. "key\nwith\nnewlines", "value\nwith\nnewlines",
  253. slog.Group("group name", slog.String("key", "value")),
  254. )
  255. s.checkNextEntry(
  256. "INF",
  257. strings.Join([]string{
  258. "Test message",
  259. "key=value",
  260. `"key with spaces"="value with spaces"`,
  261. `"key\"with\"quotes"="value\"with\"quotes"`,
  262. `"key\nwith\nnewlines"="value\nwith\nnewlines"`,
  263. `"group name"={ key=value }`,
  264. }, " "),
  265. )
  266. }
  267. func (s *FormatterPrettyTestSuite) TestSpecialFields() {
  268. s.logger().Info(
  269. "Test message",
  270. "stack", "stack value\nwith new lines",
  271. "key1", "value1",
  272. "error", errors.New("error value"),
  273. "key2", "value2",
  274. "source", "source value",
  275. "key3", "value3",
  276. slog.Group(
  277. "group",
  278. "stack", "stack in group",
  279. "error", "error in group",
  280. "source", "source in group",
  281. ),
  282. )
  283. s.checkNextEntry(
  284. "INF",
  285. strings.Join([]string{
  286. "Test message",
  287. "key1=value1",
  288. "key2=value2",
  289. "key3=value3",
  290. "group={",
  291. `stack="stack in group"`,
  292. `error="error in group"`,
  293. `source="source in group"`,
  294. "}",
  295. `error="error value"`,
  296. `source="source value"`,
  297. }, " "),
  298. )
  299. s.removeColorCodes()
  300. s.Require().Equal("stack value\nwith new lines\n", s.buf().String())
  301. }
  302. func TestFormatterPretty(t *testing.T) {
  303. suite.Run(t, new(FormatterPrettyTestSuite))
  304. }