Skip to content

arashi01/zio-scribe

Repository files navigation

zio-scribe

zio-scribe attempts to provide an idiomatic, high‑performance integration between ZIO and Scribe, enabling the use of Scribe as the underlying logging backend for ZIO’s standard logging façade (ZIO.log, ZIO.logInfo, …). It implements a native ZLogger that faithfully propagates ZIO context (fibre ID, spans, annotations, and causes) into Scribe’s structured data model, and adds fibre‑safe, effect‑scoped configuration via FiberRef.

Table of Contents

  1. Introduction
  2. Installation
  3. Usage
  1. Comparison with Alternatives
  2. License

Introduction

Rationale

Whilst Scribe can be used directly inside ZIO effects, doing so bypasses ZIO’s built‑in logging infrastructure. In practice this:

  • Loses ZIO‑specific context (fibre IDs, spans, annotations, and structured Cause data),
  • Encourages thread‑local context (e.g. MDC) that does not align with ZIO’s fibre scheduler,
  • Fragments configuration in concurrent, nested scopes.

zio-scribe addresses these by ZIO ZLogger integration. It translates ZIO log events into Scribe LogRecords, preserves Scribe’s hierarchical logger naming based on source site, and introduces a fibre‑safe mechanism to apply Scribe LogFeatures within the dynamic scope of an effect.

Goals

  • ZIO conformance: act as a native ZLogger backend.
  • Context propagation: encode fibre ID, spans, annotations, and the Cause into Scribe’s LogRecord.data and messages.
  • Fibre‑local configuration: provide effect‑scoped, leak‑free tuning using Scribe LogFeatures via a FiberRef.
  • Performance: add minimal additional runtime overhead.

Artefacts

sbt:

libraryDependencies += "io.github.arashi01" %% "zio-scribe" % "<version>" // JVM
libraryDependencies += "io.github.arashi01" %%% "zio-scribe" % "<version>" // JVM/JS/Native (cross-platform)

zio-scribe depends on:

  • ZIO 2.1.x (runtime façade)
  • Scribe 3.x (backend)

Usage

Basic Setup

Install Scribe as the ZIO backend in bootstrap using the provided layer. Two principal modes exist:

  • Replace (default): remove ZIO's default loggers and install Scribe (ScribeLogging.live).
  • Additive: keep defaults and add Scribe (ScribeLogging.additive).
import zio.*
import zio.scribe.ScribeLogging

object MyApp extends ZIOAppDefault:
  // Replace default loggers with Scribe (drop ZIO defaults)
  override val bootstrap = ScribeLogging.live

  def run: ZIO[Any, Nothing, Unit] =
    ZIO.logInfo("Hello, Scribe via ZIO") *>
      ZIO.withSpan("db.fetch") {
        ZIO.logAnnotate("requestId", "req-123") {
          ZIO.logDebug("Fetching records")
        }
      }

Notes:

  • Replace variant removes ZIO defaults; additive keeps them. Both are fully scoped (loggers revert after layer scope).
  • Logger name derives from call‑site class (via Trace), preserving Scribe's hierarchical model.

Programmatic Bootstrap (Simple → Complex)

zio-scribe uses a purely programmatic bootstrap model: you build a ScribeBootstrapConfig in code and apply it with ScribeLogging.withConfig (or withConfigAdditive). No key / HOCON bootstrap exists — this keeps the library agnostic of your external configuration schema. (ZIO Config is still used for mapping ZIO context keys; see below.)

1. Minimal (root only, inferred handler)

import zio.*
import zio.scribe.*
import scribe.*

val bootstrapLayer = ScribeLogging.withConfig(
  ScribeBootstrapConfig(
    ScribeBootstrapConfig.Root(
      level = Level.Info,
      formatter = ScribeBootstrapConfig.FormatterName.Default,
      handlers = Nil // empty => a single default handler is synthesised (Default formatter, min root level)
    )
  )
)

object MinimalApp extends ZIOAppDefault:
  override val bootstrap = bootstrapLayer
  def run = ZIO.logInfo("hello")

2. Root with explicit handler + custom formatter

val layer = ScribeLogging.withConfig(
  ScribeBootstrapConfig(
    ScribeBootstrapConfig.Root(
      level = Level.Debug,
      formatter = ScribeBootstrapConfig.FormatterName.Colored,
      handlers = List(
        // Explicit handler spec (overrides root level + its own formatter)
        ScribeBootstrapConfig.HandlerSpec(
          minimumLevel = Some(Level.Warn),
          formatter = Some(ScribeBootstrapConfig.FormatterName.Simple)
        )
      )
    )
  )
)

3. Named logger level overrides

val cfg = ScribeBootstrapConfig(
  root = ScribeBootstrapConfig.Root(Level.Info, ScribeBootstrapConfig.FormatterName.Simple, Nil),
  loggers = Map(
    "com.acme.service" -> ScribeBootstrapConfig.Named(Level.Debug),
    "com.acme.service.internal" -> ScribeBootstrapConfig.Named(Level.Error)
  )
)
val layer = ScribeLogging.withConfig(cfg)

4. Convenience helpers for named levels

val layer = ScribeLogging.withNamedLevels(
  rootLevel = Level.Info,
  formatter = ScribeBootstrapConfig.FormatterName.Default,
  namedLevels = Map("svc" -> Level.Debug, "svc.dao" -> Level.Warn),
  handlers = Nil
)

5. Additive mode

val additiveLayer = ScribeLogging.withConfigAdditive(cfg) // keeps existing loggers

Global Configuration (Formatters / Handlers / Writers)

Use the Scribe service (via ScribeConfigurator) to configure Scribe functionally:

import scribe.handler.LogHandler
import scribe.format.*
import scribe.writer.*
import zio.*
import zio.scribe.*

val pretty = Formatter.simple
val console = Writer.console

val configureRoot =
  ScribeLogging.configured(_.clearHandlers().withHandler(LogHandler(pretty, console)))

// with named logger
val configureByName: ZIO[Scribe, Nothing, Unit] =
  configure("com.example")(_.clearHandlers().withMinimumLevel(Level.Info))

Because configuration mutates Scribe’s global registry, access is wrapped in ZIO effects for purity and composability.

Scoped (Fibre‑Local) Features

Apply Scribe LogFeatures within an effect’s dynamic scope using the fibre‑local featuresRef. Use the provided syntax extensions for convenience:

import scribe.{boost, data}
import zio.*
import zio.scribe.*

val program =
  ZIO.logInfo("outside") *>
    ZIO.logInfo("user action").withScribeData("userId" -> "u-42", "role" -> "admin") *>
    ZIO.logInfo("elevated").withScribeMinLevel(Level.Warn) *>
    ZIO.logInfo("custom boost").withScribeBoost(_ + 100.0)

Semantics:

  • Features are stored in ScribeContext.featuresRef: FiberRef[List[LogFeature]].
  • Fork: children inherit the parent's features unchanged.
  • Join: child's features are appended to parent's features on explicit join, enabling compositional logging patterns.
  • Scoping: features installed via locally/locallyScoped are automatically cleaned up when the scope ends (via acquire/release), independent of join behavior.
  • Each emitted LogRecord is transformed by the features in order before being written.

Alternative Composition Styles (Transforms & Layer)

In addition to the fluent extension methods you can compose feature scoping using reusable transformations or with a declarative layer:

import zio.*
import zio.scribe.*
import scribe.data

// Reusable polymorphic transformation (type parameters chosen at call‑site)
val enrich: [R, E, A] => ZIO[R, E, A] => ZIO[R, E, A] =
  ScribeTransforms.features(data("tenant", () => "acme"))

val run1 = enrich(ZIO.logInfo("hello"))

// Feature layer: installs features for any effect provided within its scope
val tenantLayer: ZLayer[Any, Nothing, ScribeFeaturesLayer.Applied] =
  ScribeFeaturesLayer(data("tenant", () => "acme"))

val run2 = ZIO.logInfo("hello inside layer").provide(tenantLayer)

Guidance:

  • ScribeTransforms.features and ScribeAspects.features are identical; pick whichever name you find more expressive. Both return a polymorphic [R,E,A] => ZIO[R,E,A] => ZIO[R,E,A].
  • ScribeFeaturesLayer uses an acquire/finalizer pattern to append features for the lifetime of the layer and then restore the previous list. It exports a marker service Applied so that your code can depend on it (preventing unused-layer warnings) if desired.
  • Prefer the layer when wiring environments or when you want scoping expressed alongside other layers; prefer the transformation for higher‑order utilities or ad‑hoc composition.

Fork/Join Context Preservation

When you explicitly fork and join fibers, logging context flows compositionally:

import zio.*
import zio.scribe.*
import scribe.data

// Context from child fiber merges into parent on join
val processTask = for {
  fiber <- ZIO.logInfo("Processing task")
             .withScribeData("taskId" -> "123", "priority" -> "high")
             .fork
  _     <- ZIO.logInfo("Parent continues work")
  _     <- fiber.join  // Child's context (taskId, priority) merges into parent
  _     <- ZIO.logInfo("After join - parent can observe child's context")
} yield ()

// Parallel context aggregation
val parallelWork = for {
  fibers <- ZIO.foreach(List("task-1", "task-2", "task-3")) { taskId =>
              ZIO.logInfo(s"Processing $taskId")
                .withScribeData("taskId" -> taskId)
                .fork
            }
  _      <- ZIO.foreach(fibers)(_.join)  // All task contexts accumulated
  _      <- ZIO.logInfo("All tasks complete with their contexts preserved")
} yield ()

Key distinction:

  • Scoped features (via withScribeData, locallyWith): automatically cleaned up when scope ends
  • Fork/join: child's final feature state merges with parent on explicit join

This enables distributed tracing, audit logging, and parallel context aggregation patterns while maintaining proper scoping guarantees.

ZIO Context Integration

What is propagated into Scribe:

  • Fibre identity — zio.fiberId recorded as the fibre’s thread name in the data map.
  • Spans — zio.spans is a compact rendering of nested LogSpan labels (e.g. http -> db).
  • Annotations — all ZIO annotations are included twice: individually with the prefix zio.annotation.<key> and as a single summary string under zio.annotations.summary.
  • Causes — if a Throwable is present in the Cause (defect or failure), it is logged as a TraceLoggableMessage preserving the stack trace; otherwise the raw Cause is attached to the data map as zio.cause for advanced formatters.

Performance considerations:

  • The underlying Scribe Logger is looked up by class and includes(level) is checked before any heavy work.
  • All record data values are lazy (() => Any), so they are evaluated only if handlers/formatters consume them.

Configuration (Mapping ZIO Context)

ScribeConfig controls how ZIO context elements map to Scribe record data keys. Defaults are sensible, but you can override via ZIO Config (HOCON / Typesafe Config / etc.) if desired. This is separate from bootstrap — only context key mapping remains externally configurable.

Expected configuration keys (under zio.scribe.context by default):

  • fiber-id (default: "zio.fiberId")
  • spans (default: "zio.spans")
  • annotations-summary (default: "zio.annotations.summary")
  • annotation-prefix (default: "zio.annotation.")
  • cause-key (default: "zio.cause")

Example HOCON:

zio.scribe.context {
  fiber-id = "fiber"
  spans = "spans"
  annotations-summary = "annotations"
  annotation-prefix = "ctx."
  cause-key = "cause"
}

Load it implicitly by using any of the ScribeLogging.* layers (they include ScribeConfig.live). To use an alternative root path, compose ScribeConfig.live("my.app.logging") >>> ScribeLogging.live (or another variant).

Dynamic Runtime Reconfiguration

After bootstrap you can adjust levels without rebuilding the entire layer:

import zio.scribe.*
import scribe.*

val program = for
  _ <- ScribeLogging.setRootLevel(Level.Error)               // raise root threshold
  _ <- ScribeLogging.reconfigureNamedLevels(Map(
         "com.acme.service" -> Level.Debug,                // more verbose for one module
         "com.acme.service.internal" -> Level.Error
       ))
  _ <- ZIO.logDebug("debug for service")
yield ()

These APIs mutate Scribe's registry (effectfully) while preserving ZIO purity boundaries.

Advanced Usage (Combining Programmatic + Runtime)

Combine programmatic bootstrap (handlers/formatters) with runtime level tuning and scoped features:

import zio.*
import zio.scribe.*

val configured =
  ScribeLogging.configured { root =>
    root
      .clearHandlers()
      .withHandler(LogHandler.default)
      .withMinimumLevel(Level.Debug)
  }

val app: ZIO[Scribe, Nothing, Unit] =
  for
    _ <- configure("com.acme.service")(_.withMinimumLevel(Level.Info))
    _ <- ZIO.logDebug("debug detail").withScribeData("requestId" -> "req-1")
  yield ()

Comparison with Alternatives

vs. zio-logging

zio-logging is a feature‑rich logging library for ZIO with multiple backends (console/file, SLF4J v1/v2, Java System Logger), bridges (SLF4J/JUL), metrics, and its own logger context based on typed LogAnnotations. It integrates with ZIO 2’s logging façade and can replace or augment default loggers.

zio-scribe is intentionally narrower: it focuses on providing a Scribe backend for ZIO’s façade, exposing Scribe’s formatters, handlers, writers, and features, while keeping context propagation fibre‑safe and minimal.

Notable ZIO‑specific features provided by zio-logging but not implemented in zio-scribe:

  • Logger context via typed LogAnnotation with aggregation semantics and helpers such as LogAnnotation.Name, CorrelationId, and MDC abstraction. zio-scribe forwards ZIO’s built‑in annotations map as plain strings into Scribe’s data map (with a configurable prefix) and a summary string; there is no typed aggregation facility.
  • Prebuilt console/file loggers and formats in the Logging module (e.g. consoleLogger, fileLogger) and the ability to compose loggers with contraMap. With zio-scribe, you configure Scribe directly (handlers, writers, formatters). There is no separate Logging algebra.
  • Bridges: SLF4J v1/v2 bridge modules, JUL bridge, JPL backend modules to capture non‑ZIO logs inside the ZIO logger. zio-scribe does not provide such bridges; if needed, rely on Scribe’s own ecosystem or configure bridges separately.
  • Metrics integration (e.g. counters on log levels). Not provided by zio-scribe out of the box; can be implemented in Scribe via custom LogHandlers.

Semantic differences:

  • Logger naming: zio-logging commonly uses LogAnnotation.Name to set the logical logger name via logger context. zio-scribe derives the Scribe logger name from the source Trace (class name). To tune routing/levels, configure Scribe loggers by package/class rather than injecting names via context.
  • Cause rendering: zio-logging formats Cause according to its own formats; zio-scribe prioritises a real Throwable (defect or failure) and logs it using Scribe’s throwable message, otherwise attaches the raw Cause object in the data map under a configurable key for advanced formatters.
  • Context propagation model: zio-logging offers MDC with fibre‑aware semantics. zio-scribe does not adapt Scribe’s MDC (thread‑local) to fibres; instead it provides an explicit fibre‑local featuresRef to install LogFeatures safely within effect scopes.

When to prefer zio-scribe:

  • You want Scribe’s performance, feature set (formatters, writers, handlers), and hierarchical logger model as the backend for ZIO logging.
  • You prefer minimal glue with ZIO’s façade and explicit, typed control of Scribe at the edges.

When to prefer zio-logging:

  • You need SLF4J/JUL bridges, prebuilt console/file loggers, JPL integrations, or typed LogAnnotation aggregation and MDC.
  • You want a ZIO‑first logging stack without adopting Scribe’s configuration model.

vs. Direct Scribe Usage

Using Scribe directly inside ZIO effects works, but:

  • You lose ZIO façade integration (ZIO.log*), which centralises level filtering, spans, and annotations.
  • ZIO context is not propagated: spans and annotations are absent, and MDC is thread‑local (unsuitable for fibres without extra handling).
  • Configuration becomes subtly unsafe in concurrent code if you rely on thread‑local context.

zio-scribe preserves the ZIO façade, propagates ZIO context into Scribe records, and offers an explicit, fibre‑safe mechanism to enrich logging within effect scopes. All bootstrap is code-first to avoid constraining your external configuration or deployment story.


Licence

This software is released and licensed under the terms of the MIT Licence.

About

No description, website, or topics provided.

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors