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))
                }
            });


No comments:

Post a Comment

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...