hub.go 6.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278
  1. package loghub
  2. import (
  3. "context"
  4. "log/slog"
  5. "sort"
  6. "strings"
  7. "sync"
  8. "time"
  9. )
  10. // Entry is the stable, secret-neutral representation exposed by the log API.
  11. // Callers remain responsible for never adding credentials or keying material
  12. // to slog attributes.
  13. type Entry struct {
  14. Time time.Time `json:"time"`
  15. Level string `json:"level"`
  16. Message string `json:"message"`
  17. Caller string `json:"caller,omitempty"`
  18. Fields map[string]any `json:"fields,omitempty"`
  19. }
  20. type core struct {
  21. mu sync.RWMutex
  22. capacity int
  23. entries []Entry
  24. subscribers map[uint64]chan Entry
  25. nextID uint64
  26. }
  27. // Hub is both a slog.Handler and a bounded live log source.
  28. type Hub struct {
  29. next slog.Handler
  30. core *core
  31. attrs []slog.Attr
  32. groups []string
  33. }
  34. func New(next slog.Handler, capacity int) *Hub {
  35. if next == nil {
  36. next = slog.NewTextHandler(discardWriter{}, nil)
  37. }
  38. if capacity < 100 {
  39. capacity = 100
  40. }
  41. return &Hub{
  42. next: next,
  43. core: &core{
  44. capacity: capacity,
  45. entries: make([]Entry, 0, capacity),
  46. subscribers: make(map[uint64]chan Entry),
  47. },
  48. }
  49. }
  50. func (h *Hub) Enabled(ctx context.Context, level slog.Level) bool {
  51. return h.next.Enabled(ctx, level)
  52. }
  53. func (h *Hub) Handle(ctx context.Context, record slog.Record) error {
  54. err := h.next.Handle(ctx, record)
  55. fields := make(map[string]any)
  56. for _, attr := range h.attrs {
  57. appendAttribute(fields, h.groups, attr)
  58. }
  59. record.Attrs(func(attr slog.Attr) bool {
  60. appendAttribute(fields, h.groups, attr)
  61. return true
  62. })
  63. entry := Entry{
  64. Time: record.Time.UTC(),
  65. Level: levelName(record.Level),
  66. Message: record.Message,
  67. Fields: fields,
  68. }
  69. if len(fields) == 0 {
  70. entry.Fields = nil
  71. }
  72. h.publish(entry)
  73. return err
  74. }
  75. func (h *Hub) WithAttrs(attrs []slog.Attr) slog.Handler {
  76. nextAttrs := append(append([]slog.Attr(nil), h.attrs...), attrs...)
  77. return &Hub{
  78. next: h.next.WithAttrs(attrs),
  79. core: h.core,
  80. attrs: nextAttrs,
  81. groups: append([]string(nil), h.groups...),
  82. }
  83. }
  84. func (h *Hub) WithGroup(name string) slog.Handler {
  85. name = strings.TrimSpace(name)
  86. groups := append([]string(nil), h.groups...)
  87. if name != "" {
  88. groups = append(groups, name)
  89. }
  90. return &Hub{
  91. next: h.next.WithGroup(name),
  92. core: h.core,
  93. attrs: append([]slog.Attr(nil), h.attrs...),
  94. groups: groups,
  95. }
  96. }
  97. func (h *Hub) publish(entry Entry) {
  98. h.core.mu.Lock()
  99. if len(h.core.entries) == h.core.capacity {
  100. copy(h.core.entries, h.core.entries[1:])
  101. h.core.entries[len(h.core.entries)-1] = cloneEntry(entry)
  102. } else {
  103. h.core.entries = append(h.core.entries, cloneEntry(entry))
  104. }
  105. for _, subscriber := range h.core.subscribers {
  106. select {
  107. case subscriber <- cloneEntry(entry):
  108. default:
  109. select {
  110. case <-subscriber:
  111. default:
  112. }
  113. select {
  114. case subscriber <- cloneEntry(entry):
  115. default:
  116. }
  117. }
  118. }
  119. h.core.mu.Unlock()
  120. }
  121. // History returns the newest matching entries in chronological order.
  122. func (h *Hub) History(limit int, minimum slog.Level, search string) []Entry {
  123. if limit < 1 {
  124. limit = 1
  125. }
  126. if limit > h.core.capacity {
  127. limit = h.core.capacity
  128. }
  129. search = strings.ToLower(strings.TrimSpace(search))
  130. h.core.mu.RLock()
  131. result := make([]Entry, 0, limit)
  132. for index := len(h.core.entries) - 1; index >= 0 && len(result) < limit; index-- {
  133. entry := h.core.entries[index]
  134. if parseLevel(entry.Level) < minimum {
  135. continue
  136. }
  137. if search != "" && !entryContains(entry, search) {
  138. continue
  139. }
  140. result = append(result, cloneEntry(entry))
  141. }
  142. h.core.mu.RUnlock()
  143. sort.SliceStable(result, func(i, j int) bool { return result[i].Time.Before(result[j].Time) })
  144. return result
  145. }
  146. func (h *Hub) Subscribe(buffer int) (<-chan Entry, func()) {
  147. if buffer < 1 {
  148. buffer = 1
  149. }
  150. if buffer > 1000 {
  151. buffer = 1000
  152. }
  153. channel := make(chan Entry, buffer)
  154. h.core.mu.Lock()
  155. id := h.core.nextID
  156. h.core.nextID++
  157. h.core.subscribers[id] = channel
  158. h.core.mu.Unlock()
  159. var once sync.Once
  160. cancel := func() {
  161. once.Do(func() {
  162. h.core.mu.Lock()
  163. delete(h.core.subscribers, id)
  164. close(channel)
  165. h.core.mu.Unlock()
  166. })
  167. }
  168. return channel, cancel
  169. }
  170. func appendAttribute(fields map[string]any, groups []string, attr slog.Attr) {
  171. attr.Value = attr.Value.Resolve()
  172. if attr.Equal(slog.Attr{}) {
  173. return
  174. }
  175. target := fields
  176. for _, group := range groups {
  177. next, ok := target[group].(map[string]any)
  178. if !ok {
  179. next = make(map[string]any)
  180. target[group] = next
  181. }
  182. target = next
  183. }
  184. if attr.Value.Kind() == slog.KindGroup {
  185. group := make(map[string]any)
  186. for _, child := range attr.Value.Group() {
  187. appendAttribute(group, nil, child)
  188. }
  189. target[attr.Key] = group
  190. return
  191. }
  192. target[attr.Key] = attr.Value.Any()
  193. }
  194. func levelName(level slog.Level) string {
  195. switch {
  196. case level >= slog.LevelError:
  197. return "error"
  198. case level >= slog.LevelWarn:
  199. return "warn"
  200. case level >= slog.LevelInfo:
  201. return "info"
  202. default:
  203. return "debug"
  204. }
  205. }
  206. func parseLevel(value string) slog.Level {
  207. switch strings.ToLower(strings.TrimSpace(value)) {
  208. case "error":
  209. return slog.LevelError
  210. case "warn", "warning":
  211. return slog.LevelWarn
  212. case "info", "":
  213. return slog.LevelInfo
  214. default:
  215. return slog.LevelDebug
  216. }
  217. }
  218. func entryContains(entry Entry, search string) bool {
  219. if strings.Contains(strings.ToLower(entry.Message), search) ||
  220. strings.Contains(strings.ToLower(entry.Caller), search) {
  221. return true
  222. }
  223. for key, value := range entry.Fields {
  224. if strings.Contains(strings.ToLower(key), search) ||
  225. strings.Contains(strings.ToLower(toString(value)), search) {
  226. return true
  227. }
  228. }
  229. return false
  230. }
  231. func cloneEntry(entry Entry) Entry {
  232. if entry.Fields != nil {
  233. entry.Fields = cloneMap(entry.Fields)
  234. }
  235. return entry
  236. }
  237. func cloneMap(source map[string]any) map[string]any {
  238. result := make(map[string]any, len(source))
  239. for key, value := range source {
  240. if nested, ok := value.(map[string]any); ok {
  241. result[key] = cloneMap(nested)
  242. } else {
  243. result[key] = value
  244. }
  245. }
  246. return result
  247. }
  248. func toString(value any) string {
  249. if stringValue, ok := value.(string); ok {
  250. return stringValue
  251. }
  252. return slog.AnyValue(value).String()
  253. }
  254. type discardWriter struct{}
  255. func (discardWriter) Write(data []byte) (int, error) {
  256. return len(data), nil
  257. }