Friday, 19 July 2019

Sample .Net Core Service Hosting

Windows Service, Linux daemon, Google Cloud Compute Engine

Today I am going to look at implementing .Net Core Generic Service Host and host it in .Net Core app to run as daemon under Linux. We will use Google.Cloud.Logging.NLog library to integrate NLog with Stackdriver. See "Integrating NLog with Stackdriver for error reporting on Google Cloud" for more details. App will be deployed on Google Cloud Compute Engine VM running Linux and will run as daemon service. In this sample service I will run a thread that will post an information message to Google Logging Interface. The sample code for this project is hosted on github.

We will create sample_service_hosting .Net core console project, which will implement our SampleService that will post messages to Google Logging Interface.

1. Create sample_service_hosting .Net Core console project

>mkdir sample_service_hosting
>cd sample_service_hosting
>dotnet new console
Add the following package references
<ItemGroup>
<PackageReference Include="Google.Cloud.Logging.NLog" Version="2.1.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.CommandLine" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Hosting" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Configuration" Version="2.2.0" />
<PackageReference Include="Microsoft.Extensions.Logging.Console" Version="2.2.0" />
<PackageReference Include="nlog" Version="4.6.6" />
<PackageReference Include="NLog.Extensions.Hosting" Version="1.5.2" />
</ItemGroup>

NLog, NLog.Extensions.Hosting and Google.Cloud.Logging.NLog are also added for a final stage of the project to support logging messages to Google Cloud Log Viewer.

2. Implement HostBuilder configuration


Now we are going to implement HostBuilder, which will run our Generic Host with RunAsync call. I normally create it in the separate CreateHostBuilder function. It will return null if for some reason we failed to create our generic host and in Main we will need to check for returned result.
using System.IO;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
using NLog.Extensions.Hosting;
namespace VSC
{
class Program
{
static async Task Main(string[] args)
{
IHostBuilder hostBuilder = CreateHostBuilder(args);
if(hostBuilder != null)
{
IHost host = hostBuilder.Build();
await host.RunAsync();
}
}
private static IHostBuilder CreateHostBuilder(string[] args)
{
try
{
var builder = new HostBuilder()
.ConfigureAppConfiguration((hostingContext, config) =>
{
config.SetBasePath(Directory.GetCurrentDirectory());
config.AddJsonFile("appsettings.json", optional: true);
config.AddJsonFile(
$"appsettings.{hostingContext.HostingEnvironment.EnvironmentName}.json",
optional: true);
config.AddCommandLine(args);
})
.ConfigureServices((hostContext, services) =>
{
services.AddHostedService<Services.SampleService>();
services.Configure<HostOptions>(option =>
{
option.ShutdownTimeout = System.TimeSpan.FromSeconds(20);
});
})
.ConfigureLogging((hostingContext, logging) =>
{
logging.AddConfiguration(hostingContext.Configuration.GetSection("Logging"));
logging.AddConsole();
});
return builder;
}
catch { }
return null;
}
}
}
You notices that we adding appsettings.json file as well. Lets add it to our project.
{
"Logging": {
"LogLevel": {
"Default": "Trace",
"Microsoft": "Information"
}
}
}
And a SampleService class, which we will extend in the next section. Add SampleService.cs under Services folder.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
namespace VSC.Services
{
public class SampleService : IHostedService, IDisposable
{
public void Dispose()
{
// TODO: Do service clean up here
}
public Task StartAsync(CancellationToken cancellationToken)
{
// TODO: Do work here
return Task.CompletedTask;
}
public Task StopAsync(CancellationToken cancellationToken)
{
// stop service processes here
return Task.CompletedTask;
}
}
}

3. Implement SampleService hosted service


In this section we are going to inject nlog into our generic host builder and extend our SampleService host to log message every 10 seconds. We already configured HostBuilder to inject logger in ConfigureLogging section. We need to add .UseNLog() call just after var builder = new HostBuilder() in CreateHostBuilder function. In the SampleService class we will use CancellationTokenSource to cancel our service and stop Run thread when IsCancellationRequested is true. Run function will be called asynchronously and pointer to Task object is saved.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace VSC.Services
{
public class SampleService : IHostedService, IDisposable
{
private CancellationTokenSource _cancellationTokenSource;
private Task _executingTask;
ILogger<SampleService> _logger;
public SampleService(
ILogger<SampleService> logger
)
{
_logger = logger;
}
public void Dispose()
{
// TODO: Do service clean up here
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("SampleService StartAsync method called.");
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executingTask = Run(_cancellationTokenSource.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("SampleService StopAsync method called.");
// Stop called without start
if (_executingTask == null)
{
return;
}
// stop service processes here
_cancellationTokenSource.Cancel();
_logger.LogInformation("Sample Service stopped.");
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
}
private async Task Run(CancellationToken cancellationToken)
{
_logger.LogTrace("Starting iteration count Run");
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
int iterationCount = 0;
while (!cancellationToken.IsCancellationRequested)
{
iterationCount++;
_logger.LogInformation(string.Format("Running round {0}", iterationCount));
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
}
}
}

Logger is logging Trace messages and information messages within iteration loop. Add nlog.config to project.
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="info">
<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
</extensions>
<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target name="asyncLogFile" xsi:type="AsyncWrapper">
<target
xsi:type="File"
name="logfile"
fileName="log/log-${shortdate}.txt"
archiveFileName="log/archive/log-${shortdate}.txt"
keepFileOpen="true"
archiveAboveSize="20000000"
concurrentWrites="false"
archiveEvery="Day"
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
</target>
<target name="asyncConsole" xsi:type="AsyncWrapper">
<target name="console" xsi:type="ColoredConsole"
layout="${date} ${threadname:whenEmpty=${threadid}} ${level:uppercase=true} ${logger} ${message} ${onexception:${exception}}" />
</target>
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs-->
<logger name="*" minlevel="Trace" writeTo="asyncLogFile" />
<logger name="*" minlevel="Info" writeTo="asyncConsole" />
<logger name="Microsoft.*" maxLevel="Info" final="true" /> <!-- BlackHole without writeTo -->
</rules>
</nlog>


4. Enabling gradual stop of the service on SIGTERM signal


We can deploy this sample_service_hosting app to Linux VM and make it run as a daemon, but with its current implementation it will not properly respond to SIGTERM sent from Linux host to gradually stop and clean up used resources. For this we are going to use injected IApplicationLifetime object and register events ApplicationStarted, ApplicationStopping and ApplicationStoped.
using System;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Logging;
namespace VSC.Services
{
public class SampleService : IHostedService, IDisposable
{
private CancellationTokenSource _cancellationTokenSource;
private Task _executingTask;
IApplicationLifetime _appLifetime;
ILogger<SampleService> _logger;
public SampleService(
ILogger<SampleService> logger,
IApplicationLifetime appLifetime
)
{
_logger = logger;
_appLifetime = appLifetime;
}
public void Dispose()
{
// TODO: Do service clean up here
}
public Task StartAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("SampleService StartAsync method called.");
_appLifetime.ApplicationStarted.Register(OnStarted);
_appLifetime.ApplicationStopping.Register(OnStopping);
_appLifetime.ApplicationStopped.Register(OnStopped);
_cancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken);
_executingTask = Run(_cancellationTokenSource.Token);
return Task.CompletedTask;
}
public async Task StopAsync(CancellationToken cancellationToken)
{
_logger.LogTrace("SampleService StopAsync method called.");
// Stop called without start
if (_executingTask == null)
{
return;
}
// stop service processes here
_cancellationTokenSource.Cancel();
_logger.LogInformation("Sample Service stopped.");
// Wait until the task completes or the stop token triggers
await Task.WhenAny(_executingTask, Task.Delay(-1, cancellationToken));
}
private void OnStarted()
{
_logger.LogTrace("SampleService OnStarted method called.");
// Post-startup code goes here
}
private void OnStopping()
{
_logger.LogTrace("SampleService OnStopping method called.");
// On-stopping code goes here
}
private void OnStopped()
{
_logger.LogTrace("SampleService OnStopped method called.");
// Post-stopped code goes here
}
private async Task Run(CancellationToken cancellationToken)
{
_logger.LogTrace("Starting iteration count Run");
await Task.Delay(TimeSpan.FromMilliseconds(100), cancellationToken);
int iterationCount = 0;
while (!cancellationToken.IsCancellationRequested)
{
iterationCount++;
_logger.LogInformation(string.Format("Running round {0}", iterationCount));
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
}
}
}
}


5. NLog.config - Add support for Stackdriver logging


At the start we already added reference to Google.Cloud.Logging.NLog package. Now we need to update nlog.config file to include assembly Google.Cloud.Logging.NLog and GoogleStackdriver target to log to Google Logging Interface.
<?xml version="1.0" encoding="utf-8" ?>
<nlog xmlns="http://www.nlog-project.org/schemas/NLog.xsd"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
autoReload="true"
internalLogLevel="info">
<!-- enable asp.net core layout renderers -->
<extensions>
<add assembly="NLog.Web.AspNetCore"/>
<add assembly="Google.Cloud.Logging.NLog"/>
</extensions>
<!-- the targets to write to -->
<targets>
<!-- write logs to file -->
<target name="asyncLogFile" xsi:type="AsyncWrapper">
<target
xsi:type="File"
name="logfile"
fileName="log/log-${shortdate}.txt"
archiveFileName="log/archive/log-${shortdate}.txt"
keepFileOpen="true"
archiveAboveSize="20000000"
concurrentWrites="false"
archiveEvery="Day"
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
</target>
<target name="asyncConsole" xsi:type="AsyncWrapper">
<target name="console" xsi:type="ColoredConsole"
layout="${date} ${threadname:whenEmpty=${threadid}} ${level:uppercase=true} ${logger} ${message} ${onexception:${exception}}" />
</target>
<target name="asyncStackDriver" xsi:type="AsyncWrapper">
<target name="stackDriver"
xsi:type="GoogleStackdriver"
logId="Default"
layout="${longdate}|${uppercase:${level}}|${logger}|${message} ${exception:format=tostring}" />
</target>
</targets>
<!-- rules to map from logger name to target -->
<rules>
<!--All logs-->
<logger name="*" minlevel="Trace" writeTo="asyncLogFile" />
<logger name="*" minlevel="Info" writeTo="asyncConsole" />
<logger name="*" minlevel="Trace" writeTo="asyncStackDriver" />
<logger name="Microsoft.*" maxLevel="Info" final="true" /> <!-- BlackHole without writeTo -->
</rules>
</nlog>


Conclusion


Our sample_service_hosting app is done. Check the final version of this project on github. I am not going to cover here setup of this app as a daemon on Linux VM Google Cloud Compute Engine. This will be a topic of separate blog post. Stay tuned. 

References:



No comments:

Post a Comment