diff options
Diffstat (limited to 'packages/core/src/effect/logger.ts')
| -rw-r--r-- | packages/core/src/effect/logger.ts | 73 |
1 files changed, 73 insertions, 0 deletions
diff --git a/packages/core/src/effect/logger.ts b/packages/core/src/effect/logger.ts new file mode 100644 index 000000000..69f9631e0 --- /dev/null +++ b/packages/core/src/effect/logger.ts @@ -0,0 +1,73 @@ +import { Cause, Effect, Logger, References } from "effect" +import * as Log from "../util/log" + +type Fields = Record<string, unknown> + +const normalizeKey = (key: string) => (key === "sessionID" ? "session.id" : key) + +export interface Handle { + readonly debug: (msg?: unknown, extra?: Fields) => Effect.Effect<void> + readonly info: (msg?: unknown, extra?: Fields) => Effect.Effect<void> + readonly warn: (msg?: unknown, extra?: Fields) => Effect.Effect<void> + readonly error: (msg?: unknown, extra?: Fields) => Effect.Effect<void> + readonly with: (extra: Fields) => Handle +} + +const clean = (input?: Fields): Fields => + Object.fromEntries( + Object.entries(input ?? {}) + .filter((entry) => entry[1] !== undefined && entry[1] !== null) + .map(([key, value]) => [normalizeKey(key), value]), + ) + +const text = (input: unknown): string => { + // oxlint-disable-next-line no-base-to-string + if (Array.isArray(input)) return input.map((item) => String(item)).join(" ") + // oxlint-disable-next-line no-base-to-string + return input === undefined ? "" : String(input) +} + +const call = (run: (msg?: unknown) => Effect.Effect<void>, base: Fields, msg?: unknown, extra?: Fields) => { + const ann = clean({ ...base, ...extra }) + const fx = run(msg) + return Object.keys(ann).length ? Effect.annotateLogs(fx, ann) : fx +} + +export const logger = Logger.make((opts) => { + const extra = clean(opts.fiber.getRef(References.CurrentLogAnnotations)) + const now = opts.date.getTime() + for (const [key, start] of opts.fiber.getRef(References.CurrentLogSpans)) { + extra[`logSpan.${key}`] = `${now - start}ms` + } + if (opts.cause.reasons.length > 0) { + extra.cause = Cause.pretty(opts.cause) + } + + const svc = typeof extra.service === "string" ? extra.service : undefined + if (svc) delete extra.service + const log = svc ? Log.create({ service: svc }) : Log.Default + const msg = text(opts.message) + + switch (opts.logLevel) { + case "Trace": + case "Debug": + return log.debug(msg, extra) + case "Warn": + return log.warn(msg, extra) + case "Error": + case "Fatal": + return log.error(msg, extra) + default: + return log.info(msg, extra) + } +}) + +export const layer = Logger.layer([logger], { mergeWithExisting: false }) + +export const create = (base: Fields = {}): Handle => ({ + debug: (msg, extra) => call((item) => Effect.logDebug(item), base, msg, extra), + info: (msg, extra) => call((item) => Effect.logInfo(item), base, msg, extra), + warn: (msg, extra) => call((item) => Effect.logWarning(item), base, msg, extra), + error: (msg, extra) => call((item) => Effect.logError(item), base, msg, extra), + with: (extra) => create({ ...base, ...extra }), +}) |
