544 lines
16 KiB
TypeScript
544 lines
16 KiB
TypeScript
/**
|
|
* Reports Effect failures to external code.
|
|
*
|
|
* An `ErrorReporter` receives `Cause` values from `Effect.withErrorReporting`,
|
|
* manual `report` calls, or built-in reporting boundaries. It forwards each
|
|
* non-interruption error to a callback, so applications can send failures to
|
|
* logging, monitoring, or error-tracking systems. This module also includes
|
|
* layers for installing reporters and symbols for marking errors as ignored or
|
|
* attaching severity and attributes.
|
|
*
|
|
* @since 4.0.0
|
|
*/
|
|
import type * as Cause from "./Cause.ts"
|
|
import type * as Context from "./Context.ts"
|
|
import * as Effect from "./Effect.ts"
|
|
import type * as Fiber from "./Fiber.ts"
|
|
import * as effect from "./internal/effect.ts"
|
|
import * as references from "./internal/references.ts"
|
|
import * as Layer from "./Layer.ts"
|
|
import * as LogLevel from "./LogLevel.ts"
|
|
import type { Severity } from "./LogLevel.ts"
|
|
import type { ReadonlyRecord } from "./Record.ts"
|
|
import type * as Scope from "./Scope.ts"
|
|
|
|
/**
|
|
* String literal type used as the runtime type identifier for
|
|
* `ErrorReporter` values.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to refer to the runtime type identifier type in low-level integrations.
|
|
*
|
|
* @category type IDs
|
|
* @since 4.0.0
|
|
*/
|
|
export type TypeId = "~effect/ErrorReporter"
|
|
|
|
/**
|
|
* Runtime type identifier attached to `ErrorReporter` values.
|
|
*
|
|
* **Details**
|
|
*
|
|
* This marker is part of the runtime representation of `ErrorReporter`
|
|
* implementations. Most code should create reporters with `make` and register
|
|
* them with `layer`.
|
|
*
|
|
* @category type IDs
|
|
* @since 4.0.0
|
|
*/
|
|
export const TypeId: TypeId = "~effect/ErrorReporter"
|
|
|
|
/**
|
|
* An `ErrorReporter` receives reported failures and forwards them to an
|
|
* external system such as a logging service or error tracker.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use as the interface for custom reporters that forward reported Effect
|
|
* failures to logging, monitoring, or error-tracking systems.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Reporting is triggered by `Effect.withErrorReporting`,
|
|
* `ErrorReporter.report`, or built-in boundaries in the HTTP and RPC server
|
|
* modules. Use {@link make} to create a reporter; it handles deduplication
|
|
* and per-error annotation extraction automatically.
|
|
*
|
|
* @see {@link make} for creating an `ErrorReporter` from a callback
|
|
* @see {@link layer} for registering reporters in the environment
|
|
* @see {@link report} for manually reporting a `Cause`
|
|
* @see {@link Effect.withErrorReporting} for reporting failures from an effect
|
|
*
|
|
* @category models
|
|
* @since 4.0.0
|
|
*/
|
|
export interface ErrorReporter {
|
|
readonly [TypeId]: TypeId
|
|
report(options: {
|
|
readonly cause: Cause.Cause<unknown>
|
|
readonly fiber: Fiber.Fiber<unknown, unknown>
|
|
readonly timestamp: bigint
|
|
}): void
|
|
}
|
|
|
|
/**
|
|
* Creates an `ErrorReporter` from a callback.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to define how reported failures are forwarded to a logging, monitoring,
|
|
* or error-tracking backend.
|
|
*
|
|
* **Details**
|
|
*
|
|
* The returned reporter automatically deduplicates causes and individual
|
|
* errors (the same object is never reported twice), skips interruptions,
|
|
* and resolves the `ignore`, `severity`, and `attributes` annotations on
|
|
* each error before invoking your callback.
|
|
*
|
|
* **Example** (Forwarding errors to the console)
|
|
*
|
|
* ```ts
|
|
* import { ErrorReporter } from "effect"
|
|
*
|
|
* // Forward every failure to the console
|
|
* const consoleReporter = ErrorReporter.make(
|
|
* ({ error, severity, attributes }) => {
|
|
* console.error(`[${severity}]`, error.message, attributes)
|
|
* }
|
|
* )
|
|
* ```
|
|
*
|
|
* @see {@link layer} for registering reporters in the environment
|
|
* @see {@link report} for manually reporting a `Cause`
|
|
*
|
|
* @category constructors
|
|
* @since 4.0.0
|
|
*/
|
|
export const make = (
|
|
report: (options: {
|
|
readonly cause: Cause.Cause<unknown>
|
|
readonly error: Error
|
|
readonly attributes: ReadonlyRecord<string, unknown>
|
|
readonly severity: Severity
|
|
readonly fiber: Fiber.Fiber<unknown, unknown>
|
|
readonly timestamp: bigint
|
|
}) => void
|
|
): ErrorReporter => {
|
|
const reported = new WeakSet<Cause.Cause<unknown> | object>()
|
|
return {
|
|
[TypeId]: TypeId,
|
|
report(options) {
|
|
if (reported.has(options.cause)) return
|
|
reported.add(options.cause)
|
|
for (let i = 0; i < options.cause.reasons.length; i++) {
|
|
const reason = options.cause.reasons[i]
|
|
if (reason._tag === "Interrupt") continue
|
|
const original = reason._tag === "Fail" ? reason.error : reason.defect
|
|
const isObject = typeof original === "object" && original !== null
|
|
if (isObject) {
|
|
if (reported.has(original)) continue
|
|
reported.add(original)
|
|
}
|
|
if (isIgnored(original)) continue
|
|
const pretty = effect.causePrettyError(original as any, reason.annotations)
|
|
report({
|
|
...options,
|
|
error: pretty,
|
|
severity: isObject ? getSeverity(original) : "Info",
|
|
attributes: isObject ? getAttributes(original) : emptyAttributes
|
|
})
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Context reference that holds the set of active error reporters for the
|
|
* current fiber. Defaults to an empty set (no reporting).
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use when you need to read or replace the current set of error reporters
|
|
* directly.
|
|
*
|
|
* @category references
|
|
* @since 4.0.0
|
|
*/
|
|
export const CurrentErrorReporters: Context.Reference<ReadonlySet<ErrorReporter>> = references.CurrentErrorReporters
|
|
|
|
/**
|
|
* Creates a `Layer` that registers one or more `ErrorReporter`s.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to provide one or more error reporters to effects that perform error
|
|
* reporting.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Reporters can be plain `ErrorReporter` values or effectful
|
|
* `Effect<ErrorReporter>` values that are resolved when the layer is built. By
|
|
* default the provided reporters **replace** any previously registered
|
|
* reporters. Set `mergeWithExisting: true` to add them alongside existing ones.
|
|
*
|
|
* **Example** (Providing error reporters)
|
|
*
|
|
* ```ts
|
|
* import { Effect, ErrorReporter } from "effect"
|
|
*
|
|
* const consoleReporter = ErrorReporter.make(({ error, severity }) => {
|
|
* console.error(`[${severity}]`, error.message)
|
|
* })
|
|
*
|
|
* const metricsReporter = ErrorReporter.make(({ severity }) => {
|
|
* // increment an error counter by severity
|
|
* })
|
|
*
|
|
* // Replace all existing reporters
|
|
* const ReporterLive = ErrorReporter.layer([
|
|
* consoleReporter,
|
|
* metricsReporter
|
|
* ])
|
|
*
|
|
* // Add to existing reporters instead of replacing
|
|
* const ReporterMerged = ErrorReporter.layer(
|
|
* [metricsReporter],
|
|
* { mergeWithExisting: true }
|
|
* )
|
|
*
|
|
* const program = Effect.fail("boom").pipe(
|
|
* Effect.withErrorReporting,
|
|
* Effect.provide(ReporterLive)
|
|
* )
|
|
* ```
|
|
*
|
|
* @see {@link make} for creating an `ErrorReporter` from a callback
|
|
* @see {@link CurrentErrorReporters} for low-level access to the current reporters
|
|
*
|
|
* @category layers
|
|
* @since 4.0.0
|
|
*/
|
|
export const layer = <
|
|
const Reporters extends ReadonlyArray<ErrorReporter | Effect.Effect<ErrorReporter, any, any>>
|
|
>(
|
|
reporters: Reporters,
|
|
options?: { readonly mergeWithExisting?: boolean | undefined } | undefined
|
|
): Layer.Layer<
|
|
never,
|
|
Reporters extends readonly [] ? never : Effect.Error<Reporters[number]>,
|
|
Exclude<
|
|
Reporters extends readonly [] ? never : Effect.Services<Reporters[number]>,
|
|
Scope.Scope
|
|
>
|
|
> =>
|
|
Layer.effect(
|
|
CurrentErrorReporters,
|
|
Effect.withFiber(Effect.fnUntraced(function*(fiber) {
|
|
const currentReporters = new Set(
|
|
options?.mergeWithExisting === true ? fiber.getRef(references.CurrentErrorReporters) : []
|
|
)
|
|
for (const reporter of reporters) {
|
|
currentReporters.add(Effect.isEffect(reporter) ? yield* reporter : reporter)
|
|
}
|
|
return currentReporters
|
|
}))
|
|
)
|
|
|
|
/**
|
|
* Runs all registered error reporters on the current fiber for a `Cause`.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to report a failure for observability without failing the current fiber.
|
|
*
|
|
* **Example** (Reporting a cause manually)
|
|
*
|
|
* ```ts
|
|
* import { Cause, Effect, ErrorReporter } from "effect"
|
|
*
|
|
* // Log the cause for monitoring, then continue with a fallback
|
|
* const program = Effect.gen(function*() {
|
|
* const cause = Cause.fail("something went wrong")
|
|
* yield* ErrorReporter.report(cause)
|
|
* return "fallback value"
|
|
* })
|
|
* ```
|
|
*
|
|
* @category Reporting
|
|
* @since 4.0.0
|
|
*/
|
|
export const report = <E>(cause: Cause.Cause<E>): Effect.Effect<void> =>
|
|
Effect.withFiber((fiber) => {
|
|
effect.reportCauseUnsafe(fiber, cause)
|
|
return Effect.void
|
|
})
|
|
|
|
/**
|
|
* Interface that object errors can implement to control reporting behavior.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use as the annotation contract for object errors that customize how error
|
|
* reporting handles them.
|
|
*
|
|
* **Details**
|
|
*
|
|
* All three annotation properties are optional: `[ErrorReporter.ignore]`
|
|
* prevents reporting when set to `true`, `[ErrorReporter.severity]` overrides
|
|
* the default `"Info"` severity, and `[ErrorReporter.attributes]` adds extra
|
|
* key/value pairs forwarded to reporters. The global `Error` interface is
|
|
* augmented with `Reportable`, so these properties are available on `Error`
|
|
* instances at the type level.
|
|
*
|
|
* @see {@link ignore} for the runtime annotation key that suppresses reports
|
|
* @see {@link severity} for the runtime annotation key that overrides severity
|
|
* @see {@link attributes} for the runtime annotation key that attaches reporter
|
|
* metadata
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export interface Reportable {
|
|
readonly [ignore]?: boolean
|
|
readonly [severity]?: Severity
|
|
readonly [attributes]?: ReadonlyRecord<string, unknown>
|
|
}
|
|
|
|
declare global {
|
|
interface Error extends Reportable {}
|
|
}
|
|
|
|
/**
|
|
* Defines the string property key used to mark an object error as ignored by error
|
|
* reporting.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to type the property key that suppresses reporting for expected object
|
|
* errors.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Set this property to `true` on an error class or object error to prevent it
|
|
* from being forwarded to reporters. This is useful for expected failures such
|
|
* as HTTP 404 responses.
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export type ignore = "~effect/ErrorReporter/ignore"
|
|
|
|
/**
|
|
* Defines the runtime property key used to mark an object error as ignored by error
|
|
* reporting.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to suppress reporting for expected object errors, such as HTTP 404
|
|
* responses.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Set `error[ErrorReporter.ignore]` to `true` to prevent the error from being
|
|
* forwarded to reporters. This is useful for expected failures such as HTTP 404
|
|
* responses.
|
|
*
|
|
* **Example** (Marking errors as ignored)
|
|
*
|
|
* ```ts
|
|
* import { Data, ErrorReporter } from "effect"
|
|
*
|
|
* class NotFoundError extends Data.TaggedError("NotFoundError")<{}> {
|
|
* readonly [ErrorReporter.ignore] = true
|
|
* }
|
|
* ```
|
|
*
|
|
* @see {@link isIgnored} for checking whether a value carries this annotation
|
|
* @see {@link Reportable} for the annotation contract recognized on object
|
|
* errors
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const ignore: ignore = "~effect/ErrorReporter/ignore"
|
|
|
|
/**
|
|
* Returns `true` if the given value has the `ErrorReporter.ignore` annotation
|
|
* set to `true`.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to check whether an error value is annotated to be skipped before
|
|
* forwarding it to error reporting code.
|
|
*
|
|
* @see {@link ignore} for the annotation key this predicate reads
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const isIgnored = (u: unknown): boolean =>
|
|
typeof u === "object" && u !== null && ignore in u && u[ignore] === true
|
|
|
|
/**
|
|
* Defines the string property key used to override the severity level of an object error.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to type the property key that overrides the reporting severity for object
|
|
* errors.
|
|
*
|
|
* **Details**
|
|
*
|
|
* When set to a valid `LogLevel.Severity`, the reporter callback receives this
|
|
* value as `severity`. Missing or invalid values fall back to `"Info"`.
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export type severity = "~effect/ErrorReporter/severity"
|
|
|
|
/**
|
|
* Defines the runtime property key used to override the severity level of an object error.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to annotate object errors with the severity reporter callbacks should
|
|
* receive.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Set `error[ErrorReporter.severity]` to a valid `LogLevel.Severity` value.
|
|
* Missing or invalid values fall back to `"Info"`.
|
|
*
|
|
* **Example** (Setting error severity annotations)
|
|
*
|
|
* ```ts
|
|
* import { Data, ErrorReporter } from "effect"
|
|
*
|
|
* class DeprecationWarning extends Data.TaggedError("DeprecationWarning")<{}> {
|
|
* readonly [ErrorReporter.severity] = "Warn" as const
|
|
* }
|
|
* ```
|
|
*
|
|
* @see {@link getSeverity} for reading the severity stored under this key
|
|
* @see {@link Reportable} for the annotation contract recognized on object
|
|
* errors
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const severity: severity = "~effect/ErrorReporter/severity"
|
|
|
|
/**
|
|
* Reads the `ErrorReporter.severity` annotation from an error object,
|
|
* falling back to `"Info"` when the annotation is unset or invalid.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to inspect the severity that reporter callbacks will receive for an
|
|
* object error.
|
|
*
|
|
* @see {@link severity} for the annotation key used to override severity
|
|
* @see {@link Reportable} for the annotation properties recognized on object errors
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const getSeverity = (error: object): Severity => {
|
|
if (severity in error && LogLevel.values.includes(error[severity] as Severity)) {
|
|
return error[severity] as Severity
|
|
}
|
|
return "Info"
|
|
}
|
|
|
|
/**
|
|
* Defines the string property key used to attach extra key/value metadata to an object
|
|
* error report.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to type the property key that attaches metadata to object error reports.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Reporters receive these attributes alongside the error, making it easy to
|
|
* include contextual information such as user IDs, request IDs, or other
|
|
* domain-specific debugging data.
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export type attributes = "~effect/ErrorReporter/attributes"
|
|
|
|
/**
|
|
* Defines the runtime property key used to attach extra key/value metadata to an object
|
|
* error report.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to attach domain metadata to object errors so reporter callbacks receive
|
|
* it with the reported failure.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Set `error[ErrorReporter.attributes]` to a record of metadata that should be
|
|
* forwarded to reporters alongside the error.
|
|
*
|
|
* **Example** (Setting error attributes)
|
|
*
|
|
* ```ts
|
|
* import { Data, ErrorReporter } from "effect"
|
|
*
|
|
* class PaymentError extends Data.TaggedError("PaymentError")<{
|
|
* readonly orderId: string
|
|
* }> {
|
|
* readonly [ErrorReporter.attributes] = {
|
|
* orderId: this.orderId
|
|
* }
|
|
* }
|
|
* ```
|
|
*
|
|
* @see {@link ignore} for suppressing reports for expected object errors
|
|
* @see {@link severity} for overriding reporter severity
|
|
* @see {@link getAttributes} for reading the metadata stored under this key
|
|
* @see {@link Reportable} for the annotation contract recognized on object
|
|
* errors
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const attributes: attributes = "~effect/ErrorReporter/attributes"
|
|
|
|
/**
|
|
* Reads the `ErrorReporter.attributes` annotation from an error object,
|
|
* returning an empty record when unset.
|
|
*
|
|
* **When to use**
|
|
*
|
|
* Use to inspect the attributes that reporter callbacks will receive for an
|
|
* object error.
|
|
*
|
|
* **Details**
|
|
*
|
|
* Returns the value stored under `ErrorReporter.attributes`, or the module's
|
|
* shared empty record when the annotation is absent.
|
|
*
|
|
* **Gotchas**
|
|
*
|
|
* The annotation value is returned as-is; this helper does not validate or
|
|
* clone it.
|
|
*
|
|
* @see {@link attributes} for the annotation key used to attach metadata
|
|
* @see {@link Reportable} for the annotation properties recognized on object errors
|
|
*
|
|
* @category annotations
|
|
* @since 4.0.0
|
|
*/
|
|
export const getAttributes = (error: object): ReadonlyRecord<string, unknown> => {
|
|
return attributes in error ? error[attributes] as any : emptyAttributes
|
|
}
|
|
|
|
const emptyAttributes: ReadonlyRecord<string, unknown> = {}
|