From 89b14ee69623e0d7c17c47d5408c981468dd7bbc Mon Sep 17 00:00:00 2001 From: Tim Date: Mon, 21 Oct 2024 13:55:48 +1300 Subject: [PATCH] use regional approach --- packages/effect/src/Inspectable.ts | 57 +++++++++++++++----- packages/effect/src/internal/cause.ts | 24 ++++----- packages/effect/src/internal/fiberRuntime.ts | 28 +++++----- packages/effect/src/internal/logger.ts | 53 +++++++++--------- packages/effect/src/internal/redacted.ts | 4 ++ packages/platform/src/Headers.ts | 21 ++++---- packages/platform/test/Headers.test.ts | 52 ++++++++++++++++-- 7 files changed, 157 insertions(+), 82 deletions(-) diff --git a/packages/effect/src/Inspectable.ts b/packages/effect/src/Inspectable.ts index aa18b535b3..9f92242469 100644 --- a/packages/effect/src/Inspectable.ts +++ b/packages/effect/src/Inspectable.ts @@ -2,7 +2,8 @@ * @since 2.0.0 */ -import type { FiberRefs } from "./index.js" +import type * as FiberRefs from "./FiberRefs.js" +import { globalValue } from "./GlobalValue.js" import { hasProperty, isFunction } from "./Predicate.js" /** @@ -39,7 +40,7 @@ export const toJSON = (x: unknown): unknown => { } else if (Array.isArray(x)) { return x.map(toJSON) } - return x + return redact(x) } /** @@ -89,14 +90,13 @@ export abstract class Class { */ export const toStringUnknown = ( u: unknown, - whitespace: number | string | undefined = 2, - context?: FiberRefs.FiberRefs + whitespace: number | string | undefined = 2 ): string => { if (typeof u === "string") { return u } try { - return typeof u === "object" ? stringifyCircular(u, whitespace, context) : String(u) + return typeof u === "object" ? stringifyCircular(u, whitespace) : String(u) } catch (_) { return String(u) } @@ -107,8 +107,7 @@ export const toStringUnknown = ( */ export const stringifyCircular = ( obj: unknown, - whitespace?: number | string | undefined, - context?: FiberRefs.FiberRefs + whitespace?: number | string | undefined ): string => { let cache: Array = [] const retVal = JSON.stringify( @@ -117,8 +116,8 @@ export const stringifyCircular = ( typeof value === "object" && value !== null ? cache.includes(value) ? undefined // circular reference - : cache.push(value) && (context && isRedactable(value) - ? value[RedactableId](context) + : cache.push(value) && (redactableState.fiberRefs !== undefined && isRedactable(value) + ? value[symbolRedactable](redactableState.fiberRefs) : value) : value, whitespace @@ -129,18 +128,50 @@ export const stringifyCircular = ( /** * @since 3.10.0 + * @category redactable */ export interface Redactable { - readonly [RedactableId]: (fiberRefs: FiberRefs.FiberRefs) => unknown + readonly [symbolRedactable]: (fiberRefs: FiberRefs.FiberRefs) => unknown } /** * @since 3.10.0 - * @category type ids + * @category redactable */ -export const RedactableId: unique symbol = Symbol.for("effect/Inspectable/Redactable") +export const symbolRedactable: unique symbol = Symbol.for("effect/Inspectable/Redactable") /** * @since 3.10.0 + * @category redactable */ -export const isRedactable = (u: unknown): u is Redactable => typeof u === "object" && u !== null && RedactableId in u +export const isRedactable = (u: unknown): u is Redactable => + typeof u === "object" && u !== null && symbolRedactable in u + +const redactableState = globalValue("effect/Inspectable/redactableState", () => ({ + fiberRefs: undefined as FiberRefs.FiberRefs | undefined +})) + +/** + * @since 3.10.0 + * @category redactable + */ +export const withRedactableContext = (context: FiberRefs.FiberRefs, f: () => A): A => { + const prev = redactableState.fiberRefs + redactableState.fiberRefs = context + try { + return f() + } finally { + redactableState.fiberRefs = prev + } +} + +/** + * @since 3.10.0 + * @category redactable + */ +export const redact = (u: unknown): unknown => { + if (isRedactable(u) && redactableState.fiberRefs !== undefined) { + return u[symbolRedactable](redactableState.fiberRefs) + } + return u +} diff --git a/packages/effect/src/internal/cause.ts b/packages/effect/src/internal/cause.ts index b1520f8e51..bb705011d4 100644 --- a/packages/effect/src/internal/cause.ts +++ b/packages/effect/src/internal/cause.ts @@ -4,12 +4,11 @@ import * as Chunk from "../Chunk.js" import * as Either from "../Either.js" import * as Equal from "../Equal.js" import type * as FiberId from "../FiberId.js" -import type * as FiberRefs from "../FiberRefs.js" import { constFalse, constTrue, dual, identity, pipe } from "../Function.js" import { globalValue } from "../GlobalValue.js" import * as Hash from "../Hash.js" import * as HashSet from "../HashSet.js" -import { NodeInspectSymbol, stringifyCircular, toJSON } from "../Inspectable.js" +import { NodeInspectSymbol, toJSON } from "../Inspectable.js" import * as Option from "../Option.js" import { pipeArguments } from "../Pipeable.js" import type { Predicate, Refinement } from "../Predicate.js" @@ -973,12 +972,11 @@ export const reduceWithContext = dual< /** @internal */ export const pretty = (cause: Cause.Cause, options?: { readonly renderErrorCause?: boolean | undefined - readonly context?: FiberRefs.FiberRefs }): string => { if (isInterruptedOnly(cause)) { return "All fibers interrupted without errors." } - return prettyErrors(cause, options?.context).map(function(e) { + return prettyErrors(cause).map(function(e) { if (options?.renderErrorCause !== true || e.cause === undefined) { return e.stack } @@ -1000,14 +998,14 @@ const renderErrorCause = (cause: PrettyError, prefix: string) => { class PrettyError extends globalThis.Error implements Cause.PrettyError { span: undefined | Span = undefined - constructor(originalError: unknown, context?: FiberRefs.FiberRefs) { + constructor(originalError: unknown) { const originalErrorIsObject = typeof originalError === "object" && originalError !== null const prevLimit = Error.stackTraceLimit Error.stackTraceLimit = 1 super( - prettyErrorMessage(originalError, context), + prettyErrorMessage(originalError), originalErrorIsObject && "cause" in originalError && typeof originalError.cause !== "undefined" - ? { cause: new PrettyError(originalError.cause, context) } + ? { cause: new PrettyError(originalError.cause) } : undefined ) if (this.message === "") { @@ -1044,12 +1042,12 @@ class PrettyError extends globalThis.Error implements Cause.PrettyError { * 1) If the input `u` is already a string, it's considered a message. * 2) If `u` is an Error instance with a message defined, it uses the message. * 3) If `u` has a user-defined `toString()` method, it uses that method. - * 4) Otherwise, it uses `Inspectable.stringifyCircular` to produce a string representation and uses it as the error message, + * 4) Otherwise, it uses `JSON.stringify` to produce a string representation and uses it as the error message, * with "Error" added as a prefix. * * @internal */ -export const prettyErrorMessage = (u: unknown, context?: FiberRefs.FiberRefs): string => { +export const prettyErrorMessage = (u: unknown): string => { // 1) if (typeof u === "string") { return u @@ -1072,7 +1070,7 @@ export const prettyErrorMessage = (u: unknown, context?: FiberRefs.FiberRefs): s // something's off, rollback to json } // 4) - return stringifyCircular(u, undefined, context) + return JSON.stringify(u) } const locationRegex = /\((.*)\)/ @@ -1127,14 +1125,14 @@ const prettyErrorStack = (message: string, stack: string, span?: Span | undefine const spanSymbol = Symbol.for("effect/SpanAnnotation") /** @internal */ -export const prettyErrors = (cause: Cause.Cause, context?: FiberRefs.FiberRefs): Array => +export const prettyErrors = (cause: Cause.Cause): Array => reduceWithContext(cause, void 0, { emptyCase: (): Array => [], dieCase: (_, unknownError) => { - return [new PrettyError(unknownError, context)] + return [new PrettyError(unknownError)] }, failCase: (_, error) => { - return [new PrettyError(error, context)] + return [new PrettyError(error)] }, interruptCase: () => [], parallelCase: (_, l, r) => [...l, ...r], diff --git a/packages/effect/src/internal/fiberRuntime.ts b/packages/effect/src/internal/fiberRuntime.ts index ae205e641a..984f78d09b 100644 --- a/packages/effect/src/internal/fiberRuntime.ts +++ b/packages/effect/src/internal/fiberRuntime.ts @@ -827,18 +827,20 @@ export class FiberRuntime extends Effectable.Class 0) { const clockService = Context.get(this.getFiberRef(defaultServices.currentServices), clock.clockTag) const date = new Date(clockService.unsafeCurrentTimeMillis()) - for (const logger of loggers) { - logger.log({ - fiberId: this.id(), - logLevel, - message, - cause, - context: contextMap, - spans, - annotations, - date - }) - } + Inspectable.withRedactableContext(contextMap, () => { + for (const logger of loggers) { + logger.log({ + fiberId: this.id(), + logLevel, + message, + cause, + context: contextMap, + spans, + annotations, + date + }) + } + }) } } @@ -1491,7 +1493,7 @@ export const tracerLogger = globalValue( attributes["effect.logLevel"] = logLevel.label if (cause !== null && cause._tag !== "Empty") { - attributes["effect.cause"] = internalCause.pretty(cause, { renderErrorCause: true, context }) + attributes["effect.cause"] = internalCause.pretty(cause, { renderErrorCause: true }) } span.value.event( diff --git a/packages/effect/src/internal/logger.ts b/packages/effect/src/internal/logger.ts index 2cd8a98123..f3cfdfeaa4 100644 --- a/packages/effect/src/internal/logger.ts +++ b/packages/effect/src/internal/logger.ts @@ -164,7 +164,7 @@ export const zipRight = dual< /** @internal */ export const stringLogger: Logger.Logger = makeLogger( - ({ annotations, cause, context, date, fiberId, logLevel, message, spans }) => { + ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() const outputArray = [ @@ -177,7 +177,7 @@ export const stringLogger: Logger.Logger = makeLogger( const messageArr = Arr.ensure(message) for (let i = 0; i < messageArr.length; i++) { - const stringMessage = Inspectable.toStringUnknown(messageArr[i], undefined, context) + const stringMessage = Inspectable.toStringUnknown(messageArr[i]) if (stringMessage.length > 0) { output = output + " message=" output = appendQuoted(stringMessage, output) @@ -186,7 +186,7 @@ export const stringLogger: Logger.Logger = makeLogger( if (cause != null && cause._tag !== "Empty") { output = output + " cause=" - output = appendQuoted(Cause.pretty(cause, { renderErrorCause: true, context }), output) + output = appendQuoted(Cause.pretty(cause, { renderErrorCause: true }), output) } if (List.isCons(spans)) { @@ -215,7 +215,7 @@ export const stringLogger: Logger.Logger = makeLogger( } output = output + filterKeyName(key) output = output + "=" - output = appendQuoted(Inspectable.toStringUnknown(value, undefined, context), output) + output = appendQuoted(Inspectable.toStringUnknown(value), output) } } @@ -234,7 +234,7 @@ const appendQuoted = (label: string, output: string): string => /** @internal */ export const logfmtLogger = makeLogger( - ({ annotations, cause, context, date, fiberId, logLevel, message, spans }) => { + ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const nowMillis = date.getTime() const outputArray = [ @@ -247,7 +247,7 @@ export const logfmtLogger = makeLogger( const messageArr = Arr.ensure(message) for (let i = 0; i < messageArr.length; i++) { - const stringMessage = Inspectable.toStringUnknown(messageArr[i], 0, context) + const stringMessage = Inspectable.toStringUnknown(messageArr[i], 0) if (stringMessage.length > 0) { output = output + " message=" output = appendQuotedLogfmt(stringMessage, output) @@ -256,7 +256,7 @@ export const logfmtLogger = makeLogger( if (cause != null && cause._tag !== "Empty") { output = output + " cause=" - output = appendQuotedLogfmt(Cause.pretty(cause, { renderErrorCause: true, context }), output) + output = appendQuotedLogfmt(Cause.pretty(cause, { renderErrorCause: true }), output) } if (List.isCons(spans)) { @@ -285,7 +285,7 @@ export const logfmtLogger = makeLogger( } output = output + filterKeyName(key) output = output + "=" - output = appendQuotedLogfmt(Inspectable.toStringUnknown(value, 0, context), output) + output = appendQuotedLogfmt(Inspectable.toStringUnknown(value, 0), output) } } @@ -303,14 +303,14 @@ export const structuredLogger = makeLogger readonly spans: Record }>( - ({ annotations, cause, context, date, fiberId, logLevel, message, spans }) => { + ({ annotations, cause, date, fiberId, logLevel, message, spans }) => { const now = date.getTime() const annotationsObj: Record = {} const spansObj: Record = {} if (HashMap.size(annotations) > 0) { for (const [k, v] of annotations) { - annotationsObj[k] = structuredMessage(v, context) + annotationsObj[k] = structuredMessage(v) } } @@ -322,12 +322,10 @@ export const structuredLogger = makeLogger structuredMessage(_, context)), + message: messageArr.length === 1 ? structuredMessage(messageArr[0]) : messageArr.map(structuredMessage), logLevel: logLevel.label, timestamp: date.toISOString(), - cause: Cause.isEmpty(cause) ? undefined : Cause.pretty(cause, { renderErrorCause: true, context }), + cause: Cause.isEmpty(cause) ? undefined : Cause.pretty(cause, { renderErrorCause: true }), annotations: annotationsObj, spans: spansObj, fiberId: _fiberId.threadName(fiberId) @@ -335,7 +333,7 @@ export const structuredLogger = makeLogger { +export const structuredMessage = (u: unknown): unknown => { switch (typeof u) { case "bigint": case "function": @@ -343,15 +341,13 @@ export const structuredMessage = (u: unknown, context: FiberRefs.FiberRefs): unk return String(u) } default: { - return Inspectable.isRedactable(u) ? u[Inspectable.RedactableId](context) : u + return Inspectable.toJSON(u) } } } /** @internal */ -export const jsonLogger = makeLogger((options) => - Inspectable.stringifyCircular(structuredLogger.log(options), undefined, options.context) -) +export const jsonLogger = map(structuredLogger, Inspectable.stringifyCircular) /** @internal */ const filterKeyName = (key: string) => key.replace(/[\s="]/g, "_") @@ -476,7 +472,7 @@ const prettyLoggerTty = (options: { firstLine += ":" let messageIndex = 0 if (message.length > 0) { - const firstMaybeString = structuredMessage(message[0], context) + const firstMaybeString = structuredMessage(message[0]) if (typeof firstMaybeString === "string") { firstLine += " " + color(firstMaybeString, colors.bold, colors.cyan) messageIndex++ @@ -487,18 +483,18 @@ const prettyLoggerTty = (options: { if (!processIsBun) console.group() if (!Cause.isEmpty(cause)) { - log(Cause.pretty(cause, { renderErrorCause: true, context })) + log(Cause.pretty(cause, { renderErrorCause: true })) } if (messageIndex < message.length) { for (; messageIndex < message.length; messageIndex++) { - log(message[messageIndex]) + log(Inspectable.redact(message[messageIndex])) } } if (HashMap.size(annotations) > 0) { for (const [key, value] of annotations) { - log(color(`${key}:`, colors.bold, colors.white), value) + log(color(`${key}:`, colors.bold, colors.white), Inspectable.redact(value)) } } @@ -539,7 +535,7 @@ const prettyLoggerBrowser = (options: { let messageIndex = 0 if (message.length > 0) { - const firstMaybeString = structuredMessage(message[0], context) + const firstMaybeString = structuredMessage(message[0]) if (typeof firstMaybeString === "string") { firstLine += ` ${color}${firstMaybeString}` if (options.colors) { @@ -552,21 +548,22 @@ const prettyLoggerBrowser = (options: { console.groupCollapsed(firstLine, ...firstParams) if (!Cause.isEmpty(cause)) { - console.error(Cause.pretty(cause, { renderErrorCause: true, context })) + console.error(Cause.pretty(cause, { renderErrorCause: true })) } if (messageIndex < message.length) { for (; messageIndex < message.length; messageIndex++) { - console.log(message[messageIndex]) + console.log(Inspectable.redact(message[messageIndex])) } } if (HashMap.size(annotations) > 0) { for (const [key, value] of annotations) { + const redacted = Inspectable.redact(value) if (options.colors) { - console.log(`%c${key}:`, "color:gray", value) + console.log(`%c${key}:`, "color:gray", redacted) } else { - console.log(`${key}:`, value) + console.log(`${key}:`, redacted) } } } diff --git a/packages/effect/src/internal/redacted.ts b/packages/effect/src/internal/redacted.ts index bf4365c183..0dedecd231 100644 --- a/packages/effect/src/internal/redacted.ts +++ b/packages/effect/src/internal/redacted.ts @@ -1,3 +1,4 @@ +import { NodeInspectSymbol } from "effect/Inspectable" import * as Equal from "../Equal.js" import { pipe } from "../Function.js" import { globalValue } from "../GlobalValue.js" @@ -34,6 +35,9 @@ export const proto = { toJSON() { return "" }, + [NodeInspectSymbol]() { + return "" + }, [Hash.symbol](this: Redacted.Redacted): number { return pipe( Hash.hash(RedactedSymbolKey), diff --git a/packages/platform/src/Headers.ts b/packages/platform/src/Headers.ts index 78656ad245..9fc745663b 100644 --- a/packages/platform/src/Headers.ts +++ b/packages/platform/src/Headers.ts @@ -3,10 +3,10 @@ */ import { FiberRefs } from "effect" import * as FiberRef from "effect/FiberRef" -import { dual, identity, pipe } from "effect/Function" +import { dual, identity } from "effect/Function" import { globalValue } from "effect/GlobalValue" -import { type Redactable, RedactableId } from "effect/Inspectable" -import * as Option from "effect/Option" +import { type Redactable, symbolRedactable } from "effect/Inspectable" +import type * as Option from "effect/Option" import * as Predicate from "effect/Predicate" import * as Record from "effect/Record" import * as Redacted from "effect/Redacted" @@ -43,13 +43,12 @@ export interface Headers extends Redactable { const Proto = Object.assign(Object.create(null), { [HeadersTypeId]: HeadersTypeId, - [RedactableId](fiberRefs: FiberRefs.FiberRefs): Record> { - const redactedNames = FiberRefs.get(fiberRefs, currentRedactedNames) - return pipe( - redactedNames, - Option.map((redactedNames) => redact(this as any, redactedNames)), - Option.getOrElse(() => this) - ) + [symbolRedactable]( + this: Headers, + fiberRefs: FiberRefs.FiberRefs + ): Record> { + const redactedNames = FiberRefs.getOrDefault(fiberRefs, currentRedactedNames) + return redact(this, redactedNames) } }) @@ -258,7 +257,7 @@ export const redact: { * @since 1.0.0 * @category fiber refs */ -export const currentRedactedNames = globalValue( +export const currentRedactedNames: FiberRef.FiberRef> = globalValue( "@effect/platform/Headers/currentRedactedNames", () => FiberRef.unsafeMake>([ diff --git a/packages/platform/test/Headers.test.ts b/packages/platform/test/Headers.test.ts index 12a46d1382..95abe753c6 100644 --- a/packages/platform/test/Headers.test.ts +++ b/packages/platform/test/Headers.test.ts @@ -1,7 +1,7 @@ import * as Headers from "@effect/platform/Headers" -import { FiberId, FiberRefs, Inspectable } from "effect" +import { assert, describe, it } from "@effect/vitest" +import { Effect, FiberId, FiberRef, FiberRefs, HashSet, Inspectable, Logger } from "effect" import * as Redacted from "effect/Redacted" -import { assert, describe, it } from "vitest" describe("Headers", () => { describe("Redactable", () => { @@ -20,7 +20,7 @@ describe("Headers", () => { ] as const ]) ) - const r = Inspectable.toStringUnknown(headers, undefined, fiberRefs) + const r = Inspectable.withRedactableContext(fiberRefs, () => Inspectable.toStringUnknown(headers)) const redacted = JSON.parse(r) assert.deepEqual(redacted, { @@ -45,7 +45,7 @@ describe("Headers", () => { ] as const ]) ) - const r = Inspectable.toStringUnknown({ headers }, undefined, fiberRefs) + const r = Inspectable.withRedactableContext(fiberRefs, () => Inspectable.toStringUnknown({ headers })) const redacted = JSON.parse(r) as { headers: unknown } assert.deepEqual(redacted.headers, { @@ -54,7 +54,51 @@ describe("Headers", () => { "x-api-key": "some-key" }) }) + + it.effect("logs redacted", () => + Effect.gen(function*() { + const messages: Array = [] + const logger = Logger.stringLogger.pipe( + Logger.map((msg) => { + messages.push(msg) + }) + ) + yield* FiberRef.update(FiberRef.currentLoggers, HashSet.add(logger)) + const headers = Headers.fromInput({ + "Content-Type": "application/json", + "Authorization": "Bearer some-token", + "X-Api-Key": "some-key" + }) + yield* Effect.log(headers).pipe( + Effect.annotateLogs({ headers }) + ) + assert.include(messages[0], "application/json") + assert.notInclude(messages[0], "some-token") + assert.notInclude(messages[0], "some-key") + })) + + it.effect("logs redacted structured", () => + Effect.gen(function*() { + const messages: Array = [] + const logger = Logger.structuredLogger.pipe( + Logger.map((msg) => { + messages.push(msg) + }) + ) + yield* FiberRef.update(FiberRef.currentLoggers, HashSet.add(logger)) + const headers = Headers.fromInput({ + "Content-Type": "application/json", + "Authorization": "Bearer some-token", + "X-Api-Key": "some-key" + }) + yield* Effect.log(headers).pipe( + Effect.annotateLogs({ headers }) + ) + assert.strictEqual(Redacted.isRedacted(messages[0].message.authorization), true) + assert.strictEqual(Redacted.isRedacted(messages[0].annotations.headers.authorization), true) + })) }) + describe("redact", () => { it("one key", () => { const headers = Headers.fromInput({