There's a new context manager on the block. with died quietly, so naturally it's time to re-brand and revive :)
Setup:
Looking at the new using keyword I immediately found a scenario where it would be a really great pattern. Google Cloud Platform (GCP) has log(), warn() and error() for its cloud functions. And often times I need to run them locally first though, and some of the logs are pretty extensive. Instead of looking through the terminal window, I would want to import my own implementation of those functions from a helper file keeping the implementation syntax as is. This would allow me to have .log files for debugging and testing in a local folder.
I've been trying to accomplish it like this:
// localLogger.ts
import { createWriteStream } from "fs"
import { fileURLToPath } from "url"
import { dirname, join } from "path"
export function wrapLogs(
callee: string,
type: "info" | "warn" | "error" = "info"
) {
const logs: {
when: string
logs: string
payload?: any
}[] = []
const __importFile = fileURLToPath(import.meta.url)
const __logFileDir = dirname(__importFile)
const __logFile = join(__logFileDir, "../logs", `${type}.log`)
const stream = createWriteStream(__logFile, {
flags: "a",
encoding: "utf8",
})
return function (input?: string, payload: any = {}) {
if (input) {
const timeStamp = new Date().toISOString().split("T")[1].slice(0, -1)
logs.push({ when: timeStamp, logs: input })
if (Object.keys(payload).length) {
logs[logs.length - 1].payload = payload
}
}
return {
callee,
logs,
done: () => {
logs.forEach((log) => {
stream.write(`${log.when} - [${callee}] - ${type}: ${log.logs}\n`)
if (log.payload) {
stream.write(`payload: ${JSON.stringify(log.payload, null, 2)}\n`)
}
})
stream.on("finish", () => {
console.log("done")
})
stream.end()
},
}
}
}
this comes close, but no cigar. This implementation requires running the .done() method at the end of each function which implements the loggers - less than ideal, since that leaves a lot of room for forgetful mistakes. This is where using should theoretically come in.
// implementer.ts
import { wrapLogs } from './localLogger.ts'
function myCloudFuntion(){
const log = wrapLogs('myCloudFunction')
// does something and then logs
const data = { payload: 'relevant data' }
log("some useful log here", data)
// more stuff
log().done() // <- this part i would like to do without
return
}
So in the ideal world i would like to define loggers like so:
// localLogger.ts
...
// last return of the wrapLogs definition
return {
callee,
logs,
[Symbol.dispose]() {
logs.forEach((log) => {
stream.write(`${log.when} - [${callee}] - ${type}: ${log.logs}\n`)
if (log.payload) {
stream.write(`payload: ${JSON.stringify(log.payload, null, 2)}\n`)
}
})
stream.on("finish", () => {
console.log("done")
})
stream.end()
},
}
And in the implementing files:
// implementer.ts
function myCloudFunction(){
using log = wrapLogs("myCloudFunction")
//...
log("some message", data)
return // <- no more log().done()
}
Unfortunately this results in all sorts of issues.
Any help is welcome.
These are my initial steps into monad land and the using as well, so be gentle :)
EDIT: i've created a repo which should make the setup less time-consuming https://github.com/kolchurinvv/logger-replacements