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:
Field | Type | Description |
---|---|---|
Id | Text | Unique identifier (GUID). |
Time | Timestamp | The current date and time when the log entity was created. |
App | Text | The name of the app. When several apps log simultaneously, the app name helps isolate the activity in a specific app. |
Where | Text | Specifies where in the app the log entity was created. Often, the name of the method writing the log entity. |
Session | Text | Specifies 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. |
Environment | Text | Specifies the name of the environment. |
Machine | Text | Specifies the machine name. |
User | Text | The user account running the app. |
Text | Text | Describes the event logged. |
Details | Text | Can be sued to echo method arguments or local variables that supplement the description in the Text field. |
Status | Number | Defaults to 0 (Info) or 3 (Success). Can be changed to indicate other results. |
StatusText | Text | Depends on the Status field: 0: Info 1: Warning 2: Error 3: Success 4: Rejected 5: Blocked 6: Failed |
Reason | Text | Can be used to provide additional information description when the status isn’t as expected. |
Elapsed | Number | Defaults 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. |
ElapsedText | Text | The elapsed time in days, hours, minutes and seconds. |
Count | Number | Defaults to 0 (zero). Can be used to specify a count that’s relevant to the elapsed time. |
Size | Number | Defaults to 0 (zero). Can be used to specify a size that’s relevant to the elapsed time. |
Retry | Number | Defaults 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:
