Creating the LogService

Build a LogService that appends one time-stamped line per entry to a file named for the day, such as logs\errors\20260314.log. It asks for an IClock so tests can choose the date, and it never lets a logging problem crash the app.

Journeyman · C# · Updated

After this lesson you can

  • Write a LogService that appends time-stamped entries to a log file named for the day
  • Format dates and times with custom format strings such as yyyyMMdd and HH:mm:ss

Key takeaways

  • In a custom format string, MM is the month and mm the minute; HH is the 24-hour clock and hh the 12-hour one.
  • yyyyMMdd makes a good log file name, such as 20260314.log: no spaces or slashes, and sorting by name sorts by date.
  • The LogService appends one time-stamped line per entry with File.AppendAllText, so every entry is kept and each day gets a new file.
  • It asks for an IClock, so a test can pass a fixed clock and know exactly which file name and time to expect.
  • Swallowing an exception is almost always a mistake. A logger is the exception to the rule, and it still catches only IOException.

Here's the plan for the LogService:

  • It's given a folder, such as logs\errors, when it's created.
  • Each day gets its own file, named for the date: 20260314.log.
  • Each entry is one line: the date, the time and the message.
2026-03-14 09:16:41 Could not find student_grades.csv

Formatting dates

Both the file name and the entry need a date in a fixed shape. A custom format string describes the shape, letter by letter:

Letters Mean March 14, 2026, at 9:16:41 PM
yyyy 4-digit year 2026
MM 2-digit month (capital M) 03
dd 2-digit day 14
HH 2-digit hour, 0–23 (capital H) 21
hh 2-digit hour, 1–12 09
mm 2-digit minute (small m) 16
ss 2-digit second 41

Use them with ToString, or after a colon inside { }:

DateTime time = new DateTime(2026, 3, 14, 21, 16, 41);

Console.WriteLine(time.ToString("yyyyMMdd"));
Console.WriteLine(quot;{time:yyyy-MM-dd HH:mm:ss}");
Console.WriteLine(quot;{time:hh:mm}");
20260314
2026-03-14 21:16:41
09:16

Two traps: MM is the month and mm the minute; HH is the 24-hour clock and hh the 12-hour one. A log needs HH: with hh, 9 in the morning and 9 at night look the same.

yyyyMMdd makes a good file name: no spaces or slashes, and sorting the files by name sorts them by date.

The LogService

public class LogService
{
    private readonly string directory;
    private readonly IClock clock;

    public LogService(string directory, IClock clock)
    {
        this.directory = directory;
        this.clock = clock;
    }

    // Today's file, such as logs\errors\20260314.log.
    public string CurrentFilePath => Path.Combine(directory, quot;{clock.Now:yyyyMMdd}.log");

    public void Log(string message)
    {
        try
        {
            Directory.CreateDirectory(directory);
            File.AppendAllText(CurrentFilePath, quot;{clock.Now:yyyy-MM-dd HH:mm:ss} {message}{Environment.NewLine}");
        }
        catch (IOException)
        {
            // A logging problem must never crash the app, so it's ignored.
        }
    }
}

Piece by piece:

  • IClock is the same idea as the clock interface in Abstraction and Interfaces, but this one gives the whole date and time, DateTime Now, instead of just the hour, because a log needs both. The app passes a real clock; a test passes a fixed one and knows exactly which file name and time to expect.
  • CurrentFilePath builds today's file name with Path.Combine and yyyyMMdd. When the date changes, the next entry goes to a new file by itself.
  • Directory.CreateDirectory makes the folder the first time. It does nothing when the folder is already there.
  • File.AppendAllText appends, never replaces: every entry is kept, and the day's first entry creates the file.

Swallowing exceptions, on purpose

Look at the catch: it does nothing. That's called swallowing an exception, and it's almost always a mistake, because it hides problems. A logger is the exception to the rule. If the log file can't be written (the disk is full, or another program has the file locked), the app should carry on with its real work. Crashing it because a log line failed would turn a small problem into a big one.

Even so, it catches only IOException, the file problems it expects. A bug in the logger itself still shows up.

Trying it

With a fixed clock, every run gives the same result:

LogService log = new LogService(Path.Combine("logs", "application"), new FixedClock(new DateTime(2026, 3, 14, 9, 15, 2)));
log.Log("Started");
log.Log("Loaded 128 students");

Console.WriteLine(log.CurrentFilePath);
Console.WriteLine(File.ReadAllText(log.CurrentFilePath));

public interface IClock
{
    DateTime Now { get; }
}

public class FixedClock : IClock
{
    public DateTime Now { get; }

    public FixedClock(DateTime now)
    {
        Now = now;
    }
}

public class LogService
{
    private readonly string directory;
    private readonly IClock clock;

    public LogService(string directory, IClock clock)
    {
        this.directory = directory;
        this.clock = clock;
    }

    public string CurrentFilePath => Path.Combine(directory, quot;{clock.Now:yyyyMMdd}.log");

    public void Log(string message)
    {
        try
        {
            Directory.CreateDirectory(directory);
            File.AppendAllText(CurrentFilePath, quot;{clock.Now:yyyy-MM-dd HH:mm:ss} {message}{Environment.NewLine}");
        }
        catch (IOException)
        {
            // A logging problem must never crash the app, so it's ignored.
        }
    }
}
logs\application\20260314.log
2026-03-14 09:15:02 Started
2026-03-14 09:15:02 Loaded 128 students

Check yourself

2 questions to try on the lesson page.

So what

A logger is a few lines: a folder, a file per day, and an append per entry. Inject the clock so it can be tested, and never let it crash the app it's there to help.

Key terms

Cheat sheets