Logger in opencode codebase.
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.
References:
-
https://github.com/sst/opencode/blob/dev/packages/opencode/src/util/log.ts#L6
-
https://github.com/sst/opencode/blob/dev/packages/console/core/src/actor.ts#L40
-
https://www.typescriptlang.org/docs/handbook/namespaces.html