I've decoupled this situation before in two ways: Status changes and Log events.
First way is to create an IHaveStatus interface like such:
/// <summary>
/// Interface for objects that have a status message
/// that describes their current state.
/// </summary>
public interface IHaveStatus
{
/// <summary>
/// Occurs when the <seealso cref="Status"/> property has changed.
/// </summary>
event EventHandler<StatusChangedEventArgs> StatusChanged;
/// <summary>
/// The current status of the object. When this changes,
/// <seealso cref="StatusChanged"/> fires.
/// </summary>
string Status { get; }
}
As your object does stuff, you set your Status property. You can configure your property setter to fire the StatusChanged event when you set it. Whoever uses your objects can listen to your status changed event and log everything that happens.
Another version of this is to add a Log events to your objects.
public event EventHandler<LogEventArgs> Log;
The principal is pretty much the same, only your objects would be less chatty than a Status-driven log (you only fire the event when you specifically want to log something).
The idea is that its the responsibility of callers outside of your DAL to hook these events up to the proper log (hopefully set by using DI). Your DAL is oblivious to who or what is consuming these events, which separates these concerns well.