Claude-skill-registry effect-logging-discipline

Enforce Effect.log over console.log in TMNL. Effect-native logging provides structured output, dynamic log levels, annotations, spans, and observability integration.

install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/effect-logging-discipline" ~/.claude/skills/majiayu000-claude-skill-registry-effect-logging-discipline && rm -rf "$T"
manifest: skills/data/effect-logging-discipline/SKILL.md
source content

Effect Logging Discipline

CRITICAL RULE: No console.log

BANNED:

console.log
,
console.error
,
console.warn
,
console.debug

REQUIRED:

Effect.log
,
Effect.logError
,
Effect.logWarning
,
Effect.logDebug

Why Effect.log?

Featureconsole.logEffect.log
Dynamic log levels✅ Per-effect control
Structured output✅ timestamp, level, fiber
Custom destinations✅ File, service, etc.
Environment-based✅ Different levels per env
Annotations✅ Custom metadata
Spans✅ Duration tracking
Disable in tests❌ Pollutes output
LogLevel.None

Pattern 1: Basic Logging

Effect Context

import { Effect } from "effect"

// INFO level (default, always shown)
yield* Effect.log("Application started")
yield* Effect.log("Processing", "user", userId)

// Multiple messages
yield* Effect.log("message1", "message2", "message3")

Log Levels

// DEBUG - Hidden by default, enable with Logger.withMinimumLogLevel(LogLevel.Debug)
yield* Effect.logDebug("Verbose debug info")

// INFO - Default, always shown
yield* Effect.logInfo("Operation completed")
yield* Effect.log("Same as logInfo")

// WARN - For potential issues
yield* Effect.logWarning("Deprecated API called")

// ERROR - For failures
yield* Effect.logError("Request failed", cause)

// FATAL - Unrecoverable
yield* Effect.logFatal("System shutdown required")

Output Format

timestamp=2024-01-15T10:30:00.000Z level=INFO fiber=#0 message="Application started"

Pattern 2: Annotations (Structured Context)

Add metadata to all logs within a scope:

const program = Effect.gen(function* () {
  yield* Effect.log("Processing request")
  yield* Effect.log("Request complete")
}).pipe(
  Effect.annotateLogs("requestId", "req-123"),
  Effect.annotateLogs({ userId: "user-456", service: "auth" })
)

// Output:
// level=INFO message="Processing request" requestId=req-123 userId=user-456 service=auth
// level=INFO message="Request complete" requestId=req-123 userId=user-456 service=auth

Pattern 3: Spans (Duration Tracking)

Measure operation duration:

const operation = Effect.gen(function* () {
  yield* Effect.sleep("1 second")
  yield* Effect.log("The job is finished!")
}).pipe(
  Effect.withLogSpan("myOperation")
)

// Output:
// level=INFO message="The job is finished!" myOperation=1011ms

Pattern 4: Scoped Logging in Services

class SearchService extends Effect.Service<SearchService>()("app/SearchService", {
  effect: Effect.gen(function* () {
    const search = (query: string) =>
      Effect.gen(function* () {
        yield* Effect.logDebug(`Starting search for: ${query}`)
        const results = yield* performSearch(query)
        yield* Effect.log(`Found ${results.length} results`)
        return results
      }).pipe(
        Effect.annotateLogs("query", query),
        Effect.withLogSpan("SearchService.search")
      )

    return { search } as const
  }),
}) {}

Pattern 5: React Components (Non-Effect Context)

For React components that can't use Effect.gen, create helper functions:

Option A: Fire-and-forget logging (PREFERRED)

import { Effect } from "effect"

// Module-level helper
const logInfo = (message: string, ...args: unknown[]) =>
  Effect.runFork(Effect.log(message, ...args.map(String)))

const logDebug = (message: string, ...args: unknown[]) =>
  Effect.runFork(Effect.logDebug(message, ...args.map(String)))

const logError = (message: string, error?: unknown) =>
  Effect.runFork(Effect.logError(message, error ? String(error) : undefined))

// Usage in React
function MyComponent() {
  useEffect(() => {
    logInfo("[MyComponent] Mounted")
    return () => logInfo("[MyComponent] Unmounted")
  }, [])
}

Option B: Atom operation logging

import { runtimeAtom } from "./atoms"

// Log within atom operations where you have Effect context
export const ops = {
  doSomething: runtimeAtom.fn<string>()((input, ctx) =>
    Effect.gen(function* () {
      yield* Effect.logDebug(`Processing: ${input}`)
      // ... work
      yield* Effect.log("Complete")
    })
  ),
}

Pattern 6: Disable Logging in Tests

import { Logger, LogLevel } from "effect"

// Method 1: Per-effect
Effect.runFork(
  program.pipe(Logger.withMinimumLogLevel(LogLevel.None))
)

// Method 2: Via layer
const silentLayer = Logger.minimumLogLevel(LogLevel.None)
Effect.runFork(program.pipe(Effect.provide(silentLayer)))

// Method 3: In vitest setup
// vitest.setup.ts
import { Logger, LogLevel, Effect } from "effect"
beforeAll(() => {
  // Global silent logger for tests
})

Pattern 7: Enable Debug Logs

Debug logs are hidden by default:

import { Logger, LogLevel } from "effect"

// Enable for specific effect
const debuggedEffect = myEffect.pipe(
  Logger.withMinimumLogLevel(LogLevel.Debug)
)

// Enable via layer
const debugLayer = Logger.minimumLogLevel(LogLevel.Debug)

Migration Guide: console.log → Effect.log

Before (BANNED)

console.log(`[GenerativeContainer] MOUNT depth=${depth}`)
console.log(`[GenerativeContainer] prompt="${prompt?.substring(0, 60)}..."`)
console.error(`[GenerativeContainer] ERROR:`, err)

After (REQUIRED)

// If in Effect.gen context:
yield* Effect.log(`[GenerativeContainer] MOUNT depth=${depth}`)
yield* Effect.log(`[GenerativeContainer] prompt="${prompt?.substring(0, 60)}..."`)
yield* Effect.logError(`[GenerativeContainer] ERROR: ${err}`)

// If in React callback (fire-and-forget):
Effect.runFork(Effect.log(`[GenerativeContainer] MOUNT depth=${depth}`))
Effect.runFork(Effect.logError(`[GenerativeContainer] ERROR: ${err}`))

// Better: with annotations
Effect.runFork(
  Effect.log("MOUNT").pipe(
    Effect.annotateLogs({ component: "GenerativeContainer", depth })
  )
)

Anti-Patterns (BANNED)

1. Raw console calls

// BANNED
console.log("Debug:", value)
console.error("Error:", err)
console.warn("Warning")
console.debug("Trace")

2. Logging outside Effect without runFork

// WRONG - Effect.log returns Effect, doesn't execute
Effect.log("This does nothing")

// CORRECT - Fire and forget
Effect.runFork(Effect.log("This executes"))

3. Mixing console and Effect.log

// BANNED - Inconsistent
yield* Effect.log("Step 1")
console.log("Step 2")  // NO!
yield* Effect.log("Step 3")

Canonical Examples

PatternFile
Service logging
src/lib/data-manager/v1/DataManager.ts
Span usage
src/lib/slider/v1/services/SliderService.ts
Annotation usage
src/lib/geoint/services/SearchService.ts

Checklist: Logging Review

  • No
    console.log
    calls
  • No
    console.error
    calls
  • No
    console.warn
    calls
  • No
    console.debug
    calls
  • Effect.log for INFO level
  • Effect.logDebug for verbose debugging
  • Effect.logError for errors with context
  • Annotations for structured metadata
  • Spans for operation timing
  • Fire-and-forget wrapper for React callbacks

Related Skills

  • effect-patterns — General Effect-TS patterns
  • effect-service-authoring — Service logging patterns
  • tmnl-debug-instrumentation — Debug tooling