Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 | 9x 9x 9x 9x 17x 17x 15x 15x 15x 2x 4x 11x 2x 3x 20x 3x 17x 17x 3x 3x 1x 1x 2x 2x 11x 15x 15x 2x 2x 2x 2x | import { resolve } from '@/core/resolve'
import { Time } from '@/utils/Time'
import type { LogContext } from '@/types/logging/LogContext'
import type { LoggingSettings } from '@/types/LoggingSettings'
import type { LogLevel } from '@/types/logging/LogLevel'
/**
* Relative weight of each level, used to apply the threshold.
*
* @remarks
* `LogLevel` is declared in [`types/`](../types/index.ts) and imported as a type
* only. Keeping the runtime value here — rather than an enum shared with the
* configuration layer — is what stops `Logger → resolve → Defaults → Logger`
* from becoming a real import cycle.
*/
const WEIGHT: Record<LogLevel, number> = {
debug: 10,
info: 20,
warn: 30,
error: 40
}
/**
* @class
*
* Structured, level-classified logging — and nothing else.
*
* Each line is one JSON object emitted through the matching `console.*` method,
* so the runtime's log collector records the native level and keeps the fields
* searchable. Nothing here aggregates — that is {@link TelemetrySink}, a
* separate capability, because a sink samples at volume and is the wrong place
* for lines that have to be read back verbatim.
*
* @remarks
* The threshold comes from the surface registered by {@link configure}, and
* `LOG_LEVEL` in the environment overrides it per deployment, so staging can run
* at `debug` without a code change.
*
* Retention is the runtime's business, not this class's — it writes lines and
* stops there. Whether they are kept, and for how long, is a deployment setting
* wherever the process runs.
*
* @example
* ```ts
* const log = Logger.fromEnv(env, { service: 'ArticleService' })
* log.info('article created', { id })
* ```
*
* @author Bayu Dwiyan Satria
* @version 1.0.0
* @since 1.0.0
*/
export class Logger {
/**
* Minimum weight a line must carry to be emitted.
*/
private readonly threshold: number
/**
* Fields repeated on every line.
*/
private readonly context: LogContext
/**
* Constructs a Logger.
*
* @param threshold Minimum weight to emit.
* @param context Fields repeated on every line.
*/
private constructor(threshold: number, context: LogContext) {
this.threshold = threshold
this.context = context
}
/**
* Builds a logger for the current environment.
*
* @remarks
* The parameter is structural rather than a named environment type. All this
* needs is a possible `LOG_LEVEL`, so asking for exactly that keeps the kernel
* free of any platform's environment shape — a runtime's `Env` object, a
* `process.env`, or a bare object literal all satisfy it.
*
* @param env Anything carrying an optional `LOG_LEVEL`. Tolerates `null`.
* @param context Fields to repeat on every line.
* @returns A logger honouring the configured (or `LOG_LEVEL`) threshold.
*/
public static fromEnv(env: { LOG_LEVEL?: string } | null | undefined, context: LogContext = {}): Logger {
const settings = resolve<LoggingSettings>('logging')
const level = Logger.parse(env && env.LOG_LEVEL) || settings.level
return new Logger(WEIGHT[level], { service: settings.service, ...context })
}
/**
* Derives a logger with extra context, sharing this one's threshold.
*
* @param context Fields to add to every line.
* @returns The derived logger.
*/
public child(context: LogContext): Logger {
return new Logger(this.threshold, { ...this.context, ...context })
}
/**
* Emits a `debug` line.
*
* @param message What happened.
* @param data Structured detail.
*/
public debug(message: string, data?: Record<string, unknown>): void {
this.emit('debug', message, data)
}
/**
* Emits an `info` line.
*
* @param message What happened.
* @param data Structured detail.
*/
public info(message: string, data?: Record<string, unknown>): void {
this.emit('info', message, data)
}
/**
* Emits a `warn` line.
*
* @param message What happened.
* @param data Structured detail.
*/
public warn(message: string, data?: Record<string, unknown>): void {
this.emit('warn', message, data)
}
/**
* Emits an `error` line.
*
* @param message What happened.
* @param data Structured detail.
*/
public error(message: string, data?: Record<string, unknown>): void {
this.emit('error', message, data)
}
/**
* Writes one line through the `console` method matching its level.
*
* @param level Severity of the line.
* @param message What happened.
* @param data Structured detail.
*/
private emit(level: LogLevel, message: string, data?: Record<string, unknown>): void {
if (WEIGHT[level] < this.threshold) {
return
}
const line = JSON.stringify({
level,
message,
time: Time.nowIso(),
...this.context,
...(data ? { data: Logger.serialize(data) } : {})
})
switch (level) {
case 'error':
console.error(line)
break
case 'warn':
console.warn(line)
break
case 'debug':
console.debug(line)
break
default:
console.info(line)
}
}
/**
* Parses a level name, ignoring anything unrecognised.
*
* @param value The candidate level name.
* @returns The level, or `null` when the value is not one.
*/
private static parse(value?: string): LogLevel | null {
const level = (value || '').toLowerCase()
return (Object.keys(WEIGHT) as string[]).includes(level) ? (level as LogLevel) : null
}
/**
* Makes a data payload serialisable — `Error` values in particular, which
* `JSON.stringify` would otherwise flatten to `{}`.
*
* @param data The payload to convert.
* @returns The payload with errors expanded to message and stack.
*/
private static serialize(data: Record<string, unknown>): Record<string, unknown> {
const out: Record<string, unknown> = {}
for (const [key, value] of Object.entries(data)) {
out[key] = value instanceof Error ? { message: value.message, stack: value.stack } : value
}
return out
}
}
|