├── .gitignore ├── LICENSE ├── README.md └── src ├── InterpolatedLogging.Microsoft.Extensions.Logging.Tests ├── BenchmarkTests-Result.png ├── BenchmarkTests.cs ├── InterpolatedLogging.Microsoft.Extensions.Logging.Tests.csproj └── Program.cs ├── InterpolatedLogging.Microsoft.Extensions.Logging ├── InterpolatedLogging.Microsoft.Extensions.Logging.csproj ├── InterpolatedLogging.Microsoft.Extensions.Logging.nuspec ├── InterpolatedLoggingExtensions.cs └── NuGetReadMe.md ├── InterpolatedLogging.NLog ├── InterpolatedLogging.NLog.csproj ├── InterpolatedLogging.NLog.nuspec ├── InterpolatedLoggingExtensions.cs └── NuGetReadMe.md ├── InterpolatedLogging.Serilog.Tests ├── BenchmarkTests.cs ├── InterpolatedLogging.Serilog.Tests.csproj └── Program.cs ├── InterpolatedLogging.Serilog ├── InterpolatedLogging.Serilog.csproj ├── InterpolatedLogging.Serilog.nuspec ├── InterpolatedLoggingExtensions.cs └── NuGetReadMe.md ├── InterpolatedLogging.Tests ├── BasicTests.cs ├── InterpolatedLogging.Tests.csproj └── JoinableStringTests.cs ├── InterpolatedLogging.sln ├── InterpolatedLogging ├── InterpolatedLogging.csproj ├── JoinableString.cs ├── NamedProperty.cs └── StructuredLogMessage.cs └── build-release.ps1 /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | ## 4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore 5 | 6 | # User-specific files 7 | *.rsuser 8 | *.suo 9 | *.user 10 | *.userosscache 11 | *.sln.docstates 12 | 13 | # User-specific files (MonoDevelop/Xamarin Studio) 14 | *.userprefs 15 | 16 | # Mono auto generated files 17 | mono_crash.* 18 | 19 | # Build results 20 | [Dd]ebug/ 21 | [Dd]ebugPublic/ 22 | [Rr]elease/ 23 | [Rr]eleases/ 24 | x64/ 25 | x86/ 26 | [Aa][Rr][Mm]/ 27 | [Aa][Rr][Mm]64/ 28 | bld/ 29 | [Bb]in/ 30 | [Oo]bj/ 31 | [Ll]og/ 32 | [Ll]ogs/ 33 | 34 | # Visual Studio 2015/2017 cache/options directory 35 | .vs/ 36 | # Uncomment if you have tasks that create the project's static files in wwwroot 37 | #wwwroot/ 38 | 39 | # Visual Studio 2017 auto generated files 40 | Generated\ Files/ 41 | 42 | # MSTest test Results 43 | [Tt]est[Rr]esult*/ 44 | [Bb]uild[Ll]og.* 45 | 46 | # NUnit 47 | *.VisualState.xml 48 | TestResult.xml 49 | nunit-*.xml 50 | 51 | # Build Results of an ATL Project 52 | [Dd]ebugPS/ 53 | [Rr]eleasePS/ 54 | dlldata.c 55 | 56 | # Benchmark Results 57 | BenchmarkDotNet.Artifacts/ 58 | 59 | # .NET Core 60 | project.lock.json 61 | project.fragment.lock.json 62 | artifacts/ 63 | 64 | # StyleCop 65 | StyleCopReport.xml 66 | 67 | # Files built by Visual Studio 68 | *_i.c 69 | *_p.c 70 | *_h.h 71 | *.ilk 72 | *.meta 73 | *.obj 74 | *.iobj 75 | *.pch 76 | *.pdb 77 | *.ipdb 78 | *.pgc 79 | *.pgd 80 | *.rsp 81 | *.sbr 82 | *.tlb 83 | *.tli 84 | *.tlh 85 | *.tmp 86 | *.tmp_proj 87 | *_wpftmp.csproj 88 | *.log 89 | *.vspscc 90 | *.vssscc 91 | .builds 92 | *.pidb 93 | *.svclog 94 | *.scc 95 | 96 | # Chutzpah Test files 97 | _Chutzpah* 98 | 99 | # Visual C++ cache files 100 | ipch/ 101 | *.aps 102 | *.ncb 103 | *.opendb 104 | *.opensdf 105 | *.sdf 106 | *.cachefile 107 | *.VC.db 108 | *.VC.VC.opendb 109 | 110 | # Visual Studio profiler 111 | *.psess 112 | *.vsp 113 | *.vspx 114 | *.sap 115 | 116 | # Visual Studio Trace Files 117 | *.e2e 118 | 119 | # TFS 2012 Local Workspace 120 | $tf/ 121 | 122 | # Guidance Automation Toolkit 123 | *.gpState 124 | 125 | # ReSharper is a .NET coding add-in 126 | _ReSharper*/ 127 | *.[Rr]e[Ss]harper 128 | *.DotSettings.user 129 | 130 | # TeamCity is a build add-in 131 | _TeamCity* 132 | 133 | # DotCover is a Code Coverage Tool 134 | *.dotCover 135 | 136 | # AxoCover is a Code Coverage Tool 137 | .axoCover/* 138 | !.axoCover/settings.json 139 | 140 | # Visual Studio code coverage results 141 | *.coverage 142 | *.coveragexml 143 | 144 | # NCrunch 145 | _NCrunch_* 146 | .*crunch*.local.xml 147 | nCrunchTemp_* 148 | 149 | # MightyMoose 150 | *.mm.* 151 | AutoTest.Net/ 152 | 153 | # Web workbench (sass) 154 | .sass-cache/ 155 | 156 | # Installshield output folder 157 | [Ee]xpress/ 158 | 159 | # DocProject is a documentation generator add-in 160 | DocProject/buildhelp/ 161 | DocProject/Help/*.HxT 162 | DocProject/Help/*.HxC 163 | DocProject/Help/*.hhc 164 | DocProject/Help/*.hhk 165 | DocProject/Help/*.hhp 166 | DocProject/Help/Html2 167 | DocProject/Help/html 168 | 169 | # Click-Once directory 170 | publish/ 171 | 172 | # Publish Web Output 173 | *.[Pp]ublish.xml 174 | *.azurePubxml 175 | # Note: Comment the next line if you want to checkin your web deploy settings, 176 | # but database connection strings (with potential passwords) will be unencrypted 177 | *.pubxml 178 | *.publishproj 179 | 180 | # Microsoft Azure Web App publish settings. Comment the next line if you want to 181 | # checkin your Azure Web App publish settings, but sensitive information contained 182 | # in these scripts will be unencrypted 183 | PublishScripts/ 184 | 185 | # NuGet Packages 186 | *.nupkg 187 | # NuGet Symbol Packages 188 | *.snupkg 189 | # The packages folder can be ignored because of Package Restore 190 | **/[Pp]ackages/* 191 | # except build/, which is used as an MSBuild target. 192 | !**/[Pp]ackages/build/ 193 | # Uncomment if necessary however generally it will be regenerated when needed 194 | #!**/[Pp]ackages/repositories.config 195 | # NuGet v3's project.json files produces more ignorable files 196 | *.nuget.props 197 | *.nuget.targets 198 | 199 | # Microsoft Azure Build Output 200 | csx/ 201 | *.build.csdef 202 | 203 | # Microsoft Azure Emulator 204 | ecf/ 205 | rcf/ 206 | 207 | # Windows Store app package directories and files 208 | AppPackages/ 209 | BundleArtifacts/ 210 | Package.StoreAssociation.xml 211 | _pkginfo.txt 212 | *.appx 213 | *.appxbundle 214 | *.appxupload 215 | 216 | # Visual Studio cache files 217 | # files ending in .cache can be ignored 218 | *.[Cc]ache 219 | # but keep track of directories ending in .cache 220 | !?*.[Cc]ache/ 221 | 222 | # Others 223 | ClientBin/ 224 | ~$* 225 | *~ 226 | *.dbmdl 227 | *.dbproj.schemaview 228 | *.jfm 229 | *.pfx 230 | *.publishsettings 231 | orleans.codegen.cs 232 | 233 | # Including strong name files can present a security risk 234 | # (https://github.com/github/gitignore/pull/2483#issue-259490424) 235 | #*.snk 236 | 237 | # Since there are multiple workflows, uncomment next line to ignore bower_components 238 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622) 239 | #bower_components/ 240 | 241 | # RIA/Silverlight projects 242 | Generated_Code/ 243 | 244 | # Backup & report files from converting an old project file 245 | # to a newer Visual Studio version. Backup files are not needed, 246 | # because we have git ;-) 247 | _UpgradeReport_Files/ 248 | Backup*/ 249 | UpgradeLog*.XML 250 | UpgradeLog*.htm 251 | ServiceFabricBackup/ 252 | *.rptproj.bak 253 | 254 | # SQL Server files 255 | *.mdf 256 | *.ldf 257 | *.ndf 258 | 259 | # Business Intelligence projects 260 | *.rdl.data 261 | *.bim.layout 262 | *.bim_*.settings 263 | *.rptproj.rsuser 264 | *- [Bb]ackup.rdl 265 | *- [Bb]ackup ([0-9]).rdl 266 | *- [Bb]ackup ([0-9][0-9]).rdl 267 | 268 | # Microsoft Fakes 269 | FakesAssemblies/ 270 | 271 | # GhostDoc plugin setting file 272 | *.GhostDoc.xml 273 | 274 | # Node.js Tools for Visual Studio 275 | .ntvs_analysis.dat 276 | node_modules/ 277 | 278 | # Visual Studio 6 build log 279 | *.plg 280 | 281 | # Visual Studio 6 workspace options file 282 | *.opt 283 | 284 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) 285 | *.vbw 286 | 287 | # Visual Studio LightSwitch build output 288 | **/*.HTMLClient/GeneratedArtifacts 289 | **/*.DesktopClient/GeneratedArtifacts 290 | **/*.DesktopClient/ModelManifest.xml 291 | **/*.Server/GeneratedArtifacts 292 | **/*.Server/ModelManifest.xml 293 | _Pvt_Extensions 294 | 295 | # Paket dependency manager 296 | .paket/paket.exe 297 | paket-files/ 298 | 299 | # FAKE - F# Make 300 | .fake/ 301 | 302 | # CodeRush personal settings 303 | .cr/personal 304 | 305 | # Python Tools for Visual Studio (PTVS) 306 | __pycache__/ 307 | *.pyc 308 | 309 | # Cake - Uncomment if you are using it 310 | # tools/** 311 | # !tools/packages.config 312 | 313 | # Tabs Studio 314 | *.tss 315 | 316 | # Telerik's JustMock configuration file 317 | *.jmconfig 318 | 319 | # BizTalk build output 320 | *.btp.cs 321 | *.btm.cs 322 | *.odx.cs 323 | *.xsd.cs 324 | 325 | # OpenCover UI analysis results 326 | OpenCover/ 327 | 328 | # Azure Stream Analytics local run output 329 | ASALocalRun/ 330 | 331 | # MSBuild Binary and Structured Log 332 | *.binlog 333 | 334 | # NVidia Nsight GPU debugger configuration file 335 | *.nvuser 336 | 337 | # MFractors (Xamarin productivity tool) working folder 338 | .mfractor/ 339 | 340 | # Local History for Visual Studio 341 | .localhistory/ 342 | 343 | # BeatPulse healthcheck temp database 344 | healthchecksdb 345 | 346 | # Backup folder for Package Reference Convert tool in Visual Studio 2017 347 | MigrationBackup/ 348 | 349 | # Ionide (cross platform F# VS Code tools) working folder 350 | .ionide/ 351 | 352 | # Project specific 353 | /src/*/*.xml 354 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Rick Drizin 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Nuget](https://img.shields.io/nuget/v/InterpolatedLogging.Serilog?label=InterpolatedLogging.Serilog)](https://www.nuget.org/packages/InterpolatedLogging.Serilog) 2 | [![Nuget](https://img.shields.io/nuget/v/InterpolatedLogging.Microsoft.Extensions.Logging?label=InterpolatedLogging.Microsoft.Extensions.Logging)](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging) 3 | [![Nuget](https://img.shields.io/nuget/v/InterpolatedLogging.NLog?label=InterpolatedLogging.NLog)](https://www.nuget.org/packages/InterpolatedLogging.NLog) 4 | 5 | # Interpolated Logging 6 | 7 | **Extensions to Logging Libraries to write Log Messages using Interpolated Strings without losing Structured Property Names** 8 | 9 | ## Introduction 10 | 11 | Most logging libraries support **structured logging**: 12 | 13 | ```cs 14 | logger.Info("User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", 15 | name, orderId, DateTime.Now, elapsedTime); 16 | ``` 17 | 18 | This means that our logs will get not only plain strings but also the structured data, allowing us to search for specific property values (e.g. search for `OrderId="123"` to trace some order, or search for `OperationElapsedTime>1000` to find slow operations). 19 | 20 | The problem with this approach is that it's easy to put the wrong number of parameters or wrong order of parameters (the parameters at the end are **positional**, they are not matched with the message by their names). 21 | 22 | If we just used regular interpolated strings we would lose the benefit of structured logging, since the logging library won't know the names of each property. 23 | 24 | ## How it works 25 | 26 | Our extension methods solve this problem by extending popular logging libraries with extension methods that allow the use of interpolated strings while still being able to define the name of the properties: 27 | 28 | **Syntax 1: Colon-syntax:** 29 | ```cs 30 | // Define property names after the variables using colon-syntax ( {variable:propertyName} ) 31 | logger.InterpolatedInfo($"User {name:UserName} created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 32 | ``` 33 | 34 | **Syntax 2: NP-syntax:** 35 | ```cs 36 | // Define property names using explicit NP (NamedProperty) helper 37 | logger.InterpolatedInfo($"User {NP(name, "UserName")} created Order {NP(orderId, "OrderId")} at {NP(now, "Date")}, operation took {NP(elapsedTime, "OperationElapsedTime")}ms"); 38 | ``` 39 | **Syntax 3: Anonymous-objects:** 40 | ```cs 41 | // Define property names using anonymous objects 42 | logger.InterpolatedInfo($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 43 | ``` 44 | 45 | All those 3 syntaxes above are equivalent (and have near identical performance), so pick the one you like better. 46 | (I think colon-syntax is the most concise and easy to remember, but NP-syntax might be better if you want your property names to refer to existing constants or to use `nameof`). 47 | 48 | The result is that your logging library will be invoked with this template and these properties below: 49 | 50 | ```cs 51 | "User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms" 52 | // ... and the properties in order: name, orderId, DateTime.Now, elapsedTime. 53 | ``` 54 | 55 | ## Why can't I just use regular string interpolation? 56 | 57 | If you were just using regular string interpolation (without our extension methods) the final rendered log (plain message) tracked in your logging library would be the same, but the received template would be 58 | different for each log entry (template identical to the rendered message), and the properties would be tracked without meaningful names - they would be like `{0}="Rick"`, `{1}=1001`, etc. 59 | This causes two problems: 60 | - You can't group your logs by the template, since each log will have a different template 61 | - You can't search in your logging database for something like `WHERE UserName=="Rick"`. The best you could do would be like `WHERE Message LIKE "User * created Order * at *" AND Props.{0}="Rick"` - but structured logging can do much better than this. 62 | 63 | # Quickstart 64 | 65 | 1. Install the **NuGet package** according to your logging library ([Serilog](https://www.nuget.org/packages/InterpolatedLogging.Serilog), [Microsoft.Extensions.Logging](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) or [NLog](https://www.nuget.org/packages/InterpolatedLogging.NLog/)) 66 | 1. Start using like this: 67 | 68 | ```cs 69 | // for easier usage our extension methods use the same namespace of the logging libraries 70 | // using Serilog; 71 | // or using Microsoft.Extensions.Logging; 72 | // or using NLog; 73 | using static InterpolatedLogging.NamedProperties; // for using the short NP helper you need this 74 | // ... 75 | 76 | logger.InterpolatedInformation($"User {name:UserName} created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 77 | // there are also extensions for Debug, Verbose (or Trace depending on your logging library), etc, and there are also overloads that take an Exception. 78 | ``` 79 | 80 | # Supported Logging Libraries 81 | 82 | Current supported libraries: 83 | 84 | Library | Status | NuGet Package 85 | ------------ | ------------- | ------------- 86 | **Serilog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Serilog/) 87 | **Microsoft.Extensions.Logging** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) 88 | **NLog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.NLog/) 89 | log4net | Pending | 90 | 91 | 92 | # Advanced Features 93 | 94 | ## Raw strings 95 | 96 | If you want to embed raw strings in your messages (don't want them to be saved as structured properties), you don't need to create an anonymous object and you can just use the **raw modifier**: 97 | 98 | ```cs 99 | logger.InterpolatedInformation($"User {new { UserName = name }} logged as {role:raw}"); 100 | ``` 101 | 102 | ## Destructuring operator 103 | 104 | Serilog and NLog supports the `@` destructuring operator which makes a single property be stored with its internal structure (instead of just invoking `ToString()` and saving the serialized property). 105 | 106 | ```cs 107 | var input = new { Latitude = 25, Longitude = 134 }; 108 | 109 | // colon-syntax: 110 | logger.Information($"Processed {input:@SensorInput}."); 111 | 112 | // NP-syntax: 113 | logger.Information($"Processed {NP(input, "@SensorInput")}."); 114 | 115 | // or anonymous object syntax (put the @ before the interpolated block, since @ is not allowed in identifiers) 116 | logger.Information($"Processed @{ new { SensorInput = input }}."); 117 | 118 | // in plain Serilog this would be equivalent of: 119 | //logger.Information("Processed {@SensorInput}.", input); 120 | ``` 121 | 122 | ## Format specifiers 123 | 124 | You can obviously pre-format your objects and log them as formatted strings. 125 | But if you want to pass the original object and yet define some specific format for it during the message rendering you can specify format specifier as you would regularly do in an interpolated string: 126 | 127 | ```cs 128 | int time = 15; // 15 milliseconds 129 | 130 | // colon-syntax (property name comes first! then another colon and the format specifier): 131 | logger.Information($"Processed order in {time:TimeMS:000} ms."); 132 | 133 | // NP-syntax: 134 | logger.Information($"Processed order in {NP(time, "TimeMS"):000} ms."); 135 | 136 | // or anonymous object syntax 137 | logger.Information($"Processed order in { new { TimeMS = time}:000} ms."); 138 | 139 | // in plain Serilog this would be equivalent of: 140 | //logger.Information("Processed order in {TimeMS:000}ms.", input, time); 141 | ``` 142 | 143 | # Performance / Benchmarks 144 | 145 | Some people are worried that using FormattableString is much slower than using the regular strings. And some have mentioned that Microsoft is working on a proposal for a [Logging Generator](https://github.com/dotnet/designs/blob/main/accepted/2021/logging-generator.md) 146 | which uses C# 9 source generators to convert decorated partial-methods into strongly-typed logging functions (without needing string interpolation). 147 | 148 | There are benchmark tests [here](https://github.com/Drizin/InterpolatedLogging/tree/main/src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/BenchmarkTests.cs) comparing how much overhead InterpolatedLogging adds on top of this Microsoft strongly-typed solution, 149 | and the results show that InterpolatedLogging (using `FormattableString`) timings are in the same order of magnitude. When using Console sink the performance overhead was between 4% and 19%, and this difference probably gets smaller when using external sinks (like ElasticSearch/Splunk). If you're doing async logging this latency increment is probably negligible. 150 | 151 | ![Benchmarks - Microsoft LoggingGenerator](src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/BenchmarkTests-Result.png) 152 | 153 | 154 | 155 | 156 | # Collaborate 157 | 158 | This is a brand new project, and your contribution can help a lot. 159 | 160 | **Would you like to collaborate?** 161 | 162 | Please submit a pull request or if you prefer you can [create an issue](https://github.com/Drizin/InterpolatedLogging/issues) to discuss your idea. 163 | 164 | ## License 165 | MIT License 166 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/BenchmarkTests-Result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Drizin/InterpolatedLogging/8fb3d7da3996d4f49ceb521b792ad7be11c03b0b/src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/BenchmarkTests-Result.png -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/BenchmarkTests.cs: -------------------------------------------------------------------------------- 1 | using Microsoft.Extensions.Logging; 2 | using NUnit.Framework; 3 | using System; 4 | using System.Collections; 5 | using System.Collections.Generic; 6 | using System.Data; 7 | using System.Data.SqlClient; 8 | using System.Diagnostics; 9 | using System.Linq; 10 | using System.Reflection; 11 | using System.Threading; 12 | using static InterpolatedLogging.NamedProperties; 13 | 14 | namespace InterpolatedLoggingTests.MicrosoftExtensionsLogging 15 | { 16 | public class BenchmarkTests 17 | { 18 | 19 | static ILogger CreateLogger(LogLevel logLevel, bool console = true) 20 | { 21 | var fac = LoggerFactory.Create((builder) => 22 | { 23 | builder.SetMinimumLevel(logLevel); 24 | if (console) 25 | builder.AddConsole(); 26 | else 27 | builder.AddEventLog(); 28 | }); 29 | var logger = new Logger(fac); 30 | return logger; 31 | } 32 | 33 | [TestCase(LogLevel.Debug)] 34 | [TestCase(LogLevel.Critical)] 35 | public void BenchmarkTest1Parameter(LogLevel logLevel) 36 | { 37 | int iterations = (logLevel == LogLevel.Debug ? 100000 : 10000000); // printing makes it very slow, so we have to reduce the iterations 38 | Console.WriteLine($"Running test {nameof(BenchmarkTest1Parameter)} with level {logLevel} ({iterations} iterations)..."); 39 | 40 | var logger = CreateLogger(logLevel); 41 | 42 | // Microsoft Logging Generator ( https://github.com/geeknoid/LoggingGenerator ) 43 | Console.WriteLine("Microsoft Logging Generator..."); Thread.Sleep(2000); 44 | var sw = Stopwatch.StartNew(); 45 | for (int i = 0; i < iterations; i++) 46 | { 47 | Log.CouldNotOpenSocket(logger, Environment.MachineName); 48 | } 49 | sw.Stop(); 50 | long elapsedMilliseconds1 = sw.ElapsedMilliseconds; 51 | 52 | 53 | // InterpolatedLogging 54 | Console.WriteLine("InterpolatedLogging..."); Thread.Sleep(2000); 55 | sw = Stopwatch.StartNew(); 56 | for (int i = 0; i < iterations; i++) 57 | { 58 | logger.InterpolatedDebug($"Could not open socket to `{Environment.MachineName:hostName}`"); 59 | } 60 | sw.Stop(); 61 | long elapsedMilliseconds2 = sw.ElapsedMilliseconds; 62 | 63 | Thread.Sleep(3000); // let loggers flush their buffers 64 | Console.WriteLine($"Microsoft Logging Generator (1 argument): {elapsedMilliseconds1}ms"); 65 | Console.WriteLine($"InterpolatedLogging (1 argument): {elapsedMilliseconds2}ms"); 66 | } 67 | 68 | [Test] 69 | [TestCase(LogLevel.Debug)] 70 | [TestCase(LogLevel.Critical)] 71 | public void BenchmarkTest4Parameters(LogLevel logLevel) 72 | { 73 | int iterations = (logLevel == LogLevel.Debug ? 100000 : 10000000); // printing makes it very slow, so we have to reduce the iterations 74 | Console.WriteLine($"Running test {nameof(BenchmarkTest4Parameters)} with level {logLevel} ({iterations} iterations)..."); 75 | 76 | var logger = CreateLogger(logLevel); 77 | 78 | // Microsoft Logging Generator ( https://github.com/geeknoid/LoggingGenerator ) 79 | Console.WriteLine("Microsoft Logging Generator..."); Thread.Sleep(2000); 80 | var sw = Stopwatch.StartNew(); 81 | for (int i = 0; i < iterations; i++) 82 | { 83 | Log.FourArgumentsLog(logger, Environment.MachineName, DateTime.Now, 10, true); 84 | } 85 | sw.Stop(); 86 | long elapsedMilliseconds1 = sw.ElapsedMilliseconds; 87 | 88 | // InterpolatedLogging 89 | Console.WriteLine("InterpolatedLogging..."); Thread.Sleep(2000); 90 | sw = Stopwatch.StartNew(); 91 | for (int i = 0; i < iterations; i++) 92 | { 93 | logger.InterpolatedDebug($"Could not open socket to `{Environment.MachineName:hostName}` at {DateTime.Now:Timestamp} with amount {10m:Amount} - flag is {true:Flag}"); 94 | } 95 | sw.Stop(); 96 | long elapsedMilliseconds2 = sw.ElapsedMilliseconds; 97 | 98 | Thread.Sleep(3000); // let loggers flush their buffers 99 | Console.WriteLine($"Microsoft Logging Generator (4 arguments): {elapsedMilliseconds1}ms"); 100 | Console.WriteLine($"InterpolatedLogging (4 arguments): {elapsedMilliseconds2}ms"); 101 | } 102 | 103 | 104 | 105 | } 106 | 107 | static partial class Log 108 | { 109 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "1.0.0.0")] 110 | private static readonly global::System.Action __CouldNotOpenSocketCallback = 111 | global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(CouldNotOpenSocket)), "Could not open socket to `{hostName}`"); 112 | 113 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "1.0.0.0")] 114 | public static void CouldNotOpenSocket(global::Microsoft.Extensions.Logging.ILogger logger, string hostName) 115 | { 116 | if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Debug)) 117 | { 118 | __CouldNotOpenSocketCallback(logger, hostName, null); 119 | } 120 | } 121 | 122 | 123 | 124 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "1.0.0.0")] 125 | private static readonly global::System.Action _FourArgumentsCallback = 126 | global::Microsoft.Extensions.Logging.LoggerMessage.Define(global::Microsoft.Extensions.Logging.LogLevel.Debug, new global::Microsoft.Extensions.Logging.EventId(0, nameof(CouldNotOpenSocket)), "Could not open socket to `{hostName}` at {Timestamp} with amount {Amount} - flag is {Flag}"); 127 | 128 | [global::System.CodeDom.Compiler.GeneratedCodeAttribute("Microsoft.Extensions.Logging.Generators", "1.0.0.0")] 129 | public static void FourArgumentsLog(global::Microsoft.Extensions.Logging.ILogger logger, string hostName, DateTime now, decimal amount, bool flag) 130 | { 131 | if (logger.IsEnabled(global::Microsoft.Extensions.Logging.LogLevel.Debug)) 132 | { 133 | _FourArgumentsCallback(logger, hostName, now, amount, flag, null); 134 | } 135 | } 136 | 137 | } 138 | } 139 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/InterpolatedLogging.Microsoft.Extensions.Logging.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | Rick Drizin 9 | 10 | Rick Drizin 11 | 12 | MIT 13 | 14 | https://github.com/Drizin/InterpolatedLogging/ 15 | 16 | InterpolatedLoggingTests.MicrosoftExtensionsLogging 17 | 18 | InterpolatedLoggingTests.MicrosoftExtensionsLogging 19 | 20 | InterpolatedLoggingTests.MicrosoftExtensionsLogging.Program 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging.Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | 3 | namespace InterpolatedLoggingTests.MicrosoftExtensionsLogging 4 | { 5 | class Program 6 | { 7 | static void Main(string[] args) 8 | { 9 | new BenchmarkTests().BenchmarkTest1Parameter(Microsoft.Extensions.Logging.LogLevel.Debug); 10 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 11 | 12 | new BenchmarkTests().BenchmarkTest1Parameter(Microsoft.Extensions.Logging.LogLevel.Critical); 13 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 14 | 15 | new BenchmarkTests().BenchmarkTest4Parameters(Microsoft.Extensions.Logging.LogLevel.Debug); 16 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 17 | 18 | new BenchmarkTests().BenchmarkTest4Parameters(Microsoft.Extensions.Logging.LogLevel.Critical); 19 | Console.WriteLine("Press any key to exit"); Console.ReadLine(); 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging/InterpolatedLogging.Microsoft.Extensions.Logging.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net472;net5.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/InterpolatedLogging/ 8 | Extensions to Microsoft.Extensions.Logging to write Log Messages using Interpolated Strings without losing Structured Property Names 9 | Rick Drizin 10 | Rick Drizin 11 | 5.1.0 12 | false 13 | InterpolatedLogging.Microsoft.Extensions.Logging 14 | InterpolatedLogging.Microsoft.Extensions.Logging.xml 15 | true 16 | true 17 | 18 | NuGetReadMe.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | all 31 | runtime; build; native; contentfiles; analyzers; buildtransitive 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging/InterpolatedLogging.Microsoft.Extensions.Logging.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | InterpolatedLogging.Microsoft.Extensions.Logging 5 | InterpolatedLogging.Microsoft.Extensions.Logging 6 | Rick Drizin 7 | Rick Drizin 8 | MIT 9 | https://github.com/Drizin/InterpolatedLogging 10 | false 11 | Extensions to Microsoft.Extensions.Logging to write Log Messages using Interpolated Strings without losing Structured Property Names 12 | Copyright Rick Drizin 2021 13 | microsoft.extensions.logging;microsoft logging extensions;ilogger;interpolated;interpolation;formattablestring 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging/InterpolatedLoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedLogging; 2 | using Microsoft.Extensions.Logging; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | 8 | namespace Microsoft.Extensions.Logging 9 | { 10 | /// 11 | /// Extend Serilog ILogger with facades using Interpolated strings 12 | /// 13 | public static class InterpolatedLoggingExtensions 14 | { 15 | #region Facade Extensions 16 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 17 | public static void InterpolatedTrace(this ILogger logger, FormattableString message) 18 | { 19 | if (!logger.IsEnabled(LogLevel.Trace)) 20 | return; 21 | StructuredLogMessage msg = new StructuredLogMessage(message); 22 | logger.LogTrace(msg.MessageTemplate, msg.Properties); 23 | } 24 | public static void InterpolatedTrace(this ILogger logger, Exception exception, FormattableString message) 25 | { 26 | if (!logger.IsEnabled(LogLevel.Trace)) 27 | return; 28 | StructuredLogMessage msg = new StructuredLogMessage(message); 29 | logger.LogTrace(exception, msg.MessageTemplate, msg.Properties); 30 | } 31 | public static void InterpolatedTrace(this ILogger logger, EventId eventId, FormattableString message) 32 | { 33 | if (!logger.IsEnabled(LogLevel.Trace)) 34 | return; 35 | StructuredLogMessage msg = new StructuredLogMessage(message); 36 | logger.LogTrace(eventId, msg.MessageTemplate, msg.Properties); 37 | } 38 | public static void InterpolatedTrace(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 39 | { 40 | if (!logger.IsEnabled(LogLevel.Trace)) 41 | return; 42 | StructuredLogMessage msg = new StructuredLogMessage(message); 43 | logger.LogTrace(eventId, exception, msg.MessageTemplate, msg.Properties); 44 | } 45 | public static void InterpolatedDebug(this ILogger logger, FormattableString message) 46 | { 47 | if (!logger.IsEnabled(LogLevel.Debug)) 48 | return; 49 | StructuredLogMessage msg = new StructuredLogMessage(message); 50 | logger.LogDebug(msg.MessageTemplate, msg.Properties); 51 | } 52 | public static void InterpolatedDebug(this ILogger logger, Exception exception, FormattableString message) 53 | { 54 | if (!logger.IsEnabled(LogLevel.Debug)) 55 | return; 56 | StructuredLogMessage msg = new StructuredLogMessage(message); 57 | logger.LogDebug(exception, msg.MessageTemplate, msg.Properties); 58 | } 59 | public static void InterpolatedDebug(this ILogger logger, EventId eventId, FormattableString message) 60 | { 61 | if (!logger.IsEnabled(LogLevel.Debug)) 62 | return; 63 | StructuredLogMessage msg = new StructuredLogMessage(message); 64 | logger.LogDebug(eventId, msg.MessageTemplate, msg.Properties); 65 | } 66 | public static void InterpolatedDebug(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 67 | { 68 | if (!logger.IsEnabled(LogLevel.Debug)) 69 | return; 70 | StructuredLogMessage msg = new StructuredLogMessage(message); 71 | logger.LogDebug(eventId, exception, msg.MessageTemplate, msg.Properties); 72 | } 73 | public static void InterpolatedInformation(this ILogger logger, FormattableString message) 74 | { 75 | if (!logger.IsEnabled(LogLevel.Information)) 76 | return; 77 | StructuredLogMessage msg = new StructuredLogMessage(message); 78 | logger.LogInformation(msg.MessageTemplate, msg.Properties); 79 | } 80 | public static void InterpolatedInformation(this ILogger logger, Exception exception, FormattableString message) 81 | { 82 | if (!logger.IsEnabled(LogLevel.Information)) 83 | return; 84 | StructuredLogMessage msg = new StructuredLogMessage(message); 85 | logger.LogInformation(exception, msg.MessageTemplate, msg.Properties); 86 | } 87 | public static void InterpolatedInformation(this ILogger logger, EventId eventId, FormattableString message) 88 | { 89 | if (!logger.IsEnabled(LogLevel.Information)) 90 | return; 91 | StructuredLogMessage msg = new StructuredLogMessage(message); 92 | logger.LogInformation(eventId, msg.MessageTemplate, msg.Properties); 93 | } 94 | public static void InterpolatedInformation(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 95 | { 96 | if (!logger.IsEnabled(LogLevel.Information)) 97 | return; 98 | StructuredLogMessage msg = new StructuredLogMessage(message); 99 | logger.LogInformation(eventId, exception, msg.MessageTemplate, msg.Properties); 100 | } 101 | public static void InterpolatedWarning(this ILogger logger, FormattableString message) 102 | { 103 | if (!logger.IsEnabled(LogLevel.Warning)) 104 | return; 105 | StructuredLogMessage msg = new StructuredLogMessage(message); 106 | logger.LogWarning(msg.MessageTemplate, msg.Properties); 107 | } 108 | public static void InterpolatedWarning(this ILogger logger, Exception exception, FormattableString message) 109 | { 110 | if (!logger.IsEnabled(LogLevel.Warning)) 111 | return; 112 | StructuredLogMessage msg = new StructuredLogMessage(message); 113 | logger.LogWarning(exception, msg.MessageTemplate, msg.Properties); 114 | } 115 | public static void InterpolatedWarning(this ILogger logger, EventId eventId, FormattableString message) 116 | { 117 | if (!logger.IsEnabled(LogLevel.Warning)) 118 | return; 119 | StructuredLogMessage msg = new StructuredLogMessage(message); 120 | logger.LogWarning(eventId, msg.MessageTemplate, msg.Properties); 121 | } 122 | public static void InterpolatedWarning(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 123 | { 124 | if (!logger.IsEnabled(LogLevel.Warning)) 125 | return; 126 | StructuredLogMessage msg = new StructuredLogMessage(message); 127 | logger.LogWarning(eventId, exception, msg.MessageTemplate, msg.Properties); 128 | } 129 | public static void InterpolatedError(this ILogger logger, FormattableString message) 130 | { 131 | if (!logger.IsEnabled(LogLevel.Error)) 132 | return; 133 | StructuredLogMessage msg = new StructuredLogMessage(message); 134 | logger.LogError(msg.MessageTemplate, msg.Properties); 135 | } 136 | public static void InterpolatedError(this ILogger logger, Exception exception, FormattableString message) 137 | { 138 | if (!logger.IsEnabled(LogLevel.Error)) 139 | return; 140 | StructuredLogMessage msg = new StructuredLogMessage(message); 141 | logger.LogError(exception, msg.MessageTemplate, msg.Properties); 142 | } 143 | public static void InterpolatedError(this ILogger logger, EventId eventId, FormattableString message) 144 | { 145 | if (!logger.IsEnabled(LogLevel.Error)) 146 | return; 147 | StructuredLogMessage msg = new StructuredLogMessage(message); 148 | logger.LogError(eventId, msg.MessageTemplate, msg.Properties); 149 | } 150 | public static void InterpolatedError(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 151 | { 152 | if (!logger.IsEnabled(LogLevel.Error)) 153 | return; 154 | StructuredLogMessage msg = new StructuredLogMessage(message); 155 | logger.LogError(eventId, exception, msg.MessageTemplate, msg.Properties); 156 | } 157 | public static void InterpolatedCritical(this ILogger logger, FormattableString message) 158 | { 159 | if (!logger.IsEnabled(LogLevel.Critical)) 160 | return; 161 | StructuredLogMessage msg = new StructuredLogMessage(message); 162 | logger.LogCritical(msg.MessageTemplate, msg.Properties); 163 | } 164 | public static void InterpolatedCritical(this ILogger logger, Exception exception, FormattableString message) 165 | { 166 | if (!logger.IsEnabled(LogLevel.Critical)) 167 | return; 168 | StructuredLogMessage msg = new StructuredLogMessage(message); 169 | logger.LogCritical(exception, msg.MessageTemplate, msg.Properties); 170 | } 171 | public static void InterpolatedCritical(this ILogger logger, EventId eventId, FormattableString message) 172 | { 173 | if (!logger.IsEnabled(LogLevel.Critical)) 174 | return; 175 | StructuredLogMessage msg = new StructuredLogMessage(message); 176 | logger.LogCritical(eventId, msg.MessageTemplate, msg.Properties); 177 | } 178 | public static void InterpolatedCritical(this ILogger logger, EventId eventId, Exception exception, FormattableString message) 179 | { 180 | if (!logger.IsEnabled(LogLevel.Critical)) 181 | return; 182 | StructuredLogMessage msg = new StructuredLogMessage(message); 183 | logger.LogCritical(eventId, exception, msg.MessageTemplate, msg.Properties); 184 | } 185 | public static void InterpolatedLog(this ILogger logger, LogLevel logLevel, FormattableString message) 186 | { 187 | if (!logger.IsEnabled(logLevel)) 188 | return; 189 | StructuredLogMessage msg = new StructuredLogMessage(message); 190 | logger.Log(logLevel, msg.MessageTemplate, msg.Properties); 191 | } 192 | public static void InterpolatedLog(this ILogger logger, LogLevel logLevel, Exception exception, FormattableString message) 193 | { 194 | if (!logger.IsEnabled(logLevel)) 195 | return; 196 | StructuredLogMessage msg = new StructuredLogMessage(message); 197 | logger.Log(logLevel, exception, msg.MessageTemplate, msg.Properties); 198 | } 199 | public static void InterpolatedLog(this ILogger logger, EventId eventId, LogLevel logLevel, FormattableString message) 200 | { 201 | if (!logger.IsEnabled(logLevel)) 202 | return; 203 | StructuredLogMessage msg = new StructuredLogMessage(message); 204 | logger.Log(logLevel, eventId, msg.MessageTemplate, msg.Properties); 205 | } 206 | public static void InterpolatedLog(this ILogger logger, EventId eventId, LogLevel logLevel, Exception exception, FormattableString message) 207 | { 208 | if (!logger.IsEnabled(logLevel)) 209 | return; 210 | StructuredLogMessage msg = new StructuredLogMessage(message); 211 | logger.Log(logLevel, eventId, exception, msg.MessageTemplate, msg.Properties); 212 | } 213 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 214 | #endregion 215 | 216 | } 217 | } 218 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Microsoft.Extensions.Logging/NuGetReadMe.md: -------------------------------------------------------------------------------- 1 | # Interpolated Logging 2 | 3 | **Extensions to Logging Libraries to write Log Messages using Interpolated Strings without losing Structured Property Names** 4 | 5 | ## Introduction 6 | 7 | Most logging libraries support **structured logging**: 8 | 9 | ```cs 10 | logger.Info("User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", 11 | name, orderId, DateTime.Now, elapsedTime); 12 | ``` 13 | 14 | This means that our logs will get not only plain strings but also the structured data, allowing us to search for specific property values (e.g. search for `OrderId="123"` to trace some order, or search for `OperationElapsedTime>1000` to find slow operations). 15 | 16 | The problem with this approach is that it's easy to put the wrong number of parameters or wrong order of parameters (the parameters at the end are **positional**, they are not matched with the message by their names). 17 | 18 | If you just use regular interpolated strings you lose the benefit of structured logging, since the logging library won't know the names of each property: 19 | 20 | ```cs 21 | logger.Info($"User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms"); 22 | ``` 23 | 24 | This project solves this problem by extending popular logging libraries with extension methods that allow the use of interpolated strings while still being able to define the name of the properties: 25 | 26 | ```cs 27 | // Syntax 1: Define property names using explicit NP (NamedProperty) helper 28 | logger.InterpolatedInfo($"User {NP(name, "UserName")} created Order {NP(orderId, "OrderId")} at {NP(now, "Date")}, operation took {NP(elapsedTime, "OperationElapsedTime")}ms"); 29 | 30 | // Syntax 2: Define property names using anonymous objects 31 | logger.InterpolatedInfo($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 32 | 33 | // Syntax 3: Define property names after the variables using colon-syntax ( {variable:propertyName} ) 34 | logger.InterpolatedInfo($"User {name:UserName} created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 35 | ``` 36 | 37 | All those syntaxes above are equivalent (and have near-identical performance), so pick the one you like better. 38 | 39 | The result is that your logging library will get this template and properties: 40 | 41 | ```cs 42 | "User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms" 43 | // ... and the properties in order: name, orderId, DateTime.Now, elapsedTime. 44 | ``` 45 | 46 | ## Why can't I just use regular string interpolation? 47 | 48 | If you were just using regular string interpolation (without our extension methods) the final rendered log (plain message) tracked in your logging library would be the same, but the received template would be 49 | `"User {0} created Order {1} at {2}, operation took {3}ms"` 50 | and it would receive properties named like `{0}="Rick"`, `{1}=1001`, etc. 51 | In this case you wouldn't be able to searches in your logging database for something like `WHERE UserName=="Rick"`. 52 | The best you could do would be like `WHERE Template=="User {0} created Order {1} at {2}, operation took {3}ms" AND Props.{0}="Rick"` - but structured logging can do much better than this. 53 | 54 | # Quickstart 55 | 56 | 1. Install the **NuGet package** according to your logging library ([Serilog](https://www.nuget.org/packages/InterpolatedLogging.Serilog), [Microsoft.Extensions.Logging](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) or [NLog](https://www.nuget.org/packages/InterpolatedLogging.NLog/)) 57 | 1. Start using like this: 58 | 59 | ```cs 60 | // for easier usage our extension methods use the same namespace of the logging libraries 61 | // using Serilog; 62 | // or using Microsoft.Extensions.Logging; 63 | // or using NLog; 64 | using static InterpolatedLogging.NamedProperties; // for using the short NP helper you need this 65 | // ... 66 | 67 | logger.InterpolatedInformation($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 68 | // there are also extensions for Debug, Verbose (or Trace depending on your logging library), etc, and there are also overloads that take an Exception. 69 | ``` 70 | 71 | # Supported Logging Libraries 72 | 73 | Current supported libraries: 74 | 75 | Library | Status | NuGet Package 76 | ------------ | ------------- | ------------- 77 | **Serilog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Serilog/) 78 | **Microsoft.Extensions.Logging** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) 79 | **NLog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.NLog/) 80 | log4net | Pending | 81 | 82 | 83 | # Advanced Features 84 | 85 | ## Raw strings 86 | 87 | If you want to embed raw strings in your messages (don't want them to be saved as structured properties), you don't need to create an anonymous object and you can just use the **raw modifier**: 88 | 89 | ```cs 90 | logger.InterpolatedInformation($"User {new { UserName = name }} logged as {role:raw}"); 91 | ``` 92 | 93 | ## Serilog destructuring operator 94 | 95 | Serilog has this `@` destructuring operator which makes a single property be stored with its internal structure (instead of just invoking `ToString()` and saving the serialized property). You can still use that operator by using the `@` outside of the interpolation: 96 | 97 | ```cs 98 | var input = new { Latitude = 25, Longitude = 134 }; 99 | logger.Information($"Processed @{ new { SensorInput = input }} in { new { TimeMS = time}:000} ms."); 100 | // in plain Serilog this would be equivalent of: 101 | //logger.Information("Processed {@SensorInput} in {TimeMS:000}ms.", input, time); 102 | ``` 103 | 104 | 105 | # Collaborate 106 | 107 | This is a brand new project, and your contribution can help a lot. 108 | 109 | **Would you like to collaborate?** 110 | 111 | Please submit a pull request or if you prefer you can [create an issue](https://github.com/Drizin/InterpolatedLogging/issues) or [contact me](https://rdrizin.com/pages/Contact/) to discuss your idea. 112 | 113 | ## License 114 | MIT License 115 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.NLog/InterpolatedLogging.NLog.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net472;net5.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/InterpolatedLogging/ 8 | Extensions to NLog to write Log Messages using Interpolated Strings without losing Structured Property Names 9 | Rick Drizin 10 | Rick Drizin 11 | 4.0.6 12 | false 13 | InterpolatedLogging.NLog 14 | InterpolatedLogging.NLog.xml 15 | true 16 | true 17 | 18 | NuGetReadMe.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.NLog/InterpolatedLogging.NLog.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | InterpolatedLogging.NLog 5 | InterpolatedLogging.NLog 6 | Rick Drizin 7 | Rick Drizin 8 | MIT 9 | https://github.com/Drizin/InterpolatedLogging 10 | false 11 | Extensions to NLog to write Log Messages using Interpolated Strings without losing Structured Property Names 12 | Copyright Rick Drizin 2021 13 | nlog;interpolated;interpolation;formattablestring 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.NLog/InterpolatedLoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedLogging; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Data; 5 | using System.Text; 6 | 7 | namespace NLog 8 | { 9 | /// 10 | /// Extend NLog ILogger with facades using Interpolated strings 11 | /// 12 | public static class InterpolatedLoggingExtensions 13 | { 14 | #region Facade Extensions 15 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 16 | public static void InterpolatedTrace(this ILogger logger, FormattableString message) 17 | { 18 | if (!logger.IsEnabled(LogLevel.Trace)) 19 | return; 20 | StructuredLogMessage msg = new StructuredLogMessage(message); 21 | logger.Trace(msg.MessageTemplate, msg.Properties); 22 | } 23 | public static void InterpolatedTrace(this ILogger logger, Exception exception, FormattableString message) 24 | { 25 | if (!logger.IsEnabled(LogLevel.Trace)) 26 | return; 27 | StructuredLogMessage msg = new StructuredLogMessage(message); 28 | logger.Trace(exception, msg.MessageTemplate, msg.Properties); 29 | } 30 | public static void InterpolatedDebug(this ILogger logger, FormattableString message) 31 | { 32 | if (!logger.IsEnabled(LogLevel.Debug)) 33 | return; 34 | StructuredLogMessage msg = new StructuredLogMessage(message); 35 | logger.Debug(msg.MessageTemplate, msg.Properties); 36 | } 37 | public static void InterpolatedDebug(this ILogger logger, Exception exception, FormattableString message) 38 | { 39 | if (!logger.IsEnabled(LogLevel.Debug)) 40 | return; 41 | StructuredLogMessage msg = new StructuredLogMessage(message); 42 | logger.Debug(exception, msg.MessageTemplate, msg.Properties); 43 | } 44 | public static void InterpolatedInfo(this ILogger logger, FormattableString message) 45 | { 46 | if (!logger.IsEnabled(LogLevel.Info)) 47 | return; 48 | StructuredLogMessage msg = new StructuredLogMessage(message); 49 | logger.Info(msg.MessageTemplate, msg.Properties); 50 | } 51 | public static void InterpolatedInfo(this ILogger logger, Exception exception, FormattableString message) 52 | { 53 | if (!logger.IsEnabled(LogLevel.Info)) 54 | return; 55 | StructuredLogMessage msg = new StructuredLogMessage(message); 56 | logger.Info(exception, msg.MessageTemplate, msg.Properties); 57 | } 58 | public static void InterpolatedWarn(this ILogger logger, FormattableString message) 59 | { 60 | if (!logger.IsEnabled(LogLevel.Warn)) 61 | return; 62 | StructuredLogMessage msg = new StructuredLogMessage(message); 63 | logger.Warn(msg.MessageTemplate, msg.Properties); 64 | } 65 | public static void InterpolatedWarn(this ILogger logger, Exception exception, FormattableString message) 66 | { 67 | if (!logger.IsEnabled(LogLevel.Warn)) 68 | return; 69 | StructuredLogMessage msg = new StructuredLogMessage(message); 70 | logger.Warn(exception, msg.MessageTemplate, msg.Properties); 71 | } 72 | public static void InterpolatedError(this ILogger logger, FormattableString message) 73 | { 74 | if (!logger.IsEnabled(LogLevel.Error)) 75 | return; 76 | StructuredLogMessage msg = new StructuredLogMessage(message); 77 | logger.Error(msg.MessageTemplate, msg.Properties); 78 | } 79 | public static void InterpolatedError(this ILogger logger, Exception exception, FormattableString message) 80 | { 81 | if (!logger.IsEnabled(LogLevel.Error)) 82 | return; 83 | StructuredLogMessage msg = new StructuredLogMessage(message); 84 | logger.Error(exception, msg.MessageTemplate, msg.Properties); 85 | } 86 | public static void InterpolatedFatal(this ILogger logger, FormattableString message) 87 | { 88 | if (!logger.IsEnabled(LogLevel.Fatal)) 89 | return; 90 | StructuredLogMessage msg = new StructuredLogMessage(message); 91 | logger.Fatal(msg.MessageTemplate, msg.Properties); 92 | } 93 | public static void InterpolatedFatal(this ILogger logger, Exception exception, FormattableString message) 94 | { 95 | if (!logger.IsEnabled(LogLevel.Fatal)) 96 | return; 97 | StructuredLogMessage msg = new StructuredLogMessage(message); 98 | logger.Fatal(exception, msg.MessageTemplate, msg.Properties); 99 | } 100 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 101 | #endregion 102 | 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.NLog/NuGetReadMe.md: -------------------------------------------------------------------------------- 1 | # Interpolated Logging 2 | 3 | **Extensions to Logging Libraries to write Log Messages using Interpolated Strings without losing Structured Property Names** 4 | 5 | ## Introduction 6 | 7 | Most logging libraries support **structured logging**: 8 | 9 | ```cs 10 | logger.Info("User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", 11 | name, orderId, DateTime.Now, elapsedTime); 12 | ``` 13 | 14 | This means that our logs will get not only plain strings but also the structured data, allowing us to search for specific property values (e.g. search for `OrderId="123"` to trace some order, or search for `OperationElapsedTime>1000` to find slow operations). 15 | 16 | The problem with this approach is that it's easy to put the wrong number of parameters or wrong order of parameters (the parameters at the end are **positional**, they are not matched with the message by their names). 17 | 18 | If you just use regular interpolated strings you lose the benefit of structured logging, since the logging library won't know the names of each property: 19 | 20 | ```cs 21 | logger.Info($"User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms"); 22 | ``` 23 | 24 | This project solves this problem by extending popular logging libraries with extension methods that allow the use of interpolated strings while still being able to define the name of the properties: 25 | 26 | ```cs 27 | // Syntax 1: Define property names using explicit NP (NamedProperty) helper 28 | logger.InterpolatedInfo($"User {NP(name, "UserName")} created Order {NP(orderId, "OrderId")} at {NP(now, "Date")}, operation took {NP(elapsedTime, "OperationElapsedTime")}ms"); 29 | 30 | // Syntax 2: Define property names using anonymous objects 31 | logger.InterpolatedInfo($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 32 | 33 | // Syntax 3: Define property names after the variables using colon-syntax ( {variable:propertyName} ) 34 | logger.InterpolatedInfo($"User {name:UserName} created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 35 | ``` 36 | 37 | All those syntaxes above are equivalent (and have near-identical performance), so pick the one you like better. 38 | 39 | The result is that your logging library will get this template and properties: 40 | 41 | ```cs 42 | "User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms" 43 | // ... and the properties in order: name, orderId, DateTime.Now, elapsedTime. 44 | ``` 45 | 46 | ## Why can't I just use regular string interpolation? 47 | 48 | If you were just using regular string interpolation (without our extension methods) the final rendered log (plain message) tracked in your logging library would be the same, but the received template would be 49 | `"User {0} created Order {1} at {2}, operation took {3}ms"` 50 | and it would receive properties named like `{0}="Rick"`, `{1}=1001`, etc. 51 | In this case you wouldn't be able to searches in your logging database for something like `WHERE UserName=="Rick"`. 52 | The best you could do would be like `WHERE Template=="User {0} created Order {1} at {2}, operation took {3}ms" AND Props.{0}="Rick"` - but structured logging can do much better than this. 53 | 54 | # Quickstart 55 | 56 | 1. Install the **NuGet package** according to your logging library ([Serilog](https://www.nuget.org/packages/InterpolatedLogging.Serilog), [Microsoft.Extensions.Logging](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) or [NLog](https://www.nuget.org/packages/InterpolatedLogging.NLog/)) 57 | 1. Start using like this: 58 | 59 | ```cs 60 | // for easier usage our extension methods use the same namespace of the logging libraries 61 | // using Serilog; 62 | // or using Microsoft.Extensions.Logging; 63 | // or using NLog; 64 | using static InterpolatedLogging.NamedProperties; // for using the short NP helper you need this 65 | // ... 66 | 67 | logger.InterpolatedInformation($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 68 | // there are also extensions for Debug, Verbose (or Trace depending on your logging library), etc, and there are also overloads that take an Exception. 69 | ``` 70 | 71 | # Supported Logging Libraries 72 | 73 | Current supported libraries: 74 | 75 | Library | Status | NuGet Package 76 | ------------ | ------------- | ------------- 77 | **Serilog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Serilog/) 78 | **Microsoft.Extensions.Logging** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) 79 | **NLog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.NLog/) 80 | log4net | Pending | 81 | 82 | 83 | # Advanced Features 84 | 85 | ## Raw strings 86 | 87 | If you want to embed raw strings in your messages (don't want them to be saved as structured properties), you don't need to create an anonymous object and you can just use the **raw modifier**: 88 | 89 | ```cs 90 | logger.InterpolatedInformation($"User {new { UserName = name }} logged as {role:raw}"); 91 | ``` 92 | 93 | ## Serilog destructuring operator 94 | 95 | Serilog has this `@` destructuring operator which makes a single property be stored with its internal structure (instead of just invoking `ToString()` and saving the serialized property). You can still use that operator by using the `@` outside of the interpolation: 96 | 97 | ```cs 98 | var input = new { Latitude = 25, Longitude = 134 }; 99 | logger.Information($"Processed @{ new { SensorInput = input }} in { new { TimeMS = time}:000} ms."); 100 | // in plain Serilog this would be equivalent of: 101 | //logger.Information("Processed {@SensorInput} in {TimeMS:000}ms.", input, time); 102 | ``` 103 | 104 | 105 | # Collaborate 106 | 107 | This is a brand new project, and your contribution can help a lot. 108 | 109 | **Would you like to collaborate?** 110 | 111 | Please submit a pull request or if you prefer you can [create an issue](https://github.com/Drizin/InterpolatedLogging/issues) or [contact me](https://rdrizin.com/pages/Contact/) to discuss your idea. 112 | 113 | ## License 114 | MIT License 115 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog.Tests/BenchmarkTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using Serilog; 3 | using Serilog.Core; 4 | using Serilog.Events; 5 | using System; 6 | using System.Collections; 7 | using System.Collections.Generic; 8 | using System.Data; 9 | using System.Data.SqlClient; 10 | using System.Diagnostics; 11 | using System.Linq; 12 | using System.Reflection; 13 | using System.Threading; 14 | using static InterpolatedLogging.NamedProperties; 15 | 16 | namespace InterpolatedLoggingTests.Serilog 17 | { 18 | public class BenchmarkTests 19 | { 20 | 21 | static ILogger CreateLogger(LogEventLevel logLevel, bool console = true) 22 | { 23 | var builder = new LoggerConfiguration() 24 | .MinimumLevel.Is(logLevel); 25 | if (console) 26 | builder.WriteTo.Console(); 27 | //else 28 | // builder.WriteTo... 29 | return builder.CreateLogger(); 30 | } 31 | 32 | [TestCase(LogEventLevel.Debug)] 33 | [TestCase(LogEventLevel.Fatal)] 34 | public void BenchmarkTest1Parameter(LogEventLevel logLevel) 35 | { 36 | int iterations = (logLevel == LogEventLevel.Debug ? 100000 : 10000000); // printing makes it very slow, so we have to reduce the iterations 37 | Console.WriteLine($"Running test {nameof(BenchmarkTest1Parameter)} with level {logLevel} ({iterations} iterations)..."); 38 | 39 | var logger = CreateLogger(logLevel); 40 | 41 | // Pure Serilog 42 | Console.WriteLine("Pure Serilog..."); Thread.Sleep(2000); 43 | var sw = Stopwatch.StartNew(); 44 | for (int i = 0; i < iterations; i++) 45 | { 46 | logger.Debug("Could not open socket to `{hostName}`", Environment.MachineName); 47 | } 48 | sw.Stop(); 49 | long elapsedMilliseconds1 = sw.ElapsedMilliseconds; 50 | 51 | 52 | // InterpolatedLogging 53 | Console.WriteLine("InterpolatedLogging..."); Thread.Sleep(2000); 54 | sw = Stopwatch.StartNew(); 55 | for (int i = 0; i < iterations; i++) 56 | { 57 | logger.InterpolatedDebug($"Could not open socket to `{Environment.MachineName:hostName}`"); 58 | } 59 | sw.Stop(); 60 | long elapsedMilliseconds2 = sw.ElapsedMilliseconds; 61 | 62 | Thread.Sleep(3000); // let loggers flush their buffers 63 | Console.WriteLine($"Pure Serilog (1 argument): {elapsedMilliseconds1}ms"); 64 | Console.WriteLine($"InterpolatedLogging (1 argument): {elapsedMilliseconds2}ms"); 65 | } 66 | 67 | [Test] 68 | [TestCase(LogEventLevel.Debug)] 69 | [TestCase(LogEventLevel.Fatal)] 70 | public void BenchmarkTest4Parameters(LogEventLevel logLevel) 71 | { 72 | int iterations = (logLevel == LogEventLevel.Debug ? 100000 : 10000000); // printing makes it very slow, so we have to reduce the iterations 73 | Console.WriteLine($"Running test {nameof(BenchmarkTest4Parameters)} with level {logLevel} ({iterations} iterations)..."); 74 | 75 | var logger = CreateLogger(logLevel); 76 | 77 | // Pure Serilog 78 | Console.WriteLine("Pure Serilog..."); Thread.Sleep(2000); 79 | var sw = Stopwatch.StartNew(); 80 | for (int i = 0; i < iterations; i++) 81 | { 82 | logger.Debug("Could not open socket to `{hostName}` at {Timestamp} with amount {Amount} - flag is {Flag}", Environment.MachineName, DateTime.Now, 10m, true); 83 | } 84 | sw.Stop(); 85 | long elapsedMilliseconds1 = sw.ElapsedMilliseconds; 86 | 87 | // InterpolatedLogging 88 | Console.WriteLine("InterpolatedLogging..."); Thread.Sleep(2000); 89 | sw = Stopwatch.StartNew(); 90 | for (int i = 0; i < iterations; i++) 91 | { 92 | logger.InterpolatedDebug($"Could not open socket to `{Environment.MachineName:hostName}` at {DateTime.Now:Timestamp} with amount {10m:Amount} - flag is {true:Flag}"); 93 | } 94 | sw.Stop(); 95 | long elapsedMilliseconds2 = sw.ElapsedMilliseconds; 96 | 97 | 98 | Thread.Sleep(3000); // let loggers flush their buffers 99 | Console.WriteLine($"Pure Serilog (4 arguments): {elapsedMilliseconds1}ms"); 100 | Console.WriteLine($"InterpolatedLogging (4 arguments): {elapsedMilliseconds2}ms"); 101 | } 102 | 103 | 104 | 105 | } 106 | 107 | } 108 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog.Tests/InterpolatedLogging.Serilog.Tests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | Rick Drizin 9 | 10 | Rick Drizin 11 | 12 | MIT 13 | 14 | https://github.com/Drizin/InterpolatedLogging/ 15 | 16 | InterpolatedLoggingTests.Serilog 17 | 18 | InterpolatedLoggingTests.Serilog 19 | 20 | InterpolatedLoggingTests.Serilog.Program 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog.Tests/Program.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Events; 2 | using System; 3 | 4 | namespace InterpolatedLoggingTests.Serilog 5 | { 6 | class Program 7 | { 8 | static void Main(string[] args) 9 | { 10 | new BenchmarkTests().BenchmarkTest1Parameter(LogEventLevel.Debug); 11 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 12 | 13 | new BenchmarkTests().BenchmarkTest1Parameter(LogEventLevel.Fatal); 14 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 15 | 16 | new BenchmarkTests().BenchmarkTest4Parameters(LogEventLevel.Debug); 17 | Console.WriteLine("Press any key to continue"); Console.ReadLine(); 18 | 19 | new BenchmarkTests().BenchmarkTest4Parameters(LogEventLevel.Fatal); 20 | Console.WriteLine("Press any key to exit"); Console.ReadLine(); 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog/InterpolatedLogging.Serilog.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net472;net5.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/InterpolatedLogging/ 8 | Extensions to Serilog to write Log Messages using Interpolated Strings without losing Structured Property Names 9 | Rick Drizin 10 | Rick Drizin 11 | 2.0.6 12 | false 13 | InterpolatedLogging.Serilog 14 | InterpolatedLogging.Serilog.xml 15 | true 16 | true 17 | 18 | NuGetReadMe.md 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | all 30 | runtime; build; native; contentfiles; analyzers; buildtransitive 31 | 32 | 33 | 34 | 35 | 36 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog/InterpolatedLogging.Serilog.nuspec: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | InterpolatedLogging.Serilog 5 | InterpolatedLogging.Serilog 6 | Rick Drizin 7 | Rick Drizin 8 | MIT 9 | https://github.com/Drizin/InterpolatedLogging 10 | false 11 | Extensions to Serilog to write Log Messages using Interpolated Strings without losing Structured Property Names 12 | Copyright Rick Drizin 2021 13 | serilog;interpolated;interpolation;formattablestring 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog/InterpolatedLoggingExtensions.cs: -------------------------------------------------------------------------------- 1 | using InterpolatedLogging; 2 | using Serilog.Events; 3 | using System; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Text; 7 | 8 | namespace Serilog 9 | { 10 | /// 11 | /// Extend Serilog ILogger with facades using Interpolated strings 12 | /// 13 | public static class InterpolatedLoggingExtensions 14 | { 15 | #region Facade Extensions 16 | #pragma warning disable CS1591 // Missing XML comment for publicly visible type or member 17 | public static void InterpolatedVerbose(this ILogger logger, FormattableString message) 18 | { 19 | if (!logger.IsEnabled(LogEventLevel.Verbose)) 20 | return; 21 | StructuredLogMessage msg = new StructuredLogMessage(message); 22 | logger.Verbose(msg.MessageTemplate, msg.Properties); 23 | } 24 | public static void InterpolatedVerbose(this ILogger logger, Exception exception, FormattableString message) 25 | { 26 | if (!logger.IsEnabled(LogEventLevel.Verbose)) 27 | return; 28 | StructuredLogMessage msg = new StructuredLogMessage(message); 29 | logger.Verbose(exception, msg.MessageTemplate, msg.Properties); 30 | } 31 | public static void InterpolatedDebug(this ILogger logger, FormattableString message) 32 | { 33 | if (!logger.IsEnabled(LogEventLevel.Debug)) 34 | return; 35 | StructuredLogMessage msg = new StructuredLogMessage(message); 36 | logger.Debug(msg.MessageTemplate, msg.Properties); 37 | } 38 | public static void InterpolatedDebug(this ILogger logger, Exception exception, FormattableString message) 39 | { 40 | if (!logger.IsEnabled(LogEventLevel.Debug)) 41 | return; 42 | StructuredLogMessage msg = new StructuredLogMessage(message); 43 | logger.Debug(exception, msg.MessageTemplate, msg.Properties); 44 | } 45 | public static void InterpolatedInformation(this ILogger logger, FormattableString message) 46 | { 47 | if (!logger.IsEnabled(LogEventLevel.Information)) 48 | return; 49 | StructuredLogMessage msg = new StructuredLogMessage(message); 50 | logger.Information(msg.MessageTemplate, msg.Properties); 51 | } 52 | public static void InterpolatedInformation(this ILogger logger, Exception exception, FormattableString message) 53 | { 54 | if (!logger.IsEnabled(LogEventLevel.Information)) 55 | return; 56 | StructuredLogMessage msg = new StructuredLogMessage(message); 57 | logger.Information(exception, msg.MessageTemplate, msg.Properties); 58 | } 59 | public static void InterpolatedWarning(this ILogger logger, FormattableString message) 60 | { 61 | if (!logger.IsEnabled(LogEventLevel.Warning)) 62 | return; 63 | StructuredLogMessage msg = new StructuredLogMessage(message); 64 | logger.Warning(msg.MessageTemplate, msg.Properties); 65 | } 66 | public static void InterpolatedWarning(this ILogger logger, Exception exception, FormattableString message) 67 | { 68 | if (!logger.IsEnabled(LogEventLevel.Warning)) 69 | return; 70 | StructuredLogMessage msg = new StructuredLogMessage(message); 71 | logger.Warning(exception, msg.MessageTemplate, msg.Properties); 72 | } 73 | public static void InterpolatedError(this ILogger logger, FormattableString message) 74 | { 75 | if (!logger.IsEnabled(LogEventLevel.Error)) 76 | return; 77 | StructuredLogMessage msg = new StructuredLogMessage(message); 78 | logger.Error(msg.MessageTemplate, msg.Properties); 79 | } 80 | public static void InterpolatedError(this ILogger logger, Exception exception, FormattableString message) 81 | { 82 | if (!logger.IsEnabled(LogEventLevel.Error)) 83 | return; 84 | StructuredLogMessage msg = new StructuredLogMessage(message); 85 | logger.Error(exception, msg.MessageTemplate, msg.Properties); 86 | } 87 | public static void InterpolatedFatal(this ILogger logger, FormattableString message) 88 | { 89 | if (!logger.IsEnabled(LogEventLevel.Fatal)) 90 | return; 91 | StructuredLogMessage msg = new StructuredLogMessage(message); 92 | logger.Fatal(msg.MessageTemplate, msg.Properties); 93 | } 94 | public static void InterpolatedFatal(this ILogger logger, Exception exception, FormattableString message) 95 | { 96 | if (!logger.IsEnabled(LogEventLevel.Fatal)) 97 | return; 98 | StructuredLogMessage msg = new StructuredLogMessage(message); 99 | logger.Fatal(exception, msg.MessageTemplate, msg.Properties); 100 | } 101 | public static void InterpolatedWrite(this ILogger logger, LogEventLevel level, FormattableString message) 102 | { 103 | if (!logger.IsEnabled(level)) 104 | return; 105 | StructuredLogMessage msg = new StructuredLogMessage(message); 106 | logger.Write(level, msg.MessageTemplate, msg.Properties); 107 | } 108 | public static void InterpolatedWrite(this ILogger logger, LogEventLevel level, Exception exception, FormattableString message) 109 | { 110 | if (!logger.IsEnabled(level)) 111 | return; 112 | StructuredLogMessage msg = new StructuredLogMessage(message); 113 | logger.Write(level, exception, msg.MessageTemplate, msg.Properties); 114 | } 115 | #pragma warning restore CS1591 // Missing XML comment for publicly visible type or member 116 | #endregion 117 | 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Serilog/NuGetReadMe.md: -------------------------------------------------------------------------------- 1 | # Interpolated Logging 2 | 3 | **Extensions to Logging Libraries to write Log Messages using Interpolated Strings without losing Structured Property Names** 4 | 5 | ## Introduction 6 | 7 | Most logging libraries support **structured logging**: 8 | 9 | ```cs 10 | logger.Info("User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", 11 | name, orderId, DateTime.Now, elapsedTime); 12 | ``` 13 | 14 | This means that our logs will get not only plain strings but also the structured data, allowing us to search for specific property values (e.g. search for `OrderId="123"` to trace some order, or search for `OperationElapsedTime>1000` to find slow operations). 15 | 16 | The problem with this approach is that it's easy to put the wrong number of parameters or wrong order of parameters (the parameters at the end are **positional**, they are not matched with the message by their names). 17 | 18 | If you just use regular interpolated strings you lose the benefit of structured logging, since the logging library won't know the names of each property: 19 | 20 | ```cs 21 | logger.Info($"User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms"); 22 | ``` 23 | 24 | This project solves this problem by extending popular logging libraries with extension methods that allow the use of interpolated strings while still being able to define the name of the properties: 25 | 26 | ```cs 27 | // Syntax 1: Define property names using explicit NP (NamedProperty) helper 28 | logger.InterpolatedInfo($"User {NP(name, "UserName")} created Order {NP(orderId, "OrderId")} at {NP(now, "Date")}, operation took {NP(elapsedTime, "OperationElapsedTime")}ms"); 29 | 30 | // Syntax 2: Define property names using anonymous objects 31 | logger.InterpolatedInfo($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 32 | 33 | // Syntax 3: Define property names after the variables using colon-syntax ( {variable:propertyName} ) 34 | logger.InterpolatedInfo($"User {name:UserName} created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 35 | ``` 36 | 37 | All those syntaxes above are equivalent (and have near-identical performance), so pick the one you like better. 38 | 39 | The result is that your logging library will get this template and properties: 40 | 41 | ```cs 42 | "User {UserName} created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms" 43 | // ... and the properties in order: name, orderId, DateTime.Now, elapsedTime. 44 | ``` 45 | 46 | ## Why can't I just use regular string interpolation? 47 | 48 | If you were just using regular string interpolation (without our extension methods) the final rendered log (plain message) tracked in your logging library would be the same, but the received template would be 49 | `"User {0} created Order {1} at {2}, operation took {3}ms"` 50 | and it would receive properties named like `{0}="Rick"`, `{1}=1001`, etc. 51 | In this case you wouldn't be able to searches in your logging database for something like `WHERE UserName=="Rick"`. 52 | The best you could do would be like `WHERE Template=="User {0} created Order {1} at {2}, operation took {3}ms" AND Props.{0}="Rick"` - but structured logging can do much better than this. 53 | 54 | # Quickstart 55 | 56 | 1. Install the **NuGet package** according to your logging library ([Serilog](https://www.nuget.org/packages/InterpolatedLogging.Serilog), [Microsoft.Extensions.Logging](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) or [NLog](https://www.nuget.org/packages/InterpolatedLogging.NLog/)) 57 | 1. Start using like this: 58 | 59 | ```cs 60 | // for easier usage our extension methods use the same namespace of the logging libraries 61 | // using Serilog; 62 | // or using Microsoft.Extensions.Logging; 63 | // or using NLog; 64 | using static InterpolatedLogging.NamedProperties; // for using the short NP helper you need this 65 | // ... 66 | 67 | logger.InterpolatedInformation($"User {new { UserName = name }} created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 68 | // there are also extensions for Debug, Verbose (or Trace depending on your logging library), etc, and there are also overloads that take an Exception. 69 | ``` 70 | 71 | # Supported Logging Libraries 72 | 73 | Current supported libraries: 74 | 75 | Library | Status | NuGet Package 76 | ------------ | ------------- | ------------- 77 | **Serilog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Serilog/) 78 | **Microsoft.Extensions.Logging** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.Microsoft.Extensions.Logging/) 79 | **NLog** | Working | [NuGet](https://www.nuget.org/packages/InterpolatedLogging.NLog/) 80 | log4net | Pending | 81 | 82 | 83 | # Advanced Features 84 | 85 | ## Raw strings 86 | 87 | If you want to embed raw strings in your messages (don't want them to be saved as structured properties), you don't need to create an anonymous object and you can just use the **raw modifier**: 88 | 89 | ```cs 90 | logger.InterpolatedInformation($"User {new { UserName = name }} logged as {role:raw}"); 91 | ``` 92 | 93 | ## Serilog destructuring operator 94 | 95 | Serilog has this `@` destructuring operator which makes a single property be stored with its internal structure (instead of just invoking `ToString()` and saving the serialized property). You can still use that operator by using the `@` outside of the interpolation: 96 | 97 | ```cs 98 | var input = new { Latitude = 25, Longitude = 134 }; 99 | logger.Information($"Processed @{ new { SensorInput = input }} in { new { TimeMS = time}:000} ms."); 100 | // in plain Serilog this would be equivalent of: 101 | //logger.Information("Processed {@SensorInput} in {TimeMS:000}ms.", input, time); 102 | ``` 103 | 104 | 105 | # Collaborate 106 | 107 | This is a brand new project, and your contribution can help a lot. 108 | 109 | **Would you like to collaborate?** 110 | 111 | Please submit a pull request or if you prefer you can [create an issue](https://github.com/Drizin/InterpolatedLogging/issues) or [contact me](https://rdrizin.com/pages/Contact/) to discuss your idea. 112 | 113 | ## License 114 | MIT License 115 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Tests/BasicTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Data.SqlClient; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Reflection; 10 | using static InterpolatedLogging.NamedProperties; 11 | 12 | namespace InterpolatedLogging.Tests 13 | { 14 | public class BasicTests 15 | { 16 | 17 | [Test] 18 | public void ColonSyntax() 19 | { 20 | 21 | string name = "RickDrizin"; 22 | long elapsedTime = 315; 23 | int orderId = 1001; 24 | DateTime now = DateTime.Now; 25 | var msg = new StructuredLogMessage($"User '{name:UserName}' created Order {orderId:OrderId} at {now:Date}, operation took {elapsedTime:OperationElapsedTime}ms"); 26 | 27 | Assert.AreEqual("User '{UserName}' created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", msg.MessageTemplate); 28 | Assert.AreEqual(4, msg.Properties.Length); 29 | Assert.AreEqual(name, msg.Properties[0]); 30 | Assert.AreEqual(orderId, msg.Properties[1]); 31 | Assert.AreEqual(now, msg.Properties[2]); 32 | Assert.AreEqual(elapsedTime, msg.Properties[3]); 33 | } 34 | 35 | [Test] 36 | public void AnonymousSyntax() 37 | { 38 | 39 | string name = "RickDrizin"; 40 | long elapsedTime = 315; 41 | int orderId = 1001; 42 | DateTime now = DateTime.Now; 43 | var msg = new StructuredLogMessage($"User '{new { UserName = name }}' created Order {new { OrderId = orderId}} at {new { Date = now }}, operation took {new { OperationElapsedTime = elapsedTime }}ms"); 44 | 45 | Assert.AreEqual("User '{UserName}' created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", msg.MessageTemplate); 46 | Assert.AreEqual(4, msg.Properties.Length); 47 | Assert.AreEqual(name, msg.Properties[0]); 48 | Assert.AreEqual(orderId, msg.Properties[1]); 49 | Assert.AreEqual(now, msg.Properties[2]); 50 | Assert.AreEqual(elapsedTime, msg.Properties[3]); 51 | } 52 | 53 | [Test] 54 | public void SerilogDestructureAnonymous() 55 | { 56 | 57 | var input = new { Latitude = 25, Longitude = 134 }; 58 | var time = 34; 59 | 60 | // Serilog has this @ destructuring operator, which in plain Serilog should be used as " {@variable} " 61 | // but here in interpolated strings you should use " @{variable} " 62 | var msg = new StructuredLogMessage($"Processed @{ new { SensorInput = input }} in { new { TimeMS = time}:000} ms."); 63 | 64 | Assert.AreEqual("Processed {@SensorInput} in {TimeMS:000} ms.", msg.MessageTemplate); 65 | Assert.AreEqual(2, msg.Properties.Length); 66 | Assert.AreEqual(input, msg.Properties[0]); 67 | Assert.AreEqual(time, msg.Properties[1]); 68 | } 69 | [Test] 70 | public void SerilogDestructureNamedPropertySyntax() 71 | { 72 | 73 | var input = new { Latitude = 25, Longitude = 134 }; 74 | var time = 34; 75 | 76 | // Serilog has this @ destructuring operator, which in plain Serilog should be used as " {@variable} " 77 | // but here in interpolated strings you should use " @{variable} " 78 | var msg = new StructuredLogMessage($"Processed {NP(input, "@SensorInput")} in {NP(time, "TimeMS"):000} ms."); 79 | 80 | Assert.AreEqual("Processed {@SensorInput} in {TimeMS:000} ms.", msg.MessageTemplate); 81 | Assert.AreEqual(2, msg.Properties.Length); 82 | Assert.AreEqual(input, msg.Properties[0]); 83 | Assert.AreEqual(time, msg.Properties[1]); 84 | } 85 | [Test] 86 | public void SerilogDestructureColonSyntax() 87 | { 88 | 89 | var input = new { Latitude = 25, Longitude = 134 }; 90 | var time = 34; 91 | 92 | // Serilog has this @ destructuring operator, which in plain Serilog should be used as " {@variable} " 93 | // but here in interpolated strings you should use " @{variable} " 94 | var msg = new StructuredLogMessage($"Processed {input:@SensorInput} in {time:TimeMS:000} ms."); 95 | 96 | Assert.AreEqual("Processed {@SensorInput} in {TimeMS:000} ms.", msg.MessageTemplate); 97 | Assert.AreEqual(2, msg.Properties.Length); 98 | Assert.AreEqual(input, msg.Properties[0]); 99 | Assert.AreEqual(time, msg.Properties[1]); 100 | } 101 | 102 | 103 | [Test] 104 | public void ColonSyntax_with_ExplicitFormat() 105 | { 106 | DateTime now = DateTime.Now; 107 | 108 | // The format comes after the Property Name, and is forwarded to the template. Colons should be escaped inside single-quotes 109 | var msg = new StructuredLogMessage($"Date {now:Timestamp:yyyy-MM-dd HH':'mm':'sss}"); 110 | 111 | Assert.AreEqual("Date {Timestamp:yyyy-MM-dd HH:mm:sss}", msg.MessageTemplate); 112 | Assert.AreEqual(1, msg.Properties.Length); 113 | Assert.AreEqual(now, msg.Properties[0]); 114 | } 115 | 116 | [Test] 117 | public void ColonSyntax_Trim_PropName_Whitespace() 118 | { 119 | string name = "RickDrizin"; 120 | long elapsedTime = 315; 121 | int orderId = 1001; 122 | DateTime now = DateTime.Now; 123 | var msg = new StructuredLogMessage($"User '{name: UserName}' created Order {orderId: OrderId} at {now : Date}, operation took {elapsedTime : OperationElapsedTime}ms"); 124 | 125 | Assert.AreEqual("User '{UserName}' created Order {OrderId} at {Date}, operation took {OperationElapsedTime}ms", msg.MessageTemplate); 126 | Assert.AreEqual(4, msg.Properties.Length); 127 | Assert.AreEqual(name, msg.Properties[0]); 128 | Assert.AreEqual(orderId, msg.Properties[1]); 129 | Assert.AreEqual(now, msg.Properties[2]); 130 | Assert.AreEqual(elapsedTime, msg.Properties[3]); 131 | } 132 | 133 | [Test] 134 | public void AnonymousSyntax_with_ExplicitFormat() 135 | { 136 | DateTime now = DateTime.Now; 137 | 138 | // The format is explicitly forwarded to the template 139 | var msg = new StructuredLogMessage($"Date {new { Timestamp = now }:yyyy-MM-dd HH:mm:sss}"); 140 | 141 | Assert.AreEqual("Date {Timestamp:yyyy-MM-dd HH:mm:sss}", msg.MessageTemplate); 142 | Assert.AreEqual(1, msg.Properties.Length); 143 | Assert.AreEqual(now, msg.Properties[0]); 144 | } 145 | 146 | /* 147 | [Test] 148 | public void NamedProperty_ExpressionSyntax() 149 | { 150 | DateTime now = DateTime.Now; 151 | 152 | // The format is explicitly forwarded to the template 153 | var msg = new StructuredLogMessage($"Date {NP(x => now) }"); 154 | 155 | Assert.AreEqual("Date {now}", msg.MessageTemplate); 156 | Assert.AreEqual(1, msg.Properties.Length); 157 | Assert.AreEqual(now, msg.Properties[0]); 158 | } 159 | */ 160 | 161 | [Test] 162 | public void NamedProperty_ExplicitSyntax() 163 | { 164 | DateTime now = DateTime.Now; 165 | 166 | // The format is explicitly forwarded to the template 167 | var msg = new StructuredLogMessage($"Date {NP(now, "Timestamp") }"); 168 | 169 | Assert.AreEqual("Date {Timestamp}", msg.MessageTemplate); 170 | Assert.AreEqual(1, msg.Properties.Length); 171 | Assert.AreEqual(now, msg.Properties[0]); 172 | } 173 | 174 | [Test] 175 | public void NullValues_Shouldnt_Crash() 176 | { 177 | string name = null; 178 | int? orderId = null; 179 | var msg = new StructuredLogMessage($"User '{name:UserName}' created Order {orderId:OrderId}"); 180 | 181 | Assert.DoesNotThrow(() => { var prop = msg.Properties; }); 182 | Assert.AreEqual("User '{UserName}' created Order {OrderId}", msg.MessageTemplate); 183 | Assert.AreEqual(2, msg.Properties.Length); 184 | Assert.AreEqual(name, msg.Properties[0]); 185 | Assert.AreEqual(orderId, msg.Properties[1]); 186 | } 187 | 188 | [Test] 189 | public void Combine_Two_InterpolatedStrings() 190 | { 191 | string name = "RickDrizin"; 192 | int orderId = 1001; 193 | 194 | // JoinableString() allows us to append multiple interpolated strings using + operator, so we can break long log messages in multiple lines 195 | // but for best performance prefer $@ 196 | var msg = new StructuredLogMessage(new JoinableString() 197 | + $"User '{name:UserName}'\n" 198 | + $"created Order {orderId:OrderId}" 199 | ); 200 | 201 | Assert.AreEqual("User '{UserName}'\ncreated Order {OrderId}", msg.MessageTemplate); 202 | Assert.AreEqual(2, msg.Properties.Length); 203 | Assert.AreEqual(name, msg.Properties[0]); 204 | Assert.AreEqual(orderId, msg.Properties[1]); 205 | } 206 | 207 | 208 | 209 | [Test] 210 | public void PerformanceTests() 211 | { 212 | DateTime now = DateTime.Now; 213 | int iteractions = 10000; 214 | 215 | // Explicit NP syntax 216 | var sw = Stopwatch.StartNew(); 217 | for (int i = 0; i < iteractions; i++) 218 | { 219 | var msg = new StructuredLogMessage($"... at {NP(now, "Date")}"); 220 | string output = msg.MessageTemplate; 221 | } 222 | System.Diagnostics.Debug.WriteLine($"Syntax 1: {sw.ElapsedMilliseconds}ms"); 223 | 224 | // Anonymous syntax 225 | sw = Stopwatch.StartNew(); 226 | for (int i = 0; i < iteractions; i++) 227 | { 228 | var msg = new StructuredLogMessage($"... at {new { Date = now }}"); 229 | string output = msg.MessageTemplate; 230 | } 231 | System.Diagnostics.Debug.WriteLine($"Syntax 2: {sw.ElapsedMilliseconds}ms"); 232 | 233 | // Colon syntax 234 | sw = Stopwatch.StartNew(); 235 | for (int i = 0; i < iteractions; i++) 236 | { 237 | var msg = new StructuredLogMessage($"...at {now:Date}"); 238 | string output = msg.MessageTemplate; 239 | } 240 | System.Diagnostics.Debug.WriteLine($"Syntax 3: {sw.ElapsedMilliseconds}ms"); 241 | 242 | // Turns out Expression syntax is quite slow (without any benefit?), so removed. 243 | // Expression NP syntax 244 | /* 245 | sw = Stopwatch.StartNew(); 246 | for (int i = 0; i < iteractions; i++) 247 | { 248 | var msg = new StructuredLogMessage($"... at {NP(x => now)}"); 249 | string output = msg.MessageTemplate; 250 | } 251 | System.Diagnostics.Debug.WriteLine($"Syntax 4: {sw.ElapsedMilliseconds}ms"); 252 | 253 | // Expression NP syntax without reflecting over propertyName 254 | sw = Stopwatch.StartNew(); 255 | for (int i = 0; i < iteractions; i++) 256 | { 257 | var msg = new StructuredLogMessage($"... at {NP(x => now, "Date")}"); 258 | string output = msg.MessageTemplate; 259 | } 260 | System.Diagnostics.Debug.WriteLine($"Syntax 5: {sw.ElapsedMilliseconds}ms"); 261 | */ 262 | } 263 | 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Tests/InterpolatedLogging.Tests.csproj: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | netcoreapp2.1 5 | 6 | false 7 | 8 | Rick Drizin 9 | 10 | Rick Drizin 11 | 12 | MIT 13 | 14 | https://github.com/Drizin/InterpolatedLogging/ 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.Tests/JoinableStringTests.cs: -------------------------------------------------------------------------------- 1 | using NUnit.Framework; 2 | using System; 3 | using System.Collections; 4 | using System.Collections.Generic; 5 | using System.Data; 6 | using System.Data.SqlClient; 7 | using System.Diagnostics; 8 | using System.Linq; 9 | using System.Reflection; 10 | using System.Runtime.CompilerServices; 11 | using static InterpolatedLogging.JoinableStrings; 12 | 13 | namespace InterpolatedLogging.Tests 14 | { 15 | public class JoinableStringTests 16 | { 17 | 18 | [Test] 19 | public void JoinableString_6_strings() 20 | { 21 | string name = "RickDrizin"; 22 | int orderId = 1001; 23 | 24 | var combined = JS() 25 | + $"User '{name:UserName}'\n" 26 | + $"User '{name:UserName}'\n" 27 | + $"User '{name:UserName}'\n" 28 | + $"created Order {orderId:OrderId}" 29 | + $"created Order {orderId:OrderId}" 30 | + $"created Order {orderId:OrderId}"; 31 | 32 | Assert.AreEqual("User '{0:UserName}'\nUser '{1:UserName}'\nUser '{2:UserName}'\ncreated Order {3:OrderId}created Order {4:OrderId}created Order {5:OrderId}", combined.Format); 33 | var msg = new StructuredLogMessage(combined); 34 | Assert.AreEqual("User '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\ncreated Order {OrderId}created Order {OrderId}created Order {OrderId}", msg.MessageTemplate); 35 | Assert.AreEqual(6, msg.Properties.Length); 36 | Assert.AreEqual(name, msg.Properties[0]); 37 | Assert.AreEqual(name, msg.Properties[1]); 38 | Assert.AreEqual(name, msg.Properties[2]); 39 | Assert.AreEqual(orderId, msg.Properties[3]); 40 | Assert.AreEqual(orderId, msg.Properties[4]); 41 | Assert.AreEqual(orderId, msg.Properties[5]); 42 | } 43 | 44 | [Test] 45 | public void JoinableString_11_strings() 46 | { 47 | string name = "RickDrizin"; 48 | 49 | var combined = JS() 50 | + $"User '{name:UserName}'\n" 51 | + $"User '{name:UserName}'\n" 52 | + $"User '{name:UserName}'\n" 53 | + $"User '{name:UserName}'\n" 54 | + $"User '{name:UserName}'\n" 55 | + $"User '{name:UserName}'\n" 56 | + $"User '{name:UserName}'\n" 57 | + $"User '{name:UserName}'\n" 58 | + $"User '{name:UserName}'\n" 59 | + $"User '{name:UserName}'\n" 60 | + $"User '{name:UserName}'\n"; 61 | 62 | Assert.AreEqual("User '{0:UserName}'\nUser '{1:UserName}'\nUser '{2:UserName}'\nUser '{3:UserName}'\nUser '{4:UserName}'\nUser '{5:UserName}'\nUser '{6:UserName}'\nUser '{7:UserName}'\nUser '{8:UserName}'\nUser '{9:UserName}'\nUser '{10:UserName}'\n", combined.Format); 63 | var msg = new StructuredLogMessage(combined); 64 | 65 | Assert.AreEqual("User '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\n", msg.MessageTemplate); 66 | Assert.AreEqual(11, msg.Properties.Length); 67 | Assert.AreEqual(name, msg.Properties[0]); 68 | Assert.AreEqual(name, msg.Properties[1]); 69 | Assert.AreEqual(name, msg.Properties[2]); 70 | } 71 | 72 | [Test] 73 | public void CombineStrings4() 74 | { 75 | string name = "RickDrizin"; 76 | 77 | var combined = JS() 78 | + $"User '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\n" 79 | + $"User '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\nUser '{name:UserName}'\n"; 80 | 81 | Assert.AreEqual("User '{0:UserName}'\nUser '{1:UserName}'\nUser '{2:UserName}'\nUser '{3:UserName}'\nUser '{4:UserName}'\nUser '{5:UserName}'\nUser '{6:UserName}'\nUser '{7:UserName}'\nUser '{8:UserName}'\nUser '{9:UserName}'\nUser '{10:UserName}'\nUser '{11:UserName}'\nUser '{12:UserName}'\nUser '{13:UserName}'\nUser '{14:UserName}'\nUser '{15:UserName}'\nUser '{16:UserName}'\nUser '{17:UserName}'\nUser '{18:UserName}'\nUser '{19:UserName}'\nUser '{20:UserName}'\nUser '{21:UserName}'\n", combined.Format); 82 | var msg = new StructuredLogMessage(combined); 83 | 84 | 85 | Assert.AreEqual("User '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\nUser '{UserName}'\n", msg.MessageTemplate); 86 | Assert.AreEqual(22, msg.Properties.Length); 87 | Assert.AreEqual(name, msg.Properties[0]); 88 | Assert.AreEqual(name, msg.Properties[1]); 89 | Assert.AreEqual(name, msg.Properties[2]); 90 | } 91 | } 92 | 93 | 94 | 95 | } 96 | -------------------------------------------------------------------------------- /src/InterpolatedLogging.sln: -------------------------------------------------------------------------------- 1 |  2 | Microsoft Visual Studio Solution File, Format Version 12.00 3 | # Visual Studio Version 16 4 | VisualStudioVersion = 16.0.29001.49 5 | MinimumVisualStudioVersion = 10.0.40219.1 6 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging", "InterpolatedLogging\InterpolatedLogging.csproj", "{8CF86804-9246-4B3D-A1E9-78DDCF758DBC}" 7 | EndProject 8 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.Tests", "InterpolatedLogging.Tests\InterpolatedLogging.Tests.csproj", "{42D7E39A-D7CC-404B-B67A-87AF33183670}" 9 | EndProject 10 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.Serilog", "InterpolatedLogging.Serilog\InterpolatedLogging.Serilog.csproj", "{0382A8C9-BFF8-4CE5-93CA-04D9900B1CC1}" 11 | EndProject 12 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.Microsoft.Extensions.Logging", "InterpolatedLogging.Microsoft.Extensions.Logging\InterpolatedLogging.Microsoft.Extensions.Logging.csproj", "{E0B65034-9953-4B61-9537-4E8144EEE5D7}" 13 | EndProject 14 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.NLog", "InterpolatedLogging.NLog\InterpolatedLogging.NLog.csproj", "{5874FE09-2F1F-48AA-990A-AA1214AA2BF5}" 15 | EndProject 16 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.Microsoft.Extensions.Logging.Tests", "InterpolatedLogging.Microsoft.Extensions.Logging.Tests\InterpolatedLogging.Microsoft.Extensions.Logging.Tests.csproj", "{D46D5A79-1BB2-4834-988C-FB94827A479D}" 17 | EndProject 18 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "InterpolatedLogging.Serilog.Tests", "InterpolatedLogging.Serilog.Tests\InterpolatedLogging.Serilog.Tests.csproj", "{B4E68C0C-B505-4418-A1F3-4CEAE47740C7}" 19 | EndProject 20 | Global 21 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 22 | Debug|Any CPU = Debug|Any CPU 23 | Release|Any CPU = Release|Any CPU 24 | EndGlobalSection 25 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 26 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 27 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Debug|Any CPU.Build.0 = Debug|Any CPU 28 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Release|Any CPU.ActiveCfg = Release|Any CPU 29 | {8CF86804-9246-4B3D-A1E9-78DDCF758DBC}.Release|Any CPU.Build.0 = Release|Any CPU 30 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 31 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Debug|Any CPU.Build.0 = Debug|Any CPU 32 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Release|Any CPU.ActiveCfg = Release|Any CPU 33 | {42D7E39A-D7CC-404B-B67A-87AF33183670}.Release|Any CPU.Build.0 = Release|Any CPU 34 | {0382A8C9-BFF8-4CE5-93CA-04D9900B1CC1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 35 | {0382A8C9-BFF8-4CE5-93CA-04D9900B1CC1}.Debug|Any CPU.Build.0 = Debug|Any CPU 36 | {0382A8C9-BFF8-4CE5-93CA-04D9900B1CC1}.Release|Any CPU.ActiveCfg = Release|Any CPU 37 | {0382A8C9-BFF8-4CE5-93CA-04D9900B1CC1}.Release|Any CPU.Build.0 = Release|Any CPU 38 | {E0B65034-9953-4B61-9537-4E8144EEE5D7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 39 | {E0B65034-9953-4B61-9537-4E8144EEE5D7}.Debug|Any CPU.Build.0 = Debug|Any CPU 40 | {E0B65034-9953-4B61-9537-4E8144EEE5D7}.Release|Any CPU.ActiveCfg = Release|Any CPU 41 | {E0B65034-9953-4B61-9537-4E8144EEE5D7}.Release|Any CPU.Build.0 = Release|Any CPU 42 | {5874FE09-2F1F-48AA-990A-AA1214AA2BF5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 43 | {5874FE09-2F1F-48AA-990A-AA1214AA2BF5}.Debug|Any CPU.Build.0 = Debug|Any CPU 44 | {5874FE09-2F1F-48AA-990A-AA1214AA2BF5}.Release|Any CPU.ActiveCfg = Release|Any CPU 45 | {5874FE09-2F1F-48AA-990A-AA1214AA2BF5}.Release|Any CPU.Build.0 = Release|Any CPU 46 | {D46D5A79-1BB2-4834-988C-FB94827A479D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 47 | {D46D5A79-1BB2-4834-988C-FB94827A479D}.Debug|Any CPU.Build.0 = Debug|Any CPU 48 | {D46D5A79-1BB2-4834-988C-FB94827A479D}.Release|Any CPU.ActiveCfg = Release|Any CPU 49 | {D46D5A79-1BB2-4834-988C-FB94827A479D}.Release|Any CPU.Build.0 = Release|Any CPU 50 | {B4E68C0C-B505-4418-A1F3-4CEAE47740C7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 51 | {B4E68C0C-B505-4418-A1F3-4CEAE47740C7}.Debug|Any CPU.Build.0 = Debug|Any CPU 52 | {B4E68C0C-B505-4418-A1F3-4CEAE47740C7}.Release|Any CPU.ActiveCfg = Release|Any CPU 53 | {B4E68C0C-B505-4418-A1F3-4CEAE47740C7}.Release|Any CPU.Build.0 = Release|Any CPU 54 | EndGlobalSection 55 | GlobalSection(SolutionProperties) = preSolution 56 | HideSolutionNode = FALSE 57 | EndGlobalSection 58 | GlobalSection(ExtensibilityGlobals) = postSolution 59 | SolutionGuid = {AB589EF0-8B1A-4791-A8B5-F3ED178AF34F} 60 | EndGlobalSection 61 | EndGlobal 62 | -------------------------------------------------------------------------------- /src/InterpolatedLogging/InterpolatedLogging.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | netstandard2.0;net472;net5.0 5 | Rick Drizin 6 | MIT 7 | https://github.com/Drizin/InterpolatedLogging/ 8 | Extensions to Logging Libraries to write Log Messages using Interpolated Strings without losing Structured Property Names 9 | Rick Drizin 10 | Rick Drizin 11 | 1.0.6 12 | false 13 | true 14 | true 15 | 16 | $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb 17 | 18 | 19 | 20 | 21 | all 22 | runtime; build; native; contentfiles; analyzers; buildtransitive 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/InterpolatedLogging/JoinableString.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Text.RegularExpressions; 6 | 7 | namespace InterpolatedLogging 8 | { 9 | /// 10 | /// Extends FormattableString (interpolated string) but allows to append new FormattableStrings using overloaded operators + and +=. 11 | /// 12 | public class JoinableString : FormattableString 13 | { 14 | private readonly string _format; 15 | private readonly object[] _arguments; 16 | 17 | public JoinableString() 18 | { 19 | _format = ""; 20 | _arguments = new object[0]; 21 | } 22 | public JoinableString(FormattableString formattableString) 23 | { 24 | _format = formattableString.Format; 25 | _arguments = formattableString.GetArguments(); 26 | } 27 | public JoinableString(JoinableString a, JoinableString b) 28 | { 29 | var args = a.GetArguments().ToList(); 30 | _format = a.Format + ShiftArgs(args.Count, b.Format); 31 | args.AddRange(b.GetArguments()); 32 | _arguments = args.ToArray(); 33 | } 34 | 35 | public override string Format => _format; 36 | public override object[] GetArguments() => _arguments; 37 | public override int ArgumentCount => _arguments.Length; 38 | public override object GetArgument(int index) => _arguments[index]; 39 | 40 | public override string ToString(IFormatProvider formatProvider) 41 | { 42 | return string.Format(formatProvider, _format, _arguments); 43 | } 44 | 45 | public static JoinableString operator +(JoinableString a, JoinableString b) => new JoinableString(a, b); 46 | public static JoinableString operator +(JoinableString a, FormattableString b) => new JoinableString(a, new JoinableString(b)); 47 | 48 | private string ShiftArgs(int shift, string newFormat) 49 | { 50 | if (shift == 0 || string.IsNullOrEmpty(newFormat)) 51 | return newFormat; 52 | 53 | // if current string has N elements, new.Format should change from {0} to {N}, {1} to {N+1}, etc... 54 | 55 | string str = _formattableArgumentRegex.Replace(newFormat, match => { 56 | Group argPosMatch = match.Groups["ArgPos"]; 57 | int argPos = int.Parse(argPosMatch.Value); 58 | string replace = (argPos + shift).ToString(); 59 | string ret = string.Format("{0}{1}{2}", match.Value.Substring(0, argPosMatch.Index - match.Index), replace, match.Value.Substring(argPosMatch.Index - match.Index + argPosMatch.Length)); 60 | return ret; 61 | }); 62 | 63 | return str; 64 | } 65 | 66 | #region Static/Constant 67 | private static Regex _formattableArgumentRegex = new Regex( 68 | "(?@)?{(?\\d*)(:(?[^}]*))?}", 69 | RegexOptions.IgnoreCase 70 | | RegexOptions.Singleline 71 | | RegexOptions.CultureInvariant 72 | | RegexOptions.IgnorePatternWhitespace 73 | | RegexOptions.Compiled 74 | ); 75 | #endregion 76 | 77 | } 78 | 79 | /// 80 | /// Factory to create a JoinableString (a interpolated string that allows to append new interpolated strings to the end) 81 | /// You can either add "using static InterpolatedLogging.JoinableStrings;" and invoke directly JS() constructor, 82 | /// or use "using InterpolatedLogging;" and "new JoinableString()". 83 | /// 84 | public class JoinableStrings 85 | { 86 | /// 87 | /// Creates a JoinableString - a class where we can append multiple interpolated strings. 88 | /// 89 | public static JoinableString JS() 90 | { 91 | return new JoinableString(); 92 | } 93 | } 94 | 95 | 96 | } 97 | -------------------------------------------------------------------------------- /src/InterpolatedLogging/NamedProperty.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq.Expressions; 4 | using System.Text; 5 | 6 | namespace InterpolatedLogging 7 | { 8 | /// 9 | /// Property Wrapper which contains Property Name and Value 10 | /// 11 | public class NamedProperty 12 | { 13 | /// 14 | /// Property Value 15 | /// 16 | public virtual object Value { get; set; } 17 | 18 | /// 19 | /// Property Name, which is rendered in the template 20 | /// 21 | public string Name { get; set; } 22 | } 23 | 24 | /// 25 | public class NamedProperty : NamedProperty 26 | { 27 | /// 28 | /// Property Value 29 | /// 30 | public new T Value { get { return (T)base.Value; } set { base.Value = value; } } 31 | } 32 | 33 | /// 34 | /// Helpers to create Named Properties (properties with Name and Value). 35 | /// You can either add "using static InterpolatedLogging.NamedProperties;" and invoke directly NP{T}() factory, 36 | /// or add "using InterpolatedLogging;" and invoke factory as "NamedProperties.NP{T}()". 37 | /// 38 | public class NamedProperties 39 | { 40 | /// 41 | /// Created a NamedProperty (property with Name and Value) by explicitly providing its value and name 42 | /// 43 | public static NamedProperty NP(T propertyValue, string propertyName) 44 | { 45 | return new NamedProperty() { Name = propertyName, Value = propertyValue }; 46 | } 47 | 48 | /* this expression syntax is slow and has no benefit over other formats */ 49 | /* 50 | /// 51 | /// Created a NamedProperty (property with Name and Value) by providing an expression (x) => variableOrProperty 52 | /// which will be invoked to get the property value and will be reflected upon to get the property name (unless it's explicitly provided). 53 | /// 54 | /// If this is not provided the property will be named based on the lambda expression (name of variableOrProperty) 55 | public static NamedProperty NP(Expression> f, string propertyName = null) 56 | { 57 | T propertyValue = f.Compile().Invoke(""); 58 | propertyName = propertyName ?? (f.Body as MemberExpression).Member.Name; 59 | return new NamedProperty() { Name = propertyName, Value = propertyValue }; 60 | } 61 | */ 62 | } 63 | 64 | } 65 | -------------------------------------------------------------------------------- /src/InterpolatedLogging/StructuredLogMessage.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Diagnostics; 4 | using System.Linq; 5 | using System.Reflection; 6 | using System.Text; 7 | using System.Text.RegularExpressions; 8 | 9 | namespace InterpolatedLogging 10 | { 11 | /// 12 | /// 13 | /// 14 | [DebuggerDisplay("{DebuggerDisplay,nq}")] 15 | public class StructuredLogMessage 16 | { 17 | #region Properties 18 | /// 19 | /// Template to be passed to your Logging library. 20 | /// Parsed with property names instead of positions. 21 | /// E.g. "User '{UserName}' logged in at {Date}, operation took {OperationElapsedTime}ms". 22 | /// 23 | public string MessageTemplate { get { Parse(); return _messageTemplate; } } 24 | 25 | /// 26 | /// Property values to be passed to your Logging library 27 | /// 28 | public object[] Properties { get { Parse(); return _properties; } } 29 | #endregion 30 | 31 | #region ctor 32 | /// 33 | public StructuredLogMessage(FormattableString fs) 34 | { 35 | _fs = fs; // just save it for later (lazy rendering) - depending on levels we might not even have to render this message 36 | } 37 | #endregion 38 | 39 | #region Private Members 40 | private FormattableString _fs; 41 | private string _messageTemplate; 42 | private object[] _properties; 43 | StringBuilder sb = new StringBuilder(); 44 | List propertiesLst = new List(); 45 | int unamedProperties = 0; 46 | #endregion 47 | 48 | 49 | #region Static/Constant 50 | private static Regex _formattableArgumentRegex = new Regex( 51 | "(?@)?{(?\\d*)(:(?[^}]*))?}", 52 | RegexOptions.IgnoreCase 53 | | RegexOptions.Singleline 54 | | RegexOptions.CultureInvariant 55 | | RegexOptions.IgnorePatternWhitespace 56 | | RegexOptions.Compiled 57 | ); 58 | private static Regex colonSplit = new Regex("(^|:)(?(\\'([^'])*'|[^:']*)*)", 59 | RegexOptions.CultureInvariant 60 | | RegexOptions.IgnorePatternWhitespace 61 | | RegexOptions.Compiled 62 | ); 63 | 64 | #endregion 65 | 66 | #region Parse 67 | 68 | private void Parse() 69 | { 70 | // already parsed? 71 | if (_messageTemplate != null) 72 | return; 73 | ParseInner(_fs.Format, _fs.GetArguments()); 74 | 75 | _messageTemplate = sb.ToString(); 76 | _properties = propertiesLst.ToArray(); 77 | 78 | } 79 | 80 | private void ParseInner(string format, object[] arguments) 81 | { 82 | if (string.IsNullOrEmpty(format)) 83 | return; 84 | 85 | var matches = _formattableArgumentRegex.Matches(format); 86 | int currentBlockEndPos = 0; 87 | int previousBlockEndPos = 0; 88 | for (int i = 0; i < matches.Count; i++) 89 | { 90 | // unescape escaped curly braces 91 | previousBlockEndPos = currentBlockEndPos; 92 | currentBlockEndPos = matches[i].Index + matches[i].Length; 93 | string currentTextBlock = format.Substring(previousBlockEndPos, matches[i].Index - previousBlockEndPos).Replace("{{", "{").Replace("}}", "}"); 94 | 95 | // arguments[i] may not work because same argument can be used multiple times 96 | int argPos = int.Parse(matches[i].Groups["ArgPos"].Value); 97 | string argFormat = matches[i].Groups["Format"].Value; 98 | 99 | // Serilog @ destructuring operator 100 | string prefixModifier = matches[i].Groups["PrefixModifier"].Value; 101 | 102 | sb.Append(currentTextBlock); 103 | 104 | // Split multiple formats by colon (:), and accept literal colons surrounded by single quotes 105 | //List argFormats = argFormat.Split(new char[] { ':' }).Select(f => f.Replace("':'", ":")).ToList(); 106 | List argFormats = string.IsNullOrEmpty(argFormat) ? new List() : 107 | colonSplit.Matches(argFormat).Cast() 108 | .Select(m => m.Groups["ArgFormat"].Value.Replace("':'", ":")).ToList(); 109 | 110 | object arg = arguments[argPos]; 111 | if (argFormats.Contains("raw")) // interpolated arguments which are supposed to be rendered as raw strings, not as isolated properties 112 | { 113 | sb.Append(arg); 114 | continue; 115 | } 116 | if (arg is FormattableString fsArg) // Support nested FormattableString 117 | { 118 | ParseInner(fsArg.Format, fsArg.GetArguments()); 119 | continue; 120 | } 121 | 122 | Type argType = arg?.GetType(); 123 | PropertyInfo[] props; 124 | 125 | //if (argType.IsGenericType && argType.GetGenericTypeDefinition() == typeof(NamedProperty<>)) 126 | if (argType != null && argType.IsSubclassOf(typeof(NamedProperty))) 127 | { 128 | sb.Append("{" + prefixModifier + ((NamedProperty)arg).Name + (argFormat.Length > 0 ? ":" + argFormat : "") + "}"); 129 | propertiesLst.Add(((NamedProperty)arg).Value); 130 | continue; 131 | } 132 | 133 | // anonymous type with single property - get property name 134 | // e.g: " User {new { UserName = user }} " 135 | if (argType != null && argType.Name.StartsWith("<>f__AnonymousType") && (props = argType.GetProperties()) != null && props.Length == 1) 136 | { 137 | sb.Append("{" + prefixModifier + props[0].Name + (argFormat.Length > 0 ? ":" + argFormat : "") + "}"); 138 | propertiesLst.Add(props[0].GetValue(arg)); 139 | continue; 140 | } 141 | 142 | // Format contains Property Name 143 | // e.g: " User {user:UserName} " 144 | if (argFormats.Count == 1) 145 | { 146 | sb.Append("{" + prefixModifier + argFormat.Trim(' ') + "}"); 147 | propertiesLst.Add(arg); 148 | continue; 149 | } 150 | 151 | // Format contains Property Name and format 152 | // e.g: " Date {now:Timestamp:yyyy-MM-dd HH:mm:sss} " 153 | if (argFormats.Count == 2) 154 | { 155 | sb.Append("{" + prefixModifier + argFormats[0].Trim(' ') + ":" + argFormats[1] + "}"); 156 | propertiesLst.Add(arg); 157 | continue; 158 | } 159 | 160 | sb.Append("{" + prefixModifier + (unamedProperties++).ToString() + (argFormat.Length > 0 ? ":" + argFormat : "") + "}"); 161 | propertiesLst.Add(arg); 162 | 163 | } 164 | string lastPart = format.Substring(currentBlockEndPos).Replace("{{", "{").Replace("}}", "}"); 165 | sb.Append(lastPart); 166 | } 167 | #endregion 168 | 169 | #region Misc 170 | 171 | private string DebuggerDisplay 172 | { 173 | get 174 | { 175 | Parse(); 176 | //return $"\"{_messageTemplate}\": {}"; 177 | StringBuilder sb = new StringBuilder($"\"{_messageTemplate}\""); 178 | for (int i = 0; i < _properties.Length; i++) 179 | System.Diagnostics.Debug.WriteLine($"Arg[{i}]: {_properties[i]}"); 180 | return sb.ToString(); 181 | } 182 | } 183 | #endregion 184 | 185 | } 186 | } 187 | -------------------------------------------------------------------------------- /src/build-release.ps1: -------------------------------------------------------------------------------- 1 | 2 | $msbuild = ( 3 | "$Env:programfiles\Microsoft Visual Studio\2022\Enterprise\MSBuild\Current\Bin\msbuild.exe", 4 | "$Env:programfiles\Microsoft Visual Studio\2022\Professional\MSBuild\Current\Bin\msbuild.exe", 5 | "$Env:programfiles\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\msbuild.exe", 6 | "$Env:programfiles (x86)\Microsoft Visual Studio\2019\Enterprise\MSBuild\Current\Bin\msbuild.exe", 7 | "$Env:programfiles (x86)\Microsoft Visual Studio\2019\Professional\MSBuild\Current\Bin\msbuild.exe", 8 | "$Env:programfiles (x86)\Microsoft Visual Studio\2019\Community\MSBuild\Current\Bin\msbuild.exe", 9 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\BuildTools\MSBuild\15.0\Bin\msbuild.exe", 10 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Enterprise\MSBuild\15.0\Bin\msbuild.exe", 11 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Professional\MSBuild\15.0\Bin\msbuild.exe", 12 | "$Env:programfiles (x86)\Microsoft Visual Studio\2017\Community\MSBuild\15.0\Bin\msbuild.exe", 13 | "${Env:ProgramFiles(x86)}\MSBuild\14.0\Bin\MSBuild.exe", 14 | "${Env:ProgramFiles(x86)}\MSBuild\13.0\Bin\MSBuild.exe", 15 | "${Env:ProgramFiles(x86)}\MSBuild\12.0\Bin\MSBuild.exe" 16 | ) | Where-Object { Test-Path $_ } | Select-Object -first 1 17 | 18 | 19 | 20 | Remove-Item -Recurse -Force -ErrorAction Ignore ".\packages-local" 21 | Remove-Item -Recurse -Force -ErrorAction Ignore "$env:HOMEDRIVE$env:HOMEPATH\.nuget\packages\InterpolatedLogging.Microsoft.Extensions.Logging" 22 | Remove-Item -Recurse -Force -ErrorAction Ignore "$env:HOMEDRIVE$env:HOMEPATH\.nuget\packages\InterpolatedLogging.NLog" 23 | Remove-Item -Recurse -Force -ErrorAction Ignore "$env:HOMEDRIVE$env:HOMEPATH\.nuget\packages\InterpolatedLogging.Serilog" 24 | 25 | $configuration = "Release"; 26 | 27 | 28 | dotnet clean 29 | & $msbuild ".\InterpolatedLogging.Microsoft.Extensions.Logging\InterpolatedLogging.Microsoft.Extensions.Logging.csproj" ` 30 | /t:Restore /t:Build /t:Pack ` 31 | /p:PackageOutputPath="..\..\packages-local\" ` 32 | '/p:targetFrameworks="netstandard2.0;net472;net5.0"' ` 33 | /p:Configuration=$configuration ` 34 | /p:IncludeSymbols=true ` 35 | /p:SymbolPackageFormat=snupkg ` 36 | /verbosity:minimal ` 37 | /p:ContinuousIntegrationBuild=true 38 | 39 | # dotnet test InterpolatedLogging.Microsoft.Extensions.Logging.Tests\InterpolatedLogging.Microsoft.Extensions.Logging.Tests.csproj 40 | 41 | dotnet clean 42 | & $msbuild ".\InterpolatedLogging.NLog\InterpolatedLogging.NLog.csproj" ` 43 | /t:Restore /t:Build /t:Pack ` 44 | /p:PackageOutputPath="..\..\packages-local\" ` 45 | '/p:targetFrameworks="netstandard2.0;net472;net5.0"' ` 46 | /p:Configuration=$configuration ` 47 | /p:IncludeSymbols=true ` 48 | /p:SymbolPackageFormat=snupkg ` 49 | /verbosity:minimal ` 50 | /p:ContinuousIntegrationBuild=true 51 | # dotnet test InterpolatedLogging.NLog.Tests\InterpolatedLogging.NLog.Tests.csproj 52 | 53 | dotnet clean 54 | & $msbuild ".\InterpolatedLogging.Serilog\InterpolatedLogging.Serilog.csproj" ` 55 | /t:Restore /t:Build /t:Pack ` 56 | /p:PackageOutputPath="..\..\packages-local\" ` 57 | '/p:targetFrameworks="netstandard2.0;net472;net5.0"' ` 58 | /p:Configuration=$configuration ` 59 | /p:IncludeSymbols=true ` 60 | /p:SymbolPackageFormat=snupkg ` 61 | /verbosity:minimal ` 62 | /p:ContinuousIntegrationBuild=true 63 | # dotnet test InterpolatedLogging.Serilog.Tests\InterpolatedLogging.Serilog.Tests.csproj 64 | --------------------------------------------------------------------------------