「.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.Logging | MAUI の 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.txt | Day | log20250120.txt | 見つからない |
logs/log-.txt | Day | log-20250120.txt | 見つからない |
なお、ファイル名の具体的な付与ルールは「拡張子の直前に日付文字列が挿入される」と覚えると整理しやすいです。パス側にハイフンが入っていれば、そのまま日付の前に残るため、視認性が上がります。
補足:ファイルサイズ分割を併用すると “_001” などの連番が付く
rollOnFileSizeLimit(サイズで分割)を有効にしている場合、同じ日付の中でも複数ファイルに分かれ、末尾に _001 のような連番が付くことがあります。例えば次のようになります。
log-20250120.txtlog-20250120_001.txtlog-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 だけ NG | Serilog 設定(パスの組み立て、rollingInterval、バッファリング、SelfLog など) | 本記事の“日付付き命名”や SelfLog を確認 |
| 手動も NG | AppDataDirectory の扱い、ディレクトリ作成、権限・実体パスの誤認 | 実体パス出力、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を列挙する - 切り分けには “手動で書けるか” テストが最短

コメント