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
:
-
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.
-
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.
-
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.
-
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.
-
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"constprogram =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"constprogram =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"consttask1 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("2 seconds"))yield*_ (Effect .logDebug ("task1 done"))}).pipe (Logger .withMinimumLogLevel (LogLevel .Debug ))consttask2 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("1 seconds"))yield*_ (Effect .logDebug ("task2 done"))})constprogram =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"consttask1 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("2 seconds"))yield*_ (Effect .logDebug ("task1 done"))}).pipe (Logger .withMinimumLogLevel (LogLevel .Debug ))consttask2 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("1 seconds"))yield*_ (Effect .logDebug ("task2 done"))})constprogram =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"constprogram =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"constprogram =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"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logWarning (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .right }})Effect .runPromise (program )/*Output:... level=WARN fiber=#0 message="Oh uh!"*/
ts
import {Effect ,Either } from "effect"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logWarning (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .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"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logError (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .right }})Effect .runPromise (program )/*Output:... level=ERROR fiber=#0 message="Oh uh!"*/
ts
import {Effect ,Either } from "effect"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logError (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .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"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logFatal (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .right }})Effect .runPromise (program )/*Output:... level=FATAL fiber=#0 message="Oh uh!"*/
ts
import {Effect ,Either } from "effect"consttask =Effect .fail ("Oh uh!").pipe (Effect .as (2))constprogram =Effect .gen (function* (_ ) {constfailureOrSuccess = yield*_ (Effect .either (task ))if (Either .isLeft (failureOrSuccess )) {yield*_ (Effect .logFatal (failureOrSuccess .left ))return 0} else {returnfailureOrSuccess .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"constprogram =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"constprogram =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"constprogram =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 withMinimumLogLevelEffect .runPromise (program .pipe (Logger .withMinimumLogLevel (LogLevel .None )))/*Output:task done*/
ts
import {Effect ,Logger ,LogLevel } from "effect"constprogram =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 withMinimumLogLevelEffect .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"constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Executing task..."))yield*_ (Effect .sleep ("100 millis"))console .log ("task done")})constlayer =Logger .minimumLogLevel (LogLevel .None )// Logging disabled using a layerEffect .runPromise (program .pipe (Effect .provide (layer )))/*Output:task done*/
ts
import {Effect ,Logger ,LogLevel } from "effect"constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Executing task..."))yield*_ (Effect .sleep ("100 millis"))console .log ("task done")})constlayer =Logger .minimumLogLevel (LogLevel .None )// Logging disabled using a layerEffect .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"constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Executing task..."))yield*_ (Effect .sleep ("100 millis"))console .log ("task done")})constcustomRuntime =Effect .runSync (Effect .scoped (Layer .toRuntime (Logger .minimumLogLevel (LogLevel .None ))))// Logging disabled using a custom runtimeconstcustomRunPromise =Runtime .runPromise (customRuntime )customRunPromise (program )/*Output:task done*/
ts
import {Effect ,Logger ,LogLevel ,Runtime ,Layer } from "effect"constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("Executing task..."))yield*_ (Effect .sleep ("100 millis"))console .log ("task done")})constcustomRuntime =Effect .runSync (Effect .scoped (Layer .toRuntime (Logger .minimumLogLevel (LogLevel .None ))))// Logging disabled using a custom runtimeconstcustomRunPromise =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 logsconstprogram =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 layerconstLogLevelLive =Config .logLevel ("LOG_LEVEL").pipe (Effect .map ((level ) =>Logger .minimumLogLevel (level )),Layer .unwrapEffect )// Configure the program with the loaded log levelconstconfigured =Effect .provide (program ,LogLevelLive )// Test the configured program using ConfigProvider.fromMapconsttest =Effect .provide (configured ,Layer .setConfigProvider (ConfigProvider .fromMap (newMap ([["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 logsconstprogram =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 layerconstLogLevelLive =Config .logLevel ("LOG_LEVEL").pipe (Effect .map ((level ) =>Logger .minimumLogLevel (level )),Layer .unwrapEffect )// Configure the program with the loaded log levelconstconfigured =Effect .provide (program ,LogLevelLive )// Test the configured program using ConfigProvider.fromMapconsttest =Effect .provide (configured ,Layer .setConfigProvider (ConfigProvider .fromMap (newMap ([["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:
ts
import {Logger } from "effect"export constlogger =Logger .make (({logLevel ,message }) => {globalThis .console .log (`[${logLevel .label }] ${message }`)})
ts
import {Logger } from "effect"export constlogger =Logger .make (({logLevel ,message }) => {globalThis .console .log (`[${logLevel .label }] ${message }`)})
Assuming we have defined the following program:
ts
import {Effect } from "effect"consttask1 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("2 seconds"))yield*_ (Effect .logDebug ("task1 done"))})consttask2 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("1 seconds"))yield*_ (Effect .logDebug ("task2 done"))})export constprogram =Effect .gen (function* (_ ) {yield*_ (Effect .log ("start"))yield*_ (task1 )yield*_ (task2 )yield*_ (Effect .log ("done"))})
ts
import {Effect } from "effect"consttask1 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("2 seconds"))yield*_ (Effect .logDebug ("task1 done"))})consttask2 =Effect .gen (function* (_ ) {yield*_ (Effect .sleep ("1 seconds"))yield*_ (Effect .logDebug ("task2 done"))})export constprogram =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:
ts
import {Effect ,Logger ,LogLevel } from "effect"import * asCustomLogger from "./CustomLogger"import {program } from "./program"constlayer =Logger .replace (Logger .defaultLogger ,CustomLogger .logger )Effect .runPromise (Effect .provide (Logger .withMinimumLogLevel (program ,LogLevel .Debug ),layer ))
ts
import {Effect ,Logger ,LogLevel } from "effect"import * asCustomLogger from "./CustomLogger"import {program } from "./program"constlayer =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