diff --git a/.changeset/nervous-cougars-taste.md b/.changeset/nervous-cougars-taste.md new file mode 100644 index 0000000000..4e43454a50 --- /dev/null +++ b/.changeset/nervous-cougars-taste.md @@ -0,0 +1,15 @@ +--- +"effect": minor +--- + +add Logger.prettyLogger and Logger.pretty + +`Logger.pretty` is a new logger that leverages the features of the `console` APIs to provide a more visually appealing output. + +To try it out, provide it to your program: + +```ts +import { Effect, Logger } from "effect" + +Effect.log("Hello, World!").pipe(Effect.provide(Logger.pretty)) +``` diff --git a/.changeset/tame-zoos-vanish.md b/.changeset/tame-zoos-vanish.md new file mode 100644 index 0000000000..03f076062e --- /dev/null +++ b/.changeset/tame-zoos-vanish.md @@ -0,0 +1,5 @@ +--- +"effect": patch +--- + +fix types of UnsafeConsole.group diff --git a/packages/effect/src/Console.ts b/packages/effect/src/Console.ts index a0fa27a7aa..b6df353fec 100644 --- a/packages/effect/src/Console.ts +++ b/packages/effect/src/Console.ts @@ -63,10 +63,7 @@ export interface UnsafeConsole { dir(item: any, options?: any): void dirxml(...args: ReadonlyArray): void error(...args: ReadonlyArray): void - group(options?: { - readonly label?: string | undefined - readonly collapsed?: boolean | undefined - }): void + group(label?: string | undefined): void groupEnd(): void info(...args: ReadonlyArray): void log(...args: ReadonlyArray): void diff --git a/packages/effect/src/Logger.ts b/packages/effect/src/Logger.ts index 3ccacd7b6b..8690529e22 100644 --- a/packages/effect/src/Logger.ts +++ b/packages/effect/src/Logger.ts @@ -362,6 +362,19 @@ export const logfmtLogger: Logger = internal.logfmtLogger */ export const stringLogger: Logger = internal.stringLogger +/** + * @since 3.5.0 + * @category constructors + */ +export const prettyLogger: ( + options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined + } +) => Logger = internal.prettyLogger + /** * @since 2.0.0 * @category constructors @@ -397,6 +410,12 @@ export const json: Layer.Layer = replace(fiberRuntime.defaultLogger, fibe */ export const logFmt: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.logFmtLogger) +/** + * @since 3.5.0 + * @category constructors + */ +export const pretty: Layer.Layer = replace(fiberRuntime.defaultLogger, fiberRuntime.prettyLogger) + /** * @since 2.0.0 * @category constructors diff --git a/packages/effect/src/internal/core-effect.ts b/packages/effect/src/internal/core-effect.ts index d63958e7e1..577191cd72 100644 --- a/packages/effect/src/internal/core-effect.ts +++ b/packages/effect/src/internal/core-effect.ts @@ -911,11 +911,6 @@ export const logWithLevel = (level?: LogLevel.LogLevel) => i-- } } - if (message.length === 0) { - message = "" as any - } else if (message.length === 1) { - message = message[0] - } if (cause === undefined) { cause = internalCause.empty } diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index 096fb9c54c..ace835a650 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -1421,6 +1421,12 @@ export const logFmtLogger: Logger = globalValue( () => loggerWithConsoleLog(internalLogger.logfmtLogger) ) +/** @internal */ +export const prettyLogger: Logger = globalValue( + Symbol.for("effect/Logger/prettyLogger"), + () => internalLogger.prettyLogger() +) + /** @internal */ export const structuredLogger: Logger = globalValue( Symbol.for("effect/Logger/structuredLogger"), diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index 21205c04d9..13b506ff85 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -1,3 +1,6 @@ +import * as Arr from "../Array.js" +import * as Context from "../Context.js" +import * as FiberRefs from "../FiberRefs.js" import type { LazyArg } from "../Function.js" import { constVoid, dual, pipe } from "../Function.js" import * as HashMap from "../HashMap.js" @@ -9,6 +12,8 @@ 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 defaultServices from "./defaultServices.js" +import { consoleTag } from "./defaultServices/console.js" import * as _fiberId from "./fiberId.js" /** @internal */ @@ -157,7 +162,7 @@ export const zipRight = dual< >(2, (self, that) => map(zip(self, that), (tuple) => tuple[1])) /** @internal */ -export const stringLogger: Logger.Logger = makeLogger( +export const stringLogger: Logger.Logger = makeLogger( ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() @@ -169,16 +174,9 @@ export const stringLogger: Logger.Logger = makeLogger 0) { - output = output + " message=" - output = appendQuoted(stringMessage, output) - } - } - } else { - const stringMessage = Inspectable.toStringUnknown(message) + const messageArr = Arr.ensure(message) + for (let i = 0; i < messageArr.length; i++) { + const stringMessage = Inspectable.toStringUnknown(messageArr[i]) if (stringMessage.length > 0) { output = output + " message=" output = appendQuoted(stringMessage, output) @@ -204,7 +202,7 @@ export const stringLogger: Logger.Logger = makeLogger 0) { + if (HashMap.size(annotations) > 0) { output = output + " " let first = true @@ -246,16 +244,9 @@ export const logfmtLogger = makeLogger( let output = outputArray.join(" ") - if (Array.isArray(message)) { - for (let i = 0; i < message.length; i++) { - const stringMessage = Inspectable.toStringUnknown(message[i], 0) - if (stringMessage.length > 0) { - output = output + " message=" - output = appendQuotedLogfmt(stringMessage, output) - } - } - } else { - const stringMessage = Inspectable.toStringUnknown(message, 0) + const messageArr = Arr.ensure(message) + for (let i = 0; i < messageArr.length; i++) { + const stringMessage = Inspectable.toStringUnknown(messageArr[i], 0) if (stringMessage.length > 0) { output = output + " message=" output = appendQuotedLogfmt(stringMessage, output) @@ -281,7 +272,7 @@ export const logfmtLogger = makeLogger( } } - if (pipe(annotations, HashMap.size) > 0) { + if (HashMap.size(annotations) > 0) { output = output + " " let first = true @@ -328,8 +319,9 @@ export const structuredLogger = makeLogger (self: LogSpan.LogSpan): string => export const isLogger = (u: unknown): u is Logger.Logger => { return typeof u === "object" && u != null && LoggerTypeId in u } + +const processStdoutIsTTY = typeof process === "object" && "stdout" in process && process.stdout.isTTY === true +const hasWindow = typeof window === "object" + +const withColor = (text: string, ...colors: ReadonlyArray) => { + let out = "" + for (let i = 0; i < colors.length; i++) { + out += `\x1b[${colors[i]}m` + } + return out + text + "\x1b[0m" +} +const withColorNoop = (text: string, ..._colors: ReadonlyArray) => text +const colors = { + bold: "1", + red: "31", + green: "32", + yellow: "33", + blue: "34", + cyan: "36", + white: "37", + gray: "90", + black: "30", + bgBrightRed: "101" +} as const + +const logLevelColors: Record> = { + None: [], + All: [], + Trace: [colors.gray], + Debug: [colors.blue], + Info: [colors.green], + Warning: [colors.yellow], + Error: [colors.red], + Fatal: [colors.bgBrightRed, colors.black] +} + +const defaultDateFormat = (date: Date): string => + `${date.getHours().toString().padStart(2, "0")}:${date.getMinutes().toString().padStart(2, "0")}:${ + date.getSeconds().toString().padStart(2, "0") + }.${date.getMilliseconds().toString().padStart(3, "0")}` + +/** @internal */ +export const prettyLogger = (options?: { + readonly colors?: "auto" | boolean | undefined + readonly stderr?: boolean | undefined + readonly formatDate?: ((date: Date) => string) | undefined + readonly mode?: "browser" | "tty" | "auto" | undefined +}) => { + const mode_ = options?.mode ?? "auto" + const mode = mode_ === "auto" ? (hasWindow ? "browser" : "tty") : mode_ + const isBrowser = mode === "browser" + const showColors = typeof options?.colors === "boolean" ? options.colors : processStdoutIsTTY || isBrowser + const color = showColors ? withColor : withColorNoop + const formatDate = options?.formatDate ?? defaultDateFormat + + return makeLogger( + ({ annotations, cause, context, date, fiberId, logLevel, message: message_, spans }) => { + const services = FiberRefs.getOrDefault(context, defaultServices.currentServices) + const console = Context.get(services, consoleTag).unsafe + const log = options?.stderr === true ? console.error : console.log + + const message = Arr.ensure(message_) + + let firstLine = color(`[${formatDate(date)}]`, colors.white) + + ` ${color(logLevel.label, ...logLevelColors[logLevel._tag])}` + + ` (${_fiberId.threadName(fiberId)})` + + if (List.isCons(spans)) { + const now = date.getTime() + const render = renderLogSpanLogfmt(now) + for (const span of spans) { + firstLine += " " + render(span) + } + } + + firstLine += ":" + let messageIndex = 0 + if (message.length > 0) { + const firstMaybeString = structuredMessage(message[0]) + if (typeof firstMaybeString === "string") { + firstLine += " " + color(firstMaybeString, colors.bold, colors.cyan) + messageIndex++ + } + } + + if (isBrowser) { + console.group(firstLine) + } else { + log(firstLine) + console.group() + } + let currentMessage = "" + const params: Array = [] + + if (!Cause.isEmpty(cause)) { + const errors = Cause.prettyErrors(cause) + for (let i = 0; i < errors.length; i++) { + if (isBrowser) { + console.error(errors[i].stack) + } else { + currentMessage += "\n%s" + params.push(errors[i].stack) + } + } + } + + if (messageIndex < message.length) { + for (; messageIndex < message.length; messageIndex++) { + currentMessage += "\n%O" + params.push(message[messageIndex]) + } + } + + if (HashMap.size(annotations) > 0) { + for (const [key, value] of annotations) { + currentMessage += "\n" + color(`${key}:`, colors.bold, colors.white) + " %O" + params.push(value) + } + } + + if (currentMessage.length > 0) { + log(currentMessage.slice(1), ...params) + } + console.groupEnd() + } + ) +} diff --git a/packages/effect/test/Effect/scope-ref.test.ts b/packages/effect/test/Effect/scope-ref.test.ts index 4e677566c1..8bc6d44b53 100644 --- a/packages/effect/test/Effect/scope-ref.test.ts +++ b/packages/effect/test/Effect/scope-ref.test.ts @@ -46,8 +46,8 @@ describe("Effect", () => { ) assert.deepStrictEqual(messages, [ - "1 | acquire | A > INNER > OUTER > EXTERN", - "1 | release | R > INNER > OUTER > EXTERN" + ["1 | acquire | A > INNER > OUTER > EXTERN"], + ["1 | release | R > INNER > OUTER > EXTERN"] ]) })) }) diff --git a/packages/platform/test/HttpClient.test.ts b/packages/platform/test/HttpClient.test.ts index 77a9bab4e6..8dc0f03a9f 100644 --- a/packages/platform/test/HttpClient.test.ts +++ b/packages/platform/test/HttpClient.test.ts @@ -150,7 +150,7 @@ describe("HttpClient", () => { Effect.scoped ) - expect(logs).toEqual(["hello", "world"]) + expect(logs).toEqual([["hello"], ["world"]]) }).pipe(Effect.provide(HttpClient.layer), Effect.runPromise)) it("ClientRequest parses URL instances", () => {