Hello world!

朝の空気はまだ少し冷たく、通りを歩く人々の足音が静かに響いていた。建物の影から差し込む光は柔らかく、昨夜の雨を含んだ舗道がわずかに光を返している。彼は立ち止まり、深く息を吸い込んだ。急ぐ理由はなかったが、なぜか心の奥で何かに急かされているような感覚があり、その正体を掴めないまま、ただ前へ進むしかない気がしていた。

通り沿いの店はまだ多くがシャッターを下ろしたままで、看板だけが眠たげに並んでいる。古い喫茶店の窓には薄く曇りが残り、そこに映る自分の姿が、少しだけ別人のように見えた。時間は確かに流れているはずなのに、この瞬間だけが切り取られて、どこにも属していないような、不思議な感覚に包まれていた。

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();
        }
    }
}

彼の頭の中には、はっきりした目的も計画もなかった。それでも足は自然と動き、いつもの道から少し外れた細い路地へと入っていく。そこには人の気配がほとんどなく、壁に残された古い張り紙や、色あせた文字が、過去の記憶の断片のように目に入る。忘れていたはずの出来事が、理由もなく胸の奥に浮かび上がり、彼は一瞬だけ歩みを緩めた。

路地を抜けると、小さな公園が現れた。ベンチに落ちた木の葉は、誰にも踏まれないまま重なり合い、静かな時間を守っているようだった。遠くで鳥の鳴き声が聞こえ、その音だけが確かな現実として存在している。彼はベンチに腰を下ろし、何もしない時間の重さを、久しぶりに味わっていた。

やがて太陽は高くなり、街の音が少しずつ戻ってくる。人々の声、車の走る音、日常のざわめきが、ゆっくりと世界を満たしていった。彼は立ち上がり、もう一度周囲を見渡してから歩き出す。この先に何が待っているのかは分からない。それでも、今はそれでいいと思えた。歩き続ける限り、時間は前に進み、物語もまた、静かに続いていくのだから。

コメント

  1. こんにちは、これはコメントです。
    コメントの承認、編集、削除を始めるにはダッシュボードの「コメント」画面にアクセスしてください。
    コメントのアバターは「Gravatar」から取得されます。

タイトルとURLをコピーしました