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,
MMis the month andmmthe minute;HHis the 24-hour clock andhhthe 12-hour one. yyyyMMddmakes a good log file name, such as20260314.log: no spaces or slashes, and sorting by name sorts by date.- The
LogServiceappends one time-stamped line per entry withFile.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:
IClockis 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.CurrentFilePathbuilds today's file name withPath.CombineandyyyyMMdd. When the date changes, the next entry goes to a new file by itself.Directory.CreateDirectorymakes the folder the first time. It does nothing when the folder is already there.File.AppendAllTextappends, 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.