Logging and Binary Logs
|
This section documents MSBuild 18.9, the .NET SDK 10.0.402 (with its This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The C# Reference bibliography lists the reference material consulted while preparing these pages. |
MSBuild raises events — build and project started, target started, task message, warning, error — and loggers turn them into output. The console logger (or, since .NET 9, the Terminal Logger) is what you see; file loggers write the same text to disk; the binary logger records every event, every property and item, and the imported files, in one compressed file that can be replayed or browsed later. When a build misbehaves, a binary log is usually the fastest route to the cause. This page covers all of them with MSBuild 18.9 (.NET SDK 10.0.402).
Verbosity
| Level | Switch | Shows |
|---|---|---|
quiet |
|
errors, warnings and the summary |
minimal |
|
plus high-importance messages and project outputs ( |
normal |
|
plus target names and normal-importance messages — the default for |
detailed |
|
plus low-importance messages, skipped targets and reasons, task parameters |
diagnostic |
|
everything: initial properties and items, environment, timing |
The same build of Ledgerline.Report at three levels (--no-incremental; normal is cut after a few lines):
== -v:q
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:02.19
== -v:m
Determining projects to restore...
All projects are up-to-date for restore.
Ledgerline: 4 journal lines balance.
Ledgerline.Report -> ~/ledgerline/samples/Ledgerline.Report/bin/Debug/net10.0/Ledgerline.Report.dll
Build succeeded.
0 Warning(s)
0 Error(s)
Time Elapsed 00:00:01.84
== -v:n
Build started 10/23/2026 18:39:24.
1>Project "~/ledgerline/samples/Ledgerline.Report/Ledgerline.Report.csproj" on node 1 (Restore target(s)).
1>_GetAllRestoreProjectPathItems:
Determining projects to restore...
…
The Message task’s Importance decides where a message appears: high from minimal, normal from normal, low from
detailed. Custom build logic should log the result at high and the details at low, as Ledgerline.Build does.
The Terminal Logger
Since .NET 9, dotnet build, clean, msbuild, pack, publish, restore and test use the Terminal Logger
when the output is an interactive terminal (--tl:auto, the default): projects build in place on a live status area,
each finished project collapses to one line, and warnings and errors are grouped by project. When the output is
redirected (CI logs, pipes) the classic console logger is used. -tl:off (or --tl:off) forces the classic logger — which is what every example in this section uses, because the live output does not survive copying — and -tl:on
forces the Terminal Logger. MSBuild also honours the MSBUILDTERMINALLOGGER environment variable (off/on), which the
.NET environment-variable page does not list. The Terminal Logger was introduced as opt-in in MSBuild 17.8 (.NET 8).
File loggers
-fl writes msbuild.log (detailed by default); -flp:LogFile=build.log;Verbosity=diagnostic;Append;Encoding=UTF-8
configures it; -fl1 … -fl9 add more files with their own -flp1: … parameters (for example warnings only:
-flp2:LogFile=warnings.log;WarningsOnly). The parameters contain ;, so quote the switch in Unix shells — unquoted,
the shell ends the command at the first semicolon.
Binary logs
-bl writes msbuild.binlog; -bl:name.binlog names it; -bl:{} (or build-{}.binlog) inserts a unique stamp, so
repeated builds do not overwrite each other:
dotnet build -tl:off -v:q -bl:'build-{}.binlog'
ls build-*.binlog
build-20261023-183935--23455--izHU86.binlog
A binary log contains the messages of a diagnostic build, the full text of every imported file, every property
with its final value and reassignments, every item with its metadata, and task inputs and outputs — for
Ledgerline.Report about 400 KB, against 5.7 MB for a detailed text log. ProjectImports=None|Embed|ZipFile controls
how the imported files are stored (embedded by default). It costs some build time, so large builds enable it only when
needed.
A binary log can be replayed as if it were a build, into any logger and verbosity:
dotnet msbuild report.binlog -noconlog '-flp:LogFile=replay.log;Verbosity=diagnostic'
and opened in the MSBuild Structured Log Viewer (https://msbuildlog.com/, v2.3.246 (2026-08-20), a free third-party tool by Kirill Osenkov, linked from the MSBuild documentation), which shows the build as a searchable tree with a timeline, the preprocessed project, and "why did this target run" answers.
What a binary log can leak
|
A binary log can contain secrets: values of environment variables that the build reads, every property (including ones that carry tokens or passwords), task inputs (command lines with credentials), and the full text of every imported file. Never attach a binary log to a public issue or publish it as a public CI artifact; review it first, keep secrets out of the build’s environment, and share logs only privately. |
Which environment variables end up in the log was tested with a placeholder variable,
LEDGERLINE_FEED_TOKEN=placeholder-not-a-real-token, replaying each log at diagnostic verbosity and counting the lines
that contain the value:
| Build | Lines with the value |
|---|---|
the variable is set but nothing in the build reads it |
0 |
a property reads it ( |
6 |
nothing reads it, but |
3 |
With the property in place the replayed log says, among others:
~/ledgerline/samples/Ledgerline.Report/Ledgerline.Report.csproj(5,5): message : Property 'LEDGERLINE_FEED_TOKEN' with value 'placeholder-not-a-real-token' expanded from the environment.
FeedTokenForDemo = placeholder-not-a-real-token
So MSBuild 18.9 does not record the whole environment by default — only variables the build actually uses, unless
MSBUILDLOGALLENVIRONMENTVARIABLES asks for all of them — but anything a build does use, such as a feed token read
by a nuget.config or a property, is in the log in plain text. The Structured Log Viewer’s site warns that a binary log
"contains and exposes all environment variables from the machine that the build ran on"; on this baseline that is true
only with MSBUILDLOGALLENVIRONMENTVARIABLES set, so treat every binary log as sensitive regardless.
Source: MSBuild: Obtain build logs.
Custom loggers
A logger is a class implementing Microsoft.Build.Framework.ILogger (usually by deriving from
Microsoft.Build.Utilities.Logger) that subscribes to events in Initialize(IEventSource). This one counts warnings and
errors per project and prints a summary:
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>latest</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Build.Utilities.Core" Version="18.9.6" PrivateAssets="all" ExcludeAssets="runtime" />
</ItemGroup>
</Project>
using System;
using System.Collections.Generic;
using System.IO;
using Microsoft.Build.Framework;
using Microsoft.Build.Utilities;
namespace Ledgerline.BuildLogger;
/// <summary>Counts warnings and errors per project and prints one line per project at the end of the build.</summary>
public sealed class SummaryLogger : Logger
{
private readonly Dictionary<string, (int Warnings, int Errors)> _counts = new();
public override void Initialize(IEventSource eventSource)
{
eventSource.WarningRaised += (_, e) => Count(e.ProjectFile, warning: true);
eventSource.ErrorRaised += (_, e) => Count(e.ProjectFile, warning: false);
eventSource.BuildFinished += (_, e) =>
{
foreach (var pair in _counts)
{
Console.WriteLine($"[summary] {Path.GetFileName(pair.Key)}: {pair.Value.Warnings} warning(s), {pair.Value.Errors} error(s)");
}
Console.WriteLine($"[summary] build {(e.Succeeded ? "succeeded" : "failed")}; parameters: '{Parameters}'");
};
}
private void Count(string? project, bool warning)
{
var key = project ?? "(no project)";
_counts.TryGetValue(key, out var c);
_counts[key] = warning ? (c.Warnings + 1, c.Errors) : (c.Warnings, c.Errors + 1);
}
}
Attached with -logger:<class>,<assembly>;<parameters> (-noconlog hides the console logger to show only its output),
on the built-in task tour project, which raises one warning:
dotnet msbuild tasks.proj -nologo -noconlog "-logger:Ledgerline.BuildLogger.SummaryLogger,$PWD/../logger/bin/Release/netstandard2.0/Ledgerline.BuildLogger.dll;mode=demo"
[summary] tasks.proj: 1 warning(s), 0 error(s)
[summary] build succeeded; parameters: 'mode=demo'
The logger project had to be built outside the Ledgerline folder: inside it, the repository’s
Directory.Packages.props turned Central Package Management on and the explicit Version failed with NU1008.
In a multi-node build (-m), events from worker nodes are forwarded to the central node, and a logger registered with
-logger receives them all, tagged with BuildEventContext. A multi-processor-aware logger uses that context to
keep projects apart; a distributed logger (-distributedLogger:<central>*<forwarding>) adds a forwarding logger
in each node that filters events before they cross processes, which reduces traffic for verbose logs.
Common mistakes
-
Reading CI failures from a minimal console log — add
-bland upload the log as a private artifact. -
Publishing binary logs — see the warning above.
-
Unquoted
-flp:parameters in a Unix shell — the;ends the command. -
Parsing Terminal Logger output — its format is not stable; use
-tl:off,-getPropertyor a binary log. -
Custom loggers that assume one node — use
BuildEventContextto tell projects apart.