formatter_structured_test.go 6.9 KB

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