@just-be/evlog-effect
v0.1.0
Published
Effect v4 bindings for evlog wide events
Downloads
393
Readme
evlog-effect
Effect v4 (effect-smol) bindings for evlog wide events.
One evlog logger per unit of work, exposed as an Effect service. Context accumulates via set as the work proceeds; the wide event is emitted exactly once when the work completes — success, failure, or interruption — with error context extracted from the Effect Cause.
Verified against [email protected] and [email protected].
Quick start
import { Effect } from "effect"
import { Evlog, EvlogBridge, EvlogInit, WideEvent } from "@just-be/evlog-effect"
import type { DrainContext } from "evlog"
import { createAxiomDrain } from "evlog/axiom"
import { createDrainPipeline } from "evlog/pipeline"
// App-level: initLogger on startup, drain.flush() on shutdown
const drain = createDrainPipeline<DrainContext>({ batch: { size: 50 } })(createAxiomDrain())
const AppLive = EvlogInit.layer({
env: { service: "checkout", environment: "production" },
drain
})
const checkout = Effect.gen(function* () {
const log = yield* WideEvent
const user = yield* fetchUser(userId)
yield* log.set({ user: { id: user.id, plan: user.plan } })
const cart = yield* fetchCart(user)
yield* log.set({ cart: { items: cart.length, total: cart.total } })
const charge = yield* stripe.charge(cart.total)
yield* log.set({ stripe: { chargeId: charge.id } })
}).pipe(
Evlog.withWideEvent({ task: "checkout" })
// one wide event emitted here — on success, failure, or interrupt
)
Effect.runPromise(checkout.pipe(Effect.provide(AppLive)))API
Evlog (core)
withWideEvent(initialContext?)— wrap an effect as its own unit of work. Creates a fresh evlog logger, provides it as theWideEventservice, and emits viaEffect.onExitwhen the effect settles. The recommended entry point for jobs, scripts, and handlers.withRequestWideEvent(options?)— same, but uses evlog'screateRequestLoggerto pre-populatemethod/path/requestId. Pairs witheffect/unstable/httpmiddleware.layer(initialContext?)/layerRequest(options?)— layer forms; the event lives for the layer's scope and emits when it closes. Wrapped inLayer.fresh(see v4 notes).WideEvent— the service key (Context.Service). Shape:set,setLevel,info,warn,error,getContext,emit(escape hatch), andunsafe(the raw evlog logger, for audit/fork and sync interop).annotate(fields)— one-linerWideEvent.use((log) => log.set(fields)).acquire/fromLogger/finalize— lower-level building blocks if you need a custom lifecycle (e.g. a typed-fields service key of your own).
EvlogBridge
Folds Effect's built-in logging into the current wide event, so Effect.logInfo / Effect.logWarning / Effect.logError become entries on the single event instead of separate lines:
program.pipe(Effect.provide(EvlogBridge.layer()))- Inside a wide event:
Error/Fatal→log.error(...)(promotes the event level),Warn→log.warn, everything else →log.info. Log annotations and log spans ride along as fields. - Outside a wide event: falls back to Effect's default console logger (configurable via
fallback), so standalone logs still appear and nothing is double-emitted.
EvlogInit
layer(config?)— calls evlog's globalinitLogger(config)when the layer builds; ifconfig.drainexposesflush(evlog'screateDrainPipelineoutput does), registersdrain.flush()as a finalizer on the layer's scope so buffered events ship before shutdown.flush(drain)— standalone flush effect.
Failure semantics
When the unit of work fails, finalize walks the (v4 flat) Cause:
- The first failure/defect goes through
log.error(...)— evlog sets theerrorfield (name,message,stack) and promotes the event tolevel: "error". - Additional concurrent/sequential failures merge in under
error.additional(evlog's recursivesetmerge). - Pure interruption (no failures) is recorded as
outcome: "interrupted"atwarnlevel.
evlog seals the logger after emit, so a leaked fiber calling set after the event shipped produces an [evlog] console warning rather than silent data loss.
Why the design changed from a v3 sketch
Two Effect v4 changes shaped this binding:
Layers are memoized across
Effect.providecalls. In v3, providing a module-levelLayer.scoped(RequestLogger, ...)per request built a fresh logger each time. In v4 the sharedMemoMapwould silently reuse one wide event across every request. That's whywithWideEventis the primary API (plainonExit, no layer), and the layer forms are wrapped inLayer.fresh.Loggers receive the emitting fiber, and
fiber.contextis readable synchronously. The v3 bridge needed anEffect.runSynchack to reach the request logger from inside aLogger. In v4 the bridge is justContext.getOption(options.fiber.context, WideEvent)— fully synchronous, no runtime re-entry, and it naturally resolves to whichever wide event the current fiber is inside, even across forks.
Other v4 surface differences used here: Context.Service<Self, Shape>()("id") replaces Context.Tag, Layer.effect runs its effect in the layer scope (no separate Layer.scoped), LogLevel is a string union, and Cause is a flat array of Fail | Die | Interrupt reasons.
HTTP middleware sketch (effect/unstable/http)
import { Effect } from "effect"
import { HttpMiddleware, HttpServerRequest } from "effect/unstable/http"
import { Evlog } from "@just-be/evlog-effect"
export const evlogMiddleware = HttpMiddleware.make((app) =>
Effect.gen(function* () {
const request = yield* HttpServerRequest.HttpServerRequest
return yield* app.pipe(
Evlog.withRequestWideEvent({ method: request.method, path: request.url })
)
})
)(unstable/http may shift between betas — the combinator only needs to wrap the per-request effect, so it adapts to whatever the middleware signature looks like.)
Not covered (yet)
- Typed fields (
FieldContext<T>): the defaultWideEventservice isRecord-typed. For compile-time field safety, define your ownContext.Servicekey withWideEventServicespecialized to your event type and reuseacquire/finalize. log.fork: evlog's child-event forking is integration-attached; in Effect you'd typically model background work asEffect.forkDaemon(work.pipe(Evlog.withWideEvent({ operation, _parentRequestId })))instead.
Tests
bun run check # typecheck + runtime smoke tests
bun run build # emit dist/ for publishingThe smoke test exercises: success accumulation, typed-error failure capture, the Effect.log* bridge (including level promotion and annotations), the layer form, and drain flushing.
