├── .editorconfig ├── .env ├── .gitattributes ├── .github ├── ISSUE_TEMPLATE.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── cicd.yaml │ └── codeql-analysis.yml ├── .gitignore ├── .idea ├── modules.xml ├── vcs.xml └── workspace.xml ├── .ionide └── symbolCache.db ├── .vscode ├── launch.json ├── settings.json ├── tasks.json └── tasks.json.old ├── CHANGES.md ├── GitVersion.yml ├── LICENSE ├── README.md ├── assets ├── CommonAssemblyInfo.cs ├── Serilog.snk └── serilog-sink-nuget.png ├── docker-compose.readme.txt ├── docker-compose.yml ├── nuget.config ├── sample └── Serilog.Sinks.Elasticsearch.Sample │ ├── Program.cs │ ├── Serilog.Sinks.Elasticsearch.Sample.csproj │ └── appsettings.json ├── serilog-sinks-elasticsearch.sln ├── serilog-sinks-elasticsearch.sln.DotSettings ├── serilog-sinks-elasticsearch.sln.iml ├── src ├── Serilog.Formatting.Elasticsearch │ ├── DefaultJsonFormatter.cs │ ├── ElasticsearchJsonFormatter.cs │ ├── ExceptionAsObjectJsonFormatter.cs │ ├── ISerializer.cs │ └── Serilog.Formatting.Elasticsearch.csproj └── Serilog.Sinks.Elasticsearch │ ├── LoggerConfigurationElasticSearchExtensions.cs │ ├── Serilog.Sinks.Elasticsearch.csproj │ └── Sinks │ └── ElasticSearch │ ├── Durable │ ├── APayloadReader.cs │ ├── BookmarkFile.cs │ ├── ControlledLevelSwitch.cs │ ├── Elasticsearch │ │ ├── DurableElasticsearchSink.cs │ │ ├── ElasticsearchLogClient.cs │ │ ├── ElasticsearchLogShipper.cs │ │ ├── ElasticsearchPayloadReader.cs │ │ └── RollingIntervalExtensions.cs │ ├── ExponentialBackoffConnectionSchedule.cs │ ├── FileSet.cs │ ├── FileSetPosition.cs │ ├── ILogClient.cs │ ├── IPayloadReader.cs │ ├── LogShipper.cs │ └── PortableTimer.cs │ ├── ElasticSearchSink.cs │ ├── ElasticSearchTemplateProvider.cs │ ├── ElasticsearchSinkOptions.cs │ ├── ElasticsearchSinkState.cs │ ├── ElasticsearchVersionManager.cs │ └── SerializerAdapter.cs └── test ├── Serilog.Sinks.Elasticsearch.IntegrationTests ├── Bootstrap │ ├── ClientTestClusterBase.cs │ ├── ElasticsearchSinkOptionsFactory.cs │ ├── ProxyDetection.cs │ ├── SerilogSinkElasticsearchXunitRunOptions.cs │ └── XunitBootstrap.cs ├── Elasticsearch6 │ ├── Bootstrap │ │ ├── Elasticsearch6XCluster.cs │ │ └── Elasticsearch6XTestBase.cs │ ├── Elasticsearch6X.cs │ └── Elasticsearch6XUsing7X.cs ├── Elasticsearch7 │ ├── Bootstrap │ │ ├── Elasticsearch7XCluster.cs │ │ └── Elasticsearch7XTestBase.cs │ ├── Elasticsearch7X.cs │ └── Elasticsearch7XUsing6X.cs └── Serilog.Sinks.Elasticsearch.IntegrationTests.csproj └── Serilog.Sinks.Elasticsearch.Tests ├── BulkActionTests.cs ├── CustomIndexTypeNameTests.cs ├── Discrepancies ├── ElasticsearchDefaultSerializerTests.cs ├── ElasticsearchSinkUniformityTestsBase.cs ├── JsonNetSerializerTests.cs └── NoSerializerTests.cs ├── Domain └── BulkAction.cs ├── ElasticSearchLogShipperTests.cs ├── ElasticsearchJsonFormatterTests.cs ├── ElasticsearchPayloadReaderTests.cs ├── ElasticsearchSinkTests.cs ├── ExceptionAsJsonObjectFormatterTests.cs ├── FileSetTests.cs ├── IndexDeciderTests.cs ├── InlineFieldsTests.cs ├── Properties └── AssemblyInfo.cs ├── PropertyNameTests.cs ├── RealExceptionNoSerializerTests.cs ├── RealExceptionTests.cs ├── Serilog.Sinks.Elasticsearch.Tests.csproj ├── Stubs ├── ConnectionStub.cs └── ElasticsearchSinkTestsBase.cs ├── Templating ├── DiscoverVersionHandlesUnavailableServerTests.cs ├── DiscoverVersionTests.cs ├── DoNotRegisterTemplateIfItExists.cs ├── OverwriteTemplateTests.cs ├── RegisterCustomTemplateTests.cs ├── SendsTemplateHandlesUnavailableServerTests.cs ├── SendsTemplateTests.cs ├── Sendsv6TemplateTests.cs ├── Sendsv7TemplateTests.cs ├── Sendsv8TemplateTests.cs ├── SetElasticsearchSinkOptions.cs ├── SetFiveReplicasInTemplateTests.cs ├── SetTwoShardsInTemplateTests.cs ├── SetZeroReplicasInTemplateTests.cs ├── TemplateMatchTests.cs ├── template_v6.json ├── template_v7.json ├── template_v7_no-aliases.json ├── template_v8.json ├── template_v8_no-aliases_0replicas.json ├── template_v8_no-aliases_2shards.json └── template_v8_no-aliases_5replicas.json └── TestDataHelper.cs /.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 4 6 | 7 | [*.csproj] 8 | indent_size = 2 9 | 10 | # Code files 11 | [*.{cs,csx,vb,vbx}] 12 | ############################### 13 | # Naming Conventions # 14 | ############################### 15 | # Underscore for private fields 16 | dotnet_naming_rule.private_members_with_underscore.symbols = private_fields 17 | dotnet_naming_rule.private_members_with_underscore.style = prefix_underscore 18 | dotnet_naming_rule.private_members_with_underscore.severity = suggestion 19 | 20 | dotnet_naming_symbols.private_fields.applicable_kinds = field 21 | dotnet_naming_symbols.private_fields.applicable_accessibilities = private 22 | 23 | dotnet_naming_style.prefix_underscore.capitalization = camel_case 24 | dotnet_naming_style.prefix_underscore.required_prefix = _ 25 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | TAG=7.5.0 2 | ELASTIC_VERSION=7.5.0 3 | ELASTIC_PASSWORD=changeme -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | 3 | * text=auto 4 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **A few questions before you begin:** 2 | 3 | > Is this an issue related to the [Serilog core project](https://github.com/serilog/serilog) or one of the [sinks](https://github.com/serilog/serilog/wiki/Provided-Sinks) or 4 | [community projects](https://github.com/serilog/serilog/wiki/Community-Projects). 5 | This issue list is intended for Serilog Elasticsearch Sink issues. If this issue relates to another sink or to the code project, 6 | please log on the related repository. Please use [Gitter chat](https://gitter.im/serilog/serilog) and [Stack Overflow](http://stackoverflow.com/questions/tagged/serilog) for discussions and questions. 7 | 8 | 9 | **Does this issue relate to a new *feature* or an existing *bug*?** 10 | - [ ] Bug 11 | - [ ] New Feature 12 | 13 | **What version of Serilog.Sinks.Elasticsearch is affected? Please list the related NuGet package.** 14 | 15 | 16 | **What is the target framework and operating system? See [target frameworks](https://docs.microsoft.com/en-us/nuget/schema/target-frameworks) & [net standard matrix](https://docs.microsoft.com/en-us/dotnet/standard/net-standard).** 17 | 18 | - [ ] netCore 2.0 19 | - [ ] netCore 1.0 20 | - [ ] 4.7 21 | - [ ] 4.6.x 22 | - [ ] 4.5.x 23 | 24 | **Please describe the current behavior?** 25 | 26 | 27 | **Please describe the expected behavior?** 28 | 29 | 30 | **If the current behavior is a bug, please provide the steps to reproduce the issue and if possible a minimal demo of the problem** 31 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | **What issue does this PR address?** 2 | 3 | 4 | **Does this PR introduce a breaking change?** 5 | 6 | 7 | **Please check if the PR fulfills these requirements** 8 | - [ ] The commit follows our [guidelines](https://github.com/serilog/serilog/blob/dev/CONTRIBUTING.md) 9 | - [ ] Unit Tests for the changes have been added (for bug fixes / features) 10 | 11 | **Other information**: 12 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "nuget" 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "daily" 12 | 13 | - package-ecosystem: "github-actions" 14 | directory: "/" # Location of package manifests 15 | schedule: 16 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/cicd.yaml: -------------------------------------------------------------------------------- 1 | name: Continuous Integration 2 | 3 | on: 4 | push: 5 | pull_request: 6 | release: 7 | types: 8 | - published 9 | env: 10 | Configuration: Release 11 | ContinuousIntegrationBuild: true 12 | DOTNET_CLI_TELEMETRY_OPTOUT: true 13 | DOTNET_NOLOGO: true 14 | DOTNET_SKIP_FIRST_TIME_EXPERIENCE: true 15 | 16 | # GitHub Packages Feed settings 17 | GITHUB_FEED: https://nuget.pkg.github.com/serilog-contrib/ 18 | GITHUB_USER: mivano 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | 21 | jobs: 22 | package: 23 | runs-on: ubuntu-latest 24 | name: Run tests and create NuGet package 25 | outputs: 26 | coverage-reports: ${{ steps.dotnet-test.outputs.coverage-reports }} 27 | version: ${{ steps.dotnet-pack.outputs.version }} 28 | nupkg-filename: ${{ steps.dotnet-pack.outputs.nupkg-filename }} 29 | release-body: ${{ steps.tag-message.outputs.release-notes }} 30 | steps: 31 | - name: Checkout git repository 32 | uses: actions/checkout@v3 33 | with: 34 | fetch-depth: 0 35 | 36 | - name: Install .NET SDK 37 | uses: actions/setup-dotnet@v3.0.3 38 | with: 39 | dotnet-version: '7.0.x' 40 | 41 | - name: Retrieve cached NuGet packages 42 | uses: actions/cache@v3 43 | with: 44 | path: ~/.nuget/packages 45 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 46 | 47 | - name: Restore NuGet packages 48 | run: dotnet restore 49 | 50 | - name: Build solution 51 | run: dotnet build --no-restore -c Release 52 | 53 | - name: Run tests 54 | run: dotnet test --no-build -c Release --logger "html;LogFileName=TestResults-${{ runner.os }}.html" --logger "trx;LogFileName=TestResults-${{ runner.os }}.trx" --logger GitHubActions 55 | id: dotnet-test 56 | 57 | - name: Upload received files from failing tests 58 | uses: actions/upload-artifact@v3 59 | if: failure() 60 | with: 61 | name: Received-${{ runner.os }} 62 | path: "**/*.received.*" 63 | 64 | - name: Upload test results 65 | uses: actions/upload-artifact@v3 66 | if: always() 67 | with: 68 | name: TestResults-${{ runner.os }} 69 | path: test/Serilog.Sinks.Elasticsearch.Tests/TestResults/TestResults-${{ runner.os }}.html 70 | 71 | - name: Test Report 72 | uses: dorny/test-reporter@v1 73 | if: always() 74 | with: 75 | name: Test Results (${{ runner.os }}) 76 | path: '**.trx' 77 | reporter: dotnet-trx 78 | 79 | - name: Create NuGet packages 80 | run: dotnet pack --no-build -c Release --version-suffix "ci-$GITHUB_RUN_ID" --include-symbols --include-source --output . 81 | id: dotnet-pack 82 | 83 | - name: Upload NuGet package artifact 84 | uses: actions/upload-artifact@v3 85 | with: 86 | name: nuget 87 | path: '**/*.nupkg' 88 | 89 | prerelease: 90 | needs: package 91 | name: Create prerelease 92 | if: github.ref == 'refs/heads/dev' 93 | runs-on: ubuntu-latest 94 | steps: 95 | - name: Download Artifact 96 | uses: actions/download-artifact@v3 97 | with: 98 | name: nuget 99 | path: nuget 100 | - name: Push to GitHub Feed 101 | run: | 102 | dotnet nuget add source --username USERNAME --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/serilog-contrib/index.json" 103 | 104 | for f in ./nuget/*.nupkg 105 | do 106 | echo $f 107 | dotnet nuget push $f --source "github" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate 108 | done 109 | 110 | publish: 111 | runs-on: ubuntu-latest 112 | needs: package 113 | if: github.event_name == 'release' 114 | name: Publish NuGet package 115 | steps: 116 | - name: Checkout git repository 117 | uses: actions/checkout@v3 118 | with: 119 | fetch-depth: 0 120 | 121 | - name: Install .NET SDK 122 | uses: actions/setup-dotnet@v3.0.3 123 | 124 | - name: Retrieve cached NuGet packages 125 | uses: actions/cache@v3 126 | with: 127 | path: ~/.nuget/packages 128 | key: ${{ runner.os }}-nuget-${{ hashFiles('**/packages.lock.json') }} 129 | 130 | - name: Restore NuGet packages 131 | run: dotnet restore 132 | 133 | - name: Create Release NuGet package 134 | run: | 135 | arrTag=(${GITHUB_REF//\// }) 136 | VERSION="${arrTag[2]}" 137 | VERSION="${VERSION//v}" 138 | dotnet pack -v normal -c Release --include-symbols --include-source -p:Version=$VERSION -o ./nuget 139 | 140 | - name: Push to GitHub Feed 141 | run: | 142 | dotnet nuget add source --username $GITHUB_USER --password ${{ secrets.GITHUB_TOKEN }} --store-password-in-clear-text --name github "https://nuget.pkg.github.com/serilog-contrib/index.json" 143 | 144 | for f in ./nuget/*.nupkg 145 | do 146 | dotnet nuget push $f --source "github" --api-key ${{ secrets.GITHUB_TOKEN }} --skip-duplicate 147 | done 148 | 149 | - name: Publish NuGet package on nuget.org 150 | run: dotnet nuget push ./nuget/*.nupkg --api-key "${{ secrets.NUGET_API_KEY }}" --skip-duplicate 151 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # ******** NOTE ******** 12 | 13 | name: "CodeQL" 14 | 15 | on: 16 | push: 17 | branches: [ dev, master ] 18 | pull_request: 19 | # The branches below must be a subset of the branches above 20 | branches: [ dev ] 21 | schedule: 22 | - cron: '27 7 * * 6' 23 | 24 | jobs: 25 | analyze: 26 | name: Analyze 27 | runs-on: ubuntu-latest 28 | 29 | strategy: 30 | fail-fast: false 31 | matrix: 32 | language: [ 'csharp' ] 33 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python' ] 34 | # Learn more... 35 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 36 | 37 | steps: 38 | - name: Checkout repository 39 | uses: actions/checkout@v3 40 | 41 | - name: Setup dotnet 42 | uses: actions/setup-dotnet@v3.0.3 43 | with: 44 | dotnet-version: '6.0.x' 45 | 46 | # Initializes the CodeQL tools for scanning. 47 | - name: Initialize CodeQL 48 | uses: github/codeql-action/init@v2 49 | with: 50 | languages: ${{ matrix.language }} 51 | # If you wish to specify custom queries, you can do so here or in a config file. 52 | # By default, queries listed here will override any specified in a config file. 53 | # Prefix the list here with "+" to use these queries and those in the config file. 54 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 https://git.io/JvXDl 63 | 64 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 65 | # and modify them (or add more) to build your code if your project 66 | # uses a compiled language 67 | 68 | #- run: | 69 | # make bootstrap 70 | # make release 71 | 72 | - name: Perform CodeQL Analysis 73 | uses: github/codeql-action/analyze@v2 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ## Ignore Visual Studio temporary files, build results, and 2 | ## files generated by popular Visual Studio add-ons. 3 | 4 | # User-specific files 5 | *.suo 6 | *.user 7 | *.sln.docstates 8 | 9 | # Build results 10 | [Dd]ebug/ 11 | [Dd]ebugPublic/ 12 | [Rr]elease/ 13 | [Rr]eleases/ 14 | x64/ 15 | x86/ 16 | build/ 17 | bld/ 18 | [Bb]in/ 19 | [Oo]bj/ 20 | 21 | # Roslyn cache directories 22 | *.ide/ 23 | 24 | # MSTest test Results 25 | [Tt]est[Rr]esult*/ 26 | [Bb]uild[Ll]og.* 27 | 28 | #NUNIT 29 | *.VisualState.xml 30 | TestResult.xml 31 | 32 | # Build Results of an ATL Project 33 | [Dd]ebugPS/ 34 | [Rr]eleasePS/ 35 | dlldata.c 36 | 37 | buffer*.json 38 | *.bookmark 39 | failures.txt 40 | 41 | *_i.c 42 | *_p.c 43 | *_i.h 44 | *.ilk 45 | *.meta 46 | *.obj 47 | *.pch 48 | *.pdb 49 | *.pgc 50 | *.pgd 51 | *.rsp 52 | *.sbr 53 | *.tlb 54 | *.tli 55 | *.tlh 56 | *.tmp 57 | *.tmp_proj 58 | *.log 59 | *.vspscc 60 | *.vssscc 61 | .builds 62 | *.pidb 63 | *.svclog 64 | *.scc 65 | 66 | # Chutzpah Test files 67 | _Chutzpah* 68 | 69 | # Visual C++ cache files 70 | ipch/ 71 | *.aps 72 | *.ncb 73 | *.opensdf 74 | *.sdf 75 | *.cachefile 76 | 77 | # Visual Studio profiler 78 | *.psess 79 | *.vsp 80 | *.vspx 81 | 82 | # TFS 2012 Local Workspace 83 | $tf/ 84 | 85 | # Guidance Automation Toolkit 86 | *.gpState 87 | 88 | # ReSharper is a .NET coding add-in 89 | _ReSharper*/ 90 | *.[Rr]e[Ss]harper 91 | *.DotSettings.user 92 | 93 | # JustCode is a .NET coding addin-in 94 | .JustCode 95 | 96 | # TeamCity is a build add-in 97 | _TeamCity* 98 | 99 | # DotCover is a Code Coverage Tool 100 | *.dotCover 101 | 102 | # NCrunch 103 | _NCrunch_* 104 | .*crunch*.local.xml 105 | *.ncrunch* 106 | 107 | # MightyMoose 108 | *.mm.* 109 | AutoTest.Net/ 110 | 111 | # Web workbench (sass) 112 | .sass-cache/ 113 | 114 | # Installshield output folder 115 | [Ee]xpress/ 116 | 117 | # DocProject is a documentation generator add-in 118 | DocProject/buildhelp/ 119 | DocProject/Help/*.HxT 120 | DocProject/Help/*.HxC 121 | DocProject/Help/*.hhc 122 | DocProject/Help/*.hhk 123 | DocProject/Help/*.hhp 124 | DocProject/Help/Html2 125 | DocProject/Help/html 126 | 127 | # Click-Once directory 128 | publish/ 129 | 130 | # Publish Web Output 131 | *.[Pp]ublish.xml 132 | *.azurePubxml 133 | # TODO: Comment the next line if you want to checkin your web deploy settings 134 | # but database connection strings (with potential passwords) will be unencrypted 135 | *.pubxml 136 | *.publishproj 137 | 138 | # NuGet Packages 139 | *.nupkg 140 | # The packages folder can be ignored because of Package Restore 141 | **/packages/* 142 | # except build/, which is used as an MSBuild target. 143 | !**/packages/build/ 144 | # If using the old MSBuild-Integrated Package Restore, uncomment this: 145 | #!**/packages/repositories.config 146 | 147 | # Windows Azure Build Output 148 | csx/ 149 | *.build.csdef 150 | 151 | # Windows Store app package directory 152 | AppPackages/ 153 | 154 | # Others 155 | sql/ 156 | *.Cache 157 | ClientBin/ 158 | [Ss]tyle[Cc]op.* 159 | ~$* 160 | *~ 161 | *.dbmdl 162 | *.dbproj.schemaview 163 | *.pfx 164 | *.publishsettings 165 | node_modules/ 166 | 167 | # RIA/Silverlight projects 168 | Generated_Code/ 169 | 170 | # Backup & report files from converting an old project file 171 | # to a newer Visual Studio version. Backup files are not needed, 172 | # because we have git ;-) 173 | _UpgradeReport_Files/ 174 | Backup*/ 175 | UpgradeLog*.XML 176 | UpgradeLog*.htm 177 | 178 | # SQL Server files 179 | *.mdf 180 | *.ldf 181 | 182 | # Business Intelligence projects 183 | *.rdl.data 184 | *.bim.layout 185 | *.bim_*.settings 186 | 187 | # Microsoft Fakes 188 | FakesAssemblies/ 189 | 190 | project.lock.json 191 | .vs 192 | 193 | # JetBrains Rider 194 | .idea/ 195 | *.sln.iml 196 | -------------------------------------------------------------------------------- /.idea/modules.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /.idea/vcs.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /.ionide/symbolCache.db: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serilog-contrib/serilog-sinks-elasticsearch/1e9777c3034c2d8d078f60822c77b9caad5b7870/.ionide/symbolCache.db -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to find out which attributes exist for C# debugging 3 | // Use hover for the description of the existing attributes 4 | // For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": ".NET Core Launch (console)", 9 | "type": "coreclr", 10 | "request": "launch", 11 | "preLaunchTask": "build", 12 | // If you have changed target frameworks, make sure to update the program path. 13 | "program": "${workspaceRoot}/sample/Serilog.Sinks.Elasticsearch.Sample/bin/Debug/netcoreapp1.1/Serilog.Sinks.Elasticsearch.Sample.dll", 14 | "args": [], 15 | "cwd": "${workspaceRoot}/sample/Serilog.Sinks.Elasticsearch.Sample", 16 | // For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window 17 | "console": "internalConsole", 18 | "stopAtEntry": false, 19 | "internalConsoleOptions": "openOnSessionStart" 20 | }, 21 | { 22 | "name": ".NET Core Attach", 23 | "type": "coreclr", 24 | "request": "attach", 25 | "processId": "${command:pickProcess}" 26 | } 27 | ] 28 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "powershell.codeFormatting.addWhitespaceAroundPipe": true 3 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "command": "dotnet", 4 | "args": [], 5 | "tasks": [ 6 | { 7 | "label": "build", 8 | "type": "shell", 9 | "command": "dotnet", 10 | "args": [ 11 | "build", 12 | "${workspaceRoot}/sample/Serilog.Sinks.Elasticsearch.Sample/Serilog.Sinks.Elasticsearch.Sample.csproj" 13 | ], 14 | "problemMatcher": "$msCompile", 15 | "group": { 16 | "_id": "build", 17 | "isDefault": false 18 | } 19 | } 20 | ] 21 | } -------------------------------------------------------------------------------- /.vscode/tasks.json.old: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.1.0", 3 | "command": "dotnet", 4 | "isShellCommand": true, 5 | "args": [], 6 | "tasks": [ 7 | { 8 | "taskName": "build", 9 | "args": [ 10 | "${workspaceRoot}/sample/Serilog.Sinks.Elasticsearch.Sample/Serilog.Sinks.Elasticsearch.Sample.csproj" 11 | ], 12 | "isBuildCommand": true, 13 | "problemMatcher": "$msCompile" 14 | } 15 | ] 16 | } -------------------------------------------------------------------------------- /GitVersion.yml: -------------------------------------------------------------------------------- 1 | next-version: 8.3.0 2 | branches: {} 3 | ignore: 4 | sha: [] 5 | -------------------------------------------------------------------------------- /assets/CommonAssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | 3 | [assembly: AssemblyVersion("0.0.0.0")] 4 | [assembly: AssemblyFileVersion("0.0.0.0")] 5 | [assembly: AssemblyInformationalVersion("0.0.0")] 6 | -------------------------------------------------------------------------------- /assets/Serilog.snk: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serilog-contrib/serilog-sinks-elasticsearch/1e9777c3034c2d8d078f60822c77b9caad5b7870/assets/Serilog.snk -------------------------------------------------------------------------------- /assets/serilog-sink-nuget.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/serilog-contrib/serilog-sinks-elasticsearch/1e9777c3034c2d8d078f60822c77b9caad5b7870/assets/serilog-sink-nuget.png -------------------------------------------------------------------------------- /docker-compose.readme.txt: -------------------------------------------------------------------------------- 1 | Elastic (http://localhost:9200)= elastic:changeme 2 | Elastic Head: http://localhost:9100/?auth_user=elastic&auth_password=changeme 3 | Kibana: http://localhost:5601 4 | 5 | To start the components: 6 | docker-compose up 7 | 8 | add the -d option to run in the background 9 | 10 | To scale the nodes: 11 | 12 | docker-compose up --scale elasticsearch=3 13 | 14 | To shutdown: 15 | 16 | docker-compose down -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '2' 2 | 3 | services: 4 | 5 | elasticsearch: 6 | image: docker.elastic.co/elasticsearch/elasticsearch:${TAG} 7 | volumes: 8 | - esdata2:/usr/share/elasticsearch/data 9 | ports: 10 | - "9200:9200" 11 | - "9300" 12 | ulimits: 13 | memlock: 14 | soft: -1 15 | hard: -1 16 | mem_limit: 1g 17 | environment: 18 | - cluster.name=docker-cluster 19 | - discovery.type=single-node 20 | - bootstrap.memory_lock=true 21 | - "ES_JAVA_OPTS=-Xms512m -Xmx512m" 22 | - node.master=true 23 | - node.data=true 24 | - http.cors.enabled=true 25 | - http.cors.allow-origin=* 26 | - http.cors.allow-headers=Content-Type, Access-Control-Allow-Headers, Authorization, X-Requested-With 27 | - http.cors.allow-credentials=true 28 | networks: 29 | - elk 30 | 31 | kibana: 32 | image: docker.elastic.co/kibana/kibana:${TAG} 33 | environment: 34 | - "ELASTICSEARCH_URL=http://elasticsearch:9200" 35 | - server.name=kibana 36 | - server.host="0" 37 | - xpack.security.enabled=true 38 | - xpack.monitoring.enabled=true 39 | - xpack.ml.enabled=false 40 | - xpack.graph.enabled=false 41 | - xpack.reporting.enabled=false 42 | - xpack.grokdebugger.enabled=false 43 | ports: 44 | - "5601:5601" 45 | networks: 46 | - esnet 47 | depends_on: 48 | - elasticsearch 49 | 50 | head: 51 | image: mobz/elasticsearch-head:5 52 | ulimits: 53 | memlock: 54 | soft: -1 55 | hard: -1 56 | mem_limit: 1g 57 | ports: 58 | - "9100:9100" 59 | networks: 60 | - elk 61 | 62 | kibana: 63 | image: docker.elastic.co/kibana/kibana:${TAG} 64 | environment: 65 | - "ELASTICSEARCH_URL=http://elasticsearch:9200" 66 | ports: 67 | - "5601:5601" 68 | networks: 69 | - elk 70 | depends_on: 71 | - elasticsearch 72 | 73 | volumes: 74 | esdata1: 75 | driver: local 76 | esdata2: 77 | driver: local 78 | #esconfig1: 79 | # driver: local 80 | #esconfig2: 81 | # driver: local 82 | 83 | networks: 84 | elk: 85 | driver: bridge -------------------------------------------------------------------------------- /nuget.config: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /sample/Serilog.Sinks.Elasticsearch.Sample/Program.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Reflection.Metadata.Ecma335; 5 | using System.Threading; 6 | using Microsoft.Extensions.Configuration; 7 | using Serilog; 8 | using Serilog.Core; 9 | using Serilog.Debugging; 10 | using Serilog.Events; 11 | using Serilog.Formatting.Json; 12 | using Serilog.Sinks.File; 13 | using Serilog.Sinks.SystemConsole.Themes; 14 | 15 | namespace Serilog.Sinks.Elasticsearch.Sample 16 | { 17 | class Program 18 | { 19 | private static IConfiguration Configuration { get; } = new ConfigurationBuilder() 20 | .SetBasePath(Directory.GetCurrentDirectory()) 21 | .AddJsonFile("appsettings.json", true, true) 22 | .AddEnvironmentVariables() 23 | .Build(); 24 | 25 | static void Main(string[] args) 26 | { 27 | 28 | // Enable the selflog output 29 | SelfLog.Enable(Console.Error); 30 | Log.Logger = new LoggerConfiguration() 31 | .MinimumLevel.Debug() 32 | .WriteTo.Console(theme: SystemConsoleTheme.Literate) 33 | .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri(Configuration.GetConnectionString("elasticsearch"))) // for the docker-compose implementation 34 | { 35 | AutoRegisterTemplate = true, 36 | OverwriteTemplate = true, 37 | DetectElasticsearchVersion = true, 38 | AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7, 39 | NumberOfReplicas = 1, 40 | NumberOfShards = 2, 41 | //BufferBaseFilename = "./buffer", 42 | // RegisterTemplateFailure = RegisterTemplateRecovery.FailSink, 43 | FailureCallback = (e, ex) => Console.WriteLine("Unable to submit event " + e.MessageTemplate), 44 | EmitEventFailure = EmitEventFailureHandling.WriteToSelfLog | 45 | EmitEventFailureHandling.WriteToFailureSink | 46 | EmitEventFailureHandling.RaiseCallback, 47 | FailureSink = new FileSink("./fail-{Date}.txt", new JsonFormatter(), null, null) 48 | }) 49 | .CreateLogger(); 50 | 51 | Log.Information("Hello, world!"); 52 | 53 | int a = 10, b = 0; 54 | try 55 | { 56 | Log.Debug("Dividing {A} by {B}", a, b); 57 | Console.WriteLine(a / b); 58 | } 59 | catch (Exception ex) 60 | { 61 | Log.Error(ex, "Something went wrong"); 62 | } 63 | 64 | // Introduce a failure by storing a field as a different type 65 | Log.Debug("Reusing {A} by {B}", "string", true); 66 | 67 | Log.CloseAndFlush(); 68 | Console.WriteLine("Press any key to continue..."); 69 | while (!Console.KeyAvailable) 70 | { 71 | Thread.Sleep(500); 72 | } 73 | } 74 | 75 | 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /sample/Serilog.Sinks.Elasticsearch.Sample/Serilog.Sinks.Elasticsearch.Sample.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | Exe 5 | net6.0 6 | false 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | Always 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /sample/Serilog.Sinks.Elasticsearch.Sample/appsettings.json: -------------------------------------------------------------------------------- 1 | { 2 | "ConnectionStrings": { 3 | "elasticsearch": "http://elastic:changeme@localhost:9200" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /serilog-sinks-elasticsearch.sln: -------------------------------------------------------------------------------- 1 | Microsoft Visual Studio Solution File, Format Version 12.00 2 | # Visual Studio 15 3 | VisualStudioVersion = 15.0.26730.3 4 | MinimumVisualStudioVersion = 10.0.40219.1 5 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Files", "Files", "{148431F6-5BA9-4987-80CF-DF9F23F54947}" 6 | ProjectSection(SolutionItems) = preProject 7 | .editorconfig = .editorconfig 8 | .gitattributes = .gitattributes 9 | .gitignore = .gitignore 10 | CHANGES.md = CHANGES.md 11 | LICENSE = LICENSE 12 | nuget.config = nuget.config 13 | README.md = README.md 14 | GitVersion.yml = GitVersion.yml 15 | EndProjectSection 16 | EndProject 17 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Elasticsearch", "src\Serilog.Sinks.Elasticsearch\Serilog.Sinks.Elasticsearch.csproj", "{EEB0D119-687E-444E-BF14-9BDAEC9BA3EF}" 18 | EndProject 19 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Elasticsearch.Tests", "test\Serilog.Sinks.Elasticsearch.Tests\Serilog.Sinks.Elasticsearch.Tests.csproj", "{CA358673-C4B6-49D0-8EC5-D6CB50A11CC0}" 20 | EndProject 21 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Elasticsearch.Sample", "sample\Serilog.Sinks.Elasticsearch.Sample\Serilog.Sinks.Elasticsearch.Sample.csproj", "{253B37AB-D82E-4A5F-BA16-F1BE398818C8}" 22 | EndProject 23 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Formatting.Elasticsearch", "src\Serilog.Formatting.Elasticsearch\Serilog.Formatting.Elasticsearch.csproj", "{0E6D34BF-322A-4803-94D1-355F6D5024BE}" 24 | EndProject 25 | Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Elasticsearch.IntegrationTests", "test\Serilog.Sinks.Elasticsearch.IntegrationTests\Serilog.Sinks.Elasticsearch.IntegrationTests.csproj", "{23BC3821-E028-48B4-8F2C-83BB1B8B5525}" 26 | EndProject 27 | Global 28 | GlobalSection(SolutionConfigurationPlatforms) = preSolution 29 | Debug|Any CPU = Debug|Any CPU 30 | Release|Any CPU = Release|Any CPU 31 | EndGlobalSection 32 | GlobalSection(ProjectConfigurationPlatforms) = postSolution 33 | {EEB0D119-687E-444E-BF14-9BDAEC9BA3EF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 34 | {EEB0D119-687E-444E-BF14-9BDAEC9BA3EF}.Debug|Any CPU.Build.0 = Debug|Any CPU 35 | {EEB0D119-687E-444E-BF14-9BDAEC9BA3EF}.Release|Any CPU.ActiveCfg = Release|Any CPU 36 | {EEB0D119-687E-444E-BF14-9BDAEC9BA3EF}.Release|Any CPU.Build.0 = Release|Any CPU 37 | {CA358673-C4B6-49D0-8EC5-D6CB50A11CC0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 38 | {CA358673-C4B6-49D0-8EC5-D6CB50A11CC0}.Debug|Any CPU.Build.0 = Debug|Any CPU 39 | {CA358673-C4B6-49D0-8EC5-D6CB50A11CC0}.Release|Any CPU.ActiveCfg = Release|Any CPU 40 | {CA358673-C4B6-49D0-8EC5-D6CB50A11CC0}.Release|Any CPU.Build.0 = Release|Any CPU 41 | {253B37AB-D82E-4A5F-BA16-F1BE398818C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 42 | {253B37AB-D82E-4A5F-BA16-F1BE398818C8}.Debug|Any CPU.Build.0 = Debug|Any CPU 43 | {253B37AB-D82E-4A5F-BA16-F1BE398818C8}.Release|Any CPU.ActiveCfg = Release|Any CPU 44 | {253B37AB-D82E-4A5F-BA16-F1BE398818C8}.Release|Any CPU.Build.0 = Release|Any CPU 45 | {0E6D34BF-322A-4803-94D1-355F6D5024BE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 46 | {0E6D34BF-322A-4803-94D1-355F6D5024BE}.Debug|Any CPU.Build.0 = Debug|Any CPU 47 | {0E6D34BF-322A-4803-94D1-355F6D5024BE}.Release|Any CPU.ActiveCfg = Release|Any CPU 48 | {0E6D34BF-322A-4803-94D1-355F6D5024BE}.Release|Any CPU.Build.0 = Release|Any CPU 49 | {23BC3821-E028-48B4-8F2C-83BB1B8B5525}.Debug|Any CPU.ActiveCfg = Debug|Any CPU 50 | {23BC3821-E028-48B4-8F2C-83BB1B8B5525}.Debug|Any CPU.Build.0 = Debug|Any CPU 51 | {23BC3821-E028-48B4-8F2C-83BB1B8B5525}.Release|Any CPU.ActiveCfg = Release|Any CPU 52 | {23BC3821-E028-48B4-8F2C-83BB1B8B5525}.Release|Any CPU.Build.0 = Release|Any CPU 53 | EndGlobalSection 54 | GlobalSection(SolutionProperties) = preSolution 55 | HideSolutionNode = FALSE 56 | EndGlobalSection 57 | GlobalSection(ExtensibilityGlobals) = postSolution 58 | SolutionGuid = {2A076FF0-B90E-451A-B858-3144CE509516} 59 | EndGlobalSection 60 | EndGlobal 61 | -------------------------------------------------------------------------------- /serilog-sinks-elasticsearch.sln.DotSettings: -------------------------------------------------------------------------------- 1 |  2 | True 3 | True 4 | True 5 | True 6 | True 7 | True 8 | True -------------------------------------------------------------------------------- /serilog-sinks-elasticsearch.sln.iml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /src/Serilog.Formatting.Elasticsearch/ExceptionAsObjectJsonFormatter.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | 16 | using System; 17 | using System.IO; 18 | 19 | namespace Serilog.Formatting.Elasticsearch 20 | { 21 | /// 22 | /// A JSON formatter which plays nice with Kibana, 23 | /// by serializing any exception into an exception object, instead of relying on 24 | /// an array of the exceptions and the inner exception. 25 | /// 26 | /// Note that using this formatter comes at the cost that the exception tree 27 | /// with inner exceptions can grow deep. 28 | /// 29 | public class ExceptionAsObjectJsonFormatter : ElasticsearchJsonFormatter 30 | { 31 | /// 32 | /// Constructs a . 33 | /// 34 | /// If true, the properties of the event will be written to 35 | /// the output without enclosing braces. Otherwise, if false, each event will be written as a well-formed 36 | /// JSON object. 37 | /// A string that will be written after each log event is formatted. 38 | /// If null, will be used. Ignored if 39 | /// is true. 40 | /// If true, the message will be rendered and written to the output as a 41 | /// property named RenderedMessage. 42 | /// Supplies culture-specific formatting information, or null. 43 | /// Inject a serializer to force objects to be serialized over being ToString() 44 | /// When set to true values will be written at the root of the json document 45 | /// If true, the message template will be rendered and written to the output as a 46 | /// property named RenderedMessageTemplate. 47 | /// If true, splits the StackTrace by new line and writes it as a an array of strings 48 | public ExceptionAsObjectJsonFormatter(bool omitEnclosingObject = false, 49 | string closingDelimiter = null, 50 | bool renderMessage = false, 51 | IFormatProvider formatProvider = null, 52 | ISerializer serializer = null, 53 | bool inlineFields = false, 54 | bool renderMessageTemplate = true, 55 | bool formatStackTraceAsArray = false) 56 | : base(omitEnclosingObject, closingDelimiter, renderMessage, formatProvider, serializer, inlineFields, renderMessageTemplate, formatStackTraceAsArray) 57 | { 58 | } 59 | 60 | /// 61 | /// Writes out the attached exception 62 | /// 63 | protected override void WriteException(Exception exception, ref string delim, TextWriter output) 64 | { 65 | output.Write(delim); 66 | output.Write("\""); 67 | output.Write("exception"); 68 | output.Write("\":{"); 69 | WriteExceptionTree(exception, ref delim, output, 0); 70 | output.Write("}"); 71 | } 72 | 73 | private void WriteExceptionTree(Exception exception, ref string delim, TextWriter output, int depth) 74 | { 75 | delim = ""; 76 | WriteSingleException(exception, ref delim, output, depth); 77 | exception = exception.InnerException; 78 | if (exception != null) 79 | { 80 | output.Write(","); 81 | output.Write("\"innerException\":{"); 82 | WriteExceptionTree(exception, ref delim, output, depth + 1); 83 | output.Write("}"); 84 | } 85 | } 86 | } 87 | } -------------------------------------------------------------------------------- /src/Serilog.Formatting.Elasticsearch/ISerializer.cs: -------------------------------------------------------------------------------- 1 | namespace Serilog.Formatting.Elasticsearch 2 | { 3 | /// 4 | /// Defines a method to serialize custom value to string 5 | /// 6 | public interface ISerializer 7 | { 8 | /// 9 | /// Serializes object to string 10 | /// 11 | /// Object to serialization 12 | /// String representation of object 13 | string SerializeToString(object value); 14 | } 15 | } -------------------------------------------------------------------------------- /src/Serilog.Formatting.Elasticsearch/Serilog.Formatting.Elasticsearch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Michiel van Oudheusden, Martijn Laarman, Mogens Heller Grabe, Serilog Contributors 4 | Serilog.Sinks.Elasticsearch 5 | Serilog sink for Elasticsearch 6 | Copyright © Serilog Contributors 2023 7 | 8 | 9 | 10 | netstandard2.0 11 | true 12 | true 13 | Serilog.Formatting.Elasticsearch 14 | ../../assets/Serilog.snk 15 | true 16 | false 17 | Serilog.Formatting.Elasticsearch 18 | serilog;elasticsearch;logging;event;formatting 19 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch/blob/master/CHANGES.md 20 | serilog-sink-nuget.png 21 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch 22 | Apache-2.0 23 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch 24 | git 25 | Serilog.Formatting.ElasticSearch 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Serilog.Sinks.Elasticsearch.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | Michiel van Oudheusden, Martijn Laarman, Mogens Heller Grabe, Serilog Contributors 4 | Serilog.Sinks.Elasticsearch 5 | Serilog sink for Elasticsearch 6 | Copyright © Serilog Contributors 2023 7 | netstandard2.0;net6.0 8 | true 9 | latest 10 | true 11 | Serilog.Sinks.Elasticsearch 12 | ../../assets/Serilog.snk 13 | true 14 | false 15 | Serilog.Sinks.Elasticsearch 16 | serilog;elasticsearch;logging;event;collector 17 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch/blob/master/CHANGES.md 18 | serilog-sink-nuget.png 19 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch 20 | https://github.com/serilog-contrib/serilog-sinks-elasticsearch 21 | Apache-2.0 22 | git 23 | Serilog 24 | README.md 25 | 26 | 27 | 28 | 1591;1701;1702 29 | $(DefineConstants);DURABLE;THREADING_TIMER 30 | 31 | 32 | 33 | 1591;1701;1702 34 | $(DefineConstants);DURABLE;THREADING_TIMER;ASYNC_DISPOSABLE 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/APayloadReader.cs: -------------------------------------------------------------------------------- 1 | // Serilog.Sinks.Seq Copyright 2017 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if DURABLE 16 | 17 | using System; 18 | using System.IO; 19 | using System.Text; 20 | using Serilog.Debugging; 21 | 22 | namespace Serilog.Sinks.Elasticsearch.Durable 23 | { 24 | /// 25 | /// Abstract payload reader 26 | /// Generic version of https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/PayloadReader.cs 27 | /// 28 | /// 29 | public abstract class APayloadReader : IPayloadReader 30 | { 31 | /// 32 | /// 33 | /// 34 | /// 35 | public abstract TPayload GetNoPayload(); 36 | 37 | /// 38 | /// 39 | /// 40 | /// 41 | /// 42 | /// 43 | /// 44 | /// 45 | /// 46 | public TPayload ReadPayload(int batchPostingLimit, long? eventBodyLimitBytes, ref FileSetPosition position, ref int count,string fileName) 47 | { 48 | InitPayLoad(fileName); 49 | 50 | using (var current = System.IO.File.Open(position.File, FileMode.Open, FileAccess.Read, FileShare.ReadWrite)) 51 | { 52 | var nextLineStart = position.NextLineStart; 53 | while (count < batchPostingLimit && TryReadLine(current, ref nextLineStart, out var nextLine)) 54 | { 55 | position = new FileSetPosition(nextLineStart, position.File); 56 | 57 | // Count is the indicator that work was done, so advances even in the (rare) case an 58 | // oversized event is dropped. 59 | ++count; 60 | 61 | if (eventBodyLimitBytes.HasValue && Encoding.UTF8.GetByteCount(nextLine) > eventBodyLimitBytes.Value) 62 | { 63 | SelfLog.WriteLine( 64 | "Event JSON representation exceeds the byte size limit of {0} and will be dropped; data: {1}", 65 | eventBodyLimitBytes, nextLine); 66 | } 67 | else 68 | { 69 | AddToPayLoad(nextLine); 70 | } 71 | } 72 | } 73 | return FinishPayLoad(); 74 | } 75 | /// 76 | /// 77 | /// 78 | /// 79 | protected abstract void InitPayLoad(string fileName); 80 | /// 81 | /// 82 | /// 83 | /// 84 | protected abstract TPayload FinishPayLoad(); 85 | /// 86 | /// 87 | /// 88 | /// 89 | protected abstract void AddToPayLoad(string nextLine); 90 | 91 | // It would be ideal to chomp whitespace here, but not required. 92 | private static bool TryReadLine(Stream current, ref long nextStart, out string nextLine) 93 | { 94 | var includesBom = nextStart == 0; 95 | 96 | if (current.Length <= nextStart) 97 | { 98 | nextLine = null; 99 | return false; 100 | } 101 | 102 | current.Position = nextStart; 103 | 104 | // Important not to dispose this StreamReader as the stream must remain open. 105 | var reader = new StreamReader(current, Encoding.UTF8, false, 128); 106 | nextLine = reader.ReadLine(); 107 | 108 | if (nextLine == null) 109 | return false; 110 | 111 | nextStart += Encoding.UTF8.GetByteCount(nextLine) + Encoding.UTF8.GetByteCount(Environment.NewLine); 112 | if (includesBom) 113 | nextStart += 3; 114 | 115 | return true; 116 | } 117 | } 118 | } 119 | 120 | #endif 121 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/BookmarkFile.cs: -------------------------------------------------------------------------------- 1 | // Serilog.Sinks.Seq Copyright 2017 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if DURABLE 16 | 17 | using System; 18 | using System.IO; 19 | using System.Text; 20 | 21 | namespace Serilog.Sinks.Elasticsearch.Durable 22 | { 23 | /// 24 | /// https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/BookmarkFile.cs 25 | /// 26 | sealed class BookmarkFile : IDisposable 27 | { 28 | readonly FileStream _bookmark; 29 | 30 | public BookmarkFile(string bookmarkFilename) 31 | { 32 | _bookmark = System.IO.File.Open(bookmarkFilename, FileMode.OpenOrCreate, FileAccess.ReadWrite, FileShare.Read); 33 | } 34 | 35 | public FileSetPosition TryReadBookmark() 36 | { 37 | if (_bookmark.Length != 0) 38 | { 39 | _bookmark.Position = 0; 40 | 41 | // Important not to dispose this StreamReader as the stream must remain open. 42 | var reader = new StreamReader(_bookmark, Encoding.UTF8, false, 128); 43 | var current = reader.ReadLine(); 44 | 45 | if (current != null) 46 | { 47 | var parts = current.Split(new[] { ":::" }, StringSplitOptions.RemoveEmptyEntries); 48 | if (parts.Length == 2) 49 | { 50 | return new FileSetPosition(long.Parse(parts[0]), parts[1]); 51 | } 52 | } 53 | } 54 | 55 | return FileSetPosition.None; 56 | } 57 | 58 | public void WriteBookmark(FileSetPosition bookmark) 59 | { 60 | if (bookmark.File == null) 61 | return; 62 | 63 | // Don't need to truncate, since we only ever read a single line and 64 | // writes are always newline-terminated 65 | _bookmark.Position = 0; 66 | 67 | // Cannot dispose, as `leaveOpen` is not available on all target platforms 68 | var writer = new StreamWriter(_bookmark); 69 | writer.WriteLine("{0}:::{1}", bookmark.NextLineStart, bookmark.File); 70 | writer.Flush(); 71 | } 72 | 73 | public void Dispose() 74 | { 75 | _bookmark.Dispose(); 76 | } 77 | } 78 | } 79 | 80 | #endif 81 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/ControlledLevelSwitch.cs: -------------------------------------------------------------------------------- 1 | // Serilog.Sinks.Seq Copyright 2016 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Serilog.Core; 16 | using Serilog.Events; 17 | 18 | namespace Serilog.Sinks.Elasticsearch.Durable 19 | { 20 | /// 21 | /// Instances of this type are single-threaded, generally only updated on a background 22 | /// timer thread. An exception is , which may be called 23 | /// concurrently but performs no synchronization. 24 | /// https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/ControlledLevelSwitch.cs 25 | /// 26 | class ControlledLevelSwitch 27 | { 28 | // If non-null, then background level checks will be performed; set either through the constructor 29 | // or in response to a level specification from the server. Never set to null after being made non-null. 30 | LoggingLevelSwitch _controlledSwitch; 31 | LogEventLevel? _originalLevel; 32 | 33 | public ControlledLevelSwitch(LoggingLevelSwitch controlledSwitch = null) 34 | { 35 | _controlledSwitch = controlledSwitch; 36 | } 37 | 38 | public bool IsActive => _controlledSwitch != null; 39 | 40 | public bool IsIncluded(LogEvent evt) 41 | { 42 | // Concurrent, but not synchronized. 43 | var controlledSwitch = _controlledSwitch; 44 | return controlledSwitch == null || 45 | (int)controlledSwitch.MinimumLevel <= (int)evt.Level; 46 | } 47 | 48 | public void Update(LogEventLevel? minimumAcceptedLevel) 49 | { 50 | if (minimumAcceptedLevel == null) 51 | { 52 | if (_controlledSwitch != null && _originalLevel.HasValue) 53 | _controlledSwitch.MinimumLevel = _originalLevel.Value; 54 | 55 | return; 56 | } 57 | 58 | if (_controlledSwitch == null) 59 | { 60 | // The server is controlling the logging level, but not the overall logger. Hence, if the server 61 | // stops controlling the level, the switch should become transparent. 62 | _originalLevel = LevelAlias.Minimum; 63 | _controlledSwitch = new LoggingLevelSwitch(minimumAcceptedLevel.Value); 64 | return; 65 | } 66 | 67 | if (!_originalLevel.HasValue) 68 | _originalLevel = _controlledSwitch.MinimumLevel; 69 | 70 | _controlledSwitch.MinimumLevel = minimumAcceptedLevel.Value; 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/DurableElasticsearchSink.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2014 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | using System.Collections.Generic; 17 | using System.Text; 18 | using Serilog.Core; 19 | using Serilog.Events; 20 | 21 | 22 | namespace Serilog.Sinks.Elasticsearch.Durable 23 | { 24 | class DurableElasticsearchSink : ILogEventSink, IDisposable 25 | { 26 | // we rely on the date in the filename later! 27 | const string FileNameSuffix = "-.json"; 28 | 29 | readonly Logger _sink; 30 | readonly LogShipper> _shipper; 31 | readonly ElasticsearchSinkState _state; 32 | 33 | public DurableElasticsearchSink(ElasticsearchSinkOptions options) 34 | { 35 | _state = ElasticsearchSinkState.Create(options); 36 | 37 | if (string.IsNullOrWhiteSpace(options.BufferBaseFilename)) 38 | { 39 | throw new ArgumentException("Cannot create the durable ElasticSearch sink without a buffer base file name!"); 40 | } 41 | 42 | _sink = new LoggerConfiguration() 43 | .MinimumLevel.Verbose() 44 | .WriteTo.File(_state.DurableFormatter, 45 | options.BufferBaseFilename + FileNameSuffix, 46 | rollingInterval: options.BufferFileRollingInterval, 47 | fileSizeLimitBytes: options.BufferFileSizeLimitBytes, 48 | rollOnFileSizeLimit: true, 49 | retainedFileCountLimit: options.BufferFileCountLimit, 50 | levelSwitch: _state.Options.LevelSwitch, 51 | encoding: Encoding.UTF8) 52 | .CreateLogger(); 53 | 54 | var elasticSearchLogClient = new ElasticsearchLogClient( 55 | elasticLowLevelClient: _state.Client, 56 | cleanPayload: _state.Options.BufferCleanPayload, 57 | elasticOpType: _state.Options.BatchAction); 58 | 59 | var payloadReader = new ElasticsearchPayloadReader( 60 | pipelineName: _state.Options.PipelineName, 61 | typeName:_state.Options.TypeName, 62 | serialize:_state.Serialize, 63 | getIndexForEvent: _state.GetBufferedIndexForEvent, 64 | elasticOpType: _state.Options.BatchAction, 65 | rollingInterval: options.BufferFileRollingInterval); 66 | 67 | _shipper = new ElasticsearchLogShipper( 68 | bufferBaseFilename: _state.Options.BufferBaseFilename, 69 | batchPostingLimit: _state.Options.BatchPostingLimit, 70 | period: _state.Options.BufferLogShippingInterval ?? TimeSpan.FromSeconds(5), 71 | eventBodyLimitBytes: _state.Options.SingleEventSizePostingLimit, 72 | levelControlSwitch: _state.Options.LevelSwitch, 73 | logClient: elasticSearchLogClient, 74 | payloadReader: payloadReader, 75 | retainedInvalidPayloadsLimitBytes: _state.Options.BufferRetainedInvalidPayloadsLimitBytes, 76 | bufferSizeLimitBytes: _state.Options.BufferFileSizeLimitBytes, 77 | registerTemplateIfNeeded: _state.RegisterTemplateIfNeeded, 78 | rollingInterval: options.BufferFileRollingInterval); 79 | 80 | } 81 | 82 | public void Emit(LogEvent logEvent) 83 | { 84 | // This is a lagging indicator, but the network bandwidth usage benefits 85 | // are worth the ambiguity. 86 | if (_shipper.IsIncluded(logEvent)) 87 | { 88 | _sink.Write(logEvent); 89 | } 90 | } 91 | 92 | public void Dispose() 93 | { 94 | _sink.Dispose(); 95 | _shipper.Dispose(); 96 | } 97 | } 98 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/ElasticsearchLogClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Threading.Tasks; 5 | using Elasticsearch.Net; 6 | using Serilog.Debugging; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Durable 9 | { 10 | /// 11 | /// 12 | /// 13 | public class ElasticsearchLogClient : ILogClient> 14 | { 15 | private readonly IElasticLowLevelClient _elasticLowLevelClient; 16 | private readonly Func _cleanPayload; 17 | private readonly ElasticOpType _elasticOpType; 18 | 19 | /// 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | public ElasticsearchLogClient(IElasticLowLevelClient elasticLowLevelClient, 26 | Func cleanPayload, 27 | ElasticOpType elasticOpType) 28 | { 29 | _elasticLowLevelClient = elasticLowLevelClient; 30 | _cleanPayload = cleanPayload; 31 | _elasticOpType = elasticOpType; 32 | } 33 | 34 | public async Task SendPayloadAsync(List payload) 35 | { 36 | return await SendPayloadAsync(payload, true); 37 | } 38 | 39 | public async Task SendPayloadAsync(List payload, bool first) 40 | { 41 | try 42 | { 43 | if (payload == null || !payload.Any()) return new SentPayloadResult(null, true); 44 | var response = await _elasticLowLevelClient.BulkAsync(PostData.MultiJson(payload)); 45 | 46 | if (response.Success) 47 | { 48 | var cleanPayload = new List(); 49 | var invalidPayload = GetInvalidPayloadAsync(response, payload, out cleanPayload); 50 | if ((cleanPayload?.Any() ?? false) && first) 51 | { 52 | await SendPayloadAsync(cleanPayload, false); 53 | } 54 | 55 | return new SentPayloadResult(response, true, invalidPayload); 56 | } 57 | else 58 | { 59 | SelfLog.WriteLine("Received failed ElasticSearch shipping result {0}: {1}", response.HttpStatusCode, 60 | response.OriginalException); 61 | return new SentPayloadResult(response, false, 62 | new InvalidResult() 63 | { 64 | StatusCode = response.HttpStatusCode ?? 500, 65 | Content = response.OriginalException.ToString() 66 | }); 67 | } 68 | } 69 | catch (Exception ex) 70 | { 71 | SelfLog.WriteLine("Exception while emitting periodic batch from {0}: {1}", this, ex); 72 | return new SentPayloadResult(null, false, null, ex); 73 | } 74 | 75 | 76 | } 77 | 78 | private InvalidResult GetInvalidPayloadAsync(DynamicResponse baseResult, List payload, out List cleanPayload) 79 | { 80 | int i = 0; 81 | cleanPayload = new List(); 82 | var items = baseResult.Body["items"]; 83 | if (items == null) return null; 84 | List badPayload = new List(); 85 | 86 | bool hasErrors = false; 87 | foreach (dynamic item in items) 88 | { 89 | var itemIndex = item?[BatchedElasticsearchSink.BulkAction(_elasticOpType)]; 90 | long? status = itemIndex?["status"]; 91 | i++; 92 | if (!status.HasValue || status < 300) 93 | { 94 | continue; 95 | } 96 | 97 | hasErrors = true; 98 | var id = itemIndex?["_id"]; 99 | var error = itemIndex?["error"]; 100 | var errorString = $"type: {error?["type"] ?? "Unknown"}, reason: {error?["reason"] ?? "Unknown"}"; 101 | 102 | if (int.TryParse(id.Split('_')[0], out int index)) 103 | { 104 | SelfLog.WriteLine("Received failed ElasticSearch shipping result {0}: {1}. Failed payload : {2}.", status, errorString, payload.ElementAt(index * 2 + 1)); 105 | badPayload.Add(payload.ElementAt(index * 2)); 106 | badPayload.Add(payload.ElementAt(index * 2 + 1)); 107 | if (_cleanPayload != null) 108 | { 109 | cleanPayload.Add(payload.ElementAt(index * 2)); 110 | cleanPayload.Add(_cleanPayload(payload.ElementAt(index * 2 + 1), status, errorString)); 111 | } 112 | } 113 | else 114 | { 115 | SelfLog.WriteLine($"Received failed ElasticSearch shipping result {status}: {errorString}."); 116 | } 117 | } 118 | 119 | if (!hasErrors) 120 | return null; 121 | return new InvalidResult() 122 | { 123 | StatusCode = baseResult.HttpStatusCode ?? 500, 124 | Content = baseResult.ToString(), 125 | BadPayLoad = String.Join(Environment.NewLine, badPayload) 126 | }; 127 | } 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/ElasticsearchLogShipper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Text; 5 | using System.Threading.Tasks; 6 | using Serilog.Core; 7 | using Serilog.Debugging; 8 | using Serilog.Sinks.Elasticsearch.Durable; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.Durable 11 | { 12 | /// 13 | /// 14 | /// 15 | public class ElasticsearchLogShipper : LogShipper> 16 | { 17 | private readonly Action _registerTemplateIfNeeded; 18 | bool _didRegisterTemplateIfNeeded = false; 19 | 20 | /// 21 | /// 22 | /// 23 | /// 24 | /// 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | public ElasticsearchLogShipper(string bufferBaseFilename, int batchPostingLimit, TimeSpan period, 35 | long? eventBodyLimitBytes, LoggingLevelSwitch levelControlSwitch, ILogClient> logClient, 36 | IPayloadReader> payloadReader, long? retainedInvalidPayloadsLimitBytes, 37 | long? bufferSizeLimitBytes, Action registerTemplateIfNeeded, RollingInterval rollingInterval) 38 | : base(bufferBaseFilename, batchPostingLimit, period, eventBodyLimitBytes, levelControlSwitch, logClient, 39 | payloadReader, retainedInvalidPayloadsLimitBytes, bufferSizeLimitBytes, rollingInterval) 40 | { 41 | _registerTemplateIfNeeded = registerTemplateIfNeeded; 42 | } 43 | 44 | /// 45 | /// 46 | /// 47 | /// 48 | protected override async Task OnTick() 49 | { 50 | bool success = true; 51 | try 52 | { 53 | if (!_didRegisterTemplateIfNeeded) 54 | { 55 | if (_registerTemplateIfNeeded != null) 56 | { 57 | _registerTemplateIfNeeded(); 58 | _didRegisterTemplateIfNeeded = true; 59 | } 60 | } 61 | } 62 | catch (Exception ex) 63 | { 64 | SelfLog.WriteLine("Exception while emitting periodic batch from {0}: {1}", this, ex); 65 | _connectionSchedule.MarkFailure(); 66 | success = false; 67 | } 68 | if (success) 69 | await base.OnTick(); 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/ElasticsearchPayloadReader.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Globalization; 4 | using System.IO; 5 | using System.Linq; 6 | using Elasticsearch.Net; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Durable 9 | { 10 | /// 11 | /// 12 | /// 13 | public class ElasticsearchPayloadReader: APayloadReader> 14 | { 15 | private readonly string _pipelineName; 16 | private readonly string _typeName; 17 | private readonly Func _serialize; 18 | private readonly Func _getIndexForEvent; 19 | private readonly ElasticOpType _elasticOpType; 20 | private readonly RollingInterval _rollingInterval; 21 | private List _payload; 22 | private int _count; 23 | private DateTime _date; 24 | 25 | /// 26 | /// 27 | /// 28 | /// 29 | /// 30 | /// 31 | /// 32 | /// 33 | /// 34 | public ElasticsearchPayloadReader(string pipelineName, string typeName, Func serialize, 35 | Func getIndexForEvent, ElasticOpType elasticOpType, RollingInterval rollingInterval) 36 | { 37 | if ((int)rollingInterval < (int)RollingInterval.Day) 38 | { 39 | throw new ArgumentException("Rolling intervals less frequent than RollingInterval.Day are not supported"); 40 | } 41 | 42 | _pipelineName = pipelineName; 43 | _typeName = typeName; 44 | _serialize = serialize; 45 | _getIndexForEvent = getIndexForEvent; 46 | _elasticOpType = elasticOpType; 47 | _rollingInterval = rollingInterval; 48 | } 49 | 50 | /// 51 | /// 52 | /// 53 | /// 54 | public override List GetNoPayload() 55 | { 56 | return new List(); 57 | } 58 | 59 | /// 60 | /// 61 | /// 62 | /// 63 | protected override void InitPayLoad(string filename) 64 | { 65 | _payload = new List(); 66 | _count = 0; 67 | var lastToken = filename.Split('-').Last(); 68 | 69 | // lastToken should be something like 20150218.json or 20150218_3.json now 70 | if (!lastToken.ToLowerInvariant().EndsWith(".json")) 71 | { 72 | throw new FormatException(string.Format("The file name '{0}' does not seem to follow the right file pattern - it must be named [whatever]-{{Date}}[_n].json", Path.GetFileName(filename))); 73 | } 74 | 75 | var dateFormat = _rollingInterval.GetFormat(); 76 | var dateString = lastToken.Substring(0, dateFormat.Length); 77 | _date = DateTime.ParseExact(dateString, dateFormat, CultureInfo.InvariantCulture); 78 | } 79 | /// 80 | /// 81 | /// 82 | /// 83 | protected override List FinishPayLoad() 84 | { 85 | return _payload; 86 | } 87 | 88 | /// 89 | /// 90 | /// 91 | /// 92 | protected override void AddToPayLoad(string nextLine) 93 | { 94 | var indexName = _getIndexForEvent(nextLine, _date); 95 | var action = BatchedElasticsearchSink.CreateElasticAction( 96 | opType: _elasticOpType, 97 | indexName: indexName, pipelineName: _pipelineName, 98 | id: _count + "_" + Guid.NewGuid(), 99 | mappingType: _typeName); 100 | var actionJson = LowLevelRequestResponseSerializer.Instance.SerializeToString(action); 101 | 102 | _payload.Add(actionJson); 103 | _payload.Add(nextLine); 104 | _count++; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/Elasticsearch/RollingIntervalExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.CompilerServices; 3 | 4 | [assembly: 5 | InternalsVisibleTo( 6 | "Serilog.Sinks.Elasticsearch.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100fb8d13fd344a1c6fe0fe83ef33c1080bf30690765bc6eb0df26ebfdf8f21670c64265b30db09f73a0dea5b3db4c9d18dbf6d5a25af5ce9016f281014d79dc3b4201ac646c451830fc7e61a2dfd633d34c39f87b81894191652df5ac63cc40c77f3542f702bda692e6e8a9158353df189007a49da0f3cfd55eb250066b19485ec")] 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Durable 9 | { 10 | internal static class RollingIntervalExtensions 11 | { 12 | // From https://github.com/serilog/serilog-sinks-file/blob/dev/src/Serilog.Sinks.File/Sinks/File/RollingIntervalExtensions.cs#L19 13 | public static string GetFormat(this RollingInterval interval) 14 | { 15 | switch (interval) 16 | { 17 | case RollingInterval.Infinite: 18 | return ""; 19 | case RollingInterval.Year: 20 | return "yyyy"; 21 | case RollingInterval.Month: 22 | return "yyyyMM"; 23 | case RollingInterval.Day: 24 | return "yyyyMMdd"; 25 | case RollingInterval.Hour: 26 | return "yyyyMMddHH"; 27 | case RollingInterval.Minute: 28 | return "yyyyMMddHHmm"; 29 | default: 30 | throw new ArgumentException("Invalid rolling interval"); 31 | } 32 | } 33 | 34 | public static string GetMatchingDateRegularExpressionPart(this RollingInterval interval) 35 | { 36 | switch (interval) 37 | { 38 | case RollingInterval.Infinite: 39 | return ""; 40 | case RollingInterval.Year: 41 | return "\\d{4}"; 42 | case RollingInterval.Month: 43 | return "\\d{6}"; 44 | case RollingInterval.Day: 45 | return "\\d{8}"; 46 | case RollingInterval.Hour: 47 | return "\\d{10}"; 48 | case RollingInterval.Minute: 49 | return "\\d{12}"; 50 | default: 51 | throw new ArgumentException("Invalid rolling interval"); 52 | } 53 | } 54 | } 55 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/ExponentialBackoffConnectionSchedule.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2016 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using System; 16 | 17 | namespace Serilog.Sinks.Elasticsearch.Durable 18 | { 19 | /// 20 | /// Based on the BatchedConnectionStatus class from . 21 | /// https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/ExponentialBackoffConnectionSchedule.cs 22 | /// 23 | public class ExponentialBackoffConnectionSchedule 24 | { 25 | static readonly TimeSpan MinimumBackoffPeriod = TimeSpan.FromSeconds(5); 26 | static readonly TimeSpan MaximumBackoffInterval = TimeSpan.FromMinutes(10); 27 | 28 | readonly TimeSpan _period; 29 | 30 | int _failuresSinceSuccessfulConnection; 31 | 32 | public ExponentialBackoffConnectionSchedule(TimeSpan period) 33 | { 34 | if (period < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(period), "The connection retry period must be a positive timespan"); 35 | 36 | _period = period; 37 | } 38 | 39 | public void MarkSuccess() 40 | { 41 | _failuresSinceSuccessfulConnection = 0; 42 | } 43 | 44 | public void MarkFailure() 45 | { 46 | ++_failuresSinceSuccessfulConnection; 47 | } 48 | 49 | public TimeSpan NextInterval 50 | { 51 | get 52 | { 53 | // Available, and first failure, just try the batch interval 54 | if (_failuresSinceSuccessfulConnection <= 1) return _period; 55 | 56 | // Second failure, start ramping up the interval - first 2x, then 4x, ... 57 | var backoffFactor = Math.Pow(2, (_failuresSinceSuccessfulConnection - 1)); 58 | 59 | // If the period is ridiculously short, give it a boost so we get some 60 | // visible backoff. 61 | var backoffPeriod = Math.Max(_period.Ticks, MinimumBackoffPeriod.Ticks); 62 | 63 | // The "ideal" interval 64 | var backedOff = (long)(backoffPeriod * backoffFactor); 65 | 66 | // Capped to the maximum interval 67 | var cappedBackoff = Math.Min(MaximumBackoffInterval.Ticks, backedOff); 68 | 69 | // Unless that's shorter than the base interval, in which case we'll just apply the period 70 | var actual = Math.Max(_period.Ticks, cappedBackoff); 71 | 72 | return TimeSpan.FromTicks(actual); 73 | } 74 | } 75 | } 76 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/FileSetPosition.cs: -------------------------------------------------------------------------------- 1 | // Serilog.Sinks.Seq Copyright 2017 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | #if DURABLE 16 | 17 | namespace Serilog.Sinks.Elasticsearch.Durable 18 | { 19 | /// 20 | /// https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/Durable/FileSetPosition.cs 21 | /// 22 | public struct FileSetPosition 23 | { 24 | public string File { get; } 25 | 26 | public long NextLineStart { get; } 27 | 28 | public FileSetPosition(long nextLineStart, string file) 29 | { 30 | NextLineStart = nextLineStart; 31 | File = file; 32 | } 33 | 34 | public static readonly FileSetPosition None = default(FileSetPosition); 35 | } 36 | } 37 | 38 | #endif 39 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/ILogClient.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Net; 5 | using System.Text; 6 | using System.Threading.Tasks; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Durable 9 | { 10 | /// 11 | /// A wrapper client which talk to the log server 12 | /// 13 | /// 14 | public interface ILogClient 15 | { 16 | Task SendPayloadAsync(TPayload payload); 17 | } 18 | public class SentPayloadResult 19 | { 20 | 21 | public dynamic BaseResult { get; } 22 | public bool Success { get; } 23 | public InvalidResult InvalidResult { get; } 24 | 25 | 26 | public Exception Exception { get; } 27 | 28 | public SentPayloadResult(dynamic baseResult, bool success, InvalidResult invalidResult =null, Exception exception=null) 29 | { 30 | BaseResult = baseResult; 31 | Success = success; 32 | InvalidResult = invalidResult; 33 | Exception = exception; 34 | } 35 | 36 | 37 | } 38 | 39 | public class InvalidResult 40 | { 41 | public int StatusCode { get; set; } 42 | public string Content { get; set; } 43 | public string BadPayLoad { get; set; } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/IPayloadReader.cs: -------------------------------------------------------------------------------- 1 | namespace Serilog.Sinks.Elasticsearch.Durable 2 | { 3 | /// 4 | /// Reads logs from logfiles and formats it for logserver 5 | /// 6 | /// 7 | public interface IPayloadReader 8 | { 9 | TPayload ReadPayload(int batchPostingLimit, long? eventBodyLimitBytes, ref FileSetPosition position, ref int count,string fileName); 10 | TPayload GetNoPayload(); 11 | } 12 | } -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/Durable/PortableTimer.cs: -------------------------------------------------------------------------------- 1 | // Copyright 2013-2016 Serilog Contributors 2 | // 3 | // Licensed under the Apache License, Version 2.0 (the "License"); 4 | // you may not use this file except in compliance with the License. 5 | // You may obtain a copy of the License at 6 | // 7 | // http://www.apache.org/licenses/LICENSE-2.0 8 | // 9 | // Unless required by applicable law or agreed to in writing, software 10 | // distributed under the License is distributed on an "AS IS" BASIS, 11 | // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | // See the License for the specific language governing permissions and 13 | // limitations under the License. 14 | 15 | using Serilog.Debugging; 16 | using System; 17 | using System.Threading; 18 | using System.Threading.Tasks; 19 | 20 | namespace Serilog.Sinks.Elasticsearch.Durable 21 | { 22 | /// 23 | /// https://github.com/serilog/serilog-sinks-seq/blob/v4.0.0/src/Serilog.Sinks.Seq/Sinks/Seq/PortableTimer.cs 24 | /// 25 | class PortableTimer : IDisposable 26 | { 27 | readonly object _stateLock = new object(); 28 | 29 | readonly Func _onTick; 30 | readonly CancellationTokenSource _cancel = new CancellationTokenSource(); 31 | 32 | #if THREADING_TIMER 33 | readonly Timer _timer; 34 | #endif 35 | 36 | bool _running; 37 | bool _disposed; 38 | 39 | public PortableTimer(Func onTick) 40 | { 41 | if (onTick == null) throw new ArgumentNullException(nameof(onTick)); 42 | 43 | _onTick = onTick; 44 | 45 | #if THREADING_TIMER 46 | _timer = new Timer(_ => OnTick(), null, Timeout.Infinite, Timeout.Infinite); 47 | #endif 48 | } 49 | 50 | public void Start(TimeSpan interval) 51 | { 52 | if (interval < TimeSpan.Zero) throw new ArgumentOutOfRangeException(nameof(interval)); 53 | 54 | lock (_stateLock) 55 | { 56 | if (_disposed) 57 | throw new ObjectDisposedException(nameof(PortableTimer)); 58 | 59 | #if THREADING_TIMER 60 | _timer.Change(interval, Timeout.InfiniteTimeSpan); 61 | #else 62 | Task.Delay(interval, _cancel.Token) 63 | .ContinueWith( 64 | _ => OnTick(), 65 | CancellationToken.None, 66 | TaskContinuationOptions.DenyChildAttach, 67 | TaskScheduler.Default); 68 | #endif 69 | } 70 | } 71 | 72 | async void OnTick() 73 | { 74 | try 75 | { 76 | lock (_stateLock) 77 | { 78 | if (_disposed) 79 | { 80 | return; 81 | } 82 | 83 | // There's a little bit of raciness here, but it's needed to support the 84 | // current API, which allows the tick handler to reenter and set the next interval. 85 | 86 | if (_running) 87 | { 88 | Monitor.Wait(_stateLock); 89 | 90 | if (_disposed) 91 | { 92 | return; 93 | } 94 | } 95 | 96 | _running = true; 97 | } 98 | 99 | if (!_cancel.Token.IsCancellationRequested) 100 | { 101 | await _onTick(_cancel.Token); 102 | } 103 | } 104 | catch (OperationCanceledException tcx) 105 | { 106 | SelfLog.WriteLine("The timer was canceled during invocation: {0}", tcx); 107 | } 108 | finally 109 | { 110 | lock (_stateLock) 111 | { 112 | _running = false; 113 | Monitor.PulseAll(_stateLock); 114 | } 115 | } 116 | } 117 | 118 | public void Dispose() 119 | { 120 | _cancel.Cancel(); 121 | 122 | lock (_stateLock) 123 | { 124 | if (_disposed) 125 | { 126 | return; 127 | } 128 | 129 | while (_running) 130 | { 131 | Monitor.Wait(_stateLock); 132 | } 133 | 134 | #if THREADING_TIMER 135 | _timer.Dispose(); 136 | #endif 137 | 138 | _disposed = true; 139 | } 140 | } 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/ElasticsearchVersionManager.cs: -------------------------------------------------------------------------------- 1 | #nullable enable 2 | using Elasticsearch.Net; 3 | using Elasticsearch.Net.Specification.CatApi; 4 | using Serilog.Debugging; 5 | using System; 6 | using System.Linq; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Sinks.ElasticSearch 9 | { 10 | /// 11 | /// Encapsulates detection of Elasticsearch version 12 | /// and fallback in case of detection failiure. 13 | /// 14 | internal class ElasticsearchVersionManager 15 | { 16 | private readonly bool _detectElasticsearchVersion; 17 | private readonly IElasticLowLevelClient _client; 18 | 19 | /// 20 | /// We are defaulting to version 7.17.0 21 | /// as currently supported versions are 7 and 8, 22 | /// while version 8 retains wire backward compatibility with 7.17.0 23 | /// and index backward compatibility with 7.0.0 24 | /// 25 | public readonly Version DefaultVersion = new(7, 17); 26 | public Version? DetectedVersion { get; private set; } 27 | public bool DetectionAttempted { get; private set; } 28 | 29 | public ElasticsearchVersionManager( 30 | bool detectElasticsearchVersion, 31 | IElasticLowLevelClient client) 32 | { 33 | _detectElasticsearchVersion = detectElasticsearchVersion; 34 | _client = client ?? throw new ArgumentNullException(nameof(client)); 35 | } 36 | 37 | public Version EffectiveVersion 38 | { 39 | get 40 | { 41 | if (DetectedVersion is not null) 42 | return DetectedVersion; 43 | 44 | if (_detectElasticsearchVersion == false 45 | || DetectionAttempted == true) 46 | return DefaultVersion; 47 | 48 | // Attemp once 49 | DetectedVersion = DiscoverClusterVersion(); 50 | 51 | return DetectedVersion ?? DefaultVersion; 52 | } 53 | } 54 | 55 | internal Version? DiscoverClusterVersion() 56 | { 57 | try 58 | { 59 | var response = _client.DoRequest(HttpMethod.GET, "/"); 60 | if (!response.Success) return null; 61 | 62 | var discoveredVersion = response.Dictionary["version"]["number"]; 63 | 64 | if (!discoveredVersion.HasValue) 65 | return null; 66 | 67 | if (discoveredVersion.Value is not string strVersion) 68 | return null; 69 | 70 | return new Version(strVersion); 71 | 72 | } 73 | catch (Exception ex) 74 | { 75 | SelfLog.WriteLine("Failed to discover the cluster version. {0}", ex); 76 | return null; 77 | } 78 | finally 79 | { 80 | DetectionAttempted = true; 81 | } 82 | } 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /src/Serilog.Sinks.Elasticsearch/Sinks/ElasticSearch/SerializerAdapter.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Threading; 4 | using System.Threading.Tasks; 5 | using Elasticsearch.Net; 6 | using Serilog.Formatting.Elasticsearch; 7 | 8 | namespace Serilog.Sinks.Elasticsearch 9 | { 10 | internal class SerializerAdapter : ISerializer, IElasticsearchSerializer 11 | { 12 | private readonly IElasticsearchSerializer _elasticsearchSerializer; 13 | 14 | internal SerializerAdapter(IElasticsearchSerializer elasticsearchSerializer) 15 | { 16 | _elasticsearchSerializer = elasticsearchSerializer ?? 17 | throw new ArgumentNullException(nameof(elasticsearchSerializer)); 18 | } 19 | 20 | public object Deserialize(Type type, Stream stream) 21 | { 22 | return _elasticsearchSerializer.Deserialize(type, stream); 23 | } 24 | 25 | public T Deserialize(Stream stream) 26 | { 27 | return _elasticsearchSerializer.Deserialize(stream); 28 | } 29 | 30 | public Task DeserializeAsync(Type type, Stream stream, 31 | CancellationToken cancellationToken = default(CancellationToken)) 32 | { 33 | return _elasticsearchSerializer.DeserializeAsync(type, stream, cancellationToken); 34 | } 35 | 36 | public Task DeserializeAsync(Stream stream, CancellationToken cancellationToken = default(CancellationToken)) 37 | { 38 | return _elasticsearchSerializer.DeserializeAsync(stream, cancellationToken); 39 | } 40 | 41 | public void Serialize(T data, Stream stream, 42 | SerializationFormatting formatting = SerializationFormatting.Indented) 43 | { 44 | _elasticsearchSerializer.Serialize(data, stream, formatting); 45 | } 46 | 47 | public Task SerializeAsync(T data, Stream stream, 48 | SerializationFormatting formatting = SerializationFormatting.Indented, 49 | CancellationToken cancellationToken = default(CancellationToken)) 50 | { 51 | return _elasticsearchSerializer.SerializeAsync(data, stream, formatting, cancellationToken); 52 | } 53 | 54 | public string SerializeToString(object value) 55 | { 56 | return _elasticsearchSerializer.SerializeToString(value, formatting: SerializationFormatting.None); 57 | } 58 | } 59 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Bootstrap/ClientTestClusterBase.cs: -------------------------------------------------------------------------------- 1 | using Elastic.Elasticsearch.Ephemeral; 2 | using Elastic.Elasticsearch.Ephemeral.Plugins; 3 | using Elastic.Elasticsearch.Xunit; 4 | using Elastic.Stack.ArtifactsApi.Products; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap 7 | { 8 | public abstract class ClientTestClusterBase : XunitClusterBase 9 | { 10 | protected ClientTestClusterBase(ClientTestClusterConfiguration configuration) : base(configuration) { } 11 | } 12 | 13 | public class ClientTestClusterConfiguration : XunitClusterConfiguration 14 | { 15 | public ClientTestClusterConfiguration( 16 | string elasticsearchVersion, 17 | ClusterFeatures features = ClusterFeatures.None, 18 | int numberOfNodes = 1, 19 | params ElasticsearchPlugin[] plugins 20 | ) 21 | : base(elasticsearchVersion, features, new ElasticsearchPlugins(plugins), numberOfNodes) 22 | { 23 | HttpFiddlerAware = true; 24 | CacheEsHomeInstallation = true; 25 | 26 | Add(AttributeKey("testingcluster"), "true"); 27 | 28 | Add($"script.max_compilations_per_minute", "10000", "<6.0.0-rc1"); 29 | Add($"script.max_compilations_rate", "10000/1m", ">=6.0.0-rc1"); 30 | 31 | Add($"script.inline", "true", "<5.5.0"); 32 | Add($"script.stored", "true", ">5.0.0-alpha1 <5.5.0"); 33 | Add($"script.indexed", "true", "<5.0.0-alpha1"); 34 | Add($"script.allowed_types", "inline,stored", ">=5.5.0"); 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Bootstrap/ElasticsearchSinkOptionsFactory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using Elasticsearch.Net; 3 | 4 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap 5 | { 6 | public static class ElasticsearchSinkOptionsFactory 7 | { 8 | public static ElasticsearchSinkOptions Create(string indexPrefix, string templateName, Action alterOptions = null) 9 | { 10 | // make sure we run through `ipv4.fiddler` if `fiddler` or `mitmproxy` is running 11 | // NOTE with the latter you need to add `ipv4.fiddler` as an alias to 127.0.0.1 in your HOSTS file manually 12 | var pool = new SingleNodeConnectionPool(new Uri($"http://{ProxyDetection.LocalOrProxyHost}:9200")); 13 | var options = new ElasticsearchSinkOptions(pool) 14 | { 15 | IndexFormat = indexPrefix + "{0:yyyy.MM.dd}", 16 | TemplateName = templateName, 17 | }; 18 | 19 | alterOptions?.Invoke(options); 20 | // here we make sure we set a proxy on the elasticsearch connection 21 | // when we detect `mitmproxy` running. This is a cli tool similar to fiddler 22 | // on *nix systems which aids debugging what goes over the wire 23 | var provided = options.ModifyConnectionSettings; 24 | options.ModifyConnectionSettings = configuration => 25 | { 26 | if (ProxyDetection.RunningMitmProxy) 27 | configuration.Proxy(ProxyDetection.MitmProxyAddress, null, (string) null); 28 | configuration = provided?.Invoke(configuration) ?? configuration; 29 | return configuration; 30 | }; 31 | 32 | return options; 33 | } 34 | 35 | } 36 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Bootstrap/ProxyDetection.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using System.Linq; 4 | 5 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap 6 | { 7 | public static class ProxyDetection 8 | { 9 | public static readonly bool RunningMitmProxy = Process.GetProcessesByName("mitmproxy").Any(); 10 | private static readonly bool RunningFiddler = Process.GetProcessesByName("fiddler").Any(); 11 | public static string LocalOrProxyHost => RunningFiddler || RunningMitmProxy ? "ipv4.fiddler" : "localhost"; 12 | 13 | public static readonly Uri MitmProxyAddress = new Uri("http://localhost:8080"); 14 | } 15 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Bootstrap/SerilogSinkElasticsearchXunitRunOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Concurrent; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.Linq; 6 | using System.Text; 7 | using Elastic.Elasticsearch.Xunit; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap 10 | { 11 | /// Feeding TestClient.Configuration options to the runner 12 | public class SerilogSinkElasticsearchXunitRunOptions : ElasticXunitRunOptions 13 | { 14 | public SerilogSinkElasticsearchXunitRunOptions() 15 | { 16 | RunIntegrationTests = true; 17 | RunUnitTests = false; 18 | ClusterFilter = null; 19 | TestFilter = null; 20 | IntegrationTestsMayUseAlreadyRunningNode = false; 21 | } 22 | 23 | public override void OnBeforeTestsRun() { } 24 | 25 | public override void OnTestsFinished(Dictionary clusterTotals, ConcurrentBag> failedCollections) 26 | { 27 | Console.Out.Flush(); 28 | DumpClusterTotals(clusterTotals); 29 | DumpFailedCollections(failedCollections); 30 | } 31 | 32 | private static void DumpClusterTotals(Dictionary clusterTotals) 33 | { 34 | Console.WriteLine("--------"); 35 | Console.WriteLine("Individual cluster running times:"); 36 | foreach (var kv in clusterTotals) Console.WriteLine($"- {kv.Key}: {kv.Value.Elapsed}"); 37 | Console.WriteLine("--------"); 38 | } 39 | 40 | private static void DumpFailedCollections(ConcurrentBag> failedCollections) 41 | { 42 | if (failedCollections.Count <= 0) return; 43 | 44 | Console.ForegroundColor = ConsoleColor.Red; 45 | Console.WriteLine("Failed collections:"); 46 | foreach (var t in failedCollections.OrderBy(p => p.Item1).ThenBy(t => t.Item2)) 47 | 48 | { 49 | var cluster = t.Item1; 50 | Console.WriteLine($" - {cluster}: {t.Item2}"); 51 | } 52 | DumpReproduceFilters(failedCollections); 53 | Console.ResetColor(); 54 | } 55 | 56 | private static void DumpReproduceFilters(ConcurrentBag> failedCollections) 57 | { 58 | Console.ForegroundColor = ConsoleColor.Yellow; 59 | Console.WriteLine("---Reproduce: -----"); 60 | var reproduceLine = ReproduceCommandLine(failedCollections); 61 | Console.WriteLine(reproduceLine); 62 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TEAMCITY_VERSION"))) 63 | Console.WriteLine($"##teamcity[buildProblem description='{reproduceLine}']"); 64 | if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("TF_BUILD"))) 65 | { 66 | var count = failedCollections.Count; 67 | Console.WriteLine($"##vso[task.logissue type=error;]{count} test failures"); 68 | Console.WriteLine($"##vso[task.logissue type=error;]{reproduceLine}"); 69 | } 70 | Console.WriteLine("--------"); 71 | } 72 | 73 | private static string ReproduceCommandLine(ConcurrentBag> failedCollections) 74 | { 75 | var sb = new StringBuilder("build.bat "); 76 | 77 | if (failedCollections.Count > 0) 78 | { 79 | var clusters = string.Join(",", failedCollections 80 | .Select(c => c.Item1.ToLowerInvariant()) 81 | .Distinct()); 82 | sb.Append(" \""); 83 | sb.Append(clusters); 84 | sb.Append("\""); 85 | } 86 | 87 | if ((failedCollections.Count < 30) && failedCollections.Count > 0) 88 | { 89 | sb.Append(" \""); 90 | var tests = string.Join(",", failedCollections 91 | .OrderBy(t => t.Item2) 92 | .Select(c => c.Item2.ToLowerInvariant() 93 | .Split('.') 94 | .Last() 95 | .Replace("apitests", "") 96 | .Replace("usagetests", "") 97 | .Replace("tests", "") 98 | )); 99 | sb.Append(tests); 100 | sb.Append("\""); 101 | } 102 | 103 | var reproduceLine = sb.ToString(); 104 | return reproduceLine; 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Bootstrap/XunitBootstrap.cs: -------------------------------------------------------------------------------- 1 | using Elastic.Elasticsearch.Xunit; 2 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 3 | using Xunit; 4 | 5 | [assembly: TestFramework("Elastic.Elasticsearch.Xunit.Sdk.ElasticTestFramework", "Elastic.Elasticsearch.Xunit")] 6 | [assembly: ElasticXunitConfiguration(typeof(SerilogSinkElasticsearchXunitRunOptions))] 7 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch6/Bootstrap/Elasticsearch6XCluster.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 2 | 3 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6.Bootstrap 4 | { 5 | public class Elasticsearch6XCluster : ClientTestClusterBase 6 | { 7 | public Elasticsearch6XCluster() : base(CreateConfiguration()) { } 8 | 9 | private static ClientTestClusterConfiguration CreateConfiguration() 10 | { 11 | return new ClientTestClusterConfiguration("6.6.0") 12 | { 13 | MaxConcurrency = 1 14 | }; 15 | } 16 | 17 | protected override void SeedCluster() { } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch6/Bootstrap/Elasticsearch6XTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography.X509Certificates; 3 | using Elastic.Elasticsearch.Ephemeral; 4 | using Elastic.Elasticsearch.Xunit; 5 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 6 | using Elasticsearch.Net6; 7 | using Nest6; 8 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6.Bootstrap 11 | { 12 | public abstract class Elasticsearch6XTestBase : IClusterFixture 13 | { 14 | protected Elasticsearch6XTestBase(Elasticsearch6XCluster cluster) => Cluster = cluster; 15 | 16 | private Elasticsearch6XCluster Cluster { get; } 17 | 18 | protected IElasticClient Client => this.CreateClient(); 19 | 20 | protected virtual ConnectionSettings SetClusterSettings(ConnectionSettings s) => s; 21 | 22 | private IElasticClient CreateClient() => 23 | Cluster.GetOrAddClient(c => 24 | { 25 | var clusterNodes = c.NodesUris(ProxyDetection.LocalOrProxyHost); 26 | var settings = new ConnectionSettings(new StaticConnectionPool(clusterNodes)); 27 | if (ProxyDetection.RunningMitmProxy) 28 | settings = settings.Proxy(new Uri("http://localhost:8080"), null, null); 29 | settings = SetClusterSettings(settings); 30 | 31 | var current = (IConnectionConfigurationValues)settings; 32 | var notAlreadyAuthenticated = current.BasicAuthenticationCredentials == null && current.ClientCertificates == null; 33 | var noCertValidation = current.ServerCertificateValidationCallback == null; 34 | 35 | var config = Cluster.ClusterConfiguration; 36 | if (config.EnableSecurity && notAlreadyAuthenticated) 37 | settings = settings.BasicAuthentication(ClusterAuthentication.Admin.Username, ClusterAuthentication.Admin.Password); 38 | if (config.EnableSsl && noCertValidation) 39 | { 40 | //todo use CA callback instead of allow all 41 | // ReSharper disable once UnusedVariable 42 | var ca = new X509Certificate2(config.FileSystem.CaCertificate); 43 | settings = settings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); 44 | } 45 | var client = new ElasticClient(settings); 46 | return client; 47 | }); 48 | } 49 | 50 | } 51 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch6/Elasticsearch6X.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 3 | using FluentAssertions; 4 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 5 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6.Bootstrap; 6 | using Xunit; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6 9 | { 10 | public class Elasticsearch6X : Elasticsearch6XTestBase, IClassFixture 11 | { 12 | private readonly SetupSerilog _setup; 13 | 14 | public Elasticsearch6X(Elasticsearch6XCluster cluster, SetupSerilog setup) : base(cluster) => _setup = setup; 15 | 16 | 17 | [I] public void AssertTemplate() 18 | { 19 | var templateResponse = Client.GetIndexTemplate(t=>t.Name(SetupSerilog.TemplateName)); 20 | templateResponse.TemplateMappings.Should().NotBeEmpty(); 21 | templateResponse.TemplateMappings.Keys.Should().Contain(SetupSerilog.TemplateName); 22 | 23 | var template = templateResponse.TemplateMappings[SetupSerilog.TemplateName]; 24 | 25 | template.IndexPatterns.Should().Contain(pattern => pattern.StartsWith(SetupSerilog.IndexPrefix)); 26 | } 27 | 28 | [I] public void AssertLogs() 29 | { 30 | var refreshed = Client.Refresh(SetupSerilog.IndexPrefix + "*"); 31 | 32 | var search = Client.Search(s => s 33 | .Index(SetupSerilog.IndexPrefix + "*") 34 | .Type(ElasticsearchSinkOptions.DefaultTypeName) 35 | ); 36 | 37 | // Informational should be filtered 38 | search.Documents.Count().Should().Be(4); 39 | } 40 | 41 | // ReSharper disable once ClassNeverInstantiated.Global 42 | public class SetupSerilog 43 | { 44 | public const string IndexPrefix = "logs-6x-default-"; 45 | public const string TemplateName = "serilog-logs-6x"; 46 | 47 | public SetupSerilog() 48 | { 49 | var loggerConfig = new LoggerConfiguration() 50 | .MinimumLevel.Information() 51 | .WriteTo.Console() 52 | .WriteTo.Elasticsearch( 53 | ElasticsearchSinkOptionsFactory.Create(IndexPrefix, TemplateName, o => 54 | { 55 | o.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6; 56 | o.AutoRegisterTemplate = true; 57 | }) 58 | ); 59 | var logger = loggerConfig.CreateLogger(); 60 | 61 | logger.Information("Hello Information"); 62 | logger.Debug("Hello Debug"); 63 | logger.Warning("Hello Warning"); 64 | logger.Error("Hello Error"); 65 | logger.Fatal("Hello Fatal"); 66 | 67 | logger.Dispose(); 68 | } 69 | } 70 | } 71 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch6/Elasticsearch6XUsing7X.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 3 | using FluentAssertions; 4 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 5 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6.Bootstrap; 6 | using Xunit; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch6 9 | { 10 | public class Elasticsearch6XUsing7X : Elasticsearch6XTestBase, IClassFixture 11 | { 12 | private readonly SetupSerilog _setup; 13 | 14 | public Elasticsearch6XUsing7X(Elasticsearch6XCluster cluster, SetupSerilog setup) : base(cluster) => _setup = setup; 15 | 16 | [I] public void AssertTemplate() 17 | { 18 | var templateResponse = Client.GetIndexTemplate(t=>t.Name(SetupSerilog.TemplateName)); 19 | templateResponse.TemplateMappings.Should().NotBeEmpty(); 20 | templateResponse.TemplateMappings.Keys.Should().Contain(SetupSerilog.TemplateName); 21 | 22 | var template = templateResponse.TemplateMappings[SetupSerilog.TemplateName]; 23 | 24 | template.IndexPatterns.Should().Contain(pattern => pattern.StartsWith(SetupSerilog.IndexPrefix)); 25 | } 26 | 27 | [I] public void AssertLogs() 28 | { 29 | var refreshed = Client.Refresh(SetupSerilog.IndexPrefix + "*"); 30 | 31 | var search = Client.Search(s => s 32 | .Index(SetupSerilog.IndexPrefix + "*") 33 | .Type("_doc") 34 | ); 35 | 36 | // Informational should be filtered 37 | search.Documents.Count().Should().Be(4); 38 | } 39 | 40 | // ReSharper disable once ClassNeverInstantiated.Global 41 | public class SetupSerilog 42 | { 43 | public const string IndexPrefix = "logs-6x-using-7x-"; 44 | public const string TemplateName = "serilog-logs-6x-using-7x"; 45 | 46 | public SetupSerilog() 47 | { 48 | var loggerConfig = new LoggerConfiguration() 49 | .MinimumLevel.Information() 50 | .WriteTo.Console() 51 | .WriteTo.Elasticsearch( 52 | ElasticsearchSinkOptionsFactory.Create(IndexPrefix, TemplateName, o => 53 | { 54 | o.DetectElasticsearchVersion = true; 55 | o.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7; 56 | o.AutoRegisterTemplate = true; 57 | o.ApiKey = "aWQ6d3d3ZGRkZGRkZGRkZA=="; 58 | }) 59 | ); 60 | var logger = loggerConfig.CreateLogger(); 61 | 62 | logger.Information("Hello Information"); 63 | logger.Debug("Hello Debug"); 64 | logger.Warning("Hello Warning"); 65 | logger.Error("Hello Error"); 66 | logger.Fatal("Hello Fatal"); 67 | 68 | logger.Dispose(); 69 | } 70 | } 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch7/Bootstrap/Elasticsearch7XCluster.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 2 | 3 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7.Bootstrap 4 | { 5 | public class Elasticsearch7XCluster : ClientTestClusterBase 6 | { 7 | public Elasticsearch7XCluster() : base(CreateConfiguration()) { } 8 | 9 | private static ClientTestClusterConfiguration CreateConfiguration() 10 | { 11 | return new ClientTestClusterConfiguration("7.8.0") 12 | { 13 | MaxConcurrency = 1 14 | }; 15 | } 16 | 17 | protected override void SeedCluster() { } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch7/Bootstrap/Elasticsearch7XTestBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Security.Cryptography.X509Certificates; 3 | using Elastic.Elasticsearch.Ephemeral; 4 | using Elastic.Elasticsearch.Xunit; 5 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 6 | using Elasticsearch.Net; 7 | using Nest; 8 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 9 | using Xunit; 10 | 11 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7.Bootstrap 12 | { 13 | public abstract class Elasticsearch7XTestBase : IClusterFixture 14 | { 15 | protected Elasticsearch7XTestBase(Elasticsearch7XCluster cluster) => Cluster = cluster; 16 | 17 | private Elasticsearch7XCluster Cluster { get; } 18 | 19 | protected IElasticClient Client => this.CreateClient(); 20 | 21 | protected virtual ConnectionSettings SetClusterSettings(ConnectionSettings s) => s; 22 | 23 | private IElasticClient CreateClient() => 24 | Cluster.GetOrAddClient(c => 25 | { 26 | var clusterNodes = c.NodesUris(ProxyDetection.LocalOrProxyHost); 27 | var settings = new ConnectionSettings(new StaticConnectionPool(clusterNodes)); 28 | if (ProxyDetection.RunningMitmProxy) 29 | settings = settings.Proxy(new Uri("http://localhost:8080"), null, (string)null); 30 | settings = SetClusterSettings(settings); 31 | 32 | var current = (IConnectionConfigurationValues)settings; 33 | var notAlreadyAuthenticated = current.BasicAuthenticationCredentials == null && current.ClientCertificates == null; 34 | var noCertValidation = current.ServerCertificateValidationCallback == null; 35 | 36 | var config = Cluster.ClusterConfiguration; 37 | if (config.EnableSecurity && notAlreadyAuthenticated) 38 | settings = settings.BasicAuthentication(ClusterAuthentication.Admin.Username, ClusterAuthentication.Admin.Password); 39 | if (config.EnableSsl && noCertValidation) 40 | { 41 | //todo use CA callback instead of allow all 42 | // ReSharper disable once UnusedVariable 43 | var ca = new X509Certificate2(config.FileSystem.CaCertificate); 44 | settings = settings.ServerCertificateValidationCallback(CertificateValidations.AllowAll); 45 | } 46 | var client = new ElasticClient(settings); 47 | return client; 48 | }); 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch7/Elasticsearch7X.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 3 | using FluentAssertions; 4 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 5 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7.Bootstrap; 6 | using Xunit; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7 9 | { 10 | public class Elasticsearch7X : Elasticsearch7XTestBase, IClassFixture 11 | { 12 | private readonly SetupSerilog _setup; 13 | 14 | public Elasticsearch7X(Elasticsearch7XCluster cluster, SetupSerilog setup) : base(cluster) => _setup = setup; 15 | 16 | [I] public void AssertTemplate() 17 | { 18 | var templateResponse = Client.Indices.GetTemplate(SetupSerilog.TemplateName); 19 | templateResponse.TemplateMappings.Should().NotBeEmpty(); 20 | templateResponse.TemplateMappings.Keys.Should().Contain(SetupSerilog.TemplateName); 21 | 22 | var template = templateResponse.TemplateMappings[SetupSerilog.TemplateName]; 23 | 24 | template.IndexPatterns.Should().Contain(pattern => pattern.StartsWith(SetupSerilog.IndexPrefix)); 25 | } 26 | 27 | [I] public void AssertLogs() 28 | { 29 | var refreshed = Client.Indices.Refresh(SetupSerilog.IndexPrefix + "*"); 30 | 31 | var search = Client.Search(s => s.Index(SetupSerilog.IndexPrefix + "*")); 32 | 33 | // Informational should be filtered 34 | search.Documents.Count().Should().Be(4); 35 | } 36 | 37 | // ReSharper disable once ClassNeverInstantiated.Global 38 | public class SetupSerilog 39 | { 40 | public const string IndexPrefix = "logs-7x-default-"; 41 | public const string TemplateName = "serilog-logs-7x"; 42 | 43 | public SetupSerilog() 44 | { 45 | var loggerConfig = new LoggerConfiguration() 46 | .MinimumLevel.Information() 47 | .WriteTo.Console() 48 | .WriteTo.Elasticsearch( 49 | ElasticsearchSinkOptionsFactory.Create(IndexPrefix, TemplateName, o => 50 | { 51 | o.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7; 52 | o.AutoRegisterTemplate = true; 53 | }) 54 | ); 55 | using (var logger = loggerConfig.CreateLogger()) 56 | { 57 | logger.Information("Hello Information"); 58 | logger.Debug("Hello Debug"); 59 | logger.Warning("Hello Warning"); 60 | logger.Error("Hello Error"); 61 | logger.Fatal("Hello Fatal"); 62 | } 63 | } 64 | } 65 | 66 | } 67 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Elasticsearch7/Elasticsearch7XUsing6X.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using Elastic.Elasticsearch.Xunit.XunitPlumbing; 3 | using FluentAssertions; 4 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Bootstrap; 5 | using Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7.Bootstrap; 6 | using Xunit; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.IntegrationTests.Elasticsearch7 9 | { 10 | public class Elasticsearch7XUsing6X : Elasticsearch7XTestBase, IClassFixture 11 | { 12 | private readonly SetupSerilog _setup; 13 | 14 | public Elasticsearch7XUsing6X(Elasticsearch7XCluster cluster, SetupSerilog setup) : base(cluster) => _setup = setup; 15 | 16 | [I] public void AssertTemplate() 17 | { 18 | var templateResponse = Client.Indices.GetTemplate(SetupSerilog.TemplateName); 19 | templateResponse.TemplateMappings.Should().NotBeEmpty(); 20 | templateResponse.TemplateMappings.Keys.Should().Contain(SetupSerilog.TemplateName); 21 | 22 | var template = templateResponse.TemplateMappings[SetupSerilog.TemplateName]; 23 | 24 | template.IndexPatterns.Should().Contain(pattern => pattern.StartsWith(SetupSerilog.IndexPrefix)); 25 | } 26 | 27 | [I] public void AssertLogs() 28 | { 29 | var refreshed = Client.Indices.Refresh(SetupSerilog.IndexPrefix + "*"); 30 | 31 | var search = Client.Search(s => s.Index(SetupSerilog.IndexPrefix + "*")); 32 | 33 | // Informational should be filtered 34 | search.Documents.Count().Should().Be(4); 35 | } 36 | 37 | // ReSharper disable once ClassNeverInstantiated.Global 38 | public class SetupSerilog 39 | { 40 | public const string IndexPrefix = "logs-7x-using-6x"; 41 | public const string TemplateName = "serilog-logs-7x-using-6x"; 42 | 43 | public SetupSerilog() 44 | { 45 | var loggerConfig = new LoggerConfiguration() 46 | .MinimumLevel.Information() 47 | .WriteTo.Console() 48 | .WriteTo.Elasticsearch( 49 | ElasticsearchSinkOptionsFactory.Create(IndexPrefix, TemplateName, o => 50 | { 51 | o.DetectElasticsearchVersion = true; 52 | o.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6; 53 | o.AutoRegisterTemplate = true; 54 | o.ApiKey = "aWQ6d3d3ZGRkZGRkZGRkZA=="; 55 | }) 56 | ); 57 | using (var logger = loggerConfig.CreateLogger()) 58 | { 59 | logger.Information("Hello Information"); 60 | logger.Debug("Hello Debug"); 61 | logger.Warning("Hello Warning"); 62 | logger.Error("Hello Error"); 63 | logger.Fatal("Hello Fatal"); 64 | } 65 | } 66 | } 67 | 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.IntegrationTests/Serilog.Sinks.Elasticsearch.IntegrationTests.csproj: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | net6.0 5 | $(NoWarn);xUnit1013 6 | True 7 | latest 8 | True 9 | false 10 | true 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | all 24 | runtime; build; native; contentfiles; analyzers; buildtransitive 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | true 43 | 44 | $([MSBuild]::EnsureTrailingSlash($([System.IO.Path]::GetFullPath($([System.IO.Path]::Combine($(MSBuildProjectDirectory),'..','coverage')))))) 45 | 46 | cobertura 47 | 48 | 49 | 50 | 51 | $([MSBuild]::EnsureTrailingSlash('$(CoverletOutput)report')) 52 | xdg-open 53 | open 54 | explorer 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/BulkActionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using FluentAssertions; 4 | using Serilog.Events; 5 | using Serilog.Parsing; 6 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 7 | using Xunit; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests 10 | { 11 | public class BulkActionTests : ElasticsearchSinkTestsBase 12 | { 13 | [Fact(Skip = "Flaky test on GitHub actions")] 14 | public void DefaultBulkActionV7() 15 | { 16 | _options.IndexFormat = "logs"; 17 | _options.TypeName = "_doc"; 18 | _options.PipelineName = null; 19 | using (var sink = new ElasticsearchSink(_options)) 20 | { 21 | sink.Emit(ADummyLogEvent()); 22 | sink.Emit(ADummyLogEvent()); 23 | } 24 | 25 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 2, 1); 26 | const string expectedAction = @"{""index"":{""_type"":""_doc"",""_index"":""logs""}}"; 27 | bulkJsonPieces[0].Should().Be(expectedAction); 28 | } 29 | 30 | [Fact(Skip = "Flaky test on GitHub actions")] 31 | public void BulkActionV7OverrideTypeName() 32 | { 33 | _options.IndexFormat = "logs"; 34 | _options.TypeName = null; // This is the default value, starting v9.0.0 35 | _options.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7; 36 | _options.PipelineName = null; 37 | using (var sink = new ElasticsearchSink(_options)) 38 | { 39 | sink.Emit(ADummyLogEvent()); 40 | sink.Emit(ADummyLogEvent()); 41 | } 42 | 43 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 2, 1); 44 | const string expectedAction = @"{""index"":{""_type"":""_doc"",""_index"":""logs""}}"; 45 | bulkJsonPieces[0].Should().Be(expectedAction); 46 | } 47 | 48 | [Fact(Skip = "Flaky test on GitHub actions")] 49 | public void DefaultBulkActionV8() 50 | { 51 | _options.IndexFormat = "logs"; 52 | _options.TypeName = null; 53 | _options.PipelineName = null; 54 | using (var sink = new ElasticsearchSink(_options)) 55 | { 56 | sink.Emit(ADummyLogEvent()); 57 | sink.Emit(ADummyLogEvent()); 58 | } 59 | 60 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 2, 1); 61 | const string expectedAction = @"{""index"":{""_index"":""logs""}}"; 62 | bulkJsonPieces[0].Should().Be(expectedAction); 63 | } 64 | 65 | 66 | [Fact(Skip = "Flaky test on GitHub actions")] 67 | public void BulkActionDataStreams() 68 | { 69 | _options.IndexFormat = "logs-my-stream"; 70 | _options.TypeName = null; 71 | _options.PipelineName = null; 72 | _options.BatchAction = ElasticOpType.Create; 73 | 74 | using (var sink = new ElasticsearchSink(_options)) 75 | { 76 | sink.Emit(ADummyLogEvent()); 77 | sink.Emit(ADummyLogEvent()); 78 | } 79 | 80 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 2, 1); 81 | const string expectedAction = @"{""create"":{""_index"":""logs-my-stream""}}"; 82 | bulkJsonPieces[0].Should().Be(expectedAction); 83 | } 84 | 85 | [Fact(Skip = "Flaky test on GitHub actions")] 86 | public void PipelineAction() 87 | { 88 | _options.IndexFormat = "logs-my-stream"; 89 | _options.TypeName = "_doc"; 90 | _options.PipelineName = "my-pipeline"; 91 | _options.BatchAction = ElasticOpType.Index; 92 | 93 | using (var sink = new ElasticsearchSink(_options)) 94 | { 95 | sink.Emit(ADummyLogEvent()); 96 | sink.Emit(ADummyLogEvent()); 97 | } 98 | 99 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 2, 1); 100 | const string expectedAction = @"{""index"":{""_type"":""_doc"",""_index"":""logs-my-stream"",""pipeline"":""my-pipeline""}}"; 101 | bulkJsonPieces[0].Should().Be(expectedAction); 102 | } 103 | 104 | private static LogEvent ADummyLogEvent() { 105 | return new LogEvent(DateTimeOffset.Now, LogEventLevel.Information, null, 106 | new MessageTemplate("A template", Enumerable.Empty()), 107 | Enumerable.Empty()); 108 | } 109 | } 110 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/CustomIndexTypeNameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Serilog.Events; 5 | using Serilog.Parsing; 6 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 7 | using Xunit; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests 10 | { 11 | public class CustomIndexTypeNameTests : ElasticsearchSinkTestsBase 12 | { 13 | public CustomIndexTypeNameTests() 14 | : base("6.0.0") 15 | { 16 | 17 | } 18 | 19 | [Fact] 20 | public void CustomIndex_And_TypeName_EndsUpInTheOutput() 21 | { 22 | //DO NOTE that you cant send objects as scalar values through Logger.*("{Scalar}", {}); 23 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 24 | const string messageTemplate = "{Song}++ @{Complex}"; 25 | var template = new MessageTemplateParser().Parse(messageTemplate); 26 | _options.TypeName = "custom-event-type"; 27 | _options.IndexFormat = "event-index-{0:yyyy.MM.dd}"; 28 | using (var sink = new ElasticsearchSink(_options)) 29 | { 30 | var properties = new List { new LogEventProperty("Song", new ScalarValue("New Macabre")) }; 31 | var e = new LogEvent(timestamp, LogEventLevel.Information, null, template, properties); 32 | //one off 33 | sink.Emit(e); 34 | 35 | sink.Emit(e); 36 | var exception = new ArgumentException("parameter"); 37 | properties = new List 38 | { 39 | new LogEventProperty("Song", new ScalarValue("Old Macabre")), 40 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2})) 41 | }; 42 | e = new LogEvent(timestamp.AddYears(-2), LogEventLevel.Fatal, exception, template, properties); 43 | sink.Emit(e); 44 | } 45 | 46 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 47 | 48 | bulkJsonPieces.Should().HaveCount(4); 49 | bulkJsonPieces[0].Should().Contain(@"""_index"":""event-index-2013.05.28"); 50 | bulkJsonPieces[0].Should().Contain(@"""_type"":""custom-event-type"); 51 | bulkJsonPieces[1].Should().Contain("New Macabre"); 52 | bulkJsonPieces[2].Should().Contain(@"""_index"":""event-index-2011.05.28"); 53 | bulkJsonPieces[2].Should().Contain(@"""_type"":""custom-event-type"); 54 | bulkJsonPieces[3].Should().Contain("Old Macabre"); 55 | 56 | bulkJsonPieces[3].Should().Contain("Complex\":{"); 57 | 58 | } 59 | 60 | [Fact] 61 | public void UpperCasedIndex_And_TypeName_EndsUpInTheOutput() 62 | { 63 | //DO NOTE that you cant send objects as scalar values through Logger.*("{Scalar}", {}); 64 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 65 | const string messageTemplate = "{Song}++ @{Complex}"; 66 | var template = new MessageTemplateParser().Parse(messageTemplate); 67 | _options.TypeName = "custom-event-type"; 68 | _options.IndexFormat = "Event-Index-{0:yyyy.MM.dd}"; 69 | using (var sink = new ElasticsearchSink(_options)) 70 | { 71 | var properties = new List { new LogEventProperty("Song", new ScalarValue("New Macabre")) }; 72 | var e = new LogEvent(timestamp, LogEventLevel.Information, null, template, properties); 73 | //one off 74 | sink.Emit(e); 75 | 76 | sink.Emit(e); 77 | var exception = new ArgumentException("parameter"); 78 | properties = new List 79 | { 80 | new LogEventProperty("Song", new ScalarValue("Old Macabre")), 81 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2})) 82 | }; 83 | e = new LogEvent(timestamp.AddYears(-2), LogEventLevel.Fatal, exception, template, properties); 84 | sink.Emit(e); 85 | } 86 | 87 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 88 | 89 | bulkJsonPieces.Should().HaveCount(4); 90 | bulkJsonPieces[0].Should().Contain(@"""_index"":""event-index-2013.05.28"); 91 | bulkJsonPieces[0].Should().Contain(@"""_type"":""custom-event-type"); 92 | bulkJsonPieces[1].Should().Contain("New Macabre"); 93 | bulkJsonPieces[2].Should().Contain(@"""_index"":""event-index-2011.05.28"); 94 | bulkJsonPieces[2].Should().Contain(@"""_type"":""custom-event-type"); 95 | bulkJsonPieces[3].Should().Contain("Old Macabre"); 96 | 97 | bulkJsonPieces[3].Should().Contain("Complex\":{"); 98 | 99 | } 100 | 101 | } 102 | 103 | } 104 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Discrepancies/ElasticsearchDefaultSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Elasticsearch.Net; 2 | using Xunit; 3 | 4 | namespace Serilog.Sinks.Elasticsearch.Tests.Discrepancies 5 | { 6 | public class ElasticsearchDefaultSerializerTests : ElasticsearchSinkUniformityTestsBase 7 | { 8 | public ElasticsearchDefaultSerializerTests() : base(new LowLevelRequestResponseSerializer()) { } 9 | 10 | [Fact] 11 | public void Should_SerializeToExpandedExceptionObjectWhenExceptionIsSet() 12 | { 13 | this.ThrowAndLogAndCatchBulkOutput("test_with_default_serializer"); 14 | } 15 | } 16 | 17 | } 18 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Discrepancies/ElasticsearchSinkUniformityTestsBase.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Runtime.Serialization; 3 | using Elasticsearch.Net; 4 | using FluentAssertions; 5 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 6 | 7 | namespace Serilog.Sinks.Elasticsearch.Tests.Discrepancies 8 | { 9 | public class ElasticsearchSinkUniformityTestsBase : ElasticsearchSinkTestsBase 10 | { 11 | public ElasticsearchSinkUniformityTestsBase(IElasticsearchSerializer serializer) 12 | { 13 | _options.Serializer = serializer; 14 | } 15 | 16 | public void ThrowAndLogAndCatchBulkOutput(string exceptionMessage) 17 | { 18 | var loggerConfig = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .Enrich.WithMachineName() 21 | .WriteTo.Console() 22 | .WriteTo.Elasticsearch(_options); 23 | 24 | var logger = loggerConfig.CreateLogger(); 25 | using (logger as IDisposable) 26 | { 27 | try 28 | { 29 | try 30 | { 31 | throw new Exception("inner most exception"); 32 | } 33 | catch (Exception e) 34 | { 35 | var innerException = new NastyException("nasty inner exception", e); 36 | throw new Exception(exceptionMessage, innerException); 37 | } 38 | } 39 | catch (Exception e) 40 | { 41 | logger.Error(e, "Test exception. Should contain an embedded exception object."); 42 | } 43 | logger.Error("Test exception. Should not contain an embedded exception object."); 44 | } 45 | 46 | var postedEvents = this.GetPostedLogEvents(expectedCount: 2); 47 | Console.WriteLine("BULK OUTPUT BEGIN =========="); 48 | foreach (var post in _seenHttpPosts) 49 | Console.WriteLine(post); 50 | Console.WriteLine("BULK OUTPUT END ============"); 51 | 52 | var firstEvent = postedEvents[0]; 53 | firstEvent.Exceptions.Should().NotBeNull().And.HaveCount(3); 54 | firstEvent.Exceptions[0].Message.Should().NotBeNullOrWhiteSpace() 55 | .And.Be(exceptionMessage); 56 | var realException = firstEvent.Exceptions[0]; 57 | #if !NO_SERIALIZATION 58 | #if NETFRAMEWORK 59 | realException.ExceptionMethod.Should().NotBeNull(); 60 | realException.ExceptionMethod.Name.Should().NotBeNullOrWhiteSpace(); 61 | realException.ExceptionMethod.AssemblyName.Should().NotBeNullOrWhiteSpace(); 62 | realException.ExceptionMethod.AssemblyVersion.Should().NotBeNullOrWhiteSpace(); 63 | realException.ExceptionMethod.ClassName.Should().NotBeNullOrWhiteSpace(); 64 | realException.ExceptionMethod.Signature.Should().NotBeNullOrWhiteSpace(); 65 | realException.ExceptionMethod.MemberType.Should().BeGreaterThan(0); 66 | #endif 67 | var nastyException = firstEvent.Exceptions[1]; 68 | nastyException.Depth.Should().Be(1); 69 | nastyException.Message.Should().Be("nasty inner exception"); 70 | nastyException.StackTraceString.Should().Be("stack trace string"); 71 | nastyException.HelpUrl.Should().Be("help url"); 72 | nastyException.RemoteStackTraceString.Should().Be("remote stack trace string"); 73 | nastyException.RemoteStackIndex.Should().Be(1); 74 | nastyException.ClassName.Should().Be("classname nasty exception"); 75 | nastyException.HResult.Should().Be(123123); 76 | nastyException.Source.Should().Be("source"); 77 | #endif 78 | //nastyException.WatsonBuckets.Should().BeEquivalentTo(new byte[] {1,2,3}); 79 | 80 | 81 | var secondEvent = postedEvents[1]; 82 | secondEvent.Exceptions.Should().BeNullOrEmpty(); 83 | } 84 | } 85 | 86 | /// 87 | /// Exception that forces often empty serializationinfo values to have a value 88 | /// 89 | public class NastyException : Exception 90 | { 91 | public NastyException(string message) : base(message) { } 92 | public NastyException(string message, Exception innerException) : base(message, innerException) { } 93 | 94 | #if !NO_SERIALIZATION 95 | public override void GetObjectData(SerializationInfo info, StreamingContext context) 96 | { 97 | info.AddValue("Message", this.Message); 98 | info.AddValue("HelpURL", "help url"); 99 | info.AddValue("StackTraceString", "stack trace string"); 100 | 101 | info.AddValue("RemoteStackTraceString", "remote stack trace string"); 102 | info.AddValue("RemoteStackIndex", 1); 103 | info.AddValue("ExceptionMethod", "exception method"); 104 | info.AddValue("HResult", 123123); 105 | info.AddValue("Source", "source"); 106 | info.AddValue("ClassName", "classname nasty exception"); 107 | info.AddValue("WatsonBuckets", new byte[] { 1, 2, 3 }, typeof(byte[])); 108 | } 109 | #endif 110 | } 111 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Discrepancies/JsonNetSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Elasticsearch.Net; 2 | using Nest; 3 | using Nest.JsonNetSerializer; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Discrepancies 7 | { 8 | public class JsonNetSerializerTests : ElasticsearchSinkUniformityTestsBase 9 | { 10 | public JsonNetSerializerTests() : base(JsonNetSerializer.Default(LowLevelRequestResponseSerializer.Instance, new ConnectionSettings())) { } 11 | 12 | [Fact] 13 | public void Should_SerializeToExpandedExceptionObjectWhenExceptionIsSet() 14 | { 15 | this.ThrowAndLogAndCatchBulkOutput("test_with_jsonnet_serializer"); 16 | } 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Discrepancies/NoSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using Xunit; 2 | 3 | namespace Serilog.Sinks.Elasticsearch.Tests.Discrepancies 4 | { 5 | public class NoSerializerTests : ElasticsearchSinkUniformityTestsBase 6 | { 7 | public NoSerializerTests() : base(null) {} 8 | 9 | [Fact] 10 | public void Should_SerializeToExpandedExceptionObjectWhenExceptionIsSet() 11 | { 12 | this.ThrowAndLogAndCatchBulkOutput("test_with_no_serializer"); 13 | } 14 | } 15 | 16 | } 17 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Domain/BulkAction.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using Newtonsoft.Json; 4 | using Newtonsoft.Json.Converters; 5 | using Serilog.Events; 6 | 7 | namespace Serilog.Sinks.Elasticsearch.Tests.Domain 8 | { 9 | /// 10 | /// Elasticsearch _bulk follows a specific pattern: 11 | /// {operation}\n 12 | /// {operationmetadata}\n 13 | /// This provides a marker interface for both 14 | /// 15 | interface IBulkData { } 16 | 17 | public class BulkOperation : IBulkData 18 | { 19 | [JsonProperty("index")] 20 | public IndexAction IndexAction { get; set; } 21 | } 22 | 23 | public class IndexAction 24 | { 25 | [JsonProperty("_index")] 26 | public string Index { get; set; } 27 | [JsonProperty("_type")] 28 | public string Type { get; set; } 29 | } 30 | 31 | public class SerilogElasticsearchEvent : IBulkData 32 | { 33 | [JsonProperty("@timestamp")] 34 | public DateTime Timestamp { get; set; } 35 | 36 | [JsonProperty("level")] 37 | [JsonConverter(typeof(StringEnumConverter))] 38 | public LogEventLevel Level { get; set; } 39 | 40 | [JsonProperty("messageTemplate")] 41 | public string MessageTemplate { get; set; } 42 | 43 | [JsonProperty("message")] 44 | public string Message { get; set; } 45 | 46 | [JsonProperty("exceptions")] 47 | public List Exceptions { get; set; } 48 | } 49 | 50 | public class SerilogElasticsearchExceptionInfo 51 | { 52 | public int Depth { get; set; } 53 | public string ClassName { get; set; } 54 | public string Message { get; set; } 55 | public string Source { get; set; } 56 | public string StackTraceString { get; set; } 57 | public string RemoteStackTraceString { get; set; } 58 | public int RemoteStackIndex { get; set; } 59 | public SerilogExceptionMethodInfo ExceptionMethod { get; set; } 60 | public int HResult { get; set; } 61 | public string HelpUrl { get; set; } 62 | 63 | //writing byte[] will fall back to serializer and they differ in output 64 | //JsonNET assumes string, simplejson writes array of numerics. 65 | //Skip for now 66 | 67 | //public byte[] WatsonBuckets { get; set; } 68 | } 69 | 70 | public class SerilogExceptionMethodInfo 71 | { 72 | public string Name { get; set; } 73 | public string AssemblyName { get; set; } 74 | public string AssemblyVersion { get; set; } 75 | public string AssemblyCulture { get; set; } 76 | public string ClassName { get; set; } 77 | public string Signature { get; set; } 78 | public int MemberType { get; set; } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/ElasticSearchLogShipperTests.cs: -------------------------------------------------------------------------------- 1 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 2 | 3 | namespace Serilog.Sinks.Elasticsearch.Tests 4 | { 5 | public class ElasticSearchLogShipperTests : ElasticsearchSinkTestsBase 6 | { 7 | //To enable tests : 8 | // * Create public key token for Testproject, make Serilog.Sinks.Elasticsearch internals visible to testproject & scope ElasticsearchLogShipper to internal. 9 | // * or make ElasticsearchLogShipper (temporarily) public. 10 | 11 | //[Fact] 12 | //public void ElasticsearchLogShipper_TryReadLineShouldReadWithBOM() 13 | //{ 14 | // bool withBom = true; 15 | 16 | // string testLine1 = "test 1"; 17 | // string testLine2 = "test 2"; 18 | 19 | // using (MemoryStream s = new MemoryStream()) 20 | // using (StreamWriter sw = new StreamWriter(s, new UTF8Encoding(withBom))) 21 | // { 22 | // sw.WriteLine(testLine1); 23 | // sw.WriteLine(testLine2); 24 | // sw.Flush(); 25 | 26 | // string nextLine; 27 | // long nextStart = 0; 28 | 29 | // ElasticsearchLogShipper.TryReadLine(s, ref nextStart, out nextLine); 30 | 31 | // Assert.Equal(Encoding.UTF8.GetByteCount(testLine1) + +Encoding.UTF8.GetByteCount(Environment.NewLine) + 3, nextStart); 32 | // Assert.Equal(testLine1, nextLine); 33 | 34 | // ElasticsearchLogShipper.TryReadLine(s, ref nextStart, out nextLine); 35 | // Assert.Equal(testLine2, nextLine); 36 | // } 37 | //} 38 | 39 | //[Fact] 40 | //public void ElasticsearchLogShipper_TryReadLineShouldReadWithoutBOM() 41 | //{ 42 | // bool withBom = false; 43 | 44 | // string testLine1 = "test 1"; 45 | // string testLine2 = "test 2"; 46 | // Encoding.UTF8.GetBytes(testLine1); 47 | 48 | // using (MemoryStream s = new MemoryStream()) 49 | // using (StreamWriter sw = new StreamWriter(s, new UTF8Encoding(withBom))) 50 | // { 51 | // sw.WriteLine(testLine1); 52 | // sw.WriteLine(testLine2); 53 | // sw.Flush(); 54 | 55 | // string nextLine; 56 | // long nextStart = 0; 57 | 58 | // ElasticsearchLogShipper.TryReadLine(s, ref nextStart, out nextLine); 59 | 60 | // Assert.Equal(Encoding.UTF8.GetByteCount(testLine1) + Encoding.UTF8.GetByteCount(Environment.NewLine), nextStart); 61 | // Assert.Equal(testLine1, nextLine); 62 | 63 | // ElasticsearchLogShipper.TryReadLine(s, ref nextStart, out nextLine); 64 | // Assert.Equal(testLine2, nextLine); 65 | 66 | // } 67 | //} 68 | 69 | //[Fact] 70 | //public void ElasticsearchLogShipper_CreatePayLoad_ShouldSkipOversizedEvent() 71 | //{ 72 | // var selfLogMessages = new StringBuilder(); 73 | // SelfLog.Enable(new StringWriter(selfLogMessages)); 74 | // string testLine1 = "test 1"; 75 | // string testLine2 = "test 2"; 76 | // string testLine3 = "1234567"; 77 | // var startPosition = 0; 78 | // _options.BatchPostingLimit = 50; 79 | // _options.SingleEventSizePostingLimit = 6; 80 | 81 | // var payLoad = new List(); 82 | // using (MemoryStream s = new MemoryStream()) 83 | // using (StreamWriter sw = new StreamWriter(s, new UTF8Encoding(false))) 84 | // { 85 | // sw.WriteLine(testLine1); 86 | // sw.WriteLine(testLine2); 87 | // sw.WriteLine(testLine3); 88 | // sw.Flush(); 89 | 90 | // (new ElasticsearchLogShipper(_options)).CreatePayLoad(s, payLoad, "TestIndex", startPosition, "D:\\TestFile"); 91 | 92 | // var expectedNumberOfLines = 2; 93 | // Assert.Equal(expectedNumberOfLines*2, payLoad.Count()); 94 | // Assert.Contains("Skip sending to ElasticSearch", selfLogMessages.ToString()); 95 | // } 96 | //} 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchPayloadReaderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using FluentAssertions; 5 | using Serilog.Sinks.Elasticsearch.Durable; 6 | using Xunit; 7 | 8 | namespace Serilog.Sinks.Elasticsearch.Tests; 9 | 10 | public class ElasticsearchPayloadReaderTests : IDisposable 11 | { 12 | private readonly string _tempFileFullPathTemplate; 13 | private string _bufferFileName; 14 | 15 | public ElasticsearchPayloadReaderTests() 16 | { 17 | _tempFileFullPathTemplate = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")) + "-{0}.json"; 18 | } 19 | 20 | public void Dispose() 21 | { 22 | if (!string.IsNullOrEmpty(_bufferFileName)) 23 | { 24 | System.IO.File.Delete(_bufferFileName); 25 | } 26 | } 27 | 28 | [Theory] 29 | [InlineData(RollingInterval.Day)] 30 | [InlineData(RollingInterval.Hour)] 31 | [InlineData(RollingInterval.Minute)] 32 | public void ReadPayload_ShouldReadSpecifiedTypesOfRollingFile(RollingInterval rollingInterval) 33 | { 34 | // Arrange 35 | var format = rollingInterval.GetFormat(); 36 | var payloadReader = new ElasticsearchPayloadReader("testPipelineName", 37 | "TestTypeName", 38 | null, 39 | (_, _) => "TestIndex", 40 | ElasticOpType.Index, 41 | rollingInterval); 42 | var lines = new[] 43 | { 44 | rollingInterval.ToString() 45 | }; 46 | _bufferFileName = string.Format(_tempFileFullPathTemplate, 47 | string.IsNullOrEmpty(format) ? string.Empty : new DateTime(2000, 1, 1).ToString(format)); 48 | // Important to use UTF8 with BOM if we are starting from 0 position 49 | System.IO.File.WriteAllLines(_bufferFileName, lines, new UTF8Encoding(true)); 50 | 51 | // Act 52 | var fileSetPosition = new FileSetPosition(0, _bufferFileName); 53 | var count = 0; 54 | var payload = payloadReader.ReadPayload(int.MaxValue, 55 | null, 56 | ref fileSetPosition, 57 | ref count, 58 | _bufferFileName); 59 | 60 | // Assert 61 | // Thus we ensure that file was properly handled by PayloadReader 62 | payload.Count.Should().Be(lines.Length * 2); 63 | payload[1].Should().Be(lines[0]); 64 | } 65 | 66 | [Theory] 67 | [InlineData(RollingInterval.Infinite)] 68 | [InlineData(RollingInterval.Year)] 69 | [InlineData(RollingInterval.Month)] 70 | public void ElasticsearchPayloadReader_CannotUseRollingIntervalLessFrequentThanDay(RollingInterval rollingInterval) 71 | { 72 | // Arrange 73 | 74 | // Act 75 | Action act = () => new ElasticsearchPayloadReader("testPipelineName", 76 | "TestTypeName", 77 | null, 78 | (_, _) => "TestIndex", 79 | ElasticOpType.Index, 80 | rollingInterval); 81 | 82 | // Assert 83 | act.Should().Throw() 84 | .WithMessage("Rolling intervals less frequent than RollingInterval.Day are not supported"); 85 | } 86 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/ElasticsearchSinkTests.cs: -------------------------------------------------------------------------------- 1 | using Elasticsearch.Net; 2 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 3 | using System.Text; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests 7 | { 8 | public class ElasticsearchSinkTests 9 | { 10 | [Theory] 11 | [InlineData("8.0.0", "my-logevent", null)] 12 | [InlineData("7.17.5", "my-logevent", null)] 13 | [InlineData("6.8.1", "my-logevent", "my-logevent")] 14 | [InlineData("8.0.0", null, null)] 15 | [InlineData("7.17.5", null, null)] 16 | [InlineData("6.8.1", null, "logevent")] 17 | public void Ctor_DetectElasticsearchVersionSetToTrue_SetsTypeName(string elasticVersion, string configuredTypeName, string expectedTypeName) 18 | { 19 | /* ARRANGE */ 20 | var options = new ElasticsearchSinkOptions 21 | { 22 | Connection = FakeProductCheckResponse(elasticVersion), 23 | TypeName = configuredTypeName 24 | }; 25 | 26 | /* ACT */ 27 | _ = ElasticsearchSinkState.Create(options); 28 | 29 | /* Assert */ 30 | Assert.Equal(expectedTypeName, options.TypeName); 31 | } 32 | 33 | [Theory] 34 | [InlineData("8.0.0", "my-logevent", null)] 35 | [InlineData("7.17.5", "my-logevent", null)] 36 | [InlineData("6.8.1", "my-logevent", null)] 37 | [InlineData("8.0.0", null, null)] 38 | [InlineData("7.17.5", null, null)] 39 | [InlineData("6.8.1", null, null)] 40 | public void Ctor_DetectElasticsearchVersionSetToFalseAssumesVersion7_SetsTypeNameToNull(string elasticVersion, string configuredTypeName, string expectedTypeName) 41 | { 42 | /* ARRANGE */ 43 | var options = new ElasticsearchSinkOptions 44 | { 45 | Connection = FakeProductCheckResponse(elasticVersion), 46 | DetectElasticsearchVersion = false, 47 | TypeName = configuredTypeName 48 | }; 49 | 50 | /* ACT */ 51 | _ = ElasticsearchSinkState.Create(options); 52 | 53 | /* Assert */ 54 | Assert.Equal(expectedTypeName, options.TypeName); 55 | } 56 | 57 | [Theory] 58 | [InlineData("8.0.0", "my-logevent", null)] 59 | [InlineData("7.17.5", "my-logevent", null)] 60 | [InlineData("6.8.1", "my-logevent", "my-logevent")] 61 | [InlineData("8.0.0", null, null)] 62 | [InlineData("7.17.5", null, null)] 63 | [InlineData("6.8.1", null, "logevent")] 64 | public void CreateLogger_DetectElasticsearchVersionSetToTrue_SetsTypeName(string elasticVersion, string configuredTypeName, string expectedTypeName) 65 | { 66 | /* ARRANGE */ 67 | var options = new ElasticsearchSinkOptions 68 | { 69 | Connection = FakeProductCheckResponse(elasticVersion), 70 | DetectElasticsearchVersion = true, 71 | TypeName = configuredTypeName 72 | }; 73 | 74 | var loggerConfig = new LoggerConfiguration() 75 | .MinimumLevel.Debug() 76 | .Enrich.WithMachineName() 77 | .WriteTo.Console() 78 | .WriteTo.Elasticsearch(options); 79 | 80 | /* ACT */ 81 | _ = loggerConfig.CreateLogger(); 82 | 83 | /* Assert */ 84 | Assert.Equal(expectedTypeName, options.TypeName); 85 | } 86 | 87 | private static IConnection FakeProductCheckResponse(string responseText) 88 | { 89 | var productCheckResponse = ConnectionStub.ModifiedProductCheckResponse(responseText); 90 | return new InMemoryConnection(productCheckResponse); 91 | } 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/ExceptionAsJsonObjectFormatterTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using FluentAssertions; 5 | using Newtonsoft.Json; 6 | using Newtonsoft.Json.Converters; 7 | using Xunit; 8 | using Serilog.Events; 9 | using Serilog.Formatting.Elasticsearch; 10 | using Serilog.Parsing; 11 | using Serilog.Sinks.Elasticsearch.Tests.Domain; 12 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 13 | 14 | namespace Serilog.Sinks.Elasticsearch.Tests 15 | { 16 | public class ExceptionAsJsonObjectFormatterTests : ElasticsearchSinkTestsBase 17 | { 18 | private static readonly MessageTemplateParser _messageTemplateParser = new MessageTemplateParser(); 19 | 20 | public ExceptionAsJsonObjectFormatterTests() : base() 21 | { 22 | 23 | _options.CustomFormatter = new ExceptionAsObjectJsonFormatter(renderMessage:true); 24 | } 25 | 26 | [Fact] 27 | public void WhenLoggingAnEvent_OutputsValidJson() 28 | { 29 | const string expectedMessage = "test"; 30 | 31 | using (var sink = new ElasticsearchSink(_options)) 32 | { 33 | sink.Emit(LogEventWithMessage(expectedMessage)); 34 | } 35 | 36 | var eventWritten = AssertAndGetJsonEvents().First(); 37 | eventWritten.Level.Should().Be(LogEventLevel.Warning); 38 | eventWritten.Message.Should().Be(expectedMessage); 39 | } 40 | 41 | [Fact] 42 | public void WhenLogging_WithException_ExceptionShouldBeRenderedInExceptionField() 43 | { 44 | const string expectedExceptionMessage = "test exception"; 45 | 46 | using (var sink = new ElasticsearchSink(_options)) 47 | { 48 | sink.Emit(LogEventWithMessage("test", new Exception(expectedExceptionMessage))); 49 | } 50 | 51 | var eventWritten = AssertAndGetJsonEvents().First(); 52 | var exceptionInfo = eventWritten.Exception; 53 | exceptionInfo.Should().NotBeNull(); 54 | exceptionInfo.Message.Should().Be(expectedExceptionMessage); 55 | #if NETFRAMEWORK 56 | exceptionInfo.ClassName.Should().Be("System.Exception"); 57 | #endif 58 | } 59 | 60 | [Fact] 61 | public void WhenLogging_ExceptionWithInner_ExceptionShouldIncludeInnerExceptions() 62 | { 63 | var inner = new InvalidOperationException(); 64 | var exception = new Exception("outer", inner); 65 | 66 | using (var sink = new ElasticsearchSink(_options)) 67 | { 68 | sink.Emit(LogEventWithMessage("test", exception)); 69 | } 70 | 71 | var eventWritten = AssertAndGetJsonEvents().First(); 72 | var exceptionInfo = eventWritten.Exception; 73 | exceptionInfo.InnerException.Should().NotBeNull(); 74 | 75 | } 76 | 77 | private static LogEvent LogEventWithMessage(string expectedMessage, Exception exception = null) 78 | { 79 | var template = _messageTemplateParser.Parse(expectedMessage); 80 | return new LogEvent(DateTimeOffset.Now, LogEventLevel.Warning, exception, template, Enumerable.Empty()); 81 | } 82 | 83 | private IEnumerable AssertAndGetJsonEvents() 84 | { 85 | _seenHttpPosts.Should().NotBeEmpty(); 86 | return _seenHttpPosts.SelectMany(postedData => postedData.Split(new char[] { '\n'}, StringSplitOptions.RemoveEmptyEntries)) 87 | .Where((_,i) => i % 2 == 1) 88 | .Select(JsonConvert.DeserializeObject); 89 | } 90 | 91 | 92 | class KibanaFriendlyJsonEvent : IBulkData 93 | { 94 | [JsonProperty("@timestamp")] 95 | public DateTime Timestamp { get; set; } 96 | 97 | [JsonProperty("level")] 98 | [JsonConverter(typeof(StringEnumConverter))] 99 | public LogEventLevel Level { get; set; } 100 | 101 | [JsonProperty("messageTemplate")] 102 | public string MessageTemplate { get; set; } 103 | 104 | [JsonProperty("message")] 105 | public string Message { get; set; } 106 | 107 | [JsonProperty("exception")] 108 | public SerilogElasticsearchExceptionInfoWithInner Exception { get; set; } 109 | } 110 | 111 | class SerilogElasticsearchExceptionInfoWithInner : SerilogElasticsearchExceptionInfo 112 | { 113 | [JsonProperty("innerException")] 114 | public SerilogElasticsearchExceptionInfo InnerException { get; set; } 115 | } 116 | } 117 | 118 | 119 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/FileSetTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Text; 5 | using FluentAssertions; 6 | using Serilog.Sinks.Elasticsearch.Durable; 7 | using Xunit; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests; 10 | 11 | public class FileSetTests : IDisposable 12 | { 13 | private readonly string _fileNameBase; 14 | private readonly string _tempFileFullPathTemplate; 15 | private Dictionary _bufferFileNames; 16 | 17 | public FileSetTests() 18 | { 19 | _fileNameBase = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N")); 20 | _tempFileFullPathTemplate = _fileNameBase + "-{0}.json"; 21 | } 22 | 23 | public void Dispose() 24 | { 25 | foreach (var bufferFileName in _bufferFileNames.Values) 26 | { 27 | System.IO.File.Delete(bufferFileName); 28 | } 29 | } 30 | 31 | [Theory] 32 | [InlineData(RollingInterval.Day)] 33 | [InlineData(RollingInterval.Hour)] 34 | [InlineData(RollingInterval.Infinite)] 35 | [InlineData(RollingInterval.Minute)] 36 | [InlineData(RollingInterval.Month)] 37 | [InlineData(RollingInterval.Year)] 38 | // Ensures that from all presented files FileSet gets only files with specified rolling interval and not the others. 39 | public void GetBufferFiles_ReturnsOnlySpecifiedTypeOfRollingFile(RollingInterval rollingInterval) 40 | { 41 | // Arrange 42 | var format = rollingInterval.GetFormat(); 43 | _bufferFileNames = GenerateFilesUsingFormat(format); 44 | var fileSet = new FileSet(_fileNameBase, rollingInterval); 45 | var bufferFileForInterval = _bufferFileNames[rollingInterval]; 46 | 47 | // Act 48 | var bufferFiles = fileSet.GetBufferFiles(); 49 | 50 | // Assert 51 | bufferFiles.Should().BeEquivalentTo(bufferFileForInterval); 52 | } 53 | 54 | /// 55 | /// Generates buffer files for all RollingIntervals and returns dictionary of {rollingInterval, fileName} pairs. 56 | /// 57 | /// 58 | /// 59 | private Dictionary GenerateFilesUsingFormat(string format) 60 | { 61 | var result = new Dictionary(); 62 | foreach (var rollingInterval in Enum.GetValues(typeof(RollingInterval))) 63 | { 64 | var bufferFileName = string.Format(_tempFileFullPathTemplate, 65 | string.IsNullOrEmpty(format) ? string.Empty : new DateTime(2000, 1, 1).ToString(format)); 66 | var lines = new[] {rollingInterval.ToString()}; 67 | // Important to use UTF8 with BOM if we are starting from 0 position 68 | System.IO.File.WriteAllLines(bufferFileName, lines, new UTF8Encoding(true)); 69 | result.Add((RollingInterval) rollingInterval, bufferFileName); 70 | } 71 | 72 | return result; 73 | } 74 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/IndexDeciderTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using FluentAssertions; 4 | using Xunit; 5 | using Serilog.Events; 6 | using Serilog.Parsing; 7 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests 10 | { 11 | public class IndexDeciderTests : ElasticsearchSinkTestsBase 12 | { 13 | [Fact] 14 | public void IndexDecider_EndsUpInTheOutput() 15 | { 16 | //DO NOTE that you cant send objects as scalar values through Logger.*("{Scalar}", {}); 17 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 18 | const string messageTemplate = "{Song}++ @{Complex}"; 19 | var template = new MessageTemplateParser().Parse(messageTemplate); 20 | _options.IndexDecider = (l, utcTime) => string.Format("logstash-{1}-{0:yyyy.MM.dd}", utcTime, l.Level.ToString().ToLowerInvariant()); 21 | using (var sink = new ElasticsearchSink(_options)) 22 | { 23 | var properties = new List { new LogEventProperty("Song", new ScalarValue("New Macabre")) }; 24 | var e = new LogEvent(timestamp, LogEventLevel.Information, null, template, properties); 25 | //one off 26 | sink.Emit(e); 27 | 28 | 29 | sink.Emit(e); 30 | var exception = new ArgumentException("parameter"); 31 | properties = new List 32 | { 33 | new LogEventProperty("Song", new ScalarValue("Old Macabre")), 34 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2})) 35 | }; 36 | e = new LogEvent(timestamp.AddYears(-2), LogEventLevel.Fatal, exception, template, properties); 37 | sink.Emit(e); 38 | } 39 | 40 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 41 | bulkJsonPieces[0].Should().Contain(@"""_index"":""logstash-information-2013.05.28"); 42 | bulkJsonPieces[1].Should().Contain("New Macabre"); 43 | bulkJsonPieces[2].Should().Contain(@"""_index"":""logstash-fatal-2011.05.28"); 44 | bulkJsonPieces[3].Should().Contain("Old Macabre"); 45 | 46 | //serilog by default simpy .ToString()'s unknown objects 47 | bulkJsonPieces[3].Should().Contain("Complex\":{"); 48 | 49 | } 50 | } 51 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/InlineFieldsTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Serilog.Events; 6 | using Serilog.Parsing; 7 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 8 | using Xunit; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.Tests 11 | { 12 | public class InlineFieldsTests : ElasticsearchSinkTestsBase 13 | { 14 | [Fact] 15 | public async Task UsesCustomPropertyNames() 16 | { 17 | try 18 | { 19 | await this.ThrowAsync(); 20 | } 21 | catch (Exception e) 22 | { 23 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 24 | var messageTemplate = "{Song}++"; 25 | var template = new MessageTemplateParser().Parse(messageTemplate); 26 | _options.InlineFields = true; 27 | using (var sink = new ElasticsearchSink(_options)) 28 | { 29 | var properties = new List 30 | { 31 | new LogEventProperty("Song", new ScalarValue("New Macabre")), 32 | }; 33 | var logEvent = new LogEvent(timestamp, LogEventLevel.Information, e, template, properties); 34 | //one off 35 | sink.Emit(logEvent); 36 | 37 | sink.Emit(logEvent); 38 | logEvent = new LogEvent(timestamp.AddDays(2), LogEventLevel.Information, e, template, properties); 39 | sink.Emit(logEvent); 40 | } 41 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 42 | bulkJsonPieces[0].Should().Contain(@"""_index"":""logstash-2013.05.28"); 43 | bulkJsonPieces[1].Should().Contain("New Macabre"); 44 | bulkJsonPieces[1].Should().NotContain("Properties\""); 45 | bulkJsonPieces[1].Should().NotContain("fields\":{"); 46 | bulkJsonPieces[1].Should().Contain("@timestamp"); 47 | bulkJsonPieces[1].Should().Contain("Song\":"); 48 | bulkJsonPieces[1].Should().EndWith("New Macabre\"}"); 49 | bulkJsonPieces[2].Should().Contain(@"""_index"":""logstash-2013.05.30"); 50 | } 51 | } 52 | } 53 | 54 | } 55 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Properties/AssemblyInfo.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using System.Runtime.InteropServices; 3 | 4 | // General Information about an assembly is controlled through the following 5 | // set of attributes. Change these attribute values to modify the information 6 | // associated with an assembly. 7 | [assembly: AssemblyTitle("Serilog.Sinks.Elasticsearch.Tests")] 8 | [assembly: AssemblyDescription("")] 9 | [assembly: AssemblyConfiguration("")] 10 | [assembly: AssemblyCompany("")] 11 | [assembly: AssemblyProduct("Serilog.Sinks.Elasticsearch.Tests")] 12 | [assembly: AssemblyCopyright("Copyright © 2014")] 13 | [assembly: AssemblyTrademark("")] 14 | [assembly: AssemblyCulture("")] 15 | 16 | // Setting ComVisible to false makes the types in this assembly not visible 17 | // to COM components. If you need to access a type in this assembly from 18 | // COM, set the ComVisible attribute to true on that type. 19 | [assembly: ComVisible(false)] 20 | 21 | // The following GUID is for the ID of the typelib if this project is exposed to COM 22 | [assembly: Guid("298dc2e0-0c2f-4a21-b230-a643faaa21f5")] 23 | 24 | // Version information for an assembly consists of the following four values: 25 | // 26 | // Major Version 27 | // Minor Version 28 | // Build Number 29 | // Revision 30 | // 31 | // You can specify all the values or you can default the Build and Revision Numbers 32 | // by using the '*' as shown below: 33 | // [assembly: AssemblyVersion("5.3.0.0")] 34 | [assembly: AssemblyVersion("5.3.0.0")] 35 | [assembly: AssemblyFileVersion("5.3.0.0")] 36 | 37 | [assembly: AssemblyInformationalVersion("5.3.0-unstable.32+Branch.dev.Sha.7e77498e8afaaec96b508de3e8ec6bd3891b3556")] 38 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/PropertyNameTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Serilog.Events; 7 | using Serilog.Parsing; 8 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.Tests 11 | { 12 | public class PropertyNameTests : ElasticsearchSinkTestsBase 13 | { 14 | 15 | [Fact] 16 | public async Task UsesCustomPropertyNames() 17 | { 18 | try 19 | { 20 | await this.ThrowAsync(); 21 | } 22 | catch (Exception e) 23 | { 24 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 25 | var messageTemplate = "{Song}++"; 26 | var template = new MessageTemplateParser().Parse(messageTemplate); 27 | using (var sink = new ElasticsearchSink(_options)) 28 | { 29 | var properties = new List 30 | { 31 | new LogEventProperty("Song", new ScalarValue("New Macabre")), 32 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2 })) 33 | }; 34 | var logEvent = new LogEvent(timestamp, LogEventLevel.Information, e, template, properties); 35 | //one off 36 | sink.Emit(logEvent); 37 | 38 | sink.Emit(logEvent); 39 | logEvent = new LogEvent(timestamp.AddDays(2), LogEventLevel.Information, e, template, properties); 40 | sink.Emit(logEvent); 41 | } 42 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 43 | bulkJsonPieces[0].Should().Contain(@"""_index"":""logstash-2013.05.28"); 44 | bulkJsonPieces[1].Should().Contain("New Macabre"); 45 | bulkJsonPieces[1].Should().NotContain("Properties\""); 46 | bulkJsonPieces[1].Should().Contain("fields\":{"); 47 | bulkJsonPieces[1].Should().Contain("@timestamp"); 48 | bulkJsonPieces[2].Should().Contain(@"""_index"":""logstash-2013.05.30"); 49 | } 50 | } 51 | } 52 | 53 | } 54 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/RealExceptionNoSerializerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Serilog.Events; 7 | using Serilog.Parsing; 8 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.Tests 11 | { 12 | public class RealExceptionNoSerializerTests : ElasticsearchSinkTestsBase 13 | { 14 | [Fact] 15 | public async Task WhenPassingASerializer_ShouldExpandToJson() 16 | { 17 | try 18 | { 19 | await this.ThrowAsync(); 20 | } 21 | catch (Exception e) 22 | { 23 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 24 | var messageTemplate = "{Song}++"; 25 | var template = new MessageTemplateParser().Parse(messageTemplate); 26 | using (var sink = new ElasticsearchSink(_options) 27 | ) 28 | { 29 | var properties = new List 30 | { 31 | new LogEventProperty("Song", new ScalarValue("New Macabre")), 32 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2})) 33 | }; 34 | var logEvent = new LogEvent(timestamp, LogEventLevel.Information, null, template, properties); 35 | //one off 36 | sink.Emit(logEvent); 37 | 38 | sink.Emit(logEvent); 39 | logEvent = new LogEvent(timestamp.AddDays(2), LogEventLevel.Information, e, template, properties); 40 | sink.Emit(logEvent); 41 | } 42 | 43 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 44 | bulkJsonPieces[0].Should().Contain(@"""_index"":""logstash-2013.05.28"); 45 | bulkJsonPieces[1].Should().Contain("New Macabre"); 46 | bulkJsonPieces[1].Should().NotContain("Properties\""); 47 | bulkJsonPieces[2].Should().Contain(@"""_index"":""logstash-2013.05.30"); 48 | 49 | bulkJsonPieces[3].Should().Contain("Complex\":{"); 50 | bulkJsonPieces[3].Should().Contain("exceptions\":[{\"Depth\":0"); 51 | } 52 | } 53 | } 54 | 55 | } 56 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/RealExceptionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Threading.Tasks; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Serilog.Events; 7 | using Serilog.Parsing; 8 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 9 | 10 | namespace Serilog.Sinks.Elasticsearch.Tests 11 | { 12 | public class RealExceptionTests : ElasticsearchSinkTestsBase 13 | { 14 | [Fact] 15 | public async Task WhenPassingASerializer_ShouldExpandToJson() 16 | { 17 | try 18 | { 19 | await this.ThrowAsync(); 20 | } 21 | catch (Exception e) 22 | { 23 | var timestamp = new DateTimeOffset(2013, 05, 28, 22, 10, 20, 666, TimeSpan.FromHours(10)); 24 | var messageTemplate = "{Song}++"; 25 | var template = new MessageTemplateParser().Parse(messageTemplate); 26 | using (var sink = new ElasticsearchSink(_options)) 27 | { 28 | var properties = new List 29 | { 30 | new LogEventProperty("Song", new ScalarValue("New Macabre")), 31 | new LogEventProperty("Complex", new ScalarValue(new { A = 1, B = 2})) 32 | }; 33 | var logEvent = new LogEvent(timestamp, LogEventLevel.Information, null, template, properties); 34 | //one off 35 | sink.Emit(logEvent); 36 | 37 | sink.Emit(logEvent); 38 | logEvent = new LogEvent(timestamp.AddDays(2), LogEventLevel.Information, e, template, properties); 39 | sink.Emit(logEvent); 40 | } 41 | var bulkJsonPieces = this.AssertSeenHttpPosts(_seenHttpPosts, 4); 42 | bulkJsonPieces[0].Should().Contain(@"""_index"":""logstash-2013.05.28"); 43 | bulkJsonPieces[1].Should().Contain("New Macabre"); 44 | bulkJsonPieces[1].Should().NotContain("Properties\""); 45 | bulkJsonPieces[2].Should().Contain(@"""_index"":""logstash-2013.05.30"); 46 | 47 | //since we pass a serializer objects should serialize as json object and not using their 48 | //tostring implemenation 49 | //DO NOTE that you cant send objects as scalar values through Logger.*("{Scalar}", {}); 50 | bulkJsonPieces[3].Should().Contain("Complex\":{"); 51 | bulkJsonPieces[3].Should().Contain("exceptions\":[{"); 52 | } 53 | } 54 | } 55 | 56 | } 57 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/DiscoverVersionHandlesUnavailableServerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Serilog.Debugging; 7 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 10 | { 11 | [Collection("isolation")] 12 | public class DiscoverVersionHandlesUnavailableServerTests : ElasticsearchSinkTestsBase 13 | { 14 | [Fact] 15 | public void Should_not_crash_when_server_is_unavaiable() 16 | { 17 | // If this crashes, the test will fail 18 | CreateLoggerThatCrashes(); 19 | } 20 | 21 | [Fact] 22 | public void Should_write_error_to_self_log() 23 | { 24 | var selfLogMessages = new StringBuilder(); 25 | SelfLog.Enable(new StringWriter(selfLogMessages)); 26 | 27 | // Exception occurs on creation - should be logged 28 | CreateLoggerThatCrashes(); 29 | 30 | var selfLogContents = selfLogMessages.ToString(); 31 | selfLogContents.Should().Contain("Failed to discover the cluster version"); 32 | 33 | } 34 | 35 | private static ILogger CreateLoggerThatCrashes() 36 | { 37 | var loggerConfig = new LoggerConfiguration() 38 | .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9199")) 39 | { 40 | DetectElasticsearchVersion = true 41 | }); 42 | 43 | return loggerConfig.CreateLogger(); 44 | } 45 | } 46 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/DiscoverVersionTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class DiscoverVersionTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templateGet; 11 | 12 | public DiscoverVersionTests() 13 | { 14 | _options.DetectElasticsearchVersion = true; 15 | 16 | var loggerConfig = new LoggerConfiguration() 17 | .MinimumLevel.Debug() 18 | .Enrich.WithMachineName() 19 | .WriteTo.Console() 20 | .WriteTo.Elasticsearch(_options); 21 | 22 | var logger = loggerConfig.CreateLogger(); 23 | using ((IDisposable) logger) 24 | { 25 | logger.Error("Test exception. Should not contain an embedded exception object."); 26 | } 27 | 28 | this._seenHttpGets.Should().NotBeNullOrEmpty().And.HaveCount(1); 29 | _templateGet = this._seenHttpGets[0]; 30 | } 31 | 32 | 33 | [Fact] 34 | public void TemplatePutToCorrectUrl() 35 | { 36 | var uri = _templateGet.Item1; 37 | uri.AbsolutePath.Should().Be("/"); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/DoNotRegisterTemplateIfItExists.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class DoNotRegisterIfTemplateExistsTests : ElasticsearchSinkTestsBase 9 | { 10 | private void DoRegister() 11 | { 12 | _templateExistsReturnCode = 200; 13 | 14 | _options.AutoRegisterTemplate = true; 15 | var loggerConfig = new LoggerConfiguration() 16 | .MinimumLevel.Debug() 17 | .Enrich.WithMachineName() 18 | .WriteTo.Console() 19 | .WriteTo.Elasticsearch(_options); 20 | 21 | var logger = loggerConfig.CreateLogger(); 22 | using (logger as IDisposable) 23 | { 24 | logger.Error("Test exception. Should not contain an embedded exception object."); 25 | } 26 | } 27 | 28 | [Fact] 29 | public void WhenTemplateExists_ShouldNotSendAPutTemplate() 30 | { 31 | DoRegister(); 32 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 33 | this._seenHttpHeads.Should().NotBeNullOrEmpty().And.HaveCount(1); 34 | this._seenHttpPuts.Should().BeNullOrEmpty(); 35 | } 36 | } 37 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/OverwriteTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class OverwriteTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private void DoRegister() 11 | { 12 | _templateExistsReturnCode = 200; 13 | 14 | _options.AutoRegisterTemplate = true; 15 | _options.OverwriteTemplate = true; 16 | var loggerConfig = new LoggerConfiguration() 17 | .MinimumLevel.Debug() 18 | .Enrich.WithMachineName() 19 | .WriteTo.Console() 20 | .WriteTo.Elasticsearch(_options); 21 | 22 | var logger = loggerConfig.CreateLogger(); 23 | using (logger as IDisposable) 24 | { 25 | logger.Error("Test exception. Should not contain an embedded exception object."); 26 | } 27 | } 28 | 29 | [Fact] 30 | public void ShouldOverwriteTemplate() 31 | { 32 | DoRegister(); 33 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 34 | this._seenHttpHeads.Should().BeNullOrEmpty(); 35 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 36 | } 37 | 38 | 39 | } 40 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/RegisterCustomTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class RegisterCustomTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private const string CustomTemplateContent = @"{ template: ""my-custom-template-*"" }"; 11 | private readonly Tuple _templatePut; 12 | 13 | public RegisterCustomTemplateTests() 14 | { 15 | _options.AutoRegisterTemplate = true; 16 | _options.GetTemplateContent = () => CustomTemplateContent; 17 | var loggerConfig = new LoggerConfiguration() 18 | .MinimumLevel.Debug() 19 | .Enrich.WithMachineName() 20 | .WriteTo.Console() 21 | .WriteTo.Elasticsearch(_options); 22 | 23 | var logger = loggerConfig.CreateLogger(); 24 | using (logger as IDisposable) 25 | { 26 | logger.Error("Test exception. Should not contain an embedded exception object."); 27 | } 28 | 29 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 30 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | _templatePut = this._seenHttpPuts[0]; 32 | } 33 | 34 | [Fact] 35 | public void ShouldRegisterCustomTemplate() 36 | { 37 | this._templatePut.Item2.Should().BeEquivalentTo(CustomTemplateContent); 38 | } 39 | } 40 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SendsTemplateHandlesUnavailableServerTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Text; 4 | using FluentAssertions; 5 | using Xunit; 6 | using Serilog.Debugging; 7 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 8 | 9 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 10 | { 11 | [Collection("isolation")] 12 | public class SendsTemplateHandlesUnavailableServerTests : ElasticsearchSinkTestsBase 13 | { 14 | [Fact] 15 | public void Should_not_crash_when_server_is_unavailable() 16 | { 17 | // If this crashes, the test will fail 18 | CreateLoggerThatCrashes(); 19 | } 20 | 21 | [Fact] 22 | public void Should_write_error_to_self_log() 23 | { 24 | var selfLogMessages = new StringBuilder(); 25 | SelfLog.Enable(new StringWriter(selfLogMessages)); 26 | 27 | // Exception occurs on creation - should be logged 28 | CreateLoggerThatCrashes(); 29 | 30 | var selfLogContents = selfLogMessages.ToString(); 31 | selfLogContents.Should().Contain("Failed to create the template"); 32 | 33 | } 34 | 35 | private static ILogger CreateLoggerThatCrashes() 36 | { 37 | var loggerConfig = new LoggerConfiguration() 38 | .WriteTo.Elasticsearch(new ElasticsearchSinkOptions(new Uri("http://localhost:9199")) 39 | { 40 | AutoRegisterTemplate = true, 41 | TemplateName = "crash" 42 | }); 43 | 44 | return loggerConfig.CreateLogger(); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SendsTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class SendsTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public SendsTemplateTests() 13 | { 14 | _options.DetectElasticsearchVersion = false; 15 | _options.AutoRegisterTemplate = true; 16 | 17 | var loggerConfig = new LoggerConfiguration() 18 | .MinimumLevel.Debug() 19 | .Enrich.WithMachineName() 20 | .WriteTo.Console() 21 | .WriteTo.Elasticsearch(_options); 22 | 23 | var logger = loggerConfig.CreateLogger(); 24 | using (logger as IDisposable) 25 | { 26 | logger.Error("Test exception. Should not contain an embedded exception object."); 27 | } 28 | 29 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 30 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | _templatePut = this._seenHttpPuts[0]; 32 | } 33 | 34 | [Fact] 35 | public void ShouldRegisterTheVersion7TemplateOnRegistrationWhenDetectElasticsearchVersionFalse() 36 | { 37 | JsonEquals(_templatePut.Item2, "template_v7_no-aliases.json"); 38 | } 39 | 40 | [Fact] 41 | public void TemplatePutToCorrectUrl() 42 | { 43 | var uri = _templatePut.Item1; 44 | uri.AbsolutePath.Should().Be("/_template/serilog-events-template"); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/Sendsv6TemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class Sendsv6TemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public Sendsv6TemplateTests() 13 | : base("6.0.0") 14 | { 15 | _options.AutoRegisterTemplate = true; 16 | _options.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv6; 17 | 18 | var loggerConfig = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .Enrich.WithMachineName() 21 | .WriteTo.Console() 22 | .WriteTo.Elasticsearch(_options); 23 | 24 | var logger = loggerConfig.CreateLogger(); 25 | using (logger as IDisposable) 26 | { 27 | logger.Error("Test exception. Should not contain an embedded exception object."); 28 | } 29 | 30 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 32 | _templatePut = this._seenHttpPuts[0]; 33 | } 34 | 35 | [Fact] 36 | public void ShouldRegisterTheVersion6TemplateOnRegistrationWhenDetectedElasticsearchVersionIsV6() 37 | { 38 | JsonEquals(_templatePut.Item2, "template_v6.json"); 39 | } 40 | 41 | [Fact] 42 | public void TemplatePutToCorrectUrl() 43 | { 44 | var uri = _templatePut.Item1; 45 | uri.AbsolutePath.Should().Be("/_template/serilog-events-template"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/Sendsv7TemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class Sendsv7TemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public Sendsv7TemplateTests() 13 | : base("7.0.0") 14 | { 15 | _options.AutoRegisterTemplate = true; 16 | _options.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv7; 17 | _options.IndexAliases = new string[] { "logstash" }; 18 | 19 | var loggerConfig = new LoggerConfiguration() 20 | .MinimumLevel.Debug() 21 | .Enrich.WithMachineName() 22 | .WriteTo.Console() 23 | .WriteTo.Elasticsearch(_options); 24 | 25 | var logger = loggerConfig.CreateLogger(); 26 | using (logger as IDisposable) 27 | { 28 | logger.Error("Test exception. Should not contain an embedded exception object."); 29 | } 30 | 31 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 32 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 33 | _templatePut = this._seenHttpPuts[0]; 34 | } 35 | 36 | [Fact] 37 | public void ShouldRegisterTheVersion7TemplateOnRegistrationWhenDetectedElasticsearchVersionIsV7() 38 | { 39 | JsonEquals(_templatePut.Item2, "template_v7.json"); 40 | } 41 | 42 | [Fact] 43 | public void TemplatePutToCorrectUrl() 44 | { 45 | var uri = _templatePut.Item1; 46 | uri.AbsolutePath.Should().Be("/_template/serilog-events-template"); 47 | } 48 | } 49 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/Sendsv8TemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class Sendsv8TemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public Sendsv8TemplateTests() 13 | { 14 | _options.AutoRegisterTemplate = true; 15 | _options.AutoRegisterTemplateVersion = AutoRegisterTemplateVersion.ESv8; 16 | _options.IndexAliases = new string[] { "logstash" }; 17 | 18 | var loggerConfig = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .Enrich.WithMachineName() 21 | .WriteTo.Console() 22 | .WriteTo.Elasticsearch(_options); 23 | 24 | var logger = loggerConfig.CreateLogger(); 25 | using (logger as IDisposable) 26 | { 27 | logger.Error("Test exception. Should not contain an embedded exception object."); 28 | } 29 | 30 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 32 | _templatePut = this._seenHttpPuts[0]; 33 | } 34 | 35 | [Fact] 36 | public void ShouldRegisterTheVersion6TemplateOnRegistrationWhenDetectedElasticsearchVersionIsV8() 37 | { 38 | JsonEquals(_templatePut.Item2, "template_v8.json"); 39 | } 40 | 41 | [Fact] 42 | public void TemplatePutToCorrectUrl() 43 | { 44 | var uri = _templatePut.Item1; 45 | uri.AbsolutePath.Should().Be("/_index_template/serilog-events-template"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SetElasticsearchSinkOptions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class SetElasticsearchSinkOptions : ElasticsearchSinkTestsBase 9 | { 10 | 11 | [Fact] 12 | public void WhenCreatingOptions_NumberOfShardsInjected_NumberOfShardsAreSet() 13 | { 14 | var options = new ElasticsearchSinkOptions(new Uri("http://localhost:9100")) 15 | { 16 | AutoRegisterTemplate = true, 17 | 18 | NumberOfShards = 2, 19 | NumberOfReplicas = 0 20 | }; 21 | 22 | options.NumberOfReplicas.Should().Be(0); 23 | options.NumberOfShards.Should().Be(2); 24 | 25 | } 26 | } 27 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SetFiveReplicasInTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class SetFiveReplicasInTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public SetFiveReplicasInTemplateTests() 13 | { 14 | _options.AutoRegisterTemplate = true; 15 | _options.NumberOfReplicas = 5; 16 | 17 | var loggerConfig = new LoggerConfiguration() 18 | .MinimumLevel.Debug() 19 | .Enrich.WithMachineName() 20 | .WriteTo.Console() 21 | .WriteTo.Elasticsearch(_options); 22 | 23 | var logger = loggerConfig.CreateLogger(); 24 | using (logger as IDisposable) 25 | { 26 | logger.Error("Test exception. Should not contain an embedded exception object."); 27 | } 28 | 29 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 30 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | _templatePut = this._seenHttpPuts[0]; 32 | } 33 | 34 | [Fact] 35 | public void ShouldRegisterTheCorrectTemplateOnRegistration() 36 | { 37 | JsonEquals(_templatePut.Item2, "template_v8_no-aliases_5replicas.json"); 38 | } 39 | 40 | [Fact] 41 | public void TemplatePutToCorrectUrl() 42 | { 43 | var uri = _templatePut.Item1; 44 | uri.AbsolutePath.Should().Be("/_index_template/serilog-events-template"); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SetTwoShardsInTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class SetTwoShardsInTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public SetTwoShardsInTemplateTests() 13 | { 14 | _options.AutoRegisterTemplate = true; 15 | _options.NumberOfShards = 2; 16 | _options.NumberOfReplicas= 0; 17 | 18 | var loggerConfig = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .Enrich.WithMachineName() 21 | .WriteTo.Console() 22 | .WriteTo.Elasticsearch(_options); 23 | 24 | var logger = loggerConfig.CreateLogger(); 25 | using (logger as IDisposable) 26 | { 27 | logger.Error("Test exception. Should not contain an embedded exception object."); 28 | } 29 | 30 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 32 | _templatePut = this._seenHttpPuts[0]; 33 | } 34 | 35 | [Fact] 36 | public void ShouldRegisterTheCorrectTemplateOnRegistration() 37 | { 38 | JsonEquals(_templatePut.Item2, "template_v8_no-aliases_2shards.json"); 39 | } 40 | 41 | [Fact] 42 | public void TemplatePutToCorrectUrl() 43 | { 44 | var uri = _templatePut.Item1; 45 | uri.AbsolutePath.Should().Be("/_index_template/serilog-events-template"); 46 | } 47 | } 48 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/SetZeroReplicasInTemplateTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class SetZeroReplicasInTemplateTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public SetZeroReplicasInTemplateTests() 13 | { 14 | _options.AutoRegisterTemplate = true; 15 | _options.NumberOfReplicas = 0; 16 | 17 | var loggerConfig = new LoggerConfiguration() 18 | .MinimumLevel.Debug() 19 | .Enrich.WithMachineName() 20 | .WriteTo.Console() 21 | .WriteTo.Elasticsearch(_options); 22 | 23 | var logger = loggerConfig.CreateLogger(); 24 | using (logger as IDisposable) 25 | { 26 | logger.Error("Test exception. Should not contain an embedded exception object."); 27 | } 28 | 29 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 30 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | _templatePut = this._seenHttpPuts[0]; 32 | } 33 | 34 | [Fact] 35 | public void ShouldRegisterTheCorrectTemplateOnRegistration() 36 | { 37 | JsonEquals(_templatePut.Item2, "template_v8_no-aliases_0replicas.json"); 38 | } 39 | 40 | [Fact] 41 | public void TemplatePutToCorrectUrl() 42 | { 43 | var uri = _templatePut.Item1; 44 | uri.AbsolutePath.Should().Be("/_index_template/serilog-events-template"); 45 | } 46 | } 47 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/TemplateMatchTests.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using FluentAssertions; 3 | using Serilog.Sinks.Elasticsearch.Tests.Stubs; 4 | using Xunit; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests.Templating 7 | { 8 | public class TemplateMatchTests : ElasticsearchSinkTestsBase 9 | { 10 | private readonly Tuple _templatePut; 11 | 12 | public TemplateMatchTests() 13 | : base("7.0.0") 14 | { 15 | _options.AutoRegisterTemplate = true; 16 | _options.IndexFormat = "dailyindex-{0:yyyy.MM.dd}-mycompany"; 17 | _options.TemplateName = "dailyindex-logs-template"; 18 | var loggerConfig = new LoggerConfiguration() 19 | .MinimumLevel.Debug() 20 | .Enrich.WithMachineName() 21 | .WriteTo.Console() 22 | .WriteTo.Elasticsearch(_options); 23 | 24 | var logger = loggerConfig.CreateLogger(); 25 | using (logger as IDisposable) 26 | { 27 | logger.Error("Test exception. Should not contain an embedded exception object."); 28 | } 29 | 30 | this._seenHttpPosts.Should().NotBeNullOrEmpty().And.HaveCount(1); 31 | this._seenHttpPuts.Should().NotBeNullOrEmpty().And.HaveCount(1); 32 | this._seenHttpHeads.Should().NotBeNullOrEmpty().And.HaveCount(1); 33 | _templatePut = this._seenHttpPuts[0]; 34 | 35 | } 36 | 37 | [Fact] 38 | public void TemplatePutToCorrectUrl() 39 | { 40 | var uri = this._templatePut.Item1; 41 | uri.AbsolutePath.Should().Be("/_template/dailyindex-logs-template"); 42 | } 43 | 44 | [Fact] 45 | public void TemplateMatchShouldReflectConfiguredIndexFormat() 46 | { 47 | var json = this._templatePut.Item2; 48 | json.Should().Contain(@"""index_patterns"":[""dailyindex-*-mycompany""]"); 49 | } 50 | 51 | } 52 | } -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v6.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "settings": { 4 | "index.refresh_interval": "5s" 5 | }, 6 | "mappings": { 7 | "_default_": { 8 | "dynamic_templates": [ 9 | { 10 | "numerics_in_fields": { 11 | "path_match":"fields\\.[\\d+]$", 12 | "match_pattern":"regex", 13 | "mapping": { 14 | "type":"text", 15 | "index":true, 16 | "norms":false 17 | } 18 | } 19 | }, 20 | { 21 | "string_fields": { 22 | "match": "*", 23 | "match_mapping_type": "string", 24 | "mapping": { 25 | "type": "text", 26 | "index": true, 27 | "norms": false, 28 | "fields": { 29 | "raw": { 30 | "type": "keyword", 31 | "index": true, 32 | "ignore_above": 256 33 | } 34 | } 35 | } 36 | } 37 | } 38 | ], 39 | "properties": { 40 | "message": { 41 | "type": "text", 42 | "index": true 43 | }, 44 | "exceptions": { 45 | "type": "nested", 46 | "properties": { 47 | "Depth": { 48 | "type": "integer" 49 | }, 50 | "RemoteStackIndex": { 51 | "type": "integer" 52 | }, 53 | "HResult": { 54 | "type": "integer" 55 | }, 56 | "StackTraceString": { 57 | "type": "text", 58 | "index": true 59 | }, 60 | "RemoteStackTraceString": { 61 | "type": "text", 62 | "index": true 63 | }, 64 | "ExceptionMessage": { 65 | "type": "object", 66 | "properties": { 67 | "MemberType": { 68 | "type": "integer" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v7.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "settings": { 4 | "index.refresh_interval": "5s" 5 | }, 6 | "mappings": { 7 | "dynamic_templates": [ 8 | { 9 | "numerics_in_fields": { 10 | "path_match": "fields\\.[\\d+]$", 11 | "match_pattern": "regex", 12 | "mapping": { 13 | "type": "text", 14 | "index": true, 15 | "norms": false 16 | } 17 | } 18 | }, 19 | { 20 | "string_fields": { 21 | "match": "*", 22 | "match_mapping_type": "string", 23 | "mapping": { 24 | "type": "text", 25 | "index": true, 26 | "norms": false, 27 | "fields": { 28 | "raw": { 29 | "type": "keyword", 30 | "index": true, 31 | "ignore_above": 256 32 | } 33 | } 34 | } 35 | } 36 | } 37 | ], 38 | "properties": { 39 | "message": { 40 | "type": "text", 41 | "index": true 42 | }, 43 | "exceptions": { 44 | "type": "nested", 45 | "properties": { 46 | "Depth": { 47 | "type": "integer" 48 | }, 49 | "RemoteStackIndex": { 50 | "type": "integer" 51 | }, 52 | "HResult": { 53 | "type": "integer" 54 | }, 55 | "StackTraceString": { 56 | "type": "text", 57 | "index": true 58 | }, 59 | "RemoteStackTraceString": { 60 | "type": "text", 61 | "index": true 62 | }, 63 | "ExceptionMessage": { 64 | "type": "object", 65 | "properties": { 66 | "MemberType": { 67 | "type": "integer" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | "aliases": { 76 | "logstash": {} 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v7_no-aliases.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "settings": { 4 | "index.refresh_interval": "5s" 5 | }, 6 | "mappings": { 7 | "dynamic_templates": [ 8 | { 9 | "numerics_in_fields": { 10 | "path_match": "fields\\.[\\d+]$", 11 | "match_pattern": "regex", 12 | "mapping": { 13 | "type": "text", 14 | "index": true, 15 | "norms": false 16 | } 17 | } 18 | }, 19 | { 20 | "string_fields": { 21 | "match": "*", 22 | "match_mapping_type": "string", 23 | "mapping": { 24 | "type": "text", 25 | "index": true, 26 | "norms": false, 27 | "fields": { 28 | "raw": { 29 | "type": "keyword", 30 | "index": true, 31 | "ignore_above": 256 32 | } 33 | } 34 | } 35 | } 36 | } 37 | ], 38 | "properties": { 39 | "message": { 40 | "type": "text", 41 | "index": true 42 | }, 43 | "exceptions": { 44 | "type": "nested", 45 | "properties": { 46 | "Depth": { 47 | "type": "integer" 48 | }, 49 | "RemoteStackIndex": { 50 | "type": "integer" 51 | }, 52 | "HResult": { 53 | "type": "integer" 54 | }, 55 | "StackTraceString": { 56 | "type": "text", 57 | "index": true 58 | }, 59 | "RemoteStackTraceString": { 60 | "type": "text", 61 | "index": true 62 | }, 63 | "ExceptionMessage": { 64 | "type": "object", 65 | "properties": { 66 | "MemberType": { 67 | "type": "integer" 68 | } 69 | } 70 | } 71 | } 72 | } 73 | } 74 | }, 75 | "aliases": { 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v8.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "template": { 4 | "settings": { 5 | "index.refresh_interval": "5s" 6 | }, 7 | "mappings": { 8 | "dynamic_templates": [ 9 | { 10 | "numerics_in_fields": { 11 | "path_match": "fields\\.[\\d+]$", 12 | "match_pattern": "regex", 13 | "mapping": { 14 | "type": "text", 15 | "index": true, 16 | "norms": false 17 | } 18 | } 19 | }, 20 | { 21 | "string_fields": { 22 | "match": "*", 23 | "match_mapping_type": "string", 24 | "mapping": { 25 | "type": "text", 26 | "index": true, 27 | "norms": false, 28 | "fields": { 29 | "raw": { 30 | "type": "keyword", 31 | "index": true, 32 | "ignore_above": 256 33 | } 34 | } 35 | } 36 | } 37 | } 38 | ], 39 | "properties": { 40 | "message": { 41 | "type": "text", 42 | "index": true 43 | }, 44 | "exceptions": { 45 | "type": "nested", 46 | "properties": { 47 | "Depth": { 48 | "type": "integer" 49 | }, 50 | "RemoteStackIndex": { 51 | "type": "integer" 52 | }, 53 | "HResult": { 54 | "type": "integer" 55 | }, 56 | "StackTraceString": { 57 | "type": "text", 58 | "index": true 59 | }, 60 | "RemoteStackTraceString": { 61 | "type": "text", 62 | "index": true 63 | }, 64 | "ExceptionMessage": { 65 | "type": "object", 66 | "properties": { 67 | "MemberType": { 68 | "type": "integer" 69 | } 70 | } 71 | } 72 | } 73 | } 74 | } 75 | }, 76 | "aliases": { 77 | "logstash": {} 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v8_no-aliases_0replicas.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "template": { 4 | "settings": { 5 | "index.refresh_interval": "5s", 6 | "number_of_replicas": "0" 7 | }, 8 | "mappings": { 9 | "dynamic_templates": [ 10 | { 11 | "numerics_in_fields": { 12 | "path_match": "fields\\.[\\d+]$", 13 | "match_pattern": "regex", 14 | "mapping": { 15 | "type": "text", 16 | "index": true, 17 | "norms": false 18 | } 19 | } 20 | }, 21 | { 22 | "string_fields": { 23 | "match": "*", 24 | "match_mapping_type": "string", 25 | "mapping": { 26 | "type": "text", 27 | "index": true, 28 | "norms": false, 29 | "fields": { 30 | "raw": { 31 | "type": "keyword", 32 | "index": true, 33 | "ignore_above": 256 34 | } 35 | } 36 | } 37 | } 38 | } 39 | ], 40 | "properties": { 41 | "message": { 42 | "type": "text", 43 | "index": true 44 | }, 45 | "exceptions": { 46 | "type": "nested", 47 | "properties": { 48 | "Depth": { 49 | "type": "integer" 50 | }, 51 | "RemoteStackIndex": { 52 | "type": "integer" 53 | }, 54 | "HResult": { 55 | "type": "integer" 56 | }, 57 | "StackTraceString": { 58 | "type": "text", 59 | "index": true 60 | }, 61 | "RemoteStackTraceString": { 62 | "type": "text", 63 | "index": true 64 | }, 65 | "ExceptionMessage": { 66 | "type": "object", 67 | "properties": { 68 | "MemberType": { 69 | "type": "integer" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "aliases": { 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v8_no-aliases_2shards.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "template": { 4 | "settings": { 5 | "index.refresh_interval": "5s", 6 | "number_of_shards": "2", 7 | "number_of_replicas": "0" 8 | }, 9 | "mappings": { 10 | "dynamic_templates": [ 11 | { 12 | "numerics_in_fields": { 13 | "path_match": "fields\\.[\\d+]$", 14 | "match_pattern": "regex", 15 | "mapping": { 16 | "type": "text", 17 | "index": true, 18 | "norms": false 19 | } 20 | } 21 | }, 22 | { 23 | "string_fields": { 24 | "match": "*", 25 | "match_mapping_type": "string", 26 | "mapping": { 27 | "type": "text", 28 | "index": true, 29 | "norms": false, 30 | "fields": { 31 | "raw": { 32 | "type": "keyword", 33 | "index": true, 34 | "ignore_above": 256 35 | } 36 | } 37 | } 38 | } 39 | } 40 | ], 41 | "properties": { 42 | "message": { 43 | "type": "text", 44 | "index": true 45 | }, 46 | "exceptions": { 47 | "type": "nested", 48 | "properties": { 49 | "Depth": { 50 | "type": "integer" 51 | }, 52 | "RemoteStackIndex": { 53 | "type": "integer" 54 | }, 55 | "HResult": { 56 | "type": "integer" 57 | }, 58 | "StackTraceString": { 59 | "type": "text", 60 | "index": true 61 | }, 62 | "RemoteStackTraceString": { 63 | "type": "text", 64 | "index": true 65 | }, 66 | "ExceptionMessage": { 67 | "type": "object", 68 | "properties": { 69 | "MemberType": { 70 | "type": "integer" 71 | } 72 | } 73 | } 74 | } 75 | } 76 | } 77 | }, 78 | "aliases": { 79 | } 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/Templating/template_v8_no-aliases_5replicas.json: -------------------------------------------------------------------------------- 1 | { 2 | "index_patterns": [ "logstash-*" ], 3 | "template": { 4 | "settings": { 5 | "index.refresh_interval": "5s", 6 | "number_of_replicas": "5" 7 | }, 8 | "mappings": { 9 | "dynamic_templates": [ 10 | { 11 | "numerics_in_fields": { 12 | "path_match": "fields\\.[\\d+]$", 13 | "match_pattern": "regex", 14 | "mapping": { 15 | "type": "text", 16 | "index": true, 17 | "norms": false 18 | } 19 | } 20 | }, 21 | { 22 | "string_fields": { 23 | "match": "*", 24 | "match_mapping_type": "string", 25 | "mapping": { 26 | "type": "text", 27 | "index": true, 28 | "norms": false, 29 | "fields": { 30 | "raw": { 31 | "type": "keyword", 32 | "index": true, 33 | "ignore_above": 256 34 | } 35 | } 36 | } 37 | } 38 | } 39 | ], 40 | "properties": { 41 | "message": { 42 | "type": "text", 43 | "index": true 44 | }, 45 | "exceptions": { 46 | "type": "nested", 47 | "properties": { 48 | "Depth": { 49 | "type": "integer" 50 | }, 51 | "RemoteStackIndex": { 52 | "type": "integer" 53 | }, 54 | "HResult": { 55 | "type": "integer" 56 | }, 57 | "StackTraceString": { 58 | "type": "text", 59 | "index": true 60 | }, 61 | "RemoteStackTraceString": { 62 | "type": "text", 63 | "index": true 64 | }, 65 | "ExceptionMessage": { 66 | "type": "object", 67 | "properties": { 68 | "MemberType": { 69 | "type": "integer" 70 | } 71 | } 72 | } 73 | } 74 | } 75 | } 76 | }, 77 | "aliases": { 78 | } 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /test/Serilog.Sinks.Elasticsearch.Tests/TestDataHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using System.Linq; 4 | using System.Reflection; 5 | 6 | namespace Serilog.Sinks.Elasticsearch.Tests 7 | { 8 | public static class TestDataHelper 9 | { 10 | public static string ReadEmbeddedResource( 11 | Assembly assembly, 12 | string embeddedResourceNameEndsWith) 13 | { 14 | var resourceNames = assembly.GetManifestResourceNames(); 15 | var resourceName = resourceNames.SingleOrDefault(n => n.EndsWith(embeddedResourceNameEndsWith)); 16 | 17 | if (string.IsNullOrEmpty(resourceName)) 18 | { 19 | throw new ArgumentException( 20 | string.Format( 21 | "Could not find embedded resouce name that ends with '{0}', only found these: {1}", 22 | embeddedResourceNameEndsWith, 23 | string.Join(", ", resourceNames)), 24 | "embeddedResourceNameEndsWith"); 25 | } 26 | 27 | using (var stream = assembly.GetManifestResourceStream(resourceName)) 28 | { 29 | if (stream == null) 30 | { 31 | throw new ArgumentException( 32 | string.Format("Failed to open embedded resource stream for resource '{0}'", resourceName)); 33 | } 34 | 35 | using (var streamReader = new StreamReader(stream)) 36 | { 37 | return streamReader.ReadToEnd(); 38 | } 39 | } 40 | } 41 | } 42 | } 43 | --------------------------------------------------------------------------------