log.ts 4.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. import path from "path"
  2. import fs from "fs/promises"
  3. import { Global } from "../global"
  4. import z from "zod"
  5. export namespace Log {
  6. export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).openapi({ ref: "LogLevel", description: "Log level" })
  7. export type Level = z.infer<typeof Level>
  8. const levelPriority: Record<Level, number> = {
  9. DEBUG: 0,
  10. INFO: 1,
  11. WARN: 2,
  12. ERROR: 3,
  13. }
  14. let currentLevel: Level = "INFO"
  15. export function setLevel(level: Level) {
  16. currentLevel = level
  17. }
  18. export function getLevel(): Level {
  19. return currentLevel
  20. }
  21. function shouldLog(level: Level): boolean {
  22. return levelPriority[level] >= levelPriority[currentLevel]
  23. }
  24. export type Logger = {
  25. debug(message?: any, extra?: Record<string, any>): void
  26. info(message?: any, extra?: Record<string, any>): void
  27. error(message?: any, extra?: Record<string, any>): void
  28. warn(message?: any, extra?: Record<string, any>): void
  29. tag(key: string, value: string): Logger
  30. clone(): Logger
  31. time(
  32. message: string,
  33. extra?: Record<string, any>,
  34. ): {
  35. stop(): void
  36. [Symbol.dispose](): void
  37. }
  38. }
  39. const loggers = new Map<string, Logger>()
  40. export const Default = create({ service: "default" })
  41. export interface Options {
  42. print: boolean
  43. level?: Level
  44. }
  45. let logpath = ""
  46. export function file() {
  47. return logpath
  48. }
  49. export async function init(options: Options) {
  50. const dir = path.join(Global.Path.data, "log")
  51. await fs.mkdir(dir, { recursive: true })
  52. cleanup(dir)
  53. if (options.print) return
  54. logpath = path.join(dir, new Date().toISOString().split(".")[0].replace(/:/g, "") + ".log")
  55. const logfile = Bun.file(logpath)
  56. await fs.truncate(logpath).catch(() => {})
  57. const writer = logfile.writer()
  58. process.stderr.write = (msg) => {
  59. writer.write(msg)
  60. writer.flush()
  61. return true
  62. }
  63. }
  64. async function cleanup(dir: string) {
  65. const entries = await fs.readdir(dir, { withFileTypes: true })
  66. const files = entries
  67. .filter((entry) => entry.isFile() && entry.name.endsWith(".log"))
  68. .map((entry) => path.join(dir, entry.name))
  69. if (files.length <= 5) return
  70. const filesToDelete = files.slice(0, -10)
  71. await Promise.all(filesToDelete.map((file) => fs.unlink(file).catch(() => {})))
  72. }
  73. let last = Date.now()
  74. export function create(tags?: Record<string, any>) {
  75. tags = tags || {}
  76. const service = tags["service"]
  77. if (service && typeof service === "string") {
  78. const cached = loggers.get(service)
  79. if (cached) {
  80. return cached
  81. }
  82. }
  83. function build(message: any, extra?: Record<string, any>) {
  84. const prefix = Object.entries({
  85. ...tags,
  86. ...extra,
  87. })
  88. .filter(([_, value]) => value !== undefined && value !== null)
  89. .map(([key, value]) => `${key}=${typeof value === "object" ? JSON.stringify(value) : value}`)
  90. .join(" ")
  91. const next = new Date()
  92. const diff = next.getTime() - last
  93. last = next.getTime()
  94. return [next.toISOString().split(".")[0], "+" + diff + "ms", prefix, message].filter(Boolean).join(" ") + "\n"
  95. }
  96. const result: Logger = {
  97. debug(message?: any, extra?: Record<string, any>) {
  98. if (shouldLog("DEBUG")) {
  99. process.stderr.write("DEBUG " + build(message, extra))
  100. }
  101. },
  102. info(message?: any, extra?: Record<string, any>) {
  103. if (shouldLog("INFO")) {
  104. process.stderr.write("INFO " + build(message, extra))
  105. }
  106. },
  107. error(message?: any, extra?: Record<string, any>) {
  108. if (shouldLog("ERROR")) {
  109. process.stderr.write("ERROR " + build(message, extra))
  110. }
  111. },
  112. warn(message?: any, extra?: Record<string, any>) {
  113. if (shouldLog("WARN")) {
  114. process.stderr.write("WARN " + build(message, extra))
  115. }
  116. },
  117. tag(key: string, value: string) {
  118. if (tags) tags[key] = value
  119. return result
  120. },
  121. clone() {
  122. return Log.create({ ...tags })
  123. },
  124. time(message: string, extra?: Record<string, any>) {
  125. const now = Date.now()
  126. result.info(message, { status: "started", ...extra })
  127. function stop() {
  128. result.info(message, {
  129. status: "completed",
  130. duration: Date.now() - now,
  131. ...extra,
  132. })
  133. }
  134. return {
  135. stop,
  136. [Symbol.dispose]() {
  137. stop()
  138. },
  139. }
  140. },
  141. }
  142. if (service && typeof service === "string") {
  143. loggers.set(service, result)
  144. }
  145. return result
  146. }
  147. }