When you write a message, open an activity, or close an activity, you can specify additional properties. Properties are name-value pairs that are passed to the logging backend. Unlike message parameters, properties are not rendered into the message text by default. When defined on activities, properties can be inherited by child contexts and activities.
Defining properties with an anonymous type
Every method that allows you to write a message, open an activity, or close an activity accepts an optional parameter named options. The type of this parameter has a constructor that takes a parameter of type object called data. You can set properties on the message or activity by passing an anonymous object to this constructor.
For instance, the Write<T>(in T, in WriteMessageOptions) method accepts an options parameter of type WriteMessageOptions, and this type has a constructor that accepts an object.
logSource.Default.Write( Formatted( "Hello, World." ),
new WriteMessageOptions( new { User = User.Identity.Name, Day = DateTime.Today.DayOfWeek } ) );
The code above adds two properties to the message: User and Day.
You can pass any .NET type to the data parameter of the options constructor. By default, all public instance properties will be exported as logging properties unless they throw an exception.
Defining inherited properties
By default, all properties defined on an activity apply to the opening and closing message of that activity only. However, it is sometimes useful to include a property in all messages under this activity and under its child activities. This can be done using inherited properties.
You can define an inherited property by annotating it with the LoggingPropertyOptionsAttribute custom attribute and setting its IsInherited property to true. Of course, you cannot do that with an anonymous type, so you will have to define your own.
You can also use other properties of LoggingPropertyOptionsAttribute: IsIgnored, IsRendered and IsBaggage.
For instance, the following class exports a single property to PostSharp Logging, and this property will be inherited by all children messages and activities:
private class PropertiesWithAttributes
{
[LoggingPropertyOptions( IsIgnored = true )]
public string Ignored { get; set; }
[LoggingPropertyOptions( IsInherited = true )]
public string Inherited { get; set; }
}
Advanced scenarios with logging properties
When you define properties using a normal .NET object, the object that you pass is wrapped in a LogEventData. This type has two properties: Data and Metadata. The second property, of type LogEventMetadata, is responsible for mapping the value of the Data property to logging properties. You can customize this mapping by providing your own implementation of the LogEventMetadata class.
This design, which separates data from metadata, allows you to pass several properties without even allocating a single object on the heap. For instance, you can pass an existing HttpContext to PostSharp Logging and use a custom LogEventMetadata to decide how this HttpContext should be presented to logging backends.
The following example shows how to expose the HTTP context to properties (this is an abbreviated version of what is implemented by AspNetCoreLogging):
internal abstract class AspNetCoreMetadata : LogEventMetadata<AspNetCoreRequestExpressionModel>
{
protected AspNetCoreMetadata() : base( "AspNetCoreRequest" )
{
}
public override void VisitProperties<TVisitorState>( object data, ILoggingPropertyVisitor<TVisitorState> visitor,
ref TVisitorState visitorState, in LoggingPropertyVisitorOptions visitorOptions = default )
{
this.VisitProperties( (HttpContext) data, visitor, ref visitorState );
}
protected void Visit<TVisitorState>( string name, object value, ILoggingPropertyVisitor<TVisitorState> visitor,
ref TVisitorState visitorState, bool isBaggage = false )
{
if ( value != null && !(value is string s && string.IsNullOrEmpty( s )) )
{
visitor.Visit( name, value, new LoggingPropertyOptions( isBaggage: isBaggage ), ref visitorState );
}
}
protected void VisitProperties<TVisitorState>( HttpContext httpContext, ILoggingPropertyVisitor<TVisitorState> visitor,
ref TVisitorState visitorState )
{
this.Visit( "User", httpContext.User?.Identity?.Name, visitor, ref visitorState );
this.Visit( "RemoteIpAddress", httpContext.Connection.RemoteIpAddress, visitor, ref visitorState );
this.Visit( "ConnectionId", httpContext.Connection.Id, visitor, ref visitorState );
HttpRequest request = httpContext.Request;
this.Visit( "Method", request.Method, visitor, ref visitorState );
this.Visit( "Path", request.Path, visitor, ref visitorState );
this.Visit( "Protocol", request.Protocol, visitor, ref visitorState );
if ( request.QueryString.HasValue )
{
this.Visit( "QueryString", request.QueryString.Value, visitor, ref visitorState );
}
this.Visit( "Scheme", request.Scheme, visitor, ref visitorState );
foreach ( KeyValuePair<string, StringValues> header in request.Headers )
{
this.Visit( header.Key, header.Value.ToString(), visitor, ref visitorState );
}
}
}
You will also create a custom LogEventMetadata if you want to define custom transactions. See Defining Your Own Logging Transactions for details.