朝の空気はまだ少し冷たく、通りを歩く人々の足音が静かに響いていた。建物の影から差し込む光は柔らかく、昨夜の雨を含んだ舗道がわずかに光を返している。彼は立ち止まり、深く息を吸い込んだ。急ぐ理由はなかったが、なぜか心の奥で何かに急かされているような感覚があり、その正体を掴めないまま、ただ前へ進むしかない気がしていた。
通り沿いの店はまだ多くがシャッターを下ろしたままで、看板だけが眠たげに並んでいる。古い喫茶店の窓には薄く曇りが残り、そこに映る自分の姿が、少しだけ別人のように見えた。時間は確かに流れているはずなのに、この瞬間だけが切り取られて、どこにも属していないような、不思議な感覚に包まれていた。
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace DemoBlogCode
{
public enum LogLevel
{
Trace,
Debug,
Info,
Warn,
Error,
Fatal
}
public record LogEntry(DateTime Timestamp, LogLevel Level, string Message, Exception? Exception = null);
public interface IClock
{
DateTime UtcNow { get; }
}
public sealed class SystemClock : IClock
{
public DateTime UtcNow => DateTime.UtcNow;
}
public interface ILogSink
{
void Write(LogEntry entry);
void Flush();
}
public sealed class ConsoleSink : ILogSink
{
public void Write(LogEntry entry)
{
var prefix = $"{entry.Timestamp:O} [{entry.Level}] ";
Console.WriteLine(prefix + entry.Message);
if (entry.Exception is not null)
{
Console.WriteLine(entry.Exception);
}
}
public void Flush() { /* no-op */ }
}
public sealed class FileSink : ILogSink, IDisposable
{
private readonly StreamWriter _writer;
public FileSink(string path)
{
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? ".");
_writer = new StreamWriter(File.Open(path, FileMode.Append, FileAccess.Write, FileShare.Read))
{
AutoFlush = false,
NewLine = "\n"
};
}
public void Write(LogEntry entry)
{
var sb = new StringBuilder();
sb.Append(entry.Timestamp.ToString("O"))
.Append(" [").Append(entry.Level).Append("] ")
.Append(entry.Message);
if (entry.Exception is not null)
{
sb.Append(" | ex=").Append(entry.Exception.GetType().Name)
.Append(": ").Append(entry.Exception.Message);
}
_writer.WriteLine(sb.ToString());
}
public void Flush() => _writer.Flush();
public void Dispose()
{
_writer.Flush();
_writer.Dispose();
}
}
public sealed class Logger
{
private readonly IClock _clock;
private readonly List<ILogSink> _sinks = new();
public Logger(IClock clock, params ILogSink[] sinks)
{
_clock = clock;
_sinks.AddRange(sinks);
}
public void Log(LogLevel level, string message, Exception? ex = null)
{
var entry = new LogEntry(_clock.UtcNow, level, message, ex);
foreach (var sink in _sinks)
{
sink.Write(entry);
}
}
public void Info(string message) => Log(LogLevel.Info, message);
public void Warn(string message) => Log(LogLevel.Warn, message);
public void Error(string message, Exception? ex = null) => Log(LogLevel.Error, message, ex);
public void Flush()
{
foreach (var sink in _sinks)
{
sink.Flush();
}
}
}
public static class Program
{
public static void Main(string[] args)
{
using var file = new FileSink("logs/app.log");
var logger = new Logger(new SystemClock(), new ConsoleSink(), file);
logger.Info("Application started");
logger.Info($"Args: {string.Join(", ", args)}");
var numbers = Enumerable.Range(1, 20).ToList();
var evens = numbers.Where(n => n % 2 == 0).ToArray();
logger.Info($"Evens: [{string.Join(", ", evens)}]");
try
{
var average = SafeAverage(Array.Empty<int>());
logger.Info($"Average: {average}");
}
catch (Exception ex)
{
logger.Error("Failed to compute average", ex);
}
logger.Warn("Simulating work...");
for (int i = 0; i < 3; i++)
{
logger.Info($"Tick {i + 1}/3");
System.Threading.Thread.Sleep(100);
}
logger.Info("Application finished");
logger.Flush();
}
private static double SafeAverage(IEnumerable<int> items)
{
var list = items.ToList();
if (list.Count == 0)
{
throw new InvalidOperationException("Sequence contains no elements");
}
return list.Average();
}
}
}
彼の頭の中には、はっきりした目的も計画もなかった。それでも足は自然と動き、いつもの道から少し外れた細い路地へと入っていく。そこには人の気配がほとんどなく、壁に残された古い張り紙や、色あせた文字が、過去の記憶の断片のように目に入る。忘れていたはずの出来事が、理由もなく胸の奥に浮かび上がり、彼は一瞬だけ歩みを緩めた。
路地を抜けると、小さな公園が現れた。ベンチに落ちた木の葉は、誰にも踏まれないまま重なり合い、静かな時間を守っているようだった。遠くで鳥の鳴き声が聞こえ、その音だけが確かな現実として存在している。彼はベンチに腰を下ろし、何もしない時間の重さを、久しぶりに味わっていた。
やがて太陽は高くなり、街の音が少しずつ戻ってくる。人々の声、車の走る音、日常のざわめきが、ゆっくりと世界を満たしていった。彼は立ち上がり、もう一度周囲を見渡してから歩き出す。この先に何が待っているのかは分からない。それでも、今はそれでいいと思えた。歩き続ける限り、時間は前に進み、物語もまた、静かに続いていくのだから。
コメント
こんにちは、これはコメントです。
コメントの承認、編集、削除を始めるにはダッシュボードの「コメント」画面にアクセスしてください。
コメントのアバターは「Gravatar」から取得されます。