Conversation
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: bd36abc5-edbb-4e21-b3d0-e06a1b4aee1f
There was a problem hiding this comment.
Pull request overview
Adds a proposed design specification for “structured task logging” to enable task log events to carry an invariant named template (OriginalFormat) plus ordered name/value pairs, with the goals of improving binary log deduplication opportunities and enabling importance-filtered logging to skip interpolation work while preserving existing Message-based logger behavior.
Changes:
- Introduces a full design spec covering task-author APIs (interpolated string handler + explicit structured APIs) and detailed naming/formatting/materialization semantics.
- Documents transport and compatibility design: in-proc event model, node transport expectations (including the noted prototype TaskHost receive gap), binary log record/versioning approach, and Change Wave gating plan.
- Captures measurement evidence, security/privacy considerations, alternatives, rollout, and a structured list of open questions for team review.
There was a problem hiding this comment.
Warning
Firewall blocked 1 domain
The following domain was blocked by the firewall during workflow execution:
patchdiff.githubusercontent.com
To allow these domains, add them to the network.allowed list in your workflow frontmatter:
network:
allowed:
- defaults
- "patchdiff.githubusercontent.com"See Network Configuration for more information.
Generated by Expert Code Review (on open) for #14648 · sonnet46 · 71.2 AIC · ⌖ 8.2 AIC · ⊞ 5.2K
|
|
||
| The current replay API defaults and the Binary Log documentation do not agree. | ||
| This proposal does not define old-reader behavior until the team resolves that difference. | ||
|
|
There was a problem hiding this comment.
Critical: Structured errors and warnings will be silently lost in old binary-log readers
The spec states: "The record does not contain an encoded classic event. Therefore, an old reader that skips the record also skips the complete event."
A lost structured error event means a failed build can appear to succeed on replay — a serious correctness regression. A lost structured warning breaks downstream tooling that relies on warning counts or warning-as-error policy in replayed logs.
Open question #7 (dual binary records) touches this, but the spec should resolve this before the implementation proceeds — it's the most consequential design decision in the entire document. Options:
a) Emit a classic fallback record alongside every structured record — guarantees old reader fidelity at the cost of some size overhead (partially defeating the size goal).
b) Require consumers to update to a minimum reader version before the feature ships — see rollout step 3 and open question #15.
c) Document that forward-compatible skip of an error/warning is a known limitation and specify a reader upgrade path.
Option (c) is the weakest choice given MSBuild's binary-log reliability guarantees. Please resolve this before merge.
| | Node serialization | 1.00x | 1.209x | +20.9% time, +42 first-event bytes | | ||
| | Steady binary-log write | 1.00x | 1.220x | +22.0% time, +41 first-event bytes | | ||
|
|
||
| The disabled structured path also suppresses interpolation-hole evaluation. |
There was a problem hiding this comment.
Performance overhead needs end-to-end evidence before shipping
The benchmarks show +137.7% time and +144 B allocations for the common case (enabled capture, two strings). The spec acknowledges this is prototype overhead and that "They do not yet prove the primary binary-log size goal."
This is a significant concern for a feature whose primary motivation is performance improvement:
- The per-event overhead is substantial on hot paths where thousands of events are emitted per build.
- The binary-log size reduction only materializes when templates are reused at high frequency — a pattern that may not be representative of typical task logging.
- The classic composite baseline (
"Resolved {0}"style) already avoids the eager-string-creation problem; the comparison is slightly unfair to the new design.
The rollout plan step 6 ("Measure production binary logs before wider migration") is good, but the spec should state a pre-ship gate: if end-to-end logs for typical SDK builds do not show a net positive, the migration should halt.
Suggest adding an explicit acceptance criterion to the rollout plan.
|
|
||
| This gap is an implementation blocker, not a proposed behavior. | ||
| The implementation must add receive-side routing and end-to-end tests before merge. | ||
|
|
There was a problem hiding this comment.
TaskHost gap needs a resolution path, not just a blocker label
The spec correctly identifies the TaskHost routing gap as "an implementation blocker, not a proposed behavior", but stops there. For a spec that's meant to guide implementation, this section should specify:
- What the fix looks like: The receive switch in
TaskHostTaskneeds cases for the three new event identifiers, routing each to the appropriateIBuildEngine.Log*Eventcall. - What tests are required: Specifically, a test that verifies a structured error emitted in an out-of-process task host fails the build (already mentioned in the testing plan at line ~865, good).
- Whether this gap affects the Change Wave design: If the TaskHost routing is missing, should the Change Wave be disabled entirely for out-of-process task hosts as a fallback, rather than dropping events silently?
Without routing guidance here, an implementer might add the identifiers to the serialization switch but miss the deserialization receive path, recreating the same gap.
|
|
||
| `OriginalFormat` identifies the event schema. | ||
| Each template hole corresponds to the value at the same list position. | ||
|
|
There was a problem hiding this comment.
Interface nullability contract is underspecified
public interface IStructuredBuildEventArgs
{
string? OriginalFormat { get; }
IReadOnlyList<KeyValuePair<string, string?>>? StructuredValues { get; }
}Both members are nullable, but the spec doesn't define when null is valid. Open question #12 ("Should the interface permit null after normal construction, or only during deserialization?") should be resolved in this spec.
Specifically:
- A
nullOriginalFormaton a live structured event is semantically meaningless — the event can't be grouped or analyzed. Should this be non-null (withnullonly allowed during deserialization of a legacy event that implements the interface by default)? - A
nullStructuredValuesalongside a non-nullOriginalFormatis an inconsistency. Should the contract guaranteeStructuredValuesis non-null wheneverOriginalFormatis non-null?
This ambiguity will cause defensive null checks in every logger that consumes the interface, adding overhead that compounds across thousands of events per build.
| The prototype also adds `[Serializable]` to three extended event types. | ||
| That change is not part of this design. | ||
| The implementation PR must remove or justify the incidental change. | ||
|
|
There was a problem hiding this comment.
Mixed-version node compatibility is unresolved but critical
The spec states: "The current node protocol requires both endpoints to recognize the new event identifiers."
This means in a mixed-version build graph (e.g., VS 17.x invoking MSBuild 18.11 worker nodes, or a solution build spanning different project SDK versions), structured events cannot be safely forwarded. But the spec leaves the resolution to open question #9.
This is a blocking gap, not a minor detail. Possible resolutions to add to the spec:
- New nodes downgrade to classic events when connecting to an older parent node — requires version negotiation at node connect time.
- Change Wave 18.11 is disabled when any participating node is below 18.11 — simpler but very conservative.
- New nodes always emit both structured and classic records in node protocol — higher bandwidth cost but safe.
Without a decision here, the feature cannot ship safely in VS-hosted build scenarios, where mixed node versions are common during the VS update transition window.
| string? subcategory, | ||
| string warningCode, | ||
| string? helpKeyword, | ||
| string? file, |
There was a problem hiding this comment.
LogWarning handler overloads are missing warningCode nullability contract
The simplified warning overload:
public void LogWarning(
string warningCode,
ref StructuredLogInterpolatedStringHandler message);The spec says "Every structured warning and error overload requires a non-empty code" (stated at line ~451), but this is specified as throwing FormatException for malformed templates and ArgumentException for named-list mismatches. The spec doesn't state what exception is thrown for an empty/null warningCode, or whether it's validated eagerly or lazily.
Given that existing LogWarning overloads accept null code (to produce an uncoded warning), this new overload's stricter contract is a deliberate API design choice. The spec should:
- Explicitly state the exception type for null/empty
warningCode(suggestArgumentException). - Explain the design decision to require a code (good rationale: structured warnings that feed into tooling need stable identifiers for suppression).
- Confirm whether
nullis accepted and silently treated as an empty string, or rejected outright.
rainersigwald
left a comment
There was a problem hiding this comment.
In general I think I like this idea but I'm concerned about whether it's relevant. If we're already doing a functionally identical argument deduplication with the existing format strings + args, does this buy much? It's a better API for sure, but generally it's not super important and we can push pain (using old-school format strings instead of interpolation) on specific tasks that log a lot.
| Materialized task messages combine fixed text and values into one string. | ||
| These strings often vary per event, which limits binary-log string deduplication. | ||
|
|
||
| The primary goal is to reduce binary-log size for repeated message shapes. |
There was a problem hiding this comment.
Strong disagree here: the primary goal should be to reduce the overhead of logging structured messages. Binlog size reduction is desirable but secondary--if binlogs were 20% bigger but had less wall-clock-time overhead, I would take that trade.
There was a problem hiding this comment.
I think binlog size is very desirable proxy outcome for big builds.
- If we plan to collect many binlogs the sizes in storage add up
- time to upload and download in pipelines (possibly multiple times) adds up
| Structured events carry a reusable `OriginalFormat` and separate values instead of a materialized display string. | ||
| This shape lets the binary logger deduplicate each component. | ||
|
|
||
| The same structure also preserves the meaning of interpolated values. |
There was a problem hiding this comment.
Not sure I understand what this means.
| This proposal does not add a dependency on `Microsoft.Extensions.Logging`. | ||
|
|
||
| C# task authors can use normal interpolated strings. | ||
| The compiler selects an interpolated string handler after the task recompiles. |
There was a problem hiding this comment.
| The compiler selects an interpolated string handler after the task recompiles. | |
| The compiler selects an interpolated string handler after the task recompiles, with no code change required. |
| The same structure also preserves the meaning of interpolated values. | ||
| This proposal does not add a dependency on `Microsoft.Extensions.Logging`. | ||
|
|
||
| C# task authors can use normal interpolated strings. |
There was a problem hiding this comment.
Our old-school logging APIs also have TaskLoggingHelper.LogMessage(string message, params object[] messageArgs) where string message is format string with {1} holes. Can we do that transparently too?
| The handler captures a named template and an ordered list of formatted values. | ||
|
|
||
| Existing loggers continue to read `BuildEventArgs.Message`. | ||
| Structured loggers can also read `IStructuredBuildEventArgs`. |
There was a problem hiding this comment.
| Structured loggers can also read `IStructuredBuildEventArgs`. | |
| Updated loggers may also read `IStructuredBuildEventArgs`. |
|
|
||
| C# interpolation gives readable task code. | ||
| However, the current string overload evaluates every interpolation hole before MSBuild can filter the message. | ||
| Interpolation also removes source names that loggers need for queries and grouping. |
|
|
||
| ### Localized task | ||
|
|
||
| A resource-based task supplies localized display text and a separate invariant template: |
There was a problem hiding this comment.
Is this a standard pattern?
|
|
||
| ### Overload resolution and runtime requirements | ||
|
|
||
| An interpolated string expression selects the handler overload after source recompilation. |
There was a problem hiding this comment.
This requires compiler cooperation and C# 10+, right? So people writing .NET 4.7.2 tasks with default settings wouldn't get it?
| However, reserved entries are less discoverable and can conflict with template value names. | ||
| That design would need a reserved-name contract and strict validation. | ||
|
|
||
| The `params ReadOnlySpan<T>` overloads give C# callers an allocation-free argument container. |
There was a problem hiding this comment.
| The `params ReadOnlySpan<T>` overloads give C# callers an allocation-free argument container. | |
| The `params ReadOnlySpan<T>` overloads give C# callers on .NET an allocation-free argument container. |
|
Taking this back to draft because of some things I've learned about the existing lazyformattedbuildeventargs - namely that it already does the format-string + separate arguments pattern. It just doesn't handle the names of the arguments, which aren't strictly necessary but are nice for structured logging/querying. |
Context
PR #14606 prototypes structured task logging. This PR separates the design for review before or independently from the implementation.
The primary motivation is to reduce binary-log size by storing a reusable original format and separate values instead of repeatedly materialized display strings. The structure also preserves value meaning and enables importance-filtered messages to skip interpolation work.
Changes Made
Adds
documentation/specs/proposed/structured-task-logging.mdwith:The specification also identifies a prototype blocker where the out-of-process TaskHost receive path can drop structured events.
Testing
git diff --check.origin/mainby this documentation file only.Notes
This PR changes documentation only. It does not change production code, tests, packages, SDK selection, or NuGet configuration.
Implementation prototype: #14606