![]() | NeonService Class |
Namespace: Neon.Service
The NeonService type exposes the following members.
Name | Description | |
---|---|---|
![]() | NeonService |
Constructor.
|
Name | Description | |||
---|---|---|---|---|
![]() | Arguments |
Returns the list of command line arguments passed to the service. This
defaults to an empty list.
| ||
![]() | BaseUri | For services with exactly one network endpoint, this returns the base URI to be used to access the service.
| ||
![]() | Dependencies |
Used to specify other services that must be reachable via the network before a
NeonService will be allowed to start. This is exposed via the
Dependencies where these values can be configured in
code before RunAsync(Boolean) is called or they can
also be configured via environment variables as described in ServiceDependencies.
| ||
![]() | Description |
Returns the service description for this service (if any).
| ||
![]() | Endpoints |
Returns the dictionary mapping case sensitive service endpoint names to endpoint information.
| ||
![]() | ExitCode |
Returns the exit code returned by the service.
| ||
![]() | ExitException |
Returns any abnormal exception thrown by the derived OnRunAsync method.
| ||
![]() | GitVersion |
Returns GIT branch and commit the service was built from as
well as an optional indication the the build branch had
uncomitted changes (e.g. was dirty).
| ||
![]() | InDevelopment |
Returns true when the service is running in development
or test mode, when the DEV_WORKSTATION environment variable
is defined.
| ||
![]() | InProduction |
Returns true when the service is running in production,
when the DEV_WORKSTATION environment variable is
not defined. The NeonServiceFixure will set this
to true explicitly as well.
| ||
![]() | Log |
Returns the service's default logger.
| ||
![]() | LogManager |
Returns the service's log manager.
| ||
![]() | MetricsOptions | Prometheus metrics options. To enable metrics collection for non-ASPNET applications, we recommend that you simply set Mode==Scrape before calling OnRunAsync. See MetricsOptions for more details. | ||
![]() | Name |
Returns the service name.
| ||
![]() | ServiceMap |
Returns the service map (if any).
| ||
![]() | Status |
Returns the service current running status.
| ||
![]() | Terminator |
Returns the service's ProcessTerminator. This can be used
to handle termination signals.
|
Name | Description | |||
---|---|---|---|---|
![]() | Dispose | Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources. | ||
![]() | Dispose(Boolean) |
Releases all associated resources.
| ||
![]() | Equals | Determines whether the specified object is equal to the current object. (Inherited from Object.) | ||
![]() | Exit |
Used by services to stop themselves, specifying an optional process exit code.
| ||
![]() | Finalize |
Finalizer.
(Overrides ObjectFinalize.) | ||
![]() | GetConfigFilePath |
Returns the physical path for the confguration file whose logical path is specified.
| ||
![]() | GetEnvironmentVariable |
Returns the value of an environment variable.
| ||
![]() | GetHashCode | Serves as the default hash function. (Inherited from Object.) | ||
![]() | GetType | Gets the Type of the current instance. (Inherited from Object.) | ||
![]() | LoadEnvironmentVariables | Loads environment variables formatted as NAME=VALUE from a text file as service environment variables. The file will be decrypted using NeonVault if necessary.
| ||
![]() | MemberwiseClone | Creates a shallow copy of the current Object. (Inherited from Object.) | ||
![]() | OnRunAsync |
Called to actually implement the service.
| ||
![]() | RunAsync |
Starts the service if it's not already running. This will call OnRunAsync,
which is your code that actually implements the service. Note that any service dependencies
specified by Dependencies will be verified as ready before OnRunAsync
will be called.
| ||
![]() | SetArguments |
Initializes Arguments with the command line arguments passed.
| ||
![]() | SetConfigFile(String, Byte) |
Maps a logical configuration file path to a temporary file holding the
byte contents passed. This is typically used initializing confguration
files for unit testing.
| ||
![]() | SetConfigFile(String, String, Boolean) |
Maps a logical configuration file path to a temporary file holding the
string contents passed encoded as UTF-8. This is typically used for
initializing confguration files for unit testing.
| ||
![]() | SetConfigFilePath |
Maps a logical configuration file path to an actual file on the
local machine. This is used for unit testing to map a file on
the local workstation to the path where the service expects the
find to be.
| ||
![]() | SetEnvironmentVariable |
Sets or deletes a service environment variable.
| ||
![]() | SetRunningAsync |
Called by OnRunAsync implementation after they've completed any
initialization and are ready for traffic. This sets Status to
Running.
| ||
![]() | SetStatusAsync | |||
![]() | Stop | Stops the service if it's not already stopped. This is intended to be called by external things like unit test fixtures and is not intended to be called by the service itself. | ||
![]() | ToString | Returns a string that represents the current object. (Inherited from Object.) |
Name | Description | |
---|---|---|
![]() ![]() | GlobalLogging |
This controls whether any NeonService instances will use the global
Default log manager for logging or maintain its own
log manager. This defaults to true which will be appropriate for most
production situations. It may be useful to disable this for some unit tests.
|
Basing your service implementations on the Neon.Service class will make them easier to test via integration with the ServiceFixture from the Neon.Xunit library by providing some useful abstractions over service configuration, startup and shutdown including a ProcessTerminator to handle termination signals from Linux or Kubernetes.
This class is pretty easy to use. Simply derive your service class from NeonService and implement the OnRunAsync method. OnRunAsync will be called when your service is started. This is where you'll implement your service. You should perform any initialization and then call SetRunningAsync to indicate that the service is ready for business.
![]() |
---|
Note that calling SetRunningAsync after your service has initialized is important because the NeonServiceFixture won't allow tests to proceed until the service indicates that it's ready. This is necessary to avoid unit test race conditions. |
Note that your OnRunAsync method should generally not return until the Terminator signals it to stop. Alternatively, you can throw a ProgramExitException with an optional process exit code to proactively exit your service.
![]() |
---|
All services should properly handle Terminator stop signals so services deployed as containers will stop promptly and cleanly (this also applies to services running in unit tests). Your terminate handler method must return within a set period of time (30 seconds by default) to avoid killed by by Docker or Kubernetes. This is probably the trickiest thing you'll need to implement. For asynchronous service implementations, you consider passing the CancellationToken to all async method calls. |
![]() |
---|
This class uses the DEV_WORKSTATION environment variable to determine whether the service is running in test mode or not. This variable will typically be defined on developer workstations as well as CI/CD machines. This variable must never be defined for production environments. You can use the InProduction or InDevelopment properties to check this. |
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; namespace Service_Basic { public static class Program { public static async Task Main(string[] args) { // Launch the service. await new MyService().RunAsync(); } } public class MyService : NeonService { public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { // This is where you should dispose thing like your webapp, // database connections, etc. base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { // You can retrieve configuration settings from environment variables // or files passed by Kubernetes, Docker, or unit tests via these // base clase methods: var mySetting = GetEnvironmentVariable("MY_SETTING"); var myConfigPath = GetConfigFilePath("/my-config.yaml"); // Use this base class property to log things. These will be picked up // automatically by Kubernetes and Docker. Log.LogInfo("HELLO WORLD!"); // This is where your service does its thing: like starting a webapp, // process data from queues, implementing a database, or whatever. // Note that there's no need to wrap this code with a try...catch // to log exceptions because the base class already does that for you. // // We're just going to do pretend by doing nothing here except for // wait for a termination signal from Kubernetes, Docker, or the unit // test framework. await Task.Delay(TimeSpan.FromDays(365), Terminator.CancellationToken); // Return a non-zero exit code when the service terminates normally. return 0; } } }
CONFIGURATION
Services are generally configured using environment variables and/or configuration files. In production, environment variables will actually come from the environment after having been initialized by the container image or passed by Kubernetes when starting the service container. Environment variables are retrieved by name (case sensitive).
Configuration files work the same way. They are either present in the service container image or mounted to the container as a secret or config file by Kubernetes. Configuration files are specified by their path (case sensitive) within the running container.
This class provides some abstractions for managing environment variables and configuration files so that services running in production or as a unit test can configure themselves using the same code for both environments.
Services should use the GetEnvironmentVariable(String, String) method to retrieve important environment variables rather than using GetEnvironmentVariable(String). In production, this simply returns the variable directly from the current process. For tests, the environment variable will be returned from a local dictionary that was expicitly initialized by calls to SetEnvironmentVariable(String, String). This local dictionary allows the testing of multiple services at the same time with each being presented their own environment variables.
You may also use the LoadEnvironmentVariables(String, FuncString, String) methods to load environment variables from a text file (potentially encrypted via NeonVault). This will typically be done only for unit tests.
Configuration files work similarily. You'll use GetConfigFilePath(String) to map a logical file path to a physical path. The logical file path is typically specified as the path where the configuration file will be located in production. This can be any valid path with in a running production container and since we're currently Linux centric, will typically be a Linux file path like /etc/MYSERVICE.yaml or /etc/MYSERVICE/config.yaml.
For production, GetConfigFilePath(String) will simply return the file path passed so that the configuration file located there will referenced. For testing, GetConfigFilePath(String) will return the path specified by an earlier call to SetConfigFilePath(String, String, FuncString, String) or to a temporary file initialized by previous calls to SetConfigFile(String, String, Boolean) or SetConfigFile(String, Byte). This indirection provides a consistent way to run services in production as well as in tests, including tests running multiple services simultaneously.
DISPOSE IMPLEMENTATION
All services, especially those that create unmanaged resources like ASP.NET services, sockets, NATS clients, HTTP clients, thread etc. should override and implement Dispose(Boolean) to ensure that any of these resources are proactively disposed. Your method should call the base class version of the method first before disposing these resources.
protected override Dispose(bool disposing) { base.Dispose(disposing); if (appHost != null) { appHost.Dispose(); appHost = null; } }
The disposing parameter is passed as true when the base Dispose method was called or false if the garbage collector is finalizing the instance before discarding it. The difference is subtle and most services can safely ignore this parameter (other than passing it through to the base Dispose(Boolean) method).
In the example above, the service implements an ASP.NET web service where appHost was initialized as the IWebHost actually implementing the web service. The code ensures that the appHost isn't already disposed before disposing it. This will stop the web service and release the underlying listening socket. You'll want to do something like this for any other unmanaged resources your service might hold.
![]() |
---|
It's very important that you take care to dispose things like running web services and listening sockets within your Dispose(Boolean) method. You also need to ensure that any threads you've created are terminated. This means that you'll need a way to signal threads to exit and then wait for them to actually exit. This is important when testing your services with a unit testing framework like Xunit because frameworks like this run all tests within the same Test Runner process and leaving something like a listening socket open on a port (say port 80) may prevent a subsequent test from running successfully due to it not being able to open its listening socket on port 80. |
LOGGING
Each NeonService instance maintains its own LogManager instance with the a default logger created at Log. The log manager is initialized using the LOG_LEVEL environment variable value which defaults to info when not present. LogLevel for the possible values.
Note that the Default log manager will also be initialized with the log level when the service is running in a production environment so that logging in production works completely as expected.
For development environments, the Default instance's log level will not be modified. This means that loggers created from Default may not use the same log level as the service itself. This means that library classes that create their own loggers won't honor the service log level. This is an unfortunate consequence of running emulated services in the same process.
There are two ways to mitigate this. First, any source code defined within the service project should be designed to create loggers from the service's LogManager rather than using the global one. Second, you can configure your unit test to set the desired log level like:
LogManager.Default.SetLogLevel(LogLevel.Debug));
![]() |
---|
Setting the global default log level like this will impact loggers created for all emulated services, but this shouldn't be a problem for more situations. |
HEALTH PROBES
Hosting environments such as Kubernetes will often require service instances to be able to report their health via health probes. These probes are typically implemented as a script that is called periodically by the hosting environment with the script return code indicating the service instance health.
The NeonService class supports this by optionally writing a text file with various strings indicating the health status. This file will consist of a single line of text without line ending characters. You'll need to specify the fully qualified path to this file as an optional parameter to the NeonService constructor.
SERVICE DEPENDENCIES
Services often depend on other services to function, such as a database, rest API, etc. NeonService provides an easy to use integrated way to wait for other services to initialize themselves and become ready before your service will be allowed to start. This is a great way to avoid a blizzard of service failures and restarts when starting a collection of related services on a platform like Kubernetes.
You can use the Dependencies property to control this in code via the ServiceDependencies class or configure this via environment variables:
NEON_SERVICE_DEPENDENCIES_URIS=http://foo.com;tcp://10.0.0.55:1234 NEON_SERVICE_DEPENDENCIES_TIMEOUT_SECONDS=30 NEON_SERVICE_DEPENDENCIES_WAIT_SECONDS=5
The basic idea is that the RunAsync(Boolean) call to start your service will need to successfully to establish socket connections to any service dependecy URIs before your OnRunAsync method will be called. Your service will be terminated if any of the services cannot be reached after the specified timeout.
You can also specity an additional time to wait after all services are available to give them a chance to perform additional internal initialization.
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; namespace Service_Dependencies { public static class Program { public static async Task Main(string[] args) { // Construct the service and configure it to wait for another // become available before calling the services [OnRunAsync()] // method. This is useful for situations where a collection // of related services are started at the same time (e.g. via // docker-compose or Kubernetes Helm charts giving the service // service being relied on a chance to start before this service // tries to access it. // // This can also be an issue when using Istio/Envoy sidecars // in Kubernetes because the Envoy pod sidecar often takes longer // to start than the service, meaning that the network will be // unavailable for few seconds. // // We've effectively implemented the retry logic so you don't // have to. var service = new MyService(); service.Dependencies.Uris.Add(new Uri("http://waitforthis.com")); // You can control how long to wait before a [TimeoutException] // will be thrown. This defaults to 120 seconds. service.Dependencies.Timeout = TimeSpan.FromSeconds(30); // You can optionally wait longer after the dependencies are ready. service.Dependencies.Wait = TimeSpan.FromSeconds(10); await new MyService().RunAsync(); } } public class MyService : NeonService { public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { // The http://waitforthis.com endpoint will be ready at this point. base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { await Task.Delay(TimeSpan.FromDays(365), Terminator.CancellationToken); return 0; } } }
PROMETHEUS METRICS
NeonService can enable services to publish Prometheus metrics with a single line of code; simply set MetricsOptions.Mode to Scrape before calling RunAsync(Boolean). This configures your service to publish metrics via HTTP via http://0.0.0.0:NeonPrometheusScrape/metrics/. We've resistered port NeonPrometheusScrape with Prometheus as a standard port to be used for micro services running in Kubernetes or on other container platforms to make it easy configure scraping for a cluster.
You can also configure a custom port and path or configure metrics push to a Prometheus Pushgateway using other MetricsOptions properties. You can also fully customize your Prometheus configuration by leaving this disabled in MetricsOptions and setting things up using the standard prometheus-net mechanisms before calling RunAsync(Boolean).
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; namespace Service_Dependencies { public static class Program { public static async Task Main(string[] args) { // Construct the service and configure it to wait for another // become available before calling the services [OnRunAsync()] // method. This is useful for situations where a collection // of related services are started at the same time (e.g. via // docker-compose or Kubernetes Helm charts giving the service // service being relied on a chance to start before this service // tries to access it. // // This can also be an issue when using Istio/Envoy sidecars // in Kubernetes because the Envoy pod sidecar often takes longer // to start than the service, meaning that the network will be // unavailable for few seconds. // // We've effectively implemented the retry logic so you don't // have to. var service = new MyService(); service.Dependencies.Uris.Add(new Uri("http://waitforthis.com")); // You can control how long to wait before a [TimeoutException] // will be thrown. This defaults to 120 seconds. service.Dependencies.Timeout = TimeSpan.FromSeconds(30); // You can optionally wait longer after the dependencies are ready. service.Dependencies.Wait = TimeSpan.FromSeconds(10); await new MyService().RunAsync(); } } public class MyService : NeonService { public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { // The http://waitforthis.com endpoint will be ready at this point. base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { await Task.Delay(TimeSpan.FromDays(365), Terminator.CancellationToken); return 0; } } }
NETCORE Runtime METRICS
We highly recommend that you also enable .NET Runtime related metrics for services targeting .NET Core 2.2 or greater.
![]() |
---|
Although the .NET Core 2.2+ runtimes are supported, the runtime apparently has some issues that may prevent this from working properly, so that's not recommended. Note that there's currently no support for any .NET Framework runtime. |
Adding support for this is easy, simply add a reference to the prometheus-net.DotNetRuntime package to your service project and then assign a function callback to GetCollector that configures runtime metrics collection, like:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; using Prometheus; // From the [prometheus-net] nuget package namespace Service_Metrics { public static class Program { public static async Task Main(string[] args) { // You can enable Prometheus metrics with just one line of code! // // This enables scraping mode where the service is configured as an // exporter that Prometheus can scrape periodically on port [9762], // the standard NeonService metrics port. // // You can customize this port using the [MetricsOptions.Port] // property to avoid port conflicts by we recommend standarizing // on the default port when running in a container where port // conflicts won't be an issue. // // You can also configure the service to push metrics to a // Prometheus Pushgateway but doing this should be limited // to special situations. See the Prometheus documentation // for more information. var service = new MyService(); service.MetricsOptions.Mode = MetricsMode.Scrape; await new MyService().RunAsync(); } } public class MyService : NeonService { // Define a custom Prometheus counter. This value will be able to be // tracked on Prometheus related dashboards, alert rules, etc. public static Counter runTimeCounter = Metrics.CreateCounter("run-time", "Service run time in seconds."); public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { // We're just increment the runtime counter once a second until // see see the termination signal. while (!Terminator.CancellationToken.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(1)); runTimeCounter.Inc(); } return 0; } } }
You can also customize the the runtime metrics emitted like this:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; using Prometheus; // From the [prometheus-net] nuget package using Prometheus.DotNetRuntime; // From the [prometheus-net.DotNetRuntime] nuget package namespace Service_RuntimeMetrics { public static class Program { public static async Task Main(string[] args) { var service = new MyService(); service.MetricsOptions.Mode = MetricsMode.Scrape; // For .NET Core 2.2+ based services, we highly recommend that you enable // collection of the .NET Runtime metrics as well to capture information // about threads, memory, exceptions, JIT statistics, etc. You can do this // with just one more statement: service.MetricsOptions.GetCollector = () => DotNetRuntimeStatsBuilder .Default() .StartCollecting(); // The line above collects all of the available runtime metrics. You can // customize which metrics are collected using this commented line, but // we recommend collecting everything because you never know when you'll // need it: //service.MetricsOptions.GetCollector = () => // DotNetRuntimeStatsBuilder // .Customize() // .WithContentionStats() // .WithJitStats() // .WithThreadPoolSchedulingStats() // .WithThreadPoolStats() // .WithGcStats() // .WithExceptionStats() // .StartCollecting(); await new MyService().RunAsync(); } } public class MyService : NeonService { public static Counter runTimeCounter = Metrics.CreateCounter("run-time", "Service run time in seconds."); public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { while (!Terminator.CancellationToken.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(1)); runTimeCounter.Inc(); } return 0; } } }
SERVICE: FULL MEAL DEAL!
Here's a reasonable template you can use to begin implementing your service projects with all features enabled:
using System; using System.Collections.Generic; using System.Text; using System.Threading.Tasks; using Neon.Common; using Neon.Service; using Prometheus; // From the [prometheus-net] nuget package using Prometheus.DotNetRuntime; // From the [prometheus-net.DotNetRuntime] nuget package namespace Service_FullMealDeal { public static class Program { public static async Task Main(string[] args) { var service = new MyService(); service.Dependencies.Uris.Add(new Uri("http://waitforthis.com")); service.MetricsOptions.Mode = MetricsMode.Scrape; service.MetricsOptions.GetCollector = () => DotNetRuntimeStatsBuilder .Default() .StartCollecting(); await new MyService().RunAsync(); } } public class MyService : NeonService { public static Counter runTimeCounter = Metrics.CreateCounter("run-time", "Service run time in seconds."); public MyService() : base("my-service") { } /// <inheritdoc/> protected override void Dispose(bool disposing) { base.Dispose(disposing); } protected async override Task<int> OnRunAsync() { while (!Terminator.CancellationToken.IsCancellationRequested) { await Task.Delay(TimeSpan.FromSeconds(1)); runTimeCounter.Inc(); } return 0; } } }