SL3 - Serilog Recipes and some advanced features
Let's explore some advanced features of serilog that we need in everyday programming
Welcome
I decided to share my knowledge about Structured Logging
using 4 Post series in the hope that someone in community can benefits from it. I tried to make these post Beginner Friendly
so if some experienced readers find it too basic/boring i apologies in advance.This is the third post in the series
All the posts from the series:
- All about structured logging - Introduction
- Getting Started with Serilog - Fundamentals
- Serilog Recipes - Templates, LogContext, Enriching Logs etc(This Post)
- Application Insights for Structured Logging
If you are beginner with the structured logging i highly recommend to have a read of the first post in the series. It’s about 5 minutes read. But it actually represent the use case for Structured Logging
and how your applications can benefit from it in terms of monitoring and Dev-Ops aspect.Without any further delay let’s get started with the post.
I took about a week break between this post and last post because it was quite hard for me to decide a structure of this post information so that readers find it more natural or easy to understand. So let’s have a quick look at all the information that i will share with this post.
Agenda of the post:
- Serialization
- Message Templates and Properties (Some readers will prefer to read this before Serialization)
- Context, Correlation and Enriching Logs
These are the few of the Serilog features that i use almost everyday while using serilog to write structured events
from my applications. This post got dragged a little bit even after my best efforts to keep it concise. I hope you find it worth your time.
All the code examples used in this blog series can be found on it Github under the this Repo
Intro to Application Insights (If you want to follow along with code)
I was planning to delay the setup of Azure Application Insights till the last and the 4th post of the series. But then i realized it is important to have some Sink
configured to demonstrate these advanced features of Serilog
. So let’s create a application insights resource quickly. Please follow these steps if you want to run the code samples included with the post and see results for yourself. Otherwise you can skip it and we will do this configuration again in next post.
Step 1: Create free azure account using this Link
Step 2: Create Application Insight resource under your account using steps 1 to 3 from here.
Once you have the Application Insights
resource created click on it to view details and the overview section should look something like.
And you will be interested in the instrumentation key. Take a copy of it and configure your appsettings.json
file with the instrumentation key
value.
My config at this point looks like but i also use the combination of Code + Config File
to configure logger:
{
"Logging": {
"LogLevel": {
"Default": "Warning"
}
},
"ApplicationInsights": {
"InstrumentationKey": "04cf998a-699a-469e-af3f-18e4c56d4b63"//will be deleted use your own key.
},
"AllowedHosts": "*"
}
Other configuration is configured using the Code configuration option of Serilog
.
To check the above configuration run the code and hit the DummyController url https://localhost:44345/api/dummy/favorite in your browser and wait for may be 2-3 minutes then you should be able to see traces being recorded under the Search
page of Application Insights
.
There will be a delay between when code write logs to application insights and then they show up in the search console of Azure. This delay is introduced by ingestion of logging data by azure application insights. Stack Overflow Comment
This post is not about how to use Application Insights
so i provided very basic instruction to configure more details about application insights in next post.Without any further delay lets get started with first topic for the discussion.
Serialization
Let’s have a look at one of the code examples from the past posts. This code is part of the DummyController.cs
in the code files attached.
//... Removed code for readability
_logger.LogInformation("Returning car with RegistrationNo:{RegistrationNo} and {@CarInfo}", car.Registration, car);
//... Removed code for readability
If you look at the above statement it looks any typical logging statement except few things. One of them you may have noticed is @
.
Serilog
will serialize data before dumping if @
is used in front of hole placeholder.
And if it is some array
or collection
of strings it will automatically serialize you do not even need to mention the @
sign in front of it.
So in order to see the operator @
in action let check the Application Insights
traces and what get logged.
Few things to Notice:
- We have user friend message as we logged as string under “Message”
- Under custom properties we have property for “RegistrationNo”
- We have property for “CarInfo”
- “CarInfo” is serialized while logging
So if we break down things a little bit. Everything inside the {}
curly braces with VariableName
will get converted into Named custom property on the Traces
logged and Values with @
and inside {}
get converted in to named properties plus the values passed get serialized.
But why you want to store some values as Custom Properties
on insights traces ?
Because if you create custom properties you can query logs using those custom properties. You can do something like this in application insight queries
.
select * from logs where RegistrationNo = 'M5667' and timestamp > "2019-01-11T02:13:10Z"
This will return all the Log entries for a car registration number = M5667 after a particular date and time. Powerful isn’t it but more on queries in Next post of the series.
Next question is What about if you do not want to use the serialization provided by the serilog out of the box. You may have overriden the ToString
for a class and want to use that instead. We can achieve that using $
sign instead of @
. So your code will change to
_logger.LogInformation("Returning car with RegistrationNo:{RegistrationNo} and {$CarInfo}", car.Registration, car);
Destruct
If you want to control what properties of the object should be included in the serialization process then please check the Destruct
section from the last post. That section explains the Destruct functionality in full details and various ways to get it done.
Scalars
As we understand few types like Email
and Uri
in .NET world are complex types with many properties but usually we are interested in the String
value only. Instead of using $
in every logging statement we can do it globally.
To store them just as string representation we can configure logger globally to always save their String
representation.
.Destructure.AsScalar<EmailAddress>();
Request: Please be careful about the objects that you serialize using @ operator. If you serialize large nested objects. Then you can have some performance surprises.
Bit of summary of what we discussed about Serialization
is
- Use
{}
to created named custom properties. - Use
@
to serialize the objects. - Use
$
to use theToString()
representation of object. - Use
Destruct
to control what fields should be logged for an object. - Use
AsScalar
to configure logger globally to userString
representation for a particular type(Eg: EmailAddress)
That was pretty much my knowledge about how to use Serialization
features of Serilog
. Now time to look at the next topic of discussion Message Templates and Properties
.
Message Templates and Properties
This topic one of most brief topic from this blog post. But i still think it’s worth explaining all the capabilities that Message Template
offer.
First of all what is message template. If you look at the screenshot above for a Trace
entry there is a property under the custom properties
called 'MessageTemplate
and it looks like.
It is exactly the string
we used inside the logging statement so if we break it down.
Message Template:
"Returning car with RegistrationNo:{RegistrationNo} and {$CarInfo}"
Variables to replace:
car.Registration, car
Formatting options
Message templates are super set
of the .NET strings. That means they can do everything that .NET strings are capable of. Plus some additional capabilities.
What does that mean you can use all String format
capabilities of the .net Strings
.
Few of them are:
- {0:c} to format numeric value as currency (Ex: $27400.50)
- {0:n} to format as thousand seperators (Ex: 27,400.50)
- {0:x} to format as Hexadecimal
Beyond the standard numeric format strings, you can define your own specific formatting
For example:
- {0:0000.0000} will format
3.45
to00003.4500
- {0:#####:##} will format
531.456
to531.45
Then there is a plethora of options to format Dates
and other data types provided by .NET
string. For a extensive cheat sheet check this link
Why they matter?
Why the Message Templates
worth a mention in this post because they are actually driving factor behind all the benefits of Structured Logging
.
In conventional logger the when logging information is captured It is caputred Human Readable
strings then if we need to extract parameters/properties
for analysis and filtering. We do it by string parsing and extracting parameters/properties we are interested in.
Structured logging approaches data capture and formatting separately. In the structured logging workflow, capturing happens first while formatting an event into human-readable text is deferred until it’s needed later on,after any property-based filtering has been applied.
Steps in which information processed are:
- Take the description of an event and associated parameters
- Capture the description and parameter values
- Collect a serialized representation like JSON
- Filter based on parameters
- Format into human-readable text* (Using Message Templates)
- View matching events
Filtering using Message Templates
Moreover in application insights analytics
we can search for logs entries using the message templates. As they are saved as custom properties on the traces. Below is a valid Azure Application Insights
query that will filter and return Log
entries by using MessageTemplate
-- Valid App Insights query
traces
| where customDimensions.MessageTemplate == 'Returning car with RegistrationNo:{RegistrationNo} and {@CarInfo}'
I think this will help you have a basic understanding of the Message Templates and Properties
. Let’s move to the next step Context and Correlation
which is one of the most useful and important feature of Structured Logging
.
Context and Correlation
In my opinion real power of Serilog
as a structured logging come from the fact that it has such a nice api to add Contextual
and Correlation
information to your log entries.
What is correlation and why it is important (If already know please skip to next section)
Let’s first explain what does Correlation
mean for the server applications. Why it is so important.
Consider a very simple Monolith web application with multiple layers. Something like that you can see on the left in the image below.
Most probably the end user request will land on the API Layer
then the application layer call the Application Layer
and then Application Layer
will call further layers to process request.
Some information/context
that you have available at the API Layer
is not available in other layers. For example you have UserId
available at the API Layer
or RequestId
is available at the API Layer
inside your controller action. But once you call the Application Layer
that information will be not available anymore. But you may want to include these additional info in log entries so you can track/correlate
the same request by the user as it is processed through different layers of the applications.
Adding such information that actually help you relate/track
requests across different layers/applications is called correlation.
In more complex scenarios your Web Application
can be group of services (Micro Services or whatever) that work together to process end user requests. In that case you will have to track the same request across multiple services that make the application. But that discussion is beyond the scope of this post.
Different ways to add Contextual or Correlation information
There are mainly 3 ways(that i know of) to add Contextual or Correlation information to your logging entries.
- Contextual Loggers
- Ambient Contexts
- Globally attached properties
Contextual Loggers
This is the simplest way to add Context
information to Logger. This is useful mainly to tag the Source
of logs. You can do it once off while creating Logger
instance and then every event logged using that instance will have these contextual properties.
For example you want to add Controller Name
to the log statements generated by a controller code it can be as easy as
Log.ForContext("Source", typeof(CartController).FullName)
.Information("Adding {Item} to cart {CartId}", item, cart.Id);
ForContext
method returns a logger that will attach the specified property to the events logged.
But to me that looks like lot of duplicate code for each controller. Don’t worry there is a better way to achieve this.
If you are using .NET core
Dependency Inject you can inject Contextual Loggers
simply like this.
//DummyController.cs
//Just constructor code
private ILogger<DummyController> _logger;
//Constructor where Logger will have contextual information about the Controller Name
public DummyController(ILogger<DummyController> logger)
{
_logger = logger;
}
Ambient Contexts
Within a unit of work such as a single HTTP request, or processing a message from a message queue, it’s useful to tag all events with a correlation identifier unique to the unit of work. Properties like RequestId
or MessageId
fit this pattern.
If use the Contextual Logger
option we then have to account for passing the ILogger
instance throughout request processing and make sure that all logging goes through the same instance . This can result in a lot of ILogger
parameters on the various methods used during request processing.
To cater for this everyday requirement Serilog supports this use case through the ambient LogContext
:
To use the ambient context you need to opt in at the configuration time of your logger how you do that ?
//something like this will help
var loggerConfiguration = new LoggerConfiguration()
.ReadFrom.Configuration(configuration)
.Enrich.FromLogContext()//Opt in for Ambient Context Variable
So we will demonstrate this with a simple example. Where we have Endpoint on DummyController
action named api/dummy/latest
that will call some DummyService
(Application Layer) method GetLatestCars
and we will try to add RequestId
to the ambient context so that we can add it to log statement inside the application layer.
So the Dummy Service look like:
//DummyService.cs
public class DummyService : IDummyService
{
private ILogger<DummyService> _logger;
//Getting it's own version of Logger
public DummyService(ILogger<DummyService> logger)
{
_logger = logger;
}
/// <summary>
/// Just a dummy method to return a list of cars.
/// </summary>
/// <param name="count"></param>
/// <returns></returns>
public List<CarDto> GetLatestCars(short count =10)
{
_logger.LogInformation("Inside Service: User request to Fetch {Count} latest cars", count);
var range = Enumerable.Range(1,count);
var cars = new List<CarDto>();
foreach (var item in range)
{
cars.Add(new CarDto
{
EngineNo = $"ENG1234{item}",
Make = $"Toyota{item}",
Model = $"{2014+item}",
Registration = $"M228{item}"
});
}
//DO NOT LOG EVERYTHING ON PRODUCTION IT FOR DEMO PURPOSES
_logger.LogInformation("Inside Service: GetLatestCars returned {@Cars}", cars);
return cars;
}
Then we setup the New DummyService
for .NET Core
dependency injection.
//Startup.cs
//Register our Dummy service to call
services.AddTransient<IDummyService, DummyService>();
Now time to update the DummyController
to Use our shiny new DummyService
to get the data. Let’s look at how the controller constructor will be changes to inject the service.
//DummyController.cs
//...Removed code for readability
private readonly ILogger<DummyController> _logger;
private IDummyService _dummyService;
public DummyController(IDummyService dummyService, ILogger<DummyController> logger)
{
_logger = logger;
_dummyService = dummyService;
}
Now we will look at how we can use the new DummyService
inside a controller action.
//Actual action to get latest cars
[HttpGet]
[Route("latest")]
public ActionResult GetLatestCar()
{
//it can be anything i am using some random value as context variable like UserId or something
using (LogContext.PushProperty("ApiRequestId", Guid.NewGuid()))
{
_logger.LogInformation("Inside Controller: User request to fetch the Favorite car");
var cars = _dummyService.GetLatestCars(10);
return Ok(cars);
}
}
So if we look at the above I pushed the parameter ApiRequestId
to ambient context with using
block as a result we should see the same value of ApiRequest
parameter for traces logged inside the Controller action and inside the DummyService
method.
We have these logging statements
//Inside controller (Controller Trace)
_logger.LogInformation("Inside Controller: User request to fetch the Favorite car");
//Inside the DummyService (Service Trace 1)
_logger.LogInformation("Inside Service: User request to fetch the latest car");
//(Service Trace 2)
_logger.LogInformation("Inside Service: GetLatestCars returned {@Cars}", cars);
Let’s hit the https://localhost:44345/api/dummy/latest in the browser. Then go to Application Insights
and check for the traces recorded.
For the Controller Trace we recorded:
For Service Trace 1 We recorded
For Service Trace 2 We recorded
If you look carefully you can see all of these traces have the ApiRequestId
custom property with same value. We just added a variable to the ambient context from controller and it was maintained through the whole call hierarchy no matter how deep the call stack is.
I think it is enough regarding the context
and correlation
to get you started and you can explore more in your own adventures. Let’s move on to the next item on the list Globally attached properties
.
Globally attached properties
Few common pieces of information that we mostly add to context as correlation information. To identify which Application, which environment and which specific request failed or took too long to process.
- Machine Name - Server Machine Name(To find out incase only one of the machines in server farm is having issues)
- Environment - Ex: Prod, Staging, UAT etc
- Application - Name of the application
- Request Id - So we can track all the events raised by the same request.
As you want this information to be added to almost every logging event so you must prefer to do it globally and every logger instance just inherit that.
You can attached the properties at the configuration time.
Log.Logger = new LoggerConfiguration()
.Enrich.WithProperty("Application", "e-Commerce")
.Enrich.WithProperty("Environment", ConfigurationManager.AppSettings["Environment"]);
There are few pre-built Enrichers that can help you configure such global Properties
Few enrichers are
-
Enrich with machine props https://github.com/serilog/serilog-enrichers-environment
-
Enrich with Process properties https://github.com/serilog/serilog-enrichers-process
-
Enrich with Thread Related Info https://github.com/serilog/serilog-enrichers-thread
using these pre built enrichers are pretty easy to use it can be as simple as:
//using Serilog-enricher-environment.
.Enrich.WithMachineName()
There are many other amazing enrichers out there that you can use. But in case you don’t want something you want you can create your own enricher and its pretty easy to do so.
//our shiny and useless new enricher
//But i think you got my point
class NerdLifeEnricher : ILogEventEnricher
{
public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory)
{
logEvent.AddPropertyIfAbsent(
propertyFactory.CreateProperty("Remember", "LiveFullStack", false));
}
}
//How to use it now?
Log.Logger = new LoggerConfiguration()
.Enrich.With(new NerdLifeEnricher())
Time to say goodbye (+Shameless Plug)
In this post we discussed few recipes with Serilog
and some of its impressive features. Time for me to say goodbye but i will see you guys soon with another post (Last of the Series). Till then bye and take care.
If you like my posts remember your words of appreciation goes long way in inspiring me to keep writing. If you do not want to miss any of my Future posts follow me on Twitter