.NET MAUIでSerilogのlog.txtが作成されない/見つからない原因と解決策(RollingInterval.Day対応)

「.NET MAUI × Serilog で log.txt が作成されない/見つからない」現象は、実は“作られていない”のではなく、日次ローテーション設定によってファイル名が日付付きに変わっているケースがほとんどです。本記事では原因の見抜き方と、固定ファイル名・日次ローテーションそれぞれの正しい実装方法を解説します。

目次

起きている症状:logs フォルダーは作成されるのに log.txt が見つからない

.NET MAUI アプリで Serilog(Serilog.Sinks.File) を使い、FileSystem.AppDataDirectory/logs/log.txt にログを書きたいとします。ところが実際には次のような状況に遭遇しがちです。

  • アプリ起動時に logs フォルダーは作成される
  • しかし log.txt が存在せず、File.Exists が false になる
  • File.ReadAllText で読み込もうとしても「ファイルが見つからない」になる
  • 設定に rollingInterval: RollingInterval.Day(日次ローテーション)を指定している

結論から言うと、これは .NET MAUI のパスや権限問題であることもありますが、最も多い原因は 「rollingInterval を指定するとファイル名が変わる」 ことです。つまり、アプリ側が log.txt を探している限り、永遠に見つかりません。

前提:.NET MAUI で Serilog ファイル出力に必要な NuGet

構成によって差はありますが、最小限で次のパッケージが揃っていると安心です(名前が似ているため、入れ間違いもよく起きます)。

用途代表パッケージ役割
Serilog 本体Serilogロガーのコア(Log.Information など)
ファイル出力Serilog.Sinks.Fileログファイルに書き込む Sink
Microsoft.Extensions.Logging 連携(任意)Serilog.Extensions.LoggingMAUI の Logging と Serilog を橋渡し

「ログが出ない」「設定が効かない」場合、まずは Sink(File)が入っているか、拡張(Extensions.Logging)を使っているなら参照関係が正しいかを確認すると、ムダな遠回りを避けられます。

原因:RollingInterval.Day により log.txt ではなく “日付付きファイル” が作られる

Serilog.Sinks.File の rollingInterval は、一定間隔でログファイルをロール(切り替え)するための設定です。RollingInterval.Day を指定すると「1日1ファイル」に分割され、その日付がファイル名に組み込まれます。

たとえばパスを .../logs/log.txt にしていても、実際に作成されるのは次のような名前になります。

  • log20250120.txt(例)
  • 区切りを入れたいなら log-.txt のようにしておくと log-20250120.txt のように見やすくできます

つまり、File.Exists(".../logs/log.txt") が false になるのは当然で、正しくは .../logs/log20250120.txt のような“今日のファイル名”を参照する必要があります。

rollingInterval とファイル名の変化(イメージ)

指定するパスrollingInterval実際に作られる例log.txt を探すと?
logs/log.txt指定なし(Infinite)log.txt見つかる
logs/log.txtDaylog20250120.txt見つからない
logs/log-.txtDaylog-20250120.txt見つからない

なお、ファイル名の具体的な付与ルールは「拡張子の直前に日付文字列が挿入される」と覚えると整理しやすいです。パス側にハイフンが入っていれば、そのまま日付の前に残るため、視認性が上がります。

補足:ファイルサイズ分割を併用すると “_001” などの連番が付く

rollOnFileSizeLimit(サイズで分割)を有効にしている場合、同じ日付の中でも複数ファイルに分かれ、末尾に _001 のような連番が付くことがあります。例えば次のようになります。

  • log-20250120.txt
  • log-20250120_001.txt
  • log-20250120_002.txt

この場合、「当日分のファイル名を1つに決め打ち」すると取りこぼすことがあるため、後述のように log-20250120*.txt を列挙する運用が堅実です。

まずは現物を確認する:AppDataDirectory の実体パスと見つけ方

.NET MAUI の FileSystem.AppDataDirectory はプラットフォームごとに実体パスが異なります。アプリのサンドボックス内に保存されるため、PC のエクスプローラーから直接見えないこともあります。まずは「どこに書かれているか」を把握しましょう。

プラットフォームAppDataDirectory の実体(例)確認方法(代表例)よくある落とし穴
Android/data/user/0/<package>/files/ などAndroid Studio の Device File Explorer / adbリリースビルドは見えにくい・端末や設定で権限が異なる
iOSアプリコンテナ内(Documents/Library など)Xcode の Devices and Simulators でコンテナ取得シミュレーターと実機で手順が違う
Windows%LOCALAPPDATA%\\Packages\\...\\LocalState\\ などエクスプローラーで直接確認パッケージ名が長く、場所を見失いやすい

Android Studio での確認手順(Device File Explorer)

Android で「フォルダーはあるのにファイルがない」と感じたら、まず Device File Explorer で logs フォルダー直下を見てください。log.txt がなくても log20250120.txt や log-20250120.txt のようなファイルが存在しているはずです。

  • Android Studio → View → Tool Windows → Device File Explorer
  • data/data/<package>/files/(端末によっては /data/user/0/)へ移動
  • logs フォルダーを開き、log*.txt がないか確認

「ファイルが存在するのにアプリ内では見つからない」という場合は、コード側が参照しているパスを Debug.WriteLine(FileSystem.AppDataDirectory) などで出力し、Device File Explorer で辿っている場所と一致しているかを突き合わせると早いです。

対処方法:目的別に選ぶべき実装は 2 パターン

ここからが本題です。やりたいことは大きく次の 2 つに分かれます。

  • 固定ファイル名にしたい:常に log.txt を読みたい(ログ共有ボタンなどで 1 ファイルを送る想定)
  • 日次ローテーションしたい:1日1ファイルで管理したい(障害解析で日付ごとに切りたい、古い分を自動削除したい)

どちらが正解というより、「運用に合う方」を選ぶのが正解です。以下では、どちらも “実務でハマりにくい” 形で紹介します。

方法:ファイル名を固定する(log.txt で読みたい)

固定ファイル名が目的なら、最もシンプルなのは rollingInterval を指定しない ことです。これだけで log.txt が作られ、File.Exists / File.ReadAllText も期待通りに動きます。

実装例:MauiProgram.cs で明示的に log.txt を指定

using Serilog;
using Serilog.Events;

public static class MauiProgram
{
public static MauiApp CreateMauiApp()
{
var builder = MauiApp.CreateBuilder();


    var logsDir = Path.Combine(FileSystem.AppDataDirectory, "logs");
    Directory.CreateDirectory(logsDir);

    var logPath = Path.Combine(logsDir, "log.txt");

    Log.Logger = new LoggerConfiguration()
        .MinimumLevel.Debug()
        .MinimumLevel.Override("Microsoft", LogEventLevel.Warning)
        .WriteTo.File(
            path: logPath,
            // rollingInterval を指定しない(=ファイル名固定)
            rollOnFileSizeLimit: true,            // サイズで分割したい場合
            fileSizeLimitBytes: 5 * 1024 * 1024,  // 例:5MB
            retainedFileCountLimit: 3,            // 例:最大3世代保持
            shared: true                          // 複数プロセス/スレッド環境で無難
        )
        .CreateLogger();

    // Serilog.Extensions.Logging を使う場合の例
    builder.Logging.AddSerilog(Log.Logger);

    return builder.Build();
}


}

appsettings.json で固定ファイル名にしたい場合の考え方

MAUI では FileSystem.AppDataDirectory の値を appsettings.json 内だけで表現しにくいため、ログパスはコード側で組み立てるのが定番です。どうしても設定で管理したい場合は「ファイル名やテンプレートは appsettings.json、最終的なフルパスはコードで合成」といった分担にすると破綻しにくいです。

固定ファイル名のメリット・デメリット

観点メリットデメリット向いているケース
読み取り常に log.txt を読むだけで済む日付での切り分けが弱いアプリ内で “ログ送信” 機能を作る
運用実装が単純でハマりにくい放置するとファイルが大きくなりがち小規模アプリ、検証用ログ

「ログは 1 ファイルで十分。ただし肥大化が怖い」なら、日付ローテーションではなく サイズローテーション(fileSizeLimitBytes) を採用するのも現実的です。

方法:日次ローテーションを使う(1日1ファイル)

日次ローテーションが目的なら、RollingInterval.Day を使い続けて問題ありません。必要なのは、読み取り側を日付付きファイルに合わせる ことです。

実装例:log-.txt にして視認性を上げる

var logsDir = Path.Combine(FileSystem.AppDataDirectory, "logs");
Directory.CreateDirectory(logsDir);

// 日付が入る前提なので、区切りの「-」を入れておくと探しやすい
var logPath = Path.Combine(logsDir, "log-.txt");

Log.Logger = new LoggerConfiguration()
.MinimumLevel.Debug()
.WriteTo.File(
path: logPath,
rollingInterval: RollingInterval.Day,
retainedFileCountLimit: 7, // 例:直近7日分だけ残す
shared: true
)
.CreateLogger();

戦略:当日分だけ読む(ただし連番が付く可能性も考慮する)

「不具合報告ボタンで当日分だけ送る」「デバッグ画面で今日のログを表示したい」といった用途なら、当日のファイルパスを組み立てる方法が直感的です。ただし、サイズ分割が有効だと _001 が付くため、当日分はパターンで列挙して結合 するほうが安全です。

var logsDir = Path.Combine(FileSystem.AppDataDirectory, "logs");
var today = DateTime.Now.ToString("yyyyMMdd");

// log-.txt を使っている場合(当日分の全ファイル)
var pattern = $"log-{today}*.txt";

var todayFiles = Directory.EnumerateFiles(logsDir, pattern)
    .Select(p => new FileInfo(p))
    .OrderBy(f => f.Name) // 連番順に並べたい場合の一例
    .ToList();

当日分を 1 ファイルだけ読む前提で作るなら、サイズ分割は無効にする(または送信ロジック側で複数ファイルをまとめる)など、どこで吸収するかを決めておくと運用が安定します。

戦略:フォルダー内の log*.txt を列挙して、必要分を送る

障害解析では「直近数日分をまとめて欲しい」「前日の深夜から当日の朝にかけてが欲しい」など、当日分だけでは足りないことがよくあります。その場合は Directory.EnumerateFiles で列挙して、最新順に取得するのが堅実です。

var logsDir = Path.Combine(FileSystem.AppDataDirectory, "logs");
if (!Directory.Exists(logsDir))
{
    // ログフォルダーがない=まだ書き込みが始まっていない等
    return;
}

var files = Directory.EnumerateFiles(logsDir, "log*.txt")
    .Select(path => new FileInfo(path))
    .OrderByDescending(f => f.LastWriteTimeUtc)
    .ToList();

// 例:最新3ファイルを結合して送る
var latest3 = files.Take(3).Select(f => f.FullName).ToList();

ファイルを送信する場合は、そのまま全量を読み込むとメモリを圧迫することがあるため、サイズ上限を決めて末尾だけ読む(いわゆる tail) 戦略も有効です。

static string ReadTail(string path, int maxChars = 20000)
{
    // 末尾側だけ読む簡易実装(UTF-8 前提の単純版)
    var text = File.ReadAllText(path);
    if (text.Length <= maxChars) return text;
    return text.Substring(text.Length - maxChars);
}

実運用では「最新 N 日分」「最新 N ファイル」「最新 M 文字」など、送信量を制御するルールを決めておくと、サポート対応やユーザー体験が安定します。

切り分け:パス/権限問題か、Serilog の命名ルール問題かを最短で見抜く

今回の本命原因は “日付付きファイル名” ですが、現場では複数要因が重なることもあります。迷ったら、まずは Serilog を介さずに 手動で書き込み・読み取りできるか を確認すると切り分けが速いです。

var logsDir = Path.Combine(FileSystem.AppDataDirectory, "logs");
Directory.CreateDirectory(logsDir);

var testPath = Path.Combine(logsDir, "manual-test.txt");

File.WriteAllText(testPath, "hello");
var ok = File.Exists(testPath);
var text = File.ReadAllText(testPath);
結果疑うべきポイント次にやること
手動は OK、Serilog だけ NGSerilog 設定(パスの組み立て、rollingInterval、バッファリング、SelfLog など)本記事の“日付付き命名”や SelfLog を確認
手動も NGAppDataDirectory の扱い、ディレクトリ作成、権限・実体パスの誤認実体パス出力、Device File Explorer / コンテナ確認

追加でハマりやすいポイント

ログファイルは「最初のログが書かれた時」に作られる

Serilog のファイル出力は、設定した瞬間に空ファイルが必ず作られるとは限りません。最初の Log.Information などが実行されるまでファイルが存在しないケースがあります。検証時は次のように “必ず1行書く” と確認が早くなります。

Log.Information("Serilog file sink test: {Time}", DateTime.Now);

終了時に CloseAndFlush を呼ばないと、末尾が書き込まれないことがある

アプリ終了直前のログがファイルに反映されない場合、バッファリングや終了処理のタイミングが原因になりがちです。アプリのライフサイクルに合わせて Log.CloseAndFlush() を呼ぶ設計にすると、ログ欠落が減ります(実装位置はアプリ構成に合わせて調整してください)。

Android は大文字小文字が区別される

Windows だと気づきにくいですが、Android のファイルシステムは大文字小文字を区別します。Log.txt と log.txt は別物です。コードや設定ファイルで表記ゆれがあると “見つからない” の原因になります。

Serilog 自体のエラーは SelfLog で見えることがある

もし日付付きファイルも見当たらず、本当に出力できていない疑いがあるなら、Serilog の内部エラー出力(SelfLog)を有効化するとヒントが得られることがあります。特に、パスが不正/例外が発生しているのに気づかないケースで有効です。

Serilog.Debugging.SelfLog.Enable(message =>
{
    System.Diagnostics.Debug.WriteLine(message);
});

実務でおすすめの運用設計

「結局どっちにすべき?」という質問をよく受けます。結論としては次の考え方が現場で安定します。

状況おすすめ理由
ユーザーからログを送ってもらう機能を作る固定ファイル名(log.txt)+サイズローテ参照先が固定で実装が単純。送信対象が迷子になりにくい
障害解析で日付単位の切り分けが欲しい日次ローテ(RollingInterval.Day)+保持数制限調査時に見やすく、古い分を自動削除しやすい
検証段階でとにかく早く状況を見たい日次ローテでも固定でも OK(ただし “探すファイル名” を決める)今回の原因は “探し方” なので、ルールさえ揃えればどちらでも回る

重要なのは、「書き込み側の設定」と「読み取り/送信側の探し方」を同じルールで揃える ことです。rollingInterval を導入した瞬間にファイル名が変わるため、読み取り処理を据え置くと必ず破綻します。

まとめ:log.txt がないのではなく、探すべきファイル名が違う

  • RollingInterval.Day を指定すると、ログファイル名は日付付き(例:log20250120.txt)になる
  • そのため log.txt を探しても見つからず、File.Exists / File.ReadAllText が失敗する
  • 固定ファイル名が必要なら rollingInterval を外す(または RollingInterval.Infinite 相当)
  • 日次ローテを使うなら、読み取り側も日付付きファイル名を参照するか、log*.txt を列挙する
  • 切り分けには “手動で書けるか” テストが最短

参考リンク

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次