Skip to content

[Microsoft.Gen.Logging] Expanding a classified property drops its data classification, and Transitive logs [LogPropertyIgnore] properties #7856

Open

Description

@AdCodicem

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:

  1. The property's data classification is dropped. A [PersonalData] property is redacted when it is logged whole. Once expanded, its sub-properties go to TagArray in clear. This happens when the parameter's [LogProperties] is Transitive, and when the property carries [LogProperties] itself. The same combination on a parameter, a classification together with [LogProperties], is refused with LOGGEN017. On a property it is accepted silently.
  2. Under Transitive = true, [LogPropertyIgnore] is not honoured. The ignored property is not expanded, but it is still logged whole. Without Transitive it is left out, as documented.
  3. Transitive on 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; Transitive is [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.Card redacted as in A, or each of its sub-properties classified as PersonalData. Failing that, a diagnostic like LOGGEN017.
  • E: no account.Password, as in D.
  • F: shipment.Address.Geo.Latitude and .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 Transitive and 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 Transitive with [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: GetTypePropertiesToLog fills the expanded property's ClassificationAttributeTypes (L161-L169), but TraverseParameterPropertiesTransitively calls back on leaves only (LoggingMethodParameterExtensions.cs#L40). The leaves' classification sets never receive the parent's. For a parameter, LOGGEN017 refuses 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: transitive is 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions