Blog
Logger in opencode codebase.

Logger in opencode codebase.

In this article, we review logger in opencode codebase. We will look at:

  1. Log namespace

  2. 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.

References:

  1. https://github.com/sst/opencode/blob/dev/packages/opencode/src/util/log.ts#L6

  2. https://github.com/sst/opencode/blob/dev/packages/console/core/src/actor.ts#L40

  3. https://www.typescriptlang.org/docs/handbook/namespaces.html