Logging
KickJS ships with a small LoggerProvider interface and a zero-dep default that writes to console. The framework never imports a specific logging library — you bring your own when you want one.
The default
Out of the box, Logger.for('UserService').info('User created') calls console.log('[UserService] User created'). No setup, no extra deps, no pino-pretty to install. Works in Node, Bun, Deno, edge runtimes, anywhere console.* exists.
import { Logger } from '@forinda/kickjs'
const log = Logger.for('UserService')
log.info('User created', { id: 'usr_123' })
log.warn('Quota approaching')
log.error('DB unreachable', err)
log.debug('Cache miss for key=%s', key)For most apps that's enough. When it isn't, plug in a real logger.
The contract
export interface LoggerProvider {
info(msg: string, ...args: any[]): void
warn(msg: string, ...args: any[]): void
error(msg: string, ...args: any[]): void
debug(msg: string, ...args: any[]): void
trace?(msg: string, ...args: any[]): void // optional — falls back to debug
fatal?(msg: string, ...args: any[]): void // optional — falls back to error
child(bindings: { component: string }): LoggerProvider
}Implement this and pass it to Logger.setProvider() before bootstrap(). Every Logger.for(name) call after that uses your provider; the framework's internal logs do too. Use Logger.resetProvider() to revert to the console default (useful in tests).
import { Logger } from '@forinda/kickjs'
import { MyProvider } from './my-provider'
Logger.setProvider(new MyProvider())
// ... bootstrap() etc.Recipe: Pino
pnpm add pino pino-prettyimport pino from 'pino'
import { Logger, type LoggerProvider } from '@forinda/kickjs'
const root = pino({
level: process.env.LOG_LEVEL ?? 'info',
...(process.env.NODE_ENV !== 'production' && {
transport: {
target: 'pino-pretty',
options: { colorize: true, translateTime: 'SYS:HH:MM:ss.l', singleLine: true },
},
}),
})
class PinoProvider implements LoggerProvider {
constructor(private p: pino.Logger = root) {}
info(msg: string, ...args: any[]) {
this.p.info(msg, ...args)
}
warn(msg: string, ...args: any[]) {
this.p.warn(msg, ...args)
}
error(msg: string, ...args: any[]) {
this.p.error(msg, ...args)
}
debug(msg: string, ...args: any[]) {
this.p.debug(msg, ...args)
}
trace(msg: string, ...args: any[]) {
this.p.trace(msg, ...args)
}
fatal(msg: string, ...args: any[]) {
this.p.fatal(msg, ...args)
}
child({ component }: { component: string }) {
return new PinoProvider(this.p.child({ component }))
}
}
Logger.setProvider(new PinoProvider())If you bundle with Vite/esbuild for production, mark pino as external — its worker-thread transport resolves pino-pretty at runtime:
// vite.config.ts
export default defineConfig({
ssr: { external: ['pino', 'pino-pretty'] },
})Recipe: Winston
pnpm add winstonimport winston from 'winston'
import { Logger, type LoggerProvider } from '@forinda/kickjs'
const root = winston.createLogger({
level: process.env.LOG_LEVEL ?? 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.errors({ stack: true }),
winston.format.json(),
),
transports: [new winston.transports.Console()],
})
class WinstonProvider implements LoggerProvider {
constructor(private w: winston.Logger = root) {}
info(msg: string, ...args: any[]) {
this.w.info(msg, ...args)
}
warn(msg: string, ...args: any[]) {
this.w.warn(msg, ...args)
}
error(msg: string, ...args: any[]) {
this.w.error(msg, ...args)
}
debug(msg: string, ...args: any[]) {
this.w.debug(msg, ...args)
}
child({ component }: { component: string }) {
return new WinstonProvider(this.w.child({ component }))
}
}
Logger.setProvider(new WinstonProvider())Recipe: silent (tests, CLI scripts)
import { Logger, type LoggerProvider } from '@forinda/kickjs'
class SilentProvider implements LoggerProvider {
info() {}
warn() {}
error() {}
debug() {}
child() {
return this
}
}
Logger.setProvider(new SilentProvider())Injectable usage
Inside services, prefer the static factory or the @Autowired injection:
import { Service, Autowired, Logger } from '@forinda/kickjs'
@Service()
export class UserService {
@Autowired() private logger!: Logger
async create(input: CreateUserInput) {
this.logger.info('creating user', { email: input.email })
// ...
}
}@Autowired() private logger!: Logger resolves a per-class logger named after the enclosing class. Equivalent to Logger.for('UserService') but auto-named.
Component context
child() adds a component name. The default ConsoleLoggerProvider formats it as a [Name] prefix; pino, winston etc. attach it as a structured field. The contract is the same either way:
const root = Logger.for('OrderModule')
const child = root.child('PaymentService')
child.info('charged')
// Console default → "[PaymentService] charged"
// Pino → { component: 'PaymentService', msg: 'charged', ... }Why no first-party adapter packages?
We intentionally don't ship @forinda/kickjs-logger-pino or similar. Logger ecosystems move at their own pace, each has its own config surface, and the adapter glue is ~15 lines you can read at a glance. Owning the adapter in your own app means you control its version, its transports, its formatting — without waiting for a kickjs release when your logger of choice cuts a major.