logging.ts 2.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071
  1. import { Formatter, Logger, type LogLevel } from "effect"
  2. import path from "path"
  3. import { Global } from "../global"
  4. import { runID } from "./shared"
  5. function formatter(id: string = runID) {
  6. return Logger.map(Logger.formatStructured, (output) => {
  7. const messages = Array.isArray(output.message) ? output.message : [output.message]
  8. return [
  9. ["timestamp", output.timestamp],
  10. ["level", output.level],
  11. ["run", id],
  12. ...messages.flatMap((value) => (plain(value) ? flatten(value) : [["message", value] as const])),
  13. ...(output.cause === undefined ? [] : [["cause", output.cause] as const]),
  14. ...flatten(output.spans),
  15. ...flatten(output.annotations),
  16. ]
  17. .map(([key, value]) => `${key}=${format(value)}`)
  18. .join(" ")
  19. })
  20. }
  21. function flatten(
  22. input: Record<string, unknown>,
  23. prefix = "",
  24. seen = new WeakSet<object>(),
  25. ): Array<readonly [string, unknown]> {
  26. if (seen.has(input)) return [[prefix, "[Circular]"]]
  27. seen.add(input)
  28. const entries = Object.entries(input)
  29. if (entries.length === 0 && prefix) return [[prefix, input]]
  30. return entries.flatMap(([key, value]) => {
  31. const path = prefix ? `${prefix}.${key}` : key
  32. return plain(value) ? flatten(value, path, seen) : [[path, value] as const]
  33. })
  34. }
  35. function plain(input: unknown): input is Record<string, unknown> {
  36. if (input === null || typeof input !== "object" || Array.isArray(input)) return false
  37. const prototype = Object.getPrototypeOf(input)
  38. return prototype === Object.prototype || prototype === null
  39. }
  40. function format(input: unknown) {
  41. const value = typeof input === "string" ? input : Formatter.format(input)
  42. return /^[^\s="\\]+$/.test(value) ? value : JSON.stringify(value)
  43. }
  44. export function fileLogger(file = path.join(Global.Path.log, "opencode.log"), id: string = runID) {
  45. // Do not set batchWindow to 0; it causes high idle CPU usage.
  46. return Logger.toFile(formatter(id), file, { flag: "a" })
  47. }
  48. const stderrLogger = Logger.make((options) => process.stderr.write(formatter().log(options) + "\n"))
  49. export function minimumLogLevel() {
  50. const value = process.env.OPENCODE_LOG_LEVEL?.toUpperCase()
  51. const levels = {
  52. DEBUG: "Debug",
  53. INFO: "Info",
  54. WARN: "Warn",
  55. ERROR: "Error",
  56. } as const satisfies Record<string, LogLevel.LogLevel>
  57. return value && value in levels ? levels[value as keyof typeof levels] : levels.INFO
  58. }
  59. export function loggers() {
  60. return process.env.OPENCODE_PRINT_LOGS === "1" ? [fileLogger(), stderrLogger] : [fileLogger()]
  61. }
  62. export * as Logging from "./logging"