Use structured logging. When using .NET / .NET5+ I recommend using the built-in logging providers. If you're using the .NET
full framework, the library I recommend is Serilog.
This guide is mostly paraphrasing the excellent writing of Nicholas Blumhardt as I wanted to have everything in a single place.
- Don't attach your debugger when developping, logging events should be sufficient. This will allow you to increase the quality of logging over time
- Use a logging provider that supports structured events
- If you're at least partially hosting in Azure, I recommend using the Application Insights provider
- Associate log events with a build version (I tend to use InformationalVersion as it doesn't have technical limitations). This comes in handy when tracing back an
Exception
to a specific version of the code. I also record additional metadata:Application Name
(i.e.CatsApi
,DogsWorker
...): great to restricts events to a single app when queryingEnvironment
:local
,uat
,prod
...Host
(i.e.RD00155DF084AD
): sometimes containers / VMs do go rogue
- Correlate events across system boundaries (via a
CorrelationId
for example)- That includes queue messages
- This comes out of the box with Application Insights
- Use centralized logging, all your systems should be logging in one place
- Prefer a service rather than something you're hosting yourself
- If you host yourself, don't use the same storage for your production data and your logs
- Don't repeat the same property over and over again in a unit of work. Instead use a log scope
// Don't:
_l.LogInformation("[{TransactionId}] - Processing...", transactionId, ...);
// Do:
using(_l.BeginScope(new Dictionary<string, object> {["TransactionId"] = transactionId}))
{
_l.LogInformation("Processing...", ...);
}
- Consider retention
- For audit purpose
- Typical support incident period
- Don't log beginning / end of functions / requests
- Don't use logging for performance timing
Fluent Style Guideline - good events use the names of properties as content within the message. This improves readability and makes events more compact.
_l.LogWarning("Disk quota {DiskQuota} MB exceeded by {UserId}", quota, userId);
Sentences vs. fragments - log event messages are fragments, not sentences; avoid a trailing period/full stop when possible.
Templates vs. messages - Log events have a message template associated, not a message. Treating the string parameter to log methods as a message, as in the case below, will hamper your querying abilities.
// Don't:
_l.LogInformation($"Deleted user {userId}");
Instead, always use template properties to include variables in messages:
// Do:
_l.LogInformation("Deleted user {UserId}", userId);
Property Naming - Property names should use PascalCase
. Avoid generic property names as they'll be harder to query, a good rule of thumb is that the name should be meaningful when taken outside of context.
Avoid | Prefer |
---|---|
{Uri} |
{PaymentGatewayUri} |
{Id} |
{AppointmentId} |
{Revision} |
{SpaceStationBlueprintRevision} |
{Endpoint} |
{DataProtectionBlobStorageEndpoint} |
{Type} |
{SapMessageType} |
{Version} |
{DesignDocumentVersion} |
What do they mean?
- Trace - very low-level debugging/internal information (might log Personally Identifiable Information)
- These will be logged to Application Insights Live Metrics
- Debug - low level, control logic, diagnostic information (how and why)
- Information - non "internals" information / black box (not how but what)
- Warning - possible issues or service/functionality degradation
- Error - unexpected failure within the application or connected systems
- Critical - critical errors causing complete failure of the application
_l.LogTrace("Calculated {CheckDigit} for {CardNumber}", check, card);
_l.LogDebug("Applied VIP discount for {CustomerId}", customerId);
_l.LogInformation("New {OrderId} placed by {CustomerId}", orderId, customerId);
_l.LogWarning(exception, "Failed to save new {OrderId}, retrying in {SaveOrderRetryDelay} milliseconds", orderId, retryDelay);
_l.LogError("Failed to save new {OrderId}", orderId);
_l.LogCritical("Unhandled exception, application is terminating.");
Log exception instances instead of the Message
only. This gives us access to the stack trace and the inner exception(s).
Prefer:
_l.LogError(ex, "Could not authorise payment for order {OrderId}", orderId);
Avoid:
_l.LogError($"Failed to process, error: {ex.Message}");
When catching and swallowing an exception, consider logging the exception instead of a log message only. This will provided us more contact as to what went wrong.
Prefer:
catch (Exception ex)
{
_logger.LogWarning(ex, "Some action went wrong");
}
Avoid:
catch
{
_logger.LogWarning("Some action went wrong");
}