diff --git a/.gitattributes b/.gitattributes index 1ff0c42..256d68e 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,63 +1,57 @@ -############################################################################### -# Set default behavior to automatically normalize line endings. -############################################################################### -* text=auto - -############################################################################### -# Set default behavior for command prompt diff. -# -# This is need for earlier builds of msysgit that does not have it on by -# default for csharp files. -# Note: This is only used by command line -############################################################################### -#*.cs diff=csharp - -############################################################################### -# Set the merge driver for project and solution files -# -# Merging from the command prompt will add diff markers to the files if there -# are conflicts (Merging from VS is not affected by the settings below, in VS -# the diff markers are never inserted). Diff markers may cause the following -# file extensions to fail to load in VS. An alternative would be to treat -# these files as binary and thus will always conflict and require user -# intervention with every merge. To do so, just uncomment the entries below -############################################################################### -#*.sln merge=binary -#*.csproj merge=binary -#*.vbproj merge=binary -#*.vcxproj merge=binary -#*.vcproj merge=binary -#*.dbproj merge=binary -#*.fsproj merge=binary -#*.lsproj merge=binary -#*.wixproj merge=binary -#*.modelproj merge=binary -#*.sqlproj merge=binary -#*.wwaproj merge=binary - -############################################################################### -# behavior for image files -# -# image files are treated as binary by default. -############################################################################### -#*.jpg binary -#*.png binary -#*.gif binary - -############################################################################### -# diff behavior for common document formats -# -# Convert binary document formats to text before diffing them. This feature -# is only available from the command line. Turn it on by uncommenting the -# entries below. -############################################################################### -#*.doc diff=astextplain -#*.DOC diff=astextplain -#*.docx diff=astextplain -#*.DOCX diff=astextplain -#*.dot diff=astextplain -#*.DOT diff=astextplain -#*.pdf diff=astextplain -#*.PDF diff=astextplain -#*.rtf diff=astextplain -#*.RTF diff=astextplain +# Auto detect text files and perform LF normalization +* text=false + +# Custom for Visual Studio +*.cs diff=csharp +*.sln merge=text +*.csproj merge=text +*.vbproj merge=text +*.fsproj merge=text +*.dbproj merge=text + +# Standard to msysgit +*.doc diff=astextplain +*.DOC diff=astextplain +*.docx diff=astextplain +*.DOCX diff=astextplain +*.dot diff=astextplain +*.DOT diff=astextplain +*.pdf diff=astextplain +*.PDF diff=astextplain +*.rtf diff=astextplain +*.RTF diff=astextplain + +# ignore file + +*.mp3 -diff +*.wav -diff +*.enas -diff +*.png -diff +*.jpg -diff +*.psd -diff +*.gif -diff + +# binary +*.avi binary +*.bmp binary +*.exr binary +*.ico binary +*.jpeg binary +*.jpg binary +*.png binary + +*.a binary +*.so binary +*.dll binary +*.jar binary + +*.pdf binary +*.pbxproj binary +*.vec binary +*.doc binary +*.dia binary + +# CR/LF + +*.bat text eol=crlf +*.sh text eol=lf diff --git a/.github/workflows/nuget-tag-publish.yml b/.github/workflows/nuget-tag-publish.yml index b4d2b1f..ec29c6c 100644 --- a/.github/workflows/nuget-tag-publish.yml +++ b/.github/workflows/nuget-tag-publish.yml @@ -1,6 +1,6 @@ name: publish nuget -on: +on: push: tags: - '*' @@ -12,16 +12,16 @@ jobs: steps: - uses: actions/checkout@v1 - + - name: Setup .NET Core uses: actions/setup-dotnet@v1 with: - dotnet-version: 3.1.300 + dotnet-version: 8.0.300 - name: Install dotnet tool run: dotnet tool install -g dotnetCampus.TagToVersion - - name: Set tag to version + - name: Set tag to version run: dotnet TagToVersion -t ${{ github.ref }} - name: Build with dotnet diff --git a/.gitignore b/.gitignore index 6989800..1cd7ff5 100644 --- a/.gitignore +++ b/.gitignore @@ -1,7 +1,7 @@ -## Ignore Visual Studio temporary files, build results, and +## Ignore Visual Studio temporary files, build results, and ## files generated by popular Visual Studio add-ons. ## -## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore # User-specific files *.rsuser @@ -13,6 +13,9 @@ # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs +# Mono auto generated files +mono_crash.* + # Build results [Dd]ebug/ [Dd]ebugPublic/ @@ -20,12 +23,14 @@ [Rr]eleases/ x64/ x86/ +[Ww][Ii][Nn]32/ [Aa][Rr][Mm]/ [Aa][Rr][Mm]64/ bld/ [Bb]in/ [Oo]bj/ [Ll]og/ +[Ll]ogs/ # Visual Studio 2015/2017 cache/options directory .vs/ @@ -39,9 +44,10 @@ Generated\ Files/ [Tt]est[Rr]esult*/ [Bb]uild[Ll]og.* -# NUNIT +# NUnit *.VisualState.xml TestResult.xml +nunit-*.xml # Build Results of an ATL Project [Dd]ebugPS/ @@ -56,6 +62,9 @@ project.lock.json project.fragment.lock.json artifacts/ +# ASP.NET Scaffolding +ScaffoldingReadMe.txt + # StyleCop StyleCopReport.xml @@ -81,6 +90,7 @@ StyleCopReport.xml *.tmp_proj *_wpftmp.csproj *.log +*.tlog *.vspscc *.vssscc .builds @@ -122,9 +132,6 @@ _ReSharper*/ *.[Rr]e[Ss]harper *.DotSettings.user -# JustCode is a .NET coding add-in -.JustCode - # TeamCity is a build add-in _TeamCity* @@ -135,6 +142,11 @@ _TeamCity* .axoCover/* !.axoCover/settings.json +# Coverlet is a free, cross platform Code Coverage Tool +coverage*.json +coverage*.xml +coverage*.info + # Visual Studio code coverage results *.coverage *.coveragexml @@ -182,6 +194,12 @@ PublishScripts/ # NuGet Packages *.nupkg +# NuGet Symbol Packages +*.snupkg +# The packages folder can be ignored because of Package Restore +**/[Pp]ackages/* +# except build/, which is used as an MSBuild target. +!**/[Pp]ackages/build/ # Uncomment if necessary however generally it will be regenerated when needed #!**/[Pp]ackages/repositories.config # NuGet v3's project.json files produces more ignorable files @@ -202,6 +220,8 @@ BundleArtifacts/ Package.StoreAssociation.xml _pkginfo.txt *.appx +*.appxbundle +*.appxupload # Visual Studio cache files # files ending in .cache can be ignored @@ -251,7 +271,9 @@ ServiceFabricBackup/ *.bim.layout *.bim_*.settings *.rptproj.rsuser -*- Backup*.rdl +*- [Bb]ackup.rdl +*- [Bb]ackup ([0-9]).rdl +*- [Bb]ackup ([0-9][0-9]).rdl # Microsoft Fakes FakesAssemblies/ @@ -272,6 +294,17 @@ node_modules/ # Visual Studio 6 auto-generated workspace file (contains which files were open etc.) *.vbw +# Visual Studio 6 auto-generated project file (contains which files were open etc.) +*.vbp + +# Visual Studio 6 workspace and project file (working project files containing files to include in project) +*.dsw +*.dsp + +# Visual Studio 6 technical files +*.ncb +*.aps + # Visual Studio LightSwitch build output **/*.HTMLClient/GeneratedArtifacts **/*.DesktopClient/GeneratedArtifacts @@ -287,10 +320,6 @@ paket-files/ # FAKE - F# Make .fake/ -# JetBrains Rider -.idea/ -*.sln.iml - # CodeRush personal settings .cr/personal @@ -332,5 +361,39 @@ ASALocalRun/ # Local History for Visual Studio .localhistory/ +# Visual Studio History (VSHistory) files +.vshistory/ + # BeatPulse healthcheck temp database -healthchecksdb \ No newline at end of file +healthchecksdb + +# Backup folder for Package Reference Convert tool in Visual Studio 2017 +MigrationBackup/ + +# Ionide (cross platform F# VS Code tools) working folder +.ionide/ + +# Fody - auto-generated XML schema +FodyWeavers.xsd + +# VS Code files for those working on multiple tools +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace + +# Local History for Visual Studio Code +.history/ + +# Windows Installer files from build outputs +*.cab +*.msi +*.msix +*.msm +*.msp + +# JetBrains Rider +.idea/ +*.sln.iml diff --git a/CHANGELOG.md b/CHANGELOG.md deleted file mode 100644 index a87a9b1..0000000 --- a/CHANGELOG.md +++ /dev/null @@ -1 +0,0 @@ -# dotnetCampus.Logger \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index 91c5887..7a5f547 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,18 +1,24 @@ + + + latest enable - $(MSBuildThisFileDirectory)bin\$(Configuration) - dotnet campus(.NET 职业技术学院) - - + $(MSBuildThisFileDirectory)artifacts + $(MSBuildThisFileDirectory) + + + + + 提供统一的日志记录方法。使用源生成器允许库的作者在不依赖本日志库的情况下完成日志的记录,并且还能对接到产品中完成日志的统一输出。 dotnet-campus + dotnet campus(.NET 职业技术学院) + Copyright 2020-$([System.DateTime]::Now.ToString(`yyyy`)) © dotnet campus, All Rights Reserved. + git https://github.com/dotnet-campus/dotnetCampus.Logger https://github.com/dotnet-campus/dotnetCampus.Logger - 提供统一的日志记录方法,并附带各种各样的实现。 - - git - Copyright © 2020 dotnet campus, All Rights Reserved. - \ No newline at end of file + + diff --git a/build/Version.props b/build/Version.props index 4cbacab..f1d2d98 100644 --- a/build/Version.props +++ b/build/Version.props @@ -1,5 +1,5 @@ - 0.1.0-alpha + 0.1.0-alpha01 - \ No newline at end of file + diff --git a/dotnetCampus.Logger.sln b/dotnetCampus.Logger.sln index 1628268..b9f1a1e 100644 --- a/dotnetCampus.Logger.sln +++ b/dotnetCampus.Logger.sln @@ -3,21 +3,35 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio Version 16 VisualStudioVersion = 16.0.30804.86 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5D196596-756D-45C2-8A05-C8E4AB8A36E6}" -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.Logger", "src\dotnetCampus.Logger\dotnetCampus.Logger.csproj", "{F7ED61F4-920C-49EB-8DC1-74B2BE6AF272}" + ProjectSection(ProjectDependencies) = postProject + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36} = {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36} + EndProjectSection EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{AFB0DF31-474C-4ACB-88C6-DD00552D5B5A}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{AFB0DF31-474C-4ACB-88C6-DD00552D5B5A}" ProjectSection(SolutionItems) = preProject - CHANGELOG.md = CHANGELOG.md Directory.Build.props = Directory.Build.props README.md = README.md build\Version.props = build\Version.props + .gitattributes = .gitattributes + .gitignore = .gitignore EndProjectSection EndProject -Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnetCampus.FileLogger", "src\dotnetCampus.FileLogger\dotnetCampus.FileLogger\dotnetCampus.FileLogger.csproj", "{C9399790-B935-4CE3-A75B-4D99133E1DB1}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerSample.MainApp", "samples\LoggerSample.MainApp\LoggerSample.MainApp.csproj", "{C282F00B-0C42-491F-AC0D-967407E1C418}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "samples", "samples", "{34E3F27E-C7E2-45D7-8035-53C304F77E2A}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerSample.LoggerDependentLibrary", "samples\LoggerSample.LoggerDependentLibrary\LoggerSample.LoggerDependentLibrary.csproj", "{0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerSample.LoggerIndependentLibrary", "samples\LoggerSample.LoggerIndependentLibrary\LoggerSample.LoggerIndependentLibrary.csproj", "{36367E21-BABC-4CC4-891E-CEAF56D66B68}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{36852775-3A76-49CF-98CC-3067CE54A5AD}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnetCampus.Logger.Tests", "tests\dotnetCampus.Logger.Tests\dotnetCampus.Logger.Tests.csproj", "{D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnetCampus.Logger.Analyzer", "src\dotnetCampus.Logger.Analyzer\dotnetCampus.Logger.Analyzer.csproj", "{77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnetCampus.Logger.Abstractions", "src\dotnetCampus.Logger.Abstractions\dotnetCampus.Logger.Abstractions.csproj", "{73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "LoggerSample.LoggerIndependentProject", "samples\LoggerSample.LoggerIndependentProject\LoggerSample.LoggerIndependentProject.csproj", "{E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -41,38 +55,88 @@ Global {F7ED61F4-920C-49EB-8DC1-74B2BE6AF272}.Release|x64.Build.0 = Release|Any CPU {F7ED61F4-920C-49EB-8DC1-74B2BE6AF272}.Release|x86.ActiveCfg = Release|Any CPU {F7ED61F4-920C-49EB-8DC1-74B2BE6AF272}.Release|x86.Build.0 = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|Any CPU.Build.0 = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|x64.ActiveCfg = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|x64.Build.0 = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|x86.ActiveCfg = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Debug|x86.Build.0 = Debug|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|Any CPU.ActiveCfg = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|Any CPU.Build.0 = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|x64.ActiveCfg = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|x64.Build.0 = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|x86.ActiveCfg = Release|Any CPU - {C9399790-B935-4CE3-A75B-4D99133E1DB1}.Release|x86.Build.0 = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|Any CPU.Build.0 = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|x64.ActiveCfg = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|x64.Build.0 = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|x86.ActiveCfg = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Debug|x86.Build.0 = Debug|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|Any CPU.ActiveCfg = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|Any CPU.Build.0 = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|x64.ActiveCfg = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|x64.Build.0 = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|x86.ActiveCfg = Release|Any CPU - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE}.Release|x86.Build.0 = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|Any CPU.Build.0 = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|x64.ActiveCfg = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|x64.Build.0 = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|x86.ActiveCfg = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Debug|x86.Build.0 = Debug|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|Any CPU.ActiveCfg = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|Any CPU.Build.0 = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|x64.ActiveCfg = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|x64.Build.0 = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|x86.ActiveCfg = Release|Any CPU + {C282F00B-0C42-491F-AC0D-967407E1C418}.Release|x86.Build.0 = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|x64.ActiveCfg = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|x64.Build.0 = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|x86.ActiveCfg = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Debug|x86.Build.0 = Debug|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|Any CPU.Build.0 = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|x64.ActiveCfg = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|x64.Build.0 = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|x86.ActiveCfg = Release|Any CPU + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899}.Release|x86.Build.0 = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|Any CPU.Build.0 = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|x64.ActiveCfg = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|x64.Build.0 = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|x86.ActiveCfg = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Debug|x86.Build.0 = Debug|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|Any CPU.ActiveCfg = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|Any CPU.Build.0 = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|x64.ActiveCfg = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|x64.Build.0 = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|x86.ActiveCfg = Release|Any CPU + {36367E21-BABC-4CC4-891E-CEAF56D66B68}.Release|x86.Build.0 = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|x64.ActiveCfg = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|x64.Build.0 = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|x86.ActiveCfg = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Debug|x86.Build.0 = Debug|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|Any CPU.Build.0 = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|x64.ActiveCfg = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|x64.Build.0 = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|x86.ActiveCfg = Release|Any CPU + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB}.Release|x86.Build.0 = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|Any CPU.Build.0 = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|x64.ActiveCfg = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|x64.Build.0 = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|x86.ActiveCfg = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Debug|x86.Build.0 = Debug|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|Any CPU.ActiveCfg = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|Any CPU.Build.0 = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|x64.ActiveCfg = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|x64.Build.0 = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|x86.ActiveCfg = Release|Any CPU + {77F0A6B5-6C8B-4815-8C1E-4F97C24BFB36}.Release|x86.Build.0 = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|x64.ActiveCfg = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|x64.Build.0 = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|x86.ActiveCfg = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Debug|x86.Build.0 = Debug|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|Any CPU.Build.0 = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|x64.ActiveCfg = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|x64.Build.0 = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|x86.ActiveCfg = Release|Any CPU + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection GlobalSection(NestedProjects) = preSolution - {F7ED61F4-920C-49EB-8DC1-74B2BE6AF272} = {5D196596-756D-45C2-8A05-C8E4AB8A36E6} - {C9399790-B935-4CE3-A75B-4D99133E1DB1} = {5D196596-756D-45C2-8A05-C8E4AB8A36E6} - {73E5A5FB-39A6-4417-9E66-1B9F6E925CFE} = {5D196596-756D-45C2-8A05-C8E4AB8A36E6} + {C282F00B-0C42-491F-AC0D-967407E1C418} = {34E3F27E-C7E2-45D7-8035-53C304F77E2A} + {0A1ACF06-FC64-4B5C-97E9-AA2CC307C899} = {34E3F27E-C7E2-45D7-8035-53C304F77E2A} + {36367E21-BABC-4CC4-891E-CEAF56D66B68} = {34E3F27E-C7E2-45D7-8035-53C304F77E2A} + {D0ACB879-D49B-4ACD-9852-23D0E6D15DDB} = {36852775-3A76-49CF-98CC-3067CE54A5AD} + {E0DE93BF-AE07-4F92-A3FD-F4D661339BE0} = {34E3F27E-C7E2-45D7-8035-53C304F77E2A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {D7D96521-5EB7-45B4-B70D-2B3FB69A3082} diff --git a/samples/LoggerSample.LoggerDependentLibrary/DllReferenceTarget.cs b/samples/LoggerSample.LoggerDependentLibrary/DllReferenceTarget.cs new file mode 100644 index 0000000..e3d7f24 --- /dev/null +++ b/samples/LoggerSample.LoggerDependentLibrary/DllReferenceTarget.cs @@ -0,0 +1,35 @@ +using dotnetCampus.Logging; + +namespace LoggerSample.LoggerDependentLibrary; + +public class DllReferenceTarget +{ + public static void CollectLogs() + { + Log.Trace.Trace("[DllReference] Log.Trace.Trace"); + Log.Trace.Debug("[DllReference] Log.Trace.Debug"); + Log.Trace.Info("[DllReference] Log.Trace.Info"); + Log.Trace.Warn("[DllReference] Log.Trace.Warn"); + Log.Trace.Error("[DllReference] Log.Trace.Error"); + Log.Trace.Fatal("[DllReference] Log.Trace.Fatal"); + + Log.Debug.Trace("[DllReference] Log.Debug.Trace"); + Log.Debug.Debug("[DllReference] Log.Debug.Debug"); + Log.Debug.Info("[DllReference] Log.Debug.Info"); + Log.Debug.Warn("[DllReference] Log.Debug.Warn"); + Log.Debug.Error("[DllReference] Log.Debug.Error"); + Log.Debug.Fatal("[DllReference] Log.Debug.Fatal"); + + Log.Info("[DllReference] Log.Info"); + Log.Warn("[DllReference] Log.Warn"); + Log.Error("[DllReference] Log.Error"); + Log.Fatal("[DllReference] Log.Fatal"); + + Log.Current.Trace("[DllReference] Log.Current.Trace"); + Log.Current.Debug("[DllReference] Log.Current.Debug"); + Log.Current.Info("[DllReference] Log.Current.Info"); + Log.Current.Warn("[DllReference] Log.Current.Warn"); + Log.Current.Error("[DllReference] Log.Current.Error"); + Log.Current.Fatal("[DllReference] Log.Current.Fatal"); + } +} diff --git a/samples/LoggerSample.LoggerDependentLibrary/LoggerSample.LoggerDependentLibrary.csproj b/samples/LoggerSample.LoggerDependentLibrary/LoggerSample.LoggerDependentLibrary.csproj new file mode 100644 index 0000000..8b1387f --- /dev/null +++ b/samples/LoggerSample.LoggerDependentLibrary/LoggerSample.LoggerDependentLibrary.csproj @@ -0,0 +1,16 @@ + + + + + + net8.0 + enable + + + + + + + + + diff --git a/samples/LoggerSample.LoggerIndependentLibrary/LoggerSample.LoggerIndependentLibrary.csproj b/samples/LoggerSample.LoggerIndependentLibrary/LoggerSample.LoggerIndependentLibrary.csproj new file mode 100644 index 0000000..2410b13 --- /dev/null +++ b/samples/LoggerSample.LoggerIndependentLibrary/LoggerSample.LoggerIndependentLibrary.csproj @@ -0,0 +1,17 @@ + + + + + + net8.0 + enable + true + + + + + + + + + diff --git a/samples/LoggerSample.LoggerIndependentLibrary/SourceReferenceTarget.cs b/samples/LoggerSample.LoggerIndependentLibrary/SourceReferenceTarget.cs new file mode 100644 index 0000000..72923f2 --- /dev/null +++ b/samples/LoggerSample.LoggerIndependentLibrary/SourceReferenceTarget.cs @@ -0,0 +1,35 @@ +using LoggerSample.LoggerIndependentLibrary.Logging; + +namespace LoggerSample.LoggerIndependentLibrary; + +public static class SourceReferenceTarget +{ + public static void CollectLogs() + { + Log.Trace.Trace("[SourceReference] Log.Trace.Trace"); + Log.Trace.Debug("[SourceReference] Log.Trace.Debug"); + Log.Trace.Info("[SourceReference] Log.Trace.Info"); + Log.Trace.Warn("[SourceReference] Log.Trace.Warn"); + Log.Trace.Error("[SourceReference] Log.Trace.Error"); + Log.Trace.Fatal("[SourceReference] Log.Trace.Fatal"); + + Log.Debug.Trace("[SourceReference] Log.Debug.Trace"); + Log.Debug.Debug("[SourceReference] Log.Debug.Debug"); + Log.Debug.Info("[SourceReference] Log.Debug.Info"); + Log.Debug.Warn("[SourceReference] Log.Debug.Warn"); + Log.Debug.Error("[SourceReference] Log.Debug.Error"); + Log.Debug.Fatal("[SourceReference] Log.Debug.Fatal"); + + Log.Info("[SourceReference] Log.Info"); + Log.Warn("[SourceReference] Log.Warn"); + Log.Error("[SourceReference] Log.Error"); + Log.Fatal("[SourceReference] Log.Fatal"); + + Log.Current.Trace("[SourceReference] Log.Current.Trace"); + Log.Current.Debug("[SourceReference] Log.Current.Debug"); + Log.Current.Info("[SourceReference] Log.Current.Info"); + Log.Current.Warn("[SourceReference] Log.Current.Warn"); + Log.Current.Error("[SourceReference] Log.Current.Error"); + Log.Current.Fatal("[SourceReference] Log.Current.Fatal"); + } +} diff --git a/samples/LoggerSample.LoggerIndependentProject/LoggerSample.LoggerIndependentProject.csproj b/samples/LoggerSample.LoggerIndependentProject/LoggerSample.LoggerIndependentProject.csproj new file mode 100644 index 0000000..2410b13 --- /dev/null +++ b/samples/LoggerSample.LoggerIndependentProject/LoggerSample.LoggerIndependentProject.csproj @@ -0,0 +1,17 @@ + + + + + + net8.0 + enable + true + + + + + + + + + diff --git a/samples/LoggerSample.LoggerIndependentProject/SourceReferenceTarget.cs b/samples/LoggerSample.LoggerIndependentProject/SourceReferenceTarget.cs new file mode 100644 index 0000000..4536fa0 --- /dev/null +++ b/samples/LoggerSample.LoggerIndependentProject/SourceReferenceTarget.cs @@ -0,0 +1,35 @@ +using LoggerSample.LoggerIndependentProject.Logging; + +namespace LoggerSample.LoggerIndependentProject; + +public class SourceReferenceTarget +{ + public static void CollectLogs() + { + Log.Trace.Trace("[SourceReference] Log.Trace.Trace"); + Log.Trace.Debug("[SourceReference] Log.Trace.Debug"); + Log.Trace.Info("[SourceReference] Log.Trace.Info"); + Log.Trace.Warn("[SourceReference] Log.Trace.Warn"); + Log.Trace.Error("[SourceReference] Log.Trace.Error"); + Log.Trace.Fatal("[SourceReference] Log.Trace.Fatal"); + + Log.Debug.Trace("[SourceReference] Log.Debug.Trace"); + Log.Debug.Debug("[SourceReference] Log.Debug.Debug"); + Log.Debug.Info("[SourceReference] Log.Debug.Info"); + Log.Debug.Warn("[SourceReference] Log.Debug.Warn"); + Log.Debug.Error("[SourceReference] Log.Debug.Error"); + Log.Debug.Fatal("[SourceReference] Log.Debug.Fatal"); + + Log.Info("[SourceReference] Log.Info"); + Log.Warn("[SourceReference] Log.Warn"); + Log.Error("[SourceReference] Log.Error"); + Log.Fatal("[SourceReference] Log.Fatal"); + + Log.Current.Trace("[SourceReference] Log.Current.Trace"); + Log.Current.Debug("[SourceReference] Log.Current.Debug"); + Log.Current.Info("[SourceReference] Log.Current.Info"); + Log.Current.Warn("[SourceReference] Log.Current.Warn"); + Log.Current.Error("[SourceReference] Log.Current.Error"); + Log.Current.Fatal("[SourceReference] Log.Current.Fatal"); + } +} diff --git a/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj b/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj new file mode 100644 index 0000000..c34714d --- /dev/null +++ b/samples/LoggerSample.MainApp/LoggerSample.MainApp.csproj @@ -0,0 +1,21 @@ + + + + + + WinExe + net8.0 + preferReference + + + + + + + + + + + + + diff --git a/samples/LoggerSample.MainApp/Program.cs b/samples/LoggerSample.MainApp/Program.cs new file mode 100644 index 0000000..d3684d8 --- /dev/null +++ b/samples/LoggerSample.MainApp/Program.cs @@ -0,0 +1,39 @@ +using dotnetCampus.Logging.Attributes; +using dotnetCampus.Logging.Configurations; +using dotnetCampus.Logging.Writers; + +namespace LoggerSample.MainApp; + +internal class Program +{ + public static void Main(string[] args) + { + // 这里是 Main 方法入口。 + Log.Info($"[App] App started with args: {string.Join(", ", args)}"); + + // 以下初始化代码可能会较晚执行。 + new LoggerBuilder() + .WithMemoryCache(new MemoryCacheOptions + { + }) + .WithLevel(LogLevel.Information) + .WithOptions(new LogOptions + { + LogLevel = LogLevel.Debug, + }) + .AddWriter(new ConsoleLogger + { + // Options = new ConsoleLoggerOptions + // { + // IncludeScopes = true, + // }, + }) + .AddBridge(LoggerBridgeLinker.Default) + .Build() + .IntoGlobalStaticLog(); + } +} + +[ImportLoggerBridge] +[ImportLoggerBridge] +internal partial class LoggerBridgeLinker; diff --git a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger.sln b/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger.sln deleted file mode 100644 index 6a7978d..0000000 --- a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger.sln +++ /dev/null @@ -1,25 +0,0 @@ - -Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.30523.141 -MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnetCampus.FileLogger", "dotnetCampus.FileLogger\dotnetCampus.FileLogger.csproj", "{B4F4EAE9-6C2E-4E54-BFE4-EE8BEF991DAC}" -EndProject -Global - GlobalSection(SolutionConfigurationPlatforms) = preSolution - Debug|Any CPU = Debug|Any CPU - Release|Any CPU = Release|Any CPU - EndGlobalSection - GlobalSection(ProjectConfigurationPlatforms) = postSolution - {B4F4EAE9-6C2E-4E54-BFE4-EE8BEF991DAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU - {B4F4EAE9-6C2E-4E54-BFE4-EE8BEF991DAC}.Debug|Any CPU.Build.0 = Debug|Any CPU - {B4F4EAE9-6C2E-4E54-BFE4-EE8BEF991DAC}.Release|Any CPU.ActiveCfg = Release|Any CPU - {B4F4EAE9-6C2E-4E54-BFE4-EE8BEF991DAC}.Release|Any CPU.Build.0 = Release|Any CPU - EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection - GlobalSection(ExtensibilityGlobals) = postSolution - SolutionGuid = {043DBF55-1A43-4E6D-A528-8EE89439DD8B} - EndGlobalSection -EndGlobal diff --git a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLogger.cs b/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLogger.cs deleted file mode 100644 index d82c2b4..0000000 --- a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLogger.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.IO; -using System.Threading.Tasks; -using dotnetCampus.Threading; - -namespace dotnetCampus.FileLogger -{ - public class FileLogger : IAsyncDisposable - { - public FileLogger() - { - DoubleBufferTask = new DoubleBufferLazyInitializeTask(WriteFile); - } - - public FileLogger(FileLoggerConfiguration configuration) : this() - { - SetConfiguration(configuration); - } - - public void SetConfiguration(FileLoggerConfiguration configuration) - { - if (FileLoggerConfiguration != null) - { - throw new InvalidOperationException($"重复多次设置日志文件"); - } - - FileLoggerConfiguration = configuration.Clone(); - - DoubleBufferTask.OnInitialized(); - - IsInitialized = true; - } - - private FileLoggerConfiguration FileLoggerConfiguration { set; get; } = null!; - - public bool IsInitialized { private set; get; } = false; - - public FileInfo LogFile => FileLoggerConfiguration.LogFile; - - private DoubleBufferLazyInitializeTask DoubleBufferTask { get; } - - public async ValueTask DisposeAsync() - { - DoubleBufferTask.Finish(); - await DoubleBufferTask.WaitAllTaskFinish(); - } - - public void WriteLog(string logMessage) - { - DoubleBufferTask.AddTask(logMessage); - } - - private uint CurrentWriteTextCount { set; get; } = 0; - - private async Task WriteFile(List logList) - { - // 最多尝试写10次日志 - var maxWriteLogFileRetryCount = FileLoggerConfiguration.MaxWriteLogFileRetryCount; - for (var i = 0; i < maxWriteLogFileRetryCount; i++) - { - try - { - await File.AppendAllLinesAsync(LogFile.FullName, logList); - - // 当前写入的数据量 - foreach (var logText in logList) - { - CurrentWriteTextCount += (uint)logText.Length; - } - - if (CurrentWriteTextCount > FileLoggerConfiguration.NotifyMinWriteTextCount) - { - foreach (var limitTextCountFilter in FileLoggerConfiguration.LimitTextCountFilterList) - { - await limitTextCountFilter.FilterLogFile(LogFile); - } - } - - return; - } - catch (Exception e) - { - Debug.WriteLine("[FileLogger] {0}", e); - } - - Debug.WriteLine("[FileLogger] Retry count {0}", i); - await Task.Delay(FileLoggerConfiguration.RetryDelayTime); - } - - FileLoggerWriteFailed?.Invoke(this, new FileLoggerWriteFailedArgs(this, FileLoggerConfiguration, logList)); - // 如果超过次数依然写入失败,那就忽略失败了 - } - - public event EventHandler? FileLoggerWriteFailed; - } -} \ No newline at end of file diff --git a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerConfiguration.cs b/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerConfiguration.cs deleted file mode 100644 index 1c903cc..0000000 --- a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerConfiguration.cs +++ /dev/null @@ -1,39 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading.Tasks; - -namespace dotnetCampus.FileLogger -{ - public class FileLoggerConfiguration - { - public uint MaxWriteLogFileRetryCount { set; get; } = DefaultMaxWriteLogFileRetryCount; - - public const uint DefaultMaxWriteLogFileRetryCount = 10; - - public TimeSpan RetryDelayTime { set; get; } = DefaultRetryDelayTime; - - public static readonly TimeSpan DefaultRetryDelayTime = TimeSpan.FromMilliseconds(100); - - public FileInfo LogFile { set; get; } = null!; - - public uint NotifyMinWriteTextCount { set; get; } = DefaultNotifyMinWriteTextCount; - - public const uint DefaultNotifyMinWriteTextCount = 5 * 1024 * 1024;// 大约是 5-10M 左右 - - public FileLoggerConfiguration Clone() - { - var fileLoggerConfiguration = (FileLoggerConfiguration)MemberwiseClone(); - fileLoggerConfiguration.LimitTextCountFilterList = LimitTextCountFilterList.ToList(); - return fileLoggerConfiguration; - } - - public List LimitTextCountFilterList { private set;get; } = new List(); - } - - public interface ILimitTextCountFilter - { - Task FilterLogFile(FileInfo logFile); - } -} \ No newline at end of file diff --git a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerWriteFailedArgs.cs b/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerWriteFailedArgs.cs deleted file mode 100644 index 63cd7ad..0000000 --- a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/FileLoggerWriteFailedArgs.cs +++ /dev/null @@ -1,22 +0,0 @@ -using System; -using System.Collections.Generic; - -namespace dotnetCampus.FileLogger -{ - public class FileLoggerWriteFailedArgs : EventArgs - { - public FileLoggerWriteFailedArgs(FileLogger fileLogger, FileLoggerConfiguration fileLoggerConfiguration, - IReadOnlyList logList) - { - FileLoggerConfiguration = fileLoggerConfiguration; - FileLogger = fileLogger; - LogList = logList; - } - - public FileLoggerConfiguration FileLoggerConfiguration { get; } - - public FileLogger FileLogger { get; } - - public IReadOnlyList LogList { get; } - } -} \ No newline at end of file diff --git a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/dotnetCampus.FileLogger.csproj b/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/dotnetCampus.FileLogger.csproj deleted file mode 100644 index 3e1575e..0000000 --- a/src/dotnetCampus.FileLogger/dotnetCampus.FileLogger/dotnetCampus.FileLogger.csproj +++ /dev/null @@ -1,9 +0,0 @@ - - - - net5.0 - - - - - diff --git a/src/dotnetCampus.Logger.Abstractions/EventId.cs b/src/dotnetCampus.Logger.Abstractions/EventId.cs deleted file mode 100644 index aecafef..0000000 --- a/src/dotnetCampus.Logger.Abstractions/EventId.cs +++ /dev/null @@ -1,103 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace dotnetCampus.Logger.Abstractions -{ - /// - /// Identifies a logging event. The primary identifier is the "Id" property, with the "Name" property providing a short description of this type of event. - /// - /// Copy from dotnet runtime -#if LogPublicAsInternal - internal -#else - public -#endif - readonly struct EventId - { - /// - /// Implicitly creates an EventId from the given . - /// - /// The to convert to an EventId. - public static implicit operator EventId(int i) - { - return new EventId(i); - } - - /// - /// Checks if two specified instances have the same value. They are equal if they have the same Id. - /// - /// The first . - /// The second . - /// if the objects are equal. - public static bool operator ==(EventId left, EventId right) - { - return left.Equals(right); - } - - /// - /// Checks if two specified instances have different values. - /// - /// The first . - /// The second . - /// if the objects are not equal. - public static bool operator !=(EventId left, EventId right) - { - return !left.Equals(right); - } - - /// - /// Initializes an instance of the struct. - /// - /// The numeric identifier for this event. - /// The name of this event. - public EventId(int id, string? name = null) - { - Id = id; - Name = name; - } - - /// - /// Gets the numeric identifier for this event. - /// - public int Id { get; } - - /// - /// Gets the name of this event. - /// - public string? Name { get; } - - /// - public override string ToString() - { - return Name ?? Id.ToString(); - } - - /// - /// Indicates whether the current object is equal to another object of the same type. Two events are equal if they have the same id. - /// - /// An object to compare with this object. - /// if the current object is equal to the other parameter; otherwise, . - public bool Equals(EventId other) - { - return Id == other.Id; - } - - /// - public override bool Equals(object? obj) - { - if (obj is null) - { - return false; - } - - return obj is EventId eventId && Equals(eventId); - } - - /// - public override int GetHashCode() - { - return Id; - } - } -} diff --git a/src/dotnetCampus.Logger.Abstractions/ILogger.cs b/src/dotnetCampus.Logger.Abstractions/ILogger.cs deleted file mode 100644 index 173963f..0000000 --- a/src/dotnetCampus.Logger.Abstractions/ILogger.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System; - -namespace dotnetCampus.Logger.Abstractions -{ - /// - /// Represents a type used to perform logging. - /// - /// Aggregates most logging patterns to a single method. - /// Copy from dotnet runtime -#if LogPublicAsInternal - internal -#else - public -#endif - interface ILogger - { - /// - /// Writes a log entry. - /// - /// Entry will be written on this level. - /// Id of the event. - /// The entry to be written. Can be also an object. - /// The exception related to this entry. - /// Function to create a message of the and . - /// The type of the object to be written. - void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter); - } -} diff --git a/src/dotnetCampus.Logger.Abstractions/LogLevel.cs b/src/dotnetCampus.Logger.Abstractions/LogLevel.cs deleted file mode 100644 index 59ab39e..0000000 --- a/src/dotnetCampus.Logger.Abstractions/LogLevel.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; - -namespace dotnetCampus.Logger.Abstractions -{ - /// - /// Defines logging severity levels. - /// - /// Copy from dotnet runtime -#if LogPublicAsInternal - internal -#else - public -#endif - enum LogLevel - { - /// - /// Logs that contain the most detailed messages. These messages may contain sensitive application data. - /// These messages are disabled by default and should never be enabled in a production environment. - /// - Trace = 0, - - /// - /// Logs that are used for interactive investigation during development. These logs should primarily contain - /// information useful for debugging and have no long-term value. - /// - Debug = 1, - - /// - /// Logs that track the general flow of the application. These logs should have long-term value. - /// - Information = 2, - - /// - /// Logs that highlight an abnormal or unexpected event in the application flow, but do not otherwise cause the - /// application execution to stop. - /// - Warning = 3, - - /// - /// Logs that highlight when the current flow of execution is stopped due to a failure. These should indicate a - /// failure in the current activity, not an application-wide failure. - /// - Error = 4, - - /// - /// Logs that describe an unrecoverable application or system crash, or a catastrophic failure that requires - /// immediate attention. - /// - Critical = 5, - - /// - /// Not used for writing log messages. Specifies that a logging category should not write any messages. - /// - None = 6, - } -} diff --git a/src/dotnetCampus.Logger.Abstractions/dotnetCampus.Logger.Abstractions.csproj b/src/dotnetCampus.Logger.Abstractions/dotnetCampus.Logger.Abstractions.csproj deleted file mode 100644 index 9bc4a26..0000000 --- a/src/dotnetCampus.Logger.Abstractions/dotnetCampus.Logger.Abstractions.csproj +++ /dev/null @@ -1,14 +0,0 @@ - - - - netcoreapp3.1;net45;net5.0 - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - diff --git a/src/dotnetCampus.Logger.Analyzer/Assets/Templates/AggregateLoggerBridgeLinker.g.cs b/src/dotnetCampus.Logger.Analyzer/Assets/Templates/AggregateLoggerBridgeLinker.g.cs new file mode 100644 index 0000000..a5d6d43 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Assets/Templates/AggregateLoggerBridgeLinker.g.cs @@ -0,0 +1,81 @@ +#nullable enable + +using GEventId = global::dotnetCampus.Logging.EventId; +using GException = global::System.Exception; +using GILogger = global::dotnetCampus.Logging.ILogger; +using GILoggerBridgeLinker = global::dotnetCampus.Logging.Bridges.ILoggerBridgeLinker; +using GLog = global::dotnetCampus.Logging.Log; +using GLogLevel = global::dotnetCampus.Logging.LogLevel; + +namespace dotnetCampus.Logger.Assets.Templates; + +/// +/// 聚合各个来源的日志桥。调用其 方法可以将这些来源的日志桥对接到指定的日志记录器上。 +/// +partial class AggregateLoggerBridgeLinker : GILoggerBridgeLinker +{ + /// + /// 获取用于对接到日志记录系统的聚合日志桥。 + /// + public static AggregateLoggerBridgeLinker Default { get; } = new(); + + private GILogger? _logger; + + /// + /// 将所有已指派给此聚合日志桥的所有日志桥对接到 日志记录器上。 + /// + /// 要对接的日志记录器。如果不指定,则默认使用全局的 .。 + /// + /// 如果已经对接过日志记录器,则会抛出此异常。 + /// 如果希望针对不同的库对接不同的日志记录器,请编写多个聚合日志桥并分别导入各自的日志桥。 + /// + public void Link(GILogger logger) + { + if (_logger != null) + { + throw new global::System.InvalidOperationException("The logger has been linked. If you want to link different logger instance, please create a new AggregateLoggerBridge instance."); + } + + _logger = logger; + + // 链接来自各个源的日志桥: + // + // + // 源生成器请在此处添加代码... + // + // 对于 .NET 运行时: + // global::Xxx.Logging.ILoggerBridge.Link(this); + // + // 对于 .NET Framework 运行时: + // global::Xxx.Logging.LoggerBridgeLinker.Link(this); + // + // + } + + /// + /// 写入日志条目。 + /// + /// 将在此级别上写入条目。 + /// 事件的 Id。 + /// 事件的名称。 + /// 要写入的条目。也可以是一个对象。 + /// 与此条目相关的异常。 + /// 创建一条字符串消息以记录 。 + /// 要写入的对象的类型。 + public void Log( + int logLevel, + int eventId, + string? eventName, + TState state, + GException? exception, + global::System.Func formatter) + { + var logger = _logger ?? GLog.Current; + logger.Log( + (GLogLevel)logLevel, + new GEventId(eventId, eventName), + state, + exception, + formatter); + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Assets/Templates/Program.g.cs b/src/dotnetCampus.Logger.Analyzer/Assets/Templates/Program.g.cs new file mode 100644 index 0000000..1d9d9e1 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Assets/Templates/Program.g.cs @@ -0,0 +1,18 @@ +#nullable enable + +using LILogger = global::dotnetCampus.Logging.ILogger; +using LLog = global::dotnetCampus.Logging.Log; + +namespace dotnetCampus.Logger.Assets.Templates; + +partial class Program +{ + /// + /// 用于在 类的内部记录日志。 + /// + /// + /// 由于此代码是源生成器生成的代码,所以可以在日志模块初始化之前记录日志且提前生效。
+ /// 🤩 你甚至能在 Main 方法的第一行就使用它记录日志! + ///
+ private static LILogger Log => LLog.Current; +} diff --git a/src/dotnetCampus.Logger.Analyzer/CodeFixeProviders/PartialProgramCodeFixProvider.cs b/src/dotnetCampus.Logger.Analyzer/CodeFixeProviders/PartialProgramCodeFixProvider.cs new file mode 100644 index 0000000..ccf5a24 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/CodeFixeProviders/PartialProgramCodeFixProvider.cs @@ -0,0 +1,61 @@ +using System.Collections.Immutable; +using System.Composition; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CodeActions; +using Microsoft.CodeAnalysis.CodeFixes; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace dotnetCampus.Logger.CodeFixeProviders; + +[ExportCodeFixProvider(LanguageNames.CSharp), Shared] +public class PartialProgramCodeFixProvider : CodeFixProvider +{ + public override ImmutableArray FixableDiagnosticIds { get; } = ImmutableArray.Create( + Diagnostics.DL0101_ProgramIsRecommendedToBePartial.Id + ); + + public override FixAllProvider? GetFixAllProvider() => null; + + public override async Task RegisterCodeFixesAsync(CodeFixContext context) + { + var root = await context.Document.GetSyntaxRootAsync(context.CancellationToken); + if (root is null) + { + return; + } + + var semanticModel = await context.Document.GetSemanticModelAsync(context.CancellationToken); + if (semanticModel is null) + { + return; + } + + foreach (var diagnostic in context.Diagnostics) + { + var diagnosticSpan = diagnostic.Location.SourceSpan; + var cds = (ClassDeclarationSyntax)root.FindNode(diagnosticSpan); + context.RegisterCodeFix( + CodeAction.Create( + title: string.Format(DL1001_Fix, cds.Identifier.Text), + createChangedDocument: c => AddPartialModifierAsync(context.Document, cds, c), + equivalenceKey: nameof(DL1001_Fix)), + diagnostic); + } + } + + private async Task AddPartialModifierAsync(Document document, ClassDeclarationSyntax classDeclarationNode, CancellationToken cancellationToken) + { + var newClassDeclarationNode = classDeclarationNode.AddModifiers(SyntaxFactory.Token(SyntaxKind.PartialKeyword)); + var root = await document.GetSyntaxRootAsync(cancellationToken); + if (root is null) + { + return document; + } + + var newRoot = root.ReplaceNode(classDeclarationNode, newClassDeclarationNode); + return document.WithSyntaxRoot(newRoot); + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/DiagnosticAnalyzers/PartialProgramAnalyzer.cs b/src/dotnetCampus.Logger.Analyzer/DiagnosticAnalyzers/PartialProgramAnalyzer.cs new file mode 100644 index 0000000..17b3638 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/DiagnosticAnalyzers/PartialProgramAnalyzer.cs @@ -0,0 +1,46 @@ +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace dotnetCampus.Logger.DiagnosticAnalyzers; + +[DiagnosticAnalyzer(LanguageNames.CSharp)] +public class PartialProgramAnalyzer : DiagnosticAnalyzer +{ + public override ImmutableArray SupportedDiagnostics { get; } = ImmutableArray.Create( + Diagnostics.DL0101_ProgramIsRecommendedToBePartial + ); + + public override void Initialize(AnalysisContext context) + { + context.EnableConcurrentExecution(); + context.ConfigureGeneratedCodeAnalysis(GeneratedCodeAnalysisFlags.None); + + context.RegisterSyntaxNodeAction(AnalyzeProgram, SyntaxKind.MethodDeclaration); + } + + private void AnalyzeProgram(SyntaxNodeAnalysisContext context) + { + if (context.Node is MethodDeclarationSyntax + { + Parent: ClassDeclarationSyntax cds, + } mds + && Utils.CodeAnalysis.ProgramMainExtensions.CheckCanBeProgramMain(mds) + && !cds.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + var spanStart = cds.Modifiers.Count is 0 + ? cds.Keyword.SpanStart + : cds.Modifiers.Span.Start; + context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.DL0101_ProgramIsRecommendedToBePartial, + Location.Create(context.Node.SyntaxTree, new TextSpan( + spanStart, + cds.Identifier.Span.End - spanStart + )), + cds.Identifier.Text)); + } + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Diagnostics.cs b/src/dotnetCampus.Logger.Analyzer/Diagnostics.cs new file mode 100644 index 0000000..fa9d259 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Diagnostics.cs @@ -0,0 +1,74 @@ +using dotnetCampus.Logger.Properties; +using Microsoft.CodeAnalysis; + +// ReSharper disable InconsistentNaming + +namespace dotnetCampus.Logger; + +/// +/// 包含日志库中的所有诊断。 +/// +public class Diagnostics +{ + public static DiagnosticDescriptor DL0000_UnknownError { get; } = new( + nameof(DL0000), + Localize(nameof(DL0000)), + Localize(nameof(DL0000_Message)), + Categories.Useless, + DiagnosticSeverity.Error, + true); + + public static DiagnosticDescriptor DL0101_ProgramIsRecommendedToBePartial { get; } = new( + nameof(DL1001), + Localize(nameof(DL1001)), + Localize(nameof(DL1001_Message)), + Categories.Performance, + DiagnosticSeverity.Info, + true, + description: Localize(DL1001_Description)); + + private static class Categories + { + /// + /// 可能产生 bug,则报告此诊断。 + /// + public const string AvoidBugs = "dotnetCampus.AvoidBugs"; + + /// + /// 为了提供代码生成能力,则报告此诊断。 + /// + public const string CodeFixOnly = "dotnetCampus.CodeFixOnly"; + + /// + /// 因编译要求而必须满足的条件没有满足,则报告此诊断。 + /// + public const string Compiler = "dotnetCampus.Compiler"; + + /// + /// 因库内的机制限制,必须满足此要求后库才可正常工作,则报告此诊断。 + /// + public const string Mechanism = "dotnetCampus.Mechanism"; + + /// + /// 为了代码可读性,使之更易于理解、方便调试,则报告此诊断。 + /// + public const string Readable = "dotnetCampus.Readable"; + + /// + /// 为了提升性能,或避免性能问题,则报告此诊断。 + /// + public const string Performance = "dotnetCampus.Performance"; + + /// + /// 能写得出来正常编译,但会引发运行时异常,则报告此诊断。 + /// + public const string RuntimeException = "dotnetCampus.RuntimeException"; + + /// + /// 编写了无法生效的代码,则报告此诊断。 + /// + public const string Useless = "dotnetCampus.Useless"; + } + + private static LocalizableString Localize(string key) => new LocalizableResourceString(key, ResourceManager, typeof(Localizations)); +} diff --git a/src/dotnetCampus.Logger.Analyzer/GeneratorInfo.cs b/src/dotnetCampus.Logger.Analyzer/GeneratorInfo.cs new file mode 100644 index 0000000..fd517aa --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/GeneratorInfo.cs @@ -0,0 +1,20 @@ +using System; +using System.Linq; +using dotnetCampus.Logger.Utils.IO; + +namespace dotnetCampus.Logger; + +internal static class GeneratorInfo +{ + public static readonly string RootNamespace = typeof(GeneratorInfo).Namespace!; + + public static EmbeddedSourceFile GetEmbeddedTemplateFile() + { + var typeName = typeof(TReferenceType).Name; + var templateNamespace = typeof(TReferenceType).Namespace!; + var templatesFolder = templateNamespace.AsSpan().Slice(GeneratorInfo.RootNamespace.Length + 1).ToString(); + var embeddedFile = EmbeddedSourceFiles.Enumerate(templatesFolder) + .Single(x => x.TypeName == typeName); + return embeddedFile; + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Generators/GlobalUsingsGenerator.cs b/src/dotnetCampus.Logger.Analyzer/Generators/GlobalUsingsGenerator.cs new file mode 100644 index 0000000..e2b512e --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Generators/GlobalUsingsGenerator.cs @@ -0,0 +1,83 @@ +using System.Collections.Immutable; +using System.Linq; +using System.Text; +using dotnetCampus.Logger.Utils.CodeAnalysis; +using dotnetCampus.Logger.Utils.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace dotnetCampus.Logger.Generators; + +/// +/// 生成一组用于记录日志的代码。 +/// +[Generator] +public class GlobalUsingsGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + context.RegisterSourceOutput(context.AnalyzerConfigOptionsProvider, Execute); + } + + private void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider provider) + { + if (provider.GlobalOptions + .TryGetValue("_DLRootNamespace", out var rootNamespace) + .TryGetValue("_DLGenerateSource", out var generateSource) + .TryGetValue("_DLGenerateGlobalUsings", out var generateGlobalUsings) + .TryGetValue("_DLPreferGeneratedSource", out var preferGeneratedSource) + is var result + && !result) + { + // 此项目是通过依赖间接引用的,没有 build 因此无法在源生成器中使用编译属性,所以只能选择引用。 + return; + } + + if (!generateSource || !generateGlobalUsings) + { + return; + } + + var generatedCode = preferGeneratedSource + ? GenerateGlobalUsings(rootNamespace, preferGeneratedSource) + : GenerateGlobalUsings("dotnetCampus", preferGeneratedSource); + + context.AddSource("GlobalUsings.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); + } + + private string GenerateGlobalUsings(string rootNamespace, bool useGeneratedLogger) + { + var sourceFiles = EmbeddedSourceFiles.Enumerate("Assets/Sources").ToImmutableArray(); + + var globalUsingsCode = GenerateGlobalUsingsForTypes( + rootNamespace, + [..sourceFiles], + useGeneratedLogger); + return globalUsingsCode; + } + + private string GenerateGlobalUsingsForTypes(string rootNamespace, ImmutableArray sourceFiles, bool useGeneratedLogger) + { + return $""" +global using global::{rootNamespace}.Logging; + +{string.Join("\n", sourceFiles.Select(GenerateTypeUsing).OfType())} + +"""; + + string? GenerateTypeUsing(EmbeddedSourceFile sourceFile) + { + if ( + // 如果使用源生成器的日志系统,则所有类型均要导出全局引用。 + useGeneratedLogger + || sourceFile.Namespace.EndsWith("Sources") + ) + { + return $"global using {sourceFile.TypeName} = global::{rootNamespace}.Logging.{sourceFile.TypeName};"; + } + + return null; + } + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Generators/LoggerBridgeGenerator.cs b/src/dotnetCampus.Logger.Analyzer/Generators/LoggerBridgeGenerator.cs new file mode 100644 index 0000000..2f4251b --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Generators/LoggerBridgeGenerator.cs @@ -0,0 +1,139 @@ +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Diagnostics; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; +using dotnetCampus.Logger.Assets.Templates; +using dotnetCampus.Logging.Attributes; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; + +namespace dotnetCampus.Logger.Generators; + +/// +/// 生成聚合日志桥,为来自各个库的日志桥对接日志记录器。 +/// +[Generator] +public class LoggerBridgeGenerator : IIncrementalGenerator +{ + private static readonly string ImportLoggerBridgeUsageName = nameof(ImportLoggerBridgeAttribute).Replace("Attribute", ""); + private static readonly string ImportLoggerBridgeAttributeName = typeof(ImportLoggerBridgeAttribute).FullName!; + + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => + { + if (node is not ClassDeclarationSyntax cds) + { + // 必须是类声明。 + return false; + } + + var attributes = cds.AttributeLists + .SelectMany(x => x.Attributes) + .Where(x => x.Name.ToString().StartsWith(ImportLoggerBridgeUsageName)); + if (!attributes.Any()) + { + // 必须有 ImportLoggerBridge 特性。 + return false; + } + + return true; + }, (c, ct) => + { + var aggregateBridgeType = c.SemanticModel.GetDeclaredSymbol((ClassDeclarationSyntax)c.Node); + if (aggregateBridgeType is null) + { + return null; + } + + var collectedBridgeTypes = aggregateBridgeType.GetAttributes() + .Where(x => x.AttributeClass switch + { + { IsGenericType: true } ga => ga.OriginalDefinition.ToDisplayString() == $"{ImportLoggerBridgeAttributeName}", + _ => x.AttributeClass?.ToDisplayString() == ImportLoggerBridgeAttributeName, + }) + .Select(x => x.AttributeClass switch + { + { IsGenericType: true } ga => ga.TypeArguments.FirstOrDefault() as INamedTypeSymbol, + var ba => x.ConstructorArguments.FirstOrDefault().Value as INamedTypeSymbol, + }) + .OfType() + .ToImmutableArray(); + if (collectedBridgeTypes.Length is 0) + { + return null; + } + + return new LoggerBridgeItem(aggregateBridgeType, collectedBridgeTypes); + }) + .Where(x => x is not null) + .Select((x, ct) => x!); + + context.RegisterSourceOutput(provider, Execute); + } + + private void Execute(SourceProductionContext context, LoggerBridgeItem bridgeItem) + { + var bridgeNamespace = bridgeItem.Aggregate.ContainingNamespace.ToDisplayString(); + var bridgeName = bridgeItem.Aggregate.Name; + + var loggerBridgeFile = GeneratorInfo.GetEmbeddedTemplateFile(); + var intermediateCode = loggerBridgeFile.Content + .Replace(typeof(AggregateLoggerBridgeLinker).Namespace!, bridgeNamespace) + .Replace(nameof(AggregateLoggerBridgeLinker), bridgeName) + .Replace( + $"partial class {bridgeName} : GILoggerBridgeLinker", + $"partial class {bridgeName} : GILoggerBridgeLinker{string.Concat(bridgeItem.Collected.Select(x => $",\n global::{x.ToDisplayString()}"))}"); + + var generatedCode = InsertLinks(bridgeItem, intermediateCode); + + context.AddSource($"{nameof(AggregateLoggerBridgeLinker)}.{bridgeName}.g.cs", SourceText.From(generatedCode, Encoding.UTF8)); + } + + private string InsertLinks(LoggerBridgeItem bridgeItem, string sourceCode) + { + var sourceSpan = sourceCode.AsSpan(); + + var regex = GetFlagRegex(); + var match = regex.Match(sourceCode); + if (!match.Success) + { + return sourceCode; + } + + var insertStartIndex = match.Index; + var insertEndIndex = match.Index + match.Length; + var links = GenerateLinks(bridgeItem.Collected); + + return string.Concat( + sourceSpan.Slice(0, insertStartIndex).ToString(), + links, + sourceSpan.Slice(insertEndIndex, sourceSpan.Length - insertEndIndex).ToString() + ); + } + + private string GenerateLinks(IEnumerable collected) + { + return $""" + +{string.Join("\n", collected.Select(x => $" {GenerateLink(x)}"))} +"""; + } + + private string GenerateLink(INamedTypeSymbol collectedBridgeType) + { + // global::Xxx.Logging.ILoggerBridge.Link(this); + return $"global::{collectedBridgeType.ToDisplayString()}.Link(this);"; + } + + private static Regex? _flagRegex; + + private static Regex GetFlagRegex() => _flagRegex ??= new Regex(@"\s+// .+?", RegexOptions.Compiled | RegexOptions.Singleline); + + private record LoggerBridgeItem(INamedTypeSymbol Aggregate, ImmutableArray Collected); +} diff --git a/src/dotnetCampus.Logger.Analyzer/Generators/LoggerGenerator.cs b/src/dotnetCampus.Logger.Analyzer/Generators/LoggerGenerator.cs new file mode 100644 index 0000000..36460f0 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Generators/LoggerGenerator.cs @@ -0,0 +1,94 @@ +using System; +using System.Collections.Immutable; +using System.Text; +using System.Text.RegularExpressions; +using dotnetCampus.Logger.Utils.CodeAnalysis; +using dotnetCampus.Logger.Utils.IO; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.Diagnostics; +using Microsoft.CodeAnalysis.Text; + +namespace dotnetCampus.Logger.Generators; + +/// +/// 生成一组用于记录日志的代码。 +/// +[Generator] +public class LoggerGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.AnalyzerConfigOptionsProvider; + context.RegisterSourceOutput(provider, Execute); + } + + private void Execute(SourceProductionContext context, AnalyzerConfigOptionsProvider provider) + { + if (provider.GlobalOptions + .TryGetValue("_DLRootNamespace", out var rootNamespace) + .TryGetValue("_DLGenerateSource", out var isGenerateSource) + is var result + && !result) + { + // 此项目是通过依赖间接引用的,没有 build 因此无法在源生成器中使用编译属性,所以只能选择引用。 + return; + } + + if (!isGenerateSource) + { + return; + } + + var sourceFiles = EmbeddedSourceFiles.Enumerate("Assets/Sources").ToImmutableArray(); + + foreach (var file in sourceFiles) + { + var code = GenerateSource(file.TypeName, rootNamespace, file.Content); + context.AddSource($"{file.TypeName}.g.cs", SourceText.From(code, Encoding.UTF8)); + } + } + + private string GenerateSource(string typeName, string rootNamespace, string sourceText) + { + if (typeName == "Log") + { + // 源生成器为单独库生成的代码中,默认日志记录器是 BridgeLogger。 + sourceText = sourceText.Replace("new MemoryCacheLogger();", "new BridgeLogger();"); + } + + var sourceSpan = sourceText.AsSpan(); + + var namespaceKeywordIndex = sourceText.IndexOf("namespace", StringComparison.Ordinal); + var namespaceStartIndex = namespaceKeywordIndex + "namespace".Length + 1; + var namespaceEndIndex = sourceText.IndexOf(";", namespaceStartIndex, StringComparison.Ordinal); + + var classKeywordIndex = GetTypeRegex().Match(sourceText).Index; + var publicKeywordIndex = sourceText.IndexOf("public", namespaceEndIndex, classKeywordIndex - namespaceEndIndex, StringComparison.Ordinal); + + if (publicKeywordIndex < 0 || typeName.Contains("Bridge")) + { + // 此类型不是 public 的,无需修改为 internal。 + // 此类型是 BridgeLogger,应该保持 public。 + return string.Concat( + sourceSpan.Slice(0, namespaceStartIndex).ToString(), + $"{rootNamespace}.Logging", + sourceSpan.Slice(namespaceEndIndex, sourceSpan.Length - namespaceEndIndex).ToString() + ); + } + else + { + // 此类型是 public 的,需要修改为 internal。 + return string.Concat( + sourceSpan.Slice(0, namespaceStartIndex).ToString(), + $"{rootNamespace}.Logging", + sourceSpan.Slice(namespaceEndIndex, publicKeywordIndex - namespaceEndIndex).ToString(), + "internal", + sourceSpan.Slice(publicKeywordIndex + "public".Length, sourceSpan.Length - publicKeywordIndex - "public".Length).ToString() + ); + } + } + + private static Regex? _typeRegex; + + private static Regex GetTypeRegex() => _typeRegex ??= new Regex(@"\b(?:class|record|struct|enum|interface)\b", RegexOptions.Compiled); +} diff --git a/src/dotnetCampus.Logger.Analyzer/Generators/ProgramMainLogGenerator.cs b/src/dotnetCampus.Logger.Analyzer/Generators/ProgramMainLogGenerator.cs new file mode 100644 index 0000000..ed51799 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Generators/ProgramMainLogGenerator.cs @@ -0,0 +1,74 @@ +using System.Text; +using dotnetCampus.Logger.Assets.Templates; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using Microsoft.CodeAnalysis.Text; +using static dotnetCampus.Logger.Utils.CodeAnalysis.ProgramMainExtensions; + +namespace dotnetCampus.Logger.Generators; + +/// +/// 生成 Program.g.cs,为 Main 方法第一行日志生成支持代码。 +/// +[Generator] +public class ProgramMainLogGenerator : IIncrementalGenerator +{ + public void Initialize(IncrementalGeneratorInitializationContext context) + { + var provider = context.SyntaxProvider.CreateSyntaxProvider((node, ct) => + { + if (node is not MethodDeclarationSyntax mds) + { + // 必须是方法声明。 + return false; + } + + if (!CheckCanBeProgramMain(mds)) + { + // 必须符合 Main 方法的要求。 + return false; + } + + if (mds.Parent is not ClassDeclarationSyntax cds) + { + // 必须在类中。 + return false; + } + + if (!cds.Modifiers.Any(SyntaxKind.PartialKeyword)) + { + // 必须是 partial 类。 + return false; + } + + return true; + }, (c, ct) => + { + var mainMethodNode = (MethodDeclarationSyntax)c.Node; + var programClassNode = (ClassDeclarationSyntax)mainMethodNode.Parent!; + var programTypeSymbol = c.SemanticModel.GetDeclaredSymbol(programClassNode)!; + return programTypeSymbol; + }); + + context.RegisterSourceOutput(provider, Execute); + } + + private void Execute(SourceProductionContext context, INamedTypeSymbol programTypeSymbol) + { + // 生成 Program.Logger.g.cs + var partialLoggerFile = GeneratorInfo.GetEmbeddedTemplateFile(); + var generatedLoggerText = ConvertPartialProgramLogger(partialLoggerFile.Content, programTypeSymbol); + context.AddSource($"{programTypeSymbol.Name}.Logger.g.cs", SourceText.From(generatedLoggerText, Encoding.UTF8)); + } + + private string ConvertPartialProgramLogger(string sourceText, INamedTypeSymbol programTypeSymbol) + { + var templateProgramNamespace = typeof(Program).Namespace!; + var generatedProgramNamespace = programTypeSymbol.ContainingNamespace.ToDisplayString(); + return sourceText + .Replace("global::dotnetCampus.Logging.", $"global::{generatedProgramNamespace}.Logging.") + .Replace($"namespace {templateProgramNamespace};", $"namespace {generatedProgramNamespace};") + .Replace("partial class Program", $"partial class {programTypeSymbol.Name}"); + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/GlobalUsings.cs b/src/dotnetCampus.Logger.Analyzer/Properties/GlobalUsings.cs new file mode 100644 index 0000000..cf21062 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/GlobalUsings.cs @@ -0,0 +1 @@ +global using static dotnetCampus.Logger.Properties.Localizations; diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.Designer.cs b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.Designer.cs new file mode 100644 index 0000000..785b88a --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.Designer.cs @@ -0,0 +1,116 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace dotnetCampus.Logger.Properties { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "4.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + internal class Localizations { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal Localizations() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("dotnetCampus.Logger.Properties.Localizations", typeof(Localizations).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + internal static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Unknown Error. + /// + internal static string DL0000 { + get { + return ResourceManager.GetString("DL0000", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to An unknown error occurred.. + /// + internal static string DL0000_Message { + get { + return ResourceManager.GetString("DL0000_Message", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change the entry class to a partial class. + /// + internal static string DL1001 { + get { + return ResourceManager.GetString("DL1001", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to After changing the entry class to a partial class, the source generator can generate auxiliary log code according to the subsequent log initialization requirements; this way, you can even start using logs in the first sentence of the Main method without worrying about initialization issues.. + /// + internal static string DL1001_Description { + get { + return ResourceManager.GetString("DL1001_Description", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change '{0}' to a partial class. + /// + internal static string DL1001_Fix { + get { + return ResourceManager.GetString("DL1001_Fix", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Change '{0}' to a partial class to allow it to use the logging system before the logging module is initialized.. + /// + internal static string DL1001_Message { + get { + return ResourceManager.GetString("DL1001_Message", resourceCulture); + } + } + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.resx b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.resx new file mode 100644 index 0000000..593461c --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.resx @@ -0,0 +1,44 @@ + + + + + + + + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + Unknown Error + + + An unknown error occurred. {0} + + + Change the entry class to a partial class + + + Change '{0}' to a partial class to allow it to use the logging system before the logging module is initialized. + + + After changing the entry class to a partial class, the source generator can generate auxiliary log code according to the subsequent log initialization requirements; this way, you can even start using logs in the first sentence of the Main method without worrying about initialization issues. + + + [Logger/Performance] Set '{0}' to partial + + diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hans.resx b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hans.resx new file mode 100644 index 0000000..7fdf608 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hans.resx @@ -0,0 +1,36 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + 未知错误 + + + 发生了未知错误。{0} + + + 将 '{0}' 改为分部类,以允许其在日志模块初始化之前使用日志系统。 + + + 修改入口类为分部类 + + + 将入口类改为分部类后,源生成器可以根据后续日志初始化的要求生成辅助日志代码;这样,你甚至可以在 Main 方法第一句化就开始使用日志而无需担心初始化问题。 + + + [日志/性能] 将 '{0}' 设为 partial + + diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hant.resx b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hant.resx new file mode 100644 index 0000000..feefd4a --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/Localizations.zh-hant.resx @@ -0,0 +1,36 @@ + + + text/microsoft-resx + + + 1.3 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=2.0.0.0, Culture=neutral, + PublicKeyToken=b77a5c561934e089 + + + + 未知錯誤 + + + 發生了未知錯誤。{0} + + + 將 '{0}' 改為分部類,以允許其在日誌模塊初始化之前使用日誌系統。 + + + 修改入口類為分部類 + + + 將入口類改為分部類後,源生成器可以根據後續日誌初始化的要求生成輔助日誌代碼;這樣,你甚至可以在 Main 方法第一句化就開始使用日誌而無需擔心初始化問題。 + + + [日誌/性能] 將 '{0}' 設為 partial + + diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/copilot.md b/src/dotnetCampus.Logger.Analyzer/Properties/copilot.md new file mode 100644 index 0000000..e0eb565 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/copilot.md @@ -0,0 +1,13 @@ +# 多语言翻译工具 + +在有自动化翻译工具之前,我们可以先使用 Copilot 进行手工翻译。借助 Copilot 的智能提示,可以快速生成翻译文本。 + +--- + +请将以下 C# 分析器的 Id 进行翻译。 + +| Id | zh-hans | zh-hant | en | +|----------------|---------------------------|---------------------------|----------------------------------------| +| DL0000 | 未知错误 | 未知錯誤 | Unknown Error | +| DL0000_Message | 发生了未知错误。 | 發生了未知錯誤。 | An unknown error occurred. | +| DL1001 | [日志性能] 将 '{0}' 设为 partial | [日誌性能] 將 '{0}' 設為 partial | [Log Performance] Set '{0}' to partial | diff --git a/src/dotnetCampus.Logger.Analyzer/Properties/launchSettings.json b/src/dotnetCampus.Logger.Analyzer/Properties/launchSettings.json new file mode 100644 index 0000000..f3e8f20 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Properties/launchSettings.json @@ -0,0 +1,9 @@ +{ + "$schema": "https://json.schemastore.org/launchsettings.json", + "profiles": { + "DebugRoslynSourceGenerator": { + "commandName": "DebugRoslynComponent", + "targetProject": "../dotnetCampus.Logger.Analyzer.Sample/dotnetCampus.Logger.Analyzer.Sample.csproj" + } + } +} \ No newline at end of file diff --git a/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/AnalyzerConfigOptionsExtensions.cs b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/AnalyzerConfigOptionsExtensions.cs new file mode 100644 index 0000000..1ec7b40 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/AnalyzerConfigOptionsExtensions.cs @@ -0,0 +1,87 @@ +using System; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis.Diagnostics; + +namespace dotnetCampus.Logger.Utils.CodeAnalysis; + +internal static class AnalyzerConfigOptionsExtensions +{ + public static AnalyzerConfigOptionResult TryGetValue( + this AnalyzerConfigOptions options, + string key, + out T value) + where T : notnull + { + if (options.TryGetValue($"build_property.{key}", out var stringValue)) + { + value = ConvertFromString(stringValue); + return new AnalyzerConfigOptionResult(options, true) + { + UnsetPropertyNames = [], + }; + } + + value = default!; + return new AnalyzerConfigOptionResult(options, false) + { + UnsetPropertyNames = [key], + }; + } + + public static AnalyzerConfigOptionResult TryGetValue( + this AnalyzerConfigOptionResult builder, + string key, + out T value) + where T : notnull + { + var options = builder.Options; + + if (options.TryGetValue($"build_property.{key}", out var stringValue)) + { + value = ConvertFromString(stringValue); + return builder.Link(true, key); + } + + value = default!; + return builder.Link(false, key); + } + + private static T ConvertFromString(string value) + { + if (typeof(T) == typeof(string)) + { + return (T)(object)value; + } + if (typeof(T) == typeof(bool)) + { + return (T)(object)value.Equals("true", StringComparison.OrdinalIgnoreCase); + } + return default!; + } +} + +public readonly record struct AnalyzerConfigOptionResult(AnalyzerConfigOptions Options, bool GotValue) +{ + public required ImmutableList UnsetPropertyNames { get; init; } + + public AnalyzerConfigOptionResult Link(bool result, string propertyName) + { + if (result) + { + return this; + } + + if (propertyName is null) + { + throw new ArgumentNullException(nameof(propertyName), @"The property name must be specified if the result is false."); + } + + return this with + { + GotValue = false, + UnsetPropertyNames = UnsetPropertyNames.Add(propertyName), + }; + } + + public static implicit operator bool(AnalyzerConfigOptionResult result) => result.GotValue; +} diff --git a/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/DiagnosticExtensions.cs b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/DiagnosticExtensions.cs new file mode 100644 index 0000000..fa1a8b6 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/DiagnosticExtensions.cs @@ -0,0 +1,14 @@ +using Microsoft.CodeAnalysis; + +namespace dotnetCampus.Logger.Utils.CodeAnalysis; + +public static class DiagnosticExtensions +{ + public static void ReportUnknownError(this SourceProductionContext context, string message) + { + context.ReportDiagnostic(Diagnostic.Create( + Diagnostics.DL0000_UnknownError, + null, + message)); + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/ProgramMainExtensions.cs b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/ProgramMainExtensions.cs new file mode 100644 index 0000000..be9742c --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Utils/CodeAnalysis/ProgramMainExtensions.cs @@ -0,0 +1,65 @@ +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; + +namespace dotnetCampus.Logger.Utils.CodeAnalysis; + +public static class ProgramMainExtensions +{ + /// + /// 从语法上判断一个方法声明是否符合成为 Main 方法的要求。 + /// + /// 语法树中的方法声明。 + /// 是否符合成为 Main 方法的要求。 + public static bool CheckCanBeProgramMain(MethodDeclarationSyntax methodNode) + { + var methodName = methodNode.Identifier.Text; + if (methodName != "Main") + { + // 名称必须是 Main。 + return false; + } + + if (methodNode.Modifiers.Any(SyntaxKind.StaticKeyword) == false) + { + // 必须是静态方法。 + return false; + } + + if (methodNode.ParameterList.Parameters.Count > 1) + { + // 最多只能有一个参数。 + return false; + } + + if (methodNode.ParameterList.Parameters.Count == 1) + { + var parameter = methodNode.ParameterList.Parameters[0]; + if (parameter.Type is not ArrayTypeSyntax { ElementType: PredefinedTypeSyntax spts }) + { + // 参数必须是预定义类型。 + return false; + } + + if (!spts.Keyword.IsKind(SyntaxKind.StringKeyword)) + { + // 参数类型必须是 string[] 或 System.String[]。 + return false; + } + } + + if (methodNode.ReturnType is not PredefinedTypeSyntax ipts) + { + // 返回值必须是预定义类型。 + return false; + } + + if (!ipts.Keyword.IsKind(SyntaxKind.VoidKeyword) && !ipts.Keyword.IsKind(SyntaxKind.IntKeyword)) + { + // 返回值必须是 void 或 int。 + return false; + } + + return true; + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFile.cs b/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFile.cs new file mode 100644 index 0000000..0020cf1 --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFile.cs @@ -0,0 +1,14 @@ +namespace dotnetCampus.Logger.Utils.IO; + +/// +/// 嵌入的文本资源的数据。 +/// +/// 文件的名称(含扩展名)。 +/// 文件的名称(不含扩展名),或者也很可能是类型名称。 +/// 文件的命名空间。 +/// 文件的文本内容。 +internal readonly record struct EmbeddedSourceFile( + string FileName, + string TypeName, + string Namespace, + string Content); diff --git a/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFiles.cs b/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFiles.cs new file mode 100644 index 0000000..bf8a78b --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/Utils/IO/EmbeddedSourceFiles.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Reflection; + +namespace dotnetCampus.Logger.Utils.IO; + +/// +/// 从嵌入的资源中寻找源代码。 +/// +internal static class EmbeddedSourceFiles +{ + /// + /// 寻找 文件夹下的源代码名称和内容。 + /// + /// 资源文件夹名称。请以“/”或“\”分隔文件夹。 + /// + internal static IEnumerable Enumerate(string folderName) + { + // 资源字符串格式为:"{Namespace}.{Folder}.{filename}.{Extension}" + var desiredFolder = $"{GeneratorInfo.RootNamespace}.{folderName}"; + var assembly = Assembly.GetExecutingAssembly(); + foreach (var resourceName in assembly.GetManifestResourceNames()) + { + var prefix = desiredFolder.Replace('/', '.').Replace('\\', '.') + "."; + if (resourceName.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) + { + using var stream = assembly.GetManifestResourceStream(resourceName)!; + using var reader = new StreamReader(stream); + var contentText = reader.ReadToEnd(); + + var fileName = resourceName.AsSpan().Slice(prefix.Length).ToString(); + var fileNameWithoutExtension = fileName.Replace(".g.cs", "").Replace(".cs", ""); + var fileNameIndex = fileNameWithoutExtension.LastIndexOf('.'); + if (fileNameIndex < 0) + { + yield return new EmbeddedSourceFile( + fileName, + fileNameWithoutExtension, + desiredFolder, + contentText); + } + else + { + var typeName = fileNameWithoutExtension.Substring(fileNameIndex + 1); + var @namespace = $"{desiredFolder}.{fileNameWithoutExtension.Substring(0, fileNameIndex)}"; + + yield return new EmbeddedSourceFile( + fileName, + typeName, + @namespace, + contentText); + } + } + } + } +} diff --git a/src/dotnetCampus.Logger.Analyzer/dotnetCampus.Logger.Analyzer.csproj b/src/dotnetCampus.Logger.Analyzer/dotnetCampus.Logger.Analyzer.csproj new file mode 100644 index 0000000..93f27ca --- /dev/null +++ b/src/dotnetCampus.Logger.Analyzer/dotnetCampus.Logger.Analyzer.csproj @@ -0,0 +1,33 @@ + + + + netstandard2.0 + false + true + true + dotnetCampus.Logger + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + + + + + + + + + + + diff --git a/src/dotnetCampus.Logger/Attributes/ImportLoggerBridgeAttribute.cs b/src/dotnetCampus.Logger/Attributes/ImportLoggerBridgeAttribute.cs new file mode 100644 index 0000000..9c562f9 --- /dev/null +++ b/src/dotnetCampus.Logger/Attributes/ImportLoggerBridgeAttribute.cs @@ -0,0 +1,27 @@ +using System; + +namespace dotnetCampus.Logging.Attributes; + +/// +/// 指示源生成器应该生成对接指定的日志桥接器的代码。 +/// +/// +/// 桥接器的类型。通常名为 global::Xxx.Logging.ILoggerBridge,其中 Xxx 为被桥接的库的根命名空间。 +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class ImportLoggerBridgeAttribute(Type loggerBridgeInterfaceType) : Attribute +{ + /// + /// 获取桥接器的类型。 + /// + public Type LoggerBridgeInterfaceType { get; } = loggerBridgeInterfaceType; +} + +/// +/// 指示源生成器应该生成对接指定的日志桥接器的代码。 +/// +/// +/// 桥接器的类型。通常名为 global::Xxx.Logging.BridgeLogger,其中 Xxx 为被桥接的库的根命名空间。 +/// +[AttributeUsage(AttributeTargets.Class, AllowMultiple = true, Inherited = false)] +public class ImportLoggerBridgeAttribute() : ImportLoggerBridgeAttribute(typeof(T)); diff --git a/src/dotnetCampus.Logger/Bridges/BridgeLogger.g.cs b/src/dotnetCampus.Logger/Bridges/BridgeLogger.g.cs new file mode 100644 index 0000000..4bd78c0 --- /dev/null +++ b/src/dotnetCampus.Logger/Bridges/BridgeLogger.g.cs @@ -0,0 +1,23 @@ +#nullable enable + +using System; +using System.ComponentModel; + +namespace dotnetCampus.Logging.Bridges; + +/// +/// dotnetCampus.Logging.Bridges 的桥接日志记录器。 +/// +internal class BridgeLogger : ILogger +{ + /// + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { +#if NETCOREAPP3_0_OR_GREATER + ILoggerBridge +#else + LoggerBridgeLinker +#endif + .Bridge?.Log((int)logLevel, eventId.Id, eventId.Name, state, exception, formatter); + } +} diff --git a/src/dotnetCampus.Logger/Bridges/ILoggerBridge.g.cs b/src/dotnetCampus.Logger/Bridges/ILoggerBridge.g.cs new file mode 100644 index 0000000..b783bb9 --- /dev/null +++ b/src/dotnetCampus.Logger/Bridges/ILoggerBridge.g.cs @@ -0,0 +1,56 @@ +#nullable enable + +using System; +using System.ComponentModel; + +namespace dotnetCampus.Logging.Bridges; + +/// +/// 仅由 .NET 库类型构成的日志桥。用于源生成器将无依赖库中的日志重定向到应用程序聚合日志系统中。 +/// +public interface ILoggerBridge +{ + /// + /// 写入日志条目。 + /// + /// 将在此级别上写入条目。 + /// 事件的 Id。 + /// 事件的名称。 + /// 要写入的条目。也可以是一个对象。 + /// 与此条目相关的异常。 + /// 创建一条字符串消息以记录 。 + /// 要写入的对象的类型。 + void Log( + int logLevel, + int eventId, + string? eventName, + TState state, + Exception? exception, + Func formatter); + +#if !NETCOREAPP3_0_OR_GREATER +} + +/// +/// 提供一个静态类,用于连接日志桥到当前的桥接日志记录器中。 +/// +[EditorBrowsable(EditorBrowsableState.Never)] +public static class LoggerBridgeLinker +{ +#endif + + /// + /// 获取当前的日志桥。 + /// + internal static ILoggerBridge? Bridge; + + /// + /// 连接一个日志桥到当前的桥接日志记录器中。 + /// 这样,所有使用此桥接日志记录器记录的日志会全部被重定向到日志桥中。 + /// + /// 要连接此日志记录器的日志桥。 + public static void Link(ILoggerBridge bridge) + { + Bridge = bridge; + } +} diff --git a/src/dotnetCampus.Logger/Bridges/ILoggerBridgeLinker.cs b/src/dotnetCampus.Logger/Bridges/ILoggerBridgeLinker.cs new file mode 100644 index 0000000..3b04333 --- /dev/null +++ b/src/dotnetCampus.Logger/Bridges/ILoggerBridgeLinker.cs @@ -0,0 +1,17 @@ +namespace dotnetCampus.Logging.Bridges; + +/// +/// 表示一个日志桥对接器。 +/// +public interface ILoggerBridgeLinker +{ + /// + /// 将已指派给此日志桥连接器的所有日志桥对接到 日志记录器上。 + /// + /// 要对接的日志记录器。 + /// + /// 如果已经对接过日志记录器,则会抛出此异常。 + /// 如果希望针对不同的库对接不同的日志记录器,请编写多个聚合日志桥并分别导入各自的日志桥。 + /// + void Link(ILogger logger); +} diff --git a/src/dotnetCampus.Logger/Class1.cs b/src/dotnetCampus.Logger/Class1.cs deleted file mode 100644 index 62d3abd..0000000 --- a/src/dotnetCampus.Logger/Class1.cs +++ /dev/null @@ -1,9 +0,0 @@ -using System; - -namespace dotnetCampus.Logger -{ - class Class1 - { - - } -} diff --git a/src/dotnetCampus.Logger/CompositeLogger.cs b/src/dotnetCampus.Logger/CompositeLogger.cs new file mode 100644 index 0000000..5b497b7 --- /dev/null +++ b/src/dotnetCampus.Logger/CompositeLogger.cs @@ -0,0 +1,44 @@ +using System; +using dotnetCampus.Logging.Configurations; + +namespace dotnetCampus.Logging; + +/// +/// 一个聚合多个日志记录器的综合记录器,通常作为应用程序的主要日志记录器。 +/// +public class CompositeLogger : ILogger +{ + internal CompositeLogger(LogOptions options) + { + Configuration = new InheritedConfiguration(options); + } + + private InheritedConfiguration Configuration { get; } + + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public LogLevel Level + { + get => Configuration.GetValue(o => o.LogLevel); + set => Configuration.SetValue(o => o.LogLevel = value); + } + + /// + /// 当前所有的日志记录器。 + /// + public required ImmutableArrayILogger Writers { get; init; } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel < Level) + { + return; + } + + foreach (var writer in Writers) + { + writer.Log(logLevel, eventId, state, exception, formatter); + } + } +} diff --git a/src/dotnetCampus.Logger/Configurations/InheritedConfiguration.cs b/src/dotnetCampus.Logger/Configurations/InheritedConfiguration.cs new file mode 100644 index 0000000..b8b72ce --- /dev/null +++ b/src/dotnetCampus.Logger/Configurations/InheritedConfiguration.cs @@ -0,0 +1,36 @@ +using System; + +namespace dotnetCampus.Logging.Configurations; + +internal class InheritedConfiguration(T options) + where T : notnull +{ + private InheritedConfiguration? _parent; + + internal void AddChild(TChild child) + where TChild : InheritedConfiguration + { + child._parent = this; + } + + internal TValue GetValue(Func getter, TValue defaultValue = default!) + { + var value = getter(options); + if (value is not null) + { + return value; + } + + if (_parent is not null) + { + return _parent.GetValue(getter, defaultValue); + } + + return defaultValue; + } + + internal void SetValue(Func setter) + { + setter(options); + } +} diff --git a/src/dotnetCampus.Logger/Configurations/LogOptions.cs b/src/dotnetCampus.Logger/Configurations/LogOptions.cs new file mode 100644 index 0000000..477a50d --- /dev/null +++ b/src/dotnetCampus.Logger/Configurations/LogOptions.cs @@ -0,0 +1,12 @@ +namespace dotnetCampus.Logging.Configurations; + +/// +/// 用于配置日志记录器的选项。 +/// +public record LogOptions +{ + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public LogLevel LogLevel { get; set; } +} diff --git a/src/dotnetCampus.Logger/Configurations/MemoryCacheOptions.cs b/src/dotnetCampus.Logger/Configurations/MemoryCacheOptions.cs new file mode 100644 index 0000000..203877e --- /dev/null +++ b/src/dotnetCampus.Logger/Configurations/MemoryCacheOptions.cs @@ -0,0 +1,17 @@ +namespace dotnetCampus.Logging.Configurations; + +/// +/// 当日志系统尚未初始化,但有模块已经开始记录日志时,此选项指定这些日志应如何缓存。 +/// +public record MemoryCacheOptions +{ + /// + /// 缓存的日志数量上限。超过此数量的日志将被丢弃。 + /// + public int MaxCachedLogCount { get; init; } = 1024; + + /// + /// 在日志系统初始化完成前,如果应用程序崩溃退出,则会将崩溃日志写入到此文件中。 + /// + public string? EmergencyCrashLoggerFile { get; init; } +} diff --git a/src/dotnetCampus.Logger/EventId.g.cs b/src/dotnetCampus.Logger/EventId.g.cs new file mode 100644 index 0000000..7393f09 --- /dev/null +++ b/src/dotnetCampus.Logger/EventId.g.cs @@ -0,0 +1,96 @@ +#nullable enable + +using global::System.Diagnostics.CodeAnalysis; + +namespace dotnetCampus.Logging; + +/// +/// 标识一个日志事件。主要标识是 "Id" 属性,而 "Name" 属性提供了此类型事件的简短描述。 +/// +public readonly struct EventId +{ + /// + /// 从给定的 隐式创建一个 EventId。 + /// + /// 要转换为 EventId 的 。 + public static implicit operator EventId(int i) + { + return new EventId(i); + } + + /// + /// 检查两个指定的 实例是否具有相同的值。如果它们具有相同的 Id,则它们是相等的。 + /// + /// 第一个 。 + /// 第二个 。 + /// 如果对象相等,则为 + public static bool operator ==(EventId left, EventId right) + { + return left.Equals(right); + } + + /// + /// 检查两个指定的 实例是否具有不同的值。 + /// + /// 第一个 。 + /// 第二个 。 + /// 如果对象不相等,则为 + public static bool operator !=(EventId left, EventId right) + { + return !left.Equals(right); + } + + /// + /// 初始化 结构的新实例。 + /// + /// 此事件的数字标识符。 + /// 此事件的名称。 + public EventId(int id, string? name = null) + { + Id = id; + Name = name; + } + + /// + /// 获取此事件的数字标识符。 + /// + public int Id { get; } + + /// + /// 获取此事件的名称。 + /// + public string? Name { get; } + + /// + public override string ToString() + { + return Name ?? Id.ToString(); + } + + /// + /// 指示当前对象是否等于另一个相同类型的对象。如果两个事件具有相同的 id,则它们是相等的。 + /// + /// 要与此对象进行比较的对象。 + /// 如果两个对象相等,则为 ;否则为 + public bool Equals(EventId other) + { + return Id == other.Id; + } + + /// + public override bool Equals([NotNullWhen(true)] object? obj) + { + if (obj is null) + { + return false; + } + + return obj is EventId eventId && Equals(eventId); + } + + /// + public override int GetHashCode() + { + return Id; + } +} diff --git a/src/dotnetCampus.Logger/ILogger.g.cs b/src/dotnetCampus.Logger/ILogger.g.cs new file mode 100644 index 0000000..8ff001d --- /dev/null +++ b/src/dotnetCampus.Logger/ILogger.g.cs @@ -0,0 +1,30 @@ +#nullable enable + +using global::System; + +namespace dotnetCampus.Logging; + +/// +/// 表示用于执行日志记录的类型。 +/// +/// +/// 将大多数日志模式聚合到一个方法中。 +/// +public interface ILogger +{ + /// + /// 写入日志条目。 + /// + /// 将在此级别上写入条目。 + /// 事件的 Id。 + /// 要写入的条目。也可以是一个对象。 + /// 与此条目相关的异常。 + /// 创建一条字符串消息以记录 。 + /// 要写入的对象的类型。 + void Log( + LogLevel logLevel, + EventId eventId, + TState state, + Exception? exception, + Func formatter); +} diff --git a/src/dotnetCampus.Logger/Log.g.cs b/src/dotnetCampus.Logger/Log.g.cs new file mode 100644 index 0000000..4432412 --- /dev/null +++ b/src/dotnetCampus.Logger/Log.g.cs @@ -0,0 +1,104 @@ +#nullable enable + +using global::System; +using global::System.Diagnostics.CodeAnalysis; + +namespace dotnetCampus.Logging; + +/// +/// 提供静态的日志记录方法。 +/// +public static partial class Log +{ + private static ILogger _current; + + static Log() + { + // 在全局日志中,默认日志记录器是 MemoryCacheLogger。 + // 然而在源生成器为单独库生成的代码中,默认日志记录器是 BridgeLogger。 + Current = new MemoryCacheLogger(); + } + + /// + /// 获取此静态日志记录器当前所用的日志记录器实例。 + /// + public static ILogger Current + { + get => _current; + [MemberNotNull(nameof(_current), nameof(Debug), nameof(Trace))] + private set + { + if (value == null) + { + throw new ArgumentNullException(nameof(value)); + } + + if (Equals(_current, value)) + { + Debug ??= new DebugLogger(value); + Trace ??= new TraceLogger(value); + return; + } + + _current = value; + Debug = new DebugLogger(value); + Trace = new TraceLogger(value); + } + } + + /// + /// 获取仅在 debug 配置下才会记录的日志记录器。 + /// + /// + /// 请注意,所有通过此记录器记录的日志仅在以 debug 配置编译时才会被记录,并且在非 debug 编译后对此记录器的调用都会被编译器优化掉。 + /// + public static DebugLogger Debug { get; private set; } + + /// + /// 获取仅在 TRACE 条件编译符被定义时才会记录的日志记录器。 + /// 通常情况下,debug 和 release 配置下都会定义 TRACE 条件编译符。 + /// + /// + /// 请注意,所有通过此记录器记录的日志仅在定义了 TRACE 条件编译符时才会被记录,并且在未定义 TRACE 条件编译符后对此记录器的调用都会被编译器优化掉。 + /// + public static TraceLogger Trace { get; private set; } + + /// + /// 记录信息日志。 + /// + /// 要记录的消息。 + public static void Info(string message) + { + Current.Log(LogLevel.Information, default, message, null, (s, ex) => message); + } + + /// + /// 记录警告日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Warn(string message, Exception? exception = null) + { + Current.Log(LogLevel.Warning, default, message, exception, (s, ex) => message); + } + + /// + /// 记录错误日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Error(string message, Exception? exception = null) + { + Current.Log(LogLevel.Error, default, message, exception, (s, ex) => message); + } + + /// + /// 记录崩溃日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Fatal(string message, Exception? exception = null) + { + Current.Log(LogLevel.Critical, default, message, null, (s, ex) => message); + } +} diff --git a/src/dotnetCampus.Logger/LogLevel.g.cs b/src/dotnetCampus.Logger/LogLevel.g.cs new file mode 100644 index 0000000..9a46a7a --- /dev/null +++ b/src/dotnetCampus.Logger/LogLevel.g.cs @@ -0,0 +1,44 @@ +#nullable enable + +namespace dotnetCampus.Logging; + +/// +/// 定义日志的严重程度。 +/// +public enum LogLevel +{ + /// + /// 包含最详细消息的日志。这些消息可能包含敏感的应用程序数据。默认情况下禁用这些消息,不应在生产环境中启用。 + /// + Trace = 0, + + /// + /// 用于开发过程中的交互式调查的日志。这些日志应主要包含有用于调试的信息,没有长期价值。 + /// + Debug = 1, + + /// + /// 跟踪应用程序的一般流程的日志。这些日志应具有长期价值。 + /// + Information = 2, + + /// + /// 强调应用程序流程中的异常或意外事件的日志,但不会导致应用程序执行停止。 + /// + Warning = 3, + + /// + /// 强调当前执行流程由于失败而停止的日志。这些日志应指示当前活动的失败,而不是应用程序范围的失败。 + /// + Error = 4, + + /// + /// 描述不可恢复的应用程序或系统崩溃,或需要立即处理的灾难性失败的日志。 + /// + Critical = 5, + + /// + /// 此严重程度不用于写入日志消息,配置成此级别仅表示不会写入任何日志。 + /// + None = 6, +} diff --git a/src/dotnetCampus.Logger/LoggerBuilder.cs b/src/dotnetCampus.Logger/LoggerBuilder.cs new file mode 100644 index 0000000..3fcc2cb --- /dev/null +++ b/src/dotnetCampus.Logger/LoggerBuilder.cs @@ -0,0 +1,117 @@ +using System; +using System.Collections.Generic; +using dotnetCampus.Logging.Bridges; +using dotnetCampus.Logging.Configurations; +using dotnetCampus.Logging.Writers; + +namespace dotnetCampus.Logging; + +/// +/// 辅助创建日志记录器的构建器。 +/// +public class LoggerBuilder +{ + private LogOptions? _options; + private readonly List _writers = []; + private readonly List _linkers = []; + + /// + /// 指定在日志模块完成初始化之前直接或间接调用全局 所记录的日志应被缓存到内存中。 + /// + /// + /// 指定缓存日志的选项。 + /// + /// + /// 此方法不会在运行时起任何作用!!!
+ /// 此方法仅在编译期决定全局日志 的行为,并且一旦编译完成,此行为将不可更改。
+ ///
+ public LoggerBuilder WithMemoryCache(MemoryCacheOptions? options = null) + { + return this; + } + + public LoggerBuilder WithLevel(LogLevel level) + { + _options ??= new LogOptions(); + _options.LogLevel = level; + return this; + } + + public LoggerBuilder WithOptions(LogOptions options) + { + _options = options; + return this; + } + + public LoggerBuilder AddWriter(ILogger writer) + { + _writers.Add(writer); + return this; + } + + public LoggerBuilder AddBridge(ILoggerBridgeLinker linker) + { + _linkers.Add(linker); + return this; + } + + public LoggerBuilder Build() + { + var logger = new CompositeLogger(_options ?? new LogOptions()) + { + Writers = [.._writers], + }; + foreach (var linker in _linkers) + { + linker.Link(logger); + } + return new LoggerBuilder(logger); + } +} + +/// +/// 包含已经创建完成的日志记录器,日志记录器初始状态已不可再更改,但可以继续构建以设置日志记录器的其他行为。 +/// +/// 已经创建完成的日志记录器。 +/// 日志记录器的类型。 +public sealed class LoggerBuilder(T logger) where T : ILogger +{ + /// + /// 获取创建好的日志记录器。 + /// + public T Logger => logger; + + /// + /// 将创建好的日志记录器设置为全局日志记录器。 + /// + /// 已经创建完成的日志记录器。 + public T IntoGlobalStaticLog() + { + Log.SetLogger(logger); + return logger; + } + + /// + /// 隐式将创建好的日志记录器转换为日志记录器实例。 + /// + /// 要转换的日志记录器构建器。 + /// 已经创建完成的日志记录器。 + public static implicit operator T(LoggerBuilder builder) => builder.Logger; +} + +partial class Log +{ + /// + /// 仅供内部使用,将指定的日志记录器设置为全局日志记录器。 + /// + /// 要设置为全局的日志记录器。 + internal static void SetLogger(ILogger logger) + { + var oldLogger = Current; + Current = logger; + if (oldLogger is MemoryCacheLogger mcl) + { + mcl.Flush(logger); + } + } +} diff --git a/src/dotnetCampus.Logger/LoggerExtensions.g.cs b/src/dotnetCampus.Logger/LoggerExtensions.g.cs new file mode 100644 index 0000000..3d77c48 --- /dev/null +++ b/src/dotnetCampus.Logger/LoggerExtensions.g.cs @@ -0,0 +1,82 @@ +#nullable enable + +using global::System; + +namespace dotnetCampus.Logging; + +/// +/// 的常见场景的扩展方法。 +/// +public static class LoggerExtensions +{ + /// + /// 记录追踪级别的日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + /// + /// 请注意,这里的 仅代表追踪级别;如果配置输出 trace 级别的日志,即便编译时未定义 TRACE 条件编译符也会输出。
+ /// 如果希望仅在定义了 TRACE 条件编译符时输出日志,请使用 .。 + ///
+ public static void Trace(this ILogger logger, string message) + { + logger.Log(LogLevel.Trace, default, message, null, (s, ex) => message); + } + + /// + /// 记录调试级别的日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + /// + /// 请注意,这里的 仅代表调试级别;如果配置了输出 debug 级别的日志,即便是 release 编译也会输出。
+ /// 如果希望仅在 debug 配置下输出日志,请使用 .。 + ///
+ public static void Debug(this ILogger logger, string message) + { + logger.Log(LogLevel.Debug, default, message, null, (s, ex) => message); + } + + /// + /// 记录信息日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + public static void Info(this ILogger logger, string message) + { + logger.Log(LogLevel.Information, default, message, null, (s, ex) => message); + } + + /// + /// 记录警告日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Warn(this ILogger logger, string message, Exception? exception = null) + { + logger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录错误日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Error(this ILogger logger, string message, Exception? exception = null) + { + logger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录崩溃日志。 + /// + /// 记录日志所使用的记录器。 + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + public static void Fatal(this ILogger logger, string message, Exception? exception = null) + { + logger.Log(LogLevel.Critical, default, message, null, (s, ex) => message); + } +} diff --git a/src/dotnetCampus.Logger/Properties/Compatibility.cs b/src/dotnetCampus.Logger/Properties/Compatibility.cs new file mode 100644 index 0000000..a6e4d9c --- /dev/null +++ b/src/dotnetCampus.Logger/Properties/Compatibility.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using SMath = System.Math; + +namespace dotnetCampus.Logging.Properties; + +internal static class Compatibility +{ +#if NET8_0_OR_GREATER +#else + internal static ImmutableHashSetString ToImmutableHashSet(this IEnumerable source) + { + return [..source]; + } +#endif + +#if NET6_0_OR_GREATER +#else + internal static string AsSpan(this string text) + { + return text; + } + + internal static bool Contains(this string text, char value) + { + return text.Contains(value.ToString()); + } + + internal static string Slice(this string text, int start, int length) + { + return text.Substring(start, length); + } + + public static int Clamp(int value, int min, int max) + { + return SMath.Max(min, SMath.Min(max, value)); + } +#endif +} diff --git a/src/dotnetCampus.Logger/Properties/CompatibilityGlobalUsings.cs b/src/dotnetCampus.Logger/Properties/CompatibilityGlobalUsings.cs new file mode 100644 index 0000000..528e55f --- /dev/null +++ b/src/dotnetCampus.Logger/Properties/CompatibilityGlobalUsings.cs @@ -0,0 +1,20 @@ +global using dotnetCampus.Logging.Properties; + +// .NET 8.0 or later + +#if NET8_0_OR_GREATER +global using ImmutableArrayILogger = System.Collections.Immutable.ImmutableArray; +global using ImmutableHashSetString = System.Collections.Immutable.ImmutableHashSet; +#else +global using ImmutableArrayILogger = System.Collections.Generic.List; +global using ImmutableHashSetString = System.Collections.Generic.HashSet; +#endif + +// .NET 6.0 or later +#if NET6_0_OR_GREATER +global using System.Collections.Immutable; +global using Math = System.Math; + +#else +global using Math = dotnetCampus.Logging.Properties.Compatibility; +#endif diff --git a/src/dotnetCampus.Logger/Properties/GlobalUsings.cs b/src/dotnetCampus.Logger/Properties/GlobalUsings.cs new file mode 100644 index 0000000..ee32b80 --- /dev/null +++ b/src/dotnetCampus.Logger/Properties/GlobalUsings.cs @@ -0,0 +1,4 @@ +global using DebugLogger = dotnetCampus.Logging.Writers.DebugLogger; +global using MemoryCacheLogger = dotnetCampus.Logging.Writers.MemoryCacheLogger; +global using NullLogger = dotnetCampus.Logging.Writers.NullLogger; +global using TraceLogger = dotnetCampus.Logging.Writers.TraceLogger; diff --git a/src/dotnetCampus.Logger/Properties/Package/build/Package.props b/src/dotnetCampus.Logger/Properties/Package/build/Package.props new file mode 100644 index 0000000..33e458d --- /dev/null +++ b/src/dotnetCampus.Logger/Properties/Package/build/Package.props @@ -0,0 +1,24 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + + + + + diff --git a/src/dotnetCampus.Logger/Properties/Package/build/Package.targets b/src/dotnetCampus.Logger/Properties/Package/build/Package.targets new file mode 100644 index 0000000..e830c51 --- /dev/null +++ b/src/dotnetCampus.Logger/Properties/Package/build/Package.targets @@ -0,0 +1,57 @@ + + + + $(MSBuildAllProjects);$(MSBuildThisFileFullPath) + + + + + onlyReference + + + + + <_DLGenerateSource>true + <_DLGenerateGlobalUsings>false + <_DLPreferGeneratedSource>true + + + + + + + + + + <_DLGenerateSource>true + <_DLGenerateGlobalUsings>true + <_DLPreferGeneratedSource>true + + + + + <_DLGenerateSource>true + <_DLGenerateGlobalUsings>true + <_DLPreferGeneratedSource>false + + + + + <_DLGenerateSource Condition=" '$(_DLGenerateSource)' == '' ">false + <_DLGenerateGlobalUsings Condition=" '$(_DLGenerateGlobalUsings)' == '' ">false + <_DLPreferGeneratedSource Condition=" '$(_DLPreferGeneratedSource)' == '' ">false + + + + <_DLRootNamespace>$(RootNamespace) + <_DLRootNamespace Condition=" '$(_DLRootNamespace)' == '' ">$(MSBuildProjectName.Replace(" ", "_")) + + + + + + + + + + diff --git a/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs new file mode 100644 index 0000000..d068249 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/ConsoleLogger.cs @@ -0,0 +1,194 @@ +using System; +using System.IO; +using dotnetCampus.Logging.Writers.Helpers; +using C = dotnetCampus.Logging.Writers.ConsoleLoggerHelpers.ConsoleColors; +using B = dotnetCampus.Logging.Writers.ConsoleLoggerHelpers.ConsoleColors.Background; +using D = dotnetCampus.Logging.Writers.ConsoleLoggerHelpers.ConsoleColors.Decoration; +using F = dotnetCampus.Logging.Writers.ConsoleLoggerHelpers.ConsoleColors.Foreground; + +namespace dotnetCampus.Logging.Writers; + +public class ConsoleLogger : ILogger +{ + /// + /// 控制台光标控制是否启用。目前可容纳的错误次数为 3 次,当降低到 0 次时,将不再尝试移动光标。 + /// + private int _isCursorMovementEnabled = 3; + + private readonly RepeatLoggerDetector _repeat; + + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public LogLevel Level { get; set; } + + public ConsoleLogger() + { + _repeat = new(ClearAndMoveToLastLine); + } + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (logLevel < Level) + { + return; + } + + var message = formatter(state, exception); + if (!IsTagEnabled(message)) + { + return; + } + + LogCore(logLevel, exception, message, m => logLevel switch + { + LogLevel.Trace => $"{TraceTag} {TraceText}{m}{Reset}", + LogLevel.Debug => $"{DebugTag} {DebugText}{m}{Reset}", + LogLevel.Information => $"{InformationTag} {InformationText}{m}{Reset}", + LogLevel.Warning => $"{WarningTag} {WarningText}{m}{Reset}", + LogLevel.Error => $"{ErrorTag} {ErrorText}{m}{Reset}", + LogLevel.Critical => $"{CriticalTag} {CriticalText}{m}{Reset}", + _ => null, + }); + } + + private void LogCore(LogLevel logLevel, Exception? exception, string message, Func formatter) + { + if (_repeat.RepeatOrResetLastLog(logLevel, message, exception) is var count and > 1) + { + ConsoleMultilineMessage($"上述日志已重复 {count} 次", formatter, true); + } + else if (exception is null) + { + ConsoleMultilineMessage(message, formatter); + } + else + { + var tag = logLevel switch + { + LogLevel.Warning => WarningExceptionTag, + LogLevel.Error => ErrorExceptionTag, + LogLevel.Critical => CriticalExceptionTag, + _ => "", + }; + ConsoleMultilineMessage($""" + {message} + {tag}{exception.GetType().Name}: {exception.Message} + """, formatter); + } + } + + private static void ConsoleMultilineMessage(string message, Func formatter, bool forceSingleLine = false) + { + if (forceSingleLine || !message.Contains('\n')) + { + Console.WriteLine(formatter(message)); + } + else + { + using var reader = new StringReader(message); + while (reader.ReadLine() is { } line) + { + Console.WriteLine(formatter(line)); + } + } + } + + /// + /// 当前已设置的过滤标签。 + /// + private static ImmutableHashSetString ConsoleFilterTags { get; set; } = []; + + /// + /// 高于或等于此级别的日志才会被记录。 + /// + public ConsoleLogger UseLevel(LogLevel level) + { + Level = level; + return this; + } + + /// + /// 从命令行参数中提取过滤标签。 + /// + /// 命令行参数。 + public ConsoleLogger FilterConsoleTagsFromCommandLineArgs(string[] args) + { + for (var i = 0; i < args.Length; i++) + { + if (args[i] == "--log-console-tags" && i + 1 < args.Length) + { + ConsoleFilterTags = args[i + 1].Split([',', ';', ' ']).ToImmutableHashSet(); + break; + } + } + return this; + } + + /// + /// 判断某个日志是否满足当前标签过滤条件。 + /// + /// 要判断的日志原文。 + /// 是否满足过滤条件。 + private static bool IsTagEnabled(string text) + { + if (ConsoleFilterTags.Count is 0) + { + return true; + } + + var start = text.IndexOf('['); + if (start == -1) + { + return true; + } + var end = text.IndexOf(']', start); + if (end == -1) + { + return true; + } + + var tag = text.AsSpan().Slice(start + 1, end - start - 1); + return ConsoleFilterTags.Contains(tag.ToString()); + } + + private void ClearAndMoveToLastLine(int repeatCount) + { + if (_isCursorMovementEnabled > 0 && repeatCount > 2) + { + try + { + var desiredY = Console.CursorTop - 1; + var y = Math.Clamp(desiredY, 0, Console.WindowHeight - 1); + Console.SetCursorPosition(0, y); + Console.Write(new string(' ', Console.WindowWidth)); + Console.SetCursorPosition(0, y); + } + catch (IOException) + { + // 日志记录时,如果无法移动光标,说明可能当前输出位置不在缓冲区内。 + // 如果多次尝试失败,则认为当前控制台缓冲区不支持光标移动,遂放弃。 + _isCursorMovementEnabled--; + } + } + } + + private const string Reset = C.Reset; + private const string DebugText = F.White; + private const string TraceText = F.BrightBlack; + private const string InformationText = F.Green + D.Bold; + private const string WarningText = F.Yellow; + private const string ErrorText = F.BrightRed; + private const string CriticalText = F.Red; + + private static string TraceTag => $"{B.Black}{F.BrightBlack}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + private static string DebugTag => $"{B.BrightBlack}{F.White}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + private static string InformationTag => $"{B.Green}{F.Black}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + private static string WarningTag => $"{B.Yellow}{F.Black}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + private static string ErrorTag => $"{B.BrightRed}{F.Black}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + private static string CriticalTag => $"{B.Red}{F.Black}[{DateTime.Now:HH:mm:ss.fff}]{Reset}"; + + private static string WarningExceptionTag => $"{B.Yellow}{F.Black} ! {Reset}{WarningText} "; + private static string ErrorExceptionTag => $"{B.BrightRed}{F.Black} X {Reset}{ErrorText} "; + private static string CriticalExceptionTag => $"{B.Red}{F.Black} 💥 {Reset}{CriticalText} "; +} diff --git a/src/dotnetCampus.Logger/Writers/ConsoleLoggerHelpers/ConsoleColors.cs b/src/dotnetCampus.Logger/Writers/ConsoleLoggerHelpers/ConsoleColors.cs new file mode 100644 index 0000000..be71db8 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/ConsoleLoggerHelpers/ConsoleColors.cs @@ -0,0 +1,69 @@ +namespace dotnetCampus.Logging.Writers.ConsoleLoggerHelpers; + +/// +/// 包含控制台输出颜色的字符串常量。 +/// +internal static class ConsoleColors +{ + public const string Reset = "\u001b[0m"; + + public static class Foreground + { + #region 4-bit colors + + public const string Black = "\u001b[30m"; + public const string Red = "\u001b[31m"; + public const string Green = "\u001b[32m"; + public const string Yellow = "\u001b[33m"; + public const string Blue = "\u001b[34m"; + public const string Magenta = "\u001b[35m"; + public const string Cyan = "\u001b[36m"; + public const string White = "\u001b[37m"; + public const string BrightBlack = "\u001b[90m"; + public const string BrightRed = "\u001b[91m"; + public const string BrightGreen = "\u001b[92m"; + public const string BrightYellow = "\u001b[93m"; + public const string BrightBlue = "\u001b[94m"; + public const string BrightMagenta = "\u001b[95m"; + public const string BrightCyan = "\u001b[96m"; + public const string BrightWhite = "\u001b[97m"; + + #endregion + } + + public static class Background + { + #region 4-bit colors + + public const string Black = "\u001b[40m"; + public const string Red = "\u001b[41m"; + public const string Green = "\u001b[42m"; + public const string Yellow = "\u001b[43m"; + public const string Blue = "\u001b[44m"; + public const string Magenta = "\u001b[45m"; + public const string Cyan = "\u001b[46m"; + public const string White = "\u001b[47m"; + public const string BrightBlack = "\u001b[100m"; + public const string BrightRed = "\u001b[101m"; + public const string BrightGreen = "\u001b[102m"; + public const string BrightYellow = "\u001b[103m"; + public const string BrightBlue = "\u001b[104m"; + public const string BrightMagenta = "\u001b[105m"; + public const string BrightCyan = "\u001b[106m"; + public const string BrightWhite = "\u001b[107m"; + + #endregion + } + + public static class Decoration + { + public const string Bold = "\u001b[1m"; + public const string Dim = "\u001b[2m"; + public const string Italic = "\u001b[3m"; + public const string Underline = "\u001b[4m"; + public const string Blink = "\u001b[5m"; + public const string Reverse = "\u001b[7m"; + public const string Hidden = "\u001b[8m"; + public const string Strikethrough = "\u001b[9m"; + } +} diff --git a/src/dotnetCampus.Logger/Writers/DebugLogger.g.cs b/src/dotnetCampus.Logger/Writers/DebugLogger.g.cs new file mode 100644 index 0000000..842ebb0 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/DebugLogger.g.cs @@ -0,0 +1,80 @@ +#nullable enable + +using global::System; +using global::System.Diagnostics; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 提供仅在 debug 下才会记录的日志。 +/// +public class DebugLogger(ILogger realLogger) : ILogger +{ + /// + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + realLogger.Log(logLevel, eventId, state, exception, formatter); + } + + /// + /// 记录追踪级别的日志。 + /// + /// 要记录的消息。 + [Conditional("DEBUG")] + public void Trace(string message) + { + realLogger.Log(LogLevel.Trace, default, message, null, (s, ex) => message); + } + + /// + /// 记录调试级别的日志。 + /// + /// 要记录的消息。 + [Conditional("DEBUG")] + public void Debug(string message) + { + realLogger.Log(LogLevel.Debug, default, message, null, (s, ex) => message); + } + + /// + /// 记录信息日志。 + /// + /// 要记录的消息。 + [Conditional("DEBUG")] + public void Info(string message) + { + realLogger.Log(LogLevel.Information, default, message, null, (s, ex) => message); + } + + /// + /// 记录警告日志。 + /// + /// 要记录的消息,形如 [tag] message + [Conditional("DEBUG")] + public void Warn(string message) + { + realLogger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录错误日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + [Conditional("DEBUG")] + public void Error(string message, Exception? exception = null) + { + realLogger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录崩溃日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + [Conditional("DEBUG")] + public void Fatal(string message, Exception? exception = null) + { + realLogger.Log(LogLevel.Critical, default, message, null, (s, ex) => message); + } +} diff --git a/src/dotnetCampus.Logger/Writers/Helpers/RepeatLoggerDetector.cs b/src/dotnetCampus.Logger/Writers/Helpers/RepeatLoggerDetector.cs new file mode 100644 index 0000000..c87c391 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/Helpers/RepeatLoggerDetector.cs @@ -0,0 +1,29 @@ +using System; +using System.Threading; + +namespace dotnetCampus.Logging.Writers.Helpers; + +internal class RepeatLoggerDetector(Action whenRepeated) +{ + private static volatile int _lastSameItemCount; + private static LogItem? _lastItem; + + internal int RepeatOrResetLastLog(LogLevel level, string message, Exception? exception) + { + var (lastLevel, lastMessage, lastException) = _lastItem ?? new(default(LogLevel), null!, null); + if (level == lastLevel && message == lastMessage && exception == lastException) + { + // 相同日志,标记重复。 + var count = Interlocked.Increment(ref _lastSameItemCount); + whenRepeated(count); + return count; + } + + // 不同日志,设置新重复状态。 + _lastSameItemCount = 1; + _lastItem = new(level, message, exception); + return 1; + } + + private readonly record struct LogItem(LogLevel Level, string Message, Exception? Exception); +} diff --git a/src/dotnetCampus.Logger/Writers/MemoryCacheLogger.g.cs b/src/dotnetCampus.Logger/Writers/MemoryCacheLogger.g.cs new file mode 100644 index 0000000..59d5205 --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/MemoryCacheLogger.g.cs @@ -0,0 +1,53 @@ +#nullable enable + +using System; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 用于在日志模块初始化完成前先对所有记录的日志进行缓存,以便在日志模块初始化完成后再将缓存的日志写入到日志文件中。 +/// +internal class MemoryCacheLogger : ILogger +{ + private ILogger? _realLogger; + + private readonly System.Collections.Concurrent.ConcurrentQueue _queue = []; + + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + if (_realLogger is { } logger) + { + logger.Log(logLevel, eventId, state, exception, (s, e) => formatter((TState)s, e)); + } + _queue.Enqueue(new(logLevel, eventId, state!, exception, (s, e) => formatter((TState)s, e))); + } + + /// + /// 将所有缓存的日志写入到真正的日志记录器中。 + /// + /// 真正的日志记录器。 + internal void Flush(ILogger logger) + { + _realLogger = logger; + while (_queue.TryDequeue(out var context)) + { + logger.Log(context.LogLevel, context.EventId, context.State, context.Exception, context.Formatter); + } + } + + /// + /// 辅助缓存日志条目。 + /// + /// 日志级别。 + /// 事件 Id。 + /// 日志内容。 + /// 异常信息。 + /// 格式化器。 + private readonly record struct CachedLogItem( + LogLevel LogLevel, + EventId EventId, + object State, + Exception? Exception, + Func Formatter + ); +} diff --git a/src/dotnetCampus.Logger/Writers/NullLogger.g.cs b/src/dotnetCampus.Logger/Writers/NullLogger.g.cs new file mode 100644 index 0000000..c387c1c --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/NullLogger.g.cs @@ -0,0 +1,16 @@ +#nullable enable + +using global::System; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 不记录任何日志的日志记录器。 +/// +internal class NullLogger : ILogger +{ + /// + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + } +} diff --git a/src/dotnetCampus.Logger/Writers/TraceLogger.g.cs b/src/dotnetCampus.Logger/Writers/TraceLogger.g.cs new file mode 100644 index 0000000..443291a --- /dev/null +++ b/src/dotnetCampus.Logger/Writers/TraceLogger.g.cs @@ -0,0 +1,80 @@ +#nullable enable + +using global::System; +using global::System.Diagnostics; + +namespace dotnetCampus.Logging.Writers; + +/// +/// 提供仅在开启了 TRACE 条件编译符下才会记录的日志。 +/// +public class TraceLogger(ILogger realLogger) : ILogger +{ + /// + void ILogger.Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, Func formatter) + { + realLogger.Log(logLevel, eventId, state, exception, formatter); + } + + /// + /// 记录追踪级别的日志。 + /// + /// 要记录的消息。 + [Conditional("TRACE")] + public void Trace(string message) + { + realLogger.Log(LogLevel.Trace, default, message, null, (s, ex) => message); + } + + /// + /// 记录调试级别的日志。 + /// + /// 要记录的消息。 + [Conditional("TRACE")] + public void Debug(string message) + { + realLogger.Log(LogLevel.Debug, default, message, null, (s, ex) => message); + } + + /// + /// 记录信息日志。 + /// + /// 要记录的消息。 + [Conditional("TRACE")] + public void Info(string message) + { + realLogger.Log(LogLevel.Information, default, message, null, (s, ex) => message); + } + + /// + /// 记录警告日志。 + /// + /// 要记录的消息,形如 [tag] message + [Conditional("TRACE")] + public void Warn(string message) + { + realLogger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录错误日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + [Conditional("TRACE")] + public void Error(string message, Exception? exception = null) + { + realLogger.Log(LogLevel.Warning, default, message, null, (s, ex) => message); + } + + /// + /// 记录崩溃日志。 + /// + /// 要记录的消息。 + /// 如果有异常信息,可以传入此参数。 + [Conditional("TRACE")] + public void Fatal(string message, Exception? exception = null) + { + realLogger.Log(LogLevel.Critical, default, message, null, (s, ex) => message); + } +} diff --git a/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj b/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj index bc4c624..312f5e4 100644 --- a/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj +++ b/src/dotnetCampus.Logger/dotnetCampus.Logger.csproj @@ -1,36 +1,53 @@ - - netcoreapp3.1;netstandard2.0;net45 - true - - true - - - - - - - true - - - true - - - - - - true - - - - true - snupkg - - - - - - + + + + net8.0;net6.0;netstandard2.0;net45 + + + + + dotnetCampus.Logging + true + true + true + + + + + 0 + + CS1591 + + + + + true + true + true + true + snupkg + + + + + + + + + + + + + + + + diff --git a/tests/dotnetCampus.Logger.Tests/UnitTest1.cs b/tests/dotnetCampus.Logger.Tests/UnitTest1.cs new file mode 100644 index 0000000..2b72684 --- /dev/null +++ b/tests/dotnetCampus.Logger.Tests/UnitTest1.cs @@ -0,0 +1,10 @@ +namespace dotnetCampus.Logger.Tests; + +[TestClass] +public class UnitTest1 +{ + [TestMethod("Add test description here")] + public void TestMethod1() + { + } +} diff --git a/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj b/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj new file mode 100644 index 0000000..902f82c --- /dev/null +++ b/tests/dotnetCampus.Logger.Tests/dotnetCampus.Logger.Tests.csproj @@ -0,0 +1,21 @@ + + + + net8.0 + enable + false + true + + + + + + + + + + + + + +