Logging with effect

We saw Console.log in the our "Effect Hello World", which we used to print something on the console.

While Console is mostly a debugging tool, in real apps you usually need a full logging system.

Difference between stdout printing and logging

Console implements a simple stdout printing solution. Under the hood it uses console.log to print messages on the console:

  • Limited control over output format
  • No metadata: No additional information about the data is recorded
  • Suitable for small applications with minimal logging requirements (mostly debugging)

For more complex applications we need a logging system:

  • Flexible formatting: Control over output format, levels, and content
  • Metadata: Records additional information like timestamps, log levels, and spans
  • Output not limited to console: files, telemetry, and more
  • Define multiple loggers sending log information to multiple sources
  • Suitable for medium to large-scale applications with complex logging requirements

Logging in effect with Logger

Instead of Console you can use Logger and Effect.log*:

import { Effect } from "effect";

const main = Console.log("Hello world"); 
const main = Effect.log("Hello world"); 

Effect.runSync(main);

If you run this you will quickly notice the difference:

> effect-getting-started-course@1.0.0 logging
> tsx src/+1/logging.ts

timestamp=2024-07-16T10:02:28.453Z level=INFO fiber=#0 message="Hello world"

Effect.log by default prints more information on the console:

  • timestamp: when the log is executed
  • level: LogLevel (by default INFO)
  • fiber: the id of the "process" that is running this effect
  • message: the message you provide

LogLevel

One benefit of using Effect.log is controlling when a message should be printed based on LogLevel:

export type LogLevel = All | Fatal | Error | Warning | Info | Debug | Trace | None

The Effect module has different log functions for each level:

  • Effect.log (equivalent to Effect.logInfo)
  • Effect.logDebug
  • Effect.logError
  • Effect.logFatal
  • Effect.logTrace
  • Effect.logWarning

You can also use Effect.logWithLevel directly and specify LogLevel manually.

You can then change the current LogLevel of any Effect by using Logger.withMinimumLogLevel:

import { Effect, Logger, LogLevel } from "effect";

const main = Effect.logInfo("Hello world");

Effect.runSync(Logger.withMinimumLogLevel(main, LogLevel.Error));

In this example no log will be printed, since Effect.logInfo does not pass the minimum check on LogLevel.Error.

Defining a custom Logger

Effect comes with 2 default loggers:

  • Logger that uses console.log to print logging information (as we saw in the example above)
  • Logger that sends the logs to the tracer spans as events

You can then define your custom logger as well using Logger.make:

const customLogger = Logger.make(({ logLevel, message }) => {
  // 👇 Define where/how to handle logging information
  console.log(`[${logLevel.label}] ${message}`);
});

You can then add customLogger to the list of loggers when running an effect:

import { Effect, Logger } from "effect";

const customLogger = Logger.make(({ logLevel, message }) => {
  console.log(`[${logLevel.label}] ${message}`);
});

const loggerLayer = Logger.add(customLogger);

const main = Effect.logInfo("Hello world").pipe(Effect.provide(loggerLayer));

Effect.runSync(main);

Logger.add creates a new Layer<never>. Providing this layer using Effect.provide adds our new logger when running the effect.

Since we added a new logger we will now see two printed messages:

> effect-getting-started-course@1.0.0 logging
> tsx src/+1/logging.ts

timestamp=2024-07-16T10:32:12.340Z level=INFO fiber=#0 message="Hello world"
[INFO] Hello world

You can add as many loggers as you want. A Logger might not output anything to the console, it might instead send information direct to file or database.

I leave you to explore more about logging with effect:

  • You can have more than one logger (on top of the 2 default loggers)
  • Loggers can access context to add metadata (Context/Config)
  • Loggers can abstract 3rd party logging services (e.g. sentry)
  • Loggers can be controlled using LogLevel