AppBeat.io Blog

Uptime and performance monitoring made easy

How to create status page for your app or website using your domain

AppBeat website monitoring allowed you to easily create public status page for a long time. This page is hosted on our domain by using URL in following format: https://appbeat.io/status/YOUR_STATUS_PAGE_ID

However some users wanted more:

  • hosting public status page on their own domain
  • use their own CSS styling

We are happy to announce that newest AppBeat allows you all that!

Method 1: creating public status page on your domain by using simple static HTML template and JavaScript

If you navigate to "AppBeat / Public status / Dashboard" you will be able to enable your status page from there and publish on your web server static HTML file, similar to this:

<html>
   <head>
      <title>Your title</title>
      <!--Please copy css style to your web server because we could rename it in future-->
      <link rel="stylesheet" type="text/css" href="https://appbeat.io/assets/css/status-page.css" />
   </head>
   <body>
      <!--AppBeat status page is rendered into element with id appbeat-status-container-->
      <div id="appbeat-status-container" class="status-layout"></div>

      <!--Load AppBeat status page script for your page and start rendering it-->
      <script src="https://web1.appbeat.io/embed-status/YOUR_STATUS_PAGE_ID"></script>
      <!--Tip: your script is also duplicated on https://web2.appbeat.io which is hosted in different region-->
   </body>
</html>

Every time your visitor navigates to this page it will show him/her latest status of your page.

Method 2: creating public status page on your domain by getting embeddable HTML fragment on your server side

If you don't want to use JavaScript based solution for your status page, you can call following endpoints on your web server to get HTML fragment which can be embedded in your HTTP response:


Both methods can be styled with your own CSS or you can use our default style located at: https://appbeat.io/assets/css/status-page.css

Good thing about this solution is also, that you don't have to change DNS settings to get it to work. Simply publish static HTML file on your web server or embed our HTML fragment if you use dynamic pages. And that is it!

If you would like to try this feature, you can register for new free account and please contact us so we can enable your 14-day risk free trial.

Please note, that for limited time we offer you 60% discount on Starter plan.

How to build .NET Microservices

We would like to share with you very interesting NDC Conference session from Sydney which describes pros and cons of a microservices solution and how you can build one using .NET and a number of open source tools like EventStore, RabbitMq and Redis.

Presentation was done by Richard Banks and you can watch it on YouTube:

C# method for very fast and efficient traceroute (network diagnostic tool)

We like C#, .NET and .NET Core!

Last time we played a little bit with traceroute utility and thought, could we write something fast, efficient, and since we love Asynchronous Programming with async and await (C#), something that would include this pattern as well?

Here is what we came up with :)

using System;
using System.Diagnostics;
using System.Diagnostics.Contracts;
using System.IO;
using System.Net.NetworkInformation;
using System.Text;
using System.Threading.Tasks;

namespace AppBeat.Core.Utility
{
    public class TraceRoute
    {
        private const string DATA = "AppBeat Monitoring - https://appbeat.io - AppBeat Monitoring"; //60 Bytes
        private static readonly byte[] _buffer = Encoding.ASCII.GetBytes(DATA);
        private const int MAX_HOPS = 15;
        private const string STR_REQUEST_TIMEOUT = "Request timed out.";
        private const string STR_REQUEST_TIME_NA = "*";
        private const int REQUEST_TIMEOUT = 4000;

        /// <summary>
        /// Runs traceroute and writes result to console.
        /// </summary>
        public static async Task TryTraceRouteAsync(string hostNameOrAddress)
        {
            EnsureCommonArguments(hostNameOrAddress);
            Contract.EndContractBlock();

            using (var console = Console.OpenStandardOutput())
            using (var sw = new StreamWriter(console))
            {
                await TryTraceRouteAsync(hostNameOrAddress, sw);
            }
        }

        /// <summary>
        /// Runs traceroute and writes result to provided stream.
        /// </summary>
        public static async Task TryTraceRouteAsync(string hostNameOrAddress, StreamWriter outputStreamWriter)
        {
            EnsureCommonArguments(hostNameOrAddress);
            if (outputStreamWriter == null)
            {
                throw new ArgumentNullException(nameof(outputStreamWriter));
            }
            Contract.EndContractBlock();

            await outputStreamWriter.WriteLineAsync($"traceroute to {hostNameOrAddress}, {MAX_HOPS} hops max, {_buffer.Length} byte packets");

            //dispatch parallel tasks for each hop
            var arrTraceRouteTasks = new Task<TraceRouteResult>[MAX_HOPS];
            for (int zeroBasedHop = 0; zeroBasedHop < MAX_HOPS; zeroBasedHop++)
            {
                arrTraceRouteTasks[zeroBasedHop] = TryTraceRouteInternalAsync(hostNameOrAddress, zeroBasedHop);
            }

            //and wait for them to finish
            await Task.WhenAll(arrTraceRouteTasks);

            //now just collect all results and write them to output stream
            for (int hop = 0; hop < MAX_HOPS; hop++)
            {
                var traceTask = arrTraceRouteTasks[hop];
                if (traceTask.Status == TaskStatus.RanToCompletion)
                {
                    var res = traceTask.Result;
                    await outputStreamWriter.WriteLineAsync(res.Message);

                    if (res.IsComplete)
                    {
                        //trace complete
                        break;
                    }
                }
                else
                {
                    await outputStreamWriter.WriteLineAsync($"Could not get result for hop #{hop + 1}");
                }
            }
        }

        private static void EnsureCommonArguments(string hostNameOrAddress)
        {
            if (hostNameOrAddress == null)
            {
                throw new ArgumentNullException(nameof(hostNameOrAddress));
            }

            if (string.IsNullOrWhiteSpace(hostNameOrAddress))
            {
                throw new ArgumentException("Hostname or address is required", nameof(hostNameOrAddress));
            }
        }

        public class TraceRouteResult
        {
            public TraceRouteResult(string message, bool isComplete)
            {
                Message = message;
                IsComplete = isComplete;
            }

            public string Message
            {
                get; private set;
            }

            public bool IsComplete
            {
                get;private set;
            }
        }

        public static async Task<TraceRouteResult> TryTraceRouteInternalAsync(string hostNameOrAddress, int zeroBasedHop)
        {
            using (Ping pingSender = new Ping())
            {
                var hop = zeroBasedHop + 1;

                PingOptions pingOptions = new PingOptions();
                Stopwatch stopWatch = new Stopwatch();
                pingOptions.DontFragment = true;
                pingOptions.Ttl = hop;

                stopWatch.Start();

                PingReply pingReply = await pingSender.SendPingAsync(
                    hostNameOrAddress,
                    REQUEST_TIMEOUT,
                    _buffer,
                    pingOptions
                );

                stopWatch.Stop();

                var elapsedMilliseconds = stopWatch.ElapsedMilliseconds;

                string pingReplyAddress;
                string strElapsedMilliseconds;

                if (pingReply.Status == IPStatus.TimedOut)
                {
                    pingReplyAddress = STR_REQUEST_TIMEOUT;
                    strElapsedMilliseconds = STR_REQUEST_TIME_NA;
                }
                else
                {
                    pingReplyAddress = pingReply.Address.ToString();
                    strElapsedMilliseconds = $"{elapsedMilliseconds.ToString(System.Globalization.CultureInfo.InvariantCulture)} ms";
                }

                var traceResults = new StringBuilder();
                traceResults.Append(hop.ToString(System.Globalization.CultureInfo.InvariantCulture).PadRight(4, ' '));
                traceResults.Append(strElapsedMilliseconds.PadRight(10, ' '));
                traceResults.Append(pingReplyAddress);

                return new TraceRouteResult(traceResults.ToString(), pingReply.Status == IPStatus.Success);
            }
        }
    }
}

Method above basically dispatches 15 parallel (asynchronous) tasks and waits for their result. For this reason it is really fast compared to standard implementation, which would do only one step at a time, and wait for each step (15 times in a row).

Hopefully you will find this useful :)

Better website monitoring experience for your team

We deployed our upgraded rule engine. You can now have more descriptive notifications for Warning and Error rules, by combining them together with custom note:

[trigger_rule: my notification message: why so slow???]
%RESPONSE_TIME% > 10000

[trigger_rule: server X needs restart???]
%STATUS% = 503 AND MATCH(%RESPONSE%, "YOUR_REGULAR_EXPRESSION")

If conditions for trigger are met, you will automatically receive notification with your note. This may be useful in critical situtations for your team, so they can respond more quickly!

Happy monitoring!

Coming soon: multi expression rules for better monitoring experience

Very soon we will add multi expression support for Warning and Error notification rules. Syntax will be as following:

[trigger_rule: YOUR_RULE_NAME_1]
EXPRESSION(S)_1

[trigger_rule: YOUR_RULE_NAME_2]
EXPRESSION(S)_2

[trigger_rule: YOUR_RULE_NAME_N]
EXPRESSION(S)_N

On user interface this will look something like this:

This example rule then triggers "Warning" notification which is sent to you and it is also visible in monitor log:

 

How to periodically run SSH commands and trigger AppBeat alert

In AppBeat 1.4.5 we introduced support for running SSH commands where you can trigger monitoring alert by checking result of:

  • command exit status code (for example you can simply trigger error if exit status code is not 0)
  • checking specific text in standard output (stdout) by running regex expression
  • checking specific text in standard error (stderr) by running regex expression

You can add new SSH monitor by clicking "Add new periodic monitor", enter monitor name and from "Agent type" select "Run Secure Shell (SSH) command":

On next step enter SSH hostname or server IP address and type command you would like to run (for example script name or cat /proc/uptime, ...) and specify server authentication. You can use standard username/password authentication or key based authentication (with private key in OpenSSH format).

After you finished "New periodic check" wizard we will immediately start monitoring your SSH server by running your command and checking for expected result.

Happy monitoring!

p.s. if you don't have AppBeat account yet, you can create one for free!

How to detect if Azure SQL database is in primary or secondary role?

If you want to have highly available Azure SQL Database it is highly recommended that you configure Active Geo-Replication (this feature is available for all databases in all service tiers).

Active Geo-Replication consists of two (or more) databases in different regions, where one database is primary (read + write operations) and all other databases are secondaries (allowed only read operations). Secondary databases are automatically synced with changes in your primary database.

Failover event can be triggered manually or if disastrous event happens in one of Azure datacenters. In this case your primary database goes into secondary mode and one of your secondaries becomes new primary database. If you are accessing your databases directly by using connection string, you will have to update it to reflect primary / secondary database switch.

Of course, we want to update our connection string automatically and we will show you two C# code snippets how to detect which database is currently primary (in our examples we operate with one primary and one secondary database).

Example 1

Run special query on each database to determine if it is in primary or secondary role:

public static async Task<string> GetPrimaryConnectionString()
{
	bool? isPrimary = await IsPrimaryAsync(YOUR_CONNECTION_STRING_DB1);
	if (isPrimary == true)
	{
		return YOUR_CONNECTION_STRING_DB1;
	}
	
	isPrimary = await IsPrimaryAsync(YOUR_CONNECTION_STRING_DB2);
	if (isPrimary == true)
	{
		return YOUR_CONNECTION_STRING_DB2;
	}
	
	//could not determine which database is primary
	return null;
}

private static async Task<bool?> IsPrimaryAsync(string connectionString)
{
	try
	{
		using (var conn = new SqlConnection(connectionString))
		{
			if (conn.State != System.Data.ConnectionState.Open)
			{
				await conn.OpenAsync();
			}

			using (var cmd = conn.CreateCommand())
			{
				cmd.CommandText = "SELECT role FROM sys.dm_geo_replication_link_status";
				var res = await cmd.ExecuteScalarAsync();
				return Convert.ToInt32(res) == 0;
			}
		}
	}
	catch (Exception ex)
	{
		//handle exception
		return null;
	}
}

Example 2

If you try to update database which is currently in secondary role you will get following SqlException: Failed to update database "YOUR_DB_NAME" because the database is read-only.

You can use this information to update your connection string:

try
{
	//your code
}
catch (SqlException sqlException)
{
	if (IsReadOnlyDatabase(sqlException) == true)
	{
		//current connection string probably points to secondary database (sqlException.Server)
		//update your connection string to your other database
	}
}

private static bool? IsReadOnlyDatabase(SqlException se)
{
	if (se?.Errors == null) return null;

	foreach (SqlError error in se.Errors)
	{
		if (error.Number == 3906)
		{
			return true;
		}
	}

	return false;
}

 

How to send service outage alerts to your Slack channel?

Want to integrate your website monitoring notifications with Slack? Please follow these instructions.

Step 1

From AppBeat dashboard, click on Account in the left navigation and then choose Integrations from tab menu.

Step 2

Press the Slack button which opens Add notification channel dialog. Enter arbitrary name for this channel and your Slack webhook URL.

To make sure that everything works please click Test button which should send test notification to your Slack channel. If you don't receive any message we suggest that you double check your webhook URL. When everything looks OK you can click Add button.

Step 3

Click on Services & Checks in the left navigation and double click on service for which you want to enable Slack notifications. In Edit service dialog, please click on Notification types tab. Enable your slack channel and click Save. You can repeat this step for your other services.

 

Backup web client for high availability

Some of our users are constantly using our "Live status" page by their monitoring teams. Primarily for this reason we are introducing secondary backup web client which can be used to monitor "Live status" page even if there is issue or outage of primary web client. Backup web client is actively running on different datacenter and can be accessed by navigating to web2.appbeat.io (this is also displayed in application footer).

Is this feature available for our free users? Of course, this feature is available for all our users! If you are not yet member, you can register here for free.

We are one of rare monitoring provider to offer such feature to their users!

---

High level diagram of current AppBeat architecture: