logger.ts 2.6 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273
  1. import { Cause, Effect, Logger, References } from "effect"
  2. import * as Log from "../util/log"
  3. type Fields = Record<string, unknown>
  4. const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key)
  5. export interface Handle {
  6. readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
  7. readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
  8. readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
  9. readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void>
  10. readonly with: (extra: Fields) => Handle
  11. }
  12. const clean = (input?: Fields): Fields =>
  13. Object.fromEntries(
  14. Object.entries(input ?? {})
  15. .filter((entry) => entry[1] !== undefined && entry[1] !== null)
  16. .map(([key, value]) => [normalizeKey(key), value]),
  17. )
  18. const text = (input: unknown): string => {
  19. // oxlint-disable-next-line no-base-to-string
  20. if (Array.isArray(input)) return input.map((item) => String(item)).join(" ")
  21. // oxlint-disable-next-line no-base-to-string
  22. return input === undefined ? "" : String(input)
  23. }
  24. const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => {
  25. const ann = clean({ ...base, ...extra })
  26. const fx = run(msg)
  27. return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx
  28. }
  29. export const logger = Logger.make((opts) => {
  30. const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations))
  31. const now = opts.date.getTime()
  32. for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) {
  33. extra[`logSpan.${key}`] = `${now - start}ms`
  34. }
  35. if (opts.cause.reasons.length > 0) {
  36. extra.cause = Cause.pretty(opts.cause)
  37. }
  38. const svc = typeof extra.service === "string" ? extra.service : undefined
  39. if (svc) delete extra.service
  40. const log = svc ? Log.create({ service: svc }) : Log.Default
  41. const msg = text(opts.message)
  42. switch (opts.logLevel) {
  43. case "Trace":
  44. case "Debug":
  45. return log.debug(msg, extra)
  46. case "Warn":
  47. return log.warn(msg, extra)
  48. case "Error":
  49. case "Fatal":
  50. return log.error(msg, extra)
  51. default:
  52. return log.info(msg, extra)
  53. }
  54. })
  55. export const layer = Logger.layer([logger], { mergeWithExisting: false })
  56. export const create = (base: Fields = {}): Handle => ({
  57. debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra),
  58. info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra),
  59. warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra),
  60. error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra),
  61. with: (extra) => create({ ...base, ...extra }),
  62. })