Click here to Skip to main content
Click here to Skip to main content
Go to top

Using Markdown for Effective Logging

, 23 Jul 2014
Rate this:
Please Sign up or sign in to vote.
An introduction to MarkdownLog - an open source .NET library designed to produce Markdown formatted log files

The Markdown Format

The Markdown format has rapidly grown in popularity over the last 10 years. Originally conceived by John Gruber as a quick way to produce HTML markup, it has spread within the developer community to become a popular standard for producing online content.

I first heard of Markdown from Jeff Atwood's blog post when he was building StackOverflow. Since then, many apps and web pages have appeared that use the format, including GitHub, Discourse, as well as some excellent editors, such as iA Writer, MarkdownPad and the browser-based Dillinger.

It is easy to see why Markdown has become popular. Compare the difference between raw HTML and Markdown:

Comparison of HTML to Markdown source

Not only is the Markdown code easier to write, it's easier to read too. And, it can easily be converted to HTML using one of the many parsers, or apps. I'm using one of the apps, MarkdownPad, to produce this entire article.

Producing Markdown-formatted Test Results

I develop a free shopping list app for iOS. There are many other apps that help you remember what to buy in the supermarket, but I thought the world needed another shopping list app when I couldn't find one that worked how I wanted. I wanted a shopping list app that would offer suggestions as I typed, and to automatically arrange my list based on the layout of the supermarket.

The app to be tested - Shopping UK

Writing a Test Suite

I recently made some major changes to the app's suggestion and classification engine, based on user feedback from the first version.

Before making changes, I built a suite of tests that I could run during development to check I hadn't broken anything or hurt performance - basically, to protect the app from my own fallibility.

Unit Tests

In general, a unit test will test a very specific behaviour and return a PASS/FAIL result.

For example,

Assert.AreEqual(3, Math.Sqrt(9));
Assert.AreEqual(Double.NaN, Math.Sqrt(-1));

Unit tests are great for testing individual methods and classes that have well specified behaviour. I added loads of these to my suite but, to give me the extra confidence I craved, I also needed subcutaneous tests.

Subcutaneous Integration Tests

A subcutaneous test is used to show how the app performs and behaves at a level immediately below the user interface - just under its skin.

These tests would need to operate on real data, on the actual device, to produce a simulation of what users would experience. Each test would have a number of steps, corresponding to a sequence of interactions between the user and the device.

For example,

  1. User types br
  2. Expect bread to be amongst the suggestions offered
  3. User selects bread from the list of suggestions
  4. User enters Shopping Mode
  5. Expect bread to be categorised in the Bakery section

To make writing these tests easier, I built a framework so tests could be written concisely, using a fluent coding style. This was written using the same tools as the app - C#, .NET and Xamarin - which meant it could run directly on the iPhone and produce accurate, real-world timings.

Here's an example of a finished test:

tests.Run(app => app
    .Type("2 small onions").PressEnterKey()
    .Type("5 kilos of potatoes").PressEnterKey()
    .Type("br")
    .ExpectThat(x => x.SuggestionsInclude("bread"))
    .SelectSuggestion("bread")
    .StartShopping()
    .ExpectThat(x => x.CategoryContains("Bakery", new[] {"bread"}))
    .ExpectThat(x => x.CategoryContains("Fresh Fruit, Veg & Flowers",
        new[]
        {
            "2 small onions",
            "5 kilos of potatoes"
        })),
    synopsis: "Can select suggestion and categorise");

As the test runs, it records each user action, along with relevant observations, results and timing information.

This is where Markdown comes in.

As part of my testing framework, I built a library called MarkdownLog, which I've now released as open-source under the MIT license. This library is designed to make it trivial to produce Markdown formatted text from an application's data structures. Using just one line of code, a collection of .NET objects can be output as a table or list. And, because the output is Markdown, it can later be converted to HTML for publishing, if needed.

Here's an extract of a Markdown formatted log from a test run:

Summary of Test Suite Run
=========================

Suite completed on 17 July 2014 at 09:52:04 (UTC)

3 tests (223 actions) passed in **422** ms

     Result | Expected Behaviour                          | Actions | Time Taken
     ------:| ------------------------------------------- | -------:| ----------:
     PASS   | Can select suggestion and categorise        |      36 |        199
     PASS   | Common products are included in suggestions |      75 |        153 
     PASS   | Common brands are included in suggestions   |     112 |         80   

Environment
===========

     Key               | Value                               
     ----------------- | ------------------------------------ 
     HardwareModel     | iPhone 
     SystemName        | iPhone OS                           
     SystemVersion     | 7.1                                 
     AppVersion        | 1.01                                
     DeviceIdForVendor | ---

Test 1: Can select suggestion and categorise
============================================

Completed in **199** ms

*Note: Some output has been removed from here for brevity* 

Action: Type 'b'
----------------

Suggestions are shown:

     -------------
    | bread       |
    | beans       |
    | breakfast > |
     -------------

Action: Type 'r'
----------------

Suggestions are shown:

     -------------
    | bread       |
    | breakfast > |
    | British   > |
     -------------

Check: Suggestions list should include 'bread'
----------------------------------------------

PASS: 'bread' was present in the list of suggestions

Action: Choose 'bread' from suggestions list
--------------------------------------------

The shopping list now contains:

      -----------------------
     |  2 small onions       |
     |  5 kilos of potatoes  |
     |  bread                |
    >|                       |
      -----------------------

Action: Enter 'Shopping' mode
-----------------------------

Shopping mode has been entered.

Items have been categorised:

     --------------------------
    |Fresh Fruit, Veg & Flowers|
    |--------------------------|
    | [ ] 2 small onions       |
    | [ ] 5 kilos of potatoes  |
    |--------------------------|
    |Bakery                    |
    |--------------------------|
    | [ ] bread                |
     --------------------------

Check: Section 'Bakery' should contain 'bread'
----------------------------------------------

PASS: 'bread' was found in 'Bakery' section 

Check: Section 'Fresh Fruit, Veg & Flowers' should contain: '2 small onions', '5 kilos of potatoes'
---------------------------------------------------------------------------------------------------

PASS: '2 small onions' was found in 'Fresh Fruit, Veg & Flowers' section 
PASS: '5 kilos of potatoes' was found in 'Fresh Fruit, Veg & Flowers' section

Test 1 Timings (ms)
===================
      Typing '2'         |###  3
      Typing ' '         |###  3
      Typing 's'         |###  3
      Typing 'm'         |######  6
      Typing 'a'         |################################  32
      Typing 'l'         |#####  5
      Typing 'l'         |#  1
      Typing ' '         |##  2
      Typing 'o'         |##  2
      Typing 'n'         |###  3
      Typing 'i'         |#########  9
      Typing 'o'         |##  2
      Typing 'n'         |#  1
      Typing 's'         |###############  15
      Typing '5'         |  0
      Typing ' '         |#  1
      Typing 'k'         |###  3
      Typing 'i'         |#  1
      Typing 'l'         |########  8
      Typing 'o'         |##  2
      Typing 's'         |  0
      Typing ' '         |#  1
      Typing 'o'         |###  3
      Typing 'f'         |####  4
      Typing ' '         |#  1
      Typing 'p'         |####  4
      Typing 'o'         |###  3
      Typing 't'         |#####  5
      Typing 'a'         |###  3
      Typing 't'         |#  1
      Typing 'o'         |#  1
      Typing 'e'         |#########################################  41
      Typing 's'         |##  2
      Typing 'b'         |#  1
      Typing 'r'         |#  1
      Categorising items |###################  19
                         ------------------------------------------

Overall Slowest Timings
=======================

Note: timings are classified based on human perception metrics from [Jacob Nielson's article](http://www.nngroup.com/articles/powers-of-10-time-scales-in-ux/):

   * Anything that takes less than 0.1 seconds feels IMMEDIATE - as if the user 
     is directly causing something to happen.
   * Between 0.1 and 1 second is quick enough to give the user MAINTAINED-FOCUS 
     and feel in control.
   * Longer than 1 second but less than 10 seconds is acceptable for 
     MAINTAINED-ATTENTION, but the user may begin to feel impatient.
   * Generally, BROKEN-FLOW, will occur after 10 seconds and the user will
     context switch and do something else.

     Test                                        | Action             | Perception | Time (ms)
     ------------------------------------------- | ------------------ | ---------- | ---------
     Can select suggestion and categorise        | Typing 'e'         | IMMEDIATE  |        41
     Can select suggestion and categorise        | Typing 'a'         | IMMEDIATE  |        32
     Common products are included in suggestions | Typing 'g'         | IMMEDIATE  |        28
     Can select suggestion and categorise        | Categorising items | IMMEDIATE  |        19
     Common products are included in suggestions | Typing 'c'         | IMMEDIATE  |        19
     Common products are included in suggestions | Typing 'c'         | IMMEDIATE  |        19
     Can select suggestion and categorise        | Typing 's'         | IMMEDIATE  |        15
     Common products are included in suggestions | Typing 'w'         | IMMEDIATE  |        13
     Common products are included in suggestions | Typing 'b'         | IMMEDIATE  |        12
     Common products are included in suggestions | Typing 'b'         | IMMEDIATE  |        10

View as HTML

As you can see, the Markdown formatted log is very readable in its raw form so the output can be written to the Console and viewed immediately, without the need for special viewers or converters.

MarkdownLog

MarkdownLog is designed to be lightweight and simple to use. It is a .NET Portable Class Library (PCL) with no dependencies, besides the .NET Framework.

Getting Started

  1. Download MarkdownLog.dll from the build server.
  2. Add a reference to MarkdownLog.dll from your project.
  3. Add using MarkdownLog; to the top of your file.
  4. Produce Markdown output from your data.

MarkdownLog can produce all the elements described in the Markdown spec.

Create a List from a Collection

var myStrings = new[] { "John", "Paul", "Ringo", "George" };
Console.Write(myStrings.ToMarkdownBulletedList());

This produces:

   * John
   * Paul
   * Ringo
   * George

View as HTML

The collection can be of any type, not just strings. The ToString method will be called on each object to produce the list item’s text. You can also specify a function, like this:

var processes = Process.GetProcesses();
Console.Write(processes.ToMarkdownBulletedList(i => i.ProcessName));

If you'd prefer a numbered list, use the ToMarkdownNumberedList extension method instead:

var files = new DirectoryInfo(@"C:\MarkdownLog").EnumerateFiles();
Console.Write(files.ToMarkdownNumberedList(i => i.Name + " is " + i.Length + " bytes"));

This produces:

   1. appveyor.yml is 302 bytes
   2. LICENSE is 1088 bytes
   3. MarkdownLog.sln is 1480 bytes
   4. README.md is 6173 bytes

View as HTML

Create a Table

A collection of data can be output as a GitHub Flavoured Markdown Table using the ToMarkdownTable extension method.

var data = new[]
{
    new{Name = "Meryl Streep", Nominations = 18, Awards=3},
    new{Name = "Katharine Hepburn", Nominations = 12, Awards=4},
    new{Name = "Jack Nicholson", Nominations = 12, Awards=3}
};

Console.Write(data.ToMarkdownTable());

This produces:

 <code>
 Name              | Nominations | Awards
 ----------------- | -----------:| ------:
 Meryl Streep      |          18 |      3
 Katharine Hepburn |          12 |      4
 Jack Nicholson    |          12 |      3
 </code>

View as HTML

The columns are aligned automatically based on datatype. Text is left-aligned and numbers are right-aligned.

The columns to be included, and how their data is selected, can be specified using a method overload. Simply pass a function for each of the columns you need:

 var tableWithHeaders = data
            .ToMarkdownTable(i => i.Name, i => i.Nominations + i.Awards)
            .WithHeaders("Name", "Total");

 Console.Write(tableWithHeaders);

This produces:

 <code>
 Name              | Total
 ----------------- | -----:
 Meryl Streep      |    21
 Katharine Hepburn |    16
 Jack Nicholson    |    15
</code>

Create a Bar Chart

A bar chart can be created using a collection of labels and numbers.

var worldCup = new Dictionary<string, int>
{
    {"Brazil", 5},
    {"Italy", 4},
    {"Germany", 4},
    {"Argentina", 2},
    {"Uruguay", 2},
    {"France", 1},
    {"Spain", 1},
    {"England", 1}
};

Console.Write(worldCup.ToMarkdownBarChart());

This is rendered as a Markdown code block:

Brazil    |#####  5
Italy     |####  4
Germany   |####  4
Argentina |##  2
Uruguay   |##  2
France    |#  1
Spain     |#  1
England   |#  1
          ------

View as HTML

Bar charts support negative and floating point numbers, and scaling of values too:

Cos(0.0)                     |####################  1
Cos(0.3)                     |###################  0.95
Cos(0.6)                     |################  0.81
Cos(0.9)                     |############  0.59
Cos(1.3)                     |######  0.31
Cos(1.6)                     |  0
Cos(1.9)               ######|  -0.31
Cos(2.2)         ############|  -0.59
Cos(2.5)     ################|  -0.81
Cos(2.8)  ###################|  -0.95
Cos(3.1) ####################|  -1
Cos(3.5)  ###################|  -0.95
Cos(3.8)     ################|  -0.81
Cos(4.1)         ############|  -0.59
Cos(4.4)               ######|  -0.31
Cos(4.7)                     |  0
Cos(5.0)                     |######  0.31
Cos(5.3)                     |############  0.59
Cos(5.7)                     |################  0.81
Cos(6.0)                     |###################  0.95
         -----------------------------------------

View as HTML

Create a Header

Console.Write("The Origin of the Species".ToMarkdownHeader());

This produces:

The Origin of the Species
=========================

View as HTML

Create a Sub-header

Console.Write("By Means of Natural Selection".ToMarkdownSubHeader());

This produces:

By Means of Natural Selection
-----------------------------

View as HTML

Create a Word-Wrapped Paragraph

var text = "Lolita, light of my life, fire of my loins. My sin, my soul. Lo-lee-ta: the tip of the tongue taking a trip of three steps down the palate to tap, at three, on the teeth. Lo. Lee. Ta.";

Console.Write(text.ToMarkdownParagraph());

This produces:

Lolita, light of my life, fire of my loins. My sin, my soul. Lo-lee-ta: the tip 
of the tongue taking a trip of three steps down the palate to tap, at three, on 
the teeth. Lo. Lee. Ta.

View as HTML

Paragraphs are word-wrapped at the 80th column, by default, but this can be configured.

Create a Block-quote

const string text = "There are only two hard things in computer science:\n" +
                    "cache invalidation,\n" + 
                    "naming things,\n" +
                    "and off-by-one errors.";

Console.Write(text.ToMarkdownBlockquote());

This produces:

> There are only two hard things in computer science:
> cache invalidation,
> naming things,
> and off-by-one errors.

View as HTML

A block-quote can also contain other Markdown elements. For example:

var blockQuote = new Blockquote();

blockQuote.Append(new HorizontalRule());
blockQuote.Append(new Header("COMPUTING MACHINERY AND INTELLIGENCE"));
blockQuote.Append(new SubHeader("By A. M. Turing."));

blockQuote.Append(new Paragraph("..."));

blockQuote.Append(new Paragraph("The idea behind digital computers may be explained by saying that these machines are intended to carry out any operations which could be done by a human computer. The human computer is supposed to be following fixed rules; he has no authority to deviate from them in any detail. We may suppose that these rules are supplied in a book, which is altered whenever he is put on to a new job. He has also an unlimited supply of paper on which he does his calculations. He may also do his multiplications and additions on a \"desk machine,\" but this is not important."));
blockQuote.Append(new Paragraph("If we use the above explanation as a definition we shall be in danger of circularity of argument. We avoid this by giving an outline. of the means by which the desired effect is achieved. A digital computer can usually be regarded as consisting of three parts:"));

blockQuote.Append(new NumberedList("Store", "Executive unit", "Control"));

Console.Write(blockQuote);

This produces:

> --------------------------------------------------------------------------------
> 
> COMPUTING MACHINERY AND INTELLIGENCE
> ====================================
> 
> By A. M. Turing.
> ----------------
> 
> ...
> 
> The idea behind digital computers may be explained by saying that these 
> machines are intended to carry out any operations which could be done by a 
> human computer. The human computer is supposed to be following fixed rules; he 
> has no authority to deviate from them in any detail. We may suppose that these 
> rules are supplied in a book, which is altered whenever he is put on to a new 
> job. He has also an unlimited supply of paper on which he does his 
> calculations. He may also do his multiplications and additions on a "desk 
> machine," but this is not important.
> 
> If we use the above explanation as a definition we shall be in danger of 
> circularity of argument. We avoid this by giving an outline. of the means by 
> which the desired effect is achieved. A digital computer can usually be 
> regarded as consisting of three parts:
> 
>    1. Store
>    2. Executive unit
>    3. Control
> 

View as HTML

Deferring Output by Writing to a Container

So far, we've been writing each element directly to the Console window as we go. This can be useful for producing a snapshot of a specific data structure to the output window while an application is running. However, to produce a complete log, which can be saved to disk, each element should be written to a MarkdownContainer.

var log = new MarkdownContainer();

var countries = new[]{"Zimbabwe", "Italy", "Bolivia", "Finland", "Australia"};

log.Append("Countries (unsorted)".ToMarkdownHeader());
log.Append(countries.ToMarkdownNumberedList());

var sorted = countries.OrderBy(i => i);

log.Append("Countries (sorted)".ToMarkdownHeader());
log.Append(sorted.ToMarkdownNumberedList());

Console.Write(log);

This produces:

Countries (unsorted)
====================

   1. Zimbabwe
   2. Italy
   3. Bolivia
   4. Finland
   5. Australia

Countries (sorted)
==================

   1. Australia
   2. Bolivia
   3. Finland
   4. Italy
   5. Zimbabwe

View as HTML


If you'd like to learn more about MarkdownLog, please visit the Project Homepage, where you'll find a few more examples and a link to the latest source code. If you're interested in contributing, I'd very much like to hear from you!

And, if you happen to be looking for a free app for iPhone that saves you time with your weekly shop, please take a look at Shopping UK and let me know what you think Smile | :)

License

This article, along with any associated source code and files, is licensed under The MIT License

Share

About the Author

Stuart Wheelwright
Architect BlackJet Software Ltd
United Kingdom United Kingdom
Stuart Wheelwright is the Principal Architect and Software Developer at BlackJet Software Ltd.
 
He has over 16 years commercial experience producing robust, maintainable, web-based solutions and bespoke systems for Microsoft platforms.
 
His latest project is Shopping UK, an elegantly simple shopping list for iPhone.
Follow on   Twitter   Google+   LinkedIn

Comments and Discussions

 
GeneralMy vote of 5 PinprofessionalMihai MOGA13-Aug-14 1:42 
QuestionEscape Character [modified] PinmemberAlex Comerford12-Aug-14 23:28 
AnswerRe: Escape Character PinmemberStuart Wheelwright13-Aug-14 1:02 
QuestionMissing Images Pinprofessionalhschroedl31-Jul-14 1:30 
AnswerRe: Missing Images PinmemberStuart Wheelwright31-Jul-14 1:35 
GeneralMy vote of 5 PinmemberStoneFactory30-Jul-14 2:02 
QuestionSimple yet Sophisticated. Brilliant !! PinmemberSrivatsa Haridas29-Jul-14 20:27 
GeneralMy vote of 5 PinmemberHoangitk27-Jul-14 16:35 
GeneralMy vote of 5 Pinmembersdmcnitt26-Jul-14 3:27 
QuestionNice library PinmemberDylan R. E. Moonfire25-Jul-14 5:28 
AnswerRe: Nice library PinmemberStuart Wheelwright25-Jul-14 5:56 
GeneralRe: Nice library PinmemberDylan R. E. Moonfire27-Jul-14 10:30 
NewsRe: Nice library PinmemberDylan R. E. Moonfire1-Aug-14 9:27 
GeneralRe: Nice library PinmemberStuart Wheelwright1-Aug-14 10:07 
GeneralMy vote of 5 PinprofessionalVolynsky Alex24-Jul-14 10:00 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.

| Advertise | Privacy | Mobile
Web02 | 2.8.140921.1 | Last Updated 24 Jul 2014
Article Copyright 2014 by Stuart Wheelwright
Everything else Copyright © CodeProject, 1999-2014
Terms of Service
Layout: fixed | fluid