For the complete documentation index, see llms.txt. This page is also available as Markdown.

Logging

See every exception the library swallows, as a log entry with the call site attached.

Waystone.Monads.Extensions.Logging — sends the exceptions the library catches to your ILogger.

What it adds

UseLoggerFactory and UseLoggerFactoryFrom on MonadOptions. Once one of them is called, every exception Option.Try, Result.Try and their async siblings swallow becomes a log entry with the call site attached.

When to reach for it

Reach for it when the library is catching exceptions you would otherwise never see. A Try that turns a throw into a None is doing its job, and it is also the one place a real bug can hide silently.

It is the only signal that needs a package. Counters and diagnostic events work with no install at all — see Observability for those.

Install it

dotnet add package Waystone.Monads.Extensions.Logging

Point it at your logger

Do this once, during start-up. Pick the line that matches how your application is built.

You have a host and a service provider:

var app = builder.Build();

MonadOptions.Configure(options => options.UseLoggerFactoryFrom(app.Services));

You have no container — a console app, a test, a worker you built by hand:

LoggerFactory.Create is not in Microsoft.Extensions.Logging.Abstractions, which is all this package brings with it. Add Microsoft.Extensions.Logging to build a factory yourself, plus a provider package such as Microsoft.Extensions.Logging.Console. An application with a host already has both.

You already hold a logger:

UseLoggerFactoryFrom asks the provider for an ILoggerFactory through IServiceProvider.GetService. It takes no dependency-injection package, so any container that can hand you a provider works — not just Microsoft's. If no factory is registered it throws and tells you to call UseLoggerFactory instead.

What you get in each entry

The exception itself, plus three properties describing the call site the compiler recorded for you:

Property
What it holds

MemberName

The member that called Try

ArgumentExpression

The source text of the delegate you passed

LineNumber

The line the Try call sits on

The exception goes in the logger's exception parameter, not into properties of its own. Your OpenTelemetry logging bridge reads exception.type, exception.message and exception.stacktrace off it, so you get those for free and you get them once.

Property names are PascalCase on purpose. Serilog only accepts property names matching [A-Za-z0-9_]+. Write {code.function.name} in a message template and Serilog prints it as text instead of binding it. So the log properties use PascalCase, and the dotted OpenTelemetry spellings stay on the metric tags, where nothing parses a template.

Choose the level

The default is Debug. A Try that hands back a None or an Err did what you asked it to, so warning about it fills your logs with noise.

Pass a different level if you disagree:

OpenTelemetry's semantic conventions suggest WARN for an exception the application expects to handle. We default to Debug instead. Pass LogLevel.Warning to follow the convention.

Filter the library's own output

UseLoggerFactory and UseLoggerFactoryFrom create a logger in the Waystone.Monads category, so you can turn the library up or down without touching anything else:

UseLogger does not do this — the logger you pass keeps whatever category it already had.

Change the logger for one block

Both the logger and the level live on the MonadOptions scope, so BeginScope redirects them for one asynchronous flow and leaves the rest of your process alone:

This is what makes the logging usable in tests that run in parallel.

Replacing UseExceptionLogger

MonadOptions.UseExceptionLogger used to be the only way to see these exceptions. It was obsolete from 6.7.0 and 7.0.0 removes it.

The old hook held one delegate. A second observer replaced the first without saying so, which meant it could never support more than one integration at a time. The diagnostic event has no such limit.

What it does not do

  • It does not log anything you handle yourself. Only the exceptions the library catches reach it.

  • It does not replace Observability. Counters and the diagnostic events are in the core package and need no install.

  • It does not accept more than one logger factory. The last call wins, and a scope overrides it for the block.

Last updated

Was this helpful?