UNPKG

veffect

Version:

powerful TypeScript validation library built on the robust foundation of Effect combining exceptional type safety, high performance, and developer experience. Taking inspiration from Effect's functional principles, VEffect delivers a balanced approach tha

359 lines (309 loc) 10.5 kB
import type { LazyArg } from "../Function.js" import { constVoid, dual, pipe } from "../Function.js" import * as HashMap from "../HashMap.js" import * as Inspectable from "../Inspectable.js" import * as List from "../List.js" import type * as Logger from "../Logger.js" import type * as LogLevel from "../LogLevel.js" import * as LogSpan from "../LogSpan.js" import * as Option from "../Option.js" import { pipeArguments } from "../Pipeable.js" import * as Cause from "./cause.js" import * as _fiberId from "./fiberId.js" /** @internal */ const LoggerSymbolKey = "effect/Logger" /** @internal */ export const LoggerTypeId: Logger.LoggerTypeId = Symbol.for( LoggerSymbolKey ) as Logger.LoggerTypeId const loggerVariance = { /* c8 ignore next */ _Message: (_: unknown) => _, /* c8 ignore next */ _Output: (_: never) => _ } /** @internal */ export const makeLogger = <Message, Output>( log: (options: Logger.Logger.Options<Message>) => Output ): Logger.Logger<Message, Output> => ({ [LoggerTypeId]: loggerVariance, log, pipe() { return pipeArguments(this, arguments) } }) /** @internal */ export const mapInput = dual< <Message, Message2>( f: (message: Message2) => Message ) => <Output>(self: Logger.Logger<Message, Output>) => Logger.Logger<Message2, Output>, <Output, Message, Message2>( self: Logger.Logger<Message, Output>, f: (message: Message2) => Message ) => Logger.Logger<Message2, Output> >(2, (self, f) => makeLogger( (options) => self.log({ ...options, message: f(options.message) }) )) /** @internal */ export const mapInputOptions = dual< <Message, Message2>( f: (options: Logger.Logger.Options<Message2>) => Logger.Logger.Options<Message> ) => <Output>(self: Logger.Logger<Message, Output>) => Logger.Logger<Message2, Output>, <Output, Message, Message2>( self: Logger.Logger<Message, Output>, f: (options: Logger.Logger.Options<Message2>) => Logger.Logger.Options<Message> ) => Logger.Logger<Message2, Output> >(2, (self, f) => makeLogger((options) => self.log(f(options)))) /** @internal */ export const filterLogLevel = dual< ( f: (logLevel: LogLevel.LogLevel) => boolean ) => <Message, Output>(self: Logger.Logger<Message, Output>) => Logger.Logger<Message, Option.Option<Output>>, <Message, Output>( self: Logger.Logger<Message, Output>, f: (logLevel: LogLevel.LogLevel) => boolean ) => Logger.Logger<Message, Option.Option<Output>> >(2, (self, f) => makeLogger((options) => f(options.logLevel) ? Option.some(self.log(options)) : Option.none() )) /** @internal */ export const map = dual< <Output, Output2>( f: (output: Output) => Output2 ) => <Message>(self: Logger.Logger<Message, Output>) => Logger.Logger<Message, Output2>, <Message, Output, Output2>( self: Logger.Logger<Message, Output>, f: (output: Output) => Output2 ) => Logger.Logger<Message, Output2> >(2, (self, f) => makeLogger((options) => f(self.log(options)))) /** @internal */ export const none: Logger.Logger<unknown, void> = { [LoggerTypeId]: loggerVariance, log: constVoid, pipe() { return pipeArguments(this, arguments) } } as Logger.Logger<unknown, void> /** @internal */ export const simple = <A, B>(log: (a: A) => B): Logger.Logger<A, B> => ({ [LoggerTypeId]: loggerVariance, log: ({ message }) => log(message), pipe() { return pipeArguments(this, arguments) } }) /** @internal */ export const succeed = <A>(value: A): Logger.Logger<unknown, A> => { return simple(() => value) } /** @internal */ export const sync = <A>(evaluate: LazyArg<A>): Logger.Logger<unknown, A> => { return simple(evaluate) } /** @internal */ export const zip = dual< <Message2, Output2>( that: Logger.Logger<Message2, Output2> ) => <Message, Output>( self: Logger.Logger<Message, Output> ) => Logger.Logger<Message & Message2, [Output, Output2]>, <Message, Output, Message2, Output2>( self: Logger.Logger<Message, Output>, that: Logger.Logger<Message2, Output2> ) => Logger.Logger<Message & Message2, [Output, Output2]> >(2, (self, that) => makeLogger((options) => [self.log(options), that.log(options)])) /** @internal */ export const zipLeft = dual< <Message2, Output2>( that: Logger.Logger<Message2, Output2> ) => <Message, Output>( self: Logger.Logger<Message, Output> ) => Logger.Logger<Message & Message2, Output>, <Message, Output, Message2, Output2>( self: Logger.Logger<Message, Output>, that: Logger.Logger<Message2, Output2> ) => Logger.Logger<Message & Message2, Output> >(2, (self, that) => map(zip(self, that), (tuple) => tuple[0])) /** @internal */ export const zipRight = dual< <Message2, Output2>( that: Logger.Logger<Message2, Output2> ) => <Message, Output>( self: Logger.Logger<Message, Output> ) => Logger.Logger<Message & Message2, Output2>, <Message, Output, Message2, Output2>( self: Logger.Logger<Message, Output>, that: Logger.Logger<Message2, Output2> ) => Logger.Logger<Message & Message2, Output2> >(2, (self, that) => map(zip(self, that), (tuple) => tuple[1])) /** @internal */ export const stringLogger: Logger.Logger<unknown, string> = makeLogger<unknown, string>( ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() const outputArray = [ `timestamp=${date.toISOString()}`, `level=${logLevel.label}`, `fiber=${_fiberId.threadName(fiberId)}` ] let output = outputArray.join(" ") const stringMessage = Inspectable.toStringUnknown(message) if (stringMessage.length > 0) { output = output + " message=" output = appendQuoted(stringMessage, output) } if (cause != null && cause._tag !== "Empty") { output = output + " cause=" output = appendQuoted(Cause.pretty(cause), output) } if (List.isCons(spans)) { output = output + " " let first = true for (const span of spans) { if (first) { first = false } else { output = output + " " } output = output + pipe(span, LogSpan.render(nowMillis)) } } if (pipe(annotations, HashMap.size) > 0) { output = output + " " let first = true for (const [key, value] of annotations) { if (first) { first = false } else { output = output + " " } output = output + filterKeyName(key) output = output + "=" output = appendQuoted(Inspectable.toStringUnknown(value), output) } } return output } ) /** @internal */ const escapeDoubleQuotes = (str: string) => `"${str.replace(/\\([\s\S])|(")/g, "\\$1$2")}"` const textOnly = /^[^\s"=]+$/ /** @internal */ const appendQuoted = (label: string, output: string): string => output + (label.match(textOnly) ? label : escapeDoubleQuotes(label)) /** @internal */ export const logfmtLogger = makeLogger<unknown, string>( ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() const outputArray = [ `timestamp=${date.toISOString()}`, `level=${logLevel.label}`, `fiber=${_fiberId.threadName(fiberId)}` ] let output = outputArray.join(" ") const stringMessage = Inspectable.toStringUnknown(message, 0) if (stringMessage.length > 0) { output = output + " message=" output = appendQuotedLogfmt(stringMessage, output) } if (cause != null && cause._tag !== "Empty") { output = output + " cause=" output = appendQuotedLogfmt(Cause.pretty(cause), output) } if (List.isCons(spans)) { output = output + " " let first = true for (const span of spans) { if (first) { first = false } else { output = output + " " } output = output + pipe(span, renderLogSpanLogfmt(nowMillis)) } } if (pipe(annotations, HashMap.size) > 0) { output = output + " " let first = true for (const [key, value] of annotations) { if (first) { first = false } else { output = output + " " } output = output + filterKeyName(key) output = output + "=" output = appendQuotedLogfmt(Inspectable.toStringUnknown(value, 0), output) } } return output } ) /** @internal */ export const structuredLogger = makeLogger<unknown, { readonly logLevel: string readonly fiberId: string readonly timestamp: string readonly message: unknown readonly cause: string | undefined readonly annotations: Record<string, unknown> readonly spans: Record<string, number> }>( ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const now = date.getTime() const annotationsObj: Record<string, unknown> = {} const spansObj: Record<string, number> = {} if (HashMap.size(annotations) > 0) { for (const [k, v] of annotations) { annotationsObj[k] = structuredMessage(v) } } if (List.isCons(spans)) { for (const span of spans) { spansObj[span.label] = now - span.startTime } } return { message: structuredMessage(message), logLevel: logLevel.label, timestamp: date.toISOString(), cause: Cause.isEmpty(cause) ? undefined : Cause.pretty(cause), annotations: annotationsObj, spans: spansObj, fiberId: _fiberId.threadName(fiberId) } } ) export const structuredMessage = (u: unknown): unknown => { switch (typeof u) { case "bigint": case "function": case "symbol": { return String(u) } default: { return u } } } /** @internal */ export const jsonLogger = map(structuredLogger, Inspectable.stringifyCircular) /** @internal */ const filterKeyName = (key: string) => key.replace(/[\s="]/g, "_") /** @internal */ const escapeDoubleQuotesLogfmt = (str: string) => JSON.stringify(str) /** @internal */ const appendQuotedLogfmt = (label: string, output: string): string => output + (label.match(textOnly) ? label : escapeDoubleQuotesLogfmt(label)) /** @internal */ const renderLogSpanLogfmt = (now: number) => (self: LogSpan.LogSpan): string => { const label = filterKeyName(self.label) return `${label}=${now - self.startTime}ms` } /** @internal */ export const isLogger = (u: unknown): u is Logger.Logger<unknown, unknown> => { return typeof u === "object" && u != null && LoggerTypeId in u }