Exception destructuring in Serilog - c#

Serilog has a convenient way of destructuring objects as shown in this example:
logger.Debug(exception, "This is an {Exception} text", exception);
logger.Debug(exception, "This is an {#Exception} structure", exception);
The first line causes the logger to log an exception as plain text (by calling ToString()), and the second line causes the logger to write exception properties as separate fields. But what about this overload:
logger.Debug(exception, "This is an exception", exception);
This one takes an exception as its first argument, and it is always written as a string. What I would like to make possible is to enable logging exception in a structured way. Is it possible to configure Serilog to achieve this?
UPDATE. I guess this question leads to another aspect of logging exceptions: how can I ensure that messages are enriched with exception properties (so they are logged in a structured way to the rich sinks like Elasticsearch) without writing all exception properties to the rendered text message (so plain text loggers are not filled with huge piles of exception details).

Take a look at Serilog.Exceptions logs exception details and custom properties that are not output in Exception.ToString().
This library has custom code to deal with extra properties on most common exception types and only falls back to using reflection to get the extra information if the exception is not supported by Serilog.Exceptions internally.
Add the NuGet package and then add the enricher like so:
using Serilog;
using Serilog.Exceptions;
ILogger logger = new LoggerConfiguration()
.Enrich.WithExceptionDetails()
.WriteTo.Sink(new RollingFileSink(
#"C:\logs",
new JsonFormatter(renderMessage: true))
.CreateLogger();
Your JSON logs will now be supplemented with detailed exception information and even custom exception properties. Here is an example of what happens when you log a DbEntityValidationException from EntityFramework (This exception is notorious for having deeply nested custom properties which are not included in the .ToString()).
try
{
...
}
catch (DbEntityValidationException exception)
{
logger.Error(exception, "Hello World");
}
The code above logs the following:
{
"Timestamp": "2015-12-07T12:26:24.0557671+00:00",
"Level": "Error",
"MessageTemplate": "Hello World",
"RenderedMessage": "Hello World",
"Exception": "System.Data.Entity.Validation.DbEntityValidationException: Message",
"Properties": {
"ExceptionDetail": {
"EntityValidationErrors": [
{
"Entry": null,
"ValidationErrors": [
{
"PropertyName": "PropertyName",
"ErrorMessage": "PropertyName is Required.",
"Type": "System.Data.Entity.Validation.DbValidationError"
}
],
"IsValid": false,
"Type": "System.Data.Entity.Validation.DbEntityValidationResult"
}
],
"Message": "Validation failed for one or more entities. See 'EntityValidationErrors' property for more details.",
"Data": {},
"InnerException": null,
"TargetSite": null,
"StackTrace": null,
"HelpLink": null,
"Source": null,
"HResult": -2146232032,
"Type": "System.Data.Entity.Validation.DbEntityValidationException"
},
"Source": "418169ff-e65f-456e-8b0d-42a0973c3577"
}
}
Serilog.Exceptions supports the .NET Standard and supports many common exception types without reflection but we'd like to add more, so please feel free to contribute.
Top Tip - Human Readable Stack Traces
You can use the Ben.Demystifier NuGet package to get human readable stack traces for your exceptions or the serilog-enrichers-demystify NuGet package if you are using Serilog.

There's a forum thread discussing this, in which a couple of solutions are presented. Thomas Bolon has created an 'exception destructuring' extension you can find in a Gist.
In this case you use only this syntax:
logger.Debug(exception, "This is an exception");
There's no need to add the exception into the format string.
To ensure the exception is printed to text sinks, just make sure {Exception} is included in the output template. The standard built-in ones already have this, e.g.:
outputTemplate: "{Timestamp} [{Level}] {Message}{NewLine}{Exception}";

This should be avoided altogether. Both ElasticSearch and Serilog aren't designed with the idea in mind that you will be serializing arbitrary objects. Logging objects with the conflicting shapes will result in mapping exceptions in ElasticSearch. If you are using the ElasticSearch sink in NuGet anything that results in a mapping conflict will be lost. Also Serilog does not handle Cyclical relationships so this will result in depth limiter selflog errors. There is a project that attempts to address this by destructuring into dictionaries and passing this to Serilog but you will still wind up with messy logs and mapping exceptions.
Serilog: https://nblumhardt.com/2016/02/serilog-tip-dont-serialize-arbitrary-objects/
I've found it best to be specific about logging exception properties based on what you find useful in the exception.

Related

AdaptiveCards throws an exception during .NET parsing

I currently encountered the following problem parsing an adaptive card.
This is the card:
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "{{DATE(${$root.AdditionalData['DUE-DATE']},COMPACT)}}",
"wrap": true
}
]
}
This is the card-content:
{
"AdditionalData": {
"DUE-DATE": "2021-09-10T16:29:59Z"
}
}
Code:
c# on .NET Framework 4.7.2 where layout is a string with the above card and content is a string with the above card-content:
AdaptiveCardTemplate template = new AdaptiveCardTemplate(layout);
string cardJson = template.Expand(content);
AdaptiveCardParseResult card = AdaptiveCard.FromJson(cardJson);
And it crashes with:
AdaptiveCards.AdaptiveSerializationException: 'Error reading string. Unexpected token: Undefined. Path 'text', line 1, position 137.'
JsonReaderException: Error reading string. Unexpected token: Undefined. Path 'text', line 1, position 137.
The generated JSON on cardJson looks wrong to me at the text property:
{"type":"AdaptiveCard","$schema":"http://adaptivecards.io/schemas/adaptive-card.json","version":"1.4","body":[{"type":"TextBlock","text":,"wrap":true}]}
I'm using the adaptive cards nuget packages:
AdaptiveCards 2.7.2
AdaptiveCards.Templating 1.2.
Did I encounter a parsing bug? The value for the text property should be 10.9.2021.
In the designer on adaptivecards.io everything works fine for some reason. Does anyone have a fix/workaround?
If you want the literal "text":"10.9.2021" to appear in your cardJson, use "${formatDateTime(AdditionalData['DUE-DATE'], 'd.M.yyyy')}" to generate the required value for your TextBlock:
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "${formatDateTime(AdditionalData['DUE-DATE'], 'd.M.yyyy')}",
"wrap": true
}
]
}
This causes all date formatting to be performed by the AdaptiveCardTemplate and results in:
{
"type":"AdaptiveCard",
"$schema":"http://adaptivecards.io/schemas/adaptive-card.json",
"version":"1.4",
"body":[
{
"type":"TextBlock",
"text":"10.9.2021",
"wrap":true
}
]
}
Demo fiddle #1 here.
If you would prefer "{{DATE(2021-09-10T16:29:59Z, COMPACT)}}" in your cardJson which delegates date formatting to the TextBlock, use:
{
"type": "AdaptiveCard",
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"version": "1.4",
"body": [
{
"type": "TextBlock",
"text": "{{DATE(${AdditionalData['DUE-DATE']}, COMPACT)}}",
"wrap": true
}
]
}
Which results in
"text": "{{DATE(2021-09-10T16:29:59Z, COMPACT)}}",
Demo fiddle #2 here.
Notes:
According to the Microsoft documentation:
Use Dot-notation to access sub-objects of an object hierarchy. E.g., ${myParent.myChild}
Use Indexer syntax to retrieve properties by key or items in an array. E.g., ${myArray[0]}
But, when accessing an object property with a hyphen (or some other reserved operator) in its name, it is apparently necessary to use the Indexer syntax ['DUE-DATE'] instead of Dot‑notation to retrieve its value, passing the property name inside a single-quoted string as the indexer.
According to the docs
There are a few reserved keywords to access various binding scopes. ...
"$root": "The root data object. Useful when iterating to escape to parent object",
Thus you do not need to use $root when accessing properties of the currently scoped object (which is, by default, the root) when using Dot-notation. If for whatever reason you need or want to address the root object directly, you may use $root like so:
"text": "${formatDateTime($root.AdditionalData['DUE-DATE'], 'd.M.yyyy')}",
Demo fiddle #3 here.
However, it seems that using $root in combination with {{DATE()}} causes malformed JSON to be generated. I.e.
"text": "{{DATE(${$root.AdditionalData['DUE-DATE']}, COMPACT)}}",
results in "text":, as indicated in your question.
Demo fiddle #4 here.
This looks to be a bug in the framework. Possibly the parser is choking on the sequence of tokens ${$, as your issue somewhat resembles Issue #6026: [Authoring][.NET][Templating] Inconsistency in accessing $root inside a $when property in adaptive card templating which reports a failure to parse "$when": "${$root.UserName != null}" correctly.
You can avoid the problem either by omitting $root entirely, or by wrapping $root.AdditionalData['DUE-DATE'] in an additional formatDateTime() like so:
"text": "{{DATE(${formatDateTime($root.AdditionalData['DUE-DATE'])}, COMPACT)}}",
Resulting in
"text": "{{DATE(2021-09-10T16:29:59.000Z, COMPACT)}}",
Demo fiddle #5 here.
From the documentation page Adaptive Card Templating SDKs: Troubleshooting:
Q. Why date/time in RFC 3389 format e.g "2017-02-14T06:08:00Z" when used with template doesn't works with TIME/DATE functions?
A. .NET sdk nuget version 1.0.0-rc.0 exhibits this behavior. this behavior is corrected in the subsequent releases... Please use formatDateTime() function to format the date/time string to RFC 3389 as seen in this example, or you can bypass TIME/DATE functions, and just use formatDateTime(). For more information on formatDateTime(), please go here.
While this recommendation to use formatDateTime was to fix a problem in 1.0.0-rc.0, the trick also resolves the issue mentioned in note #2 above.

NSwag: Generation of a C# client results in invalid code

I am trying to generate a C# client for the Trello API. Therefore I have downloaded the Open API specification from https://developer.atlassian.com/cloud/trello/swagger.v3.json and run the following nswag command from the bash.
nswag openapi2csclient /input:swagger.v3.json \
/classname:TrelloClient \
/namespace:Integrations.Trello \
/output:TrelloClient.cs \
/GenerateClientInterfaces:True \
/GenerateExceptionClasses:True \
/GenerateClientClasses:True \
/DisposeHttpClient:False \
/OperationGenerationMode:SingleClientFromOperationId
The code generation completes without errors, but the generated code does not compile because it contains many errors. For example, some of the generated method names contain invalid expressions like =idAsync, or method signatures have ambiguous parameters (for instance, multiple key and token parameters of type string). The following method declaration has been generated for the GetMembers method, which is obviously the wrong syntax.
System.Threading.Tasks.Task<Member> GetMembers=idAsync(
string key, string token, string id, string actions, string boards, BoardBackgrounds? boardBackgrounds,
BoardsInvited? boardsInvited, BoardFields? boardsInvited_fields, bool? boardStars, string cards,
CustomBoardBackgrounds? customBoardBackgrounds, CustomEmoji2? customEmoji, CustomStickers? customStickers,
MemberFields? fields, string notifications, Organizations? organizations,
OrganizationFields? organization_fields, bool? organization_paid_account,
OrganizationsInvited? organizationsInvited, OrganizationFields? organizationsInvited_fields,
bool? paid_account, bool? savedSearches, Tokens? tokens);
Are there any special options that need to be set when processing an Open API specification document of version 3?
I was able to solve syntax issues regarding the get-members method in the generated code by changing the OperationGenerationMode from SingleClientFromOperationId to SingleClientFromPathSegments. This works as a workaround because the =id term only appears in the operationId.
I inspected Trello´s Open API specification document and found out that the definition of get-members contained the =id term in the operationId; not sure if this is wrong in the spec, or the generator is not able to handle that case correctly. The definition of the get-members method looks like that (foreshortened):
... "/members/{id}":
{
"get":
{
"tags": [],
"operationId":"get-members=id",
"parameters" ...
}
}
As a sidenote, the reason why I use the SingleClient... operation modes is that Nswag also adds GeneratedCode attributes to any partial class and interface which also results in invalid code (this attribute can only be applied once). The SingleClient... operation mode solves that.
What remains is the problem that NSwag generates methods with duplicated parameters, which has been reported as an issue, but is not solved yet; see https://github.com/RicoSuter/NSwag/issues/2560 for further information. Last but not least, I tried to remove the =id from the operationId and switched back to the SingleClientFromOperationId operation mode to check whether this does the trick, which is not the case.
I assume that NSwag is used by a broad audience which makes me think that this must be more related to the processed spec than Nswag. Thus, I looked up the spec for methods that have been reported by the C# compiler to have duplicated parameters. Here is an example.
...
{
"name": "token",
"in": "query",
"description": "The API token to use",
"required": true,
"schema": {
"$ref": "#/components/schemas/APIToken"
}
},
{
"name": "token",
"in": "path",
"description": "",
"required": true,
"schema": {
"type": "string"
}
}
...
So it seems that a method can have parameters of the same name, but they can appear in different places such a the query-string, or be part of the URL path. Then, it might be Nswag that produces the wrong output; if it is intended that both parameters can be sent to the API, then the generator should just prefix the names to avoid ambiguities, for instance, queryToken and pathToken, leaving it up to the developer to decide what parameter to use or to give a hint to the expected value (in case both parameters are required and expect different values).

AspNetCore Health Checks UI Reporting Wrongly

I've been having a little issue implementing AspNetCore.HealthChecks.UI correctly. I have two endpoints that run health checks using the "live" and "ready" as tags. Both endpoints work work as expected but after implementing the HealthChecksUI, The health check results displayed is always "Unhealthy
An error occurred while sending the request." even though the endpoint returns "Healthy" from postman. Please see screenshots and relevant code snippets and configurations.
AppSettings Configuration
"HealthChecksUI": {
"HealthChecks": [
{
"Name": "Crowd Funding App",
"Uri": "http://localhost:5001/healthui"
}
],
"EvaluationTimeinSeconds": 10,
"MinimumSecondsBetweenFailureNotifications": 60
}
In the Configure function of the startup class, I have the following code.

How to add a parameter to .Net Core structured logging without referencing it in a message?

I can do this in .net core
_logger.LogInformation("Token validated {clientId}", "MyId");
And then logging libraries like NLog will know that there is a property called clientId with the value MyId in the message and can render it in a special way.
I am trying to do the same without including the property in the message itself, but cannot manage to nail it. This is what I have done so far and it does not result in a property in NLog:
LogEventInfo info = new LogEventInfo
{
Properties = {{"clientId", "MyId"}},
};
_logger.Log(Microsoft.Extensions.Logging.LogLevel.Information, "Token validated", info, null, info.MessageFormatter);
This results in a message without property. Is there a better way to do this or have I done something wrong?
The whole idea with Microsoft-Extension-Logging (MEL) ILogger-interface is not being dependent on a specific Logging-Framework.
If you start creating NLog LogEventInfo-objects, then you might as well call NLog.LogManager.GetCurrentClassLogger() and use that as Logger.
But maybe this wiki-page can give you some ideas:
https://github.com/NLog/NLog.Extensions.Logging/wiki/NLog-properties-with-Microsoft-Extension-Logging

JObject/JToken doesnt Log on NLog

I have a class and some of properties are dynamic, when my class is pass to some action API the method undestand as a JObject, the problem is to Log on NLOG, when I try to do that, my log from my properties Dynamic shows up "[]" example bellow:
myProperty:[
[[]],
[],
[]
]
When I try convert all over my class Objet to JObject(Newtonsoft) all my log will be [] like bellow:
{ "time": "2019-05-13 18:12:16.2224", "level": "DEBUG", "JsonProperties": { "log": [[[[[]],[[]],[[]],[[[[]],[[]],[[]]]],[[[[[[[[]],[[]],[[]],[[]],[[]],[[]],[[]],[[]]]]],[[[[]],[[]],[[]],[[]],[[]],[[[[]],[[]],[[]]]],[[[[[]],[[]]],[[[]],[[]]]]],[[[[[]],[[]],[[]]],[[[]],[[]],[[]]]]],[[[[[]],[[]]],[[[]],[[]]]]]]]]]]]],[[[[[]],[[]],[[]],[[[[]],[[]],[[]],[[]]]]]]]] }, "message": "Testando performance no LoggerGenerator.", "log": [[],[]] }
I solved this problem converts JObject to Dictionary and Works perfectly, the problem is overhead to do that.
I need to solve this problem without create my own method and converts to Dictionary
Thanks.
JObject is an IEnumerable and NLog will try to enumerate it.
You can do the following:
logger.Info("Hello {0}", jObject); // No structured logging, becomes string.Format
logger.Info("Hello {$myobj}", jObject); // Structured logging that forces JObject.ToString
logger.Info("Hello {myobj}", jObject.ToString()); // Converts to string upfront
You can also customize how NLog handles special objects (Like JObject) by overriding these:
NLog.Config.ConfigurationItemFactory.Default.ValueFormatter
NLog.Config.ConfigurationItemFactory.Default.JsonConverter

Categories

Resources