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 | NoneThe Effect module has different log functions for each level:
Effect.log(equivalent toEffect.logInfo)Effect.logDebugEffect.logErrorEffect.logFatalEffect.logTraceEffect.logWarning
You can also use
Effect.logWithLeveldirectly and specifyLogLevelmanually.
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.logto 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 worldYou can add as many loggers as you want. A
Loggermight 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
