Logging

On this page

Logging is a crucial aspect of software development, especially when it comes to debugging and monitoring the behavior of your applications. In this section, we'll delve into Effect's logging utilities and explore their advantages over traditional methods like console.log.

Advantages Over Traditional Logging

Effect's logging utilities offer several advantages over traditional logging methods like console.log:

  1. Dynamic Log Level Control: With Effect's logging, you have the ability to change the log level dynamically. This means you can control which log messages get displayed based on their severity. For example, you can configure your application to log only warnings or errors, which can be extremely helpful in production environments to reduce noise.

  2. Custom Logging Output: Effect's logging utilities allow you to change how logs are handled. You can direct log messages to various destinations, such as a service or a file, using a custom logger. This flexibility ensures that logs are stored and processed in a way that best suits your application's requirements.

  3. Fine-Grained Logging: Effect enables fine-grained control over logging on a per-part basis of your program. You can set different log levels for different parts of your application, tailoring the level of detail to each specific component. This can be invaluable for debugging and troubleshooting, as you can focus on the information that matters most.

  4. Environment-Based Logging: Effect's logging utilities can be combined with deployment environments to achieve granular logging strategies. For instance, during development, you might choose to log everything at a trace level and above for detailed debugging. In contrast, your production version could be configured to log only errors or critical issues, minimizing the impact on performance and noise in production logs.

  5. Additional Features: Effect's logging utilities come with additional features such as the ability to measure time spans, alter log levels on a per-effect basis, and integrate spans for performance monitoring.

Now, let's dive into the specific logging utilities provided by Effect.

log

The log function is used to print a message at the current log level, which is INFO by default.

ts
import { Effect } from "effect"
 
const program = Effect.log("Application started")
 
Effect.runSync(program)
/*
Output:
timestamp=2023-07-05T09:14:53.275Z level=INFO fiber=#0 message="Application started"
*/
ts
import { Effect } from "effect"
 
const program = Effect.log("Application started")
 
Effect.runSync(program)
/*
Output:
timestamp=2023-07-05T09:14:53.275Z level=INFO fiber=#0 message="Application started"
*/

The log message contains the following information:

  • timestamp: The timestamp when the log message was generated.
  • level: The log level at which the message is logged.
  • fiber: The identifier of the fiber executing the program.
  • message: The content of the log message.
  • span: (Optional) The duration of the span in milliseconds.

Log Levels

logDebug

By default, DEBUG messages are not printed.

However, you can configure the default logger to enable them using Logger.withMinimumLogLevel and setting the minimum log level to LogLevel.Debug.

Here's an example that demonstrates how to enable DEBUG messages for a specific task (task1):


ts
import { Effect, Logger, LogLevel } from "effect"
 
const task1 = Effect.gen(function* (_) {
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.logDebug("task1 done"))
}).pipe(Logger.withMinimumLogLevel(LogLevel.Debug))
 
const task2 = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logDebug("task2 done"))
})
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("start"))
yield* _(task1)
yield* _(task2)
yield* _(Effect.log("done"))
})
 
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=DEBUG message="task1 done" <-- 2 seconds later
... level=INFO message=done <-- 1 second later
*/
ts
import { Effect, Logger, LogLevel } from "effect"
 
const task1 = Effect.gen(function* (_) {
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.logDebug("task1 done"))
}).pipe(Logger.withMinimumLogLevel(LogLevel.Debug))
 
const task2 = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logDebug("task2 done"))
})
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("start"))
yield* _(task1)
yield* _(task2)
yield* _(Effect.log("done"))
})
 
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=DEBUG message="task1 done" <-- 2 seconds later
... level=INFO message=done <-- 1 second later
*/

In the above example, we enable DEBUG messages specifically for task1 by using the Logger.withMinimumLogLevel function.

By using Logger.withMinimumLogLevel(effect, level), you have the flexibility to selectively enable different log levels for specific effects in your program. This allows you to control the level of detail in your logs and focus on the information that is most relevant to your debugging and troubleshooting needs.

logInfo

By default, INFO messages are printed.


ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.logInfo("start"))
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logInfo("done"))
})
 
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=INFO message=done <-- 3 seconds later
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.logInfo("start"))
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logInfo("done"))
})
 
Effect.runPromise(program)
/*
Output:
... level=INFO message=start
... level=INFO message=done <-- 3 seconds later
*/

In the above example, the Effect.log function is used to log an INFO message with the content "start" and "done". These messages will be printed during the execution of the program.

logWarning

By default, WARN messages are printed.


ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logWarning(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=WARN fiber=#0 message="Oh uh!"
*/
ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logWarning(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=WARN fiber=#0 message="Oh uh!"
*/

logError

By default, ERROR messages are printed.


ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logError(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=ERROR fiber=#0 message="Oh uh!"
*/
ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logError(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=ERROR fiber=#0 message="Oh uh!"
*/

logFatal

By default, FATAL messages are printed.


ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logFatal(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=FATAL fiber=#0 message="Oh uh!"
*/
ts
import { Effect, Either } from "effect"
 
const task = Effect.fail("Oh uh!").pipe(Effect.as(2))
 
const program = Effect.gen(function* (_) {
const failureOrSuccess = yield* _(Effect.either(task))
if (Either.isLeft(failureOrSuccess)) {
yield* _(Effect.logFatal(failureOrSuccess.left))
return 0
} else {
return failureOrSuccess.right
}
})
 
Effect.runPromise(program)
/*
Output:
... level=FATAL fiber=#0 message="Oh uh!"
*/

Spans

Effect also provides support for spans, allowing you to measure the duration of specific operations or tasks within your program.


ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.log("The job is finished!"))
}).pipe(Effect.withLogSpan("myspan"))
 
Effect.runPromise(program)
/*
Output:
... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
*/
ts
import { Effect } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.log("The job is finished!"))
}).pipe(Effect.withLogSpan("myspan"))
 
Effect.runPromise(program)
/*
Output:
... level=INFO fiber=#0 message="The job is finished!" myspan=1011ms
*/

In the above example, a span is created using the Effect.withLogSpan function. It measures the duration of the code block within the span and logs an INFO message with the content "The job is finished!" along with the span duration of 1011ms (myspan=1011ms).

Disabling Default Logging

If you ever find yourself needing to turn off default logging, perhaps during test execution, there are various ways to achieve this within the Effect framework. In this section, we'll explore different methods to disable default logging.

Using withMinimumLogLevel

Effect provides a convenient function called withMinimumLogLevel that allows you to set the minimum log level, effectively disabling logging:

ts
import { Effect, Logger, LogLevel } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
// Logging enabled (default)
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Executing task..."
task done
*/
 
// Logging disabled using withMinimumLogLevel
Effect.runPromise(program.pipe(Logger.withMinimumLogLevel(LogLevel.None)))
/*
Output:
task done
*/
ts
import { Effect, Logger, LogLevel } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
// Logging enabled (default)
Effect.runPromise(program)
/*
Output:
timestamp=... level=INFO fiber=#0 message="Executing task..."
task done
*/
 
// Logging disabled using withMinimumLogLevel
Effect.runPromise(program.pipe(Logger.withMinimumLogLevel(LogLevel.None)))
/*
Output:
task done
*/

By setting the log level to LogLevel.None, you effectively disable logging, and only the final result will be displayed.

Using a Layer

Another approach to disable logging is by creating a layer that sets the minimum log level to LogLevel.None, effectively turning off all logging:

ts
import { Effect, Logger, LogLevel } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
const layer = Logger.minimumLogLevel(LogLevel.None)
 
// Logging disabled using a layer
Effect.runPromise(program.pipe(Effect.provide(layer)))
/*
Output:
task done
*/
ts
import { Effect, Logger, LogLevel } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
const layer = Logger.minimumLogLevel(LogLevel.None)
 
// Logging disabled using a layer
Effect.runPromise(program.pipe(Effect.provide(layer)))
/*
Output:
task done
*/

Using a Custom Runtime

You can also disable logging by creating a custom runtime that includes the configuration to turn off logging:

ts
import { Effect, Logger, LogLevel, Runtime, Layer } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
const customRuntime = Effect.runSync(
Effect.scoped(Layer.toRuntime(Logger.minimumLogLevel(LogLevel.None)))
)
 
// Logging disabled using a custom runtime
const customRunPromise = Runtime.runPromise(customRuntime)
 
customRunPromise(program)
/*
Output:
task done
*/
ts
import { Effect, Logger, LogLevel, Runtime, Layer } from "effect"
 
const program = Effect.gen(function* (_) {
yield* _(Effect.log("Executing task..."))
yield* _(Effect.sleep("100 millis"))
console.log("task done")
})
 
const customRuntime = Effect.runSync(
Effect.scoped(Layer.toRuntime(Logger.minimumLogLevel(LogLevel.None)))
)
 
// Logging disabled using a custom runtime
const customRunPromise = Runtime.runPromise(customRuntime)
 
customRunPromise(program)
/*
Output:
task done
*/

In this approach, you create a custom runtime that incorporates the configuration to disable logging, and then you execute your program using this custom runtime.

Loading the Log Level from Configuration

To retrieve the log level from a configuration and incorporate it into your program, utilize the layer produced by Logger.minimumLogLevel:

ts
import {
Effect,
Config,
Logger,
Layer,
ConfigProvider,
LogLevel
} from "effect"
 
// Simulate a program with logs
const program = Effect.gen(function* (_) {
yield* _(Effect.logError("ERROR!"))
yield* _(Effect.logWarning("WARNING!"))
yield* _(Effect.logInfo("INFO!"))
yield* _(Effect.logDebug("DEBUG!"))
})
 
// Load the log level from the configuration as a layer
const LogLevelLive = Config.logLevel("LOG_LEVEL").pipe(
Effect.map((level) => Logger.minimumLogLevel(level)),
Layer.unwrapEffect
)
 
// Configure the program with the loaded log level
const configured = Effect.provide(program, LogLevelLive)
 
// Test the configured program using ConfigProvider.fromMap
const test = Effect.provide(
configured,
Layer.setConfigProvider(
ConfigProvider.fromMap(new Map([["LOG_LEVEL", LogLevel.Warning.label]]))
)
)
 
Effect.runPromise(test)
/*
Output:
... level=ERROR fiber=#0 message=ERROR!
... level=WARN fiber=#0 message=WARNING!
*/
ts
import {
Effect,
Config,
Logger,
Layer,
ConfigProvider,
LogLevel
} from "effect"
 
// Simulate a program with logs
const program = Effect.gen(function* (_) {
yield* _(Effect.logError("ERROR!"))
yield* _(Effect.logWarning("WARNING!"))
yield* _(Effect.logInfo("INFO!"))
yield* _(Effect.logDebug("DEBUG!"))
})
 
// Load the log level from the configuration as a layer
const LogLevelLive = Config.logLevel("LOG_LEVEL").pipe(
Effect.map((level) => Logger.minimumLogLevel(level)),
Layer.unwrapEffect
)
 
// Configure the program with the loaded log level
const configured = Effect.provide(program, LogLevelLive)
 
// Test the configured program using ConfigProvider.fromMap
const test = Effect.provide(
configured,
Layer.setConfigProvider(
ConfigProvider.fromMap(new Map([["LOG_LEVEL", LogLevel.Warning.label]]))
)
)
 
Effect.runPromise(test)
/*
Output:
... level=ERROR fiber=#0 message=ERROR!
... level=WARN fiber=#0 message=WARNING!
*/

To evaluate the configured program, you can utilize ConfigProvider.fromMap for testing (refer to Testing Services for more details).

Custom loggers

In this section, we will learn how to define a custom logger and set it as the default logger in our program.

First, let's define our custom logger:

CustomLogger.ts
ts
import { Logger } from "effect"
 
export const logger = Logger.make(({ logLevel, message }) => {
globalThis.console.log(`[${logLevel.label}] ${message}`)
})
CustomLogger.ts
ts
import { Logger } from "effect"
 
export const logger = Logger.make(({ logLevel, message }) => {
globalThis.console.log(`[${logLevel.label}] ${message}`)
})

Assuming we have defined the following program:


program.ts
ts
import { Effect } from "effect"
 
const task1 = Effect.gen(function* (_) {
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.logDebug("task1 done"))
})
 
const task2 = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logDebug("task2 done"))
})
 
export const program = Effect.gen(function* (_) {
yield* _(Effect.log("start"))
yield* _(task1)
yield* _(task2)
yield* _(Effect.log("done"))
})
program.ts
ts
import { Effect } from "effect"
 
const task1 = Effect.gen(function* (_) {
yield* _(Effect.sleep("2 seconds"))
yield* _(Effect.logDebug("task1 done"))
})
 
const task2 = Effect.gen(function* (_) {
yield* _(Effect.sleep("1 seconds"))
yield* _(Effect.logDebug("task2 done"))
})
 
export const program = Effect.gen(function* (_) {
yield* _(Effect.log("start"))
yield* _(task1)
yield* _(task2)
yield* _(Effect.log("done"))
})

To replace the default logger, we simply need to create a specific layer using Logger.replace and provide it to our program using Effect.provide before executing it:

index.ts
ts
import { Effect, Logger, LogLevel } from "effect"
import * as CustomLogger from "./CustomLogger"
import { program } from "./program"
 
const layer = Logger.replace(Logger.defaultLogger, CustomLogger.logger)
 
Effect.runPromise(
Effect.provide(Logger.withMinimumLogLevel(program, LogLevel.Debug), layer)
)
index.ts
ts
import { Effect, Logger, LogLevel } from "effect"
import * as CustomLogger from "./CustomLogger"
import { program } from "./program"
 
const layer = Logger.replace(Logger.defaultLogger, CustomLogger.logger)
 
Effect.runPromise(
Effect.provide(Logger.withMinimumLogLevel(program, LogLevel.Debug), layer)
)

This is what we see printed on the console after executing the program:

[INFO] start
[DEBUG] task1 done
[DEBUG] task2 done
[INFO] done
[INFO] start
[DEBUG] task1 done
[DEBUG] task2 done
[INFO] done