What every ASP.NET Core Web API project needs - Part 1 - Serilog
Tags: dotnet, aspnetcore, webapi, architecture
Published: 2021-02-27 14:26:38

In a series of articles, I'm going to show the implementation of an architecture that is suitable for a thin Web API project or Web API that fits in a microservices architecture. In the first few articles, I'm going to introduce several useful libraries.

Let's get started with logging. Logging is just essential for debugging, troubleshooting and monitoring. A good logging system makes life much easier.

Why [Serilog](https://serilog.net/)? It is easy to set up, has a clean API, and is portable between recent .NET platforms. The big difference between Serilog and the other frameworks is that it is designed to do structured logging out of the box. Another thing I really like about Serilog is that it can be configured via the `appsetting.json` file alongside configuring through code. Changing logging configuration without touching the codebase is really helpful, especially in the production environment.

Let's start with creating a new project. 
###Step 1 - New project
Create a new ASP.NET Core 5.0 API project.

###Step 2 - Install package
Install [Serilog.AspNetCore](https://www.nuget.org/packages/Serilog.AspNetCore) nuget package.

###Step 3 - Add `UseSerilog` extension method
Open `Program.cs` file and modify `CreateHostBuilder` method:

```csharp
public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(webBuilder =>
        {
            webBuilder.UseStartup<Startup>();
        })
        .UseSerilog((hostingContext, loggerConfiguration) =>
            loggerConfiguration.ReadFrom.Configuration(hostingContext.Configuration));
```
`UseSerilog` sets Serilog as the logging provider. We are going to config Serilog via the `appsettings.json` file.

###Step 4 - Remove default configuration
Open `appsettings.json` and `appsettings.Development.json` file and get rid of the logging section:
```json
"Logging": {
  "LogLevel": {
    "Default": "Information",
    "Microsoft": "Warning",
    "Microsoft.Hosting.Lifetime": "Information"
  }
}
```

###Step 5: Add Serilog configuration
Add Serilog configuration section to `appsettings.Development.json` file:
```json
"Serilog": {
  "MinimumLevel": {
    "Default": "Debug",
    "Override": {
      "Microsoft": "Information",
      "System": "Information"
    },
    "Using": [ ],
  },
  "WriteTo": [
    { }
  ]
}
```
`Serilog.AspNetCore` nuget package has dependency on [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) nuget package and it is a Serilog settings provider that reads from `Microsoft.Extensions.Configuration` sources. The above configuration is equivalent to this:
```csharp
Log.Logger = new LoggerConfiguration()
    .MinimumLevel.Debug()
    .MinimumLevel.Override("Microsoft", LogEventLevel.Information)
    .MinimumLevel.Override("System", LogEventLevel.Information)
    .Enrich.FromLogContext()
    .CreateLogger();
```

###Step 6 - Installing Sinks
Serilog uses [sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks) to write log events to storage for example database, file, etc. One of the most popular sinks for debugging environment is the [Console](https://github.com/serilog/serilog-sinks-console) sink. 
- Install `Serilog.Sinks.Console`nuget package
- Add following configuration:
```json
"Serilog": {
  "MinimumLevel": {
    "Default": "Debug",
    "Override": {
      "Microsoft": "Information",
      "System": "Information"
    }
  },
  "Using": [ "Serilog.Sinks.Console" ],
  "WriteTo": [
    { "Name": "Console" }
  ]
}
```
![Console Sink](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/14yyzb1ra5012dy551s1.jpg)
After installing the sink 
- In the `Using` section add the sink's nuget package name `"Using": [ "Serilog.Sinks.Console" ]`
- In the `WriteTo` section add sink name and arguments `"WriteTo":[ { "Name": "Console" } ]`

Now we want to use SQL Server sink for other environments:
- Install [Serilog.Sinks.MSSqlServer](https://www.nuget.org/packages/serilog.sinks.mssqlserver) sink
- Copy Serilog setting form `appsettings.Development.json` to `appsettings.json` file
```json
"Serilog": {
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "Microsoft": "Error",
      "System": "Error"
    },
    "Using": [ "Serilog.Sinks.MSSqlServer" ]
  },
  "WriteTo": [
    {
      "Name": "MSSqlServer",
      "Args": {
        "connectionString": "ConnectionString",
        "tableName": "Logs",
        "autoCreateSqlTable": true
      }
    }
  ]
}
```

###Step 7 - How to configure a sink
Well, configuring a sink via `appsettings.json` could be harder than configuring through the code, and for each sink, you might not be able to find a JSON configuration sample. Normally each sink accepts several parameters to configure the sink. For instance, the Console sink accepts the below parameters:
![Console Sink Configuration](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3eomnfbykh3r19mer21w.jpg)
Each one of these parameters can be configured through JSON:
```json
"WriteTo": [
  {
    "Name": "Console",
    "Args": {
      "restrictedToMinimumLevel": "Verbose",
      "outputTemplate": "[{Timestamp:HH:mm:ss} {Level:u3}] {Message:lj} <s:{SourceContext}>{NewLine}{Exception}",
      "theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
    }
  }
]
```
![Colored Console Sink](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/a3gus8s8idplzrc9k03c.jpg)
To see complete SQL Server sink JSON configuration check out [this](https://github.com/serilog/serilog-sinks-mssqlserver/blob/dcdb0cb87ad8bdc2f94229579acb57c3abfdfa3c/sample/WorkerServiceDemo/appsettings.json).

###Step 8 - Enrichers
Log events can be enriched with properties in various ways. You can add additional data by enrichers. For instance, in the production environment, we want to add the IP of the client to the log events. 
- Install [Serilog.Enrichers.ClientInfo](https://www.nuget.org/packages/Serilog.Enrichers.ClientInfo) package
- Add enriched package name to `Using` section 
- Add `Enrich` section with `WithClientIp` value (enriched name normally starts with `With` word)
```json
"Using": [ "Serilog.Sinks.MSSqlServer", "Serilog.Enrichers.ClientInfo" ],
"Enrich": [ "WithClientIp" ]
```
All events written through log will carry a property `ClientIp` with the IP of the client. Check out the list of available enrichers [here](https://github.com/serilog/serilog/wiki/Enrichment#available-enricher-packages).

###Step 9 - Filters
By using filters you can exclude or include some log events.
- Install `Serilog.Expressions` nuget package
- Add the "Filter" section to Serilog settings
```json
"Filter": [
  {
    "Name": "ByExcluding",
    "Args": {
      "expression": "RequestPath like '%swagger%'"
    }
  }
]
```
All log events that contain `swagger` will be excluded.
![Serilog exclude](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9h3lmyp061pyxkcxuw5i.jpg)
To see all possible configurations check out [Serilog.Settings.Configuration](https://github.com/serilog/serilog-settings-configuration) Github repository.

###Step 10 - HTTP requests loging
Moreover, you can log the HTTP requests.
- In `Startup.cs` file, add the middleware with `UseSerilogRequestLogging()`:
```csharp
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    ...
    app.UseSerilogRequestLogging();
```
- In `MinimumLevel.Override` section add `"Microsoft.AspNetCore": "Warning"`:
```json
"MinimumLevel": {
  "Default": "Information",
  "Override": {
    "Microsoft": "Error",
    "Microsoft.AspNetCore": "Warning",
    "System": "Error"
  },
```

###Step 11 - Overridng configuration in docker
Last but not least here I want to mention is that you can override the Serilog setting by Docker environment variable. Consider the following configuration:
```json
"Serilog": {
  "MinimumLevel": {
    "Default": "Information",
    "Override": {
      "Microsoft": "Error",
      "System": "Error"
    },
    "Using": [ "Serilog.Sinks.MSSqlServer" ]
  },
  "WriteTo": [
    {
      "Name": "MSSqlServer",
      "Args": {
        "connectionString": "",
        "tableName": "Logs",
        "autoCreateSqlTable": true
      }
    }
  ]
}
```
Now in the dcoker-compose file we want pass the actual connection string:
```yaml
  my-api:
    environment:
      - ASPNETCORE_ENVIRONMENT=Production
      - Serilog__MinimumLevel__Default=Warning
      - Serilog__WriteTo__0__Args__connectionString="Your connection string"
```
The value of each section can be accessed by ``__``, for instance, `Serilog__MinimumLevel__Default` is equivalent to:
```
"Serilog": {
  "MinimumLevel": {
    "Default": "",
```
In a section to access an item inside the array, use the item index number. `"WriteTo"` section accepts an array of sinks configuration. If you are using two sinks use `Serilog__WriteTo__0__` to access the first sink and `Serilog__WriteTo__1__` to access the second sink.

###Test
Let's do a simple test. Open a `CMD` or  `Powershell` at the project directory:
- Type `dotnet add package Serilog.Sinks.File` to install File sink
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/qaul1k7w2r2zxnsu6rvb.jpg)
- Open `appsettings.josn` file and change the logging configuration like this:
```json
"Serilog": {
  "MinimumLevel": {
    "Default": "Debug",
    "Override": {
      "Microsoft": "Information",
      "System": "Information"
    }
  },
  "Using": [ "Serilog.Sinks.Console", "Serilog.Sinks.File" ],
  "WriteTo": [
    { "Name": "Console" },
    {
      "Name": "File",
      "Args": {
        "path": "log.txt",
        "rollingInterval": "Day"
      }
    }
  ]
}
```
- Type `dotnet build` then `dotnet run`
- After running the application you should see a log file inside the project directory
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/nbliowa010otvpm8p4l4.jpg)
As you can see without touching codes we added another sink to save the log events.

If you're using SQL Server, PostgreSQL or MongoDB sinks, I have developed a small [log viewer](https://github.com/mo-esmp/serilog-ui) for small projects. It helps a lot, especially in the production environment and you don't need to query the database to view logs.
![Alt Text](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rj129hh9ucvuifmh2no1.jpg)

The source code for this walkthrough could be found on [Github](https://github.com/mo-esmp/cool-webapi).