Repository: serilog/serilog-expressions Branch: dev Commit: 697c8fa89ec1 Files: 188 Total size: 526.8 KB Directory structure: gitextract_cw7ezsg_/ ├── .gitattributes ├── .github/ │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ └── workflows/ │ └── ci.yml ├── .gitignore ├── Build.ps1 ├── Directory.Build.props ├── Directory.Version.props ├── LICENSE ├── README.md ├── RunPerfTests.ps1 ├── assets/ │ └── Serilog.snk ├── example/ │ └── Sample/ │ ├── Program.cs │ └── Sample.csproj ├── global.json ├── serilog-expressions.sln ├── serilog-expressions.sln.DotSettings ├── src/ │ └── Serilog.Expressions/ │ ├── Expressions/ │ │ ├── Ast/ │ │ │ ├── AccessorExpression.cs │ │ │ ├── AmbientNameExpression.cs │ │ │ ├── ArrayExpression.cs │ │ │ ├── CallExpression.cs │ │ │ ├── ConstantExpression.cs │ │ │ ├── Element.cs │ │ │ ├── Expression.cs │ │ │ ├── IndexOfMatchExpression.cs │ │ │ ├── IndexerExpression.cs │ │ │ ├── IndexerWildcard.cs │ │ │ ├── IndexerWildcardExpression.cs │ │ │ ├── ItemElement.cs │ │ │ ├── LambdaExpression.cs │ │ │ ├── LocalNameExpression.cs │ │ │ ├── Member.cs │ │ │ ├── ObjectExpression.cs │ │ │ ├── ParameterExpression.cs │ │ │ ├── PropertyMember.cs │ │ │ ├── SpreadElement.cs │ │ │ └── SpreadMember.cs │ │ ├── BuiltInProperty.cs │ │ ├── Compilation/ │ │ │ ├── Arrays/ │ │ │ │ └── ConstantArrayEvaluator.cs │ │ │ ├── DefaultFunctionNameResolver.cs │ │ │ ├── ExpressionCompiler.cs │ │ │ ├── ExpressionValidationException.cs │ │ │ ├── Linq/ │ │ │ │ ├── EventIdHash.cs │ │ │ │ ├── ExpressionConstantMapper.cs │ │ │ │ ├── Intrinsics.cs │ │ │ │ ├── LinqExpressionCompiler.cs │ │ │ │ └── ParameterReplacementVisitor.cs │ │ │ ├── OrderedNameResolver.cs │ │ │ ├── Pattern.cs │ │ │ ├── Properties/ │ │ │ │ └── PropertiesObjectAccessorTransformer.cs │ │ │ ├── Text/ │ │ │ │ ├── LikeSyntaxTransformer.cs │ │ │ │ └── TextMatchingTransformer.cs │ │ │ ├── Transformations/ │ │ │ │ ├── IdentityTransformer.cs │ │ │ │ ├── NodeReplacer.cs │ │ │ │ └── SerilogExpressionTransformer.cs │ │ │ ├── Variadics/ │ │ │ │ └── VariadicCallRewriter.cs │ │ │ └── Wildcards/ │ │ │ ├── WildcardComprehensionTransformer.cs │ │ │ └── WildcardSearch.cs │ │ ├── CompiledExpression.cs │ │ ├── Evaluatable.cs │ │ ├── EvaluationContext.cs │ │ ├── ExpressionResult.cs │ │ ├── Helpers.cs │ │ ├── LoggingFilterSwitch.cs │ │ ├── NameResolver.cs │ │ ├── Operators.cs │ │ ├── Parsing/ │ │ │ ├── Combinators.cs │ │ │ ├── ExpressionKeyword.cs │ │ │ ├── ExpressionParser.cs │ │ │ ├── ExpressionTextParsers.cs │ │ │ ├── ExpressionToken.cs │ │ │ ├── ExpressionTokenParsers.cs │ │ │ ├── ExpressionTokenizer.cs │ │ │ └── ParserExtensions.cs │ │ ├── Runtime/ │ │ │ ├── Coerce.cs │ │ │ ├── Locals.cs │ │ │ ├── RuntimeOperators.cs │ │ │ └── Support/ │ │ │ └── UnflattenDottedPropertyNames.cs │ │ ├── SerilogExpression.cs │ │ └── StaticMemberNameResolver.cs │ ├── LoggerEnrichmentConfigurationExtensions.cs │ ├── LoggerFilterConfigurationExtensions.cs │ ├── LoggerSinkConfigurationExtensions.cs │ ├── ParserConstruction/ │ │ ├── Combinators.cs │ │ ├── Display/ │ │ │ ├── Presentation.cs │ │ │ └── TokenAttribute.cs │ │ ├── Model/ │ │ │ ├── Position.cs │ │ │ ├── Result.cs │ │ │ ├── Result`1.cs │ │ │ ├── TextSpan.cs │ │ │ ├── TokenListParserResult.cs │ │ │ ├── TokenListParserResult`2.cs │ │ │ ├── TokenList`1.cs │ │ │ ├── Token`1.cs │ │ │ └── Unit.cs │ │ ├── Parse.cs │ │ ├── ParseException.cs │ │ ├── ParserExtensions.cs │ │ ├── Parsers/ │ │ │ ├── Character.cs │ │ │ ├── Numerics.cs │ │ │ ├── Span.cs │ │ │ └── Token.cs │ │ ├── README.md │ │ ├── TextParser`1.cs │ │ ├── TokenListParser`2.cs │ │ ├── Tokenizer`1.cs │ │ └── Util/ │ │ ├── CharInfo.cs │ │ └── Friendly.cs │ ├── Pipeline/ │ │ └── ComputedPropertyEnricher.cs │ ├── Properties/ │ │ └── AssemblyInfo.cs │ ├── Serilog.Expressions.csproj │ └── Templates/ │ ├── Ast/ │ │ ├── Conditional.cs │ │ ├── FormattedExpression.cs │ │ ├── LiteralText.cs │ │ ├── Repetition.cs │ │ ├── Template.cs │ │ └── TemplateBlock.cs │ ├── Compilation/ │ │ ├── CompiledConditional.cs │ │ ├── CompiledExceptionToken.cs │ │ ├── CompiledFormattedExpression.cs │ │ ├── CompiledLevelToken.cs │ │ ├── CompiledLiteralText.cs │ │ ├── CompiledMessageToken.cs │ │ ├── CompiledRepetition.cs │ │ ├── CompiledTemplate.cs │ │ ├── CompiledTemplateBlock.cs │ │ ├── CompiledTimestampToken.cs │ │ ├── NameResolution/ │ │ │ ├── ExpressionLocalNameBinder.cs │ │ │ └── TemplateLocalNameBinder.cs │ │ ├── TemplateCompiler.cs │ │ ├── TemplateFunctionNameResolver.cs │ │ ├── UnreferencedProperties/ │ │ │ ├── ExpressionReferencedPropertiesFinder.cs │ │ │ ├── TemplateReferencedPropertiesFinder.cs │ │ │ └── UnreferencedPropertiesFunction.cs │ │ └── Unsafe/ │ │ └── UnsafeOutputFunction.cs │ ├── Encoding/ │ │ ├── EncodedCompiledTemplate.cs │ │ ├── EncodedTemplateFactory.cs │ │ ├── EscapableEncodedCompiledFormattedExpression.cs │ │ ├── PreEncodedValue.cs │ │ └── TemplateOutputEncoder.cs │ ├── ExpressionTemplate.cs │ ├── Parsing/ │ │ ├── TemplateParser.cs │ │ ├── TemplateTokenParsers.cs │ │ └── TemplateTokenizer.cs │ ├── Rendering/ │ │ ├── AlignmentExtensions.cs │ │ ├── Casing.cs │ │ ├── LevelRenderer.cs │ │ └── Padding.cs │ └── Themes/ │ ├── Style.cs │ ├── StyleReset.cs │ ├── TemplateTheme.cs │ ├── TemplateThemeStyle.cs │ ├── TemplateThemes.cs │ └── ThemedJsonValueFormatter.cs └── test/ ├── Serilog.Expressions.PerformanceTests/ │ ├── ComparisonBenchmark.cs │ ├── Harness.cs │ ├── Serilog.Expressions.PerformanceTests.csproj │ └── Support/ │ └── Some.cs └── Serilog.Expressions.Tests/ ├── Cases/ │ ├── expression-evaluation-cases.asv │ ├── template-encoding-cases.asv │ ├── template-evaluation-cases.asv │ └── translation-cases.asv ├── ConfigurationTests.cs ├── ExpressionCompilerTests.cs ├── ExpressionEvaluationTests.cs ├── ExpressionParserTests.cs ├── ExpressionTranslationTests.cs ├── ExpressionValidationTests.cs ├── ExpressionValueTests.cs ├── Expressions/ │ ├── NameResolverTests.cs │ └── Runtime/ │ ├── LocalsTests.cs │ └── RuntimeOperatorsTests.cs ├── FormatParityTests.cs ├── LoggingFilterSwitchTests.cs ├── Properties/ │ └── launchSettings.json ├── Serilog.Expressions.Tests.csproj ├── Support/ │ ├── AsvCases.cs │ ├── CollectingSink.cs │ ├── ParenthesizingEncoder.cs │ ├── Some.cs │ ├── StringHashPrefixingTheme.cs │ └── TestHelperNameResolver.cs ├── TemplateEncodingTests.cs ├── TemplateEvaluationTests.cs ├── TemplateParserTests.cs ├── TemplateTokenizerTests.cs └── Templates/ └── UnreferencedPropertiesFunctionTests.cs ================================================ FILE CONTENTS ================================================ ================================================ FILE: .gitattributes ================================================ # Auto detect text files and perform LF normalization * text=auto ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Report a bug and help us to improve Serilog.Expressions title: '' labels: bug assignees: '' --- The Serilog maintainers want you to have a great experience using Serilog.Expressions, and will happily track down and resolve bugs. We all have limited time, though, so please think through all of the factors that might be involved and include as much useful information as possible 😊. ℹ If the problem is caused by a sink or other extension package, please track down the correct repository for that package and create the report there: this tracker is for the **Serilog.Expressions** package only. **Description** What's going wrong? **Reproduction** Please provide code samples showing how you're configuring and calling Serilog.Expressions to produce the behavior. **Expected behavior** A concise description of what you expected to happen. **Relevant package, tooling and runtime versions** What Serilog version are you using, on what platform? **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ contact_links: - name: Ask for help url: https://stackoverflow.com/tags/serilog about: Ask the community for help on how to use Serilog.Expressions ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an improvement to Serilog.Expressions title: '' labels: enhancement assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. For example, "I'd like to do _x_ but currently I can't because _y_ [...]". **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any workarounds or alternative solutions you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/ci.yml ================================================ # If this file is renamed, the incrementing run attempt number will be reset. name: CI on: push: branches: [ "dev", "main" ] pull_request: branches: [ "dev", "main" ] env: CI_BUILD_NUMBER_BASE: ${{ github.run_number }} CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} jobs: build: # The build must run on Windows so that .NET Framework targets can be built and tested. runs-on: windows-latest permissions: contents: write steps: - uses: actions/checkout@v4 - name: Setup uses: actions/setup-dotnet@v4 with: dotnet-version: 9.0.x - name: Compute build number shell: bash run: | echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+2300))" >> $GITHUB_ENV - name: Build and Publish env: DOTNET_CLI_TELEMETRY_OPTOUT: true NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} shell: pwsh run: | ./Build.ps1 ================================================ FILE: .gitignore ================================================ ## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. # User-specific files *.suo *.user *.userosscache *.sln.docstates .idea # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs # Build results [Dd]ebug/ [Dd]ebugPublic/ [Rr]elease/ [Rr]eleases/ x64/ x86/ bld/ [Bb]in/ [Oo]bj/ # Visual Studio 2015 cache/options directory .vs/ # Uncomment if you have tasks that create the project's static files in wwwroot #wwwroot/ # MSTest test Results [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* # NUNIT *.VisualState.xml TestResult.xml # Build Results of an ATL Project [Dd]ebugPS/ [Rr]eleasePS/ dlldata.c # DNX project.lock.json artifacts/ *_i.c *_p.c *_i.h *.ilk *.meta *.obj *.pch *.pdb *.pgc *.pgd *.rsp *.sbr *.tlb *.tli *.tlh *.tmp *.tmp_proj *.log *.vspscc *.vssscc .builds *.pidb *.svclog *.scc # Chutzpah Test files _Chutzpah* # Visual C++ cache files ipch/ *.aps *.ncb *.opendb *.opensdf *.sdf *.cachefile # Visual Studio profiler *.psess *.vsp *.vspx *.sap # TFS 2012 Local Workspace $tf/ # Guidance Automation Toolkit *.gpState # ReSharper is a .NET coding add-in _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user # JustCode is a .NET coding add-in .JustCode # TeamCity is a build add-in _TeamCity* # DotCover is a Code Coverage Tool *.dotCover # NCrunch _NCrunch_* .*crunch*.local.xml nCrunchTemp_* # MightyMoose *.mm.* AutoTest.Net/ # Web workbench (sass) .sass-cache/ # Installshield output folder [Ee]xpress/ # DocProject is a documentation generator add-in DocProject/buildhelp/ DocProject/Help/*.HxT DocProject/Help/*.HxC DocProject/Help/*.hhc DocProject/Help/*.hhk DocProject/Help/*.hhp DocProject/Help/Html2 DocProject/Help/html # Click-Once directory publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml # TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj # NuGet Packages *.nupkg # The packages folder can be ignored because of Package Restore **/packages/* # except build/, which is used as an MSBuild target. !**/packages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/packages/repositories.config # NuGet v3's project.json files produces more ignoreable files *.nuget.props *.nuget.targets # Microsoft Azure Build Output csx/ *.build.csdef # Microsoft Azure Emulator ecf/ rcf/ # Microsoft Azure ApplicationInsights config file ApplicationInsights.config # Windows Store app package directory AppPackages/ BundleArtifacts/ # Visual Studio cache files # files ending in .cache can be ignored *.[Cc]ache # but keep track of directories ending in .cache !*.[Cc]ache/ # Others ClientBin/ ~$* *~ *.dbmdl *.dbproj.schemaview *.pfx *.publishsettings node_modules/ orleans.codegen.cs # RIA/Silverlight projects Generated_Code/ # Backup & report files from converting an old project file # to a newer Visual Studio version. Backup files are not needed, # because we have git ;-) _UpgradeReport_Files/ Backup*/ UpgradeLog*.XML UpgradeLog*.htm # SQL Server files *.mdf *.ldf # Business Intelligence projects *.rdl.data *.bim.layout *.bim_*.settings # Microsoft Fakes FakesAssemblies/ # GhostDoc plugin setting file *.GhostDoc.xml # Node.js Tools for Visual Studio .ntvs_analysis.dat # Visual Studio 6 build log *.plg # Visual Studio 6 workspace options file *.opt # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts **/*.DesktopClient/ModelManifest.xml **/*.Server/GeneratedArtifacts **/*.Server/ModelManifest.xml _Pvt_Extensions # Paket dependency manager .paket/paket.exe # FAKE - F# Make .fake/ example/Sample/log.txt BenchmarkDotNet.Artifacts/ ================================================ FILE: Build.ps1 ================================================ Write-Output "build: Tool versions follow" dotnet --version dotnet --list-sdks Write-Output "build: Build started" Push-Location $PSScriptRoot try { if(Test-Path .\artifacts) { Write-Output "build: Cleaning ./artifacts" Remove-Item ./artifacts -Force -Recurse } & dotnet restore --no-cache $dbp = [Xml] (Get-Content .\Directory.Version.props) $versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix Write-Output "build: Package version prefix is $versionPrefix" $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER]; $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] $commitHash = $(git rev-parse --short HEAD) $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] Write-Output "build: Package version suffix is $suffix" Write-Output "build: Build version suffix is $buildSuffix" & dotnet build -c Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true if($LASTEXITCODE -ne 0) { throw "Build failed" } foreach ($src in Get-ChildItem src/*) { Push-Location $src Write-Output "build: Packaging project in $src" if ($suffix) { & dotnet pack -c Release --no-build --no-restore -o ../../artifacts --version-suffix=$suffix } else { & dotnet pack -c Release --no-build --no-restore -o ../../artifacts } if($LASTEXITCODE -ne 0) { throw "Packaging failed" } Pop-Location } foreach ($test in Get-ChildItem test/*.Tests) { Push-Location $test Write-Output "build: Testing project in $test" & dotnet test -c Release --no-build --no-restore if($LASTEXITCODE -ne 0) { throw "Testing failed" } Pop-Location } if ($env:NUGET_API_KEY) { # GitHub Actions will only supply this to branch builds and not PRs. We publish # builds from any branch this action targets (i.e. main and dev). Write-Output "build: Publishing NuGet packages" foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" if($LASTEXITCODE -ne 0) { throw "Publishing failed" } } if (!($suffix)) { Write-Output "build: Creating release for version $versionPrefix" iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)" } } } finally { Pop-Location } ================================================ FILE: Directory.Build.props ================================================ latest True true $(MSBuildThisFileDirectory)assets/Serilog.snk false enable enable true true true true snupkg ================================================ FILE: Directory.Version.props ================================================ 5.1.0 ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # Serilog.Expressions [![Build status](https://github.com/serilog/serilog-expressions/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/serilog/serilog-expressions/actions) [![NuGet Package](https://img.shields.io/nuget/vpre/serilog.expressions)](https://nuget.org/packages/serilog.expressions) An embeddable mini-language for filtering, enriching, and formatting Serilog events, ideal for use with JSON or XML configuration. ## Getting started Install the package from NuGet: ```shell dotnet add package Serilog.Expressions ``` The package adds extension methods to Serilog's `Filter`, `WriteTo`, and `Enrich` configuration objects, along with an `ExpressionTemplate` type that's compatible with Serilog sinks accepting an `ITextFormatter`. ### Filtering example _Serilog.Expressions_ adds `ByExcluding()` and `ByIncludingOnly()` overloads to the `Filter` configuration object that accept filter expressions: ```csharp Log.Logger = new LoggerConfiguration() .Filter.ByExcluding("RequestPath like '/health%'") .CreateLogger(); ``` Events with a `RequestPath` property that matches the expression will be excluded by the filter. > Note that if the expression syntax is invalid, an `ArgumentException` will be thrown from the `ByExcluding()` method, and by similar methods elsewhere in the package. To check expression syntax without throwing, see the `Try*()` methods in the `SerilogExpression` class. #### An `appSettings.json` JSON configuration example In [`appSettings.json` configuration](https://github.com/serilog/serilog-settings-configuration) this is written as: ```json { "Serilog": { "Using": ["Serilog.Expressions"], "Filter": [ { "Name": "ByExcluding", "Args": { "expression": "RequestPath like '/health%'" } } ] } } ``` #### An `` XML configuration example In [XML configuration files](https://github.com/serilog/serilog-settings-appsettings), this is written as: ```xml ``` ## Supported configuration APIs _Serilog.Expressions_ adds a number of expression-based overloads and helper methods to the Serilog configuration syntax: * `Filter.ByExcluding()`, `Filter.ByIncludingOnly()` - use an expression to filter events passing through the Serilog pipeline * `WriteTo.Conditional()` - use an expression to select the events passed to a particular sink * `Enrich.When()` - conditionally enable an enricher when events match an expression * `Enrich.WithComputed()` - add or modify event properties using an expression ## Formatting with `ExpressionTemplate` _Serilog.Expressions_ includes the `ExpressionTemplate` class for text formatting. `ExpressionTemplate` implements `ITextFormatter`, so it works with any text-based Serilog sink, including `Console`, `File`, `Debug`, and `Email`: ```csharp // using Serilog.Templates; Log.Logger = new LoggerConfiguration() .WriteTo.Console(new ExpressionTemplate( "[{@t:HH:mm:ss} {@l:u3} ({SourceContext})] {@m} (first item is {Cart[0]})\n{@x}")) .CreateLogger(); // Produces log events like: // [21:21:40 INF (Sample.Program)] Cart contains ["Tea","Coffee"] (first item is Tea) ``` Templates are based on .NET format strings, and support standard padding, alignment, and format specifiers. Along with standard properties for the event timestamp (`@t`), level (`@l`) and so on, "holes" in expression templates can include complex expressions over the first-class properties of the event, like `{SourceContext}` and `{Cart[0]}` in the example.. Templates support customizable color themes when used with the `Console` sink: ```csharp .WriteTo.Console(new ExpressionTemplate( "[{@t:HH:mm:ss} {@l:u3}] {@m}\n{@x}", theme: TemplateTheme.Code)) ``` ![Screenshot showing colored terminal output](https://raw.githubusercontent.com/serilog/serilog-expressions/dev/assets/screenshot.png) Newline-delimited JSON (for example, replicating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated using object literals: ```csharp .WriteTo.Console(new ExpressionTemplate( "{ {@t, @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n")) ``` ## Language reference ### Properties The following properties are available in expressions: * **All first-class properties of the event** - no special syntax: `SourceContext` and `Cart` are used in the formatting examples above * `@t` - the event's timestamp, as a `DateTimeOffset` * `@m` - the rendered message (Note: do not add format specifiers like `:lj` or you'll lose theme color rendering. These format specifiers are not supported as they've become the default and only option - [see the discussion here](https://github.com/serilog/serilog-expressions/issues/56#issuecomment-1146472988) * `@mt` - the raw message template * `@l` - the event's level, as a `LogEventLevel` * `@x` - the exception associated with the event, if any, as an `Exception` * `@p` - a dictionary containing all first-class properties; this supports properties with non-identifier names, for example `@p['snake-case-name']` * `@i` - event id; a 32-bit numeric hash of the event's message template * `@r` - renderings; if any tokens in the message template include .NET-specific formatting, an array of rendered values for each such token * `@tr` - trace id; The id of the trace that was active when the event was created, if any * `@sp` - span id; The id of the span that was active when the event was created, if any The built-in properties mirror those available in the CLEF format. The exception property `@x` is treated as a scalar and will appear as a string when formatted into text. The properties of the underlying `Exception` object can be accessed using `Inspect()`, for example `Inspect(@x).Message`, and the type of the exception retrieved using `TypeOf(@x)`. ### Literals | Data type | Description | Examples | | :--- | :--- | :--- | | Null | Corresponds to .NET's `null` value | `null` | | Number | A number in decimal or hexadecimal notation, represented by .NET `decimal` | `0`, `100`, `-12.34`, `0xC0FFEE` | | String | A single-quoted Unicode string literal; to escape `'`, double it | `'pie'`, `'isn''t'`, `'😋'` | | Boolean | A Boolean value | `true`, `false` | | Array | An array of values, in square brackets | `[1, 'two', null]` | | Object | A mapping of string keys to values; keys that are valid identifiers do not need to be quoted | `{a: 1, 'b c': 2, d}` | Array and object literals support the spread operator: `[1, 2, ..others]`, `{a: 1, ..others}`. Specifying an undefined property in an object literal will remove it from the result: `{..User, Email: Undefined()}` ### Operators and conditionals A typical set of operators is supported: * Equality `=` and inequality `<>`, including for arrays and objects * Boolean `and`, `or`, `not` * Arithmetic `+`, `-`, `*`, `/`, `^`, `%` * Numeric comparison `<`, `<=`, `>`, `>=` * Existence `is null` and `is not null` * SQL-style `like` and `not like`, with `%` and `_` wildcards (double wildcards to escape them) * Array membership with `in` and `not in` * Accessors `a.b` * Indexers `a['b']` and `a[0]` * Wildcard indexing - `a[?]` any, and `a[*]` all * Conditional `if a then b else c` (all branches required; see also the section below on _conditional blocks_) Comparision operators that act on text all accept an optional postfix `ci` modifier to select case-insensitive comparisons: ``` User.Name like 'n%' ci ``` ### Functions Functions are called using typical `Identifier(args)` syntax. Except for the `IsDefined()` function, the result of calling a function will be undefined if: * any argument is undefined, or * any argument is of an incompatible type. | Function | Description | |:--------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | `Coalesce(a0, a1, [..aN])` | Returns the first defined, non-null argument. | | `Concat(s0, s1, [..sN])` | Concatenate two or more strings. | | `Contains(s, p)` | Tests whether the string `s` contains the substring `p`. | | `ElementAt(x, i)` | Retrieves a property of `x` by name `i`, or array element of `x` by numeric index `i`. | | `EndsWith(s, p)` | Tests whether the string `s` ends with substring `p`. | | `IndexOf(s, p)` | Returns the first index of substring `p` in string `s`, or -1 if the substring does not appear. | | `IndexOfMatch(s, p)` | Returns the index of the first match of regular expression `p` in string `s`, or -1 if the regular expression does not match. | | `Inspect(o, [deep])` | Read properties from an object captured as the scalar value `o`. | | `IsMatch(s, p)` | Tests whether the regular expression `p` matches within the string `s`. | | `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. | | `LastIndexOf(s, p)` | Returns the last index of substring `p` in string `s`, or -1 if the substring does not appear. | | `Length(x)` | Returns the length of a string or array. | | `Nest(o)` | Converts dotted (flattened) property names of object `o` into nested sub-objects. | | `Now()` | Returns `DateTimeOffset.Now`. | | `Replace(s, p, r)` | Replace occurrences of substring `p` in string `s` with replacement `r`. | | `Rest([deep])` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template. If `deep` is `true`, also excludes properties referenced in the event's message template. | | `Round(n, m)` | Round the number `n` to `m` decimal places. | | `StartsWith(s, p)` | Tests whether the string `s` starts with substring `p`. | | `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. | | `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). | | `ToString(x, [format])` | Convert `x` to a string, applying the format string `format` if `x` is `IFormattable`. | | `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. | | `Undefined()` | Explicitly mark an undefined value. | | `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. | Functions that compare text accept an optional postfix `ci` modifier to select case-insensitive comparisons: ``` StartsWith(User.Name, 'n') ci ``` ### Template directives #### Conditional blocks Within an `ExpressionTemplate`, a portion of the template can be conditionally evaluated using `#if`. ```csharp Log.Logger = new LoggerConfiguration() .WriteTo.Console(new ExpressionTemplate( "[{@t:HH:mm:ss} {@l:u3}{#if SourceContext is not null} ({SourceContext}){#end}] {@m}\n{@x}")) .CreateLogger(); // Produces log events like: // [21:21:45 INF] Starting up // [21:21:46 INF (Sample.Program)] Firing engines ``` The block between the `{#if }` and `{#end}` directives will only appear in the output if `` is `true` - in the example, events with a `SourceContext` include this in parentheses, while those without, don't. It's important to notice that the directive requires a Boolean `true` before the conditional block will be evaluated. It wouldn't be sufficient in this case to write `{#if SourceContext}`, since no values other than `true` are considered "truthy". The syntax supports `{#if }`, chained `{#else if }`, `{#else}`, and `{#end}`, with arbitrary nesting. #### Repetition If a log event includes structured data in arrays or objects, a template block can be repeated for each element or member using `#each`/`in` (newlines, double quotes and construction of the `ExpressionTemplate` omitted for clarity): ``` {@l:w4}: {SourceContext} {#each s in Scope}=> {s}{#delimit} {#end} {@m} {@x} ``` This example uses the optional `#delimit` to add a space between each element, producing output like: ``` info: Sample.Program => Main => TextFormattingExample Hello, world! ``` When using `{#each in }` over an object, such as the built-in `@p` (properties) object, `` will be bound to the _names_ of the properties of the object. To get to the _values_ of the properties, use a second binding: ``` {#each k, v in @p}{k} = {v}{#delimit},{#end} ``` This example, if an event has three properties, will produce output like: ``` Account = "nblumhardt", Cart = ["Tea", "Coffee"], Powerup = 42 ``` The syntax supports `{#each [, ] in }`, an optional `{#delimit}` block, and finally an optional `{#else}` block, which will be evaluated if the array or object is empty. ## Recipes **Trim down `SourceContext` to a type name only:** ``` Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1) ``` This expression takes advantage of `LastIndexOf()` returning -1 when no `.` character appears in `SourceContext`, to yield a `startIndex` of 0 in that case. **Write not-referenced context properties (only if there are any):** ``` {#if rest(true) <> {}} {#end} ``` **Access a property with a non-identifier name:** ``` @p['some name'] ``` Any structured value, including the built-in `@p`, can be indexed by string key. This means that `User.Name` and `User['Name']` are equivalent, for example. **Access a property with inconsistent casing:** ``` ElementAt(@p, 'someName') ci ``` `ElementAt()` is a function-call version of the `[]` indexer notation, which means it can accept the `ci` case-insensitivity modifier. **Format events as newline-delimited JSON (template, embedded in C# or JSON):** ``` { {Timestamp: @t, Username: User.Name} }\n ``` This output template shows the use of a space between the opening `{` of a hole, and the enclosed object literal with `Timestamp` and `Username` fields. The object will be formatted as JSON. The trailing `\n` is a C# or JSON newline literal (don't escape this any further, as it's not part of the output template syntax). ## Working with the raw API The package provides the class `SerilogExpression` in the `Serilog.Expressions` namespace for working with expressions. ```csharp if (SerilogExpression.TryCompile("RequestPath like '/health%'", out var compiled, out var error) { // `compiled` is a function that can be executed against `LogEvent`s: var result = compiled(someEvent); // `result` will contain a `LogEventPropertyValue`, or `null` if the result of evaluating the // expression is undefined (for example if the event has no `RequestPath` property). if (result is ScalarValue value && value.Value is bool matches && matches) { Console.WriteLine("The event matched."); } } else { // `error` describes a syntax error. Console.WriteLine($"Couldn't compile the expression; {error}."); } ``` Compiled expression delegates return `LogEventPropertyValue` because this is the most convenient type to work with in many Serilog scenarios (enrichers, sinks, ...). To convert the result to plain-old-.NET-types like `string`, `bool`, `Dictionary` and `Array`, use the functions in the `Serilog.Expressions.ExpressionResult` class: ```csharp var result = compiled(someEvent); // `true` only if `result` is a scalar Boolean `true`; `false` otherwise: if (ExpressionResult.IsTrue(result)) { Console.WriteLine("The event matched."); } ``` ## Implementing user-defined functions User-defined functions can be plugged in by implementing static methods that: * Return `LogEventPropertyValue?`, * Have arguments of type `LogEventPropertyValue?` or `LogEvent`, * If the `ci` modifier is supported, accept a `StringComparison`, and * If culture-specific formatting or comparisons are used, accepts an `IFormatProvider`. For example: ```csharp public static class MyFunctions { public static LogEventPropertyValue? IsHello( StringComparison comparison, LogEventPropertyValue? maybeHello) { if (maybeHello is ScalarValue sv && sv.Value is string s) return new ScalarValue(s.Equals("Hello", comparison)); // Undefined - argument was not a string. return null; } } ``` In the example, `IsHello('Hello')` will evaluate to `true`, `IsHello('HELLO')` will be `false`, `IsHello('HELLO') ci` will be `true`, and `IsHello(42)` will be undefined. User-defined functions are supplied through an instance of `NameResolver`: ```csharp var myFunctions = new StaticMemberNameResolver(typeof(MyFunctions)); var expr = SerilogExpression.Compile("IsHello(User.Name)", nameResolver: myFunctions); // Filter events based on whether `User.Name` is `'Hello'` :-) ``` ## Acknowledgements Includes the parser combinator implementation from [Superpower](https://github.com/datalust/superpower), copyright Datalust, Superpower Contributors, and Sprache Contributors; licensed under the Apache License, 2.0. ================================================ FILE: RunPerfTests.ps1 ================================================ Push-Location $PSScriptRoot ./Build.ps1 foreach ($test in ls test/*.PerformanceTests) { Push-Location $test echo "perf: Running performance test project in $test" & dotnet test -c Release if($LASTEXITCODE -ne 0) { exit 2 } Pop-Location } Pop-Location ================================================ FILE: example/Sample/Program.cs ================================================ using Serilog; using Serilog.Debugging; using Serilog.Templates; using Serilog.Templates.Themes; SelfLog.Enable(Console.Error); TextFormattingExample1(); JsonFormattingExample(); PipelineComponentExample(); TextFormattingExample2(); return; static void TextFormattingExample1() { using var log = new LoggerConfiguration() .Enrich.WithProperty("Application", "Sample") .WriteTo.Console(new ExpressionTemplate( "[{@t:HH:mm:ss} {@l:u3}" + "{#if SourceContext is not null} ({Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}){#end}] " + "{@m} (first item is {coalesce(Items[0], '')}) {rest()}\n{@x}", theme: TemplateTheme.Code)) .CreateLogger(); log.Information("Running {Example}", nameof(TextFormattingExample1)); log.ForContext() .Information("Cart contains {@Items}", new[] { "Tea", "Coffee" }); log.ForContext() .Information("Cart contains {@Items}", new[] { "Apricots" }); } static void JsonFormattingExample() { using var log = new LoggerConfiguration() .Enrich.WithProperty("Application", "Example") .WriteTo.Console(new ExpressionTemplate( "{ {@t: UtcDateTime(@t), @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n")) .CreateLogger(); log.Information("Running {Example}", nameof(JsonFormattingExample)); log.ForContext() .Information("Cart contains {@Items}", new[] { "Tea", "Coffee" }); log.ForContext() .Warning("Cart is empty"); } static void PipelineComponentExample() { using var log = new LoggerConfiguration() .Enrich.WithProperty("Application", "Example") .Enrich.WithComputed("FirstItem", "coalesce(Items[0], '')") .Enrich.WithComputed("SourceContext", "coalesce(Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1), '')") .Filter.ByIncludingOnly("Items is null or Items[?] like 'C%'") .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level:u3} ({SourceContext})] {Message:lj} (first item is {FirstItem}){NewLine}{Exception}") .CreateLogger(); log.Information("Running {Example}", nameof(PipelineComponentExample)); log.ForContext() .Information("Cart contains {@Items}", new[] { "Tea", "Coffee" }); log.ForContext() .Information("Cart contains {@Items}", new[] { "Apricots" }); } static void TextFormattingExample2() { // Emulates `Microsoft.Extensions.Logging`'s `ConsoleLogger`. var melon = new TemplateTheme(TemplateTheme.Literate, new Dictionary { // `Information` is dark green in MEL. [TemplateThemeStyle.LevelInformation] = "\x1b[38;5;34m", [TemplateThemeStyle.String] = "\x1b[38;5;159m", [TemplateThemeStyle.Number] = "\x1b[38;5;159m" }); using var log = new LoggerConfiguration() .WriteTo.Console(new ExpressionTemplate( "{@l:w4}: {SourceContext}\n" + "{#if Scope is not null}" + " {#each s in Scope}=> {s}{#delimit} {#end}\n" + "{#end}" + " {@m}\n" + "{@x}", theme: melon)) .CreateLogger(); var program = log.ForContext(); program.Information("Host listening at {ListenUri}", "https://hello-world.local"); program .ForContext("Scope", new[] {"Main", "TextFormattingExample2()"}) .Information("HTTP {Method} {Path} responded {StatusCode} in {Elapsed:0.000} ms", "GET", "/api/hello", 200, 1.23); program.Warning("We've reached the end of the line"); } ================================================ FILE: example/Sample/Sample.csproj ================================================  net9.0 Exe false ================================================ FILE: global.json ================================================ { "sdk": { "version": "9.0.200", "allowPrerelease": false, "rollForward": "latestFeature" } } ================================================ FILE: serilog-expressions.sln ================================================  Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.31410.223 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{91E482DE-E1E7-4CE1-9511-C0AF07F3648A}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "global", "global", "{24B1126F-184C-469E-9F06-C1019DEF165A}" ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes .gitignore = .gitignore Build.ps1 = Build.ps1 Directory.Build.props = Directory.Build.props LICENSE = LICENSE README.md = README.md RunPerfTests.ps1 = RunPerfTests.ps1 Directory.Version.props = Directory.Version.props global.json = global.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{BD94A77E-34B1-478E-B921-E87A5F71B574}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B03B3086-D197-4B32-9AE2-8536C345EA2D}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "example\Sample\Sample.csproj", "{776EECAC-3C50-45EA-847D-0EBE5158E51E}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Expressions", "src\Serilog.Expressions\Serilog.Expressions.csproj", "{A420C4E3-3A2D-4369-88EB-77E4DB1D0219}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Expressions.Tests", "test\Serilog.Expressions.Tests\Serilog.Expressions.Tests.csproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Expressions.PerformanceTests", "test\Serilog.Expressions.PerformanceTests\Serilog.Expressions.PerformanceTests.csproj", "{D7A37F73-BBA3-4DAE-9648-1A753A86F968}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {776EECAC-3C50-45EA-847D-0EBE5158E51E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {776EECAC-3C50-45EA-847D-0EBE5158E51E}.Debug|Any CPU.Build.0 = Debug|Any CPU {776EECAC-3C50-45EA-847D-0EBE5158E51E}.Release|Any CPU.ActiveCfg = Release|Any CPU {776EECAC-3C50-45EA-847D-0EBE5158E51E}.Release|Any CPU.Build.0 = Release|Any CPU {A420C4E3-3A2D-4369-88EB-77E4DB1D0219}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {A420C4E3-3A2D-4369-88EB-77E4DB1D0219}.Debug|Any CPU.Build.0 = Debug|Any CPU {A420C4E3-3A2D-4369-88EB-77E4DB1D0219}.Release|Any CPU.ActiveCfg = Release|Any CPU {A420C4E3-3A2D-4369-88EB-77E4DB1D0219}.Release|Any CPU.Build.0 = Release|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Debug|Any CPU.Build.0 = Debug|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.ActiveCfg = Release|Any CPU {3C2D8E01-5580-426A-BDD9-EC59CD98E618}.Release|Any CPU.Build.0 = Release|Any CPU {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Debug|Any CPU.Build.0 = Debug|Any CPU {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Release|Any CPU.ActiveCfg = Release|Any CPU {D7A37F73-BBA3-4DAE-9648-1A753A86F968}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution {776EECAC-3C50-45EA-847D-0EBE5158E51E} = {BD94A77E-34B1-478E-B921-E87A5F71B574} {A420C4E3-3A2D-4369-88EB-77E4DB1D0219} = {91E482DE-E1E7-4CE1-9511-C0AF07F3648A} {3C2D8E01-5580-426A-BDD9-EC59CD98E618} = {B03B3086-D197-4B32-9AE2-8536C345EA2D} {D7A37F73-BBA3-4DAE-9648-1A753A86F968} = {B03B3086-D197-4B32-9AE2-8536C345EA2D} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {EB6672D6-318E-493E-8B60-77F5A7A90E66} EndGlobalSection EndGlobal ================================================ FILE: serilog-expressions.sln.DotSettings ================================================  CI True True True True True True True True True True True True True ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/AccessorExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An accessor retrieves a property from a (structured) object. For example, in the expression /// Headers.ContentType the . operator forms an accessor expression that /// retrieves the ContentType property from the Headers object. /// /// Note that the AST type can represent accessors that cannot be validly written using /// . notation. In these cases, if the accessor is formatted back out as an expression, /// notation will be used. class AccessorExpression : Expression { public AccessorExpression(Expression receiver, string memberName) { MemberName = memberName ?? throw new ArgumentNullException(nameof(memberName)); Receiver = receiver; } public string MemberName { get; } public Expression Receiver { get; } public override string ToString() { if (SerilogExpression.IsValidIdentifier(MemberName)) return Receiver + "." + MemberName; return $"{Receiver}['{SerilogExpression.EscapeStringContent(MemberName)}']"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/AmbientNameExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An ambient name is generally a property name or built-in that appears standalone in an expression. For example, /// in Headers.ContentType, Headers is an ambient name that produces an /// . Built-ins like @Level are also parsed as ambient names. /// class AmbientNameExpression : Expression { readonly bool _requiresEscape; public AmbientNameExpression(string name, bool isBuiltIn) { PropertyName = name ?? throw new ArgumentNullException(nameof(name)); IsBuiltIn = isBuiltIn; _requiresEscape = !SerilogExpression.IsValidIdentifier(name); } public string PropertyName { get; } public bool IsBuiltIn { get; } public override string ToString() { if (_requiresEscape) return $"@Properties['{SerilogExpression.EscapeStringContent(PropertyName)}']"; return (IsBuiltIn ? "@" : "") + PropertyName; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/ArrayExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An array expression constructs an array from a list of elements. For example, [1, 2, 3] is an /// array expression. The items in an array expression can be literal values or expressions, like in the /// above example, or they can be spread expressions that describe zero or more elements to include in the /// list. Whether included via regular elements or spread expressions, undefined values are ignored and won't /// appear in the resulting array value. /// class ArrayExpression : Expression { public ArrayExpression(Element[] elements) { Elements = elements ?? throw new ArgumentNullException(nameof(elements)); } public Element[] Elements { get; } public override string ToString() { return "[" + string.Join(", ", Elements.Select(o => o.ToString())) + "]"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/CallExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// A is a function call made up of the function name, parenthesised argument /// list, and optional postfix ci modifier. For example, Substring(RequestPath, 0, 5). /// class CallExpression : Expression { public CallExpression(bool ignoreCase, string operatorName, params Expression[] operands) { IgnoreCase = ignoreCase; OperatorName = operatorName ?? throw new ArgumentNullException(nameof(operatorName)); Operands = operands ?? throw new ArgumentNullException(nameof(operands)); } public bool IgnoreCase { get; } public string OperatorName { get; } public Expression[] Operands { get; } public override string ToString() { return OperatorName + "(" + string.Join(", ", Operands.Select(o => o.ToString())) + ")" + (IgnoreCase ? " ci" : ""); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/ConstantExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Globalization; using Serilog.Events; namespace Serilog.Expressions.Ast; /// /// A constant such as 'hello', true, null, or 123.45. /// class ConstantExpression : Expression { public ConstantExpression(LogEventPropertyValue constant) { Constant = constant ?? throw new ArgumentNullException(nameof(constant)); } public LogEventPropertyValue Constant { get; } public override string ToString() { if (Constant is ScalarValue sv) { return sv.Value switch { string s => "'" + s.Replace("'", "''") + "'", true => "true", false => "false", IFormattable formattable => formattable.ToString(null, CultureInfo.InvariantCulture), _ => (sv.Value ?? "null").ToString() ?? "" }; } return Constant.ToString(); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/Element.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An element in an . /// abstract class Element; ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/Expression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An AST node. /// abstract class Expression { /// /// The representation of an is not /// guaranteed to be syntactically valid: this is provided for debugging purposes only. /// /// A textual representation of the expression. public abstract override string ToString(); } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/IndexOfMatchExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Text.RegularExpressions; namespace Serilog.Expressions.Ast; /// /// A non-syntax expression tree node used when compiling the , /// , and SQL-style like expressions. /// class IndexOfMatchExpression : Expression { public Expression Corpus { get; } public Regex Regex { get; } public IndexOfMatchExpression(Expression corpus, Regex regex) { Corpus = corpus ?? throw new ArgumentNullException(nameof(corpus)); Regex = regex ?? throw new ArgumentNullException(nameof(regex)); } public override string ToString() { return $"_Internal_IndexOfMatch({Corpus}, '{Regex.ToString().Replace("'", "''")}')"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/IndexerExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An retrieves a property from an object, by name, or an item from an array /// by zero-based numeric index. For example, Headers['Content-Type'] and Items[2] are /// parsed as indexer expressions. /// class IndexerExpression : Expression { public Expression Receiver { get; } public Expression Index { get; } public IndexerExpression(Expression receiver, Expression index) { Receiver = receiver; Index = index; } public override string ToString() { return $"{Receiver}[{Index}]"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/IndexerWildcard.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// Describes the wildcard in a . /// enum IndexerWildcard { Undefined, Any, All } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/IndexerWildcardExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An indexer wildcard is a placeholder in a property path expression. For example, /// in Headers[?] = 'test', the question-mark token is a wildcard that means "any property of /// the Headers object". The other wildcard indexer is the asterisk, meaning "all". /// class IndexerWildcardExpression : Expression { public IndexerWildcardExpression(IndexerWildcard wildcard) { Wildcard = wildcard; } public IndexerWildcard Wildcard { get; } public override string ToString() { switch (Wildcard) { case IndexerWildcard.Any: return "?"; case IndexerWildcard.All: return "*"; default: throw new NotSupportedException("Unrecognized wildcard " + Wildcard); } } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/ItemElement.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// A single item in an . /// class ItemElement : Element { public Expression Value { get; } public ItemElement(Expression value) { Value = value; } public override string ToString() { return Value.ToString(); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/LambdaExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// A non-syntax expression tree node used in the compilation of . Only /// very limited support for lambda expressions is currently present. /// class LambdaExpression : Expression { public LambdaExpression(ParameterExpression[] parameters, Expression body) { Parameters = parameters; Body = body; } public ParameterExpression[] Parameters { get; } public Expression Body { get; } public override string ToString() { return "|" + string.Join(", ", Parameters.Select(p => p.ToString())) + "| {" + Body + "}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/LocalNameExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// Represents the iteration variable in template #each directives. /// class LocalNameExpression : Expression { public LocalNameExpression(string name) { Name = name ?? throw new ArgumentNullException(nameof(name)); } public string Name { get; } public override string ToString() { // No unambiguous syntax for this right now, `$` will do to make these stand out when debugging, // but the result won't round-trip parse. return $"${Name}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/Member.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// A member in an . /// abstract class Member; ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/ObjectExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// Constructs an object given a list of members. Members can be name: value pairs, or spread /// expressions that include members from another object. Where names conflict, the rightmost appearance of /// a name wins. Members that evaluate to an undefined value do not appear in the resulting object. /// class ObjectExpression : Expression { public ObjectExpression(Member[] members) { Members = members; } public Member[] Members { get; } public override string ToString() { return "{" + string.Join(", ", Members.Select(m => m.ToString())) + "}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/ParameterExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// Non-syntax expression type used to represent parameters in bodies. /// class ParameterExpression : Expression { public ParameterExpression(string parameterName) { ParameterName = parameterName; } public string ParameterName { get; } public override string ToString() { return "$$" + ParameterName; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/PropertyMember.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; namespace Serilog.Expressions.Ast; /// /// An member comprising an optionally-quoted name and a value, for example /// the object {Username: 'alice'} includes a single with name /// Username and value 'alice'. /// class PropertyMember : Member { public string Name { get; } public Expression Value { get; } public PropertyMember(string name, Expression value) { Name = name; Value = value; } public override string ToString() { return $"{new ScalarValue(Name)}: {Value}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/SpreadElement.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An element in an that describes zero or more items to include in the array. /// Spread elements are written with two dots preceding an expression that evaluates to an array of elements to /// insert into the result array at the position of the spread element, for example, in [1, 2, ..Others], /// the ..Others expression is a spread element. If the value of the array in the spread is /// undefined, no items will be added to the list. /// class SpreadElement : Element { public Expression Content { get; } public SpreadElement(Expression content) { Content = content; } public override string ToString() { return $"..{Content}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Ast/SpreadMember.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Ast; /// /// An member that designates another object from which to copy members into the /// current object. Spread member expressions comprise two dots preceding an expression that is expected to /// evaluate to an object. /// class SpreadMember : Member { public Expression Content { get; } public SpreadMember(Expression content) { Content = content; } public override string ToString() { return $"..{Content}"; } } ================================================ FILE: src/Serilog.Expressions/Expressions/BuiltInProperty.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions; // See https://github.com/serilog/serilog-formatting-compact#reified-properties static class BuiltInProperty { public const string Exception = "x"; public const string Level = "l"; public const string Timestamp = "t"; public const string Message = "m"; public const string MessageTemplate = "mt"; public const string Properties = "p"; public const string Renderings = "r"; public const string EventId = "i"; public const string TraceId = "tr"; public const string SpanId = "sp"; } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Arrays/ConstantArrayEvaluator.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Arrays; class ConstantArrayEvaluator : IdentityTransformer { static readonly ConstantArrayEvaluator Instance = new(); public static Expression Rewrite(Expression expression) { return Instance.Transform(expression); } protected override Expression Transform(ArrayExpression ax) { // This should probably go depth-first. if (ax.Elements.All(el => el is ItemElement item && item.Value is ConstantExpression)) { return new ConstantExpression( new SequenceValue(ax.Elements .Cast() .Select(item => ((ConstantExpression)item.Value).Constant))); } return base.Transform(ax); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/DefaultFunctionNameResolver.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Runtime; namespace Serilog.Expressions.Compilation; static class DefaultFunctionNameResolver { public static NameResolver Build(NameResolver? additionalNameResolver) { var defaultResolver = new StaticMemberNameResolver(typeof(RuntimeOperators)); return additionalNameResolver == null ? defaultResolver : new OrderedNameResolver(new[] {defaultResolver, additionalNameResolver }); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Arrays; using Serilog.Expressions.Compilation.Linq; using Serilog.Expressions.Compilation.Properties; using Serilog.Expressions.Compilation.Text; using Serilog.Expressions.Compilation.Variadics; using Serilog.Expressions.Compilation.Wildcards; namespace Serilog.Expressions.Compilation; static class ExpressionCompiler { public static Expression Translate(Expression expression) { var actual = expression; actual = VariadicCallRewriter.Rewrite(actual); actual = TextMatchingTransformer.Rewrite(actual); actual = LikeSyntaxTransformer.Rewrite(actual); actual = PropertiesObjectAccessorTransformer.Rewrite(actual); actual = ConstantArrayEvaluator.Rewrite(actual); actual = WildcardComprehensionTransformer.Rewrite(actual); return actual; } public static Evaluatable Compile(Expression expression, IFormatProvider? formatProvider, NameResolver nameResolver) { var actual = Translate(expression); return LinqExpressionCompiler.Compile(actual, formatProvider, nameResolver); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/ExpressionValidationException.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Compilation; class ExpressionValidationException : ArgumentException { public ExpressionValidationException(string message) : base(message) { } public ExpressionValidationException(string message, Exception innerException) : base(message, innerException) { } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ReSharper disable ForCanBeConvertedToForeach namespace Serilog.Expressions.Compilation.Linq; /// /// Hash functions for message templates. See . /// public static class EventIdHash { /// /// Compute a 32-bit hash of the provided . The /// resulting hash value can be uses as an event id in lieu of transmitting the /// full template string. /// /// A message template. /// A 32-bit hash of the template. [CLSCompliant(false)] public static uint Compute(string messageTemplate) { if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate)); // Jenkins one-at-a-time https://en.wikipedia.org/wiki/Jenkins_hash_function unchecked { uint hash = 0; for (var i = 0; i < messageTemplate.Length; ++i) { hash += messageTemplate[i]; hash += hash << 10; hash ^= hash >> 6; } hash += hash << 3; hash ^= hash >> 11; hash += hash << 15; return hash; } } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Linq/ExpressionConstantMapper.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Linq.Expressions; using Serilog.Events; namespace Serilog.Expressions.Compilation.Linq; class ExpressionConstantMapper : ExpressionVisitor { readonly IDictionary _mapping; public ExpressionConstantMapper(IDictionary mapping) { _mapping = mapping; } protected override Expression VisitConstant(ConstantExpression node) { if (node.Value is ScalarValue { Value: {} sv } && _mapping.TryGetValue(sv, out var substitute)) return substitute; return base.VisitConstant(node); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections.Specialized; using System.Text.RegularExpressions; using Serilog.Events; using Serilog.Expressions.Runtime; using Serilog.Parsing; using Serilog.Templates.Compilation; // ReSharper disable ParameterTypeCanBeEnumerable.Global namespace Serilog.Expressions.Compilation.Linq; static class Intrinsics { static readonly LogEventPropertyValue NegativeOne = new ScalarValue(-1); static readonly LogEventPropertyValue Tombstone = new ScalarValue("😬 (if you see this you have found a bug.)"); public static List CollectSequenceElements(LogEventPropertyValue?[] elements) { return elements.ToList(); } public static List ExtendSequenceValueWithItem(List elements, LogEventPropertyValue? element) { // Mutates the list; returned so we can nest calls instead of emitting a block. if (element != null) elements.Add(element); return elements; } public static List ExtendSequenceValueWithSpread(List elements, LogEventPropertyValue? content) { if (content is SequenceValue sequence) foreach (var element in sequence.Elements) elements.Add(element); return elements; } public static LogEventPropertyValue ConstructSequenceValue(List elements) { if (elements.Any(el => el == null)) return new SequenceValue(elements.Where(el => el != null)!); return new SequenceValue(elements!); } public static List CollectStructureProperties(string[] names, LogEventPropertyValue?[] values) { var properties = new List(); for (var i = 0; i < names.Length; ++i) { var name = names[i]; var value = values[i]; properties.Add(new(name, value ?? Tombstone)); } return properties; } public static LogEventPropertyValue ConstructStructureValue(List properties) { if (properties.Any(p => p == null || p.Value == Tombstone)) return new StructureValue(properties.Where(p => p != null && p.Value != Tombstone)); return new StructureValue(properties); } public static List ExtendStructureValueWithSpread( List properties, LogEventPropertyValue? content) { if (content is StructureValue structure) { foreach (var property in structure.Properties) if (property != null) properties.Add(property); } return properties; } public static List ExtendStructureValueWithProperty( List properties, string name, LogEventPropertyValue? value) { // Mutates the list; returned so we can nest calls instead of emitting a block. properties.Add(new(name, value ?? Tombstone)); return properties; } public static LogEventPropertyValue CompleteStructureValue(List properties) { var result = new OrderedDictionary(); foreach (var property in properties) { if (result.Contains(property.Name)) result.Remove(property.Name); if (property.Value != Tombstone) result.Add(property.Name, new LogEventProperty(property.Name, property.Value)); } return new StructureValue(result.Values.Cast().ToList()); } public static bool CoerceToScalarBoolean(LogEventPropertyValue value) { if (value is ScalarValue sv && sv.Value is bool b) return b; return false; } public static LogEventPropertyValue? IndexOfMatch(LogEventPropertyValue value, Regex regex) { if (value is ScalarValue scalar && scalar.Value is string s) { var m = regex.Match(s); if (m.Success) return new ScalarValue(m.Index); return NegativeOne; } return null; } public static LogEventPropertyValue? GetPropertyValue(EvaluationContext ctx, string propertyName) { if (!ctx.LogEvent.Properties.TryGetValue(propertyName, out var value)) return null; return value; } public static LogEventPropertyValue? GetLocalValue(EvaluationContext ctx, string localName) { if (!Locals.TryGetValue(ctx.Locals, localName, out var value)) return null; return value; } public static LogEventPropertyValue? TryGetStructurePropertyValue(StringComparison sc, LogEventPropertyValue maybeStructure, string name) { if (maybeStructure is StructureValue sv) { foreach (var prop in sv.Properties) { if (prop.Name.Equals(name, sc)) { return prop.Value; } } } return null; } // Use of `CompiledMessageToken` is a layering violation here, but we want to ensure the formatting implementations // line up exactly. Some refactoring here might be worthwhile, though with an eye on indirection costs. public static string RenderMessage(CompiledMessageToken formatter, EvaluationContext ctx) { var sw = new StringWriter(); formatter.Evaluate(ctx, sw); return sw.ToString(); } public static LogEventPropertyValue? GetRenderings(LogEvent logEvent, IFormatProvider? formatProvider) { List? elements = null; foreach (var token in logEvent.MessageTemplate.Tokens) { if (token is PropertyToken {Format: { }} pt) { elements ??= new(); var space = new StringWriter(); pt.Render(logEvent.Properties, space, formatProvider); elements.Add(new ScalarValue(space.ToString())); } } return elements == null ? null : new SequenceValue(elements); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Linq.Expressions; using System.Reflection; using System.Runtime.InteropServices; using System.Text; using Serilog.Debugging; using Serilog.Events; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; using Serilog.Templates.Compilation; using Serilog.Templates.Themes; using ConstantExpression = Serilog.Expressions.Ast.ConstantExpression; using Expression = Serilog.Expressions.Ast.Expression; using ParameterExpression = System.Linq.Expressions.ParameterExpression; using LX = System.Linq.Expressions.Expression; using ExpressionBody = System.Linq.Expressions.Expression; // ReSharper disable UseIndexFromEndExpression namespace Serilog.Expressions.Compilation.Linq; class LinqExpressionCompiler : SerilogExpressionTransformer { readonly NameResolver _nameResolver; readonly IFormatProvider? _formatProvider; static readonly MethodInfo CollectSequenceElementsMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.CollectSequenceElements), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ExtendSequenceValueWithSpreadMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ExtendSequenceValueWithSpread), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ExtendSequenceValueWithItemMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ExtendSequenceValueWithItem), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ConstructSequenceValueMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ConstructSequenceValue), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo CollectStructurePropertiesMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.CollectStructureProperties), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ConstructStructureValueMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ConstructStructureValue), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo CompleteStructureValueMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.CompleteStructureValue), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ExtendStructureValueWithSpreadMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ExtendStructureValueWithSpread), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo ExtendStructureValueWithPropertyMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.ExtendStructureValueWithProperty), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo CoerceToScalarBooleanMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.CoerceToScalarBoolean), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo IndexOfMatchMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.IndexOfMatch), BindingFlags.Static | BindingFlags.Public)!; static readonly MethodInfo TryGetStructurePropertyValueMethod = typeof(Intrinsics) .GetMethod(nameof(Intrinsics.TryGetStructurePropertyValue), BindingFlags.Static | BindingFlags.Public)!; static readonly PropertyInfo EvaluationContextLogEventProperty = typeof(EvaluationContext) .GetProperty(nameof(EvaluationContext.LogEvent), BindingFlags.Instance | BindingFlags.Public)!; ParameterExpression Context { get; } = LX.Variable(typeof(EvaluationContext), "ctx"); LinqExpressionCompiler(IFormatProvider? formatProvider, NameResolver nameResolver) { _nameResolver = nameResolver; _formatProvider = formatProvider; } public static Evaluatable Compile(Expression expression, IFormatProvider? formatProvider, NameResolver nameResolver) { if (expression == null) throw new ArgumentNullException(nameof(expression)); var compiler = new LinqExpressionCompiler(formatProvider, nameResolver); var body = compiler.Transform(expression); return LX.Lambda(body, compiler.Context).Compile(); } ExpressionBody Splice(Expression lambda) { return ParameterReplacementVisitor.ReplaceParameters(lambda, Context); } protected override ExpressionBody Transform(CallExpression call) { if (!_nameResolver.TryResolveFunctionName(call.OperatorName, out var m)) throw new ExpressionValidationException($"The function name `{call.OperatorName}` was not recognized."); var methodParameters = m.GetParameters() .Select(info => (pi: info, optional: info.GetCustomAttribute() != null)) .ToList(); // Log warning for CI modifier usage on functions that don't support it // Note: We log a warning rather than throwing to maintain backward compatibility // Previously, invalid CI usage was silently ignored if (call.IgnoreCase) { var supportsStringComparison = methodParameters.Any(p => p.pi.ParameterType == typeof(StringComparison)); if (!supportsStringComparison) { SelfLog.WriteLine($"The function `{call.OperatorName}` does not support case-insensitive operation; the 'ci' modifier will be ignored."); } } var allowedParameters = methodParameters.Where(info => info.pi.ParameterType == typeof(LogEventPropertyValue)).ToList(); var requiredParameterCount = allowedParameters.Count(info => !info.optional); if (call.Operands.Length < requiredParameterCount || call.Operands.Length > allowedParameters.Count) { var requirements = DescribeRequirements(allowedParameters.Select(info => (info.pi.Name!, info.optional)).ToList()); throw new ArgumentException($"The function `{call.OperatorName}` {requirements}."); } var operands = new Queue(call.Operands.Select(Transform)); // `and` and `or` short-circuit to save execution time; unlike the earlier Serilog.Filters.Expressions, nothing else does. if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpAnd)) return CompileLogical(LX.AndAlso, operands.Dequeue(), operands.Dequeue()); if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpOr)) return CompileLogical(LX.OrElse, operands.Dequeue(), operands.Dequeue()); var boundParameters = new List(methodParameters.Count); foreach (var (pi, optional) in methodParameters) { if (pi.ParameterType == typeof(LogEventPropertyValue)) { boundParameters.Add(operands.Count > 0 ? operands.Dequeue() : LX.Constant(null, typeof(LogEventPropertyValue))); } else if (pi.ParameterType == typeof(StringComparison)) boundParameters.Add(LX.Constant(call.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal)); else if (pi.ParameterType == typeof(IFormatProvider)) boundParameters.Add(LX.Constant(_formatProvider, typeof(IFormatProvider))); else if (pi.ParameterType == typeof(LogEvent)) boundParameters.Add(LX.Property(Context, EvaluationContextLogEventProperty)); else if (_nameResolver.TryBindFunctionParameter(pi, out var binding)) boundParameters.Add(LX.Constant(binding, pi.ParameterType)); else if (optional) boundParameters.Add(LX.Constant( pi.GetCustomAttribute()?.Value, pi.ParameterType)); else throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has argument `{pi.Name}` which could not be bound."); } return LX.Call(m, boundParameters); } static string DescribeRequirements(IReadOnlyList<(string name, bool optional)> parameters) { static string DescribeArgument((string name, bool optional) p) => $"`{p.name}`" + (p.optional ? " (optional)" : ""); if (parameters.Count == 0) return "accepts no arguments"; if (parameters.Count == 1) return $"accepts one argument, {DescribeArgument(parameters[0])}"; if (parameters.Count == 2) return $"accepts two arguments, {DescribeArgument(parameters[0])} and {DescribeArgument(parameters[1])}"; var result = new StringBuilder("accepts arguments"); for (var i = 0; i < parameters.Count - 1; ++i) result.Append($" {DescribeArgument(parameters[i])},"); result.Append($" and {DescribeArgument(parameters[parameters.Count - 1])}"); return result.ToString(); } static ExpressionBody CompileLogical(Func apply, ExpressionBody lhs, ExpressionBody rhs) { return LX.Convert( LX.New( typeof(ScalarValue).GetConstructor([typeof(object)])!, LX.Convert(apply( LX.Call(CoerceToScalarBooleanMethod, lhs), LX.Call(CoerceToScalarBooleanMethod, rhs)), typeof(object))), typeof(LogEventPropertyValue)); } protected override ExpressionBody Transform(AccessorExpression spx) { var receiver = Transform(spx.Receiver); return LX.Call(TryGetStructurePropertyValueMethod, LX.Constant(StringComparison.Ordinal), receiver, LX.Constant(spx.MemberName, typeof(string))); } protected override ExpressionBody Transform(ConstantExpression cx) { return LX.Constant(cx.Constant); } protected override ExpressionBody Transform(AmbientNameExpression px) { if (px.IsBuiltIn) { var formatter = new CompiledMessageToken(_formatProvider, null, TemplateTheme.None); var formatProvider = _formatProvider; return px.PropertyName switch { BuiltInProperty.Level => Splice(context => new ScalarValue(context.LogEvent.Level)), BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(formatter, context))), BuiltInProperty.Exception => Splice(context => context.LogEvent.Exception == null ? null : new ScalarValue(context.LogEvent.Exception)), BuiltInProperty.TraceId => Splice(context => context.LogEvent.TraceId == null ? null : new ScalarValue(context.LogEvent.TraceId.Value)), BuiltInProperty.SpanId => Splice(context => context.LogEvent.SpanId == null ? null : new ScalarValue(context.LogEvent.SpanId.Value)), BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.LogEvent.Timestamp)), BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.LogEvent.MessageTemplate.Text)), BuiltInProperty.Properties => Splice(context => new StructureValue(context.LogEvent.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)), null)), BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context.LogEvent, formatProvider)), BuiltInProperty.EventId => Splice(context => new ScalarValue(EventIdHash.Compute(context.LogEvent.MessageTemplate.Text))), var alias when _nameResolver.TryResolveBuiltInPropertyName(alias, out var target) => Transform(new AmbientNameExpression(target, true)), _ => LX.Constant(null, typeof(LogEventPropertyValue)) }; } // Don't close over the AST node. var propertyName = px.PropertyName; return Splice(context => Intrinsics.GetPropertyValue(context, propertyName)); } protected override ExpressionBody Transform(LocalNameExpression nlx) { // Don't close over the AST node. var name = nlx.Name; return Splice(context => Intrinsics.GetLocalValue(context, name)); } protected override ExpressionBody Transform(Ast.LambdaExpression lmx) { var parameters = lmx.Parameters.Select(px => Tuple.Create(px, LX.Parameter(typeof(LogEventPropertyValue), px.ParameterName))).ToList(); var paramSwitcher = new ExpressionConstantMapper(parameters.ToDictionary(px => (object)px.Item1, px => (System.Linq.Expressions.Expression)px.Item2)); var rewritten = paramSwitcher.Visit(Transform(lmx.Body)); Type delegateType; if (lmx.Parameters.Length == 1) delegateType = typeof(Func); else if (lmx.Parameters.Length == 2) delegateType = typeof(Func); else throw new NotSupportedException("Unsupported lambda signature."); var lambda = LX.Lambda(delegateType, rewritten!, parameters.Select(px => px.Item2).ToArray()); // Unfortunately, right now, functions need to be threaded through in constant scalar values :-D return LX.New(typeof(ScalarValue).GetConstructor([typeof(object)])!, LX.Convert(lambda, typeof(object))); } protected override ExpressionBody Transform(Ast.ParameterExpression prx) { // Will be within a lambda, which will subsequently sub-in the actual value. // The `prx` placeholder needs to be wrapped in a `ScalarValue` so that eager // typechecking doesn't fail before we've substituted the real value in. return LX.Constant(new ScalarValue(prx), typeof(LogEventPropertyValue)); } protected override ExpressionBody Transform(IndexerWildcardExpression wx) { return LX.Constant(null, typeof(LogEventPropertyValue)); } protected override ExpressionBody Transform(ArrayExpression ax) { var elements = new List(ax.Elements.Length); var i = 0; for (; i < ax.Elements.Length; ++i) { var element = ax.Elements[i]; if (element is ItemElement item) elements.Add(Transform(item.Value)); else break; } var arr = LX.NewArrayInit(typeof(LogEventPropertyValue), elements.ToArray()); var collected = LX.Call(CollectSequenceElementsMethod, arr); for (; i < ax.Elements.Length; ++i) { var element = ax.Elements[i]; if (element is ItemElement item) collected = LX.Call(ExtendSequenceValueWithItemMethod, collected, Transform(item.Value)); else { var spread = (SpreadElement) element; collected = LX.Call(ExtendSequenceValueWithSpreadMethod, collected, Transform(spread.Content)); } } return LX.Call(ConstructSequenceValueMethod, collected); } protected override ExpressionBody Transform(ObjectExpression ox) { var names = new List(); var values = new List(); var i = 0; for (; i < ox.Members.Length; ++i) { var member = ox.Members[i]; if (member is PropertyMember property) { if (names.Contains(property.Name)) { var oldPos = names.IndexOf(property.Name); values[oldPos] = Transform(property.Value); } else { names.Add(property.Name); values.Add(Transform(property.Value)); } } else { break; } } var namesConstant = LX.Constant(names.ToArray(), typeof(string[])); var valuesArr = LX.NewArrayInit(typeof(LogEventPropertyValue), values.ToArray()); var properties = LX.Call(CollectStructurePropertiesMethod, namesConstant, valuesArr); if (i == ox.Members.Length) { // No spreads; more efficient than `Complete*` because erasure is not required. return LX.Call(ConstructStructureValueMethod, properties); } for (; i < ox.Members.Length; ++i) { var member = ox.Members[i]; if (member is PropertyMember property) { properties = LX.Call( ExtendStructureValueWithPropertyMethod, properties, LX.Constant(property.Name), Transform(property.Value)); } else { var spread = (SpreadMember) member; properties = LX.Call( ExtendStructureValueWithSpreadMethod, properties, Transform(spread.Content)); } } return LX.Call(CompleteStructureValueMethod, properties); } protected override ExpressionBody Transform(IndexerExpression ix) { return Transform(new CallExpression(false, Operators.OpElementAt, ix.Receiver, ix.Index)); } protected override ExpressionBody Transform(IndexOfMatchExpression mx) { var rx = LX.Constant(mx.Regex); var target = Transform(mx.Corpus); return LX.Call(IndexOfMatchMethod, target, rx); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Linq/ParameterReplacementVisitor.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Linq.Expressions; namespace Serilog.Expressions.Compilation.Linq; class ParameterReplacementVisitor : ExpressionVisitor { readonly ParameterExpression[] _from, _to; public static Expression ReplaceParameters(LambdaExpression lambda, params ParameterExpression[] newParameters) { var v = new ParameterReplacementVisitor(lambda.Parameters.ToArray(), newParameters); return v.Visit(lambda.Body)!; } ParameterReplacementVisitor(ParameterExpression[] from, ParameterExpression[] to) { if (from == null) throw new ArgumentNullException(nameof(from)); if (to == null) throw new ArgumentNullException(nameof(to)); if (from.Length != to.Length) throw new InvalidOperationException("Mismatched parameter lists"); _from = from; _to = to; } protected override Expression VisitParameter(ParameterExpression node) { for (var i = 0; i < _from.Length; i++) { if (node == _from[i]) return _to[i]; } return node; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/OrderedNameResolver.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Serilog.Expressions.Compilation; class OrderedNameResolver : NameResolver { readonly NameResolver[] _orderedResolvers; public OrderedNameResolver(IEnumerable orderedResolvers) { _orderedResolvers = orderedResolvers.ToArray(); } public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { foreach (var resolver in _orderedResolvers) { if (resolver.TryResolveFunctionName(name, out implementation)) return true; } implementation = null; return false; } public override bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue) { foreach (var resolver in _orderedResolvers) { if (resolver.TryBindFunctionParameter(parameter, out boundValue)) return true; } boundValue = null; return false; } public override bool TryResolveBuiltInPropertyName(string alias, [NotNullWhen(true)] out string? target) { foreach (var resolver in _orderedResolvers) { if (resolver.TryResolveBuiltInPropertyName(alias, out target)) return true; } target = null; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Pattern.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using Serilog.Events; using Serilog.Expressions.Ast; namespace Serilog.Expressions.Compilation; static class Pattern { public static bool IsAmbientProperty(Expression expression, string name, bool isBuiltIn) { return expression is AmbientNameExpression px && px.PropertyName == name && px.IsBuiltIn == isBuiltIn; } public static bool IsStringConstant(Expression expression, [MaybeNullWhen(false)] out string value) { if (expression is ConstantExpression cx && cx.Constant is ScalarValue sv && sv.Value is string s) { value = s; return true; } value = null; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Properties/PropertiesObjectAccessorTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Properties; class PropertiesObjectAccessorTransformer : IdentityTransformer { public static Expression Rewrite(Expression actual) { return new PropertiesObjectAccessorTransformer().Transform(actual); } protected override Expression Transform(AccessorExpression ax) { if (!Pattern.IsAmbientProperty(ax.Receiver, BuiltInProperty.Properties, true)) return base.Transform(ax); return new AmbientNameExpression(ax.MemberName, false); } protected override Expression Transform(IndexerExpression ix) { if (!Pattern.IsAmbientProperty(ix.Receiver, BuiltInProperty.Properties, true) || !Pattern.IsStringConstant(ix.Index, out var name)) return base.Transform(ix); return new AmbientNameExpression(name, false); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Text/LikeSyntaxTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Text.RegularExpressions; using Serilog.Debugging; using Serilog.Events; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Text; class LikeSyntaxTransformer: IdentityTransformer { static readonly LikeSyntaxTransformer Instance = new(); public static Expression Rewrite(Expression expression) { return Instance.Transform(expression); } protected override Expression Transform(CallExpression call) { if (call.Operands.Length != 2) return base.Transform(call); if (Operators.SameOperator(call.OperatorName, Operators.IntermediateOpLike)) return TryCompileLikeExpression(call.IgnoreCase, call.Operands[0], call.Operands[1]); if (Operators.SameOperator(call.OperatorName, Operators.IntermediateOpNotLike)) return new CallExpression( false, Operators.RuntimeOpStrictNot, TryCompileLikeExpression(call.IgnoreCase, call.Operands[0], call.Operands[1])); return base.Transform(call); } Expression TryCompileLikeExpression(bool ignoreCase, Expression corpus, Expression like) { if (like is ConstantExpression { Constant: ScalarValue { Value: string s } }) { var regex = LikeToRegex(s); var opts = RegexOptions.Compiled | RegexOptions.ExplicitCapture; if (ignoreCase) opts |= RegexOptions.IgnoreCase; var compiled = new Regex(regex, opts, TimeSpan.FromMilliseconds(100)); var indexof = new IndexOfMatchExpression(Transform(corpus), compiled); return new CallExpression(ignoreCase, Operators.RuntimeOpNotEqual, indexof, new ConstantExpression(new ScalarValue(-1))); } SelfLog.WriteLine($"Serilog.Expressions: `like` requires a constant string argument; found ${like}."); return new CallExpression(false, Operators.OpUndefined); } static string LikeToRegex(string like) { var begin = "^"; var regex = ""; var end = "$"; for (var i = 0; i < like.Length; ++i) { var ch = like[i]; var following = i == like.Length - 1 ? (char?)null : like[i + 1]; if (ch == '%') { if (following == '%') { regex += '%'; ++i; } else { if (i == 0) begin = ""; if (i == like.Length - 1) end = ""; if (i == 0 && i == like.Length - 1) regex += ".*"; if (i != 0 && i != like.Length - 1) regex += "(?:.|\\r|\\n)*"; // ~= RegexOptions.Singleline } } else if (ch == '_') { if (following == '_') { regex += '_'; ++i; } else { regex += '.'; // Newlines aren't considered matches for _ } } else regex += Regex.Escape(ch.ToString()); } return begin + regex + end; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Text/TextMatchingTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Text.RegularExpressions; using Serilog.Debugging; using Serilog.Events; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Text; class TextMatchingTransformer: IdentityTransformer { static readonly TextMatchingTransformer Instance = new(); public static Expression Rewrite(Expression expression) { return Instance.Transform(expression); } protected override Expression Transform(CallExpression call) { if (call.Operands.Length != 2) return base.Transform(call); if (Operators.SameOperator(call.OperatorName, Operators.OpIndexOfMatch)) return TryCompileIndexOfMatch(call.IgnoreCase, call.Operands[0], call.Operands[1]); if (Operators.SameOperator(call.OperatorName, Operators.OpIsMatch)) return new CallExpression( false, Operators.RuntimeOpNotEqual, TryCompileIndexOfMatch(call.IgnoreCase, call.Operands[0], call.Operands[1]), new ConstantExpression(new ScalarValue(-1))); return base.Transform(call); } Expression TryCompileIndexOfMatch(bool ignoreCase, Expression corpus, Expression regex) { if (regex is ConstantExpression { Constant: ScalarValue { Value: string s } }) { try { var opts = RegexOptions.Compiled | RegexOptions.ExplicitCapture; if (ignoreCase) opts |= RegexOptions.IgnoreCase; var compiled = new Regex(s, opts, TimeSpan.FromMilliseconds(100)); return new IndexOfMatchExpression(Transform(corpus), compiled); } catch (ArgumentException ex) { throw new ExpressionValidationException($"Invalid regular expression in IndexOfMatch: {ex.Message}", ex); } } SelfLog.WriteLine($"Serilog.Expressions: `IndexOfMatch()` requires a constant string regular expression argument; found {regex}."); return new CallExpression(false, Operators.OpUndefined); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Transformations/IdentityTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; namespace Serilog.Expressions.Compilation.Transformations; class IdentityTransformer : SerilogExpressionTransformer { bool TryTransform(Expression expr, out Expression result) { result = Transform(expr); return !ReferenceEquals(expr, result); } protected override Expression Transform(CallExpression call) { var any = false; var operands = new List(); foreach (var op in call.Operands) { if (TryTransform(op, out var result)) any = true; operands.Add(result); } if (!any) return call; return new CallExpression(call.IgnoreCase, call.OperatorName, operands.ToArray()); } protected override Expression Transform(ConstantExpression cx) { return cx; } protected override Expression Transform(AmbientNameExpression px) { return px; } protected override Expression Transform(LocalNameExpression nlx) { return nlx; } protected override Expression Transform(AccessorExpression spx) { if (!TryTransform(spx.Receiver, out var recv)) return spx; return new AccessorExpression(recv, spx.MemberName); } protected override Expression Transform(LambdaExpression lmx) { if (!TryTransform(lmx.Body, out var body)) return lmx; // By default we maintain the parameters available in the body return new LambdaExpression(lmx.Parameters, body); } // Only touches uses of the parameters, not decls protected override Expression Transform(ParameterExpression prx) { return prx; } protected override Expression Transform(IndexerWildcardExpression wx) { return wx; } protected override Expression Transform(ArrayExpression ax) { var any = false; var elements = new List(); foreach (var el in ax.Elements) { if (el is ItemElement item) { if (TryTransform(item.Value, out var result)) any = true; elements.Add(new ItemElement(result)); } else { var spread = (SpreadElement) el; if (TryTransform(spread.Content, out var result)) any = true; elements.Add(new SpreadElement(result)); } } if (!any) return ax; return new ArrayExpression(elements.ToArray()); } protected override Expression Transform(ObjectExpression ox) { var any = false; var members = new List(); foreach (var m in ox.Members) { if (m is PropertyMember p) { if (TryTransform(p.Value, out var result)) any = true; members.Add(new PropertyMember(p.Name, result)); } else { var s = (SpreadMember) m; if (TryTransform(s.Content, out var result)) any = true; members.Add(new SpreadMember(result)); } } if (!any) return ox; return new ObjectExpression(members.ToArray()); } protected override Expression Transform(IndexerExpression ix) { var transformedRecv = TryTransform(ix.Receiver, out var recv); if (!TryTransform(ix.Index, out var index) && !transformedRecv) return ix; return new IndexerExpression(recv, index); } protected override Expression Transform(IndexOfMatchExpression mx) { if (!TryTransform(mx.Corpus, out var corpus)) return mx; return new IndexOfMatchExpression(corpus, mx.Regex); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Transformations/NodeReplacer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; namespace Serilog.Expressions.Compilation.Transformations; class NodeReplacer : IdentityTransformer { readonly Expression _source; readonly Expression _dest; public static Expression Replace(Expression expr, Expression source, Expression dest) { var replacer = new NodeReplacer(source, dest); return replacer.Transform(expr); } NodeReplacer(Expression source, Expression dest) { _source = source; _dest = dest; } protected override Expression Transform(Expression x) { if (x == _source) return _dest; return base.Transform(x); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Transformations/SerilogExpressionTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; namespace Serilog.Expressions.Compilation.Transformations; abstract class SerilogExpressionTransformer { protected virtual TResult Transform(Expression expression) { return expression switch { CallExpression call => Transform(call), ConstantExpression constant => Transform(constant), AccessorExpression accessor => Transform(accessor), AmbientNameExpression property => Transform(property), LocalNameExpression local => Transform(local), LambdaExpression lambda => Transform(lambda), ParameterExpression parameter => Transform(parameter), IndexerWildcardExpression wildcard => Transform(wildcard), ArrayExpression array => Transform(array), ObjectExpression obj => Transform(obj), IndexerExpression indexer => Transform(indexer), IndexOfMatchExpression match => Transform(match), null => throw new ArgumentNullException(nameof(expression)), // Non-exhaustive because `InternalsVisibleTo` is applied to the assembly. _ => throw new NotSupportedException($"{expression} is not supported.") }; } protected abstract TResult Transform(CallExpression call); protected abstract TResult Transform(ConstantExpression cx); protected abstract TResult Transform(AmbientNameExpression px); protected abstract TResult Transform(LocalNameExpression nlx); protected abstract TResult Transform(AccessorExpression spx); protected abstract TResult Transform(LambdaExpression lmx); protected abstract TResult Transform(ParameterExpression prx); protected abstract TResult Transform(IndexerWildcardExpression wx); protected abstract TResult Transform(ArrayExpression ax); protected abstract TResult Transform(ObjectExpression ox); protected abstract TResult Transform(IndexerExpression ix); protected abstract TResult Transform(IndexOfMatchExpression mx); } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Variadics/VariadicCallRewriter.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Variadics; // Handles variadic `coalesce()` and `concat()`, as well as optional arguments for other functions. class VariadicCallRewriter : IdentityTransformer { static readonly VariadicCallRewriter Instance = new(); public static Expression Rewrite(Expression expression) { return Instance.Transform(expression); } protected override Expression Transform(CallExpression call) { if (Operators.SameOperator(call.OperatorName, Operators.OpCoalesce) || Operators.SameOperator(call.OperatorName, Operators.OpConcat)) { if (call.Operands.Length == 0) return new CallExpression(false, Operators.OpUndefined); if (call.Operands.Length == 1) return Transform(call.Operands.Single()); if (call.Operands.Length > 2) { var first = Transform(call.Operands.First()); return new CallExpression(call.IgnoreCase, call.OperatorName, first, Transform(new CallExpression(call.IgnoreCase, call.OperatorName, call.Operands.Skip(1).ToArray()))); } } return base.Transform(call); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Wildcards; class WildcardComprehensionTransformer : IdentityTransformer { int _nextParameter; public static Expression Rewrite(Expression root) { var wc = new WildcardComprehensionTransformer(); return wc.Transform(root); } // This matches expression fragments such as `A[?] = 'test'` and // transforms them into `any(A, |p| p = 'test)`. // // As the comparand in such expressions can be complex, e.g. // `Substring(A[?], 0, 4) = 'test')`, the search for `?` and `*` wildcards // is deep, but, it terminates upon reaching any other wildcard-compatible // comparison. Thus `(A[?] = 'test') = true` will result in `any(A, |p| p = 'test') = true` and // not `any(A, |p| (p = 'test') = true)`, which is important because short-circuiting when the first // argument to `any()` is undefined will change the semantics of the resulting expression, otherwise. protected override Expression Transform(CallExpression lx) { if (!Operators.WildcardComparators.Contains(lx.OperatorName)) return base.Transform(lx); IndexerExpression? indexer = null; Expression? wildcardPath = null; var indexerOperand = -1; for (var i = 0; i < lx.Operands.Length; ++i) { indexer = WildcardSearch.FindWildcardIndexer(lx.Operands[i]); if (indexer != null) { indexerOperand = i; wildcardPath = lx.Operands[i]; break; } } if (indexer == null || wildcardPath == null) return base.Transform(lx); // N/A, or invalid var px = new ParameterExpression("p" + _nextParameter++); var nestedComparand = NodeReplacer.Replace(wildcardPath, indexer, px); var coll = indexer.Receiver; var wc = ((IndexerWildcardExpression)indexer.Index).Wildcard; var comparisonArgs = lx.Operands.ToArray(); comparisonArgs[indexerOperand] = nestedComparand; var body = new CallExpression(lx.IgnoreCase, lx.OperatorName, comparisonArgs); var lambda = new LambdaExpression([px], body); var op = Operators.ToRuntimeWildcardOperator(wc); var call = new CallExpression(false, op, coll, lambda); return Transform(call); } // Detects and transforms standalone `A[?]` fragments that are not part of a comparision; these // are effectively Boolean tests. protected override Expression Transform(IndexerExpression ix) { if (ix.Index is not IndexerWildcardExpression wx) return base.Transform(ix); var px = new ParameterExpression("p" + _nextParameter++); var coll = Transform(ix.Receiver); var lambda = new LambdaExpression([px], px); var op = Operators.ToRuntimeWildcardOperator(wx.Wildcard); return new CallExpression(false, op, coll, lambda); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardSearch.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Expressions.Compilation.Wildcards; class WildcardSearch : SerilogExpressionTransformer { static readonly WildcardSearch Instance = new(); public static IndexerExpression? FindWildcardIndexer(Expression fx) { return Instance.Transform(fx); } protected override IndexerExpression? Transform(IndexerExpression ix) { if (ix.Index is IndexerWildcardExpression) return ix; return Transform(ix.Receiver); } protected override IndexerExpression? Transform(ConstantExpression cx) { return null; } protected override IndexerExpression? Transform(AmbientNameExpression px) { return null; } protected override IndexerExpression? Transform(LocalNameExpression nlx) { return null; } protected override IndexerExpression? Transform(AccessorExpression spx) { return Transform(spx.Receiver); } protected override IndexerExpression? Transform(LambdaExpression lmx) { return null; } protected override IndexerExpression? Transform(ParameterExpression prx) { return null; } protected override IndexerExpression? Transform(IndexerWildcardExpression wx) { // Must be within an indexer return null; } protected override IndexerExpression? Transform(ArrayExpression ax) { return null; } protected override IndexerExpression? Transform(CallExpression call) { // If we hit a wildcard-compatible operation, then any wildcards within its operands "belong" to // it and can't be the result of this search. if (Operators.WildcardComparators.Contains(call.OperatorName)) return null; return call.Operands.Select(Transform).FirstOrDefault(e => e != null); } protected override IndexerExpression? Transform(IndexOfMatchExpression mx) { return Transform(mx.Corpus); } protected override IndexerExpression? Transform(ObjectExpression ox) { return null; } } ================================================ FILE: src/Serilog.Expressions/Expressions/CompiledExpression.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; namespace Serilog.Expressions; /// /// A compiled expression evaluated against a . /// /// /// The result of evaluating the expression, represented as a , /// or null if the result is undefined. public delegate LogEventPropertyValue? CompiledExpression(LogEvent logEvent); ================================================ FILE: src/Serilog.Expressions/Expressions/Evaluatable.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; namespace Serilog.Expressions; delegate LogEventPropertyValue? Evaluatable(EvaluationContext ctx); ================================================ FILE: src/Serilog.Expressions/Expressions/EvaluationContext.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions.Runtime; namespace Serilog.Expressions; readonly struct EvaluationContext { public LogEvent LogEvent { get; } public Locals? Locals { get; } public EvaluationContext(LogEvent logEvent, Locals? locals = null) { LogEvent = logEvent ?? throw new ArgumentNullException(nameof(logEvent)); Locals = locals; } } ================================================ FILE: src/Serilog.Expressions/Expressions/ExpressionResult.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions.Runtime; namespace Serilog.Expressions; /// /// Helper functions for working with the results of evaluating expressions. /// public static class ExpressionResult { /// /// Test whether is true. /// /// The value to test, which may be null /// if the result of evaluating an expression was undefined. /// Returns true if and only if the /// is a scalar Boolean with the /// value true. Returns false, otherwise. public static bool IsTrue(LogEventPropertyValue? value) { return Coerce.IsTrue(value); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Helpers.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Text.RegularExpressions; #if NO_CI_STRING_CONTAINS namespace Serilog.Expressions { /// /// Helper methods. /// static class Helpers { /// /// Backport .NET Standard 2.1 additions to maintain .NET Standard 2.0 compatibility. /// Returns a value indicating whether a specified string occurs within this string, using the specified comparison rules. /// /// From; /// https://github.com/dotnet/runtime/issues/22198 /// https://stackoverflow.com/questions/444798/case-insensitive-containsstring/444818#444818 /// /// input string /// The string to seek. /// Specifies the rule to use in the comparison. /// public static bool Contains(this string @this, string value, StringComparison comparisonType) { return @this.IndexOf(value, comparisonType) >= 0; } public static string Replace(this string @this, string oldValue, string newValue, StringComparison comparisonType) { if ("a".Equals("A", comparisonType)) { return Regex.Replace( @this, Regex.Escape(oldValue), newValue.Replace("$", "$$"), RegexOptions.IgnoreCase); } return @this.Replace(oldValue, newValue); } } } #endif ================================================ FILE: src/Serilog.Expressions/Expressions/LoggingFilterSwitch.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Core; using Serilog.Events; using Serilog.Expressions.Runtime; namespace Serilog.Expressions; /// /// A log event filter that can be modified at runtime. /// public class LoggingFilterSwitch : ILogEventFilter { // Reference assignments are atomic. While this class makes // no attempt to synchronize Expression, ToString(), and IsIncluded(), // for any observer, this at least ensures they won't be permanently out-of-sync for // all observers. volatile Tuple? _filter; /// /// Construct a , optionally initialized /// with the . /// /// A filter expression against which log events will be tested. /// Only expressions that evaluate to true are included /// by the filter. A null expression will accept all /// events. public LoggingFilterSwitch(string? expression = null) { Expression = expression; } /// /// A filter expression against which log events will be tested. /// Only expressions that evaluate to true are included /// by the filter. A null expression will accept all /// events. /// public string? Expression { // ReSharper disable once UnusedMember.Global get { var filter = _filter; return filter?.Item1; } set { if (value == null) { _filter = null; } else { _filter = new( value, SerilogExpression.Compile(value)); } } } /// public bool IsEnabled(LogEvent logEvent) { if (logEvent == null) throw new ArgumentNullException(nameof(logEvent)); var filter = _filter; if (filter == null) return true; return Coerce.IsTrue(filter.Item2(logEvent)); } /// public override string ToString() { var filter = _filter; return filter?.Item1 ?? ""; } } ================================================ FILE: src/Serilog.Expressions/Expressions/NameResolver.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Reflection; using Serilog.Events; namespace Serilog.Expressions; /// /// Looks up the implementations of functions that appear in expressions. /// public abstract class NameResolver { /// /// Match a function name to a method that implements it. /// /// The function name as it appears in the expression source. Names are not case-sensitive. /// A implementing the function. /// True if the name could be resolved; otherwise, false. /// The method implementing a function should be static, return , /// and accept parameters of type . If the ci modifier is supported, /// a should be included in the argument list. If the function is culture-specific, /// an should be included in the argument list. public virtual bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { implementation = null; return false; } /// /// Provide a value for a non- parameter. This allows user-defined state to /// be threaded through user-defined functions. /// /// A parameter of a method implementing a user-defined function, which could not be /// bound to any of the standard runtime-provided values or operands. /// The value that should be provided when the method is called. /// True if the parameter could be bound; otherwise, false. public virtual bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue) { boundValue = null; return false; } /// /// Map an unrecognized built-in property name to a recognised one. /// /// Intended predominantly to support migration from Serilog.Filters.Expressions. /// The unrecognized name, for example, "Message"; the @ prefix is /// not included. /// If the name could be resolved, the target property name, without any prefix; for /// example, "m". /// True if the alias was mapped to a built-in property; otherwise, false. public virtual bool TryResolveBuiltInPropertyName(string alias, [NotNullWhen(true)] out string? target) { target = null; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Operators.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; // ReSharper disable InconsistentNaming, MemberCanBePrivate.Global namespace Serilog.Expressions; static class Operators { static StringComparer OperatorComparer { get; } = StringComparer.OrdinalIgnoreCase; // Core filter language // Op* means usable in expressions _and_ runtime executable. // RuntimeOp* means runtime only. public const string OpCoalesce = "Coalesce"; public const string OpConcat = "Concat"; public const string OpContains = "Contains"; public const string OpElementAt = "ElementAt"; public const string OpEndsWith = "EndsWith"; public const string OpIndexOf = "IndexOf"; public const string OpIndexOfMatch = "IndexOfMatch"; public const string OpInspect = "Inspect"; public const string OpIsMatch = "IsMatch"; public const string OpIsDefined = "IsDefined"; public const string OpLastIndexOf = "LastIndexOf"; public const string OpLength = "Length"; public const string OpNest = "Nest"; public const string OpNow = "Now"; public const string OpReplace = "Replace"; public const string OpRound = "Round"; public const string OpStartsWith = "StartsWith"; public const string OpSubstring = "Substring"; public const string OpTagOf = "TagOf"; public const string OpToString = "ToString"; public const string OpTypeOf = "TypeOf"; public const string OpUndefined = "Undefined"; public const string OpUtcDateTime = "UtcDateTime"; public const string IntermediateOpLike = "_Internal_Like"; public const string IntermediateOpNotLike = "_Internal_NotLike"; public const string RuntimeOpAdd = "_Internal_Add"; public const string RuntimeOpSubtract = "_Internal_Subtract"; public const string RuntimeOpMultiply = "_Internal_Multiply"; public const string RuntimeOpDivide = "_Internal_Divide"; public const string RuntimeOpModulo = "_Internal_Modulo"; public const string RuntimeOpPower = "_Internal_Power"; public const string RuntimeOpAnd = "_Internal_And"; public const string RuntimeOpOr = "_Internal_Or"; public const string RuntimeOpLessThanOrEqual = "_Internal_LessThanOrEqual"; public const string RuntimeOpLessThan = "_Internal_LessThan"; public const string RuntimeOpGreaterThan = "_Internal_GreaterThan"; public const string RuntimeOpGreaterThanOrEqual = "_Internal_GreaterThanOrEqual"; public const string RuntimeOpEqual = "_Internal_Equal"; public const string RuntimeOpNotEqual = "_Internal_NotEqual"; public const string RuntimeOpNegate = "_Internal_Negate"; public const string RuntimeOpNot = "_Internal_Not"; public const string RuntimeOpAny = "_Internal_Any"; public const string RuntimeOpAll = "_Internal_All"; public const string RuntimeOpIsNull = "_Internal_IsNull"; public const string RuntimeOpIsNotNull = "_Internal_IsNotNull"; public const string RuntimeOpIn = "_Internal_In"; public const string RuntimeOpNotIn = "_Internal_NotIn"; public const string RuntimeOpStrictNot = "_Internal_StrictNot"; public const string RuntimeOpIfThenElse = "_Internal_IfThenElse"; public static readonly HashSet WildcardComparators = new(OperatorComparer) { OpContains, OpStartsWith, OpEndsWith, RuntimeOpNotEqual, RuntimeOpEqual, RuntimeOpLessThan, RuntimeOpLessThanOrEqual, RuntimeOpGreaterThan, RuntimeOpGreaterThanOrEqual, IntermediateOpLike, IntermediateOpNotLike, RuntimeOpIn, RuntimeOpNotIn, OpIsMatch, OpIsDefined, RuntimeOpIsNull, RuntimeOpIsNotNull, RuntimeOpAny, RuntimeOpAll }; public static bool SameOperator(string op1, string op2) { if (op1 == null) throw new ArgumentNullException(nameof(op1)); if (op2 == null) throw new ArgumentNullException(nameof(op2)); return OperatorComparer.Equals(op1, op2); } public static string ToRuntimeWildcardOperator(IndexerWildcard wildcard) { return wildcard switch { IndexerWildcard.All => RuntimeOpAll, IndexerWildcard.Any => RuntimeOpAny, _ => throw new ArgumentException("Unsupported wildcard.") }; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/Combinators.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; namespace Serilog.Expressions.Parsing; static class Combinators { public static TokenListParser ChainModified( TokenListParser @operator, TokenListParser operand, TokenListParser modify, Func apply) { if (@operator == null) throw new ArgumentNullException(nameof (@operator)); if (operand == null) throw new ArgumentNullException(nameof (operand)); if (modify == null) throw new ArgumentNullException(nameof(modify)); if (apply == null) throw new ArgumentNullException(nameof (apply)); return input => { var parseResult = operand(input); if (!parseResult.HasValue ) return parseResult; var result = parseResult.Value; var remainder = parseResult.Remainder; var operatorResult = @operator(remainder); while (operatorResult.HasValue || operatorResult.SubTokenErrorPosition.HasValue || remainder != operatorResult.Remainder) { // If operator read any input, but failed to read complete input, we return error if (!operatorResult.HasValue) return TokenListParserResult.CastEmpty(operatorResult); var operandResult = operand(operatorResult.Remainder); remainder = operandResult.Remainder; if (!operandResult.HasValue) return operandResult; var modifierResult = modify(remainder); remainder = modifierResult.Remainder; if (!modifierResult.HasValue) return TokenListParserResult.CastEmpty(modifierResult); result = apply(operatorResult.Value, result, operandResult.Value, modifierResult.Value); operatorResult = @operator(remainder); } return TokenListParserResult.Value(result, input, remainder); }; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionKeyword.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Expressions.Parsing; readonly struct ExpressionKeyword { public string Text { get; } public ExpressionToken Token { get; } public ExpressionKeyword(string text, ExpressionToken token) { Text = text ?? throw new ArgumentNullException(nameof(text)); Token = token; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionParser.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using Serilog.Expressions.Ast; namespace Serilog.Expressions.Parsing; class ExpressionParser { readonly ExpressionTokenizer _tokenizer = new(); public Expression Parse(string expression) { if (!TryParse(expression, out var root, out var error)) throw new ArgumentException(error); return root; } public bool TryParse(string filterExpression, [MaybeNullWhen(false)] out Expression root, [MaybeNullWhen(true)] out string error) { if (filterExpression == null) throw new ArgumentNullException(nameof(filterExpression)); var tokenList = _tokenizer.TryTokenize(filterExpression); if (!tokenList.HasValue) { error = tokenList.ToString(); root = null; return false; } var result = ExpressionTokenParsers.TryParse(tokenList.Value); if (!result.HasValue) { error = result.ToString(); root = null; return false; } root = result.Value; error = null; return true; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionTextParsers.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Parsers; namespace Serilog.Expressions.Parsing; static class ExpressionTextParsers { static readonly TextParser LessOrEqual = Span.EqualTo("<=").Value(ExpressionToken.LessThanOrEqual); static readonly TextParser GreaterOrEqual = Span.EqualTo(">=").Value(ExpressionToken.GreaterThanOrEqual); static readonly TextParser NotEqual = Span.EqualTo("<>").Value(ExpressionToken.NotEqual); static readonly TextParser Spread = Span.EqualTo("..").Value(ExpressionToken.Spread); public static readonly TextParser CompoundOperator = GreaterOrEqual.Or(LessOrEqual.Try().Or(NotEqual)).Or(Spread); public static readonly TextParser HexInteger = Span.EqualTo("0x") .IgnoreThen(Character.Digit.Or(Character.Matching(ch => ch is >= 'a' and <= 'f' or >= 'A' and <= 'F', "a-f")) .Named("hex digit") .AtLeastOnce()) .Select(chars => new string(chars)); static readonly TextParser StringContentChar = Span.EqualTo("''").Value('\'').Try().Or(Character.Except('\'')); public static readonly TextParser String = Character.EqualTo('\'') .IgnoreThen(StringContentChar.Many()) .Then(s => Character.EqualTo('\'').Value(new string(s))); public static readonly TextParser Real = Numerics.Integer .Then(n => Character.EqualTo('.').IgnoreThen(Numerics.Integer).OptionalOrDefault() .Select(f => f == TextSpan.None ? n : new(n.Source!, n.Position, n.Length + f.Length + 1))); } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionToken.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; namespace Serilog.Expressions.Parsing; enum ExpressionToken { None, Identifier, [Token(Description = "built-in identifier")] BuiltInIdentifier, String, Number, [Token(Description = "hexadecimal number")] HexNumber, [Token(Example = ",")] Comma, [Token(Example = ".")] Period, [Token(Example = "..")] Spread, [Token(Example = "[")] LBracket, [Token(Example = "]")] RBracket, [Token(Example = "(")] LParen, [Token(Example = ")")] RParen, [Token(Example = "{")] LBrace, [Token(Example = "}")] RBrace, [Token(Example = ":")] Colon, [Token(Example = "?")] QuestionMark, [Token(Category = "operator", Example = "+")] Plus, [Token(Category = "operator", Example = "-")] Minus, [Token(Example = "*")] Asterisk, [Token(Category = "operator", Example = "/")] ForwardSlash, [Token(Category = "operator", Example = "%")] Percent, [Token(Category = "operator", Example = "^")] Caret, [Token(Category = "operator", Example = "<")] LessThan, [Token(Category = "operator", Example = "<=")] LessThanOrEqual, [Token(Category = "operator", Example = ">")] GreaterThan, [Token(Category = "operator", Example = ">=")] GreaterThanOrEqual, [Token(Category = "operator", Example = "=")] Equal, [Token(Category = "operator", Example = "<>")] NotEqual, [Token(Category = "keyword", Example = "and")] And, [Token(Category = "keyword", Example = "in")] In, [Token(Category = "keyword", Example = "is")] Is, [Token(Category = "keyword", Example = "like")] Like, [Token(Category = "keyword", Example = "not")] Not, [Token(Category = "keyword", Example = "or")] Or, [Token(Category = "keyword", Example = "true")] True, [Token(Category = "keyword", Example = "false")] False, [Token(Category = "keyword", Example = "null")] Null, [Token(Category = "keyword", Example = "if")] If, [Token(Category = "keyword", Example = "then")] Then, [Token(Category = "keyword", Example = "else")] Else, [Token(Category = "keyword", Example = "ci")] CI, // Template syntax [Token(Description = "text")] Text, [Token(Example = "{{")] DoubleLBrace, [Token(Example = "}}")] DoubleRBrace, [Token(Example = "{#")] LBraceHash, [Token(Description = "format specifier")] Format, [Token(Category = "keyword", Example = "end")] End, [Token(Category = "keyword", Example = "each")] Each, [Token(Category = "keyword", Example = "delimit")] Delimit, } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionTokenParsers.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Globalization; using Serilog.Events; using Serilog.Expressions.Ast; using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Parsers; namespace Serilog.Expressions.Parsing; static class ExpressionTokenParsers { public static TokenListParserResult TryParse( TokenList input) { return Expr.AtEnd().TryParse(input); } public static TokenListParserResult TryPartialParse( TokenList input) { return Expr.TryParse(input); } static readonly TokenListParser Add = Token.EqualTo(ExpressionToken.Plus).Value(Operators.RuntimeOpAdd); static readonly TokenListParser Subtract = Token.EqualTo(ExpressionToken.Minus).Value(Operators.RuntimeOpSubtract); static readonly TokenListParser Multiply = Token.EqualTo(ExpressionToken.Asterisk).Value(Operators.RuntimeOpMultiply); static readonly TokenListParser Divide = Token.EqualTo(ExpressionToken.ForwardSlash).Value(Operators.RuntimeOpDivide); static readonly TokenListParser Modulo = Token.EqualTo(ExpressionToken.Percent).Value(Operators.RuntimeOpModulo); static readonly TokenListParser Power = Token.EqualTo(ExpressionToken.Caret).Value(Operators.RuntimeOpPower); static readonly TokenListParser And = Token.EqualTo(ExpressionToken.And).Value(Operators.RuntimeOpAnd); static readonly TokenListParser Or = Token.EqualTo(ExpressionToken.Or).Value(Operators.RuntimeOpOr); static readonly TokenListParser Lte = Token.EqualTo(ExpressionToken.LessThanOrEqual).Value(Operators.RuntimeOpLessThanOrEqual); static readonly TokenListParser Lt = Token.EqualTo(ExpressionToken.LessThan).Value(Operators.RuntimeOpLessThan); static readonly TokenListParser Gt = Token.EqualTo(ExpressionToken.GreaterThan).Value(Operators.RuntimeOpGreaterThan); static readonly TokenListParser Gte = Token.EqualTo(ExpressionToken.GreaterThanOrEqual).Value(Operators.RuntimeOpGreaterThanOrEqual); static readonly TokenListParser Eq = Token.EqualTo(ExpressionToken.Equal).Value(Operators.RuntimeOpEqual); static readonly TokenListParser Neq = Token.EqualTo(ExpressionToken.NotEqual).Value(Operators.RuntimeOpNotEqual); static readonly TokenListParser Negate = Token.EqualTo(ExpressionToken.Minus).Value(Operators.RuntimeOpNegate); static readonly TokenListParser Not = Token.EqualTo(ExpressionToken.Not).Value(Operators.RuntimeOpNot); static readonly TokenListParser Like = Token.EqualTo(ExpressionToken.Like).Value(Operators.IntermediateOpLike); static readonly TokenListParser NotLike = Token.EqualTo(ExpressionToken.Not) .IgnoreThen(Token.EqualTo(ExpressionToken.Like)) .Value(Operators.IntermediateOpNotLike); static readonly TokenListParser In = Token.EqualTo(ExpressionToken.In).Value(Operators.RuntimeOpIn); static readonly TokenListParser NotIn = Token.EqualTo(ExpressionToken.Not) .IgnoreThen(Token.EqualTo(ExpressionToken.In)) .Value(Operators.RuntimeOpNotIn); static readonly TokenListParser> PropertyPathStep = Token.EqualTo(ExpressionToken.Period) .IgnoreThen(Token.EqualTo(ExpressionToken.Identifier)) .Then(n => Parse.Return>(r => new AccessorExpression(r, n.ToStringValue()))); static readonly TokenListParser Wildcard = Token.EqualTo(ExpressionToken.QuestionMark).Value((Expression)new IndexerWildcardExpression(IndexerWildcard.Any)) .Or(Token.EqualTo(ExpressionToken.Asterisk).Value((Expression)new IndexerWildcardExpression(IndexerWildcard.All))); static readonly TokenListParser> PropertyPathIndexerStep = from open in Token.EqualTo(ExpressionToken.LBracket) from indexer in Wildcard.Or(Parse.Ref(() => Expr!)) from close in Token.EqualTo(ExpressionToken.RBracket) select new Func(r => new IndexerExpression(r, indexer)); static readonly TokenListParser Function = (from name in Token.EqualTo(ExpressionToken.Identifier) from lparen in Token.EqualTo(ExpressionToken.LParen) from expr in Parse.Ref(() => Expr!).ManyDelimitedBy(Token.EqualTo(ExpressionToken.Comma)) from rparen in Token.EqualTo(ExpressionToken.RParen) from ci in Token.EqualTo(ExpressionToken.CI).Value(true).OptionalOrDefault() select (Expression)new CallExpression(ci, name.ToStringValue(), expr)).Named("function"); static readonly TokenListParser ArrayElement = Token.EqualTo(ExpressionToken.Spread) .IgnoreThen(Parse.Ref(() => Expr!)) .Select(content => (Element)new SpreadElement(content)) .Or(Parse.Ref(() => Expr!).Select(item => (Element) new ItemElement(item))); static readonly TokenListParser ArrayLiteral = (from lbracket in Token.EqualTo(ExpressionToken.LBracket) from elements in ArrayElement.ManyDelimitedBy(Token.EqualTo(ExpressionToken.Comma)) from rbracket in Token.EqualTo(ExpressionToken.RBracket) select (Expression)new ArrayExpression(elements)).Named("array"); static readonly TokenListParser IdentifierMember = from key in Token.EqualTo(ExpressionToken.Identifier).Or(Token.EqualTo(ExpressionToken.BuiltInIdentifier)) from value in Token.EqualTo(ExpressionToken.Colon) .IgnoreThen(Parse.Ref(() => Expr!)) .Cast() .OptionalOrDefault() select (Member) new PropertyMember( key.ToStringValue(), value ?? (key.Kind == ExpressionToken.BuiltInIdentifier ? new AmbientNameExpression(key.ToStringValue().Substring(1), true) : new AmbientNameExpression(key.ToStringValue(), false))); static readonly TokenListParser StringMember = from key in Token.EqualTo(ExpressionToken.String).Apply(ExpressionTextParsers.String) from colon in Token.EqualTo(ExpressionToken.Colon) from value in Parse.Ref(() => Expr!) select (Member)new PropertyMember(key, value); static readonly TokenListParser SpreadMember = from spread in Token.EqualTo(ExpressionToken.Spread) from content in Parse.Ref(() => Expr!) select (Member) new SpreadMember(content); static readonly TokenListParser ObjectMember = IdentifierMember.Or(StringMember).Or(SpreadMember).Named("object member"); static readonly TokenListParser ObjectLiteral = (from lbrace in Token.EqualTo(ExpressionToken.LBrace) from members in ObjectMember.ManyDelimitedBy(Token.EqualTo(ExpressionToken.Comma)) from rbrace in Token.EqualTo(ExpressionToken.RBrace) select (Expression)new ObjectExpression(members)).Named("object"); static readonly TokenListParser RootProperty = (from notFunction in Parse.Not(Token.EqualTo(ExpressionToken.Identifier).IgnoreThen(Token.EqualTo(ExpressionToken.LParen))) from p in Token.EqualTo(ExpressionToken.BuiltInIdentifier).Select(b => (Expression) new AmbientNameExpression(b.ToStringValue().Substring(1), true)) .Or(Token.EqualTo(ExpressionToken.Identifier).Select(t => (Expression) new AmbientNameExpression(t.ToStringValue(), false))) select p).Named("property"); static readonly TokenListParser String = Token.EqualTo(ExpressionToken.String) .Apply(ExpressionTextParsers.String) .Select(s => (Expression)new ConstantExpression(new ScalarValue(s))); static readonly TokenListParser HexNumber = Token.EqualTo(ExpressionToken.HexNumber) .Apply(ExpressionTextParsers.HexInteger) .SelectCatch(n => ulong.Parse(n, NumberStyles.HexNumber, CultureInfo.InvariantCulture), "the numeric literal is too large") .Select(u => (Expression)new ConstantExpression(new ScalarValue((decimal)u))); static readonly TokenListParser Number = Token.EqualTo(ExpressionToken.Number) .Apply(ExpressionTextParsers.Real) .SelectCatch(n => decimal.Parse(n.ToStringValue(), CultureInfo.InvariantCulture), "the numeric literal is too large") .Select(d => (Expression)new ConstantExpression(new ScalarValue(d))); static readonly TokenListParser Conditional = from _ in Token.EqualTo(ExpressionToken.If) from condition in Parse.Ref(() => Expr!) from __ in Token.EqualTo(ExpressionToken.Then) from consequent in Parse.Ref(() => Expr!) from ___ in Token.EqualTo(ExpressionToken.Else) from alternative in Parse.Ref(() => Expr!) select (Expression)new CallExpression(false, Operators.RuntimeOpIfThenElse, condition, consequent, alternative); static readonly TokenListParser Literal = String .Or(Number) .Or(HexNumber) .Or(Token.EqualTo(ExpressionToken.True).Value((Expression)new ConstantExpression(new ScalarValue(true)))) .Or(Token.EqualTo(ExpressionToken.False).Value((Expression)new ConstantExpression(new ScalarValue(false)))) .Or(Token.EqualTo(ExpressionToken.Null).Value((Expression)new ConstantExpression(new ScalarValue(null)))) .Named("literal"); static readonly TokenListParser Item = Literal .Or(RootProperty) .Or(Function) .Or(ArrayLiteral) .Or(ObjectLiteral) .Or(Conditional); static readonly TokenListParser Factor = (from lparen in Token.EqualTo(ExpressionToken.LParen) from expr in Parse.Ref(() => Expr!) from rparen in Token.EqualTo(ExpressionToken.RParen) select expr) .Or(Item); static readonly TokenListParser Path = from root in Factor from path in PropertyPathStep.Or(PropertyPathIndexerStep).Many() select path.Aggregate(root, (o, f) => f(o)); static readonly TokenListParser Operand = (from op in Negate.Or(Not) from path in Path select MakeUnary(op, path)) .Or(Path) .Then(operand => Token.EqualTo(ExpressionToken.Is).Try() .IgnoreThen( Token.EqualTo(ExpressionToken.Null).Value(Operators.RuntimeOpIsNull) .Or(Token.EqualTo(ExpressionToken.Not).IgnoreThen(Token.EqualTo(ExpressionToken.Null)).Value(Operators.RuntimeOpIsNotNull))) .Select(op => (Expression)new CallExpression(false, op, operand)) .OptionalOrDefault(operand)) .Named("expression"); static readonly TokenListParser InnerTerm = Parse.Chain(Power, Operand, MakeBinary); static readonly TokenListParser Term = Parse.Chain(Multiply.Or(Divide).Or(Modulo), InnerTerm, MakeBinary); static readonly TokenListParser Comparand = Parse.Chain(Add.Or(Subtract), Term, MakeBinary); static readonly TokenListParser Comparison = Combinators.ChainModified( NotLike.Try().Or(Like) .Or(NotIn.Try().Or(In)) .Or(Lte.Or(Neq).Or(Lt)) .Or(Gte.Or(Gt)) .Or(Eq), Comparand, Token.EqualTo(ExpressionToken.CI).Value(true).OptionalOrDefault(), (o, l, r, ci) => new CallExpression(ci, o, l, r)); static readonly TokenListParser Conjunction = Parse.Chain(And, Comparison, MakeBinary); static readonly TokenListParser Disjunction = Parse.Chain(Or, Conjunction, MakeBinary); public static readonly TokenListParser Expr = Disjunction; static Expression MakeBinary(string operatorName, Expression leftOperand, Expression rightOperand) { return new CallExpression(false, operatorName, leftOperand, rightOperand); } static Expression MakeUnary(string operatorName, Expression operand) { return new CallExpression(false, operatorName, operand); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ExpressionTokenizer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; namespace Serilog.Expressions.Parsing; class ExpressionTokenizer : Tokenizer { readonly ExpressionToken[] _singleCharOps = new ExpressionToken[128]; readonly ExpressionKeyword[] _keywords = [ new("and", ExpressionToken.And), new("in", ExpressionToken.In), new("is", ExpressionToken.Is), new("like", ExpressionToken.Like), new("not", ExpressionToken.Not), new("or", ExpressionToken.Or), new("true", ExpressionToken.True), new("false", ExpressionToken.False), new("null", ExpressionToken.Null), new("if", ExpressionToken.If), new("then", ExpressionToken.Then), new("else", ExpressionToken.Else), new("end", ExpressionToken.End), new("ci", ExpressionToken.CI), new("each", ExpressionToken.Each), new("delimit", ExpressionToken.Delimit) ]; public ExpressionTokenizer() { _singleCharOps['+'] = ExpressionToken.Plus; _singleCharOps['-'] = ExpressionToken.Minus; _singleCharOps['*'] = ExpressionToken.Asterisk; _singleCharOps['/'] = ExpressionToken.ForwardSlash; _singleCharOps['%'] = ExpressionToken.Percent; _singleCharOps['^'] = ExpressionToken.Caret; _singleCharOps['<'] = ExpressionToken.LessThan; _singleCharOps['>'] = ExpressionToken.GreaterThan; _singleCharOps['='] = ExpressionToken.Equal; _singleCharOps[','] = ExpressionToken.Comma; _singleCharOps['.'] = ExpressionToken.Period; _singleCharOps['('] = ExpressionToken.LParen; _singleCharOps[')'] = ExpressionToken.RParen; _singleCharOps['{'] = ExpressionToken.LBrace; _singleCharOps['}'] = ExpressionToken.RBrace; _singleCharOps[':'] = ExpressionToken.Colon; _singleCharOps['['] = ExpressionToken.LBracket; _singleCharOps[']'] = ExpressionToken.RBracket; _singleCharOps['*'] = ExpressionToken.Asterisk; _singleCharOps['?'] = ExpressionToken.QuestionMark; } public TokenList GreedyTokenize(TextSpan textSpan) { // Dropping error info off for now return new( Tokenize(textSpan) .TakeWhile(r => r.HasValue) .Select(r => new Token(r.Value, r.Location.Until(r.Remainder))) .ToArray()); } public IEnumerable> LazyTokenize(TextSpan span) { return Tokenize(span); } protected override IEnumerable> Tokenize(TextSpan stringSpan) { var next = SkipWhiteSpace(stringSpan); if (!next.HasValue) yield break; do { if (char.IsDigit(next.Value)) { var hex = ExpressionTextParsers.HexInteger(next.Location); if (hex.HasValue) { next = hex.Remainder.ConsumeChar(); yield return Result.Value(ExpressionToken.HexNumber, hex.Location, hex.Remainder); } else { var real = ExpressionTextParsers.Real(next.Location); if (!real.HasValue) yield return Result.CastEmpty(real); else yield return Result.Value(ExpressionToken.Number, real.Location, real.Remainder); next = real.Remainder.ConsumeChar(); } if (!IsDelimiter(next)) { yield return Result.Empty(next.Location, ["digit"]); } } else if (next.Value == '\'') { var str = ExpressionTextParsers.String(next.Location); if (!str.HasValue) yield return Result.CastEmpty(str); next = str.Remainder.ConsumeChar(); yield return Result.Value(ExpressionToken.String, str.Location, str.Remainder); } else if (next.Value == '@') { var beginIdentifier = next.Location; var startOfName = next.Remainder; do { next = next.Remainder.ConsumeChar(); } while (next.HasValue && char.IsLetterOrDigit(next.Value)); if (next.Remainder == startOfName) { yield return Result.Empty(startOfName, ["built-in identifier name"]); } else { yield return Result.Value(ExpressionToken.BuiltInIdentifier, beginIdentifier, next.Location); } } else if (char.IsLetter(next.Value) || next.Value == '_') { var beginIdentifier = next.Location; do { next = next.Remainder.ConsumeChar(); } while (next.HasValue && (char.IsLetterOrDigit(next.Value) || next.Value == '_')); if (TryGetKeyword(beginIdentifier.Until(next.Location), out var keyword)) { yield return Result.Value(keyword, beginIdentifier, next.Location); } else { yield return Result.Value(ExpressionToken.Identifier, beginIdentifier, next.Location); } } else { var compoundOp = ExpressionTextParsers.CompoundOperator(next.Location); if (compoundOp.HasValue) { yield return Result.Value(compoundOp.Value, compoundOp.Location, compoundOp.Remainder); next = compoundOp.Remainder.ConsumeChar(); } else if (next.Value < _singleCharOps.Length && _singleCharOps[next.Value] != ExpressionToken.None) { yield return Result.Value(_singleCharOps[next.Value], next.Location, next.Remainder); next = next.Remainder.ConsumeChar(); } else { yield return Result.Empty(next.Location); next = next.Remainder.ConsumeChar(); } } next = SkipWhiteSpace(next.Location); } while (next.HasValue); } bool IsDelimiter(Result next) { return !next.HasValue || char.IsWhiteSpace(next.Value) || next.Value < _singleCharOps.Length && _singleCharOps[next.Value] != ExpressionToken.None; } bool TryGetKeyword(TextSpan span, out ExpressionToken keyword) { foreach (var kw in _keywords) { if (span.EqualsValueIgnoreCase(kw.Text)) { keyword = kw.Token; return true; } } keyword = ExpressionToken.None; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Parsing/ParserExtensions.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; namespace Serilog.Expressions.Parsing; static class ParserExtensions { public static TokenListParser SelectCatch(this TokenListParser parser, Func trySelector, string errorMessage) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (trySelector == null) throw new ArgumentNullException(nameof(trySelector)); if (errorMessage == null) throw new ArgumentNullException(nameof(errorMessage)); return input => { var t = parser(input); if (!t.HasValue) return TokenListParserResult.CastEmpty(t); try { var u = trySelector(t.Value); return TokenListParserResult.Value(u, input, t.Remainder); } catch { return TokenListParserResult.Empty(input, errorMessage); } }; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Runtime/Coerce.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using Serilog.Events; namespace Serilog.Expressions.Runtime; static class Coerce { static readonly Type[] NumericTypes = [ typeof(decimal), typeof(int), typeof(long), typeof(double), typeof(float), typeof(uint), typeof(sbyte), typeof(byte), typeof(short), typeof(ushort), typeof(ulong) ]; public static bool Numeric(LogEventPropertyValue? value, out decimal numeric) { if (value is ScalarValue sv && sv.Value != null && NumericTypes.Contains(sv.Value.GetType())) { numeric = (decimal)Convert.ChangeType(sv.Value, typeof(decimal)); return true; } numeric = default; return false; } public static bool Boolean(LogEventPropertyValue? value, out bool boolean) { if (value is ScalarValue sv && sv.Value is bool b) { boolean = b; return true; } boolean = default; return false; } public static bool IsTrue(LogEventPropertyValue? value) { return Boolean(value, out var b) && b; } public static bool String(LogEventPropertyValue? value, [NotNullWhen(true)] out string? str) { if (value is ScalarValue sv) { if (sv.Value is string s) { str = s; return true; } if (sv.Value is Exception ex) { str = ex.ToString(); return true; } if (sv.Value?.GetType().IsEnum ?? false) { str = sv.Value.ToString()!; return true; } if (sv.Value is ActivityTraceId traceId) { str = traceId.ToHexString(); return true; } if (sv.Value is ActivitySpanId spanId) { str = spanId.ToHexString(); return true; } } str = default; return false; } public static bool Predicate(LogEventPropertyValue? value, [MaybeNullWhen(false)] out Func predicate) { if (value is ScalarValue sv && sv.Value is Func pred) { predicate = pred; return true; } predicate = default; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Runtime/Locals.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using Serilog.Events; namespace Serilog.Expressions.Runtime; /// /// Named local variables. We just look them up by name. The structure is a /// linked list with a null terminator: most of the time expressions won't have any /// locals, and when they do, they'll only have one or two at a given point. /// class Locals { readonly Locals? _others; readonly string _name; readonly LogEventPropertyValue _value; Locals(Locals? others, string name, LogEventPropertyValue value) { _others = others; _name = name; _value = value; } public static Locals Set(Locals? others, string name, LogEventPropertyValue value) { return new(others, name, value); } public static bool TryGetValue(Locals? locals, string name, [MaybeNullWhen(false)] out LogEventPropertyValue value) { while (locals != null) { if (name == locals._name) { value = locals._value; return true; } locals = locals._others; } value = null; return false; } } ================================================ FILE: src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Reflection; using Serilog.Debugging; using Serilog.Events; using Serilog.Expressions.Compilation.Linq; using Serilog.Expressions.Runtime.Support; using Serilog.Templates.Rendering; // ReSharper disable ForCanBeConvertedToForeach, InvertIf, MemberCanBePrivate.Global, UnusedMember.Global, InconsistentNaming, ReturnTypeCanBeNotNullable namespace Serilog.Expressions.Runtime; static class RuntimeOperators { static readonly LogEventPropertyValue ConstantTrue = new ScalarValue(true), ConstantFalse = new ScalarValue(false); internal static LogEventPropertyValue ScalarBoolean(bool value) { return value ? ConstantTrue : ConstantFalse; } public static LogEventPropertyValue? Undefined() { return null; } public static LogEventPropertyValue? _Internal_Add(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return new ScalarValue(l + r); return default; } public static LogEventPropertyValue? _Internal_Subtract(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return new ScalarValue(l - r); return default; } public static LogEventPropertyValue? _Internal_Multiply(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return new ScalarValue(l * r); return default; } public static LogEventPropertyValue? _Internal_Divide(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r) && r != 0) return new ScalarValue(l / r); return default; } public static LogEventPropertyValue? _Internal_Modulo(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r) && r != 0) return new ScalarValue(l % r); return default; } public static LogEventPropertyValue? _Internal_Power(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return new ScalarValue(Math.Pow((double)l, (double)r)); return default; } // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? _Internal_And(LogEventPropertyValue? left, LogEventPropertyValue? right) { throw new InvalidOperationException("Logical operators should be evaluated intrinsically."); } // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? _Internal_Or(LogEventPropertyValue? left, LogEventPropertyValue? right) { throw new InvalidOperationException("Logical operators should be evaluated intrinsically."); } public static LogEventPropertyValue? _Internal_LessThanOrEqual(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return ScalarBoolean(l <= r); return default; } public static LogEventPropertyValue? _Internal_LessThan(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return ScalarBoolean(l < r); return default; } public static LogEventPropertyValue? _Internal_GreaterThan(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return ScalarBoolean(l > r); return default; } public static LogEventPropertyValue? _Internal_GreaterThanOrEqual(LogEventPropertyValue? left, LogEventPropertyValue? right) { if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return ScalarBoolean(l >= r); return default; } public static LogEventPropertyValue? _Internal_Equal(StringComparison sc, LogEventPropertyValue? left, LogEventPropertyValue? right) { // Undefined values propagate through comparisons if (left == null || right == null) return null; return ScalarBoolean(UnboxedEqualHelper(sc, left, right)); } // Return value is a regular `bool` and not a scalar value as you'd get from `Equal` static bool UnboxedEqualHelper(StringComparison sc, LogEventPropertyValue? left, LogEventPropertyValue? right) { if (left == null || right == null) throw new ArgumentException("Undefined values should short-circuit."); if (Coerce.Numeric(left, out var l) && Coerce.Numeric(right, out var r)) return l == r; if (Coerce.String(left, out var ls) && Coerce.String(right, out var rs)) return ls.Equals(rs, sc); if (left is ScalarValue sl && right is ScalarValue sr) return sl.Value?.Equals(sr.Value) ?? sr.Value == null; if (left is SequenceValue ql && right is SequenceValue qr) { // Not in any way optimized :-) return ql.Elements.Count == qr.Elements.Count && ql.Elements.Zip(qr.Elements, (ll, rr) => UnboxedEqualHelper(sc, ll, rr)).All(eq => eq); } if (left is StructureValue tl && right is StructureValue tr) { // .... even less optimized; lots of work to de-dup keys with last-in-wins precedence. var lhs = new Dictionary(); foreach (var property in tl.Properties) lhs[property.Name] = property.Value; var rhs = new Dictionary(); foreach (var property in tr.Properties) rhs[property.Name] = property.Value; return lhs.Keys.Count == rhs.Keys.Count && lhs.All(kv => rhs.TryGetValue(kv.Key, out var value) && UnboxedEqualHelper(sc, kv.Value, value)); } return false; } public static LogEventPropertyValue? _Internal_In(StringComparison sc, LogEventPropertyValue? item, LogEventPropertyValue? collection) { if (item == null) return null; if (collection is SequenceValue arr) { for (var i = 0; i < arr.Elements.Count; ++i) { var element = arr.Elements[i]; // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (element != null && UnboxedEqualHelper(sc, element, item)) return ConstantTrue; } return ConstantFalse; } return null; } public static LogEventPropertyValue? _Internal_NotIn(StringComparison sc, LogEventPropertyValue? item, LogEventPropertyValue? collection) { return _Internal_StrictNot(_Internal_In(sc, item, collection)); } public static LogEventPropertyValue? _Internal_NotEqual(StringComparison sc, LogEventPropertyValue? left, LogEventPropertyValue? right) { if (left == null || right == null) return null; return ScalarBoolean(!UnboxedEqualHelper(sc, left, right)); } public static LogEventPropertyValue? _Internal_Negate(LogEventPropertyValue? value) { if (Coerce.Numeric(value, out var numeric)) return new ScalarValue(-numeric); return null; } public static LogEventPropertyValue? Round(LogEventPropertyValue? value, LogEventPropertyValue? places) { if (!Coerce.Numeric(value, out var v) || !Coerce.Numeric(places, out var p) || p is < 0 or > 32) // Check my memory, here :D { return null; } return new ScalarValue(Math.Round(v, (int)p)); } public static LogEventPropertyValue? _Internal_Not(LogEventPropertyValue? value) { if (value is null) return ConstantTrue; return Coerce.Boolean(value, out var b) ? ScalarBoolean(!b) : null; } public static LogEventPropertyValue? _Internal_StrictNot(LogEventPropertyValue? value) { return Coerce.Boolean(value, out var b) ? ScalarBoolean(!b) : null; } public static LogEventPropertyValue? Contains(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { if (!Coerce.String(haystack, out var ctx) || !Coerce.String(needle, out var ptx)) return null; return ScalarBoolean(ctx.Contains(ptx, sc)); } public static LogEventPropertyValue? IndexOf(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { if (!Coerce.String(haystack, out var ctx) || !Coerce.String(needle, out var ptx)) return null; return new ScalarValue(ctx.IndexOf(ptx, sc)); } public static LogEventPropertyValue? LastIndexOf(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { if (!Coerce.String(haystack, out var ctx) || !Coerce.String(needle, out var ptx)) return null; return new ScalarValue(ctx.LastIndexOf(ptx, sc)); } public static LogEventPropertyValue? Length(LogEventPropertyValue? value) { if (Coerce.String(value, out var s)) return new ScalarValue(s.Length); if (value is SequenceValue seq) return new ScalarValue(seq.Elements.Count); return null; } public static LogEventPropertyValue? StartsWith(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { if (!Coerce.String(haystack, out var ctx) || !Coerce.String(needle, out var ptx)) return null; return ScalarBoolean(ctx.StartsWith(ptx, sc)); } public static LogEventPropertyValue? EndsWith(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { if (!Coerce.String(haystack, out var ctx) || !Coerce.String(needle, out var ptx)) return null; return ScalarBoolean(ctx.EndsWith(ptx, sc)); } public static LogEventPropertyValue IsDefined(LogEventPropertyValue? value) { return ScalarBoolean(value != null); } public static LogEventPropertyValue? ElementAt(StringComparison sc, LogEventPropertyValue? collection, LogEventPropertyValue? index) { // ReSharper disable once ConvertIfStatementToSwitchStatement if (collection is SequenceValue arr && Coerce.Numeric(index, out var ix)) { if (ix != Math.Floor(ix)) return null; var idx = (int)ix; if (idx >= arr.Elements.Count) return null; return arr.Elements.ElementAt(idx); } if (collection is StructureValue st && Coerce.String(index, out var s)) { return Intrinsics.TryGetStructurePropertyValue(sc, st, s); } if (collection is DictionaryValue dict && index is ScalarValue sv) { // The lack of eager numeric type coercion means that here, `sv` may logically equal one // of the keys, but not be equal according to the dictionary's `IEqualityComparer`. // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract var entry = dict.Elements.FirstOrDefault(kv => kv.Key != null && UnboxedEqualHelper(sc, kv.Key, sv)); return entry.Value; // KVP is a struct; default is a pair of nulls. } return null; } public static LogEventPropertyValue? _Internal_Any(LogEventPropertyValue? collection, LogEventPropertyValue? predicate) { if (!Coerce.Predicate(predicate, out var pred)) return null; if (collection is SequenceValue arr) { return ScalarBoolean(arr.Elements.Any(e => Coerce.IsTrue(pred(e)))); } if (collection is StructureValue structure) { return ScalarBoolean(structure.Properties.Any(e => Coerce.IsTrue(pred(e.Value)))); } if (collection is DictionaryValue dictionary) { return ScalarBoolean(dictionary.Elements.Any(e => Coerce.IsTrue(pred(e.Value)))); } return null; } public static LogEventPropertyValue? _Internal_All(LogEventPropertyValue? collection, LogEventPropertyValue? predicate) { if (!Coerce.Predicate(predicate, out var pred)) return null; if (collection is SequenceValue arr) { return ScalarBoolean(arr.Elements.All(e => Coerce.IsTrue(pred(e)))); } if (collection is StructureValue structure) { return ScalarBoolean(structure.Properties.All(e => Coerce.IsTrue(pred(e.Value)))); } if (collection is DictionaryValue dictionary) { return ScalarBoolean(dictionary.Elements.All(e => Coerce.IsTrue(pred(e.Value)))); } return null; } public static LogEventPropertyValue? TagOf(LogEventPropertyValue? value) { if (value is StructureValue structure) return new ScalarValue(structure.TypeTag); // I.e. may be null return null; } public static LogEventPropertyValue TypeOf(LogEventPropertyValue? value) { if (value is DictionaryValue) return new ScalarValue("dictionary"); if (value is StructureValue) return new ScalarValue("object"); if (value is SequenceValue) return new ScalarValue("array"); if (value is ScalarValue scalar) { return new ScalarValue(scalar.Value?.GetType().ToString() ?? "null"); } return new ScalarValue("undefined"); } public static LogEventPropertyValue _Internal_IsNull(LogEventPropertyValue? value) { return ScalarBoolean(value is null or ScalarValue {Value: null}); } public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? value) { return ScalarBoolean(value is not (null or ScalarValue {Value: null})); } // Ideally this will be compiled as a short-circuiting intrinsic public static LogEventPropertyValue? Coalesce(LogEventPropertyValue? value0, LogEventPropertyValue? value1) { if (value0 is null or ScalarValue {Value: null}) return value1; return value0; } public static LogEventPropertyValue? Substring(LogEventPropertyValue? value, LogEventPropertyValue? startIndex, LogEventPropertyValue? length = null) { if (!Coerce.String(value, out var str) || !Coerce.Numeric(startIndex, out var si)) return null; if (si < 0 || si >= str.Length || (int)si != si) return null; if (length == null) return new ScalarValue(str.Substring((int)si)); if (!Coerce.Numeric(length, out var len) || (int)len != len) return null; if (len + si > str.Length) return new ScalarValue(str.Substring((int)si)); return new ScalarValue(str.Substring((int)si, (int)len)); } public static LogEventPropertyValue? Concat(LogEventPropertyValue? string0, LogEventPropertyValue? string1) { if (Coerce.String(string0, out var f) && Coerce.String(string1, out var s)) { return new ScalarValue(f + s); } return null; } public static LogEventPropertyValue? Replace(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle, LogEventPropertyValue? replacement) { if (Coerce.String(haystack, out var h) && Coerce.String(needle, out var n) && Coerce.String(replacement, out var r)) { return new ScalarValue(h.Replace(n, r, sc)); } return null; } // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? IndexOfMatch(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { throw new InvalidOperationException("Regular expression evaluation is intrinsic."); } // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? IsMatch(StringComparison sc, LogEventPropertyValue? haystack, LogEventPropertyValue? needle) { throw new InvalidOperationException("Regular expression evaluation is intrinsic."); } // Ideally this will be compiled as a short-circuiting intrinsic public static LogEventPropertyValue? _Internal_IfThenElse( LogEventPropertyValue? condition, LogEventPropertyValue? consequent, LogEventPropertyValue? alternative) { return Coerce.IsTrue(condition) ? consequent : alternative; } public static LogEventPropertyValue? ToString(IFormatProvider? formatProvider, LogEventPropertyValue? value, LogEventPropertyValue? format = null) { if (value is not ScalarValue sv || sv.Value == null || !(Coerce.String(format, out var fmt) || format is null or ScalarValue { Value: null })) { return null; } var toString = sv.Value switch { LogEventLevel level => LevelRenderer.GetLevelMoniker(level, fmt), IFormattable formattable => formattable.ToString(fmt, formatProvider), _ => sv.Value.ToString() }; return new ScalarValue(toString); } public static LogEventPropertyValue? UtcDateTime(LogEventPropertyValue? dateTime) { if (dateTime is ScalarValue sv) { if (sv.Value is DateTimeOffset dto) return new ScalarValue(dto.UtcDateTime); if (sv.Value is DateTime dt) return new ScalarValue(dt.ToUniversalTime()); } return null; } // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? Now() { // DateTimeOffset.Now is the generator for LogEvent.Timestamp. return new ScalarValue(DateTimeOffset.Now); } public static LogEventPropertyValue? Inspect(LogEventPropertyValue? value, LogEventPropertyValue? deep = null) { if (value is not ScalarValue { Value: {} toCapture }) return value; var result = new List(); var logger = new LoggerConfiguration().CreateLogger(); var properties = toCapture.GetType() .GetProperties(BindingFlags.Instance | BindingFlags.Public | BindingFlags.GetProperty); foreach (var property in properties) { object? p; try { p = property.GetValue(toCapture); } catch (Exception ex) { SelfLog.WriteLine("Serilog.Expressions Inspect() target property threw exception: {0}", ex); continue; } if (deep is ScalarValue { Value: true }) { if (logger.BindProperty(property.Name, p, destructureObjects: true, out var bound)) result.Add(bound); } else { result.Add(new LogEventProperty(property.Name, new ScalarValue(p))); } } return new StructureValue(result); } public static LogEventPropertyValue? Nest(LogEventPropertyValue? maybeStructure) { if (maybeStructure is not StructureValue { Properties: { } flat }) return null; var byName = new Dictionary(flat.Count); foreach (var property in flat) { // Supports duplicate property names, despite these being hard to generate. byName[property.Name] = property.Value; } var props = UnflattenDottedPropertyNames.ProcessDottedPropertyNames(byName); return new StructureValue(props.Select(p => new LogEventProperty(p.Key, p.Value)).ToList()); } } ================================================ FILE: src/Serilog.Expressions/Expressions/Runtime/Support/UnflattenDottedPropertyNames.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics; using Serilog.Events; namespace Serilog.Expressions.Runtime.Support; /// /// Nest (un-flatten) properties with dotted names. A property with name "a.b" will be transmitted to Seq as /// a structure with name "a", and one member "b". /// /// Forked from . static class UnflattenDottedPropertyNames { const int MaxDepth = 10; public static IReadOnlyDictionary ProcessDottedPropertyNames(IReadOnlyDictionary maybeDotted) { return DottedToNestedRecursive(maybeDotted, 0); } static IReadOnlyDictionary DottedToNestedRecursive(IReadOnlyDictionary maybeDotted, int depth) { if (depth == MaxDepth) return maybeDotted; // Assume that the majority of entries will be bare or have unique prefixes. var result = new Dictionary(maybeDotted.Count); // Sorted for determinism. var dotted = new SortedDictionary(StringComparer.Ordinal); // First - give priority to bare names, since these would otherwise be claimed by the parents of further nested // layers and we'd have nowhere to put them when resolving conflicts. (Dotted entries that conflict can keep their dotted keys). foreach (var kv in maybeDotted) { if (IsDottedIdentifier(kv.Key)) { // Stash for processing in the next stage. dotted.Add(kv.Key, kv.Value); } else { result.Add(kv.Key, kv.Value); } } // Then - for dotted keys with a prefix not already present in the result, convert to structured data and add to // the result. Any set of dotted names that collide with a preexisting key will be left as-is. string? prefix = null; Dictionary? nested = null; foreach (var kv in dotted) { var (newPrefix, rem) = TakeFirstIdentifier(kv.Key); if (prefix != null && prefix != newPrefix) { result.Add(prefix, MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1))); prefix = null; nested = null; } if (nested != null && !nested.ContainsKey(rem)) { prefix = newPrefix; nested.Add(rem, kv.Value); } else if (nested == null && !result.ContainsKey(newPrefix)) { prefix = newPrefix; nested = new () { { rem, kv.Value } }; } else { result.Add(kv.Key, kv.Value); } } if (prefix != null) { result[prefix] = MakeStructureValue(DottedToNestedRecursive(nested!, depth + 1)); } return result; } static StructureValue MakeStructureValue(IReadOnlyDictionary properties) { return new StructureValue(properties.Select(kv => new LogEventProperty(kv.Key, kv.Value)), typeTag: null); } static bool IsDottedIdentifier(string key) => key.Contains('.') && !key.StartsWith(".", StringComparison.Ordinal) && !key.EndsWith(".", StringComparison.Ordinal) && key.Split('.').All(IsIdentifier); static bool IsIdentifier(string s) => s.Length != 0 && !char.IsDigit(s[0]) && s.All(ch => char.IsLetter(ch) || char.IsDigit(ch) || ch == '_'); static (string, string) TakeFirstIdentifier(string dottedIdentifier) { // We can do this simplistically because keys in `dotted` conform to `IsDottedName`. Debug.Assert(IsDottedIdentifier(dottedIdentifier)); var firstDot = dottedIdentifier.IndexOf('.'); var prefix = dottedIdentifier.Substring(0, firstDot); var rem = dottedIdentifier.Substring(firstDot + 1); return (prefix, rem); } } ================================================ FILE: src/Serilog.Expressions/Expressions/SerilogExpression.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Globalization; using Serilog.Expressions.Compilation; using Serilog.Expressions.Parsing; // ReSharper disable MemberCanBePrivate.Global namespace Serilog.Expressions; /// /// Helper methods to assist with construction of well-formed expressions. /// public static class SerilogExpression { /// /// Create an evaluation function based on the provided expression. /// /// An expression. /// Optionally, a format provider that will be used for culture-specific formatting; /// by default, is used. /// Optionally, a /// with which to resolve function names that appear in the template. /// A function that evaluates the expression in the context of a log event. public static CompiledExpression Compile(string expression, IFormatProvider? formatProvider = null, NameResolver? nameResolver = null) { if (expression == null) throw new ArgumentNullException(nameof(expression)); if (!TryCompileImpl(expression, formatProvider, nameResolver, out var filter, out var error)) throw new ArgumentException(error); return filter; } /// /// Create an evaluation function based on the provided expression. /// /// An expression. /// A function that evaluates the expression in the context of a log event. /// The reported error, if compilation was unsuccessful. /// True if the function could be created; otherwise, false. /// Validation errors including invalid regular expressions and unknown function names are returned /// as friendly error messages. Invalid case-insensitive modifiers are ignored with warnings. public static bool TryCompile( string expression, [MaybeNullWhen(false)] out CompiledExpression result, [MaybeNullWhen(true)] out string error) { if (expression == null) throw new ArgumentNullException(nameof(expression)); return TryCompileImpl(expression, null, null, out result, out error); } /// /// Create an evaluation function based on the provided expression. /// /// An expression. /// Optionally, a format provider that will be used for culture-specific formatting; /// by default, is used. /// A /// with which to resolve function names that appear in the template. /// A function that evaluates the expression in the context of a log event. /// The reported error, if compilation was unsuccessful. /// True if the function could be created; otherwise, false. /// Validation errors including invalid regular expressions and unknown function names are returned /// as friendly error messages. Invalid case-insensitive modifiers are ignored with warnings. public static bool TryCompile(string expression, IFormatProvider? formatProvider, NameResolver nameResolver, [MaybeNullWhen(false)] out CompiledExpression result, [MaybeNullWhen(true)] out string error) { if (expression == null) throw new ArgumentNullException(nameof(expression)); if (nameResolver == null) throw new ArgumentNullException(nameof(nameResolver)); return TryCompileImpl(expression, formatProvider, nameResolver, out result, out error); } static bool TryCompileImpl(string expression, IFormatProvider? formatProvider, NameResolver? nameResolver, [MaybeNullWhen(false)] out CompiledExpression result, [MaybeNullWhen(true)] out string error) { var expressionParser = new ExpressionParser(); if (!expressionParser.TryParse(expression, out var root, out error)) { result = null; return false; } try { var evaluate = ExpressionCompiler.Compile(root, formatProvider, DefaultFunctionNameResolver.Build(nameResolver)); result = evt => evaluate(new(evt)); error = null; return true; } catch (ExpressionValidationException ex) { // Catch validation errors from compilation result = null; error = ex.Message; return false; } } /// /// Escape a value that is to appear in a `like` expression. /// /// The text to escape. /// The text with any special values escaped. Will need to be passed through /// if it is being embedded directly into a filter expression. // ReSharper disable once UnusedMember.Global public static string EscapeLikeExpressionContent(string text) { if (text == null) throw new ArgumentNullException(nameof(text)); return EscapeStringContent(text) .Replace("%", "%%") .Replace("_", "__"); } /// /// Escape a fragment of text that will appear within a string. /// /// The text to escape. /// The text with any special values escaped. public static string EscapeStringContent(string text) { if (text == null) throw new ArgumentNullException(nameof(text)); return text.Replace("'", "''"); } /// /// Determine if the specified text is a valid identifier. /// /// The text to check. /// True if the text can be used verbatim as a property name. public static bool IsValidIdentifier(string identifier) { return identifier.Length != 0 && !char.IsDigit(identifier[0]) && identifier.All(ch => char.IsLetter(ch) || char.IsDigit(ch) || ch == '_'); } } ================================================ FILE: src/Serilog.Expressions/Expressions/StaticMemberNameResolver.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Reflection; namespace Serilog.Expressions; /// /// A that matches public static members of a class by name. /// public class StaticMemberNameResolver : NameResolver { readonly IReadOnlyDictionary _methods; /// /// Create a that returns members of the specified . /// /// A with public static members implementing runtime functions. public StaticMemberNameResolver( #if NET6_0_OR_GREATER [DynamicallyAccessedMembers(DynamicallyAccessedMemberTypes.PublicMethods)] #endif Type type) { if (type == null) throw new ArgumentNullException(nameof(type)); _methods = type .GetTypeInfo() .GetMethods(BindingFlags.Static | BindingFlags.Public) .ToDictionary(m => m.Name, StringComparer.OrdinalIgnoreCase); } /// public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { return _methods.TryGetValue(name, out implementation); } } ================================================ FILE: src/Serilog.Expressions/LoggerEnrichmentConfigurationExtensions.cs ================================================ // Copyright 2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Configuration; using Serilog.Expressions; using Serilog.Expressions.Runtime; using Serilog.Pipeline; namespace Serilog; /// /// Extends logger enrichment configuration with methods for filtering with expressions. /// public static class LoggerEnrichmentConfigurationExtensions { /// /// Write to a sink only when evaluates to true. /// /// Enrichment configuration. /// An expression that evaluates to true when the supplied /// should be enriched. /// An action that configures the wrapped enricher. /// The underlying . public static LoggerConfiguration When( this LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, string expression, Action configureEnricher) { if (loggerEnrichmentConfiguration == null) throw new ArgumentNullException(nameof(loggerEnrichmentConfiguration)); if (expression == null) throw new ArgumentNullException(nameof(expression)); if (configureEnricher == null) throw new ArgumentNullException(nameof(configureEnricher)); var compiled = SerilogExpression.Compile(expression); return loggerEnrichmentConfiguration.When(e => Coerce.IsTrue(compiled(e)), configureEnricher); } /// /// Enrich events with a property computed by evaluating /// in the context of the event. /// /// Enrichment configuration. /// The name of the property to attach; if the property already /// exists, and evaluates to a defined value, it will be overwritten. /// An expression to evaluate in the context of each event. If the result of /// evaluating the expression is defined, it will be attached to the event as . /// The underlying . public static LoggerConfiguration WithComputed( this LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, string propertyName, string expression) { if (propertyName == null) throw new ArgumentNullException(nameof(propertyName)); if (expression == null) throw new ArgumentNullException(nameof(expression)); var compiled = SerilogExpression.Compile(expression); return loggerEnrichmentConfiguration.With(new ComputedPropertyEnricher(propertyName, compiled)); } } ================================================ FILE: src/Serilog.Expressions/LoggerFilterConfigurationExtensions.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Configuration; using Serilog.Expressions; using Serilog.Expressions.Runtime; // ReSharper disable UnusedMember.Global namespace Serilog; /// /// Extends logger filter configuration with methods for filtering with expressions. /// public static class LoggerFilterConfigurationExtensions { /// /// Include only log events that match the provided expression. /// /// Filter configuration. /// The expression to apply. /// The underlying . public static LoggerConfiguration ByIncludingOnly(this LoggerFilterConfiguration loggerFilterConfiguration, string expression) { if (loggerFilterConfiguration == null) throw new ArgumentNullException(nameof(loggerFilterConfiguration)); if (expression == null) throw new ArgumentNullException(nameof(expression)); var compiled = SerilogExpression.Compile(expression); return loggerFilterConfiguration.ByIncludingOnly(e => Coerce.IsTrue(compiled(e))); } /// /// Exclude log events that match the provided expression. /// /// Filter configuration. /// The expression to apply. /// The underlying . public static LoggerConfiguration ByExcluding(this LoggerFilterConfiguration loggerFilterConfiguration, string expression) { if (loggerFilterConfiguration == null) throw new ArgumentNullException(nameof(loggerFilterConfiguration)); if (expression == null) throw new ArgumentNullException(nameof(expression)); var compiled = SerilogExpression.Compile(expression); return loggerFilterConfiguration.ByExcluding(e => Coerce.IsTrue(compiled(e))); } /// /// Use a to dynamically control filtering. /// /// Filter configuration. /// A that can be used to dynamically control /// log filtering. /// The underlying . public static LoggerConfiguration ControlledBy(this LoggerFilterConfiguration loggerFilterConfiguration, LoggingFilterSwitch @switch) { if (loggerFilterConfiguration == null) throw new ArgumentNullException(nameof(loggerFilterConfiguration)); if (@switch == null) throw new ArgumentNullException(nameof(@switch)); return loggerFilterConfiguration.With(@switch); } } ================================================ FILE: src/Serilog.Expressions/LoggerSinkConfigurationExtensions.cs ================================================ // Copyright 2019 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Configuration; using Serilog.Expressions; using Serilog.Expressions.Runtime; namespace Serilog; /// /// Extends logger sink configuration with methods for filtering with expressions. /// public static class LoggerSinkConfigurationExtensions { /// /// Write to a sink only when evaluates to true. /// /// Sink configuration. /// An expression that evaluates to true when the /// supplied /// should be written to the configured sink. /// An action that configures the wrapped sink. /// Configuration object allowing method chaining. /// The underlying . public static LoggerConfiguration Conditional( this LoggerSinkConfiguration loggerSinkConfiguration, string expression, Action configureSink) { if (loggerSinkConfiguration == null) throw new ArgumentNullException(nameof(loggerSinkConfiguration)); if (expression == null) throw new ArgumentNullException(nameof(expression)); if (configureSink == null) throw new ArgumentNullException(nameof(configureSink)); var compiled = SerilogExpression.Compile(expression); return loggerSinkConfiguration.Conditional(e => Coerce.IsTrue(compiled(e)), configureSink); } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Combinators.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Util; // ReSharper disable MemberCanBePrivate.Global namespace Serilog.ParserConstruction; /// /// Functions that construct more complex parsers by combining simpler ones. /// static class Combinators { /// /// Apply the text parser to the span represented by the parsed token. /// /// The kind of the tokens being parsed. /// The type of the resulting value. /// The parser. /// A text parser to apply. /// A parser that returns the result of parsing the token value. public static TokenListParser Apply(this TokenListParser> parser, TextParser valueParser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (valueParser == null) throw new ArgumentNullException(nameof(valueParser)); var valueParserAtEnd = valueParser.AtEnd(); return input => { var rt = parser(input); if (!rt.HasValue) return TokenListParserResult.CastEmpty, U>(rt); var uResult = valueParserAtEnd(rt.Value.Span); if (uResult.HasValue) return TokenListParserResult.Value(uResult.Value, rt.Location, rt.Remainder); var problem = uResult.Remainder.IsAtEnd ? "incomplete" : "invalid"; var textError = uResult.Remainder.IsAtEnd ? uResult.Expectations != null ? $", expected {Friendly.List(uResult.Expectations)}" : "" : $", {uResult.FormatErrorMessageFragment()}"; var message = $"{problem} {Presentation.FormatExpectation(rt.Value.Kind)}{textError}"; return new(input, rt.Remainder, uResult.Remainder.Position, message, null, uResult.Backtrack); }; } /// /// Construct a parser that succeeds only if the source is at the end of input. /// /// The type of value being parsed. /// The parser. /// The resulting parser. public static TextParser AtEnd(this TextParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var result = parser(input); if (!result.HasValue) return result; if (result.Remainder.IsAtEnd) return result; return Result.Empty(result.Remainder); }; } /// /// Construct a parser that succeeds only if the source is at the end of input. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The parser. /// The resulting parser. public static TokenListParser AtEnd(this TokenListParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var result = parser(input); if (!result.HasValue) return result; if (result.Remainder.IsAtEnd) return result; return TokenListParserResult.Empty(result.Remainder); }; } /// /// Construct a parser that matches one or more instances of applying . /// /// The type of value being parsed. /// The parser. /// The resulting parser. public static TextParser AtLeastOnce(this TextParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.Then(first => parser.Many().Select(rest => (T[])[first, ..rest])); } /// /// Construct a parser that matches one or more instances of applying , delimited by . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// The parser that matches the delimiters. /// The resulting parser. public static TokenListParser AtLeastOnceDelimitedBy(this TokenListParser parser, TokenListParser delimiter) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (delimiter == null) throw new ArgumentNullException(nameof(delimiter)); return parser.Then(first => delimiter.IgnoreThen(parser).Many().Select(rest => (T[])[first, ..rest])); } /// /// Construct a parser that matches , discards the resulting value, then returns the result of . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The first parser. /// The second parser. /// The resulting parser. public static TokenListParser IgnoreThen(this TokenListParser first, TokenListParser second) { if (first == null) throw new ArgumentNullException(nameof(first)); if (second == null) throw new ArgumentNullException(nameof(second)); return input => { var rt = first(input); if (!rt.HasValue) return TokenListParserResult.CastEmpty(rt); var ru = second(rt.Remainder); if (!ru.HasValue) return ru; return TokenListParserResult.Value(ru.Value, input, ru.Remainder); }; } /// /// Construct a parser that matches , discards the resulting value, then returns the result of . /// /// The type of value being parsed. /// The type of the resulting value. /// The first parser. /// The second parser. /// The resulting parser. public static TextParser IgnoreThen(this TextParser first, TextParser second) { if (first == null) throw new ArgumentNullException(nameof(first)); if (second == null) throw new ArgumentNullException(nameof(second)); return input => { var rt = first(input); if (!rt.HasValue) return Result.CastEmpty(rt); var ru = second(rt.Remainder); if (!ru.HasValue) return ru; return Result.Value(ru.Value, input, ru.Remainder); }; } /// /// Construct a parser that matches zero or more times. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The parser. /// The resulting parser. /// Many will fail if any item partially matches this. To modify this behavior use . public static TokenListParser Many(this TokenListParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var result = new List(); var from = input; var r = parser(input); while (r.HasValue) { if (from == r.Remainder) // Broken parser, not a failed parsing. throw new ParseException($"Many() cannot be applied to zero-width parsers; value {r.Value} at position {r.Location.Position}.", r.ErrorPosition); result.Add(r.Value); from = r.Remainder; r = parser(r.Remainder); } if (!r.Backtrack && r.IsPartial(from)) return TokenListParserResult.CastEmpty(r); return TokenListParserResult.Value(result.ToArray(), input, from); }; } /// /// Construct a parser that matches zero or more times. /// /// The type of value being parsed. /// The parser. /// The resulting parser. /// Many will fail if any item partially matches this. To modify this behavior use . public static TextParser Many(this TextParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var result = new List(); var from = input; var r = parser(input); while (r.HasValue) { if (from == r.Remainder) // Broken parser, not a failed parsing. throw new ParseException($"Many() cannot be applied to zero-width parsers; value {r.Value} at position {r.Location.Position}.", r.Location.Position); result.Add(r.Value); from = r.Remainder; r = parser(r.Remainder); } if (!r.Backtrack && r.IsPartial(from)) return Result.CastEmpty(r); return Result.Value(result.ToArray(), input, from); }; } /// /// Construct a parser that matches zero or more times, delimited by . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// The parser that matches the delimiters. /// A parser to match a final trailing delimiter, if required. Specifying /// this can improve error reporting for some lists. /// The resulting parser. public static TokenListParser ManyDelimitedBy( this TokenListParser parser, TokenListParser delimiter, TokenListParser? end = null) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (delimiter == null) throw new ArgumentNullException(nameof(delimiter)); // ReSharper disable once ConvertClosureToMethodGroup if (end != null) return parser .AtLeastOnceDelimitedBy(delimiter) .Then(p => end.Value(p)) .Or(end.Value(Array.Empty())); return parser .Then(first => delimiter.IgnoreThen(parser).Many().Select(rest => (T[])[first, ..rest])) .OptionalOrDefault([]); } /// /// Construct a parser that returns as its "expectation" if fails. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The parser. /// The name given to . /// The resulting parser. public static TokenListParser Named(this TokenListParser parser, string name) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (name == null) throw new ArgumentNullException(nameof(name)); return input => { var result = parser(input); if (result.HasValue || result.IsPartial(input)) return result; return TokenListParserResult.Empty(result.Remainder, [name]); }; } /// /// Construct a parser that returns as its "expectation" if fails. /// /// The type of value being parsed. /// The parser. /// The name given to . /// The resulting parser. public static TextParser Named(this TextParser parser, string name) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (name == null) throw new ArgumentNullException(nameof(name)); return input => { var result = parser(input); if (result.HasValue || result.IsPartial(input)) return result; return Result.Empty(result.Remainder, [name]); }; } /// /// Construct a parser that matches zero or one instance of , returning when /// no match is possible. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The parser. /// The default value /// The resulting parser. public static TokenListParser OptionalOrDefault(this TokenListParser parser, T defaultValue = default!) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.Or(Parse.Return(defaultValue!)); } /// /// Construct a parser that matches zero or one instance of , returning when /// no match is possible. /// /// The type of value being parsed. /// The parser. /// The default value. /// The resulting parser. public static TextParser OptionalOrDefault(this TextParser parser, T defaultValue = default!) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.Or(Parse.Return(defaultValue!)); } /// /// Construct a parser that tries first the parser, and if it fails, applies . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The first parser to try. /// The second parser to try. /// The resulting parser. /// Or will fail if the first item partially matches this. To modify this behavior use . public static TokenListParser Or(this TokenListParser lhs, TokenListParser rhs) { if (lhs == null) throw new ArgumentNullException(nameof(lhs)); if (rhs == null) throw new ArgumentNullException(nameof(rhs)); return input => { var first = lhs(input); if (first.HasValue || !first.Backtrack && first.IsPartial(input)) return first; var second = rhs(input); if (second.HasValue) return second; return TokenListParserResult.CombineEmpty(first, second); }; } /// /// Construct a parser that tries first the parser, and if it fails, applies . /// /// The type of value being parsed. /// The first parser to try. /// The second parser to try. /// The resulting parser. /// Or will fail if the first item partially matches this. To modify this behavior use . public static TextParser Or(this TextParser lhs, TextParser rhs) { if (lhs == null) throw new ArgumentNullException(nameof(lhs)); if (rhs == null) throw new ArgumentNullException(nameof(rhs)); return input => { var first = lhs(input); if (first.HasValue || !first.Backtrack && first.IsPartial(input)) return first; var second = rhs(input); if (second.HasValue) return second; return Result.CombineEmpty(first, second); }; } /// /// Construct a parser that takes the result of and converts it value using . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// A mapping from the first result to the second. /// The resulting parser. public static TokenListParser Select(this TokenListParser parser, Func selector) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (selector == null) throw new ArgumentNullException(nameof(selector)); return input => { var rt = parser(input); if (!rt.HasValue) return TokenListParserResult.CastEmpty(rt); var u = selector(rt.Value); return TokenListParserResult.Value(u, input, rt.Remainder); }; } /// /// Construct a parser that takes the result of and converts it value using . /// /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// A mapping from the first result to the second. /// The resulting parser. public static TextParser Select(this TextParser parser, Func selector) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (selector == null) throw new ArgumentNullException(nameof(selector)); return input => { var rt = parser(input); if (!rt.HasValue) return Result.CastEmpty(rt); var u = selector(rt.Value); return Result.Value(u, input, rt.Remainder); }; } /// /// Construct a parser that takes the result of and casts it to . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// The resulting parser. public static TokenListParser Cast(this TokenListParser parser) where T: U { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.Select(rt => (U)rt); } /// /// The LINQ query comprehension pattern. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// /// The parser. /// A mapping from the first result to the second parser. /// Function mapping the results of the first two parsers onto the final result. /// The resulting parser. public static TokenListParser SelectMany( this TokenListParser parser, Func> selector, Func projector) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (selector == null) throw new ArgumentNullException(nameof(selector)); if (projector == null) throw new ArgumentNullException(nameof(projector)); return parser.Then(t => selector(t).Select(u => projector(t, u))); } /// /// Construct a parser that applies , provides the value to and returns the result. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The first parser. /// The second parser. /// The resulting parser. public static TokenListParser Then(this TokenListParser first, Func> second) { if (first == null) throw new ArgumentNullException(nameof(first)); if (second == null) throw new ArgumentNullException(nameof(second)); return input => { var rt = first(input); if (!rt.HasValue) return TokenListParserResult.CastEmpty(rt); var ru = second(rt.Value)(rt.Remainder); if (!ru.HasValue) return ru; return TokenListParserResult.Value(ru.Value, input, ru.Remainder); }; } /// /// Construct a parser that applies , provides the value to and returns the result. /// /// The type of value being parsed. /// The type of the resulting value. /// The first parser. /// The second parser. /// The resulting parser. public static TextParser Then(this TextParser first, Func> second) { if (first == null) throw new ArgumentNullException(nameof(first)); if (second == null) throw new ArgumentNullException(nameof(second)); return input => { var rt = first(input); if (!rt.HasValue) return Result.CastEmpty(rt); var ru = second(rt.Value)(rt.Remainder); if (!ru.HasValue) return ru; return Result.Value(ru.Value, input, ru.Remainder); }; } /// /// Construct a parser that tries one parser, and backtracks if unsuccessful so that no input /// appears to have been consumed by subsequent checks against the result. /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The parser. /// The resulting parser. public static TokenListParser Try(this TokenListParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var rt = parser(input); if (rt.HasValue) return rt; rt.Backtrack = true; return rt; }; } /// /// Construct a parser that tries one parser, and backtracks if unsuccessful so that no input /// appears to have been consumed by subsequent checks against the result. /// /// The type of value being parsed. /// The parser. /// The resulting parser. public static TextParser Try(this TextParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var rt = parser(input); if (rt.HasValue) return rt; rt.Backtrack = true; return rt; }; } /// /// Construct a parser that applies the first, and returns . /// /// The kind of the tokens being parsed. /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// The value to return. /// The resulting parser. public static TokenListParser Value(this TokenListParser parser, U value) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.IgnoreThen(Parse.Return(value)); } /// /// Construct a parser that applies the first, and returns . /// /// The type of value being parsed. /// The type of the resulting value. /// The parser. /// The value to return. /// The resulting parser. public static TextParser Value(this TextParser parser, U value) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser.IgnoreThen(Parse.Return(value)); } /// /// Parse a sequence of operands connected by left-associative operators. /// /// The kind of the tokens being parsed. /// The type of the leftmost operand and of the ultimate result. /// The type of the operator. /// The type of subsequent operands. /// The parser for the leftmost operand. /// A parser matching operators. /// A parser matching operands. /// A function combining the operator, left operand, and right operand, into the result. /// The result of calling successively on pairs of operands. public static TokenListParser Chain( this TokenListParser parser, TokenListParser @operator, TokenListParser operand, Func apply) { if (parser == null) throw new ArgumentNullException(nameof(parser)); if (@operator == null) throw new ArgumentNullException(nameof(@operator)); if (operand == null) throw new ArgumentNullException(nameof(operand)); if (apply == null) throw new ArgumentNullException(nameof(apply)); return input => { var parseResult = parser(input); if (!parseResult.HasValue ) return parseResult; var result = parseResult.Value; var operandRemainder = parseResult.Remainder; var operatorResult = @operator(operandRemainder); while (operatorResult.HasValue || operatorResult.IsPartial(operandRemainder)) { // If operator read any input, but failed to read complete input, we return error if (!operatorResult.HasValue) return TokenListParserResult.CastEmpty(operatorResult); var operandResult = operand(operatorResult.Remainder); operandRemainder = operandResult.Remainder; if (!operandResult.HasValue) return TokenListParserResult.CastEmpty(operandResult); result = apply(operatorResult.Value, result, operandResult.Value); operatorResult = @operator(operandRemainder); } return TokenListParserResult.Value(result, input, operandRemainder); }; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Display/Presentation.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Reflection; using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction.Display; static class Presentation { static string FormatKind(object kind) { return kind.ToString()!.ToLower(); } static TokenAttribute? TryGetTokenAttribute(Type type) { return type.GetTypeInfo().GetCustomAttribute(); } static TokenAttribute? TryGetTokenAttribute(TKind kind) { var kindTypeInfo = typeof(TKind).GetTypeInfo(); if (kindTypeInfo.IsEnum) { var field = kindTypeInfo.GetDeclaredField(kind!.ToString()!); if (field != null) { return field.GetCustomAttribute() ?? TryGetTokenAttribute(typeof(TKind)); } } return TryGetTokenAttribute(typeof(TKind)); } public static string FormatExpectation(TKind kind) { var description = TryGetTokenAttribute(kind); if (description != null) { if (description.Description != null) return description.Description; if (description.Example != null) return FormatLiteral(description.Example); } return FormatKind(kind!); } public static string FormatAppearance(TKind kind, string value) { var clipped = FormatLiteral(Friendly.Clip(value, 12)); var description = TryGetTokenAttribute(kind); if (description != null) { if (description.Category != null) return $"{description.Category} {clipped}"; if (description.Example != null) return clipped; } return $"{FormatKind(kind!)} {clipped}"; } public static string FormatLiteral(char literal) { switch (literal) { //Unicode Category: Space Separators case '\x00A0': return "U+00A0 no-break space"; case '\x1680': return "U+1680 ogham space mark"; case '\x2000': return "U+2000 en quad"; case '\x2001': return "U+2001 em quad"; case '\x2002': return "U+2002 en space"; case '\x2003': return "U+2003 em space"; case '\x2004': return "U+2004 three-per-em space"; case '\x2005': return "U+2005 four-per-em space"; case '\x2006': return "U+2006 six-per-em space"; case '\x2007': return "U+2007 figure space"; case '\x2008': return "U+2008 punctuation space"; case '\x2009': return "U+2009 thin space"; case '\x200A': return "U+200A hair space"; case '\x202F': return "U+202F narrow no-break space"; case '\x205F': return "U+205F medium mathematical space"; case '\x3000': return "U+3000 ideographic space"; //Line Separator case '\x2028': return "U+2028 line separator"; //Paragraph Separator case '\x2029': return "U+2029 paragraph separator"; //Unicode C0 Control Codes (ASCII equivalent) case '\x0000': return "NUL"; //\0 case '\x0001': return "U+0001 start of heading"; case '\x0002': return "U+0002 start of text"; case '\x0003': return "U+0003 end of text"; case '\x0004': return "U+0004 end of transmission"; case '\x0005': return "U+0005 enquiry"; case '\x0006': return "U+0006 acknowledge"; case '\x0007': return "U+0007 bell"; case '\x0008': return "U+0008 backspace"; case '\x0009': return "tab"; //\t case '\x000A': return "line feed"; //\n case '\x000B': return "U+000B vertical tab"; case '\x000C': return "U+000C form feed"; case '\x000D': return "carriage return"; //\r case '\x000E': return "U+000E shift in"; case '\x000F': return "U+000F shift out"; case '\x0010': return "U+0010 data link escape"; case '\x0011': return "U+0011 device ctrl 1"; case '\x0012': return "U+0012 device ctrl 2"; case '\x0013': return "U+0013 device ctrl 3"; case '\x0014': return "U+0014 device ctrl 4"; case '\x0015': return "U+0015 not acknowledge"; case '\x0016': return "U+0016 synchronous idle"; case '\x0017': return "U+0017 end transmission block"; case '\x0018': return "U+0018 cancel"; case '\x0019': return "U+0019 end of medium"; case '\x0020': return "space"; case '\x001A': return "U+001A substitute"; case '\x001B': return "U+001B escape"; case '\x001C': return "U+001C file separator"; case '\x001D': return "U+001D group separator"; case '\x001E': return "U+001E record separator"; case '\x001F': return "U+001F unit separator"; case '\x007F': return "U+007F delete"; default: return "`" + literal + "`"; } } public static string FormatLiteral(string literal) { return "`" + literal + "`"; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Display/TokenAttribute.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ReSharper disable UnusedAutoPropertyAccessor.Global, ClassNeverInstantiated.Global namespace Serilog.ParserConstruction.Display; /// /// Applied to enum members representing tokens to control how they are rendered. /// [AttributeUsage(AttributeTargets.Field|AttributeTargets.Class)] class TokenAttribute : Attribute { /// /// The category of the token, e.g. "keyword" or "identifier". /// public string? Category { get; set; } /// /// For tokens that correspond to exact text, e.g. punctuation, the canonical /// example of how the token looks. /// public string? Example { get; set; } /// /// A description of the token, for example "regular expression". /// public string? Description { get; set; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/Position.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Model; /// /// A position within a stream of character input. /// readonly struct Position { /// /// The zero-based absolute index of the position. /// public int Absolute { get; } /// /// The one-based line number. /// public int Line { get; } /// /// The one-based column number. /// public int Column { get; } /// /// Construct a position. /// /// The absolute position. /// The line number. /// The column number. Position(int absolute, int line, int column) { #if CHECKED if (absolute < 0) throw new ArgumentOutOfRangeException(nameof(line), "Absolute positions start at 0."); if (line < 1) throw new ArgumentOutOfRangeException(nameof(line), "Line numbering starts at 1."); if (column < 1) throw new ArgumentOutOfRangeException(nameof(column), "Column numbering starts at 1."); #endif Absolute = absolute; Line = line; Column = column; } /// /// The position corresponding to the zero index. /// public static Position Zero { get; } = new(0, 1, 1); /// /// A position with no value. /// public static Position Empty => default; /// /// True if the position has a value. /// public bool HasValue => Line > 0; /// /// Advance over , advancing line and column numbers /// as appropriate. /// /// The character being advanced over. /// The updated position. public Position Advance(char overChar) { if (overChar == '\n') return new(Absolute + 1, Line + 1, 1); return new(Absolute + 1, Line, Column + 1); } /// public override string ToString() { return $"{Absolute} (line {Line}, column {Column})"; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/Result.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction.Model; /// /// Helper methods for working with . /// static class Result { /// /// An empty result indicating no value could be parsed. /// /// The result type. /// The start of un-parsed input. /// A result. public static Result Empty(TextSpan remainder) { return new(remainder, null, null, false); } /// /// An empty result indicating no value could be parsed. /// /// The result type. /// The start of un-parsed input. /// Literal descriptions of expectations not met. /// A result. public static Result Empty(TextSpan remainder, string[] expectations) { return new(remainder, null, expectations, false); } /// /// A result carrying a successfully-parsed value. /// /// The result type. /// The value. /// The location corresponding to the beginning of the parsed span. /// The start of un-parsed input. /// A result. public static Result Value(T value, TextSpan location, TextSpan remainder) { return new(value, location, remainder, false); } /// /// Convert an empty result of one type into another. /// /// The source type. /// The target type. /// The value to convert. /// A result of type carrying the same information as . public static Result CastEmpty(Result result) { return new(result.Remainder, result.ErrorMessage, result.Expectations, result.Backtrack); } /// /// Combine two empty results. /// /// The source type. /// The first value to combine. /// The second value to combine. /// A result of type carrying information from both results. public static Result CombineEmpty(Result first, Result second) { if (first.Remainder != second.Remainder) return second; var expectations = first.Expectations; if (expectations == null) expectations = second.Expectations; else if (second.Expectations != null) expectations = [..first.Expectations!, ..second.Expectations]; return new(second.Remainder, second.ErrorMessage, expectations, second.Backtrack); } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/Result`1.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction.Model; /// /// The result of parsing from a text span. /// /// The type of the value being parsed. struct Result { readonly T _value; /// /// If the result is a value, the location in the input corresponding to the /// value. If the result is an error, it's the location of the error. /// public TextSpan Location { get; } /// /// The first un-parsed location in the input. /// public TextSpan Remainder { get; } /// /// True if the result carries a successfully-parsed value; otherwise, false. /// public bool HasValue { get; } /// /// If the result is an error, the source-level position of the error; otherwise, . /// public Position ErrorPosition => HasValue ? Position.Empty : Location.Position; /// /// A provided error message, or null. /// public string? ErrorMessage { get; } /// /// A list of expectations that were unmet, or null. /// public string[]? Expectations { get; } internal bool IsPartial(TextSpan from) => from != Remainder; internal bool Backtrack { get; set; } /// /// The parsed value. /// public T Value { get { if (!HasValue) throw new InvalidOperationException($"{nameof(Result)} has no value."); return _value; } } internal Result(T value, TextSpan location, TextSpan remainder, bool backtrack) { Location = location; Remainder = remainder; _value = value; HasValue = true; ErrorMessage = null; Expectations = null; Backtrack = backtrack; } internal Result(TextSpan remainder, string? errorMessage, string[]? expectations, bool backtrack) { Location = Remainder = remainder; _value = default!; // Default value is not observable. HasValue = false; Expectations = expectations; ErrorMessage = errorMessage; Backtrack = backtrack; } /// public override string ToString() { if (Remainder == TextSpan.None) return "(Empty result.)"; if (HasValue) return $"Successful parsing of {Value}."; var message = FormatErrorMessageFragment(); var location = ""; if (!Location.IsAtEnd) { location = $" (line {Location.Position.Line}, column {Location.Position.Column})"; } return $"Syntax error{location}: {message}."; } /// /// If the result is empty, format the fragment of text describing the error. /// /// The error fragment. public string FormatErrorMessageFragment() { if (ErrorMessage != null) return ErrorMessage; string message; if (Location.IsAtEnd) { message = "unexpected end of input"; } else { var next = Location.ConsumeChar().Value; message = $"unexpected {Display.Presentation.FormatLiteral(next)}"; } if (Expectations != null) { var expected = Friendly.List(Expectations); message += $", expected {expected}"; } return message; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/TextSpan.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Model; /// /// A span of text within a larger string. /// readonly struct TextSpan : IEquatable { /// /// The source string containing the span. /// public string? Source { get; } /// /// The position of the start of the span within the string. /// public Position Position { get; } /// /// The length of the span. /// public int Length { get; } /// /// Construct a span encompassing an entire string. /// /// The source string. public TextSpan(string source) : this(source, Position.Zero, source.Length) { } /// /// Construct a string span for a substring of . /// /// The source string. /// The start of the span. /// The length of the span. public TextSpan(string source, Position position, int length) { #if CHECKED if (source == null) throw new ArgumentNullException(nameof(source)); if (length < 0) throw new ArgumentOutOfRangeException(nameof(length), "The length must be non-negative."); if (source.Length < position.Absolute + length) throw new ArgumentOutOfRangeException(nameof(length), "The token extends beyond the end of the input."); #endif Source = source; Position = position; Length = length; } /// /// A span with no value. /// public static TextSpan None => default; /// /// True if the span has no content. /// public bool IsAtEnd { get { EnsureHasValue(); return Length == 0; } } void EnsureHasValue() { if (Source == null) throw new InvalidOperationException("String span has no value."); } /// /// Consume a character from the start of the span. /// /// A result with the character and remainder. public Result ConsumeChar() { EnsureHasValue(); if (IsAtEnd) return Result.Empty(this); var ch = Source![Position.Absolute]; return Result.Value(ch, this, new(Source, Position.Advance(ch), Length - 1)); } /// public override bool Equals(object? obj) { if (obj is not TextSpan other) return false; return Equals(other); } /// public override int GetHashCode() { unchecked { return ((Source?.GetHashCode() ?? 0) * 397) ^ Position.Absolute; } } /// /// Compare a string span with another using source identity /// semantics - same source, same position, same length. /// /// The other span. /// True if the spans are the same. public bool Equals(TextSpan other) { return ReferenceEquals(Source, other.Source) && Position.Absolute == other.Position.Absolute && Length == other.Length; } /// /// Compare two spans using source identity semantics. /// /// One span. /// Another span. /// True if the spans are the same. public static bool operator ==(TextSpan lhs, TextSpan rhs) { return lhs.Equals(rhs); } /// /// Compare two spans using source identity semantics. /// /// One span. /// Another span. /// True if the spans are the different. public static bool operator !=(TextSpan lhs, TextSpan rhs) { return !(lhs == rhs); } /// /// Return a new span from the start of this span to the beginning of another. /// /// The next span. /// A sub-span. public TextSpan Until(TextSpan next) { #if CHECKED next.EnsureHasValue(); if (next.Source != Source) throw new ArgumentException("The spans are on different source strings.", nameof(next)); #endif var charCount = next.Position.Absolute - Position.Absolute; return First(charCount); } /// /// Return a span comprising the first characters of this span. /// /// The number of characters to return. /// The sub-span. TextSpan First(int length) { #if CHECKED if (length > Length) throw new ArgumentOutOfRangeException(nameof(length), "Length exceeds the source span's length."); #endif return new(Source!, Position, length); } /// public override string ToString() { if (Source == null) return "(empty source span)"; return ToStringValue(); } /// /// Compute the string value of this span. /// /// A string with the value of this span. public string ToStringValue() { EnsureHasValue(); return Source!.Substring(Position.Absolute, Length); } /// /// Compare the contents of this span with , ignoring invariant character case. /// /// The string value to compare. /// True if the values are the same ignoring case. public bool EqualsValueIgnoreCase(string otherValue) { if (otherValue == null) throw new ArgumentNullException(nameof(otherValue)); EnsureHasValue(); if (Length != otherValue.Length) return false; for (var i = 0; i < Length; ++i) { if (char.ToUpperInvariant(Source![Position.Absolute + i]) != char.ToUpperInvariant(otherValue[i])) return false; } return true; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/TokenListParserResult.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Model; /// /// Helper methods for working with . /// static class TokenListParserResult { /// /// Create a token result with no value, indicating a failure to parse any value. /// /// The kind of token. /// The result type. /// The start of un-parsed input. /// An empty result. public static TokenListParserResult Empty(TokenList remainder) { return new(remainder, Position.Empty, null, null, false); } /// /// Create a token result with no value, indicating a failure to parse any value. /// /// The kind of token. /// The result type. /// The start of un-parsed input. /// Expectations that could not be fulfilled. /// An empty result. public static TokenListParserResult Empty(TokenList remainder, string[] expectations) { return new(remainder, Position.Empty, null, expectations, false); } /// /// Create a token result with no value, indicating a failure to parse any value. /// /// The kind of token. /// The result type. /// The start of un-parsed input. /// An error message describing why the tokens could not be parsed. /// An empty result. public static TokenListParserResult Empty(TokenList remainder, string errorMessage) { return new(remainder, Position.Empty, errorMessage, null, false); } /// /// Create a token result with the provided value. /// /// The kind of token. /// The result type. /// The value. /// The location where parsing began. /// The first un-parsed location. /// public static TokenListParserResult Value(T value, TokenList location, TokenList remainder) { return new(value, location, remainder, false); } /// /// Convert an empty result of one type into another. /// /// The kind of token. /// The source type. /// The destination type. /// The result to convert. /// The converted result. public static TokenListParserResult CastEmpty(TokenListParserResult result) { return new(result.Remainder, result.SubTokenErrorPosition, result.ErrorMessage, result.Expectations, result.Backtrack); } /// /// Combine two empty results. /// /// The source type. /// The kind of token. /// The first value to combine. /// The second value to combine. /// A result of type carrying information from both results. public static TokenListParserResult CombineEmpty(TokenListParserResult first, TokenListParserResult second) { if (first.Remainder != second.Remainder) return second; var expectations = first.Expectations; if (expectations == null) expectations = second.Expectations; else if (second.Expectations != null) { expectations = new string[first.Expectations!.Length + second.Expectations.Length]; var i = 0; for (; i < first.Expectations!.Length; ++i) expectations[i] = first.Expectations![i]; for (var j = 0; j < second.Expectations.Length; ++i, ++j) expectations[i] = second.Expectations[j]; } return new(second.Remainder, second.SubTokenErrorPosition, first.ErrorMessage, expectations, second.Backtrack); } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/TokenListParserResult`2.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction.Model; /// /// The result of parsing from a token list. /// /// The type of the value being parsed. /// The kind of token being parsed. struct TokenListParserResult { readonly T _value; /// /// If the result has a value, this carries the location of the value in the token /// list. If the result is an error, it's the location of the error. /// public TokenList Location { get; } /// /// The first un-parsed location in the list. /// public TokenList Remainder { get; } /// /// True if the result carries a successfully-parsed value; otherwise, false. /// public bool HasValue { get; } /// /// If the result is an error, the source-level position of the error; otherwise, . /// public Position ErrorPosition { get { if (HasValue) return Position.Empty; if (SubTokenErrorPosition.HasValue) return SubTokenErrorPosition; if (!Remainder.IsAtEnd) return Remainder.ConsumeToken().Value.Position; return Location.ComputeEndOfInputPosition(); } } /// /// If the result is an error, the source-level position of the error; otherwise, . /// public Position SubTokenErrorPosition { get; } /// /// A provided error message, or null. /// public string? ErrorMessage { get; } /// /// A list of expectations that were unmet, or null. /// public string[]? Expectations { get; } /// /// The parsed value. /// public T Value { get { if (!HasValue) throw new InvalidOperationException($"{nameof(TokenListParserResult)} has no value."); return _value; } } internal bool IsPartial(TokenList from) => SubTokenErrorPosition.HasValue || from != Remainder; internal bool Backtrack { get; set; } internal TokenListParserResult(T value, TokenList location, TokenList remainder, bool backtrack) { Location = location; Remainder = remainder; _value = value; HasValue = true; SubTokenErrorPosition = Position.Empty; ErrorMessage = null; Expectations = null; Backtrack = backtrack; } internal TokenListParserResult(TokenList location, TokenList remainder, Position errorPosition, string? errorMessage, string[]? expectations, bool backtrack) { Location = location; Remainder = remainder; _value = default!; // Default value is not observable. HasValue = false; SubTokenErrorPosition = errorPosition; ErrorMessage = errorMessage; Expectations = expectations; Backtrack = backtrack; } internal TokenListParserResult(TokenList remainder, Position errorPosition, string? errorMessage, string[]? expectations, bool backtrack) { Location = Remainder = remainder; _value = default!; // Default value is not observable. HasValue = false; SubTokenErrorPosition = errorPosition; ErrorMessage = errorMessage; Expectations = expectations; Backtrack = backtrack; } /// public override string ToString() { if (Remainder == TokenList.Empty) return "(Empty result.)"; if (HasValue) return $"Successful parsing of {Value}."; var message = FormatErrorMessageFragment(); var location = ""; if (!Remainder.IsAtEnd) { // Since the message notes `end of input`, don't report line/column here. var sourcePosition = SubTokenErrorPosition.HasValue ? SubTokenErrorPosition : Remainder.ConsumeToken().Value.Position; location = $" (line {sourcePosition.Line}, column {sourcePosition.Column})"; } return $"Syntax error{location}: {message}."; } /// /// If the result is empty, format the fragment of text describing the error. /// /// The error fragment. string FormatErrorMessageFragment() { if (ErrorMessage != null) return ErrorMessage; string message; if (Remainder.IsAtEnd) { message = "unexpected end of input"; } else { var next = Remainder.ConsumeToken().Value; var appearance = Presentation.FormatAppearance(next.Kind, next.ToStringValue()); message = $"unexpected {appearance}"; } if (Expectations != null) { var expected = Friendly.List(Expectations); message += $", expected {expected}"; } return message; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/TokenList`1.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Collections; namespace Serilog.ParserConstruction.Model; /// /// A list of /// /// The kind of tokens held in the list. readonly struct TokenList : IEquatable>, IEnumerable> { readonly Token[]? _tokens; /// /// The position of the token list in the token stream. /// public int Position { get; } /// /// Construct a token list containing . /// /// The tokens in the list. public TokenList(Token[] tokens) : this(tokens, 0) { if (tokens == null) throw new ArgumentNullException(nameof(tokens)); } TokenList(Token[] tokens, int position) { #if CHECKED // Called on every advance or backtrack if (tokens == null) throw new ArgumentNullException(nameof(tokens)); if (position > tokens.Length) throw new ArgumentOutOfRangeException(nameof(position), "Position is past end + 1."); #endif Position = position; _tokens = tokens; } /// /// A token list with no value. /// public static TokenList Empty { get; } = default; /// /// True if the token list contains no tokens. /// public bool IsAtEnd { get { EnsureHasValue(); return Position == _tokens!.Length; } } void EnsureHasValue() { if (_tokens == null) throw new InvalidOperationException("Token list has no value."); } /// /// Consume a token from the start of the list, returning a result with the token and remainder. /// /// public TokenListParserResult> ConsumeToken() { EnsureHasValue(); if (IsAtEnd) return TokenListParserResult.Empty>(this); var token = _tokens![Position]; return TokenListParserResult.Value(token, this, new(_tokens, Position + 1)); } /// public IEnumerator> GetEnumerator() { EnsureHasValue(); for (var position = Position; position < _tokens!.Length; ++position) yield return _tokens[position]; } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } /// public override bool Equals(object? obj) { if (obj is not TokenList other) return false; return Equals(other); } /// public override int GetHashCode() { unchecked { return ((_tokens?.GetHashCode() ?? 0) * 397) ^ Position; } } /// /// Compare two token lists using identity semantics - same list, same position. /// /// The other token list. /// True if the token lists are the same. public bool Equals(TokenList other) { return Equals(_tokens, other._tokens) && Position == other.Position; } /// /// Compare two token lists using identity semantics. /// /// The first token list. /// The second token list. /// True if the token lists are the same. public static bool operator ==(TokenList lhs, TokenList rhs) { return lhs.Equals(rhs); } /// /// Compare two token lists using identity semantics. /// /// The first token list. /// The second token list. /// True if the token lists are the different. public static bool operator !=(TokenList lhs, TokenList rhs) { return !(lhs == rhs); } /// public override string ToString() { if (_tokens == null) return "Token list (empty)"; return "Token list"; } // A mildly expensive way to find the "end of input" position for error reporting. internal Position ComputeEndOfInputPosition() { EnsureHasValue(); if (_tokens!.Length == 0) return Model.Position.Zero; var lastSpan = _tokens[_tokens.Length - 1].Span; var source = lastSpan.Source; var position = lastSpan.Position; for (var i = position.Absolute; i < source!.Length; ++i) position = position.Advance(source[i]); return position; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/Token`1.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Model; /// /// A token. /// /// The type of the token's kind. readonly struct Token { /// /// The kind of the token. /// public TKind Kind { get; } /// /// The string span containing the value of the token. /// public TextSpan Span { get; } /// /// Get the string value of the token. /// /// The token as a string. public string ToStringValue() => Span.ToStringValue(); /// /// The position of the token within the source string. /// public Position Position => Span.Position; /// /// True if the token has a value. /// bool HasValue => Span != TextSpan.None; /// /// Construct a token. /// /// The kind of the token. /// The span holding the token's value. public Token(TKind kind, TextSpan span) { Kind = kind; Span = span; } /// public override string ToString() { if (!HasValue) return "(empty token)"; return $"{Kind}@{Position}: {Span}"; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Model/Unit.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Model; /// /// A structure with no information. /// struct Unit { /// /// The singleton value of the struct, with no value. /// public static Unit Value => default; } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Parse.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction; /// /// General parsing helper methods. /// static class Parse { /// /// Parse a sequence of similar operands connected by left-associative operators. /// /// The type being parsed. /// The type of the operator. /// The kind of token being parsed. /// A parser matching operators. /// A parser matching operands. /// A function combining an operator and two operands into the result. /// The result of calling successively on pairs of operands. /// public static TokenListParser Chain( TokenListParser @operator, TokenListParser operand, Func apply) { return operand.Chain(@operator, operand, apply); } /// /// Constructs a parser that will fail if the given parser succeeds, /// and will succeed if the given parser fails. In any case, it won't /// consume any input. It's like a negative look-ahead in a regular expression. /// /// The result type of the given parser. /// The kind of token being parsed. /// The parser to wrap /// A parser that is the negation of the given parser. public static TokenListParser Not(TokenListParser parser) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return input => { var result = parser(input); if (result.HasValue) { // This is usually a success case for Not(), so the allocations here are a bit of a pity. var current = input.ConsumeToken(); var last = result.Remainder.ConsumeToken(); if (current.HasValue) { var span = last.HasValue ? current.Value.Span.Source!.Substring(current.Value.Position.Absolute, last.Value.Position.Absolute - current.Value.Position.Absolute) : current.Value.Span.Source!.Substring(current.Value.Position.Absolute); return TokenListParserResult.Empty(input, $"unexpected successful parsing of {Presentation.FormatLiteral(Friendly.Clip(span, 12))}"); } return TokenListParserResult.Empty(input, "unexpected successful parsing"); } return TokenListParserResult.Value(Unit.Value, input, input); }; } /// /// Lazily construct a parser, so that circular dependencies are possible. /// /// A function creating the parser, when required. /// The type of value being parsed. /// The kind of token being parsed. /// A parser that lazily evaluates . /// is null. public static TokenListParser Ref(Func> reference) { if (reference == null) throw new ArgumentNullException(nameof(reference)); TokenListParser? parser = null; return i => { parser ??= reference(); return parser(i); }; } /// /// Construct a parser with a fixed value. /// /// The value returned by the parser. /// The type of . /// The parser. public static TextParser Return(T value) { return input => Result.Value(value, input, input); } /// /// Construct a parser with a fixed value. /// /// The value returned by the parser. /// The type of . /// The kind of token being parsed. /// The parser. public static TokenListParser Return(T value) { return input => TokenListParserResult.Value(value, input, input); } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/ParseException.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Model; // ReSharper disable IntroduceOptionalParameters.Global, MemberCanBePrivate.Global, UnusedAutoPropertyAccessor.Global namespace Serilog.ParserConstruction; /// /// Represents an error that occurs during parsing. /// class ParseException : Exception { /// /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. /// The position of the error in the input text. public ParseException(string message, Position errorPosition) : this(message, errorPosition, null) { } /// /// Initializes a new instance of the class with a specified error message. /// /// The message that describes the error. /// The position of the error in the input text. /// The exception that is the cause of the current exception. public ParseException(string message, Position errorPosition, Exception? innerException) : base(message, innerException) { ErrorPosition = errorPosition; } /// /// The position of the error in the input text, or if no position is specified. /// public Position ErrorPosition { get; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/ParserExtensions.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction; /// /// Helper methods for working with parsers. /// static class ParserExtensions { /// /// Tries to parse the input without throwing an exception upon failure. /// /// The type of tokens consumed by the parser. /// The type of the result. /// The parser. /// The input. /// The result of the parser /// The parser or input is null. public static TokenListParserResult TryParse(this TokenListParser parser, TokenList input) { if (parser == null) throw new ArgumentNullException(nameof(parser)); return parser(input); } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Parsers/Character.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction.Parsers; /// /// Parsers for matching individual characters. /// static class Character { static TextParser Matching(Func predicate, string[] expectations) { if (predicate == null) throw new ArgumentNullException(nameof(predicate)); if (expectations == null) throw new ArgumentNullException(nameof(expectations)); return input => { var next = input.ConsumeChar(); if (!next.HasValue || !predicate(next.Value)) return Result.Empty(input, expectations); return next; }; } /// /// Parse a single character matching . /// public static TextParser Matching(Func predicate, string name) { if (predicate == null) throw new ArgumentNullException(nameof(predicate)); if (name == null) throw new ArgumentNullException(nameof(name)); return Matching(predicate, [name]); } /// /// Parse a single character except those matching . /// /// Characters not to match. /// Description of characters that don't match. /// A parser for characters except those matching . static TextParser Except(Func predicate, string description) { if (predicate == null) throw new ArgumentNullException(nameof(predicate)); if (description == null) throw new ArgumentNullException(nameof(description)); return Matching(c => !predicate(c), "any character except " + description); } /// /// Parse a single specified character. /// public static TextParser EqualTo(char ch) { return Matching(parsed => parsed == ch, Presentation.FormatLiteral(ch)); } /// /// Parse a single character except . /// public static TextParser Except(char ch) { return Except(parsed => parsed == ch, Presentation.FormatLiteral(ch)); } /// /// Parse a digit. /// public static TextParser Digit { get; } = Matching(char.IsDigit, "digit"); } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Parsers/Numerics.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Util; namespace Serilog.ParserConstruction.Parsers; /// /// Parsers for numeric patterns. /// //* Fairly large amount of duplication/repetition here, due to the lack //* of generics over numbers in C#. static class Numerics { static readonly string[] ExpectedDigit = ["digit"]; static readonly string[] ExpectedSignOrDigit = ["sign", "digit"]; /// /// A string of digits, converted into a . /// public static TextParser NaturalUInt32 { get; } = input => { var next = input.ConsumeChar(); if (!next.HasValue || !CharInfo.IsLatinDigit(next.Value)) return Result.Empty(input, ExpectedDigit); TextSpan remainder; var val = 0u; do { val = 10 * val + (uint)(next.Value - '0'); remainder = next.Remainder; next = remainder.ConsumeChar(); } while (next.HasValue && CharInfo.IsLatinDigit(next.Value)); return Result.Value(val, input, remainder); }; /// /// A string of digits with an optional +/- sign. /// public static TextParser Integer { get; } = input => { var next = input.ConsumeChar(); if (!next.HasValue) return Result.Empty(input, ExpectedSignOrDigit); if (next.Value is '-' or '+') next = next.Remainder.ConsumeChar(); if (!next.HasValue || !CharInfo.IsLatinDigit(next.Value)) return Result.Empty(input, ExpectedDigit); TextSpan remainder; do { remainder = next.Remainder; next = remainder.ConsumeChar(); } while (next.HasValue && CharInfo.IsLatinDigit(next.Value)); return Result.Value(input.Until(remainder), input, remainder); }; } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Parsers/Span.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction.Parsers; /// /// Parsers for spans of characters. /// static class Span { /// /// Match a span equal to . /// /// The text to match. /// The matched text. public static TextParser EqualTo(string text) { if (text == null) throw new ArgumentNullException(nameof(text)); var expectations = new[] { Presentation.FormatLiteral(text) }; return input => { var remainder = input; // ReSharper disable once ForCanBeConvertedToForeach for (var i = 0; i < text.Length; ++i) { var ch = remainder.ConsumeChar(); if (!ch.HasValue || ch.Value != text[i]) { if (ch.Location == input) return Result.Empty(ch.Location, expectations); return Result.Empty(ch.Location, [Presentation.FormatLiteral(text[i])]); } remainder = ch.Remainder; } return Result.Value(input.Until(remainder), input, remainder); }; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Parsers/Token.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction.Parsers; /// /// Parsers for matching individual tokens. /// static class Token { /// /// Parse a token of the kind . /// /// The type of the token being matched. /// The kind of token to match. /// The matched token. // ReSharper disable once MemberCanBePrivate.Global public static TokenListParser> EqualTo(TKind kind) { var expectations = new[] { Presentation.FormatExpectation(kind) }; return input => { var next = input.ConsumeToken(); if (!next.HasValue || !next.Value.Kind!.Equals(kind)) return TokenListParserResult.Empty>(input, expectations); return next; }; } /// /// Parse a sequence of tokens of the kind . /// /// The type of the tokens being matched. /// The kinds of token to match, once each in order. /// The matched tokens. public static TokenListParser[]> Sequence(params TKind[] kinds) { if (kinds == null) throw new ArgumentNullException(nameof(kinds)); TokenListParser[]> result = input => TokenListParserResult.Value(new Token[kinds.Length], input, input); for (var i = 0; i < kinds.Length; ++i) { var token = EqualTo(kinds[i]); var index = i; result = result.Then(arr => token.Select(t => { arr[index] = t; return arr; })); } return result; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/README.md ================================================ # Superpower The classes in this namespace are from [Superpower](https://github.com/datalust/superpower). They are included here because _Serilog.Expressions_ aims to eventually be integrated at a lower level in the Serilog ecosystem, where being dependency-free is advantageous. _Serilog.Expressions_ only uses a small slice of Superpower; if something you need appears to be missing, it's probably been shaken out and can be retrieved again from the original codebase for inclusion. The Superpower tests have not been brought across; as only code used in _Serilog.Expressions_ remains here, we should be able to achieve good test coverage by testing the expression parser implementation. ## License Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "{}" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright {yyyy} {name of copyright owner} Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: src/Serilog.Expressions/ParserConstruction/TextParser`1.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction; /// /// A parser that consumes text from a string span. /// /// The type of values produced by the parser. /// The span of text to parse. /// A result with a parsed value, or an empty result indicating error. delegate Result TextParser(TextSpan input); ================================================ FILE: src/Serilog.Expressions/ParserConstruction/TokenListParser`2.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction; /// /// A parser that consumes elements from a list of tokens. /// /// The type of values produced by the parser. /// The type of tokens being parsed. /// The list of tokens to parse. /// A result with a parsed value, or an empty result indicating error. delegate TokenListParserResult TokenListParser(TokenList input); ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Tokenizer`1.cs ================================================ // Copyright 2016-2018 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.ParserConstruction.Display; using Serilog.ParserConstruction.Model; namespace Serilog.ParserConstruction; /// /// Base class for tokenizers, types whose instances convert strings into lists of tokens. /// /// The kind of tokens produced. abstract class Tokenizer { /// /// Tokenize . /// /// The source to tokenize. /// The list of tokens or an error. /// Tokenization failed. public TokenList Tokenize(string source) { var result = TryTokenize(source); if (result.HasValue) return result.Value; throw new ParseException(result.ToString(), result.ErrorPosition); } /// /// Tokenize . /// /// The source to tokenize. /// A result with the list of tokens or an error. /// is null. /// The tokenizer could not correctly perform tokenization. public Result> TryTokenize(string source) { if (source == null) throw new ArgumentNullException(nameof(source)); var sourceSpan = new TextSpan(source); var remainder = sourceSpan; var results = new List>(); foreach (var result in Tokenize(sourceSpan)) { if (!result.HasValue) return Result.CastEmpty>(result); if (result.Remainder == remainder) // Broken parser, not a failed parsing. throw new ParseException($"Zero-width tokens are not supported; token {Presentation.FormatExpectation(result.Value)} at position {result.Location.Position}.", result.Location.Position); remainder = result.Remainder; var token = new Token(result.Value, result.Location.Until(result.Remainder)); results.Add(token); } var value = new TokenList(results.ToArray()); return Result.Value(value, sourceSpan, remainder); } /// /// Subclasses should override to perform tokenization. /// /// The input span to tokenize. /// A list of parsed tokens. protected abstract IEnumerable> Tokenize(TextSpan span); /// /// Advance until the first non-whitespace character is encountered. /// /// The span to advance from. /// A result with the first non-whitespace character. protected static Result SkipWhiteSpace(TextSpan span) { var next = span.ConsumeChar(); while (next.HasValue && char.IsWhiteSpace(next.Value)) { next = next.Remainder.ConsumeChar(); } return next; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Util/CharInfo.cs ================================================ // Copyright 2018 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Util; static class CharInfo { public static bool IsLatinDigit(char ch) { return ch is >= '0' and <= '9'; } } ================================================ FILE: src/Serilog.Expressions/ParserConstruction/Util/Friendly.cs ================================================ // Copyright 2016 Datalust, Superpower Contributors, Sprache Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.ParserConstruction.Util; static class Friendly { public static string List(IEnumerable items) { if (items == null) throw new ArgumentNullException(nameof(items)); // Keep the order stable var seen = new HashSet(); var unique = new List(); foreach (var item in items) { if (seen.Contains(item)) continue; seen.Add(item); unique.Add(item); } if (unique.Count == 0) throw new ArgumentException("Friendly list formatting requires at least one element.", nameof(items)); if (unique.Count == 1) return unique.Single(); return $"{string.Join(", ", unique.Take(unique.Count - 1))} or {unique.Last()}"; } public static string Clip(string value, int maxLength) { if (value.Length > maxLength) return value.Substring(0, maxLength - 3) + "..."; return value; } } ================================================ FILE: src/Serilog.Expressions/Pipeline/ComputedPropertyEnricher.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Core; using Serilog.Events; using Serilog.Expressions; namespace Serilog.Pipeline; class ComputedPropertyEnricher : ILogEventEnricher { readonly string _propertyName; readonly CompiledExpression _computeValue; public ComputedPropertyEnricher(string propertyName, CompiledExpression computeValue) { _propertyName = propertyName ?? throw new ArgumentNullException(nameof(propertyName)); _computeValue = computeValue ?? throw new ArgumentNullException(nameof(computeValue)); } public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { var value = _computeValue(logEvent); if (value != null) logEvent.AddOrUpdateProperty(new(_propertyName, value)); } } ================================================ FILE: src/Serilog.Expressions/Properties/AssemblyInfo.cs ================================================ using System.Runtime.CompilerServices; [assembly: CLSCompliant(true)] [assembly: InternalsVisibleTo("Serilog.Expressions.Tests, PublicKey=" + "0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c" + "6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9" + "d18dbf6d5a25af5ce9016f281014d79dc3b4201ac646c451830fc7e61a2dfd633d34c39f87b818" + "94191652df5ac63cc40c77f3542f702bda692e6e8a9158353df189007a49da0f3cfd55eb250066" + "b19485ec")] ================================================ FILE: src/Serilog.Expressions/Serilog.Expressions.csproj ================================================ An embeddable mini-language for filtering, enriching, and formatting Serilog events, ideal for use with JSON or XML configuration. Serilog Contributors net471;net462 $(TargetFrameworks);net9.0;net8.0;net6.0;netstandard2.0 serilog https://github.com/serilog/serilog-expressions icon.png Apache-2.0 Serilog README.md $(DefineConstants);NO_CI_STRING_CONTAINS $(DefineConstants);NO_CI_STRING_CONTAINS $(DefineConstants);NO_CI_STRING_CONTAINS ================================================ FILE: src/Serilog.Expressions/Templates/Ast/Conditional.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; namespace Serilog.Templates.Ast; class Conditional : Template { public Expression Condition { get; } public Template Consequent { get; } public Template? Alternative { get; } public Conditional(Expression condition, Template consequent, Template? alternative) { Condition = condition ?? throw new ArgumentNullException(nameof(condition)); Consequent = consequent ?? throw new ArgumentNullException(nameof(consequent)); Alternative = alternative; } } ================================================ FILE: src/Serilog.Expressions/Templates/Ast/FormattedExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Parsing; namespace Serilog.Templates.Ast; class FormattedExpression : Template { public Expression Expression { get; } public string? Format { get; } public Alignment? Alignment { get; } public FormattedExpression(Expression expression, string? format, Alignment? alignment) { Expression = expression ?? throw new ArgumentNullException(nameof(expression)); Format = format; Alignment = alignment; } } ================================================ FILE: src/Serilog.Expressions/Templates/Ast/LiteralText.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Ast; class LiteralText : Template { public string Text { get; } public LiteralText(string text) { Text = text ?? throw new ArgumentNullException(nameof(text)); } } ================================================ FILE: src/Serilog.Expressions/Templates/Ast/Repetition.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; namespace Serilog.Templates.Ast; class Repetition: Template { public Expression Enumerable { get; } public string[] BindingNames { get; } public Template Body { get; } public Template? Delimiter { get; } public Template? Alternative { get; } public Repetition( Expression enumerable, string[] bindingNames, Template body, Template? delimiter, Template? alternative) { Enumerable = enumerable ?? throw new ArgumentNullException(nameof(enumerable)); BindingNames = bindingNames; Body = body ?? throw new ArgumentNullException(nameof(body)); Delimiter = delimiter; Alternative = alternative; } } ================================================ FILE: src/Serilog.Expressions/Templates/Ast/Template.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Ast; abstract class Template; ================================================ FILE: src/Serilog.Expressions/Templates/Ast/TemplateBlock.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Ast; class TemplateBlock : Template { public Template[] Elements { get; } public TemplateBlock(Template[] elements) { Elements = elements ?? throw new ArgumentNullException(nameof(elements)); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledConditional.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; namespace Serilog.Templates.Compilation; class CompiledConditional : CompiledTemplate { readonly Evaluatable _condition; readonly CompiledTemplate _consequent; readonly CompiledTemplate? _alternative; public CompiledConditional(Evaluatable condition, CompiledTemplate consequent, CompiledTemplate? alternative) { _condition = condition ?? throw new ArgumentNullException(nameof(condition)); _consequent = consequent ?? throw new ArgumentNullException(nameof(consequent)); _alternative = alternative; } public override void Evaluate(EvaluationContext ctx, TextWriter output) { if (ExpressionResult.IsTrue(_condition.Invoke(ctx))) _consequent.Evaluate(ctx, output); else _alternative?.Evaluate(ctx, output); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledExceptionToken.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledExceptionToken : CompiledTemplate { const string StackFrameLinePrefix = " "; readonly Style _text, _secondaryText; public CompiledExceptionToken(TemplateTheme theme) { _text = theme.GetStyle(TemplateThemeStyle.Text); _secondaryText = theme.GetStyle(TemplateThemeStyle.SecondaryText); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { // Padding and alignment are not applied by this renderer. if (ctx.LogEvent.Exception is null) return; StringReader lines; try { lines = new StringReader(ctx.LogEvent.Exception.ToString()); } catch (Exception ex) { lines = new StringReader( $"[Exception.ToString() failed: {ex.Message}] Original exception type: {ctx.LogEvent.Exception.GetType().FullName}{Environment.NewLine}"); } while (lines.ReadLine() is { } nextLine) { var style = nextLine.StartsWith(StackFrameLinePrefix) ? _secondaryText : _text; var _ = 0; using (style.Set(output, ref _)) output.WriteLine(nextLine); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledFormattedExpression.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions; using Serilog.Parsing; using Serilog.Templates.Rendering; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledFormattedExpression : CompiledTemplate { readonly ThemedJsonValueFormatter _jsonFormatter; readonly Evaluatable _expression; readonly string? _format; readonly Alignment? _alignment; readonly IFormatProvider? _formatProvider; readonly Style _secondaryText; public CompiledFormattedExpression(Evaluatable expression, string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme) { _expression = expression ?? throw new ArgumentNullException(nameof(expression)); _format = format; _alignment = alignment; _formatProvider = formatProvider; _secondaryText = theme.GetStyle(TemplateThemeStyle.SecondaryText); _jsonFormatter = new(theme); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var invisibleCharacterCount = 0; if (_alignment == null) { EvaluateUnaligned(ctx, output, _formatProvider, ref invisibleCharacterCount); } else { var writer = new StringWriter(); EvaluateUnaligned(ctx, writer, _formatProvider, ref invisibleCharacterCount); Padding.Apply(output, writer.ToString(), _alignment.Value.Widen(invisibleCharacterCount)); } } void EvaluateUnaligned(EvaluationContext ctx, TextWriter output, IFormatProvider? formatProvider, ref int invisibleCharacterCount) { var value = _expression(ctx); if (value == null) return; // Undefined is empty if (value is ScalarValue scalar) { if (scalar.Value is null) return; // Null is empty using var style = _secondaryText.Set(output, ref invisibleCharacterCount); if (scalar.Value is IFormattable fmt) output.Write(fmt.ToString(_format, formatProvider)); else output.Write(scalar.Value.ToString()); } else { invisibleCharacterCount += _jsonFormatter.Format(value, output); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledLevelToken.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Parsing; using Serilog.Templates.Rendering; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledLevelToken : CompiledTemplate { readonly string? _format; readonly Alignment? _alignment; readonly Style[] _levelStyles; public CompiledLevelToken(string? format, Alignment? alignment, TemplateTheme theme) { _format = format; _alignment = alignment; _levelStyles = [ theme.GetStyle(TemplateThemeStyle.LevelVerbose), theme.GetStyle(TemplateThemeStyle.LevelDebug), theme.GetStyle(TemplateThemeStyle.LevelInformation), theme.GetStyle(TemplateThemeStyle.LevelWarning), theme.GetStyle(TemplateThemeStyle.LevelError), theme.GetStyle(TemplateThemeStyle.LevelFatal) ]; } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var invisibleCharacterCount = 0; if (_alignment == null) { EvaluateUnaligned(ctx, output, ref invisibleCharacterCount); } else { var writer = new StringWriter(); EvaluateUnaligned(ctx, writer, ref invisibleCharacterCount); Padding.Apply(output, writer.ToString(), _alignment.Value.Widen(invisibleCharacterCount)); } } void EvaluateUnaligned(EvaluationContext ctx, TextWriter output, ref int invisibleCharacterCount) { var levelIndex = (int) ctx.LogEvent.Level; if (levelIndex < 0 || levelIndex >= _levelStyles.Length) return; using var _ = _levelStyles[levelIndex].Set(output, ref invisibleCharacterCount); output.Write(LevelRenderer.GetLevelMoniker(ctx.LogEvent.Level, _format)); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledLiteralText.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledLiteralText : CompiledTemplate { readonly string _text; readonly Style _style; public CompiledLiteralText(string text, TemplateTheme theme) { _text = text ?? throw new ArgumentNullException(nameof(text)); _style = theme.GetStyle(TemplateThemeStyle.TertiaryText); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var _ = 0; using (_style.Set(output, ref _)) output.Write(_text); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledMessageToken.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions; using Serilog.Parsing; using Serilog.Templates.Rendering; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledMessageToken : CompiledTemplate { readonly IFormatProvider? _formatProvider; readonly Alignment? _alignment; readonly Style _text, _invalid, _null, _bool, _string, _num, _scalar; readonly ThemedJsonValueFormatter _jsonFormatter; public CompiledMessageToken(IFormatProvider? formatProvider, Alignment? alignment, TemplateTheme theme) { _formatProvider = formatProvider; _alignment = alignment; _text = theme.GetStyle(TemplateThemeStyle.Text); _null = theme.GetStyle(TemplateThemeStyle.Null); _bool = theme.GetStyle(TemplateThemeStyle.Boolean); _num = theme.GetStyle(TemplateThemeStyle.Number); _string = theme.GetStyle(TemplateThemeStyle.String); _scalar = theme.GetStyle(TemplateThemeStyle.Scalar); _invalid = theme.GetStyle(TemplateThemeStyle.Invalid); _jsonFormatter = new(theme); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var invisibleCharacterCount = 0; if (_alignment == null) { EvaluateUnaligned(ctx, output, ref invisibleCharacterCount); } else { var writer = new StringWriter(); EvaluateUnaligned(ctx, writer, ref invisibleCharacterCount); Padding.Apply(output, writer.ToString(), _alignment.Value.Widen(invisibleCharacterCount)); } } void EvaluateUnaligned(EvaluationContext ctx, TextWriter output, ref int invisibleCharacterCount) { foreach (var token in ctx.LogEvent.MessageTemplate.Tokens) { switch (token) { case TextToken tt: { using var _ = _text.Set(output, ref invisibleCharacterCount); output.Write(tt.Text); break; } case PropertyToken pt: { EvaluateProperty(ctx.LogEvent.Properties, pt, output, ref invisibleCharacterCount); break; } default: { output.Write(token); break; } } } } void EvaluateProperty(IReadOnlyDictionary properties, PropertyToken pt, TextWriter output, ref int invisibleCharacterCount) { if (!properties.TryGetValue(pt.PropertyName, out var value)) { using var _ = _invalid.Set(output, ref invisibleCharacterCount); output.Write(pt.ToString()); return; } if (pt.Alignment is null) { EvaluatePropertyUnaligned(value, output, pt.Format, ref invisibleCharacterCount); return; } var buffer = new StringWriter(); var resultInvisibleCharacters = 0; EvaluatePropertyUnaligned(value, buffer, pt.Format, ref resultInvisibleCharacters); var result = buffer.ToString(); invisibleCharacterCount += resultInvisibleCharacters; if (result.Length - resultInvisibleCharacters >= pt.Alignment.Value.Width) output.Write(result); else Padding.Apply(output, result, pt.Alignment.Value.Widen(resultInvisibleCharacters)); } void EvaluatePropertyUnaligned(LogEventPropertyValue propertyValue, TextWriter output, string? format, ref int invisibleCharacterCount) { if (propertyValue is not ScalarValue scalar) { invisibleCharacterCount += _jsonFormatter.Format(propertyValue, output); return; } var value = scalar.Value; if (value == null) { using (_null.Set(output, ref invisibleCharacterCount)) output.Write("null"); return; } if (value is string str) { using (_string.Set(output, ref invisibleCharacterCount)) output.Write(str); return; } if (value is ValueType) { if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort) { using (_num.Set(output, ref invisibleCharacterCount)) output.Write(((IFormattable)value).ToString(format, _formatProvider)); return; } if (value is double d) { using (_num.Set(output, ref invisibleCharacterCount)) output.Write(d.ToString(format, _formatProvider)); return; } if (value is float f) { using (_num.Set(output, ref invisibleCharacterCount)) output.Write(f.ToString(format, _formatProvider)); return; } if (value is bool b) { using (_bool.Set(output, ref invisibleCharacterCount)) output.Write(b); return; } } if (value is IFormattable formattable) { using (_scalar.Set(output, ref invisibleCharacterCount)) output.Write(formattable.ToString(format, _formatProvider)); return; } using (_scalar.Set(output, ref invisibleCharacterCount)) output.Write(value); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledRepetition.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; using Serilog.Expressions; using Serilog.Expressions.Runtime; namespace Serilog.Templates.Compilation; class CompiledRepetition : CompiledTemplate { readonly Evaluatable _enumerable; readonly string? _keyOrElementName; readonly string? _valueOrIndexName; readonly CompiledTemplate _body; readonly CompiledTemplate? _delimiter; readonly CompiledTemplate? _alternative; public CompiledRepetition( Evaluatable enumerable, string? keyOrElementName, string? valueOrIndexName, CompiledTemplate body, CompiledTemplate? delimiter, CompiledTemplate? alternative) { _enumerable = enumerable; _keyOrElementName = keyOrElementName; _valueOrIndexName = valueOrIndexName; _body = body; _delimiter = delimiter; _alternative = alternative; } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var enumerable = _enumerable(ctx); if (enumerable is null or ScalarValue) { _alternative?.Evaluate(ctx, output); return; } if (enumerable is SequenceValue sequence) { if (sequence.Elements.Count == 0) { _alternative?.Evaluate(ctx, output); return; } for (var i = 0; i < sequence.Elements.Count; ++i) { // Null elements should have been invalid but Serilog didn't check, and so this does occur in the wild. var element = sequence.Elements[i] ?? new ScalarValue(null); if (i != 0) { _delimiter?.Evaluate(ctx, output); } var local = _keyOrElementName != null ? new EvaluationContext(ctx.LogEvent, Locals.Set(ctx.Locals, _keyOrElementName, element)) : ctx; local = _valueOrIndexName != null ? new EvaluationContext(local.LogEvent, Locals.Set(local.Locals, _valueOrIndexName, new ScalarValue(i))) : local; _body.Evaluate(local, output); } return; } if (enumerable is StructureValue structure) { if (structure.Properties.Count == 0) { _alternative?.Evaluate(ctx, output); return; } var first = true; foreach (var member in structure.Properties) { if (first) first = false; else _delimiter?.Evaluate(ctx, output); var local = _keyOrElementName != null ? new(ctx.LogEvent, Locals.Set(ctx.Locals, _keyOrElementName, new ScalarValue(member.Name))) : ctx; local = _valueOrIndexName != null ? new(local.LogEvent, Locals.Set(local.Locals, _valueOrIndexName, member.Value)) : local; _body.Evaluate(local, output); } } if (enumerable is DictionaryValue dict) { if (dict.Elements.Count == 0) { _alternative?.Evaluate(ctx, output); return; } var first = true; foreach (var element in dict.Elements) { if (first) first = false; else _delimiter?.Evaluate(ctx, output); var local = _keyOrElementName != null ? new(ctx.LogEvent, Locals.Set(ctx.Locals, _keyOrElementName, element.Key)) : ctx; local = _valueOrIndexName != null ? new(local.LogEvent, Locals.Set(local.Locals, _valueOrIndexName, element.Value)) : local; _body.Evaluate(local, output); } } // Unsupported; not much we can do. } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledTemplate.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; namespace Serilog.Templates.Compilation; abstract class CompiledTemplate { public abstract void Evaluate(EvaluationContext ctx, TextWriter output); } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledTemplateBlock.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; namespace Serilog.Templates.Compilation; class CompiledTemplateBlock : CompiledTemplate { readonly CompiledTemplate[] _elements; public CompiledTemplateBlock(CompiledTemplate[] elements) { _elements = elements ?? throw new ArgumentNullException(nameof(elements)); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { foreach (var element in _elements) element.Evaluate(ctx, output); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/CompiledTimestampToken.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Parsing; using Serilog.Templates.Rendering; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; class CompiledTimestampToken : CompiledTemplate { readonly string? _format; readonly Alignment? _alignment; readonly IFormatProvider? _formatProvider; readonly Style _secondaryText; public CompiledTimestampToken(string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme) { _format = format; _alignment = alignment; _formatProvider = formatProvider; _secondaryText = theme.GetStyle(TemplateThemeStyle.SecondaryText); } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var invisibleCharacterCount = 0; if (_alignment == null) { EvaluateUnaligned(ctx, output, _formatProvider, ref invisibleCharacterCount); } else { var writer = new StringWriter(); EvaluateUnaligned(ctx, writer, _formatProvider, ref invisibleCharacterCount); Padding.Apply(output, writer.ToString(), _alignment.Value.Widen(invisibleCharacterCount)); } } void EvaluateUnaligned(EvaluationContext ctx, TextWriter output, IFormatProvider? formatProvider, ref int invisibleCharacterCount) { var value = ctx.LogEvent.Timestamp; using var style = _secondaryText.Set(output, ref invisibleCharacterCount); #if FEATURE_SPAN Span buffer = stackalloc char[36]; if (value.TryFormat(buffer, out int charsWritten, _format, _formatProvider)) { output.Write(buffer[..charsWritten]); return; } #endif output.Write(value.ToString(_format, formatProvider)); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/NameResolution/ExpressionLocalNameBinder.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Templates.Compilation.NameResolution; class ExpressionLocalNameBinder : IdentityTransformer { readonly IReadOnlyCollection _localNames; public static Expression BindLocalValueNames(Expression expression, IReadOnlyCollection locals) { var expressionLocalNameBinder = new ExpressionLocalNameBinder(locals); return expressionLocalNameBinder.Transform(expression); } ExpressionLocalNameBinder(IReadOnlyCollection localNames) { _localNames = localNames ?? throw new ArgumentNullException(nameof(localNames)); } protected override Expression Transform(AmbientNameExpression px) { if (!px.IsBuiltIn && _localNames.Contains(px.PropertyName)) return new LocalNameExpression(px.PropertyName); return base.Transform(px); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/NameResolution/TemplateLocalNameBinder.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Templates.Ast; // ReSharper disable MemberCanBeMadeStatic.Local, SuggestBaseTypeForParameter namespace Serilog.Templates.Compilation.NameResolution; class TemplateLocalNameBinder { public static Template BindLocalValueNames(Template template) { var binder = new TemplateLocalNameBinder(); return binder.Transform(template, new()); } Template Transform(Template template, Stack locals) { return template switch { TemplateBlock block => Transform(block, locals), LiteralText text => text, FormattedExpression fx => Transform(fx, locals), Conditional cond => Transform(cond, locals), Repetition rep => Transform(rep, locals), _ => throw new NotSupportedException("Unsupported template type.") }; } Template Transform(TemplateBlock block, Stack locals) { return new TemplateBlock(block.Elements .Select(e => Transform(e, locals)) .ToArray()); } Template Transform(FormattedExpression fx, Stack locals) { if (locals.Count == 0) return fx; return new FormattedExpression( ExpressionLocalNameBinder.BindLocalValueNames(fx.Expression, locals), fx.Format, fx.Alignment); } Template Transform(Conditional cond, Stack locals) { return new Conditional( ExpressionLocalNameBinder.BindLocalValueNames(cond.Condition, locals), Transform(cond.Consequent, locals), cond.Alternative != null ? Transform(cond.Alternative, locals) : null); } Template Transform(Repetition rep, Stack locals) { foreach (var name in rep.BindingNames) locals.Push(name); var body = Transform(rep.Body, locals); foreach (var _ in rep.BindingNames) locals.Pop(); return new Repetition( ExpressionLocalNameBinder.BindLocalValueNames(rep.Enumerable, locals), rep.BindingNames, body, rep.Delimiter != null ? Transform(rep.Delimiter, locals) : null, rep.Alternative != null ? Transform(rep.Alternative, locals) : null); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/TemplateCompiler.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation; using Serilog.Templates.Ast; using Serilog.Templates.Encoding; using Serilog.Templates.Themes; namespace Serilog.Templates.Compilation; static class TemplateCompiler { public static CompiledTemplate Compile(Template template, IFormatProvider? formatProvider, NameResolver nameResolver, TemplateTheme theme, EncodedTemplateFactory encoder) { return template switch { LiteralText text => new CompiledLiteralText(text.Text, theme), FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Level} } level => encoder.Wrap(new CompiledLevelToken(level.Format, level.Alignment, theme)), FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Exception }, Alignment: null, Format: null } => encoder.Wrap(new CompiledExceptionToken(theme)), FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Message }, Format: null } message => encoder.Wrap(new CompiledMessageToken(formatProvider, message.Alignment, theme)), FormattedExpression { Expression: AmbientNameExpression { IsBuiltIn: true, PropertyName: BuiltInProperty.Timestamp }, } timestamp => encoder.Wrap(new CompiledTimestampToken(timestamp.Format, timestamp.Alignment, formatProvider, theme)), FormattedExpression expression => encoder.MakeCompiledFormattedExpression( ExpressionCompiler.Compile(expression.Expression, formatProvider, nameResolver), expression.Format, expression.Alignment, formatProvider, theme), TemplateBlock block => new CompiledTemplateBlock(block.Elements.Select(e => Compile(e, formatProvider, nameResolver, theme, encoder)).ToArray()), Conditional conditional => new CompiledConditional( ExpressionCompiler.Compile(conditional.Condition, formatProvider, nameResolver), Compile(conditional.Consequent, formatProvider, nameResolver, theme, encoder), conditional.Alternative == null ? null : Compile(conditional.Alternative, formatProvider, nameResolver, theme, encoder)), Repetition repetition => new CompiledRepetition( ExpressionCompiler.Compile(repetition.Enumerable, formatProvider, nameResolver), repetition.BindingNames.Length > 0 ? repetition.BindingNames[0] : null, repetition.BindingNames.Length > 1 ? repetition.BindingNames[1] : null, Compile(repetition.Body, formatProvider, nameResolver, theme, encoder), repetition.Delimiter == null ? null : Compile(repetition.Delimiter, formatProvider, nameResolver, theme, encoder), repetition.Alternative == null ? null : Compile(repetition.Alternative, formatProvider, nameResolver, theme, encoder)), _ => throw new NotSupportedException() }; } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/TemplateFunctionNameResolver.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Expressions.Compilation; using Serilog.Expressions.Runtime; using Serilog.Templates.Ast; using Serilog.Templates.Compilation.UnreferencedProperties; using Serilog.Templates.Compilation.Unsafe; namespace Serilog.Templates.Compilation; static class TemplateFunctionNameResolver { public static NameResolver Build(NameResolver? additionalNameResolver, Template template) { var resolvers = new List { new StaticMemberNameResolver(typeof(RuntimeOperators)), new UnreferencedPropertiesFunction(template), new UnsafeOutputFunction() }; if (additionalNameResolver != null) resolvers.Add(additionalNameResolver); return new OrderedNameResolver(resolvers); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/UnreferencedProperties/ExpressionReferencedPropertiesFinder.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions; using Serilog.Expressions.Ast; using Serilog.Expressions.Compilation; using Serilog.Expressions.Compilation.Transformations; namespace Serilog.Templates.Compilation.UnreferencedProperties; class ExpressionReferencedPropertiesFinder : SerilogExpressionTransformer> { public IEnumerable FindReferencedProperties(Expression expression) { return Transform(expression); } protected override IEnumerable Transform(CallExpression call) { return call.Operands.SelectMany(Transform); } protected override IEnumerable Transform(ConstantExpression cx) { yield break; } protected override IEnumerable Transform(AmbientNameExpression px) { if (!px.IsBuiltIn) yield return px.PropertyName; } protected override IEnumerable Transform(LocalNameExpression nlx) { yield break; } protected override IEnumerable Transform(AccessorExpression spx) { if (Pattern.IsAmbientProperty(spx.Receiver, BuiltInProperty.Properties, true)) yield return spx.MemberName; foreach (var nested in Transform(spx.Receiver)) yield return nested; } protected override IEnumerable Transform(LambdaExpression lmx) { return Transform(lmx.Body); } protected override IEnumerable Transform(ParameterExpression prx) { yield break; } protected override IEnumerable Transform(IndexerWildcardExpression wx) { yield break; } protected override IEnumerable Transform(ArrayExpression ax) { return ax.Elements.OfType().SelectMany(i => Transform(i.Value)) .Concat(ax.Elements.OfType().SelectMany(i => Transform(i.Content))); } protected override IEnumerable Transform(ObjectExpression ox) { return ox.Members.OfType().SelectMany(m => Transform(m.Value)) .Concat(ox.Members.OfType().SelectMany(m => Transform(m.Content))); } protected override IEnumerable Transform(IndexerExpression ix) { if (Pattern.IsAmbientProperty(ix.Receiver, BuiltInProperty.Properties, true) && Pattern.IsStringConstant(ix.Index, out var name)) { yield return name; } else { foreach (var nested in Transform(ix.Index).Concat(Transform(ix.Receiver))) { yield return nested; } } } protected override IEnumerable Transform(IndexOfMatchExpression mx) { return Transform(mx.Corpus); } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/UnreferencedProperties/TemplateReferencedPropertiesFinder.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Templates.Ast; namespace Serilog.Templates.Compilation.UnreferencedProperties; class TemplateReferencedPropertiesFinder { readonly ExpressionReferencedPropertiesFinder _rpf = new(); public IEnumerable FindReferencedProperties(Template template) { return template switch { Conditional conditional => _rpf.FindReferencedProperties(conditional.Condition) .Concat(FindReferencedProperties(conditional.Consequent)) .Concat(conditional.Alternative != null ? FindReferencedProperties(conditional.Alternative) : Enumerable.Empty()), FormattedExpression formattedExpression => _rpf.FindReferencedProperties(formattedExpression.Expression), LiteralText => Enumerable.Empty(), Repetition repetition => _rpf.FindReferencedProperties(repetition.Enumerable) .Concat(FindReferencedProperties(repetition.Body)) .Concat(repetition.Alternative != null ? FindReferencedProperties(repetition.Alternative) : Enumerable.Empty()) .Concat(repetition.Delimiter != null ? FindReferencedProperties(repetition.Delimiter) : Enumerable.Empty()), TemplateBlock templateBlock => templateBlock.Elements.SelectMany(FindReferencedProperties), _ => throw new ArgumentOutOfRangeException(nameof(template)) }; } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/UnreferencedProperties/UnreferencedPropertiesFunction.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Reflection; using Serilog.Events; using Serilog.Expressions; using Serilog.Expressions.Runtime; using Serilog.Parsing; using Serilog.Templates.Ast; namespace Serilog.Templates.Compilation.UnreferencedProperties; /// /// This little extension implements the rest() function in expression templates. It's based on /// Serilog.Sinks.SystemConsole.PropertiesTokenRenderer, and is equivalent to how Properties is rendered by /// the console sink. rest() will return a structure containing all of the user-defined properties from a /// log event except those referenced in either the event's message template, or the expression template itself. /// /// /// The existing semantics of Properties in output templates isn't suitable for expression templates. The /// @p object provides access to all event properties in an expression template, so it would make no /// sense to render that object without all of its members. /// class UnreferencedPropertiesFunction : NameResolver { const string FunctionName = "rest"; readonly HashSet _referencedInTemplate; public UnreferencedPropertiesFunction(Template template) { var finder = new TemplateReferencedPropertiesFinder(); _referencedInTemplate = [..finder.FindReferencedProperties(template)]; } public override bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue) { if (parameter.ParameterType == typeof(UnreferencedPropertiesFunction)) { boundValue = this; return true; } boundValue = null; return false; } public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { if (name.Equals(FunctionName, StringComparison.OrdinalIgnoreCase)) { implementation = typeof(UnreferencedPropertiesFunction).GetMethod(nameof(Implementation), BindingFlags.Static | BindingFlags.Public)!; return true; } implementation = null; return false; } // By convention, built-in functions accept and return nullable values. // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? Implementation(UnreferencedPropertiesFunction self, LogEvent logEvent, LogEventPropertyValue? deep = null) { var checkMessageTemplate = Coerce.IsTrue(deep); return new StructureValue(logEvent.Properties .Where(kvp => !self._referencedInTemplate.Contains(kvp.Key) && (!checkMessageTemplate || !TemplateContainsPropertyName(logEvent.MessageTemplate, kvp.Key))) .Select(kvp => new LogEventProperty(kvp.Key, kvp.Value))); } static bool TemplateContainsPropertyName(MessageTemplate messageTemplate, string propertyName) { foreach (var token in messageTemplate.Tokens) { if (token is PropertyToken namedProperty && namedProperty.PropertyName == propertyName) { return true; } } return false; } } ================================================ FILE: src/Serilog.Expressions/Templates/Compilation/Unsafe/UnsafeOutputFunction.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using System.Reflection; using Serilog.Events; using Serilog.Expressions; using Serilog.Templates.Encoding; namespace Serilog.Templates.Compilation.Unsafe; /// /// Marks an expression in a template as bypassing the output encoding mechanism. /// class UnsafeOutputFunction : NameResolver { const string FunctionName = "unsafe"; public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { if (name.Equals(FunctionName, StringComparison.OrdinalIgnoreCase)) { implementation = typeof(UnsafeOutputFunction).GetMethod(nameof(Implementation), BindingFlags.Static | BindingFlags.Public)!; return true; } implementation = null; return false; } // By convention, built-in functions accept and return nullable values. // ReSharper disable once ReturnTypeCanBeNotNullable public static LogEventPropertyValue? Implementation(LogEventPropertyValue? inner) { return new ScalarValue(new PreEncodedValue(inner)); } } ================================================ FILE: src/Serilog.Expressions/Templates/Encoding/EncodedCompiledTemplate.cs ================================================ using Serilog.Expressions; using Serilog.Templates.Compilation; namespace Serilog.Templates.Encoding { class EncodedCompiledTemplate : CompiledTemplate { readonly CompiledTemplate _inner; readonly TemplateOutputEncoder _encoder; public EncodedCompiledTemplate(CompiledTemplate inner, TemplateOutputEncoder encoder) { _inner = inner; _encoder = encoder; } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var buffer = new StringWriter(output.FormatProvider); _inner.Evaluate(ctx, buffer); var encoded = _encoder.Encode(buffer.ToString()); output.Write(encoded); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Encoding/EncodedTemplateFactory.cs ================================================ using Serilog.Expressions; using Serilog.Parsing; using Serilog.Templates.Compilation; using Serilog.Templates.Themes; namespace Serilog.Templates.Encoding { class EncodedTemplateFactory { readonly TemplateOutputEncoder? _encoder; public EncodedTemplateFactory(TemplateOutputEncoder? encoder) { _encoder = encoder; } public CompiledTemplate Wrap(CompiledTemplate inner) { if (_encoder == null) return inner; return new EncodedCompiledTemplate(inner, _encoder); } public CompiledTemplate MakeCompiledFormattedExpression(Evaluatable expression, string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme) { if (_encoder == null) return new CompiledFormattedExpression(expression, format, alignment, formatProvider, theme); return new EscapableEncodedCompiledFormattedExpression(expression, format, alignment, formatProvider, theme, _encoder); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Encoding/EscapableEncodedCompiledFormattedExpression.cs ================================================ using Serilog.Events; using Serilog.Expressions; using Serilog.Expressions.Runtime; using Serilog.Parsing; using Serilog.Templates.Compilation; using Serilog.Templates.Themes; namespace Serilog.Templates.Encoding { class EscapableEncodedCompiledFormattedExpression : CompiledTemplate { static int _nextSubstituteLocalNameSuffix; readonly string _substituteLocalName = $"%sub{Interlocked.Increment(ref _nextSubstituteLocalNameSuffix)}"; readonly Evaluatable _expression; readonly TemplateOutputEncoder _encoder; readonly CompiledFormattedExpression _inner; public EscapableEncodedCompiledFormattedExpression(Evaluatable expression, string? format, Alignment? alignment, IFormatProvider? formatProvider, TemplateTheme theme, TemplateOutputEncoder encoder) { _expression = expression; _encoder = encoder; // `expression` can't be passed through, because it may include calls to the `unsafe()` function (nested in arbitrary subexpressions) that // need to be evaluated first. So, instead, we evaluate `expression` and unwrap the result of `unsafe`, placing the result in a local variable // that the formatting expression we construct here can read from. _inner = new CompiledFormattedExpression(GetSubstituteLocalValue, format, alignment, formatProvider, theme); } LogEventPropertyValue? GetSubstituteLocalValue(EvaluationContext context) { return Locals.TryGetValue(context.Locals, _substituteLocalName, out var computed) ? computed : null; } public override void Evaluate(EvaluationContext ctx, TextWriter output) { var value = _expression(ctx); if (value is ScalarValue { Value: PreEncodedValue pv }) { var rawContext = pv.Inner == null ? new EvaluationContext(ctx.LogEvent) : new EvaluationContext(ctx.LogEvent, Locals.Set(ctx.Locals, _substituteLocalName, pv.Inner)); _inner.Evaluate(rawContext, output); return; } var buffer = new StringWriter(output.FormatProvider); var bufferedContext = value == null ? ctx : new EvaluationContext(ctx.LogEvent, Locals.Set(ctx.Locals, _substituteLocalName, value)); _inner.Evaluate(bufferedContext, buffer); var encoded = _encoder.Encode(buffer.ToString()); output.Write(encoded); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Encoding/PreEncodedValue.cs ================================================ using Serilog.Events; namespace Serilog.Templates.Encoding { class PreEncodedValue { public LogEventPropertyValue? Inner { get; } public PreEncodedValue(LogEventPropertyValue? inner) { Inner = inner; } public override string ToString() { // This code path indicates that the template expects encoding to be performed, but no encoder is // registered (probably a bad situation to be in). throw new InvalidOperationException("No output encoder is registered."); } } } ================================================ FILE: src/Serilog.Expressions/Templates/Encoding/TemplateOutputEncoder.cs ================================================ namespace Serilog.Templates.Encoding { /// /// An encoder applied to the output substituted into template holes. /// public abstract class TemplateOutputEncoder { /// /// Encode . /// /// The raw template output to encode. /// The encoded output. public abstract string Encode(string value); } } ================================================ FILE: src/Serilog.Expressions/Templates/ExpressionTemplate.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using Serilog.Events; using Serilog.Expressions; using Serilog.Formatting; using Serilog.Templates.Compilation; using Serilog.Templates.Compilation.NameResolution; using Serilog.Templates.Encoding; using Serilog.Templates.Parsing; using Serilog.Templates.Themes; namespace Serilog.Templates; /// /// Formats s into text using embedded expressions. /// public class ExpressionTemplate : ITextFormatter { readonly CompiledTemplate _compiled; /// /// Construct an . /// /// The template text. /// The parsed template, if successful. /// A description of the error, if unsuccessful. /// true if the template was well-formed. public static bool TryParse( string template, [MaybeNullWhen(false)] out ExpressionTemplate result, [MaybeNullWhen(true)] out string error) { if (template == null) throw new ArgumentNullException(nameof(template)); return TryParse(template, null, null, null, false, null, out result, out error); } /// /// Construct an . /// /// The template text. /// Optionally, an to use when formatting /// embedded values. /// Optionally, an ANSI theme to apply to the template output. /// The parsed template, if successful. /// A description of the error, if unsuccessful. /// Optionally, a /// with which to resolve function names that appear in the template. /// Apply even when /// or returns true. /// Optionally, an encoder to apply to output substituted into template holes. /// true if the template was well-formed. public static bool TryParse( string template, IFormatProvider? formatProvider, NameResolver? nameResolver, TemplateTheme? theme, bool applyThemeWhenOutputIsRedirected, TemplateOutputEncoder? encoder, [MaybeNullWhen(false)] out ExpressionTemplate result, [MaybeNullWhen(true)] out string error) { if (template == null) throw new ArgumentNullException(nameof(template)); var templateParser = new TemplateParser(); if (!templateParser.TryParse(template, out var parsed, out error)) { result = null; return false; } var planned = TemplateLocalNameBinder.BindLocalValueNames(parsed); result = new( TemplateCompiler.Compile( planned, formatProvider, TemplateFunctionNameResolver.Build(nameResolver, planned), SelectTheme(theme, applyThemeWhenOutputIsRedirected), new EncodedTemplateFactory(encoder))); return true; } ExpressionTemplate(CompiledTemplate compiled) { _compiled = compiled; } /// /// Construct an . /// /// The template text. /// Optionally, an to use when formatting /// embedded values. /// Optionally, a /// with which to resolve function names that appear in the template. /// Optionally, an ANSI theme to apply to the template output. /// Apply even when /// or returns true. /// Optionally, an encoder to apply to output substituted into template holes. public ExpressionTemplate( string template, IFormatProvider? formatProvider = null, NameResolver? nameResolver = null, TemplateTheme? theme = null, bool applyThemeWhenOutputIsRedirected = false, TemplateOutputEncoder? encoder = null) { if (template == null) throw new ArgumentNullException(nameof(template)); var templateParser = new TemplateParser(); if (!templateParser.TryParse(template, out var parsed, out var error)) throw new ArgumentException(error); var planned = TemplateLocalNameBinder.BindLocalValueNames(parsed); _compiled = TemplateCompiler.Compile( planned, formatProvider, TemplateFunctionNameResolver.Build(nameResolver, planned), SelectTheme(theme, applyThemeWhenOutputIsRedirected), new EncodedTemplateFactory(encoder)); } static TemplateTheme SelectTheme(TemplateTheme? supplied, bool applyThemeWhenOutputIsRedirected) { if (supplied == null || (Console.IsOutputRedirected || Console.IsErrorRedirected) && !applyThemeWhenOutputIsRedirected) { return TemplateTheme.None; } return supplied; } /// public void Format(LogEvent logEvent, TextWriter output) { _compiled.Evaluate(new(logEvent), output); } } ================================================ FILE: src/Serilog.Expressions/Templates/Parsing/TemplateParser.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Diagnostics.CodeAnalysis; using Serilog.Templates.Ast; namespace Serilog.Templates.Parsing; class TemplateParser { readonly TemplateTokenizer _tokenizer = new(); readonly TemplateTokenParsers _templateTokenParsers = new(); public bool TryParse( string template, [MaybeNullWhen(false)] out Template parsed, [MaybeNullWhen(true)] out string error) { if (template == null) throw new ArgumentNullException(nameof(template)); var tokenList = _tokenizer.TryTokenize(template); if (!tokenList.HasValue) { error = tokenList.ToString(); parsed = null; return false; } var result = _templateTokenParsers.TryParse(tokenList.Value); if (!result.HasValue) { error = result.ToString(); parsed = null; return false; } parsed = result.Value; error = null; return true; } } ================================================ FILE: src/Serilog.Expressions/Templates/Parsing/TemplateTokenParsers.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Ast; using Serilog.Expressions.Parsing; using Serilog.Parsing; using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; using Serilog.ParserConstruction.Parsers; using Serilog.Templates.Ast; using static Serilog.Expressions.Parsing.ExpressionToken; // ReSharper disable SuggestBaseTypeForParameter, ConvertIfStatementToSwitchStatement, AccessToModifiedClosure namespace Serilog.Templates.Parsing; class TemplateTokenParsers { readonly TokenListParser _template; public TemplateTokenParsers() { TokenListParser? block = null; var alignment = Token.EqualTo(Comma).IgnoreThen( (from direction in Token.EqualTo(Minus).Value(AlignmentDirection.Left) .OptionalOrDefault(AlignmentDirection.Right) from width in Token.EqualTo(Number).Apply(Numerics.NaturalUInt32) select new Alignment(direction, (int) width)).Named("alignment and width")); var format = Token.EqualTo(Colon) .IgnoreThen(Token.EqualTo(Format)) .Select(fmt => (string?) fmt.ToStringValue()); var hole = from _ in Token.EqualTo(LBrace) from expr in ExpressionTokenParsers.Expr from align in alignment.Select(a => (Alignment?)a).OptionalOrDefault() from fmt in format.OptionalOrDefault() from __ in Token.EqualTo(RBrace) select (Template) new FormattedExpression(expr, fmt, align); static TokenListParser Directive( bool hasArgument, params ExpressionToken[] signifiers) { var open = Token.EqualTo(LBraceHash) .IgnoreThen(Token.Sequence(signifiers)).Try(); if (hasArgument) return open .IgnoreThen(ExpressionTokenParsers.Expr.Cast()) .Then(v => Token.EqualTo(RBrace).Value(v)); return open.IgnoreThen(Token.EqualTo(RBrace)).Value((Expression?) null); } static Template? LeftReduceConditional((Expression?, Template)[] first, Template? last) { for (var i = first.Length - 1; i >= 0; i--) { last = new Conditional(first[i].Item1!, first[i].Item2, last); } return last; } var conditional = from iff in Directive(true, If) from consequent in Parse.Ref(() => block!) from alternatives in Directive(true, Else, If) .Then(elsif => Parse.Ref(() => block!).Select(b => (elsif, b))) .Many() from final in Directive(false, Else) .IgnoreThen(Parse.Ref(() => block!).Select(b => ((Expression?) null, b))) .OptionalOrDefault() from end in Directive(false, End) let firstAlt = LeftReduceConditional(alternatives, final.b) select (Template) new Conditional(iff!, consequent, firstAlt); var eachDirective = Token.EqualTo(LBraceHash) .IgnoreThen(Token.EqualTo(Each)).Try() .IgnoreThen(Token.EqualTo(Identifier) .Select(i => i.ToStringValue()) .AtLeastOnceDelimitedBy(Token.EqualTo(Comma))) .Then(bindings => Token.EqualTo(In).Value(bindings)) .Then(bindings => ExpressionTokenParsers.Expr.Cast() .Select(enumerable => new {enumerable, bindings})) .Then(v => Token.EqualTo(RBrace).Value(v)); var repetition = from each in eachDirective from body in Parse.Ref(() => block!) from delimiter in Directive(false, Delimit) .IgnoreThen(Parse.Ref(() => block!)) .Cast() .OptionalOrDefault() from alternative in Directive(false, Else) .IgnoreThen(Parse.Ref(() => block!)) .Cast() .OptionalOrDefault() from end in Directive(false, End) select (Template) new Repetition( each.enumerable, each.bindings, body, delimiter, alternative); var element = Token.EqualTo(Text).Select(t => (Template)new LiteralText(t.ToStringValue())) .Or(Token.EqualTo(DoubleLBrace) .Value((Template) new LiteralText("{"))) .Or(Token.EqualTo(DoubleRBrace) .Value((Template) new LiteralText("}"))) .Or(conditional) .Or(repetition) .Or(hole); block = element.Many().Select(elements => elements.Length == 1 ? elements[0] : new TemplateBlock(elements)); _template = block.AtEnd(); } public TokenListParserResult TryParse( TokenList input) { return _template.TryParse(input); } } ================================================ FILE: src/Serilog.Expressions/Templates/Parsing/TemplateTokenizer.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Expressions.Parsing; using Serilog.ParserConstruction; using Serilog.ParserConstruction.Model; namespace Serilog.Templates.Parsing; class TemplateTokenizer : Tokenizer { readonly ExpressionTokenizer _expressionTokenizer = new(); protected override IEnumerable> Tokenize(TextSpan span) { var start = span; var rem = start; do { var next = rem.ConsumeChar(); if (!next.HasValue) { if (rem != start) yield return Result.Value(ExpressionToken.Text, start, rem); yield break; } if (next.Value == '{') { if (rem != start) yield return Result.Value(ExpressionToken.Text, start, rem); var peek = next.Remainder.ConsumeChar(); if (peek.HasValue && peek.Value == '{') { yield return Result.Value(ExpressionToken.DoubleLBrace, next.Location, peek.Remainder); start = rem = peek.Remainder; } else { if (peek.HasValue && peek.Value == '#') { yield return Result.Value(ExpressionToken.LBraceHash, next.Location, peek.Remainder); start = rem = peek.Remainder; } else { yield return Result.Value(ExpressionToken.LBrace, next.Location, next.Remainder); start = rem = next.Remainder; } foreach (var token in TokenizeHole(rem)) { yield return token; start = rem = token.Remainder; } } } else if (next.Value == '}') { if (rem != start) yield return Result.Value(ExpressionToken.Text, start, rem); var peek = next.Remainder.ConsumeChar(); if (peek.HasValue && peek.Value == '}') { yield return Result.Value(ExpressionToken.DoubleRBrace, next.Location, peek.Remainder); start = rem = peek.Remainder; } else { yield return Result.Empty(next.Remainder, ["escaped `}`"]); yield break; } } else { rem = next.Remainder; } } while (true); } IEnumerable> TokenizeHole(TextSpan span) { // Stack braces, brackets, and parens. // If we hit , or :, the stack is empty, and everything we've seen is balanced, we switch into // alignment/width tokenization. // If we hit } and the stack is empty, and everything we've seen is balanced, we yield the final // '}' and return to literal text mode. var toMatch = new Stack(); var unbalanced = false; foreach (var token in _expressionTokenizer.LazyTokenize(span)) { if (unbalanced) yield break; yield return token; if (!token.HasValue) yield break; if (token.Value is ExpressionToken.LParen or ExpressionToken.LBrace or ExpressionToken.LBracket) { toMatch.Push(token.Value); } else if (toMatch.Count > 0) { if (token.Value == ExpressionToken.RParen) { if (toMatch.Peek() != ExpressionToken.LParen) unbalanced = true; else toMatch.Pop(); } else if (token.Value == ExpressionToken.RBrace) { if (toMatch.Peek() != ExpressionToken.LBrace) unbalanced = true; else toMatch.Pop(); } else if (token.Value == ExpressionToken.RBracket) { if (toMatch.Peek() != ExpressionToken.LBracket) unbalanced = true; else toMatch.Pop(); } } else if (token.Value == ExpressionToken.RBrace) { yield break; } // The default tokenization of `,` alignment (comma, [minus], number) is fine, only // formats require special handling. else if (token.Value == ExpressionToken.Colon) { var formatStart = token.Remainder; var next = formatStart.ConsumeChar(); while (next.HasValue) { if (next.Value == '}') { yield return Result.Value(ExpressionToken.Format, formatStart, next.Location); yield return Result.Value(ExpressionToken.RBrace, next.Location, next.Remainder); yield break; } next = next.Remainder.ConsumeChar(); } if (formatStart != next.Location) { yield return Result.Value(ExpressionToken.Format, formatStart, next.Location); } yield break; } } } } ================================================ FILE: src/Serilog.Expressions/Templates/Rendering/AlignmentExtensions.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Parsing; namespace Serilog.Templates.Rendering; static class AlignmentExtensions { public static Alignment Widen(this Alignment alignment, int amount) { return new(alignment.Direction, alignment.Width + amount); } } ================================================ FILE: src/Serilog.Expressions/Templates/Rendering/Casing.cs ================================================ // Copyright Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Rendering; static class Casing { /// /// Apply upper or lower casing to when is provided. /// Returns when no or invalid format provided. /// /// Provided string for formatting. /// Format string. /// The provided with formatting applied. public static string Format(string value, string? format = null) { switch (format) { case "u": return value.ToUpperInvariant(); case "w": return value.ToLowerInvariant(); default: return value; } } } ================================================ FILE: src/Serilog.Expressions/Templates/Rendering/LevelRenderer.cs ================================================ // Copyright 2017 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Events; // ReSharper disable StringLiteralTypo namespace Serilog.Templates.Rendering; /// /// Implements the {Level} element. /// can now have a fixed width applied to it, as well as casing rules. /// Width is set through formats like "u3" (uppercase three chars), /// "w1" (one lowercase char), or "t4" (title case four chars). /// static class LevelRenderer { static readonly string[][] TitleCaseLevelMap = [ ["V", "Vb", "Vrb", "Verb"], ["D", "De", "Dbg", "Dbug"], ["I", "In", "Inf", "Info"], ["W", "Wn", "Wrn", "Warn"], ["E", "Er", "Err", "Eror"], ["F", "Fa", "Ftl", "Fatl"] ]; static readonly string[][] LowercaseLevelMap = [ ["v", "vb", "vrb", "verb"], ["d", "de", "dbg", "dbug"], ["i", "in", "inf", "info"], ["w", "wn", "wrn", "warn"], ["e", "er", "err", "eror"], ["f", "fa", "ftl", "fatl"] ]; static readonly string[][] UppercaseLevelMap = [ ["V", "VB", "VRB", "VERB"], ["D", "DE", "DBG", "DBUG"], ["I", "IN", "INF", "INFO"], ["W", "WN", "WRN", "WARN"], ["E", "ER", "ERR", "EROR"], ["F", "FA", "FTL", "FATL"] ]; public static string GetLevelMoniker(LogEventLevel value, string? format) { if (format == null) return value.ToString(); if (format.Length != 2 && format.Length != 3) return Casing.Format(value.ToString(), format); // Using int.Parse() here requires allocating a string to exclude the first character prefix. // Junk like "wxy" will be accepted but produce benign results. var width = format[1] - '0'; if (format.Length == 3) { width *= 10; width += format[2] - '0'; } if (width < 1) return string.Empty; if (width > 4) { var stringValue = value.ToString(); if (stringValue.Length > width) stringValue = stringValue.Substring(0, width); return Casing.Format(stringValue); } var index = (int)value; if (index >= 0 && index <= (int)LogEventLevel.Fatal) { switch (format[0]) { case 'w': return LowercaseLevelMap[index][width - 1]; case 'u': return UppercaseLevelMap[index][width - 1]; case 't': return TitleCaseLevelMap[index][width - 1]; } } return Casing.Format(value.ToString(), format); } } ================================================ FILE: src/Serilog.Expressions/Templates/Rendering/Padding.cs ================================================ // Copyright 2013-2020 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using Serilog.Parsing; namespace Serilog.Templates.Rendering; static class Padding { static readonly char[] PaddingChars = Enumerable.Repeat(' ', 80).ToArray(); /// /// Writes the provided value to the output, applying direction-based padding when is provided. /// public static void Apply(TextWriter output, string value, Alignment alignment) { if (value.Length >= alignment.Width) { output.Write(value); return; } var pad = alignment.Width - value.Length; if (alignment.Direction == AlignmentDirection.Left) output.Write(value); if (pad <= PaddingChars.Length) { output.Write(PaddingChars, 0, pad); } else { output.Write(new string(' ', pad)); } if (alignment.Direction == AlignmentDirection.Right) output.Write(value); } } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/Style.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Themes; readonly struct Style { readonly string? _ansiStyle; public Style(string ansiStyle) { _ansiStyle = ansiStyle; } internal StyleReset Set(TextWriter output, ref int invisibleCharacterCount) { if (_ansiStyle != null) { output.Write(_ansiStyle); invisibleCharacterCount += _ansiStyle.Length; invisibleCharacterCount += StyleReset.ResetCharCount; return new(output); } return default; } public string? GetAnsiStyle() { return _ansiStyle; } } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/StyleReset.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Themes; readonly struct StyleReset : IDisposable { const string AnsiStyleResetSequence = "\x1b[0m"; public const int ResetCharCount = 4; readonly TextWriter? _output; public StyleReset(TextWriter output) { _output = output; } public void Dispose() { _output?.Write(AnsiStyleResetSequence); } } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/TemplateTheme.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Themes; /// /// A template theme using the ANSI terminal escape sequences. /// public class TemplateTheme { /// /// A 256-color theme along the lines of Visual Studio Code. /// public static TemplateTheme Code { get; } = TemplateThemes.Code; /// /// A theme using only gray, black and white. /// public static TemplateTheme Grayscale { get; } = TemplateThemes.Grayscale; /// /// A theme in the style of the original Serilog.Sinks.Literate. /// public static TemplateTheme Literate { get; } = TemplateThemes.Literate; /// /// A theme in the style of the original Serilog.Sinks.Literate using only standard 16 terminal colors that will work on light backgrounds. /// public static TemplateTheme Sixteen { get; } = TemplateThemes.Sixteen; internal static TemplateTheme None { get; } = new(new Dictionary()); readonly Dictionary _styles; /// /// Construct a theme given a set of styles. /// /// Styles to apply within the theme. The dictionary maps style names to ANSI /// sequences implementing the styles. /// When is null public TemplateTheme(IReadOnlyDictionary ansiStyles) { if (ansiStyles is null) throw new ArgumentNullException(nameof(ansiStyles)); _styles = ansiStyles.ToDictionary(kv => kv.Key, kv => new Style(kv.Value)); } /// /// Construct a theme given a set of styles. /// /// A base template theme, which will supply styles not overridden in . /// Styles to apply within the theme. The dictionary maps style names to ANSI /// sequences implementing the styles. /// When is null public TemplateTheme(TemplateTheme baseTheme, IReadOnlyDictionary ansiStyles) { if (baseTheme == null) throw new ArgumentNullException(nameof(baseTheme)); if (ansiStyles is null) throw new ArgumentNullException(nameof(ansiStyles)); _styles = new(baseTheme._styles); foreach (var kv in ansiStyles) _styles[kv.Key] = new(kv.Value); } internal Style GetStyle(TemplateThemeStyle templateThemeStyle) { _styles.TryGetValue(templateThemeStyle, out var style); return style; } } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/TemplateThemeStyle.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Themes; /// /// Elements styled by a template theme. /// public enum TemplateThemeStyle { /// /// Prominent text, generally content within an event's message. /// Text, /// /// Boilerplate text, for example items specified in an output template. /// SecondaryText, /// /// De-emphasized text, for example literal text in output templates and /// punctuation used when writing structured data. /// TertiaryText, /// /// Output demonstrating some kind of configuration issue, e.g. an invalid /// message template token. /// Invalid, /// /// The built-in value. /// Null, /// /// Property and type names. /// Name, /// /// Strings. /// String, /// /// Numbers. /// Number, /// /// values. /// Boolean, /// /// All other scalar values, e.g. instances. /// Scalar, /// /// Level indicator. /// LevelVerbose, /// /// Level indicator. /// LevelDebug, /// /// Level indicator. /// LevelInformation, /// /// Level indicator. /// LevelWarning, /// /// Level indicator. /// LevelError, /// /// Level indicator. /// LevelFatal, } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/TemplateThemes.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. namespace Serilog.Templates.Themes; static class TemplateThemes { public static TemplateTheme Literate { get; } = new( new Dictionary { [TemplateThemeStyle.Text] = "\x1b[38;5;0015m", [TemplateThemeStyle.SecondaryText] = "\x1b[38;5;0007m", [TemplateThemeStyle.TertiaryText] = "\x1b[38;5;0008m", [TemplateThemeStyle.Invalid] = "\x1b[38;5;0011m", [TemplateThemeStyle.Null] = "\x1b[38;5;0027m", [TemplateThemeStyle.Name] = "\x1b[38;5;0007m", [TemplateThemeStyle.String] = "\x1b[38;5;0045m", [TemplateThemeStyle.Number] = "\x1b[38;5;0200m", [TemplateThemeStyle.Boolean] = "\x1b[38;5;0027m", [TemplateThemeStyle.Scalar] = "\x1b[38;5;0085m", [TemplateThemeStyle.LevelVerbose] = "\x1b[38;5;0007m", [TemplateThemeStyle.LevelDebug] = "\x1b[38;5;0007m", [TemplateThemeStyle.LevelInformation] = "\x1b[38;5;0015m", [TemplateThemeStyle.LevelWarning] = "\x1b[38;5;0011m", [TemplateThemeStyle.LevelError] = "\x1b[38;5;0015m\x1b[48;5;0196m", [TemplateThemeStyle.LevelFatal] = "\x1b[38;5;0015m\x1b[48;5;0196m", }); public static TemplateTheme Grayscale { get; } = new( new Dictionary { [TemplateThemeStyle.Text] = "\x1b[37;1m", [TemplateThemeStyle.SecondaryText] = "\x1b[37m", [TemplateThemeStyle.TertiaryText] = "\x1b[30;1m", [TemplateThemeStyle.Invalid] = "\x1b[37;1m\x1b[47m", [TemplateThemeStyle.Null] = "\x1b[1m\x1b[37;1m", [TemplateThemeStyle.Name] = "\x1b[37m", [TemplateThemeStyle.String] = "\x1b[1m\x1b[37;1m", [TemplateThemeStyle.Number] = "\x1b[1m\x1b[37;1m", [TemplateThemeStyle.Boolean] = "\x1b[1m\x1b[37;1m", [TemplateThemeStyle.Scalar] = "\x1b[1m\x1b[37;1m", [TemplateThemeStyle.LevelVerbose] = "\x1b[30;1m", [TemplateThemeStyle.LevelDebug] = "\x1b[30;1m", [TemplateThemeStyle.LevelInformation] = "\x1b[37;1m", [TemplateThemeStyle.LevelWarning] = "\x1b[37;1m\x1b[47m", [TemplateThemeStyle.LevelError] = "\x1b[30m\x1b[47m", [TemplateThemeStyle.LevelFatal] = "\x1b[30m\x1b[47m", }); public static TemplateTheme Code { get; } = new( new Dictionary { [TemplateThemeStyle.Text] = "\x1b[38;5;0253m", [TemplateThemeStyle.SecondaryText] = "\x1b[38;5;0246m", [TemplateThemeStyle.TertiaryText] = "\x1b[38;5;0242m", [TemplateThemeStyle.Invalid] = "\x1b[33;1m", [TemplateThemeStyle.Null] = "\x1b[38;5;0038m", [TemplateThemeStyle.Name] = "\x1b[38;5;0081m", [TemplateThemeStyle.String] = "\x1b[38;5;0216m", [TemplateThemeStyle.Number] = "\x1b[38;5;151m", [TemplateThemeStyle.Boolean] = "\x1b[38;5;0038m", [TemplateThemeStyle.Scalar] = "\x1b[38;5;0079m", [TemplateThemeStyle.LevelVerbose] = "\x1b[37m", [TemplateThemeStyle.LevelDebug] = "\x1b[37m", [TemplateThemeStyle.LevelInformation] = "\x1b[37;1m", [TemplateThemeStyle.LevelWarning] = "\x1b[38;5;0229m", [TemplateThemeStyle.LevelError] = "\x1b[38;5;0197m\x1b[48;5;0238m", [TemplateThemeStyle.LevelFatal] = "\x1b[38;5;0197m\x1b[48;5;0238m", }); public static TemplateTheme Sixteen { get; } = new( new Dictionary { [TemplateThemeStyle.Text] = string.Empty, [TemplateThemeStyle.SecondaryText] = string.Empty, [TemplateThemeStyle.TertiaryText] = string.Empty, [TemplateThemeStyle.Invalid] = "\x1b[33m", [TemplateThemeStyle.Null] = "\x1b[34m", [TemplateThemeStyle.Name] = string.Empty, [TemplateThemeStyle.String] = "\x1b[36m", [TemplateThemeStyle.Number] = "\x1b[35m", [TemplateThemeStyle.Boolean] = "\x1b[34m", [TemplateThemeStyle.Scalar] = "\x1b[32m", [TemplateThemeStyle.LevelVerbose] = "\x1b[30;1m", [TemplateThemeStyle.LevelDebug] = "\x1b[1m", [TemplateThemeStyle.LevelInformation] = "\x1b[36;1m", [TemplateThemeStyle.LevelWarning] = "\x1b[33;1m", [TemplateThemeStyle.LevelError] = "\x1b[31;1m", [TemplateThemeStyle.LevelFatal] = "\x1b[31;1m", }); } ================================================ FILE: src/Serilog.Expressions/Templates/Themes/ThemedJsonValueFormatter.cs ================================================ // Copyright © Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using System.Globalization; using Serilog.Data; using Serilog.Events; using Serilog.Formatting.Json; // ReSharper disable ForCanBeConvertedToForeach namespace Serilog.Templates.Themes; class ThemedJsonValueFormatter : LogEventPropertyValueVisitor { const string TypeTagPropertyName = "$type"; readonly Style _null, _bool, _num, _string, _scalar, _tertiary, _name; public ThemedJsonValueFormatter(TemplateTheme theme) { _null = theme.GetStyle(TemplateThemeStyle.Null); _bool = theme.GetStyle(TemplateThemeStyle.Boolean); _num = theme.GetStyle(TemplateThemeStyle.Number); _string = theme.GetStyle(TemplateThemeStyle.String); _scalar = theme.GetStyle(TemplateThemeStyle.Scalar); _tertiary = theme.GetStyle(TemplateThemeStyle.TertiaryText); _name = theme.GetStyle(TemplateThemeStyle.Name); } public int Format(LogEventPropertyValue value, TextWriter output) { return Visit(output, value); } protected override int VisitScalarValue(TextWriter state, ScalarValue scalar) { return FormatLiteralValue(scalar, state); } protected override int VisitSequenceValue(TextWriter state, SequenceValue sequence) { var count = 0; using (_tertiary.Set(state, ref count)) state.Write('['); var delim = string.Empty; for (var index = 0; index < sequence.Elements.Count; ++index) { if (delim.Length != 0) { using (_tertiary.Set(state, ref count)) state.Write(delim); } delim = ","; count += Visit(state, sequence.Elements[index]); } using (_tertiary.Set(state, ref count)) state.Write(']'); return count; } protected override int VisitStructureValue(TextWriter state, StructureValue structure) { var count = 0; using (_tertiary.Set(state, ref count)) state.Write('{'); var delim = string.Empty; for (var index = 0; index < structure.Properties.Count; ++index) { if (delim.Length != 0) { using (_tertiary.Set(state, ref count)) state.Write(delim); } delim = ","; var property = structure.Properties[index]; using (_name.Set(state, ref count)) JsonValueFormatter.WriteQuotedJsonString(property.Name, state); using (_tertiary.Set(state, ref count)) state.Write(":"); count += Visit(state, property.Value); } if (structure.TypeTag != null) { using (_tertiary.Set(state, ref count)) state.Write(delim); using (_name.Set(state, ref count)) JsonValueFormatter.WriteQuotedJsonString(TypeTagPropertyName, state); using (_tertiary.Set(state, ref count)) state.Write(":"); using (_string.Set(state, ref count)) JsonValueFormatter.WriteQuotedJsonString(structure.TypeTag, state); } using (_tertiary.Set(state, ref count)) state.Write('}'); return count; } protected override int VisitDictionaryValue(TextWriter state, DictionaryValue dictionary) { var count = 0; using (_tertiary.Set(state, ref count)) state.Write('{'); var delim = string.Empty; foreach (var element in dictionary.Elements) { if (delim.Length != 0) { using (_tertiary.Set(state, ref count)) state.Write(delim); } delim = ","; var style = element.Key.Value == null ? _null : element.Key.Value is string ? _string : _scalar; using (style.Set(state, ref count)) JsonValueFormatter.WriteQuotedJsonString(element.Key.Value?.ToString() ?? "null", state); using (_tertiary.Set(state, ref count)) state.Write(":"); count += Visit(state, element.Value); } using (_tertiary.Set(state, ref count)) state.Write('}'); return count; } int FormatLiteralValue(ScalarValue scalar, TextWriter output) { var value = scalar.Value; var count = 0; if (value == null) { using (_null.Set(output, ref count)) output.Write("null"); return count; } if (value is string str) { using (_string.Set(output, ref count)) JsonValueFormatter.WriteQuotedJsonString(str, output); return count; } if (value is ValueType) { if (value is int or uint or long or ulong or decimal or byte or sbyte or short or ushort) { using (_num.Set(output, ref count)) output.Write(((IFormattable)value).ToString(null, CultureInfo.InvariantCulture)); return count; } if (value is double d) { using (_num.Set(output, ref count)) { if (double.IsNaN(d) || double.IsInfinity(d)) JsonValueFormatter.WriteQuotedJsonString(d.ToString(CultureInfo.InvariantCulture), output); else output.Write(d.ToString("R", CultureInfo.InvariantCulture)); } return count; } if (value is float f) { using (_num.Set(output, ref count)) { if (double.IsNaN(f) || double.IsInfinity(f)) JsonValueFormatter.WriteQuotedJsonString(f.ToString(CultureInfo.InvariantCulture), output); else output.Write(f.ToString("R", CultureInfo.InvariantCulture)); } return count; } if (value is bool b) { using (_bool.Set(output, ref count)) output.Write(b ? "true" : "false"); return count; } if (value is char ch) { using (_scalar.Set(output, ref count)) JsonValueFormatter.WriteQuotedJsonString(ch.ToString(), output); return count; } if (value is DateTime or DateTimeOffset) { using (_scalar.Set(output, ref count)) { output.Write('"'); output.Write(((IFormattable)value).ToString("O", CultureInfo.InvariantCulture)); output.Write('"'); } return count; } } using (_scalar.Set(output, ref count)) JsonValueFormatter.WriteQuotedJsonString(value.ToString() ?? "", output); return count; } } ================================================ FILE: test/Serilog.Expressions.PerformanceTests/ComparisonBenchmark.cs ================================================ using BenchmarkDotNet.Attributes; using Serilog.Events; using Serilog.Expressions.PerformanceTests.Support; using Xunit; namespace Serilog.Expressions.PerformanceTests; /// /// Tests the performance of various filtering mechanisms. /// public class ComparisonBenchmark { readonly Func _trivialFilter, _handwrittenFilter, _expressionFilter; readonly LogEvent _event = Some.InformationEvent("{A}", 3); public ComparisonBenchmark() { // Just the delegate invocation overhead _trivialFilter = _ => true; // `A == 3`, the old way _handwrittenFilter = evt => { if (evt.Properties.TryGetValue("A", out var a) && (a as ScalarValue)?.Value is int) { return (int)((ScalarValue)a).Value! == 3; } return false; }; // The code we're interested in; the `true.Equals()` overhead is normally added when // this is used with Serilog var compiled = SerilogExpression.Compile("A = 3"); _expressionFilter = evt => ExpressionResult.IsTrue(compiled(evt)); Assert.True(_trivialFilter(_event) && _handwrittenFilter(_event) && _expressionFilter(_event)); } [Benchmark] public void TrivialFilter() { _trivialFilter(_event); } [Benchmark(Baseline = true)] public void HandwrittenFilter() { _handwrittenFilter(_event); } [Benchmark] public void ExpressionFilter() { _expressionFilter(_event); } } ================================================ FILE: test/Serilog.Expressions.PerformanceTests/Harness.cs ================================================ // Copyright 2013-2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // // http://www.apache.org/licenses/LICENSE-2.0 // // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. using BenchmarkDotNet.Running; using Serilog.Expressions.PerformanceTests; using Xunit; namespace Serilog.PerformanceTests; public class Harness { [Fact] public void ComparisonBenchmark() { BenchmarkRunner.Run(); } } ================================================ FILE: test/Serilog.Expressions.PerformanceTests/Serilog.Expressions.PerformanceTests.csproj ================================================ net9.0 true false all runtime; build; native; contentfiles; analyzers; buildtransitive ================================================ FILE: test/Serilog.Expressions.PerformanceTests/Support/Some.cs ================================================ using Serilog.Events; using Xunit.Sdk; namespace Serilog.Expressions.PerformanceTests.Support; static class Some { public static LogEvent InformationEvent(string messageTemplate = "Hello, world!", params object[] propertyValues) { return LogEvent(LogEventLevel.Information, messageTemplate, propertyValues); } public static LogEvent WarningEvent(string messageTemplate = "Hello, world!", params object[] propertyValues) { return LogEvent(LogEventLevel.Warning, messageTemplate, propertyValues); } public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "Hello, world!", params object[] propertyValues) { var log = new LoggerConfiguration().CreateLogger(); #pragma warning disable Serilog004 // Constant MessageTemplate verifier if (!log.BindMessageTemplate(messageTemplate, propertyValues, out var template, out var properties)) #pragma warning restore Serilog004 // Constant MessageTemplate verifier { throw new XunitException("Template could not be bound."); } return new(DateTimeOffset.Now, level, null, template, properties); } } ================================================ FILE: test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv ================================================ // Primitives null ⇶ null true ⇶ true false ⇶ false // Arrays [] ⇶ [] [5, 6] ⇶ [5, 6] [5, undefined()] ⇶ [5] [1] ⇶ [1] [[1]] ⇶ [[1]] [1, ..[2, 3], 4, ..[undefined(), 5]] ⇶ [1, 2, 3, 4, 5] // Objects {} ⇶ {} {a: 1, 'b c': 2} ⇶ {a: 1, 'b c': 2} {User} ⇶ {User: {Id: 42, Name: 'nblumhardt'}} {@l} ⇶ {'@l': @l} {a: 1, a: 2} ⇶ {a: 2} {a: 1, b: undefined()} ⇶ {a: 1} {a: 1, a: undefined()} ⇶ {} {a: 1, ..{b: 2, c: 3}} ⇶ {a: 1, b: 2, c: 3} {a: 1, ..{b: 2}, a: undefined()} ⇶ {b: 2} {a: 1, ..{b: 2}, b: undefined()} ⇶ {a: 1} {a: 1, ..{a: undefined()}} ⇶ {a: 1} {..{a: 1}, ..{b: 2, c: 3}} ⇶ {a: 1, b: 2, c: 3} {a: 1}['a'] ⇶ 1 {a: 1}['A'] ⇶ undefined() {a: 1}.a ⇶ 1 {a: 1}.A ⇶ undefined() ElementAt({a: 1}, 'A') ⇶ undefined() ElementAt({a: 1}, 'A') ci ⇶ 1 // Strings '' ⇶ '' 'foo' ⇶ 'foo' '''' ⇶ '''' 'a' ⇶ 'a' '''b' ⇶ '''b' 'b''' ⇶ 'b''' 'a''b' ⇶ 'a''b' '😂' ⇶ '😂' // Numbers 0 ⇶ 0 0.0 ⇶ 0 1 ⇶ 1 1.0 ⇶ 1 123 ⇶ 123 1.23 ⇶ 1.23 -1 ⇶ -1 -1.23 ⇶ -1.23 // Math 1 + 1 ⇶ 2 1 + 2 + 3 ⇶ 6 'foo' + 1 ⇶ undefined() 1 - 2 ⇶ -1 -1 - -1 ⇶ 0 3 - 2 ⇶ 1 'foo' - - 1 ⇶ undefined() 1.0 + 2.0 ⇶ 3 1 * 0 ⇶ 0 12 * 12 ⇶ 144 3 / 1 ⇶ 3 3 / 0 ⇶ undefined() 3 ^ 2 ⇶ 9 0 ^ 1 ⇶ 0 0 ^ 0 ⇶ 1 0 % 0 ⇶ undefined() 0 % 1 ⇶ 0 1 % 0 ⇶ undefined() 1 % 1 ⇶ 0 5 % 2 ⇶ 1 79228162514264337593543950334 + 1 ⇶ 79228162514264337593543950335 round(1.4343434343, 2) ⇶ 1.43 round(1.435, 2) ⇶ 1.44 round(0.00000000, 5) ⇶ 0 round(0.99999999, 5) ⇶ 1 52 ^ -3 ⇶ 0.00000711197086936732 2 ^ 3.14 ⇶ 8.81524092701289 // Boolean logic true or false and false ⇶ true (true or false) and false ⇶ false not false and false ⇶ false not (false and false) ⇶ true false and true ⇶ false false and false ⇶ false true and true ⇶ true true and null ⇶ false false and null ⇶ false null and null ⇶ false undefined() and undefined() ⇶ false undefined() and true ⇶ false true and undefined() ⇶ false false or false ⇶ false true or false ⇶ true true or true ⇶ true false or true ⇶ true true or null ⇶ true null or true ⇶ true true or undefined() ⇶ true undefined() or true ⇶ true undefined() or undefined() ⇶ false not true ⇶ false not false ⇶ true not undefined() ⇶ true not(true) ⇶ false not(false and false) ⇶ true not(undefined()) ⇶ true 'not a bool' or true ⇶ true // Coalesce coalesce(null, 1) ⇶ 1 coalesce(undefined(), 1) ⇶ 1 coalesce(1, null) ⇶ 1 coalesce(undefined(), undefined()) ⇶ undefined() coalesce(null, null) ⇶ null coalesce(null, null, 3) ⇶ 3 coalesce() ⇶ undefined() coalesce(1) ⇶ 1 // Identifiers A ⇶ undefined() _a ⇶ undefined() a1 ⇶ undefined() // EQ 1 = 1 ⇶ true 1 = 0 ⇶ false null = 0 ⇶ false null = null ⇶ true null = undefined() ⇶ undefined() undefined() = undefined() ⇶ undefined() // NEQ 1 <> 1 ⇶ false 1 <> 0 ⇶ true null <> 0 ⇶ true null <> null ⇶ false undefined() <> null ⇶ undefined() undefined() <> undefined() ⇶ undefined() // LT 1 < 2 ⇶ true 1 < 1 ⇶ false 1 < 0 ⇶ false null < 0 ⇶ undefined() null < null ⇶ undefined() undefined() < null ⇶ undefined() undefined() < undefined() ⇶ undefined() // LTE 1 <= 2 ⇶ true 1 <= 1 ⇶ true 1 <= 0 ⇶ false null <= 0 ⇶ undefined() null <= null ⇶ undefined() undefined() <= null ⇶ undefined() undefined() <= undefined() ⇶ undefined() // GT 3 > 1 ⇶ true 1 > 1 ⇶ false 1 > 0 ⇶ true null > 0 ⇶ undefined() null > null ⇶ undefined() undefined() > null ⇶ undefined() undefined() > undefined() ⇶ undefined() // GTE 2 >= 1 ⇶ true 1 >= 1 ⇶ true 1 >= 0 ⇶ true -1 >= 0 ⇶ false null >= 0 ⇶ undefined() null >= null ⇶ undefined() undefined() >= null ⇶ undefined() undefined() >= undefined() ⇶ undefined() // in/not in 1 in [1, 2, 3] ⇶ true 5 in [1, 2, 3] ⇶ false 1 not in [1, 2, 3] ⇶ false 5 not in [1, 2, 3] ⇶ true undefined() in [1, 2, 3] ⇶ undefined() undefined() not in [1, 2, 3] ⇶ undefined() 1 in undefined() ⇶ undefined() null in [1, null, 3] ⇶ true // is null/is not null null is null ⇶ true null is not null ⇶ false undefined() is null ⇶ true 10 is null ⇶ false // Property names and accessors [5, 6, 7][1] ⇶ 6 User.Name ⇶ 'nblumhardt' {Name: 'nblumhardt'}.Name ⇶ 'nblumhardt' // Wildcards [1,2,3][?] > 2 ⇶ true [1,2,3][*] > 2 ⇶ false {k:'test'}[?] = 'test' ⇶ true {k:'test'}[?] like 'test' ⇶ true {k:'test'}[?] like 'TEST' ⇶ false {k:'test'}[?] like 'TEST' ci ⇶ true {k:'test'}[?] like '%TES%' ci ⇶ true {k:'test'}[?] = 'none' ⇶ false test_dict({k:'test'})[?] = 'test' ⇶ true test_dict({k:'test'})[?] like 'test' ⇶ true test_dict({k:'test'})[?] like 'TEST' ⇶ false test_dict({k:'test'})[?] like 'TEST' ci ⇶ true test_dict({k:'test'})[?] like '%TES%' ci ⇶ true test_dict({k:'test'})[?] = 'none' ⇶ false // Text and regex ismatch('foo', 'f') ⇶ true indexofmatch('foo', 'o') ⇶ 1 ismatch('foo', '^f') ⇶ true ismatch('foo', 'F') ⇶ false ismatch('foo', 'F') ci ⇶ true ismatch('foo', '^o') ⇶ false indexofmatch('foo', 'x') ⇶ -1 substring('abcd', 1, 2) ⇶ 'bc' substring('abcd', 1) ⇶ 'bcd' concat('a', 'b', 'c') ⇶ 'abc' concat('a', 42, 'c') ⇶ undefined() concat('a', undefined()) ⇶ undefined() concat(undefined(), 'b') ⇶ undefined() // Conditional if true then 1 else 2 ⇶ 1 if 1 + 2 = 3 then 1 else 2 ⇶ 1 if false then 1 else 2 ⇶ 2 if undefined() then 1 else 2 ⇶ 2 if 'string' then 1 else 2 ⇶ 2 if true then if false then 1 else 2 else 3 ⇶ 2 // ToString tostring(16) ⇶ '16' tostring('test') ⇶ 'test' tostring({}) ⇶ undefined() tostring([]) ⇶ undefined() tostring(16, '000') ⇶ '016' tostring(null) ⇶ undefined() tostring(undefined()) ⇶ undefined() tostring('test', '000') ⇶ 'test' tostring('test', []) ⇶ undefined() tostring('test', 42) ⇶ undefined() tostring(16, undefined()) ⇶ '16' tostring(16, null) ⇶ '16' // Tests are in fr-FR tostring(16.3) ⇶ '16,3' // TypeOf typeof(undefined()) ⇶ 'undefined' typeof('test') ⇶ 'System.String' typeof(10) ⇶ 'System.Decimal' typeof(true) ⇶ 'System.Boolean' typeof(null) ⇶ 'null' typeof([]) ⇶ 'array' typeof({}) ⇶ 'object' typeof(@x) ⇶ 'System.DivideByZeroException' // UtcDateTime tostring(utcdatetime(now()), 'o') like '20%' ⇶ true // Case comparison 'test' = 'TEST' ⇶ false 'tschüß' = 'TSCHÜSS' ⇶ false 'ὈΔΥΣΣΕΎΣ!' = 'ὀδυσσεύς!' ⇶ false 'ὈΔΥΣΣΕΎΣ!' = 'ὀδυσσεύσ!' ⇶ false 'test' = 'TEST' ci ⇶ true 'tschüß' = 'TSCHÜSS' ci ⇶ false 'ὈΔΥΣΣΕΎΣ!' = 'ὀδυσσεύσ!' ci ⇶ true null = 0 ci ⇶ false null = null ci ⇶ true null = undefined() ci ⇶ undefined() undefined() = undefined() ci ⇶ undefined() // Like 'test' like 'test' ⇶ true 'test' like 'Test' ⇶ false 'test' like 'Test' ci ⇶ true 'test' like '%st' ⇶ true 'test' like '_est' ⇶ true 'test' like 't%t' ⇶ true 'test' like 't_st' ⇶ true 'test' like 'te%' ⇶ true 'test' like 'tes_' ⇶ true 'test' like '%su' ⇶ false 'test' like '_esu' ⇶ false 'test' like 't%u' ⇶ false 'test' like 't_su' ⇶ false 'test' like 'ue%' ⇶ false 'test' like 'ues_' ⇶ false 'test' like '%s_' ⇶ true 'test' like '%' ⇶ true 'test' like 't%s%' ⇶ true 'test' like 'es' ⇶ false 'test' like '' ⇶ false '' like '' ⇶ true // Built-ins @m ⇶ 'Hello, World!' @mt ⇶ 'Hello, {Name}!' tostring(@x) like 'System.DivideByZeroException%' ⇶ true @l ⇶ 'Warning' @sp ⇶ 'bb1111820570b80e' @tr ⇶ '1befc31e94b01d1a473f63a7905f6c9b' // Inspect inspect(@x).Message ⇶ 'Attempted to divide by zero.' // Replace replace('xYX', 'x', 'z') ⇶ 'zYX' replace('xYX', 'x', 'z') ci ⇶ 'zYz' replace('mess', 'ss', 'an') ⇶ 'mean' replace('mess', 's', 'an') ⇶ 'meanan' replace('xyz', 'x', '$0') ⇶ '$0yz' replace('xyz', 'x', concat('$', '0')) ⇶ '$0yz' // Nest nest({'a.b': 1, 'a.c': 2, 'a.c.d': 3}) ⇶ {a: {b: 1, c: 2, 'c.d': 3}} nest('a.b.c') ⇶ undefined() ================================================ FILE: test/Serilog.Expressions.Tests/Cases/template-encoding-cases.asv ================================================ {@m} ⇶ (Hello, nblumhardt!) {'world'}! ⇶ (world)! {unsafe('world')}! ⇶ world! {substring('world', 0, 2)}! ⇶ (wo)! |{somethingnothere}| ⇶ |()| {#if true}x{'A'}x{#end} ⇶ x(A)x {#each a in [1,2,3]}<{a}>{#delimit},{#end} ⇶ <(1)>,<(2)>,<(3)> { {name: 'world' } }! ⇶ ({"name":"world"})! ================================================ FILE: test/Serilog.Expressions.Tests/Cases/template-evaluation-cases.asv ================================================ Hello, {'world'}! ⇶ Hello, world! {@l} ⇶ Information {@l:u3} ⇶ INF { {level: ToString(@l, 'u3')} } ⇶ {"level":"INF"} Items are {[1, 2]} ⇶ Items are [1,2] Members are { {a: 1, 'b c': 2} } ⇶ Members are {"a":1,"b c":2} {@p} ⇶ {"Name":"nblumhardt"} Hello, {'my } brackety { {}} friends'}! ⇶ Hello, my } brackety { {}} friends! Text only ⇶ Text only {{ Escaped {{ left {{ ⇶ { Escaped { left { }} Escaped }} right }} ⇶ } Escaped } right } Formatted {42:0000} ⇶ Formatted 0042 Aligned {42,4}! ⇶ Aligned 42! Left {42,-4}! ⇶ Left 42 ! Under width {42,0}! ⇶ Under width 42! {@m} ⇶ Hello, nblumhardt! Hello, {#if true}world{#end}! ⇶ Hello, world! Hello, {#if true}w{42}d{#end}! ⇶ Hello, w42d! Hello, {#if 1 = 1}world{#else}there{#end}! ⇶ Hello, world! Hello, {#if 1 = 2}world{#else}there{#end}! ⇶ Hello, there! Hello, {#if undefined()}world{#else}there{#end}! ⇶ Hello, there! A{#if false}B{#else if false}C{#else if true}D{#else}E{#end} ⇶ AD A{#if false}B{#else if true}C{#end} ⇶ AC {#if true}A{#if false}B{#else}C{#end}D{#end} ⇶ ACD {#each a in [1,2,3]}<{a}>{#delimit},{#end} ⇶ <1>,<2>,<3> {#each a, i in [1,2,3]}<{a}>({i}){#delimit},{#end} ⇶ <1>(0),<2>(1),<3>(2) {#each a in {x: 1, y: 2}}{a}{#end} ⇶ xy {#each a, b in {x: 1, y: 2}}{a}.{b}{#end} ⇶ x.1y.2 {#each a, b in {x: {y: 'z'}}}{#each c, d in b}A: {a}, C: {c}, D: {d}{#end}{#end} ⇶ A: x, C: y, D: z {#if true}A{#each a in [1]}B{a}{#end}C{#end}D ⇶ AB1CD {#each a in []}{a}!{#else}none{#end} ⇶ none Culture-specific {42.34} ⇶ Culture-specific 42,34 {rest()} ⇶ {"Name":"nblumhardt"} {Name} {rest()} ⇶ nblumhardt {} {rest(true)} ⇶ {} {@t:yyyy-MM-dd HH:mm:ss.ffff zzz} ⇶ 2000-12-31 23:59:58.1230 +10:00 {@t:yyyy-MM-dd HH:mm:ss.ffff zzz------------------} ⇶ 2000-12-31 23:59:58.1230 +10:00------------------ ================================================ FILE: test/Serilog.Expressions.Tests/Cases/translation-cases.asv ================================================ // Like A like 'a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a$'), -1) A like 'a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a'), -1) A like '%a%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a'), -1) A like '%a' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a$'), -1) A like '%a_b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, 'a.b'), -1) A like 'a%b%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '^a(?:.|\r|\n)*b'), -1) A like '%' ⇶ _Internal_NotEqual(_Internal_IndexOfMatch(A, '.*'), -1) // Root properties @p['Test'] ⇶ Test @p[Test] ⇶ @p[Test] // Variadics coalesce(A, B, C, D) ⇶ coalesce(A, coalesce(B, coalesce(C, D))) // Wildcards! A[?] ⇶ _Internal_Any(A, |$$p0| {$$p0}) A or B[*] ⇶ _Internal_Or(A, _Internal_All(B, |$$p0| {$$p0})) not (A is not null) or not (A[?] = 'a') ⇶ _Internal_Or(_Internal_Not(_Internal_IsNotNull(A)), _Internal_Not(_Internal_Any(A, |$$p0| {_Internal_Equal($$p0, 'a')}))) A[?].B[*].C = D ⇶ _Internal_Any(A, |$$p1| {_Internal_All($$p1.B, |$$p0| {_Internal_Equal($$p0.C, D)})}) ================================================ FILE: test/Serilog.Expressions.Tests/ConfigurationTests.cs ================================================ using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests; public class ConfigurationTests { [Fact] public void ExpressionsControlConditionalSinks() { var sink = new CollectingSink(); var logger = new LoggerConfiguration() .WriteTo.Conditional("A = 1 or A = 2", wt => wt.Sink(sink)) .CreateLogger(); foreach (var a in Enumerable.Range(0, 5)) logger.Information("{A}", a); Assert.Equal(2, sink.Events.Count); } [Fact] public void ExpressionsControlConditionalEnrichment() { var sink = new CollectingSink(); var logger = new LoggerConfiguration() .Enrich.When("A = 1 or A = 2", e => e.WithProperty("B", 1)) .WriteTo.Sink(sink) .CreateLogger(); foreach (var a in Enumerable.Range(0, 5)) logger.Information("{A}", a); Assert.Equal(2, sink.Events.Count(e => e.Properties.ContainsKey("B"))); } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionCompilerTests.cs ================================================ using Serilog.Events; using Serilog.Expressions.Tests.Support; using Xunit; // ReSharper disable CoVariantArrayConversion namespace Serilog.Expressions.Tests; public class ExpressionCompilerTests { [Fact] public void ExpressionsEvaluateStringEquality() { AssertEvaluation("Fruit = 'Apple'", Some.InformationEvent("Snacking on {Fruit}", "Apple"), Some.InformationEvent(), Some.InformationEvent("Snacking on {Fruit}", "Acerola")); } [Fact] public void ComparisonsAreCaseSensitive() { AssertEvaluation("Fruit = 'Apple'", Some.InformationEvent("Snacking on {Fruit}", "Apple"), Some.InformationEvent("Snacking on {Fruit}", "APPLE")); } [Fact] public void ExpressionsEvaluateStringContent() { AssertEvaluation("Fruit like '%pp%'", Some.InformationEvent("Snacking on {Fruit}", "Apple"), Some.InformationEvent("Snacking on {Fruit}", "Acerola")); } [Fact] public void ExpressionsEvaluateStringPrefix() { AssertEvaluation("Fruit like 'Ap%'", Some.InformationEvent("Snacking on {Fruit}", "Apple"), Some.InformationEvent("Snacking on {Fruit}", "Acerola")); } [Fact] public void ExpressionsEvaluateStringSuffix() { AssertEvaluation("Fruit like '%le'", Some.InformationEvent("Snacking on {Fruit}", "Apple"), Some.InformationEvent("Snacking on {Fruit}", "Acerola")); } [Fact] public void LikeIsCaseSensitive() { AssertEvaluation("Fruit like 'apple'", Some.InformationEvent("Snacking on {Fruit}", "apple"), Some.InformationEvent("Snacking on {Fruit}", "Apple")); } [Fact] public void ExpressionsEvaluateNumericComparisons() { AssertEvaluation("Volume > 11", Some.InformationEvent("Adding {Volume} L", 11.5), Some.InformationEvent("Adding {Volume} L", 11)); } [Fact] public void ExpressionsEvaluateWildcardsOnCollectionItems() { AssertEvaluation("Items[?] like 'C%'", Some.InformationEvent("Cart contains {@Items}", [new[] { "Tea", "Coffee" }]), // Test helper doesn't correct this case Some.InformationEvent("Cart contains {@Items}", [new[] { "Apricots" }])); } [Fact] public void ExpressionsEvaluateBuiltInProperties() { AssertEvaluation("@l = 'Information'", Some.InformationEvent(), Some.WarningEvent()); } [Fact] public void ExpressionsEvaluateExistentials() { AssertEvaluation("AppId is not null", Some.InformationEvent("{AppId}", 10), Some.InformationEvent("{AppId}", [null]), Some.InformationEvent()); } [Fact] public void ExpressionsLogicalOperations() { AssertEvaluation("A and B", Some.InformationEvent("{A} {B}", true, true), Some.InformationEvent("{A} {B}", true, false), Some.InformationEvent()); } [Fact] public void ExpressionsEvaluateSubproperties() { AssertEvaluation("Cart.Total > 10", Some.InformationEvent("Checking out {@Cart}", new { Total = 20 }), Some.InformationEvent("Checking out {@Cart}", new { Total = 5 })); } [Fact] public void SequenceLengthCanBeDetermined() { AssertEvaluation("length(Items) > 1", Some.InformationEvent("Checking out {Items}", [new[] { "pears", "apples" }]), Some.InformationEvent("Checking out {Items}", [new[] { "pears" }])); } [Fact] public void InMatchesLiterals() { AssertEvaluation("@l in ['Warning', 'Error']", Some.LogEvent(LogEventLevel.Error, "Hello"), Some.InformationEvent("Hello")); } [Fact] public void InExaminesSequenceValues() { AssertEvaluation("5 not in Numbers", Some.InformationEvent("{Numbers}", new []{1, 2, 3}), Some.InformationEvent("{Numbers}", new [] { 1, 5, 3 }), Some.InformationEvent()); } [Theory] [InlineData("now(1)", "The function `now` accepts no arguments.")] [InlineData("length()", "The function `length` accepts one argument, `value`.")] [InlineData("length(1, 2)", "The function `length` accepts one argument, `value`.")] [InlineData("round()", "The function `round` accepts two arguments, `value` and `places`.")] [InlineData("substring()", "The function `substring` accepts arguments `value`, `startIndex`, and `length` (optional).")] public void ReportsArityMismatches(string call, string expectedError) { // These will eventually be reported gracefully by `TryCompile()`... var ex = Assert.Throws(() => SerilogExpression.Compile(call)); Assert.Equal(expectedError, ex.Message); } static void AssertEvaluation(string expression, LogEvent match, params LogEvent[] noMatches) { var sink = new CollectingSink(); var log = new LoggerConfiguration() .Filter.ByIncludingOnly(expression) .WriteTo.Sink(sink) .CreateLogger(); foreach (var noMatch in noMatches) log.Write(noMatch); log.Write(match); Assert.Single(sink.Events); Assert.Same(match, sink.Events.Single()); } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionEvaluationTests.cs ================================================ using System.Diagnostics; using System.Globalization; using Serilog.Events; using Serilog.Expressions.Runtime; using Serilog.Expressions.Tests.Support; using Serilog.Parsing; using Xunit; namespace Serilog.Expressions.Tests; public class ExpressionEvaluationTests { public static IEnumerable ExpressionEvaluationCases => AsvCases.ReadCases("expression-evaluation-cases.asv"); [Theory] [MemberData(nameof(ExpressionEvaluationCases))] public void ExpressionsAreCorrectlyEvaluated(string expr, string result) { var evt = new LogEvent( new DateTimeOffset(2025, 5, 15, 13, 12, 11, 789, TimeSpan.FromHours(10)), LogEventLevel.Warning, new DivideByZeroException(), new MessageTemplateParser().Parse("Hello, {Name}!"), new [] { new LogEventProperty("Name", new ScalarValue("World")), new LogEventProperty("User", new StructureValue(new[] { new LogEventProperty("Id", new ScalarValue(42)), new LogEventProperty("Name", new ScalarValue("nblumhardt")), })) }, ActivityTraceId.CreateFromString("1befc31e94b01d1a473f63a7905f6c9b"), ActivitySpanId.CreateFromString("bb1111820570b80e")); var frFr = CultureInfo.GetCultureInfoByIetfLanguageTag("fr-FR"); var testHelpers = new TestHelperNameResolver(); var actual = SerilogExpression.Compile(expr, formatProvider: frFr, testHelpers)(evt); var expected = SerilogExpression.Compile(result, nameResolver: testHelpers)(evt); if (expected is null) { Assert.True(actual is null, $"Expected value: undefined{Environment.NewLine}Actual value: {Display(actual)}"); } else { Assert.True(Coerce.IsTrue(RuntimeOperators._Internal_Equal(StringComparison.OrdinalIgnoreCase, actual, expected)), $"Expected value: {Display(expected)}{Environment.NewLine}Actual value: {Display(actual)}"); } } static string Display(LogEventPropertyValue? value) { if (value == null) return "undefined"; return value.ToString(); } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionParserTests.cs ================================================ using Serilog.Expressions.Parsing; using Xunit; namespace Serilog.Expressions.Tests; public class ExpressionParserTests { [Theory] [InlineData("contains(@Message, 'some text')", "contains(@Message, 'some text')")] [InlineData("AProperty", null)] [InlineData("AProperty = 'some text'", "_Internal_Equal(AProperty, 'some text')")] [InlineData("AProperty = 42", "_Internal_Equal(AProperty, 42)")] [InlineData("@Properties['0'] = 42", "_Internal_Equal(@Properties['0'], 42)")] [InlineData("AProperty = null", "_Internal_Equal(AProperty, null)")] [InlineData("AProperty <> 42", "_Internal_NotEqual(AProperty, 42)")] [InlineData("has(AProperty)", null)] [InlineData("not A", "_Internal_Not(A)")] [InlineData("not (1 = 2)", "_Internal_Not(_Internal_Equal(1, 2))")] [InlineData("not(1 = 2)", "_Internal_Not(_Internal_Equal(1, 2))")] [InlineData("AProperty = 3 and Another < 12", "_Internal_And(_Internal_Equal(AProperty, 3), _Internal_LessThan(Another, 12))")] [InlineData("@Timestamp", null)] [InlineData("1 + (2 + 3) * 4", "_Internal_Add(1, _Internal_Multiply(_Internal_Add(2, 3), 4))")] [InlineData("AProperty.Another = 3", "_Internal_Equal(AProperty.Another, 3)")] [InlineData("AProperty /2/3 = 7", "_Internal_Equal(_Internal_Divide(_Internal_Divide(AProperty, 2), 3), 7)")] [InlineData("AProperty[0] = 1", "_Internal_Equal(AProperty[0], 1)")] [InlineData("AProperty[0] = note", "_Internal_Equal(AProperty[0], note)")] // Ensure correct tokenization of 'not' [InlineData("equal(AProperty[0].Description, 1)", null)] [InlineData("equal(AProperty[?].Description, 1)", null)] [InlineData("equal(AProperty[*].Description, 1)", null)] [InlineData("equal(AProperty[ * ].Description, 1)", "equal(AProperty[*].Description, 1)")] [InlineData("AProperty like '%foo'", "_Internal_Like(AProperty, '%foo')")] [InlineData("AProperty not like '%foo'", "_Internal_NotLike(AProperty, '%foo')")] [InlineData("A is null", "_Internal_IsNull(A)")] [InlineData("A IS NOT NULL", "_Internal_IsNotNull(A)")] [InlineData("A is not null or B", "_Internal_Or(_Internal_IsNotNull(A), B)")] [InlineData("@EventType = 0xC0ffee", "_Internal_Equal(@EventType, 12648430)")] [InlineData("@Level in ['Error', 'Warning']", "_Internal_In(@Level, ['Error', 'Warning'])")] [InlineData("5 not in [1, 2]", "_Internal_NotIn(5, [1, 2])")] [InlineData("1+1", "_Internal_Add(1, 1)")] [InlineData("'te\nst'", null)] [InlineData("A.B is null", "_Internal_IsNull(A.B)")] public void ValidSyntaxIsAccepted(string input, string? expected = null) { var expressionParser = new ExpressionParser(); var roundTrip = expressionParser.Parse(input).ToString(); Assert.Equal(expected ?? input, roundTrip); } [Theory] [InlineData(" ")] [InlineData("\"Hello!\"")] [InlineData("@\"Hello!\"")] [InlineData("$FE6789E6")] [InlineData("AProperty == 'some text'")] [InlineData("AProperty != 42")] [InlineData("!(1 == 2)")] [InlineData("AProperty = 3 && Another < 12")] [InlineData("A = 99999999999999999999999999999999999999999999")] [InlineData("A = 0x99999999999999999999999999999999999999999999")] public void InvalidSyntaxIsRejected(string input) { var expressionParser = new ExpressionParser(); Assert.Throws(() => expressionParser.Parse(input)); } [Theory] [InlineData("A = 'b", "Syntax error: unexpected end of input, expected `'`.")] [InlineData("A or B) and C", "Syntax error (line 1, column 7): unexpected `)`.")] [InlineData("A lik3 C", "Syntax error (line 1, column 3): unexpected identifier `lik3`.")] [InlineData("A > 1234f", "Syntax error (line 1, column 9): unexpected `f`, expected digit.")] public void PreciseErrorsAreReported(string input, string expectedMessage) { var expressionParser = new ExpressionParser(); var ex = Assert.Throws(() => expressionParser.Parse(input)); Assert.Equal(expectedMessage, ex.Message); } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionTranslationTests.cs ================================================ using Serilog.Expressions.Compilation; using Serilog.Expressions.Parsing; using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests; public class ExpressionTranslationTests { public static IEnumerable ExpressionTranslationCases => AsvCases.ReadCases("translation-cases.asv"); [Theory] [MemberData(nameof(ExpressionTranslationCases))] public void ExpressionsAreCorrectlyTranslated(string expr, string expected) { var parsed = new ExpressionParser().Parse(expr); var translated = ExpressionCompiler.Translate(parsed); var actual = translated.ToString(); Assert.Equal(expected, actual); } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionValidationTests.cs ================================================ using Xunit; namespace Serilog.Expressions.Tests; public class ExpressionValidationTests { [Theory] [InlineData("IsMatch(Name, '[invalid')", "Invalid regular expression")] [InlineData("IndexOfMatch(Text, '(?<')", "Invalid regular expression")] [InlineData("IsMatch(Name, '(?P)')", "Invalid regular expression")] [InlineData("IsMatch(Name, '(unclosed')", "Invalid regular expression")] [InlineData("IndexOfMatch(Text, '*invalid')", "Invalid regular expression")] public void InvalidRegularExpressionsAreReportedGracefully(string expression, string expectedErrorFragment) { var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.False(result); Assert.Contains(expectedErrorFragment, error); Assert.Null(compiled); } [Theory] [InlineData("UnknownFunction()", "The function name `UnknownFunction` was not recognized.")] [InlineData("foo(1, 2, 3)", "The function name `foo` was not recognized.")] [InlineData("MyCustomFunc(Name)", "The function name `MyCustomFunc` was not recognized.")] [InlineData("notAFunction()", "The function name `notAFunction` was not recognized.")] public void UnknownFunctionNamesAreReportedGracefully(string expression, string expectedError) { var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.False(result); Assert.Equal(expectedError, error); Assert.Null(compiled); } [Theory] [InlineData("Length(Name) ci")] [InlineData("Round(Value, 2) ci")] [InlineData("Now() ci")] [InlineData("TypeOf(Value) ci")] [InlineData("IsDefined(Prop) ci")] public void InvalidCiModifierUsageCompilesWithWarning(string expression) { var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.True(result, $"Failed to compile: {error}"); Assert.NotNull(compiled); Assert.Null(error); } [Theory] [InlineData("Contains(Name, 'test') ci")] [InlineData("StartsWith(Path, '/api') ci")] [InlineData("EndsWith(File, '.txt') ci")] [InlineData("IsMatch(Email, '@example') ci")] [InlineData("IndexOfMatch(Text, 'pattern') ci")] [InlineData("IndexOf(Name, 'x') ci")] [InlineData("Name = 'test' ci")] [InlineData("Name <> 'test' ci")] [InlineData("Name like '%test%' ci")] public void ValidCiModifierUsageCompiles(string expression) { var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.True(result, $"Failed to compile: {error}"); Assert.NotNull(compiled); Assert.Null(error); } [Fact] public void FirstErrorIsReportedInComplexExpressions() { var expression = "UnknownFunc() and Length(Value) > 5"; var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.False(result); Assert.Null(compiled); // Should report the first error encountered Assert.Contains("UnknownFunc", error); Assert.NotNull(error); } [Fact] public void ValidExpressionsStillCompileWithoutErrors() { var validExpressions = new[] { "IsMatch(Name, '^[A-Z]')", "IndexOfMatch(Text, '\\d+')", "Contains(Name, 'test') ci", "Length(Items) > 5", "Round(Value, 2)", "TypeOf(Data) = 'String'", "Name like '%test%'", "StartsWith(Path, '/') and EndsWith(Path, '.json')" }; foreach (var expr in validExpressions) { var result = SerilogExpression.TryCompile(expr, out var compiled, out var error); Assert.True(result, $"Failed to compile: {expr}. Error: {error}"); Assert.NotNull(compiled); Assert.Null(error); } } [Fact] public void CompileMethodStillThrowsForInvalidExpressions() { // Ensure Compile() method (not TryCompile) maintains throwing behavior for invalid expressions Assert.Throws(() => SerilogExpression.Compile("UnknownFunction()")); Assert.Throws(() => SerilogExpression.Compile("IsMatch(Name, '[invalid')")); // CI modifier on non-supporting functions compiles with warning var compiledWithCi = SerilogExpression.Compile("Length(Name) ci"); Assert.NotNull(compiledWithCi); Assert.Throws(() => SerilogExpression.Compile("IndexOfMatch(Text, '(?<')")); } [Theory] [InlineData("IsMatch(Name, Name)")] // Non-constant pattern [InlineData("IndexOfMatch(Text, Value)")] // Non-constant pattern public void NonConstantRegexPatternsHandledGracefully(string expression) { // These should compile but may log warnings (not errors) var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); // These compile successfully but return undefined at runtime Assert.True(result); Assert.NotNull(compiled); Assert.Null(error); } [Fact] public void RegexTimeoutIsRespected() { // This regex should compile fine - timeout only matters at runtime var expression = @"IsMatch(Text, '(a+)+b')"; var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.True(result); Assert.NotNull(compiled); Assert.Null(error); } [Fact] public void ComplexExpressionsReportFirstError() { var expression = "UnknownFunc1() or Length(Value) > 5"; var result = SerilogExpression.TryCompile(expression, out var compiled, out var error); Assert.False(result); Assert.Null(compiled); Assert.NotNull(error); // Should report the first error encountered during compilation Assert.Contains("UnknownFunc1", error); } [Fact] public void BackwardCompatibilityPreservedForInvalidCiUsage() { // These previously compiled (CI was silently ignored) // They should still compile with the new changes var expressions = new[] { "undefined() ci", "null = undefined() ci", "Length(Name) ci", "Round(Value, 2) ci" }; foreach (var expr in expressions) { var result = SerilogExpression.TryCompile(expr, out var compiled, out var error); Assert.True(result, $"Breaking change detected: {expr} no longer compiles. Error: {error}"); Assert.NotNull(compiled); Assert.Null(error); } } } ================================================ FILE: test/Serilog.Expressions.Tests/ExpressionValueTests.cs ================================================ using Serilog.Events; using Xunit; namespace Serilog.Expressions.Tests; public class ExpressionValueTests { [Fact] public void UndefinedResultsAreFalse() { Assert.False(ExpressionResult.IsTrue(null)); } [Fact] public void NonBooleanResultsAreFalse() { Assert.False(ExpressionResult.IsTrue(new ScalarValue(10))); } [Fact] public void TrueIsTrue() { Assert.True(ExpressionResult.IsTrue(new ScalarValue(true))); } [Fact] public void FalseIsNotTrue() { Assert.False(ExpressionResult.IsTrue(new ScalarValue(false))); } } ================================================ FILE: test/Serilog.Expressions.Tests/Expressions/NameResolverTests.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Serilog.Events; using Serilog.Expressions.Runtime; using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests.Expressions; public class NameResolverTests { // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? Magic(LogEventPropertyValue? number) { if (!Coerce.Numeric(number, out var num)) return null; return new ScalarValue(num + 42); } // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? SecretWordAt(string word, LogEventPropertyValue? index) { if (!Coerce.Numeric(index, out var i)) return null; return new ScalarValue(word[(int)i].ToString()); } class SecretWordResolver : NameResolver { readonly NameResolver _inner; readonly string _word; public SecretWordResolver(NameResolver inner, string word) { _inner = inner; _word = word; } public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) => _inner.TryResolveFunctionName(name, out implementation); public override bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue) { if (parameter.ParameterType == typeof(string)) { boundValue = _word; return true; } boundValue = null; return false; } } class LegacyLevelPropertyNameResolver: NameResolver { public override bool TryResolveBuiltInPropertyName(string alias, [NotNullWhen(true)] out string? target) { if (alias == "Level") { target = "l"; return true; } target = null; return false; } } [Fact] public void UserDefinedFunctionsAreCallableInExpressions() { var expr = SerilogExpression.Compile( "magic(10) + 3 = 55", nameResolver: new StaticMemberNameResolver(typeof(NameResolverTests))); Assert.True(Coerce.IsTrue(expr(Some.InformationEvent()))); } [Fact] public void UserDefinedFunctionsCanReceiveUserProvidedParameters() { var expr = SerilogExpression.Compile( "SecretWordAt(1) = 'e'", nameResolver: new SecretWordResolver(new StaticMemberNameResolver(typeof(NameResolverTests)), "hello")); Assert.True(Coerce.IsTrue(expr(Some.InformationEvent()))); } [Fact] public void BuiltInPropertiesCanBeAliased() { var expr = SerilogExpression.Compile( "@Level = 'Information'", nameResolver: new LegacyLevelPropertyNameResolver()); Assert.True(Coerce.IsTrue(expr(Some.InformationEvent()))); } } ================================================ FILE: test/Serilog.Expressions.Tests/Expressions/Runtime/LocalsTests.cs ================================================ using Serilog.Expressions.Runtime; using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests.Expressions.Runtime; public class LocalsTests { [Fact] public void NoValueIsDefinedInNoLocals() { Assert.False(Locals.TryGetValue(null, "A", out _)); } [Fact] public void ASetValueIsRetrieved() { var expected = Some.LogEventPropertyValue(); var locals = Locals.Set(null, "A", expected); Assert.True(Locals.TryGetValue(locals, "A", out var actual)); Assert.Same(expected, actual); } [Fact] public void ASetValueIsRetrievedFromMany() { var expected = Some.LogEventPropertyValue(); var locals = Locals.Set(null, "A", expected); locals = Locals.Set(locals, "B", Some.LogEventPropertyValue()); Assert.True(Locals.TryGetValue(locals, "A", out var actual)); Assert.Same(expected, actual); } [Fact] public void TheTopmostValueIsRetrievedForAName() { var expected = Some.LogEventPropertyValue(); var locals = Locals.Set(null, "A", Some.LogEventPropertyValue()); locals = Locals.Set(locals, "B", Some.LogEventPropertyValue()); locals = Locals.Set(locals, "A", expected); Assert.True(Locals.TryGetValue(locals, "A", out var actual)); Assert.Same(expected, actual); } } ================================================ FILE: test/Serilog.Expressions.Tests/Expressions/Runtime/RuntimeOperatorsTests.cs ================================================ using System.Reflection; using Serilog.Events; using Serilog.Expressions.Runtime; using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests.Expressions.Runtime; public class RuntimeOperatorsTests { [Fact] public void InspectReadsPublicPropertiesFromScalarValue() { var message = Some.String(); var ex = new DivideByZeroException(message); var scalar = new ScalarValue(ex); var inspected = RuntimeOperators.Inspect(scalar); var structure = Assert.IsType(inspected); var asProperties = structure.Properties.ToDictionary(p => p.Name, p => p.Value); Assert.Contains("Message", asProperties); Assert.Contains("StackTrace", asProperties); var messageResult = Assert.IsType(asProperties["Message"]); Assert.Equal(message, messageResult.Value); } [Fact] public void DeepInspectionReadsSubproperties() { var innerMessage = Some.String(); var inner = new DivideByZeroException(innerMessage); var ex = new TargetInvocationException(inner); var scalar = new ScalarValue(ex); var inspected = RuntimeOperators.Inspect(scalar, deep: new ScalarValue(true)); var structure = Assert.IsType(inspected); var innerStructure = Assert.IsType(structure.Properties.Single(p => p.Name == "InnerException").Value); var innerMessageValue = Assert.IsType(innerStructure.Properties.Single(p => p.Name == "Message").Value); Assert.Equal(innerMessage, innerMessageValue.Value); } } ================================================ FILE: test/Serilog.Expressions.Tests/FormatParityTests.cs ================================================ using Serilog.Events; using Serilog.Expressions.Tests.Support; using Serilog.Formatting; using Serilog.Formatting.Compact; using Serilog.Formatting.Json; using Serilog.Parsing; using Serilog.Templates; using Xunit; namespace Serilog.Expressions.Tests; /// /// These tests track the ability of Serilog.Expressions to faithfully reproduce the JSON formats implemented in /// Serilog and Serilog.Formatting.Compact. The tests jump through a few hoops to achieve byte-for-byte correctness; /// in practice, valid JSON in these formats can be constructed with simpler templates. /// public class FormatParityTests { // Implements CLEF-style `@@` escaping of property names that begin with `@`. // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? ClefEscape(LogEventPropertyValue? logEventProperties) { if (logEventProperties is not StructureValue st) return null; foreach (var check in st.Properties) { if (check.Name.Length > 0 && check.Name[0] == '@') { var properties = new List(); foreach (var member in st.Properties) { var property = new LogEventProperty( member.Name.Length > 0 && member.Name[0] == '@' ? "@" + member.Name : member.Name, member.Value); properties.Add(property); } return new StructureValue(properties, st.TypeTag); } } return logEventProperties; } // Renders a message template with old-style "quoted" strings (expression templates use the newer :lj formatting always). // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? ClassicRender(LogEventPropertyValue? messageTemplate, LogEventPropertyValue? properties) { if (messageTemplate is not ScalarValue {Value: string smt} || properties is not StructureValue stp) { return null; } var mt = new MessageTemplateParser().Parse(smt); var space = new StringWriter(); mt.Render(stp.Properties.ToDictionary(p => p.Name, p => p.Value), space); return new ScalarValue(space.ToString()); } // Constructs the Renderings property used in the old JSON format. // ReSharper disable once UnusedMember.Global public static LogEventPropertyValue? ClassicRenderings(LogEventPropertyValue? messageTemplate, LogEventPropertyValue? properties) { if (messageTemplate is not ScalarValue {Value: string smt} || properties is not StructureValue stp) { return null; } var mt = new MessageTemplateParser().Parse(smt); var tokensWithFormat = mt.Tokens .OfType() .Where(pt => pt.Format != null) .GroupBy(pt => pt.PropertyName); // ReSharper disable once PossibleMultipleEnumeration if (!tokensWithFormat.Any()) return null; var propertiesByName = stp.Properties.ToDictionary(p => p.Name, p => p.Value); var renderings = new List(); // ReSharper disable once PossibleMultipleEnumeration foreach (var propertyFormats in tokensWithFormat) { var values = new List(); foreach (var format in propertyFormats) { var sw = new StringWriter(); format.Render(propertiesByName, sw); values.Add(new StructureValue(new [] { new LogEventProperty("Format", new ScalarValue(format.Format)), new LogEventProperty("Rendering", new ScalarValue(sw.ToString())), })); } renderings.Add(new(propertyFormats.Key, new SequenceValue(values))); } return new StructureValue(renderings); } readonly ITextFormatter _clef = new CompactJsonFormatter(), _renderedClef = new RenderedCompactJsonFormatter(), _classic = new JsonFormatter(), _clefExpression = new ExpressionTemplate( "{ {@t: UtcDateTime(@t), @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..ClefEscape(@p)} }" + Environment.NewLine, null, new StaticMemberNameResolver(typeof(FormatParityTests))), _renderedClefExpression = new ExpressionTemplate( "{ {@t: UtcDateTime(@t), @m: ClassicRender(@mt, @p), @i: ToString(@i, 'x8'), @l: if @l = 'Information' then undefined() else @l, @x, ..ClefEscape(@p)} }" + Environment.NewLine, null, new StaticMemberNameResolver(typeof(FormatParityTests))), _classicExpression = new ExpressionTemplate( "{ {Timestamp: @t, Level: @l, MessageTemplate: @mt, Exception: @x, Properties: if IsDefined(@p[?]) then @p else undefined(), Renderings: ClassicRenderings(@mt, @p)} }" + Environment.NewLine, null, new StaticMemberNameResolver(typeof(FormatParityTests))); static string Render( ITextFormatter formatter, LogEvent logEvent) { var space = new StringWriter(); formatter.Format(logEvent, space); return space.ToString(); } void AssertWriteParity( LogEventLevel level, Exception? exception, string messageTemplate, params object[] propertyValues) { var sink = new CollectingSink(); using (var log = new LoggerConfiguration() .MinimumLevel.Is(LevelAlias.Minimum) .WriteTo.Sink(sink) .CreateLogger()) { log.Write(level, exception, messageTemplate, propertyValues); } var clef = Render(_clef, sink.SingleEvent); var clefExpression = Render(_clefExpression, sink.SingleEvent); Assert.Equal(clef, clefExpression); var renderedClef = Render(_renderedClef, sink.SingleEvent); var renderedClefExpression = Render(_renderedClefExpression, sink.SingleEvent); Assert.Equal(renderedClef, renderedClefExpression); var renderedClassic = Render(_classic, sink.SingleEvent); var renderedClassicExpression = Render(_classicExpression, sink.SingleEvent); Assert.Equal(renderedClassic, renderedClassicExpression); } [Fact] public void ParityIsMaintained() { AssertWriteParity(LogEventLevel.Information, null, "Hello, world!"); AssertWriteParity(LogEventLevel.Debug, new(), "Hello, {Name}, {Number:000}, {Another:#.00}", "world", 42, 3.1); } } ================================================ FILE: test/Serilog.Expressions.Tests/LoggingFilterSwitchTests.cs ================================================ using Serilog.Expressions.Tests.Support; using Xunit; namespace Serilog.Expressions.Tests; public class LoggingFilterSwitchTests { [Fact] public void WhenTheFilterExpressionIsModifiedTheFilterChanges() { var @switch = new LoggingFilterSwitch(); var sink = new CollectingSink(); var log = new LoggerConfiguration() .Filter.ControlledBy(@switch) .WriteTo.Sink(sink) .CreateLogger(); var v11 = Some.InformationEvent("Adding {Volume} L", 11); log.Write(v11); Assert.Same(v11, sink.SingleEvent); sink.Events.Clear(); @switch.Expression = "Volume > 12"; log.Write(v11); Assert.Empty(sink.Events); @switch.Expression = "Volume > 10"; log.Write(v11); Assert.Same(v11, sink.SingleEvent); sink.Events.Clear(); @switch.Expression = null; log.Write(v11); Assert.Same(v11, sink.SingleEvent); } } ================================================ FILE: test/Serilog.Expressions.Tests/Properties/launchSettings.json ================================================ { "profiles": { "test": { "commandName": "test" }, "test-dnxcore50": { "commandName": "test", "sdkVersion": "dnx-coreclr-win-x86.1.0.0-rc1-final" } } } ================================================ FILE: test/Serilog.Expressions.Tests/Serilog.Expressions.Tests.csproj ================================================  net9.0 true false all runtime; build; native; contentfiles; analyzers; buildtransitive PreserveNewest ================================================ FILE: test/Serilog.Expressions.Tests/Support/AsvCases.cs ================================================ namespace Serilog.Expressions.Tests.Support; // "Arrow-separated values ;-) ... convenient because the Unicode `⇶` character doesn't appear in // any of the cases themselves, and so we ignore any need for special character escaping (which is // troublesome when the language the cases are written in uses just about all special characters somehow // or other!). // // The ASV format informally supports `//` comment lines, as long as they don't contain the arrow character. static class AsvCases { static readonly string CasesPath = Path.Combine(AppDomain.CurrentDomain.BaseDirectory!, "Cases"); public static IEnumerable ReadCases(string filename) { return from line in File.ReadLines(Path.Combine(CasesPath, filename)) select line.Split("⇶", StringSplitOptions.RemoveEmptyEntries) into cols where cols.Length == 2 select cols.Select(c => c.Trim()).ToArray(); } } ================================================ FILE: test/Serilog.Expressions.Tests/Support/CollectingSink.cs ================================================ using Serilog.Core; using Serilog.Events; namespace Serilog.Expressions.Tests.Support; class CollectingSink : ILogEventSink { readonly List _events = new(); public List Events { get { return _events; } } public LogEvent SingleEvent { get { return _events.Single(); } } public void Emit(LogEvent logEvent) { _events.Add(logEvent); } } ================================================ FILE: test/Serilog.Expressions.Tests/Support/ParenthesizingEncoder.cs ================================================ using Serilog.Templates.Encoding; namespace Serilog.Expressions.Tests.Support; public class ParenthesizingEncoder : TemplateOutputEncoder { public override string Encode(string value) { return $"({value})"; } } ================================================ FILE: test/Serilog.Expressions.Tests/Support/Some.cs ================================================ using Serilog.Events; using Xunit.Sdk; namespace Serilog.Expressions.Tests.Support; static class Some { static int _next; public static LogEvent InformationEvent(string messageTemplate = "Hello, world!", params object?[] propertyValues) { return LogEvent(LogEventLevel.Information, messageTemplate, propertyValues); } public static LogEvent InformationEvent(DateTimeOffset timestamp, string messageTemplate = "Hello, world!", params object?[] propertyValues) { return LogEvent(timestamp, LogEventLevel.Information, messageTemplate, propertyValues); } public static LogEvent WarningEvent(string messageTemplate = "Hello, world!", params object?[] propertyValues) { return LogEvent(LogEventLevel.Warning, messageTemplate, propertyValues); } public static LogEvent LogEvent(LogEventLevel level, string messageTemplate = "Hello, world!", params object?[] propertyValues) { return LogEvent(DateTimeOffset.Now, level, messageTemplate, propertyValues); } public static LogEvent LogEvent(DateTimeOffset timestamp, LogEventLevel level, string messageTemplate = "Hello, world!", params object?[] propertyValues) { var log = new LoggerConfiguration().CreateLogger(); #pragma warning disable Serilog004 // Constant MessageTemplate verifier if (!log.BindMessageTemplate(messageTemplate, propertyValues, out var template, out var properties)) #pragma warning restore Serilog004 // Constant MessageTemplate verifier { throw new XunitException("Template could not be bound."); } return new(timestamp, level, null, template, properties); } public static object AnonymousObject() { return new {A = Int()}; } public static LogEventPropertyValue LogEventPropertyValue() { return new ScalarValue(AnonymousObject()); } static int Int() { return Interlocked.Increment(ref _next); } public static string String() { return $"+S_{Int()}"; } } ================================================ FILE: test/Serilog.Expressions.Tests/Support/StringHashPrefixingTheme.cs ================================================ using Serilog.Templates.Themes; namespace Serilog.Expressions.Tests.Support; static class StringHashPrefixingTheme { public static readonly TemplateTheme Instance = new(new Dictionary { [TemplateThemeStyle.String] = "#" }); } ================================================ FILE: test/Serilog.Expressions.Tests/Support/TestHelperNameResolver.cs ================================================ using System.Diagnostics.CodeAnalysis; using System.Reflection; using Serilog.Events; namespace Serilog.Expressions.Tests.Support; public class TestHelperNameResolver: NameResolver { public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation) { if (name == "test_dict") { implementation = GetType().GetMethod(nameof(TestDict))!; return true; } implementation = null; return false; } public static LogEventPropertyValue? TestDict(LogEventPropertyValue? value) { if (value is not StructureValue sv) return null; return new DictionaryValue(sv.Properties.Select(kv => KeyValuePair.Create(new ScalarValue(kv.Name), kv.Value))); } } ================================================ FILE: test/Serilog.Expressions.Tests/TemplateEncodingTests.cs ================================================ using Serilog.Expressions.Tests.Support; using Serilog.Templates; using Xunit; namespace Serilog.Expressions.Tests; public class TemplateEncodingTests { public static IEnumerable TemplateEvaluationCases => AsvCases.ReadCases("template-encoding-cases.asv"); [Theory] [MemberData(nameof(TemplateEvaluationCases))] public void TemplatesAreCorrectlyEvaluated(string template, string expected) { var evt = Some.InformationEvent("Hello, {Name}!", "nblumhardt"); var compiled = new ExpressionTemplate(template, encoder: new ParenthesizingEncoder()); var output = new StringWriter(); compiled.Format(evt, output); var actual = output.ToString(); Assert.Equal(expected, actual); } // Either theme or encoding must be applied first; although it's possible to imagine future scenarios (themes as HTML elements with styles...) that // might combine these both in a more sophisticated way, the current implementation chooses to wrap the entire output in the encoder, instead // of the other way around, because it's much easier to exclude any possibility of missed encoding using this approach. [Fact] public void EncodingAppliesToThemedOutput() { var evt = Some.InformationEvent("Hello, {Name}!", "nblumhardt"); var compiled = new ExpressionTemplate("-{@m}-", theme: StringHashPrefixingTheme.Instance, encoder: new ParenthesizingEncoder(), applyThemeWhenOutputIsRedirected: true); var output = new StringWriter(); compiled.Format(evt, output); var actual = output.ToString(); Assert.Equal("-(Hello, #nblumhardt\x1b[0m!)-", actual); } } ================================================ FILE: test/Serilog.Expressions.Tests/TemplateEvaluationTests.cs ================================================ using System.Globalization; using Serilog.Events; using Serilog.Expressions.Tests.Support; using Serilog.Templates; using Xunit; namespace Serilog.Expressions.Tests; public class TemplateEvaluationTests { static readonly DateTimeOffset TestTimestamp = new( 2000, 12, 31, 23, 59, 58, 123, TimeSpan.FromHours(10)); public static IEnumerable TemplateEvaluationCases => AsvCases.ReadCases("template-evaluation-cases.asv"); [Theory] [MemberData(nameof(TemplateEvaluationCases))] public void TemplatesAreCorrectlyEvaluated(string template, string expected) { var evt = Some.InformationEvent(TestTimestamp, "Hello, {Name}!", "nblumhardt"); var frFr = CultureInfo.GetCultureInfoByIetfLanguageTag("fr-FR"); var compiled = new ExpressionTemplate(template, formatProvider: frFr); var output = new StringWriter(); compiled.Format(evt, output); var actual = output.ToString(); Assert.Equal(expected, actual); } } ================================================ FILE: test/Serilog.Expressions.Tests/TemplateParserTests.cs ================================================ using Serilog.Templates; using Serilog.Templates.Ast; using Serilog.Templates.Parsing; using Xunit; namespace Serilog.Expressions.Tests; public class TemplateParserTests { [Theory] [InlineData("Trailing {", "Syntax error: unexpected end of input, expected expression.")] [InlineData("Lonely } bracket", "Syntax error (line 1, column 9): unexpected space, expected escaped `}`.")] [InlineData("Trailing }", "Syntax error: unexpected end of input, expected escaped `}`.")] [InlineData("Unclosed {hole", "Syntax error: unexpected end of input, expected `}`.")] [InlineData("Syntax {+Err}or", "Syntax error (line 1, column 9): unexpected operator `+`, expected expression.")] [InlineData("Syntax {1 + 2 and}or", "Syntax error (line 1, column 18): unexpected `}`, expected expression.")] [InlineData("Missing {Align,-} digits", "Syntax error (line 1, column 17): unexpected `}`, expected number.")] [InlineData("Non-digit {Align,x} specifier", "Syntax error (line 1, column 18): unexpected identifier `x`, expected alignment and width.")] [InlineData("Empty {Align,} digits", "Syntax error (line 1, column 14): unexpected `}`, expected alignment and width.")] public void ErrorsAreReported(string input, string error) { Assert.False(ExpressionTemplate.TryParse(input, null, null, null, false, null, out _, out var actual)); Assert.Equal(error, actual); } [Fact] public void DefaultAlignmentIsNull() { var parser = new TemplateParser(); Assert.True(parser.TryParse("{x}", out var template, out _)); var avt = Assert.IsType(template); Assert.Null(avt.Alignment); } } ================================================ FILE: test/Serilog.Expressions.Tests/TemplateTokenizerTests.cs ================================================ using Serilog.Expressions.Parsing; using Serilog.Templates.Parsing; using Xunit; using static Serilog.Expressions.Parsing.ExpressionToken; namespace Serilog.Expressions.Tests; public class TemplateTokenizerTests { public static IEnumerable ValidCases { get { return new[] { [ "aa", new[] {Text} ], [ "{bb}", new[] {LBrace, Identifier, RBrace} ], [ "aa{bb}", new[] {Text, LBrace, Identifier, RBrace} ], [ "aa{{bb}}", new[] {Text, DoubleLBrace, Text, DoubleRBrace} ], [ "{ {b: b} }c", new[] {LBrace, LBrace, Identifier, Colon, Identifier, RBrace, RBrace, Text} ], new object[] { "{bb,-10:cc}", new[] {LBrace, Identifier, Comma, Minus, Number, Colon, Format, RBrace} }, }; } } [Theory] [MemberData(nameof(ValidCases))] public void ValidTemplatesAreTokenized(string template, object expected) { var expectedTokens = (ExpressionToken[]) expected; var tokenizer = new TemplateTokenizer(); var actual = tokenizer.Tokenize(template).Select(t => t.Kind); Assert.Equal(expectedTokens, actual); } [Theory] [InlineData("aa{{bb}", "unexpected end of input, expected escaped `}`")] [InlineData("aa{ {b: 'b} }", "unexpected end of input, expected `'`")] public void InvalidTemplatesAreReported(string template, string fragment) { var tokenizer = new TemplateTokenizer(); var err = tokenizer.TryTokenize(template); Assert.False(err.HasValue); Assert.Equal(fragment, err.FormatErrorMessageFragment()); } } ================================================ FILE: test/Serilog.Expressions.Tests/Templates/UnreferencedPropertiesFunctionTests.cs ================================================ using Serilog.Events; using Serilog.Parsing; using Serilog.Templates.Ast; using Serilog.Templates.Compilation.UnreferencedProperties; using Serilog.Templates.Parsing; using Xunit; namespace Serilog.Expressions.Tests.Templates; public class UnreferencedPropertiesFunctionTests { [Fact] public void UnreferencedPropertiesFunctionIsNamedRest() { var function = new UnreferencedPropertiesFunction(new LiteralText("test")); Assert.True(function.TryResolveFunctionName("Rest", out _)); } [Fact] public void UnreferencedPropertiesExcludeThoseInMessageAndTemplate() { Assert.True(new TemplateParser().TryParse("{@m}{A + 1}{#if true}{B}{@p.C}{@p['D']}{#end}", out var template, out _)); var function = new UnreferencedPropertiesFunction(template); var evt = new LogEvent( DateTimeOffset.Now, LogEventLevel.Debug, null, new(new[] {new PropertyToken("E", "{E}")}), new[] { new LogEventProperty("A", new ScalarValue(null)), new LogEventProperty("B", new ScalarValue(null)), new LogEventProperty("C", new ScalarValue(null)), new LogEventProperty("D", new ScalarValue(null)), new LogEventProperty("E", new ScalarValue(null)), new LogEventProperty("F", new ScalarValue(null)), }); var deep = UnreferencedPropertiesFunction.Implementation(function, evt, new ScalarValue(true)); var sv = Assert.IsType(deep); var included = Assert.Single(sv.Properties); Assert.Equal("F", included.Name); var shallow = UnreferencedPropertiesFunction.Implementation(function, evt); sv = Assert.IsType(shallow); Assert.Contains(sv.Properties, p => p.Name == "E"); Assert.Contains(sv.Properties, p => p.Name == "F"); } }