From 8e347d2232355e2eedb1cb9bb3652e826f4b30e9 Mon Sep 17 00:00:00 2001 From: Steve Dunn Date: Sun, 24 Dec 2023 07:10:15 +0000 Subject: [PATCH] Added logging tutorial --- docs/site/Writerside/hi.tree | 1 + .../topics/Working-with-logging-frameworks.md | 227 ++++++++++++++++++ 2 files changed, 228 insertions(+) create mode 100644 docs/site/Writerside/topics/Working-with-logging-frameworks.md diff --git a/docs/site/Writerside/hi.tree b/docs/site/Writerside/hi.tree index ca588b684d..b5ee223db2 100644 --- a/docs/site/Writerside/hi.tree +++ b/docs/site/Writerside/hi.tree @@ -17,6 +17,7 @@ + diff --git a/docs/site/Writerside/topics/Working-with-logging-frameworks.md b/docs/site/Writerside/topics/Working-with-logging-frameworks.md new file mode 100644 index 0000000000..324ba8ad4e --- /dev/null +++ b/docs/site/Writerside/topics/Working-with-logging-frameworks.md @@ -0,0 +1,227 @@ +# Working with logging frameworks + +In this tutorial, we'll see how to log instances of value objects. + +## Writing to the console + +In a new console application, add a NuGet reference to `Vogen`, and create: + +```c# +[ValueObject] +public readonly partial struct Age +{ +} +``` + +Now, create an instance and write it to the console: + +```c# +var age = Age.From(42); +Console.WriteLine("Age is " + age); +``` + +As expected, you'll see: + +```Bash +Age is 42 +``` + +Vogen generates a default `ToString` method that looks like this: + +```C# +public override string ToString() =>_isInitialized + ? Value.ToString() + : "[UNINITIALIZED]"; +``` + +If the value is uninitialized, for example, through deserialization, then it writes +uninitialized, otherwise, it uses the underlying type's `ToString` method. + +[//]: # (TODO: expand on ToString, either here, or in another page, and/or another How-to article) + +It is possible to override `ToString`—for example, you might want to pad the value with zeroes. Add the following +method to `Age` above: + +```C# +[ValueObject] +public readonly partial struct Age +{ + public override string ToString() =>_isInitialized + ? Value.ToString("D4") + : "[UNINITIALIZED]"; +} +``` + +That will now print: + +```Bash +Age is 0042 +``` + +## Using the default logging framework + +Next, we'll log values out using the default .NET logging framework. + +Add NuGet references to: +* `Microsoft.Extensions.Logging` +* `Microsoft.Extensions.Logging.Console` + +Next, add a namespace reference to `Microsoft.Extensions.Logging` and create an instance of a logger that writes +to the console: + +```c# +using Microsoft.Extensions.Logging; + +using var loggerFactory = LoggerFactory.Create(builder => +{ + builder.AddConsole(); +}); + +ILogger logger = loggerFactory.CreateLogger(); +``` + +Now, change the `Console.WriteLine` to: + +```C# +logger.LogInformation($"Age is {Age}", age); +``` + +The output is something like this: + +```Bash +info: Program[0] + Age is 0042 +``` + +So far, we've used the standard .NET logger to write to a console, and we've written a value object using structured +logging. + +Next, we'll look more at structured logging and switch to **Serilog**, a common choice for .NET. + +## Structured logging with Serilog + + +Structured logging means that you're logging events as data structures rather than formatted text messages. +With structured logging, you're essentially writing log events as structured data objects rather than plain text. +The structured data objects can then be stored, queried, and processed in ways that aren’t possible with text logs. + + +Structure logging is supported by the default .NET logger. +The structured properties are inside 'index placeholders', for example, `{Age}` above. +The default logger supports structured logging. +However, it can't fully use structured data. +For this, we'll use Serilog. + +* create a new console app +* add NuGet packages for `Vogen`, `Serilog`, and `Serilog.Sinks.Console` +* create the `Age` value object from above + +Add the following initialization: + +```C# +using Serilog; +using Vogen; + +Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .CreateLogger(); + + +Age age = Age.From(42); +Log.Information("Age is {Age}", age); +``` + +Run it again, and you'll see something like this: + +```Bash +[06:24:32 INF] Age is 0042 +``` + +Let's create a bit more structure. Add a `Person` object: + +```C# +public record Person(string Name, Age Age) +{ +} +``` + +Replace what is logged with: + +```C# +Log.Information("Person is {Person}", new Person("Joe", age)); +``` + +You'll see: + +```c# +[06:26:32 INF] Person is Person { Name = Joe, Age = 0042 } +``` + +In Serilog and some other .NET logging libraries that support structured logging, the `@` character in a placeholder +is called the destructuring operator. +When you use the @ character before a placeholder, it tells the logging system to serialize the object completely +instead of just calling ToString(). +This means the complete state of the object is logged, +which can give you much more useful information for complex objects. + +In our simple example, changing the logging line to: + +```C# +Log.Information("Person is {@Person}", new Person("Joe", age)); +``` + +Results in: +```Bash +Person is {"Name": "Joe", "Age": {"Value": 42, "$type": "Age"}, "$type": "Person"} +``` + +You can see that `Name` is written as: `Joe`. +But `Age` is written as: `{"Value": 42, "$type": "Age"}` + +You might find this output too detailed for value objects and that it pollutes the logs. + +We can tell Serilog to use the simpler format for value objects generated by Vogen. + +Add the following `Destructure` line to the initialization: + +```C# +Log.Logger = new LoggerConfiguration() + .Enrich.FromLogContext() + .WriteTo.Console() + .Destructure.With(new VogenDestructuringPolicy()) + .CreateLogger(); +``` + +And add this type to the project: + +```C# +public class VogenDestructuringPolicy : IDestructuringPolicy +{ + public bool TryDestructure( + object value, + ILogEventPropertyValueFactory propertyValueFactory, + out LogEventPropertyValue result) + { + if(value.GetType().GetCustomAttribute( + typeof(ValueObjectAttribute)) is ValueObjectAttribute) + { + result = new ScalarValue(value.ToString()); + return true; + } + + result = null; + return false; + } +} +``` + +Now, run again, and see that our value object `Age` is written more simply: + +```Bash +Person is {"Name": "Joe", "Age": "0042", "$type": "Person"} +``` + +In this tutorial, we've seen how to log value objects to the console and to the default logger. +We've also seen how to use structured logging and how to customize the output of value objects written to Serilog. +