Repository navigation
[Microsoft.Gen.Logging] Expanding a classified property drops its data classification, and Transitive logs [LogPropertyIgnore] properties #7856
Description
Description
When the [LoggerMessage] generator expands a property of a [LogProperties] parameter into that property's own properties, it ignores attributes on the property being expanded:
- The property's data classification is dropped. A
[PersonalData]property is redacted when it is logged whole. Once expanded, its sub-properties go toTagArrayin clear. This happens when the parameter's[LogProperties]isTransitive, and when the property carries[LogProperties]itself. The same combination on a parameter, a classification together with[LogProperties], is refused withLOGGEN017. On a property it is accepted silently. - Under
Transitive = true,[LogPropertyIgnore]is not honoured. The ignored property is not expanded, but it is still logged whole. WithoutTransitiveit is left out, as documented. Transitiveon a property's own[LogProperties]is not read. Only the parameter's value is used, so[LogProperties(Transitive = true)]on a property expands it one level only. This is the least important of the three;Transitiveis[Experimental("EXTEXP0003")].
Points 1 and 2 write data the application marked as sensitive, or excluded from logs, in clear.
[TagProvider] on a property, also never called, is already tracked by #6605 (PR #7751), so it is left out here.
Reproduction Steps
Full project, with a repro.sh that prints the generated tag writes and what a logger provider receives: https://github.com/AdCodicem/dotnet-upstream-repros/tree/main/extensions-genlogging-logproperties-expansion
public sealed class PersonalDataAttribute() : DataClassificationAttribute(new DataClassification("Repro", "PersonalData"));
public sealed class Card
{
public string Number { get; init; } = "";
public string Expiry { get; init; } = "";
public override string ToString() => $"{Number} {Expiry}";
}
public sealed class Order
{
public int Id { get; init; }
[PersonalData]
public Card? Card { get; init; }
}
public sealed class Receipt
{
[PersonalData]
[LogProperties]
public Card? Card { get; init; }
}
public sealed class Account
{
public string Name { get; init; } = "";
[LogPropertyIgnore]
public string Password { get; init; } = "";
}
internal static partial class Log
{
[LoggerMessage(Level = LogLevel.Information, Message = "Order placed")]
public static partial void OrderPlaced(ILogger logger, [LogProperties] Order order); // A
[LoggerMessage(Level = LogLevel.Information, Message = "Order placed")]
public static partial void OrderPlacedTransitive(ILogger logger, [LogProperties(Transitive = true)] Order order); // B
[LoggerMessage(Level = LogLevel.Information, Message = "Receipt sent")]
public static partial void Receipted(ILogger logger, [LogProperties] Receipt receipt); // C
[LoggerMessage(Level = LogLevel.Information, Message = "Signed in")]
public static partial void SignedIn(ILogger logger, [LogProperties] Account account); // D
[LoggerMessage(Level = LogLevel.Information, Message = "Signed in")]
public static partial void SignedInTransitive(ILogger logger, [LogProperties(Transitive = true)] Account account); // E
}The app enables redaction (logging.EnableRedaction()) and sets a fallback redactor that writes <redacted>, so any classified tag is redacted. A logger provider prints the tags it receives. The repro also has case F, point 3: [LogProperties(Transitive = true)] on a property Address of type Address { string City; Geo Geo }.
Expected behavior
- B and C:
order.Card/receipt.Cardredacted as in A, or each of its sub-properties classified asPersonalData. Failing that, a diagnostic likeLOGGEN017. - E: no
account.Password, as in D. - F:
shipment.Address.Geo.Latitudeand.Longitude.
Actual behavior
Generated code:
OrderPlaced:
TagArray[0] = new("order.Id", order?.Id);
ClassifiedTagArray[0] = new("order.Card", order?.Card, new global::Microsoft.Extensions.Compliance.Classification.DataClassificationSet(_PersonalDataAttribute));
OrderPlacedTransitive:
TagArray[2] = new("order.Id", order?.Id);
TagArray[1] = new("order.Card.Number", order?.Card?.Number);
TagArray[0] = new("order.Card.Expiry", order?.Card?.Expiry);
SignedInTransitive:
TagArray[1] = new("account.Name", account?.Name);
TagArray[0] = new("account.Password", account?.Password);
What the logger provider receives:
A. [PersonalData] property, [LogProperties] (redacted, as expected):
"order.Card":"<redacted>", "order.Id":"42"
B. Same property, [LogProperties(Transitive = true)] (expected order.Card.* redacted):
"order.Card.Expiry":"12/29", "order.Card.Number":"4111 1111 1111 1111", "order.Id":"42"
C. [PersonalData] and [LogProperties] on the same property (expected receipt.Card.* redacted, or LOGGEN017 as on a parameter):
"receipt.Card.Expiry":"12/29", "receipt.Card.Number":"4111 1111 1111 1111"
D. [LogPropertyIgnore] on a property, [LogProperties] (left out, as expected):
"account.Name":"jane"
E. Same property, [LogProperties(Transitive = true)] (expected no account.Password):
"account.Password":"hunter2", "account.Name":"jane"
F. [LogProperties(Transitive = true)] on a property, [LogProperties] on the parameter (expected shipment.Address.Geo.Latitude and .Longitude):
"shipment.Address.Geo":"Geo.ToString()", "shipment.Address.City":"Paris"
No diagnostic is reported for any of them.
Regression?
No. Microsoft.Extensions.Telemetry.Abstractions 9.10.0 gives the same output. The code on main (42a0f35) has not changed since 10.11.0.
Known Workarounds
- 1: do not expand a classified property. Leave out
Transitiveand the property's own[LogProperties], or classify each sub-property of its type, which is not possible for a type of another library. - 2: do not combine
Transitivewith[LogPropertyIgnore].
Configuration
- Windows 11 (10.0.26200), x64, .NET SDK 10.0.402,
net10.0. - Microsoft.Extensions.Telemetry.Abstractions, Microsoft.Extensions.Telemetry and Microsoft.Extensions.Compliance.Redaction 10.11.0.
- Compile-time behaviour of the generator, so not specific to the OS.
Other information
Where it happens, on main (42a0f35), in Parser.LogProperties.cs:
- 1:
GetTypePropertiesToLogfills the expanded property'sClassificationAttributeTypes(L161-L169), butTraverseParameterPropertiesTransitivelycalls back on leaves only (LoggingMethodParameterExtensions.cs#L40). The leaves' classification sets never receive the parent's. For a parameter,LOGGEN017refuses the same combination (Parser.cs#L143-L147). - 2: an ignored property is not expanded (L247), but the check that skips it runs only when
!transitive(L303-L311), so the property is added to the result. - 3:
transitiveis read from the parameter's attribute (L48) and passed down unchanged (L279). A property's own[LogProperties]values are not read.
Related: #5339 asked whether [LogPropertyIgnore] works with Transitive, and a maintainer could not get it to work at the top level. It was closed for lack of author feedback. #6605 / #7751 cover [TagName] and [TagProvider] on properties.
Investigation and write-up assisted by Claude (Anthropic); the linked repro was run on Windows 11 (10.0.26200), .NET SDK 10.0.402, Microsoft.Extensions.Telemetry.Abstractions / Microsoft.Extensions.Telemetry / Microsoft.Extensions.Compliance.Redaction 10.11.0, and I reviewed this report.
Activity