diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..e10c2dd --- /dev/null +++ b/.editorconfig @@ -0,0 +1,181 @@ +# To learn more about .editorconfig see https://aka.ms/editorconfigdocs +############################### +# Core EditorConfig Options # +############################### +# All files +[*] +indent_style = space +trim_trailing_whitespace = true + +[Caddyfile] +indent_size = tab + +# XML project files +[*.{csproj,vbproj,vcxproj,vcxproj.filters,proj,projitems,shproj,dcproj}] +indent_size = 2 + +# XML config files +[*.{props,targets,ruleset,config,nuspec,resx,vsixmanifest,vsct}] +indent_size = 2 + +# JSON config files +[*.{json,jsonc}] +indent_size = 2 +insert_final_newline = true + +# YAML config files +[*.{yml,yaml}] +indent_size = 2 +insert_final_newline = true + +# Code files +[*.{cs,csx,vb,vbx}] +indent_size = 4 +insert_final_newline = true +charset = utf-8-bom + +# Markdown +[*.{md, mmd}] +indent_size = 4 +insert_final_newline = true +trim_trailing_whitespace = false + +############################### +# .NET Coding Conventions # +############################### +[*.{cs,vb}] +# Instance fields are camelCase and start with _ +dotnet_naming_rule.instance_fields_should_be_camel_case.severity = suggestion +dotnet_naming_rule.instance_fields_should_be_camel_case.symbols = instance_fields +dotnet_naming_rule.instance_fields_should_be_camel_case.style = instance_field_style +dotnet_naming_symbols.instance_fields.applicable_kinds = field +dotnet_naming_style.instance_field_style.capitalization = camel_case +dotnet_naming_style.instance_field_style.required_prefix = _ +# Organize usings +dotnet_sort_system_directives_first = true +# this. preferences +dotnet_style_qualification_for_field = false:silent +dotnet_style_qualification_for_property = false:silent +dotnet_style_qualification_for_method = false:silent +dotnet_style_qualification_for_event = false:silent +# Language keywords vs BCL types preferences +dotnet_style_predefined_type_for_locals_parameters_members = true:silent +dotnet_style_predefined_type_for_member_access = true:silent +# Parentheses preferences +dotnet_style_parentheses_in_arithmetic_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_relational_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_binary_operators = always_for_clarity:silent +dotnet_style_parentheses_in_other_operators = never_if_unnecessary:silent +# Modifier preferences +dotnet_style_require_accessibility_modifiers = for_non_interface_members:silent +dotnet_style_readonly_field = true:suggestion +# Expression-level preferences +dotnet_style_object_initializer = true:suggestion +dotnet_style_collection_initializer = true:suggestion +dotnet_style_explicit_tuple_names = true:suggestion +dotnet_style_null_propagation = true:suggestion +dotnet_style_coalesce_expression = true:suggestion +dotnet_style_prefer_is_null_check_over_reference_equality_method = true:silent +dotnet_style_prefer_inferred_tuple_names = true:suggestion +dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion +dotnet_style_prefer_auto_properties = true:silent +dotnet_style_prefer_conditional_expression_over_assignment = true:silent +dotnet_style_prefer_conditional_expression_over_return = true:silent +############################### +# Naming Conventions # +############################### +# Style Definitions +dotnet_naming_style.pascal_case_style.capitalization = pascal_case +# Use PascalCase for constant fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.severity = suggestion +dotnet_naming_rule.constant_fields_should_be_pascal_case.symbols = constant_fields +dotnet_naming_rule.constant_fields_should_be_pascal_case.style = pascal_case_style +dotnet_naming_symbols.constant_fields.applicable_kinds = field +dotnet_naming_symbols.constant_fields.applicable_accessibilities = * +dotnet_naming_symbols.constant_fields.required_modifiers = const +dotnet_style_prefer_simplified_boolean_expressions = true:suggestion +dotnet_style_operator_placement_when_wrapping = beginning_of_line +tab_width = 4 +end_of_line = crlf +dotnet_style_prefer_compound_assignment = true:suggestion +dotnet_style_prefer_simplified_interpolation = true:suggestion +dotnet_style_namespace_match_folder = true:suggestion +dotnet_style_prefer_collection_expression = true:suggestion +############################### +# C# Coding Conventions # +############################### +[*.cs] +# var preferences +csharp_style_var_for_built_in_types = true:silent +csharp_style_var_when_type_is_apparent = true:silent +csharp_style_var_elsewhere = true:silent +# Expression-bodied members +csharp_style_expression_bodied_methods = false:silent +csharp_style_expression_bodied_constructors = false:silent +csharp_style_expression_bodied_operators = false:silent +csharp_style_expression_bodied_properties = true:silent +csharp_style_expression_bodied_indexers = true:silent +csharp_style_expression_bodied_accessors = true:silent +# Pattern matching preferences +csharp_style_pattern_matching_over_is_with_cast_check = true:suggestion +csharp_style_pattern_matching_over_as_with_null_check = true:suggestion +# Null-checking preferences +csharp_style_throw_expression = true:suggestion +csharp_style_conditional_delegate_call = true:suggestion +# Modifier preferences +csharp_preferred_modifier_order = public,private,protected,internal,static,extern,new,virtual,abstract,sealed,override,readonly,unsafe,volatile,async:suggestion +# Expression-level preferences +csharp_prefer_braces = true:silent +csharp_style_deconstructed_variable_declaration = true:suggestion +csharp_prefer_simple_default_expression = true:suggestion +csharp_style_pattern_local_over_anonymous_function = true:suggestion +csharp_style_inlined_variable_declaration = true:suggestion +############################### +# C# Formatting Rules # +############################### +# New line preferences +csharp_new_line_before_open_brace = all +csharp_new_line_before_else = true +csharp_new_line_before_catch = true +csharp_new_line_before_finally = true +csharp_new_line_before_members_in_object_initializers = true +csharp_new_line_before_members_in_anonymous_types = true +csharp_new_line_between_query_expression_clauses = true +# Indentation preferences +csharp_indent_case_contents = true +csharp_indent_switch_labels = true +csharp_indent_labels = flush_left +# Space preferences +csharp_space_after_cast = false +csharp_space_after_keywords_in_control_flow_statements = true +csharp_space_between_method_call_parameter_list_parentheses = false +csharp_space_between_method_declaration_parameter_list_parentheses = false +csharp_space_between_parentheses = false +csharp_space_before_colon_in_inheritance_clause = true +csharp_space_after_colon_in_inheritance_clause = true +csharp_space_around_binary_operators = before_and_after +csharp_space_between_method_declaration_empty_parameter_list_parentheses = false +csharp_space_between_method_call_name_and_opening_parenthesis = false +csharp_space_between_method_call_empty_parameter_list_parentheses = false +# Wrapping preferences +csharp_preserve_single_line_statements = true +csharp_preserve_single_line_blocks = true +csharp_using_directive_placement = outside_namespace:suggestion +csharp_prefer_simple_using_statement = true:suggestion +csharp_style_namespace_declarations = file_scoped:suggestion +csharp_style_expression_bodied_lambdas = true:silent +csharp_style_expression_bodied_local_functions = true:silent +csharp_style_prefer_null_check_over_type_check = true:suggestion +csharp_style_prefer_method_group_conversion = true:silent +csharp_style_prefer_top_level_statements = true:silent +csharp_style_prefer_local_over_anonymous_function = true:suggestion +csharp_style_prefer_index_operator = true:suggestion +csharp_style_prefer_range_operator = true:suggestion +csharp_style_implicit_object_creation_when_type_is_apparent = true:suggestion +csharp_style_prefer_primary_constructors = true:suggestion +############################### +# VB Coding Conventions # +############################### +[*.vb] +# Modifier preferences +visual_basic_preferred_modifier_order = Partial,Default,Private,Protected,Public,Friend,NotOverridable,Overridable,MustOverride,Overloads,Overrides,MustInherit,NotInheritable,Static,Shared,Shadows,ReadOnly,WriteOnly,Dim,Const,WithEvents,Widening,Narrowing,Custom,Async:suggestion diff --git a/CHANGES.md b/CHANGES.md index b76714a..f606b53 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -69,7 +69,7 @@ - Event Collector fluent interface changed to `.WriteTo.EventCollector` - Event Collector Sink targeting core - TCP/UDP Sinks targeting 4.5 *ONLY* - - Updated Event Collector HTTP Client to add URI endpoint to host: "services/collector" if not included. + - Updated Event Collector HTTP Client to add URI endpoint to host: "services/collector/event" if not included. - Event Collector changed to use epoch time [#15](https://github.com/serilog/serilog-sinks-splunk/pull/15) ## 1.8 diff --git a/Create-DockerfileSolutionRestore.ps1 b/Create-DockerfileSolutionRestore.ps1 new file mode 100644 index 0000000..bd52768 --- /dev/null +++ b/Create-DockerfileSolutionRestore.ps1 @@ -0,0 +1,32 @@ +param ( + [string]$solution = "serilog-sinks-splunk.sln" +) + +$outfile = "DockerfileSolutionRestore.txt" + +# This script creates the $outfile file, with Dockerfile commands to restore all the packages for the solution, +# so you can insert them (by hand) into Dockerfiles right before the "COPY . ." line +# to increase build speed by optimizing the use of docker build images cache. + +# This script is only needed when adding or removing projects from the solution. + +Write-Output "" > $outfile +Add-Content -Path $outfile "# Create this ""restore-solution"" section by running ./Create-DockerfileSolutionRestore.ps1, to optimize build cache reuse" +Select-String -Path $solution -Pattern ', "(.*?\.csproj)"' | ForEach-Object { $_.Matches.Groups[1].Value.Replace("\", "/") } | Sort-Object | ForEach-Object {"COPY [""$_"", """ + $_.Substring(0, $_.LastIndexOf("/") + 1) + """]"} | Out-File -FilePath $outfile -Append +Add-Content -Path $outfile "COPY [""docker-compose.dcproj"", ""./""]" +Add-Content -Path $outfile "COPY [""$solution"", ""./""]" +Add-Content -Path $outfile "RUN dotnet restore ""$solution""" +Add-Content -Path $outfile "" + + +Add-Content -Path $outfile "# Docker Compose Paths" + +Get-ChildItem -Path "./" -Recurse -Filter "Dockerfile" | + Resolve-Path -Relative | + ForEach-Object { $_.Replace("\", "/") } + Sort-Object | + ForEach-Object {" ""$_"""} | +Out-File -FilePath $outfile -Append + + +Get-Content $outfile diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index ac43bb8..0000000 --- a/Dockerfile +++ /dev/null @@ -1,10 +0,0 @@ -FROM microsoft/dotnet:8.0-sdk AS build -ADD . / -WORKDIR /sample/Sample -RUN dotnet restore -RUN dotnet publish -c Release -o out -f net8.0 - -FROM microsoft/dotnet:8.0-runtime AS runtime -WORKDIR /sample/Sample -COPY --from=build /sample/Sample/out ./ -ENTRYPOINT ["dotnet", "Sample.dll"] \ No newline at end of file diff --git a/DockerfileSolutionRestore.txt b/DockerfileSolutionRestore.txt new file mode 100644 index 0000000..a783374 --- /dev/null +++ b/DockerfileSolutionRestore.txt @@ -0,0 +1,13 @@ + +# Create this "restore-solution" section by running ./Create-DockerfileSolutionRestore.ps1, to optimize build cache reuse +COPY ["sample/Sample/Sample.csproj", "sample/Sample/"] +COPY ["src/Serilog.Sinks.Splunk/Serilog.Sinks.Splunk.csproj", "src/Serilog.Sinks.Splunk/"] +COPY ["src/Serilog.Sinks.TCP/Serilog.Sinks.Splunk.TCP.csproj", "src/Serilog.Sinks.TCP/"] +COPY ["src/Serilog.Sinks.UDP/Serilog.Sinks.Splunk.UDP.csproj", "src/Serilog.Sinks.UDP/"] +COPY ["test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj", "test/Serilog.Sinks.Splunk.Tests/"] +COPY ["docker-compose.dcproj", "./"] +COPY ["nuget.config", "./"] +COPY ["serilog-sinks-splunk.sln", "./"] +RUN dotnet restore "serilog-sinks-splunk.sln" + +# Docker Compose Paths diff --git a/docker-compose.dcproj b/docker-compose.dcproj new file mode 100644 index 0000000..a21fe1d --- /dev/null +++ b/docker-compose.dcproj @@ -0,0 +1,17 @@ + + + + 2.1 + Linux + 1b9defa3-d600-45fa-93a5-79006076fb5c + serilogsinkssplunk + + + + + docker-compose.yml + + + + + \ No newline at end of file diff --git a/docker-compose.override.yml b/docker-compose.override.yml new file mode 100644 index 0000000..6c2a928 --- /dev/null +++ b/docker-compose.override.yml @@ -0,0 +1,13 @@ +version: '3.4' +#https://learn.microsoft.com/en-us/visualstudio/containers/docker-compose-properties + +services: + splunk: + ports: + - 8000:8000 + - 8088:8088 + - 8089:8089 + + sampleconsoleapp: + ports: + - 8080:8080 diff --git a/docker-compose.yml b/docker-compose.yml index db99c14..7639e45 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,25 +1,22 @@ version: '3' services: splunk: - build: ./sample/splunk - image: serilog-splunk + container_name: splunk + build: + context: . + dockerfile: sample/splunk/Dockerfile + volumes: + - "./sample/splunk/default.yml:/tmp/defaults/default.yml" environment: - SPLUNK_START_ARGS: --accept-license --answer-yes --seed-passwd changeme - SPLUNK_ENABLE_LISTEN: 9997 - SPLUNK_PASSWORD: changemeplease! - ports: - - 8000:8000 - - 8088:8088 - - 8089:8089 - networks: - splunkbase_docker: + SPLUNK_START_ARGS: --accept-license --answer-yes --seed-passwd changeme + SPLUNK_ENABLE_LISTEN: 9997 + SPLUNK_PASSWORD: changemeplease! + SPLUNK_HEC_TOKEN: 00112233-4455-6677-8899-AABBCCDDEEFF + sampleconsoleapp: + container_name: sample depends_on: - "splunk" - build: . - image: serilog-console-sample - networks: - splunkbase_docker: -networks: - splunkbase_docker: - + build: + context: . + dockerfile: sample/Sample/Dockerfile diff --git a/sample/Sample/Dockerfile b/sample/Sample/Dockerfile new file mode 100644 index 0000000..a23ba1c --- /dev/null +++ b/sample/Sample/Dockerfile @@ -0,0 +1,31 @@ +#See https://aka.ms/customizecontainer to learn how to customize your debug container and how Visual Studio uses this Dockerfile to build your images for faster debugging. +FROM mcr.microsoft.com/dotnet/runtime:8.0 AS base +WORKDIR /app +EXPOSE 8080 + +FROM mcr.microsoft.com/dotnet/sdk:8.0 AS build +ARG BUILD_CONFIGURATION=Release +WORKDIR /src + +# Create this "restore-solution" section by running ./Create-DockerfileSolutionRestore.ps1, to optimize build cache reuse +COPY ["sample/Sample/Sample.csproj", "sample/Sample/"] +COPY ["src/Serilog.Sinks.Splunk/Serilog.Sinks.Splunk.csproj", "src/Serilog.Sinks.Splunk/"] +COPY ["src/Serilog.Sinks.TCP/Serilog.Sinks.Splunk.TCP.csproj", "src/Serilog.Sinks.TCP/"] +COPY ["src/Serilog.Sinks.UDP/Serilog.Sinks.Splunk.UDP.csproj", "src/Serilog.Sinks.UDP/"] +COPY ["test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj", "test/Serilog.Sinks.Splunk.Tests/"] +COPY ["docker-compose.dcproj", "./"] +COPY ["serilog-sinks-splunk.sln", "./"] +RUN dotnet restore "serilog-sinks-splunk.sln" + +COPY . . +WORKDIR "/src/sample/Sample/" +RUN dotnet build "./Sample.csproj" -c $BUILD_CONFIGURATION -o /app/build + +FROM build AS publish +ARG BUILD_CONFIGURATION=Release +RUN dotnet publish "./Sample.csproj" -c $BUILD_CONFIGURATION -o /app/publish /p:UseAppHost=false + +FROM base AS final +WORKDIR /app +COPY --from=publish /app/publish . +ENTRYPOINT ["dotnet", "Sample.dll"] \ No newline at end of file diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 1f1dfd9..459c3a7 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading.Tasks; namespace Sample { @@ -15,7 +16,7 @@ public class Program const string SPLUNK_HEC_TOKEN = "00112233-4455-6677-8899-AABBCCDDEEFF"; // Your HEC token. See http://docs.splunk.com/Documentation/Splunk/latest/Data/UsetheHTTPEventCollector public static string EventCollectorToken = SPLUNK_HEC_TOKEN; - public static void Main(string[] args) + public static async Task Main(string[] args) { // Bootstrap a simple logger. var logger = new LoggerConfiguration() @@ -27,9 +28,9 @@ public static void Main(string[] args) logger.Information("Sample app starting up..."); logger.Information("Startup arguments \"{Arguments}\".", args); - var eventsToCreate = 100; + var eventsToCreate = 10; var runSSL = false; - var millisecsToWait = 60000; + var secToWait = 30; if (args.Length > 0) eventsToCreate = int.Parse(args[0]); @@ -38,7 +39,7 @@ public static void Main(string[] args) runSSL = bool.Parse(args[1]); if (args.Length == 3) - millisecsToWait = int.Parse(args[2]); + secToWait = int.Parse(args[2]); Serilog.Debugging.SelfLog.Enable(msg => { @@ -46,8 +47,18 @@ public static void Main(string[] args) throw new Exception("Failed to write to Serilog.", new Exception(msg)); }); - logger.Information("Waiting {MillisecondsToWait} millisecs...", millisecsToWait); - System.Threading.Thread.Sleep(millisecsToWait); + + while (secToWait-- > 0) + { + logger.Information("Waiting {secToWait} seconds...", secToWait); + await Task.Delay(1000); + } + + logger.Information("Creating logger {MethodName}.", nameof(OverridingSubsecondPrecisionMicroseconds)); + OverridingSubsecondPrecisionMicroseconds(eventsToCreate); + + logger.Information("Creating logger {MethodName}.", nameof(OverridingSubsecondPrecisionNanoseconds)); + OverridingSubsecondPrecisionNanoseconds(eventsToCreate); logger.Information("Creating logger {MethodName}.", nameof(UsingAppSettingsJson)); UsingAppSettingsJson(eventsToCreate); @@ -131,6 +142,50 @@ private static void WithCompactSplunkFormatter(int eventsToCreate) Log.CloseAndFlush(); } + public static void OverridingSubsecondPrecisionMicroseconds(int eventsToCreate) + { + // Override Source + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.EventCollector( + SPLUNK_ENDPOINT, + Program.EventCollectorToken, + subSecondPrecision: SubSecondPrecision.Microseconds) + .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample", "ViaEventCollector") + .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "Source Override") + .CreateLogger(); + + foreach (var i in Enumerable.Range(0, eventsToCreate)) + { + Log.Information("Running source override loop {Counter} subseconds: {subsecondPrecision}", i, SubSecondPrecision.Microseconds.ToString()); + } + + Log.CloseAndFlush(); + } + + public static void OverridingSubsecondPrecisionNanoseconds(int eventsToCreate) + { + // Override Source + Log.Logger = new LoggerConfiguration() + .MinimumLevel.Debug() + .WriteTo.Console() + .WriteTo.EventCollector( + SPLUNK_ENDPOINT, + Program.EventCollectorToken, + subSecondPrecision: SubSecondPrecision.Nanoseconds) + .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample", "ViaEventCollector") + .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "Source Override") + .CreateLogger(); + + foreach (var i in Enumerable.Range(0, eventsToCreate)) + { + Log.Information("Running source override loop {Counter} subseconds: {subsecondPrecision}", i, SubSecondPrecision.Nanoseconds.ToString()); + } + + Log.CloseAndFlush(); + } + public static void OverridingSource(int eventsToCreate) { // Override Source @@ -229,7 +284,7 @@ public static void UsingHostOnly(int eventsToCreate) SPLUNK_ENDPOINT, Program.EventCollectorToken) .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample", "ViaEventCollector") - .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "Vanilla No services/collector in uri") + .Enrich.WithProperty("Serilog.Sinks.Splunk.Sample.TestType", "Vanilla No services/collector/event in uri") .CreateLogger(); foreach (var i in Enumerable.Range(0, eventsToCreate)) diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index 0521562..6fd1eb4 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -16,7 +16,7 @@ - + PreserveNewest diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index 70ed38e..7fab711 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -21,4 +21,4 @@ "Serilog.Sinks.Splunk.Sample.TestType": "AppSettings.json" } } -} \ No newline at end of file +} diff --git a/sample/splunk/Dockerfile b/sample/splunk/Dockerfile index 9f46425..f006f84 100644 --- a/sample/splunk/Dockerfile +++ b/sample/splunk/Dockerfile @@ -1,2 +1,2 @@ FROM splunk/splunk:latest -ADD etc ${SPLUNK_HOME}/etc \ No newline at end of file +ADD sample/splunk/etc ${SPLUNK_HOME}/etc \ No newline at end of file diff --git a/sample/splunk/default.yml b/sample/splunk/default.yml new file mode 100644 index 0000000..c344bef --- /dev/null +++ b/sample/splunk/default.yml @@ -0,0 +1,7 @@ +splunk: + hec: + enable: True + ssl: false + port: 8088 + # hec.token is used only for ingestion (receiving Splunk events) + token: 00112233-4455-6677-8899-AABBCCDDEEFF diff --git a/sample/splunk/etc/apps/splunk_httpinput/local/inputs.conf b/sample/splunk/etc/apps/splunk_httpinput/local/inputs.conf index ff47956..a65d8a1 100644 --- a/sample/splunk/etc/apps/splunk_httpinput/local/inputs.conf +++ b/sample/splunk/etc/apps/splunk_httpinput/local/inputs.conf @@ -2,5 +2,5 @@ disabled = 0 enableSSL = 0 -[http://serilog_sample] +[http://splunk-sample] token = 00112233-4455-6677-8899-AABBCCDDEEFF \ No newline at end of file diff --git a/sample/splunk/etc/system/local/props.conf b/sample/splunk/etc/system/local/props.conf new file mode 100644 index 0000000..665e07b --- /dev/null +++ b/sample/splunk/etc/system/local/props.conf @@ -0,0 +1,4 @@ +TIME_PREFIX = \"time\"\:\s*\" +TIME_FORMAT = %Y-%m-%d %H:%M:%S.%9N +ADD_EXTRA_TIME_FIELDS = all +MAX_TIMESTAMP_LOOKAHEAD = 30 \ No newline at end of file diff --git a/serilog-sinks-splunk.sln b/serilog-sinks-splunk.sln index ba502cd..c1f15b4 100644 --- a/serilog-sinks-splunk.sln +++ b/serilog-sinks-splunk.sln @@ -1,12 +1,13 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26430.12 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34714.143 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{7A774CBB-A6E9-4854-B4DB-4CF860B0C1C5}" EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{B9B13339-749C-4098-8845-780ED4FA488A}" ProjectSection(SolutionItems) = preProject + .editorconfig = .editorconfig .travis.yml = .travis.yml appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 @@ -20,15 +21,22 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sample", "sample", "{1C75E4 EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{B9451AD8-09B9-4C09-A152-FBAE24806614}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Splunk", "src\Serilog.Sinks.Splunk\Serilog.Sinks.Splunk.csproj", "{32CF915C-3ECD-496C-8FDB-1214581274A6}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Splunk", "src\Serilog.Sinks.Splunk\Serilog.Sinks.Splunk.csproj", "{32CF915C-3ECD-496C-8FDB-1214581274A6}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Splunk.Tests", "test\Serilog.Sinks.Splunk.Tests\Serilog.Sinks.Splunk.Tests.csproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Splunk.Tests", "test\Serilog.Sinks.Splunk.Tests\Serilog.Sinks.Splunk.Tests.csproj", "{3C2D8E01-5580-426A-BDD9-EC59CD98E618}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Sample", "sample\Sample\Sample.csproj", "{4A4E361D-8BBE-4DDD-9E6C-53960C2B5F5B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sample.csproj", "{4A4E361D-8BBE-4DDD-9E6C-53960C2B5F5B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Splunk.TCP", "src\Serilog.Sinks.TCP\Serilog.Sinks.Splunk.TCP.csproj", "{F74FCFD0-536B-4311-AA66-0BD16112D895}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Splunk.TCP", "src\Serilog.Sinks.TCP\Serilog.Sinks.Splunk.TCP.csproj", "{F74FCFD0-536B-4311-AA66-0BD16112D895}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Serilog.Sinks.Splunk.UDP", "src\Serilog.Sinks.UDP\Serilog.Sinks.Splunk.UDP.csproj", "{FE1504A6-5444-4B87-819C-E6F477662B7F}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Serilog.Sinks.Splunk.UDP", "src\Serilog.Sinks.UDP\Serilog.Sinks.Splunk.UDP.csproj", "{FE1504A6-5444-4B87-819C-E6F477662B7F}" +EndProject +Project("{E53339B2-1760-4266-BCC7-CA923CBCF16C}") = "docker-compose", "docker-compose.dcproj", "{1B9DEFA3-D600-45FA-93A5-79006076FB5C}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "splunk", "splunk", "{21EEF50A-C0FC-4406-97A1-8F5F499AE2FC}" + ProjectSection(SolutionItems) = preProject + sample\splunk\Dockerfile = sample\splunk\Dockerfile + EndProjectSection EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -56,6 +64,10 @@ Global {FE1504A6-5444-4B87-819C-E6F477662B7F}.Debug|Any CPU.Build.0 = Debug|Any CPU {FE1504A6-5444-4B87-819C-E6F477662B7F}.Release|Any CPU.ActiveCfg = Release|Any CPU {FE1504A6-5444-4B87-819C-E6F477662B7F}.Release|Any CPU.Build.0 = Release|Any CPU + {1B9DEFA3-D600-45FA-93A5-79006076FB5C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B9DEFA3-D600-45FA-93A5-79006076FB5C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1B9DEFA3-D600-45FA-93A5-79006076FB5C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1B9DEFA3-D600-45FA-93A5-79006076FB5C}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -66,5 +78,9 @@ Global {4A4E361D-8BBE-4DDD-9E6C-53960C2B5F5B} = {1C75E4A9-4CB1-497C-AD17-B438882051A1} {F74FCFD0-536B-4311-AA66-0BD16112D895} = {7A774CBB-A6E9-4854-B4DB-4CF860B0C1C5} {FE1504A6-5444-4B87-819C-E6F477662B7F} = {7A774CBB-A6E9-4854-B4DB-4CF860B0C1C5} + {21EEF50A-C0FC-4406-97A1-8F5F499AE2FC} = {1C75E4A9-4CB1-497C-AD17-B438882051A1} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {D7BFF439-D18D-4124-A36F-15CFB8E84BCC} EndGlobalSection EndGlobal diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/CompactSplunkJsonFormatter.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/CompactSplunkJsonFormatter.cs index 79132fa..7b56f49 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/CompactSplunkJsonFormatter.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/CompactSplunkJsonFormatter.cs @@ -31,6 +31,7 @@ public class CompactSplunkJsonFormatter : ITextFormatter private static readonly JsonValueFormatter ValueFormatter = new JsonValueFormatter(typeTagName: "$type"); private readonly string _suffix; private readonly bool _renderTemplate; + private readonly SubSecondPrecision _subSecondPrecision; /// /// Construct a . @@ -40,9 +41,19 @@ public class CompactSplunkJsonFormatter : ITextFormatter /// The host of the event /// The Splunk index to log to /// If true, the template used will be rendered and written to the output as a property named MessageTemplate - public CompactSplunkJsonFormatter(bool renderTemplate = false, string source = null, string sourceType = null, string host = null, string index = null) + /// Timestamp sub-second precision. Splunk props.conf setup is required. + + public CompactSplunkJsonFormatter( + bool renderTemplate = false, + string source = null, + string sourceType = null, + string host = null, + string index = null, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) { _renderTemplate = renderTemplate; + _subSecondPrecision = subSecondPrecision; + var suffixWriter = new StringWriter(); suffixWriter.Write("}"); // Terminates "event" @@ -80,7 +91,7 @@ public void Format(LogEvent logEvent, TextWriter output) if (output == null) throw new ArgumentNullException(nameof(output)); output.Write("{\"time\":\""); - output.Write(logEvent.Timestamp.ToEpoch().ToString(CultureInfo.InvariantCulture)); + output.Write(logEvent.Timestamp.ToEpoch(_subSecondPrecision)); output.Write("\",\"event\":{\"@l\":\""); output.Write(logEvent.Level); output.Write('"'); @@ -134,4 +145,4 @@ public void Format(LogEvent logEvent, TextWriter output) output.WriteLine(_suffix); } } -} \ No newline at end of file +} diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/Epoch.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/Epoch.cs index 0f4c39b..1d85a2d 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/Epoch.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/Epoch.cs @@ -1,4 +1,4 @@ -// Copyright 2016 Serilog Contributors +// Copyright 2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -15,20 +15,49 @@ namespace Serilog.Sinks.Splunk { using System; + using System.Globalization; - internal static class EpochExtensions + /// + /// Provides extension methods for DateTimeOffset to convert to epoch time. + /// + public static class EpochExtensions { - private static DateTimeOffset Epoch = new DateTimeOffset(1970,1,1,0,0,0,TimeSpan.Zero); + private static DateTimeOffset Epoch = new DateTimeOffset(1970, 1, 1, 0, 0, 0, TimeSpan.Zero); + const string MillisecondFormat = "#.000"; + const string MicrosecondFormat = "#.000000"; + const string NanosecondFormat = "#.000000000"; - public static double ToEpoch(this DateTimeOffset value) + /// + /// Converts a DateTimeOffset value to epoch time with specified sub-second precision. + /// + /// The DateTimeOffset value to convert. + /// Timestamp sub-second precision. + /// The epoch time representation of the DateTimeOffset value. + public static string ToEpoch(this DateTimeOffset value, SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) { // From Splunk HTTP Collector Protocol // The default time format is epoch time format, in the format .. // For example, 1433188255.500 indicates 1433188255 seconds and 500 milliseconds after epoch, // or Monday, June 1, 2015, at 7:50:55 PM GMT. - // See: http://dev.splunk.com/view/SP-CAAAE6P + // See: https://docs.splunk.com/Documentation/Splunk/9.2.0/SearchReference/Commontimeformatvariables - return Math.Round((value - Epoch).TotalSeconds, 3, MidpointRounding.AwayFromZero); + var totalSeconds = ToSeconds(value.Ticks - Epoch.Ticks); + var format = subSecondPrecision switch { + SubSecondPrecision.Nanoseconds => NanosecondFormat, + SubSecondPrecision.Microseconds => MicrosecondFormat, + _ => MillisecondFormat, + }; + + return Math.Round(totalSeconds, (int)subSecondPrecision, MidpointRounding.AwayFromZero).ToString(format, CultureInfo.InvariantCulture); + } + + private static decimal ToSeconds(long ticks) + { + long wholeSecondPortion = (ticks / TimeSpan.TicksPerSecond) * TimeSpan.TicksPerSecond; + long subsecondPortion = ticks - wholeSecondPortion; + decimal wholeSeconds = wholeSecondPortion / (decimal)TimeSpan.TicksPerSecond; + decimal subseconds = subsecondPortion / (decimal)TimeSpan.TicksPerSecond; + return wholeSeconds + subseconds; } } } diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs index 1754dd2..79c5e2f 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/EventCollectorSink.cs @@ -1,4 +1,4 @@ -// Copyright 2016 Serilog Contributors +// Copyright 2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,6 +12,10 @@ // See the License for the specific language governing permissions and // limitations under the License. +using Serilog.Debugging; +using Serilog.Events; +using Serilog.Formatting; +using Serilog.Sinks.PeriodicBatching; using System; using System.Collections.Generic; using System.IO; @@ -19,10 +23,6 @@ using System.Net; using System.Net.Http; using System.Threading.Tasks; -using Serilog.Debugging; -using Serilog.Events; -using Serilog.Formatting; -using Serilog.Sinks.PeriodicBatching; namespace Serilog.Sinks.Splunk { @@ -56,17 +56,23 @@ public class EventCollectorSink : IBatchedLogEventSink /// The token to use when authenticating with the event collector /// The format provider used when rendering the message /// Whether to render the message template + /// Include "RenderedMessage" parameter from output JSON message. + /// Timestamp sub-second precision. Splunk props.conf setup is required. public EventCollectorSink( string splunkHost, string eventCollectorToken, IFormatProvider formatProvider = null, - bool renderTemplate = true) + bool renderTemplate = true, + bool renderMessage = true, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) : this( splunkHost, eventCollectorToken, null, null, null, null, null, formatProvider, - renderTemplate) + renderTemplate, + renderMessage, + subSecondPrecision: subSecondPrecision) { } @@ -83,6 +89,8 @@ public class EventCollectorSink : IBatchedLogEventSink /// The source type of the event /// The host of the event /// The handler used to send HTTP requests + /// Include "RenderedMessage" parameter from output JSON message. + /// Timestamp sub-second precision. Splunk props.conf setup is required. public EventCollectorSink( string splunkHost, string eventCollectorToken, @@ -93,13 +101,14 @@ public class EventCollectorSink : IBatchedLogEventSink string index, IFormatProvider formatProvider = null, bool renderTemplate = true, - HttpMessageHandler messageHandler = null) + bool renderMessage = true, + HttpMessageHandler messageHandler = null, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) : this( splunkHost, eventCollectorToken, uriPath, - - new SplunkJsonFormatter(renderTemplate, formatProvider, source, sourceType, host, index), + new SplunkJsonFormatter(renderTemplate, renderMessage, formatProvider, source, sourceType, host, index, subSecondPrecision: subSecondPrecision), messageHandler) { } @@ -118,6 +127,8 @@ public class EventCollectorSink : IBatchedLogEventSink /// The source type of the event /// The host of the event /// The handler used to send HTTP requests + /// Include "RenderedMessage" parameter from output JSON message. + /// Timestamp sub-second precision. Splunk props.conf setup is required. public EventCollectorSink( string splunkHost, string eventCollectorToken, @@ -129,13 +140,15 @@ public class EventCollectorSink : IBatchedLogEventSink CustomFields fields, IFormatProvider formatProvider = null, bool renderTemplate = true, - HttpMessageHandler messageHandler = null) + bool renderMessage = true, + HttpMessageHandler messageHandler = null, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) // TODO here is the jsonformatter creation. We must make way to test output of jsonformatter. : this( splunkHost, eventCollectorToken, uriPath, - new SplunkJsonFormatter(renderTemplate, formatProvider, source, sourceType, host, index, fields), + new SplunkJsonFormatter(renderTemplate, renderMessage, formatProvider, source, sourceType, host, index, fields, subSecondPrecision: subSecondPrecision), messageHandler) { } diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs index 17ee054..ac9eaa5 100644 --- a/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SplunkJsonFormatter.cs @@ -32,6 +32,8 @@ public class SplunkJsonFormatter : ITextFormatter private readonly bool _renderTemplate; private readonly IFormatProvider _formatProvider; + private readonly SubSecondPrecision _subSecondPrecision; + private readonly bool _renderMessage; private readonly string _suffix; /// @@ -40,10 +42,12 @@ public class SplunkJsonFormatter : ITextFormatter /// /// Supplies culture-specific formatting information, or null. /// If true, the template used will be rendered and written to the output as a property named MessageTemplate + /// Removes the "RenderedMessage" parameter from output JSON message. public SplunkJsonFormatter( bool renderTemplate, + bool renderMessage, IFormatProvider formatProvider) - : this(renderTemplate, formatProvider, null, null, null, null) + : this(renderTemplate, renderMessage, formatProvider, null, null, null, null) { } @@ -56,14 +60,18 @@ public class SplunkJsonFormatter : ITextFormatter /// The source of the event /// The source type of the event /// The host of the event + /// Timestamp sub-second precision. Splunk props.conf setup is required. + /// Removes the "RenderedMessage" parameter from output JSON message. public SplunkJsonFormatter( bool renderTemplate, + bool renderMessage, IFormatProvider formatProvider, string source, string sourceType, string host, - string index) - : this(renderTemplate, formatProvider, source, sourceType, host, index, null) + string index, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) + : this(renderTemplate, renderMessage, formatProvider, source, sourceType, host, index, null, subSecondPrecision: subSecondPrecision) { } @@ -77,17 +85,23 @@ public class SplunkJsonFormatter : ITextFormatter /// The source type of the event /// The host of the event /// Object that describes extra splunk fields that should be indexed with event see: http://dev.splunk.com/view/event-collector/SP-CAAAFB6 + /// Timestamp sub-second precision. Splunk props.conf setup is required. + /// Include "RenderedMessage" parameter from output JSON message. public SplunkJsonFormatter( bool renderTemplate, + bool renderMessage, IFormatProvider formatProvider, string source, string sourceType, string host, string index, - CustomFields customFields) + CustomFields customFields, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) { _renderTemplate = renderTemplate; _formatProvider = formatProvider; + _subSecondPrecision = subSecondPrecision; + _renderMessage = renderMessage; using (var suffixWriter = new StringWriter()) { @@ -157,7 +171,7 @@ public void Format(LogEvent logEvent, TextWriter output) if (output == null) throw new ArgumentNullException(nameof(output)); output.Write("{\"time\":\""); - output.Write(logEvent.Timestamp.ToEpoch().ToString(CultureInfo.InvariantCulture)); + output.Write(logEvent.Timestamp.ToEpoch(_subSecondPrecision)); output.Write("\",\"event\":{\"Level\":\""); output.Write(logEvent.Level); output.Write('"'); @@ -168,8 +182,11 @@ public void Format(LogEvent logEvent, TextWriter output) JsonValueFormatter.WriteQuotedJsonString(logEvent.MessageTemplate.Text, output); } - output.Write(",\"RenderedMessage\":"); - JsonValueFormatter.WriteQuotedJsonString(logEvent.RenderMessage(_formatProvider), output); + if (_renderMessage) + { + output.Write(",\"RenderedMessage\":"); + JsonValueFormatter.WriteQuotedJsonString(logEvent.RenderMessage(_formatProvider), output); + } if (logEvent.Exception != null) { diff --git a/src/Serilog.Sinks.Splunk/Sinks/Splunk/SubSecondPrecision.cs b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SubSecondPrecision.cs new file mode 100644 index 0000000..7788daa --- /dev/null +++ b/src/Serilog.Sinks.Splunk/Sinks/Splunk/SubSecondPrecision.cs @@ -0,0 +1,37 @@ +// Copyright 2016 Serilog Contributors +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +namespace Serilog.Sinks.Splunk +{ + /// + /// Enum representing the precision of sub-second time measurements. + /// + public enum SubSecondPrecision + { + /// + /// Represents precision in milliseconds. Value corresponds to the number of decimal places. + /// + Milliseconds = 3, + + /// + /// Represents precision in microseconds. Value corresponds to the number of decimal places. + /// + Microseconds = 6, + + /// + /// Represents precision in nanoseconds. Value corresponds to the number of decimal places. + /// + Nanoseconds = 9 + } +} diff --git a/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs b/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs index 05165f6..333dd81 100644 --- a/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs +++ b/src/Serilog.Sinks.Splunk/SplunkLoggingConfigurationExtensions.cs @@ -17,7 +17,6 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting; -using Serilog.Formatting.Json; using Serilog.Sinks.PeriodicBatching; using Serilog.Sinks.Splunk; using System; @@ -44,11 +43,13 @@ public static class SplunkLoggingConfigurationExtensions /// The minimum log event level required in order to write an event to the sink. /// Supplies culture-specific formatting information, or null. /// If true, the message template will be rendered + /// Include "RenderedMessage" parameter from output JSON message. /// The interval in seconds that the queue should be instpected for batching /// The size of the batch /// Maximum number of events in the queue /// The handler used to send HTTP requests /// A switch allowing the pass-through minimum level to be changed at runtime. + /// Timestamp sub-second precision. Splunk props.conf setup is required. /// public static LoggerConfiguration EventCollector( this LoggerSinkConfiguration configuration, @@ -62,11 +63,13 @@ public static class SplunkLoggingConfigurationExtensions LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null, bool renderTemplate = true, + bool renderMessage = true, int batchIntervalInSeconds = 2, int batchSizeLimit = 100, int? queueLimit = null, HttpMessageHandler messageHandler = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); @@ -88,7 +91,9 @@ public static class SplunkLoggingConfigurationExtensions index, formatProvider, renderTemplate, - messageHandler); + renderMessage, + subSecondPrecision: subSecondPrecision); + var batchingSink = new PeriodicBatchingSink(eventCollectorSink, batchingOptions); return configuration.Sink(batchingSink, restrictedToMinimumLevel, levelSwitch); @@ -103,7 +108,6 @@ public static class SplunkLoggingConfigurationExtensions /// The text formatter used to render log events into a JSON format for consumption by Splunk /// Change the default endpoint of the Event Collector e.g. services/collector/event /// The minimum log event level required in order to write an event to the sink. - /// The interval in seconds that the queue should be instpected for batching /// The size of the batch /// Maximum number of events in the queue @@ -168,6 +172,8 @@ public static class SplunkLoggingConfigurationExtensions /// The handler used to send HTTP requests /// A switch allowing the pass-through minimum level to be changed at runtime. /// Customfields that will be indexed in splunk with this event + /// Include "RenderedMessage" parameter from output JSON message. + /// Timestamp sub-second precision. Splunk props.conf setup is required. /// public static LoggerConfiguration EventCollector( this LoggerSinkConfiguration configuration, @@ -182,11 +188,13 @@ public static class SplunkLoggingConfigurationExtensions LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, IFormatProvider formatProvider = null, bool renderTemplate = true, + bool renderMessage = true, int batchIntervalInSeconds = 2, int batchSizeLimit = 100, int? queueLimit = null, HttpMessageHandler messageHandler = null, - LoggingLevelSwitch levelSwitch = null) + LoggingLevelSwitch levelSwitch = null, + SubSecondPrecision subSecondPrecision = SubSecondPrecision.Milliseconds) { if (configuration == null) throw new ArgumentNullException(nameof(configuration)); @@ -209,7 +217,11 @@ public static class SplunkLoggingConfigurationExtensions fields, formatProvider, renderTemplate, - messageHandler); + renderMessage, + messageHandler, + subSecondPrecision: subSecondPrecision + ); + var batchingSink = new PeriodicBatchingSink(eventCollectorSink, batchingOptions); diff --git a/test/Serilog.Sinks.Splunk.Tests/EpochExtensionsTests.cs b/test/Serilog.Sinks.Splunk.Tests/EpochExtensionsTests.cs new file mode 100644 index 0000000..949905d --- /dev/null +++ b/test/Serilog.Sinks.Splunk.Tests/EpochExtensionsTests.cs @@ -0,0 +1,67 @@ +using Serilog.Sinks.Splunk; +using System; +using System.Diagnostics; +using Xunit; + +namespace Serilog.Sinks.Splunk.Tests +{ + public class EpochExtensionsTests + { + [Fact] + public void ToEpoch_ShouldReturnCorrectEpochTime() + { + // Arrange + var dateTimeOffset = new DateTimeOffset(2022, 1, 1, 0, 0, 0, TimeSpan.Zero); + var expectedEpochTime = "1640995200.000"; // Epoch time for 2022-01-01 00:00:00 + + // Act + var result = dateTimeOffset.ToEpoch(); + + // Assert + Assert.Equal(expectedEpochTime, result); + } + + [Fact] + public void ToEpoch_ShouldReturnCorrectEpochTime_Milliseconds() + { + // Arrange + var dateTimeOffset = new DateTimeOffset(2022, 1, 1, 0, 0, 0, 123, TimeSpan.Zero); + var expectedEpochTime = "1640995200.123"; // Epoch time for 2022-01-01 00:00:00.123 + + // Act + var result = dateTimeOffset.ToEpoch(SubSecondPrecision.Milliseconds); + + // Assert + Assert.Equal(expectedEpochTime, result); + } + + [Fact] + public void ToEpoch_ShouldReturnCorrectEpochTime_Microseconds() + { + // Arrange + var dateTimeOffset = new DateTimeOffset(2022, 1, 1, 0, 0, 0, 123, TimeSpan.Zero) + TimeSpan.FromMicroseconds(456); + var expectedEpochTime = "1640995200.123456"; // Epoch time for 2022-01-01 00:00:00.123 + + // Act + var result = dateTimeOffset.ToEpoch(SubSecondPrecision.Microseconds); + + // Assert + Assert.Equal(expectedEpochTime, result); + } + + [Fact] + public void ToEpoch_ShouldReturnCorrectEpochTime_Nanoseconds() + { + // Arrange + // using from ticks here, NanoSeconds is not available in TimeSpan. Nanoseconds Per Tick = 100L. + var dateTimeOffset = new DateTimeOffset(2022, 1, 1, 0, 0, 0, 123, TimeSpan.Zero) + TimeSpan.FromMicroseconds(456) + TimeSpan.FromTicks(7); + var expectedEpochTime = "1640995200.123456700"; // Epoch time for 2022-01-01 00:00:00.123 + + // Act + var result = dateTimeOffset.ToEpoch(SubSecondPrecision.Nanoseconds); + + // Assert + Assert.Equal(expectedEpochTime, result); + } + } +} diff --git a/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj b/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj index 7f73a20..3ab383c 100644 --- a/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj +++ b/test/Serilog.Sinks.Splunk.Tests/Serilog.Sinks.Splunk.Tests.csproj @@ -3,25 +3,24 @@ net8.0 Serilog.Sinks.Splunk.Tests - Serilog.Sinks.Splunk.Tests - true - $(PackageTargetFallback);dnxcore50;portable-net45+win8 - 1.0.4 - ../../assets/Serilog.snk - true - true - true true - - + - all - runtime; build; native; contentfiles; analyzers; buildtransitive + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all diff --git a/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs b/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs index 2310062..af34962 100644 --- a/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs +++ b/test/Serilog.Sinks.Splunk.Tests/SplunkJsonFormatterTests.cs @@ -1,28 +1,29 @@ -using System; -using System.Collections.Generic; -using System.IO; +using Newtonsoft.Json; using Newtonsoft.Json.Linq; -using Xunit; -using Serilog.Sinks.Splunk.Tests.Support; using Serilog.Events; using Serilog.Parsing; +using Serilog.Sinks.Splunk.Tests.Support; +using System; +using System.Collections.Generic; using System.Globalization; -using Newtonsoft.Json; +using System.IO; +using Xunit; namespace Serilog.Sinks.Splunk.Tests { + public class SplunkJsonFormatterTests { - void AssertValidJson(Action act, + void AssertValidJson(Action act, string source = "", - string sourceType= "", - string host= "", - string index= "", - CustomFields fields=null) + string sourceType = "", + string host = "", + string index = "", + CustomFields fields = null) { StringWriter outputRendered = new StringWriter(), output = new StringWriter(); var log = new LoggerConfiguration() - .WriteTo.Sink(new TextWriterSink(output, new SplunkJsonFormatter(false, null, source, sourceType, host, index))) - .WriteTo.Sink(new TextWriterSink(outputRendered, new SplunkJsonFormatter(true, null, source, sourceType, host, index))) + .WriteTo.Sink(new TextWriterSink(output, new SplunkJsonFormatter(false, true, null, source, sourceType, host, index))) + .WriteTo.Sink(new TextWriterSink(outputRendered, new SplunkJsonFormatter(true, true, null, source, sourceType, host, index))) .CreateLogger(); act(log); @@ -67,7 +68,7 @@ public void AMinimalEventWithSourceIsValidJson() { AssertValidJson(log => log.Information("One {Property}", 42), source: "A Test Source"); } - + [Fact] public void AMinimalEventWithSourceTypeIsValidJson() { @@ -122,7 +123,7 @@ public void Test_CustomFields_Jsonformatter_for_Splunk_Sink() var timeStamp = DateTimeOffset.Now; // var timeStampUnix = (Math.Round(timeStamp.ToUnixTimeMilliseconds() / 1000.0, 3)).ToString("##.###", CultureInfo.InvariantCulture); //req dotnet 4.6.2 var timeStampUnix = ((Int32)(DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1))).TotalSeconds).ToString("##.###", CultureInfo.InvariantCulture); - var sut = new SplunkJsonFormatter(renderTemplate: true, formatProvider: null, source: "BackPackTestServerChannel", sourceType: "_json", host: "Wanda", index: "Main", customFields: metaData); + var sut = new SplunkJsonFormatter(renderTemplate: true, renderMessage: true, formatProvider: null, source: "BackPackTestServerChannel", sourceType: "_json", host: "Wanda", index: "Main", customFields: metaData); try { var willnotwork = a / b; @@ -189,6 +190,6 @@ public class TestEventResultObject public TestCustomFields Fields { get; set; } } - #endregion + #endregion } } \ No newline at end of file