In this article, we review logger in opencode codebase. We will look at:
- Log namespace 
- Log usage 
I study patterns used in an open source project found on Github Trending. For this week, I reviewed some parts of opencode codebase and wrote this article.
Log namespace
Logger in Opencode is defined at sst/opencode/packages/opencode/src/util/log.ts as a namespace.
 
export namespace Log {
  export const Level = z.enum(["DEBUG", "INFO", "WARN", "ERROR"]).meta({ ref: "LogLevel", description: "Log level" })
  export type Level = z.infer<typeof Level>
In TypeScript, a namespace serves as a mechanism to organize code and prevent naming conflicts, particularly in larger applications or when integrating with older JavaScript libraries. It acts as a container for logically related code, such as classes, interfaces, functions, and variables, effectively encapsulating them within a dedicated scope.
Learn more about Namespaces in TypeScript.
Logger type
Logger type is defined as shown below:
export type Logger = {
    debug(message?: any, extra?: Record<string, any>): void
    info(message?: any, extra?: Record<string, any>): void
    error(message?: any, extra?: Record<string, any>): void
    warn(message?: any, extra?: Record<string, any>): void
    tag(key: string, value: string): Logger
    clone(): Logger
    time(
      message: string,
      extra?: Record<string, any>,
    ): {
      stop(): void
      [Symbol.dispose](): void
    }
  }
This image shows a list of symbols in log.ts file, we are particularly interested in create method as this helps in understanding how this logger is used/invoked in other parts of codebase.
create function
create function has the following definition:
export function create(tags?: Record<string, any>) {
    tags = tags || {}
    const service = tags["service"]
    if (service && typeof service === "string") {
      const cached = loggers.get(service)
      if (cached) {
        return cached
      }
    }
    function build(message: any, extra?: Record<string, any>) {
      ...
    }
    const result: Logger = {
      debug(message?: any, extra?: Record<string, any>) {
      },
      info(message?: any, extra?: Record<string, any>) {
      },
      error(message?: any, extra?: Record<string, any>) {
      },
      warn(message?: any, extra?: Record<string, any>) {
      },
      tag(key: string, value: string) {
        if (tags) tags[key] = value
        return result
      },
      clone() {
        return Log.create({ ...tags })
      },
      time(message: string, extra?: Record<string, any>) {
      },
    }
    if (service && typeof service === "string") {
      loggers.set(service, result)
    }
    return result
  }
This create method accepts tags as a parameter. Below is the cache mechanism it has in place:
const service = tags["service"]
if (service && typeof service === "string") {
  const cached = loggers.get(service)
  if (cached) {
    return cached
  }
}
result is assigned an object with some functions defined. For example, following code snippet shows the debug function:
const result: Logger = {
  debug(message?: any, extra?: Record<string, any>) {
    if (shouldLog("DEBUG")) {
      process.stderr.write("DEBUG " + build(message, extra))
    }
  },
Another example is the tags:
tag(key: string, value: string) {
  if (tags) tags[key] = value
  return result
},
Log usage
At sst/opencode/packages/console/core/src/actor.ts#L40, you will find the following code:
import { Log } from "./util/log"
...
const log = Log.create().tag("namespace", "actor")
...
But this Log is imported from util/log file and it contains the below code:
import { Context } from "../context"
export namespace Log {
  const ctx = Context.create<{
    tags: Record<string, any>
  }>()
  export function create(tags?: Record<string, any>) {
    tags = tags || {}
    const result = {
      info(message?: any, extra?: Record<string, any>) {
        const prefix = Object.entries({
          ...use().tags,
          ...tags,
          ...extra,
        })
          .map(([key, value]) => `${key}=${value}`)
          .join(" ")
        console.log(prefix, message)
        return result
      },
      tag(key: string, value: string) {
        if (tags) tags[key] = value
        return result
      },
      clone() {
        return Log.create({ ...tags })
      },
    }
    return result
  }
  export function provide<R>(tags: Record<string, any>, cb: () => R) {
    const existing = use()
    return ctx.provide(
      {
        tags: {
          ...existing.tags,
          ...tags,
        },
      },
      cb,
    )
  }
  function use() {
    try {
      return ctx.use()
    } catch (e) {
      return { tags: {} }
    }
  }
}
Wait, does this mean this code is duplicated? I will leave that to you to find out ;)
About me:
Hey, my name is Ramu Narasinga. I study codebase architecture in large open-source projects.
Email: ramu.narasinga@gmail.com
Want to learn from open-source? Solve challenges inspired by open-source projects.
 



 
    
Top comments (0)