mon

mon is a monitoring application that helps achieve observability in my other apps. It also serves to demonstrate that concept of observability, which is a key design concern I always address in solution architectures.

My apps run on remote servers, which makes it difficult to observe their activity. Logging key events and measurements enables me to observe their behavior and performance, and detect gradual changes before they cause disruption.

In a client solution I’d typically recommend writing logs to ElasticSearch (ELK) but my hosting provider doesn’t support ELK, so I’ve developed mon and some simple C# classes to generate logs from my other apps to demonstrate the concept.

To see it working, toy around with uml, and then start mon to see what was logged and measured.

Design notes

The LogDb class builds on DbContext, adding connection pooling and using either in-memory, SQL Server, MySQL or Sqlite as storage.

The LogEntity class defines the record structure of the log entities in the underlying database table.

The Log class is used to build log entities and write them to the database.

The fileds in log entries can be used as follows:

FieldTypeDescription
IdTextUnique identifier (GUID).
TimeTimestampThe current date and time when the log entity was created.
AppTextThe name of the app. When several apps log simultaneously, the app name helps isolate the activity in a specific app.
WhereTextSpecifies where in the app the log entity was created. Often, the name of the method writing the log entity.
SessionTextSpecifies the session from which the log entity was created. A web app can be used by many users simultaneously, so the session isolated log entities specific to one user’s activities.
EnvironmentTextSpecifies the name of the environment.
MachineTextSpecifies the machine name.
UserTextThe user account running the app.
TextTextDescribes the event logged.
DetailsTextCan be sued to echo method arguments or local variables that supplement the description in the Text field.
StatusNumberDefaults to 0 (Info) or 3 (Success). Can be changed to indicate other results.
StatusTextTextDepends on the Status field:
0: Info
1: Warning
2: Error
3: Success
4: Rejected
5: Blocked
6: Failed
ReasonTextCan be used to provide additional information description when the status isn’t as expected.
ElapsedNumberDefaults to 0 (zero). If a start time is given in the From() method when the log entity is initialized, this field tells the elapsed time in milliseconds until the log entry is written.
ElapsedTextTextThe elapsed time in days, hours, minutes and seconds.
CountNumberDefaults to 0 (zero). Can be used to specify a count that’s relevant to the elapsed time.
SizeNumberDefaults to 0 (zero). Can be used to specify a size that’s relevant to the elapsed time.
RetryNumberDefaults to 0 (zero). Can be used to specify the number of the attempt, which was previously blocked.

The uml Program.cs source file shows how to log key events in the web app server process:

using log;

// Track start time and assign a session id.
var start = DateTime.Now;
const string uml = "uml";
const string where = "Program.Main";
var session = Guid.NewGuid().ToString();

// Open the log database.
LogDb.UseSqlite( @"Data Source=..\db\sqlite.db" );
LogDb.GetConnection().Database.EnsureCreated();

// Log the starting of the web app.
Log.From( uml, where, session ).Text( "App starting." ).Write();

// Create the web application builder.
var builder = WebApplication.CreateBuilder( args );

// Enable API controllers.
builder.Services.AddControllers();

// Build the web application.
var app = builder.Build();

// Check if the code executes in production.
if( !app.Environment.IsDevelopment() )
{
    // Enforce the use of HTTP Strict Transport Security Protocol.
    app.UseHsts();
}

// Enforce HTTPS.
app.UseHttpsRedirection();

// Make the user's browser aware of the wwwroot folder.
app.UseDefaultFiles();

// Make the user's browser load the index.html file.
app.MapStaticAssets();

// Activate standard routing.
app.UseRouting();

// Activate API controllers.
app.MapControllers();

// Hook into application startup to log and measure startup time.
app.Lifetime.ApplicationStarted.Register( () =>
{
    // Log startup time.
    Log.From( uml, where, session, start ).Text( "App started." ).Write();
} );

// Hook into application shutdown to log and measure uptime.
app.Lifetime.ApplicationStopped.Register( () =>
{
    // Log shutdown and lifetime.
    Log.From( uml, where, session, start ).Text( "App stopped." ).Write();
} );

// Start the web application on the web server.
app.Run();

To enable logging activity in the client JavaScript code, the following method is declared in Index.html:

// Log a message.
function log( message ) {
    
    // Call the POST controller.
    fetch(location.href + "api/Log", {

        method: "POST",
        headers: {
            "Accept": "application/json",
            "Content-Type": "application/json"
        },
        body: JSON.stringify( { session : session, message : message } )
    } )
    .finally();
}

And this method is called from event handler like the one when the user presses the render button:

// Event handler called when the copy button is called.
function onCopy() {

    // Log the event.
    log( "Copy button clicked." );

    // noinspection JSIgnoredPromiseFromCall.
    navigator.clipboard.writeText( editor.getDoc().getValue() );    
}

The render event handler doesn’t log itself, but calls an API on the web app server that does the rendering – and does the logging:

// Event handler called when the render button is clicked. 
function onRender() {

    // Call the POST controller.
    fetch( location.href + "api/Render", {
        
        method: "POST",
        headers: {
            "Accept": "application/json",
            "Content-Type": "application/json"
        },
        body: JSON.stringify( { session : session, text : editor.getDoc().getValue() } )
    } )
        .then( ( response) => {
            
            // Throw exception if POST controller returns error.
            if( !response.ok ) {
                throw new Error();
            }
            
            // Pass JSON data to the next .then block.
            return response.json();    
        } )
        .then( ( json ) => {
            
            // Create a temporary DOM image element.
            const newImage = new Image();
            newImage.src = "data:image/png;base64," + json.toString();
            
            // Render the invisible temporary DOM image element.
            newImage.decode()
                .then( () => {
                    
                    // Transfer the rendered temporary image.
                    image.src = newImage.src;
                    
                    // Set the size of the visible image based on the size of the invisible image.
                    image.setAttribute( "width", newImage.naturalWidth.toString() );
                    image.setAttribute( "height", newImage.naturalHeight.toString() );
                    
                    // Render the visible DOM image element.
                    image.decode()
                        .then( () => {
                            
                            // Show the image and hide the error text.
                            error.style.visibility = "hidden";
                            image.style.visibility = "visible";
                            
                            // Delete the temporary DOM image element.
                            newImage.remove();
                        } );
                } );
        } )
        .catch( () => {
            
            // Hide the image and shor the error text.
            image.style.visibility = "hidden";
            error.style.visibility = "visible";
        } )
        .finally( () => {
            
            // Always refocus on the editor.
            editor.focus();
        } );
}

The Render API controller method logs and measures the call to the remote rendering service:

namespace uml
{
    // API controller.
    [ Route( "api/Render" ) ]
    [ ApiController ]
    public class RenderController : ControllerBase
    {
        // Create the PlantUML renderer factory.
        private static readonly RendererFactory Factory = new();
        
        // Create the renderer, which calls the external PlantUML webservice.
        private static readonly IPlantUmlRenderer Renderer = Factory.CreateRenderer( new PlantUmlSettings() );

        
        public class Args
        {
            public string? session { get; set; }
            public string? text { get; set; }
        }
    
    
        // Respond to POST request with the rendered image.
        [HttpPost]
        public IActionResult OnPost( Args args )
        {
            var start = DateTime.Now;

            var log = Log.From( "uml", "RenderController.OnPost", args.session, start )
                .Text( "Render image button clicked" ).Count( args.text.Length );

            try
            {
                // Call the external PlantUML rendering webservice.
                var bytes = Renderer.Render( args.text, OutputFormat.Png );

                log.Size( bytes.Length ).Write();
                
                // Return the rendered image data as a base 64 encoded string.
                return Ok( Convert.ToBase64String( bytes ) );
            }
            catch( Exception e )
            {
                log.Rejected()
                    .Details( e )
                    .Write();
                
                // Return a HTTP error code.
                return BadRequest();
            }
        }
    }
}

The mon app shows the most recent log entities: