Thursday, August 20, 2020

Custom HealthCheck owin Middleware in Asp.net

In my previous article, I have explain Asp.net owin middleware in detail.  

Here is the example of Custom Health Check Owin middleware in Asp.net :- 

We will take three different type of health check middleware pipeline example   

public static class AppBuilderExtensions
    {
        public static IAppBuilder UseHealthCheck(this IAppBuilder app, string route, HealthCheckConfiguration config = null)
        {
            config = config ?? new HealthCheckConfiguration();
            return app.Map(route, x => x.Use<HealthCheckMiddleware>(config));
        }
    }
 public class HealthCheckConfiguration
    {
        public TimeSpan Timeout { get; set; }

        public IList<ICheckHealth> HealthChecks { get; set; }

        public HealthCheckConfiguration(TimeSpan timeout = default(TimeSpan), IList<ICheckHealth> healthchecks = null)
        {
            Timeout = timeout;
            HealthChecks = healthchecks;
        }
    }
 public interface ICheckHealth
    {
        string Name { get; }
        Task<HealthCheckStatus> Check();
    }
public sealed class HealthCheckStatus
    {
        public string Message { get; set; }

        public bool HasFailed { get; set; }

        public HealthCheckStatus(string message, bool hasFailed)
        {
            Message = message;
            HasFailed = hasFailed;
        }

        public static HealthCheckStatus Failed(string message = null)
        {
            return new HealthCheckStatus(message ?? "Failed", true);
        }

        public static HealthCheckStatus Passed(string message = null)
        {
            return new HealthCheckStatus(message ?? "Success", false);
        }
    }
 public class HealthCheckMiddleware : OwinMiddleware
    {
        private IList<ICheckHealth> _healthChecks;
        private readonly TimeSpan _timeout;

        public HealthCheckMiddleware(OwinMiddleware next, HealthCheckConfiguration config) : base(next)
        {
            _healthChecks = (config.HealthChecks ?? Enumerable.Empty<ICheckHealth>()).ToArray(); ;
            _timeout = config.Timeout;
        }

        public override async Task Invoke(IOwinContext context)
        {
            if (!string.IsNullOrEmpty(context.Request.Path.Value))
            {
                await Next.Invoke(context);
                return;
            }
            //var queryParameters = context.Request.GetQueryParameters();
            //var debug = false;
            //if (queryParameters.ContainsKey("debug"))
            //    bool.TryParse(queryParameters["debug"], out debug);

            var checkTasks = _healthChecks.Select(async x =>
            {
                try
                {
                    return new
                    {
                        Name = x.Name,
                        Status = await x.Check().ConfigureAwait(false)
                    };
                }
                catch (Exception e)
                {
                    return new
                    {
                        Name = x.Name,
                        Status = HealthCheckStatus.Failed(e.Message)
                    };
                }
            });

            var allTasks = Task.WhenAll(checkTasks.ToArray());
            if (allTasks != await Task.WhenAny(allTasks, Task.Delay(_timeout)).ConfigureAwait(false))
            {
                context.Response.StatusCode = (int)HttpStatusCode.GatewayTimeout;
            }
            else
            {
                var results = allTasks.Result;
                var hasFailed = results.Any(x => x.Status.HasFailed);
                context.Response.StatusCode = hasFailed ? (int)HttpStatusCode.ServiceUnavailable : (int)HttpStatusCode.OK;

                var sb = new StringBuilder();
                foreach (var r in results)
                    sb.AppendLine(r.Name + ": " + r.Status.Message);
                await context.Response.WriteAsync(sb.ToString());

            }

        }
    } 
public class ComponentHealth
    {
        public string ComponentName { get; set; }

        public ComponentStatus StatusName
        {
            get { return Status; }
        }

        public ComponentStatus Status { get; set; }

        public ComponentHealth(string componentName, ComponentStatus status)
        {
            ComponentName = componentName;
            Status = status;
        } 
public abstract class BaseHealthCheck : ICheckHealth
    {
        public string Name { get; set; }
        public HealthStatus CheckHealth()
        {
            return new HealthStatus()
            {
                Components = new[] { new ComponentHealth("Universe", ComponentStatus.Weird) }
            };
        }
        public abstract Task<HealthCheckStatus> Check();

    }

   } 

public enum ComponentStatus

    {

        Healthy,    // Ok

        Fatal,      // Error

        Weird       // Might be problems

    }

public class HealthStatus

    {

        public IEnumerable<ComponentHealth> Components { get; set; }

    }

1. HttpHealthCheck  :- This pipeline will use to check the http client health check in Asp.net pipeline

public class HttpHealthCheck : BaseHealthCheck
    {
        private readonly Uri _uri;
        private readonly ICredentials _credentials;

        public HttpHealthCheck(string name, Uri uri, ICredentials credentials = null)
        {
            Name = name;
            _uri = uri;
            _credentials = credentials;
        }

        public override async Task<HealthCheckStatus> Check()
        {
            var request = (HttpWebRequest)WebRequest.Create(_uri);
            request.UserAgent = "Owin.HealthCheck";
            request.Method = "GET";
            request.Credentials = _credentials;

            using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
            {
                var statusCode = (int)response.StatusCode;
                if (statusCode >= 200 && statusCode < 300)
                    return HealthCheckStatus.Passed(response.StatusDescription);
                else
                    return HealthCheckStatus.Failed(response.StatusDescription);
            }
        }
    }

2. Ping Health Check Pipeline:- This pipeline is used to check the server health while process the request

 public class PingHealthCheck : BaseHealthCheck
    {
        private readonly string _host;
        private readonly TimeSpan _timeout;

        public PingHealthCheck(string name, string host, TimeSpan timeout)
        {
            if (string.IsNullOrEmpty(host))
                throw new ArgumentException(string.Format("host cannot be empty {0}", name));
            Name = name;
            _host = host;
            _timeout = timeout;
        }

        public override async Task<HealthCheckStatus> Check()
        {
            var ping = new Ping();
            var result = await ping.SendPingAsync(_host, (int)_timeout.TotalMilliseconds).ConfigureAwait(false);
            return new HealthCheckStatus(result.Status.ToString(), result.Status != IPStatus.Success);
        }
    }

3. Sql Health Check :- This pipeline will use to check the sql db health check in Asp.net framework

public class SqlHealthCheck : BaseHealthCheck
    {
        private readonly string _connectionString;

        public SqlHealthCheck(string name, string connectionString)
        {
            if (string.IsNullOrEmpty(connectionString))
                throw new ArgumentException(string.Format("invalid connection string {0}",connectionString));
            Name = name;
            _connectionString = connectionString;
        }

        public override async Task<HealthCheckStatus> Check()
        {
            using (var connection = new SqlConnection(_connectionString))
            {
                await connection.OpenAsync().ConfigureAwait(false);
                var cmd = new SqlCommand("select 1", connection);
                await cmd.ExecuteScalarAsync().ConfigureAwait(false);
                return HealthCheckStatus.Passed();
            }
        }
    }

To register the middleware you need to pass the type 

 app.UseHealthCheck("/healthcheck", new HealthCheckConfiguration
            {
                Timeout = TimeSpan.FromSeconds(20),
                HealthChecks = new List<ICheckHealth>()
                {
                     new HttpHealthCheck("Google Check", new Uri("https://www.google.com")),
                     new PingHealthCheck("Local Ping", "localhost", TimeSpan.FromSeconds(10))
                }
            });


Custom Owin Middleware in Asp.Net PipeLine

Before starting the fundamental about Owin Middleware, Let me start with Owin (Open web interface) in .net.

Owin come in picture when Asp.net decoupled web app and web servers.It defines a standard way for middleware to be used in a pipeline to handle requests and associated responses .

Owin Middleware :- This is a component that could be use to control request and response in Asp.net.

A good takeaway from the above is that the Next property is optional, meaning we can stop the pipeline ourselves by simply not invoking the next OwinMiddleware.

To implement OwinMiddleware we will need a constructor that calls the base constructor and an implementation of the Invoke method. Let's spin up an example.

  Required Packages

Install-Package Microsoft.Owin.Host.SystemWeb

The abstract OwinMiddleware class looks like this:

/// <summary>
/// An abstract base class for a standard middleware pattern.
/// </summary>
public abstract class OwinMiddleware {

    /// <summary>
    /// Instantiates the middleware with an optional pointer to the next component.
    /// </summary>
    /// <param name="next"></param>
    protected OwinMiddleware(OwinMiddleware next) {
        Next = next;
    }

    /// <summary>
    /// The optional next component.
    /// </summary>
    protected OwinMiddleware Next { get; set; }

    /// <summary>
    /// Process an individual request.
    /// </summary>
    /// <param name="context"></param>
    /// <returns></returns>
    public abstract Task Invoke(IOwinContext context);
}

This is then registered like in StartUp class

app.Use(typeof(MyMiddlewareClass));

Here is the example of Custom Health Check Owin middleware in Asp.net :- 

We will take two different type of health check middleware pipeline example   

public static class AppBuilderExtensions
    {
        public static IAppBuilder UseHealthCheck(this IAppBuilder app, string route, HealthCheckConfiguration config = null)
        {
            config = config ?? new HealthCheckConfiguration();
            return app.Map(route, x => x.Use<HealthCheckMiddleware>(config));
        }
    }
 public class HealthCheckConfiguration
    {
        public TimeSpan Timeout { get; set; }

        public IList<ICheckHealth> HealthChecks { get; set; }

        public HealthCheckConfiguration(TimeSpan timeout = default(TimeSpan), IList<ICheckHealth> healthchecks = null)
        {
            Timeout = timeout;
            HealthChecks = healthchecks;
        }
    }
 public interface ICheckHealth
    {
        string Name { get; }
        Task<HealthCheckStatus> Check();
    }
public sealed class HealthCheckStatus
    {
        public string Message { get; set; }

        public bool HasFailed { get; set; }

        public HealthCheckStatus(string message, bool hasFailed)
        {
            Message = message;
            HasFailed = hasFailed;
        }

        public static HealthCheckStatus Failed(string message = null)
        {
            return new HealthCheckStatus(message ?? "Failed", true);
        }

        public static HealthCheckStatus Passed(string message = null)
        {
            return new HealthCheckStatus(message ?? "Success", false);
        }
    }
 public class HealthCheckMiddleware : OwinMiddleware
    {
        private IList<ICheckHealth> _healthChecks;
        private readonly TimeSpan _timeout;

        public HealthCheckMiddleware(OwinMiddleware next, HealthCheckConfiguration config) : base(next)
        {
            _healthChecks = (config.HealthChecks ?? Enumerable.Empty<ICheckHealth>()).ToArray(); ;
            _timeout = config.Timeout;
        }

        public override async Task Invoke(IOwinContext context)
        {
            if (!string.IsNullOrEmpty(context.Request.Path.Value))
            {
                await Next.Invoke(context);
                return;
            }
            //var queryParameters = context.Request.GetQueryParameters();
            //var debug = false;
            //if (queryParameters.ContainsKey("debug"))
            //    bool.TryParse(queryParameters["debug"], out debug);

            var checkTasks = _healthChecks.Select(async x =>
            {
                try
                {
                    return new
                    {
                        Name = x.Name,
                        Status = await x.Check().ConfigureAwait(false)
                    };
                }
                catch (Exception e)
                {
                    return new
                    {
                        Name = x.Name,
                        Status = HealthCheckStatus.Failed(e.Message)
                    };
                }
            });

            var allTasks = Task.WhenAll(checkTasks.ToArray());
            if (allTasks != await Task.WhenAny(allTasks, Task.Delay(_timeout)).ConfigureAwait(false))
            {
                context.Response.StatusCode = (int)HttpStatusCode.GatewayTimeout;
            }
            else
            {
                var results = allTasks.Result;
                var hasFailed = results.Any(x => x.Status.HasFailed);
                context.Response.StatusCode = hasFailed ? (int)HttpStatusCode.ServiceUnavailable : (int)HttpStatusCode.OK;

                var sb = new StringBuilder();
                foreach (var r in results)
                    sb.AppendLine(r.Name + ": " + r.Status.Message);
                await context.Response.WriteAsync(sb.ToString());

            }

        }
    } 


1. HttpHealthCheck  :- This pipeline will use to check the http client health check in Asp.net pipeline

public class HttpHealthCheck : BaseHealthCheck
    {
        private readonly Uri _uri;
        private readonly ICredentials _credentials;

        public HttpHealthCheck(string name, Uri uri, ICredentials credentials = null)
        {
            Name = name;
            _uri = uri;
            _credentials = credentials;
        }

        public override async Task<HealthCheckStatus> Check()
        {
            var request = (HttpWebRequest)WebRequest.Create(_uri);
            request.UserAgent = "Owin.HealthCheck";
            request.Method = "GET";
            request.Credentials = _credentials;

            using (var response = (HttpWebResponse)await request.GetResponseAsync().ConfigureAwait(false))
            {
                var statusCode = (int)response.StatusCode;
                if (statusCode >= 200 && statusCode < 300)
                    return HealthCheckStatus.Passed(response.StatusDescription);
                else
                    return HealthCheckStatus.Failed(response.StatusDescription);
            }
        }
    }

2. Ping Health Check Pipeline:- This pipeline is used to check the server health while process the request

 public class PingHealthCheck : BaseHealthCheck
    {
        private readonly string _host;
        private readonly TimeSpan _timeout;

        public PingHealthCheck(string name, string host, TimeSpan timeout)
        {
            if (string.IsNullOrEmpty(host))
                throw new ArgumentException(string.Format("host cannot be empty {0}", name));
            Name = name;
            _host = host;
            _timeout = timeout;
        }

        public override async Task<HealthCheckStatus> Check()
        {
            var ping = new Ping();
            var result = await ping.SendPingAsync(_host, (int)_timeout.TotalMilliseconds).ConfigureAwait(false);
            return new HealthCheckStatus(result.Status.ToString(), result.Status != IPStatus.Success);
        }
    }

To register the middleware you need to pass the type 

 app.UseHealthCheck("/healthcheck", new HealthCheckConfiguration
            {
                Timeout = TimeSpan.FromSeconds(20),
                HealthChecks = new List<ICheckHealth>()
                {
                     new HttpHealthCheck("Google Check", new Uri("https://www.google.com")),
                     new PingHealthCheck("Local Ping", "localhost", TimeSpan.FromSeconds(10))
                }
            });


Kafka setup in window

Here's a step-by-step guide on how to do it : 1. Prerequisites : Before you begin, make sure you have the following prerequisites inst...