diff --git a/.github/.DS_Store b/.github/.DS_Store new file mode 100644 index 0000000..39056af Binary files /dev/null and b/.github/.DS_Store differ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..acd3bc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# If this file is renamed, the incrementing run attempt number will be reset. + +name: CI + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +env: + CI_BUILD_NUMBER_BASE: ${{ github.run_number }} + CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + build: + + # The build must run on Windows so that .NET Framework targets can be built and tested. + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Compute build number + shell: bash + run: | + echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+2300))" >> $GITHUB_ENV + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + ./Build.ps1 diff --git a/.gitignore b/.gitignore index 94420dc..4f6c9b2 100644 --- a/.gitignore +++ b/.gitignore @@ -234,3 +234,5 @@ _Pvt_Extensions # FAKE - F# Make .fake/ + +.DS_Store/ diff --git a/Build.ps1 b/Build.ps1 index ba41ab1..e798284 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,48 +1,79 @@ -echo "build: Build started" +Write-Output "build: Tool versions follow" + +dotnet --version +dotnet --list-sdks + +Write-Output "build: Build started" Push-Location $PSScriptRoot +try { + if(Test-Path .\artifacts) { + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse + } -if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse -} + & dotnet restore --no-cache -& dotnet restore --no-cache + $dbp = [Xml] (Get-Content .\Directory.Version.props) + $versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix -$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] -$commitHash = $(git rev-parse --short HEAD) -$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + Write-Output "build: Package version prefix is $versionPrefix" -echo "build: Package version suffix is $suffix" -echo "build: Build version suffix is $buildSuffix" + $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] -foreach ($src in ls src/*) { - Push-Location $src + Write-Output "build: Package version suffix is $suffix" + Write-Output "build: Build version suffix is $buildSuffix" - echo "build: Packaging project in $src" + & dotnet build -c Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true + if($LASTEXITCODE -ne 0) { throw "Build failed" } - & dotnet build -c Release --version-suffix=$buildSuffix -p:EnableSourceLink=true - if ($suffix) { - & dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --no-build - } else { - & dotnet pack -c Release -o ..\..\artifacts --no-build + foreach ($src in Get-ChildItem src/*) { + Push-Location $src + + Write-Output "build: Packaging project in $src" + + if ($suffix) { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts --version-suffix=$suffix + } else { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts + } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } + + Pop-Location } - if($LASTEXITCODE -ne 0) { throw "build failed" } - Pop-Location -} + foreach ($test in Get-ChildItem test/*.Tests) { + Push-Location $test + + Write-Output "build: Testing project in $test" + + & dotnet test -c Release --no-build --no-restore + if($LASTEXITCODE -ne 0) { throw "Testing failed" } + + Pop-Location + } + + if ($env:NUGET_API_KEY) { + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). -foreach ($test in ls test/*.Tests) { - Push-Location $test + Write-Output "build: Publishing NuGet packages" - echo "build: Testing project in $test" + foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { + & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" + if($LASTEXITCODE -ne 0) { throw "Publishing failed" } + } - & dotnet test -c Release - if($LASTEXITCODE -ne 0) { throw "tests failed" } + if (!($suffix)) { + Write-Output "build: Creating release for version $versionPrefix" + iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)" + } + } +} finally { Pop-Location } - -Pop-Location \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props new file mode 100644 index 0000000..95d7522 --- /dev/null +++ b/Directory.Build.props @@ -0,0 +1,25 @@ + + + + + latest + True + + true + $(MSBuildThisFileDirectory)assets/Serilog.snk + false + enable + enable + true + true + true + true + snupkg + + + + + + + diff --git a/Directory.Version.props b/Directory.Version.props new file mode 100644 index 0000000..e37c3ef --- /dev/null +++ b/Directory.Version.props @@ -0,0 +1,5 @@ + + + 6.1.0 + + diff --git a/README.md b/README.md index 397b2e5..a708a9c 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ -# Serilog.Sinks.Console [![Build status](https://ci.appveyor.com/api/projects/status/w1w3m1wyk3in1c96/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-console/branch/master) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.Console.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.Console/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Help](https://img.shields.io/badge/stackoverflow-serilog-orange.svg)](http://stackoverflow.com/questions/tagged/serilog) +# Serilog.Sinks.Console [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Sinks.Console.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.Console/) [![Documentation](https://img.shields.io/badge/docs-wiki-yellow.svg)](https://github.com/serilog/serilog/wiki) [![Help](https://img.shields.io/badge/stackoverflow-serilog-orange.svg)](http://stackoverflow.com/questions/tagged/serilog) -A Serilog sink that writes log events to the Windows Console or an ANSI terminal via standard output. Coloring and custom themes are supported, including ANSI 256-color themes on macOS, Linux and Windows 10. The default output is plain text; JSON formatting can be plugged in using a package such as [_Serilog.Formatting.Compact_](https://github.com/serilog/serilog-formatting-compact). +A Serilog sink that writes log events to the Windows Console or an ANSI terminal via standard output. Coloring and custom themes are supported, including ANSI 256-color themes on macOS, Linux and Windows 10+. The default output is plain text; JSON formatting can be plugged in using [_Serilog.Formatting.Compact_](https://github.com/serilog/serilog-formatting-compact) or the [fully-customizable](https://nblumhardt.com/2021/06/customize-serilog-json-output/) [_Serilog.Expressions_](https://github.com/serilog/serilog-expressions). ### Getting started @@ -16,7 +16,7 @@ Then enable the sink using `WriteTo.Console()`: Log.Logger = new LoggerConfiguration() .WriteTo.Console() .CreateLogger(); - + Log.Information("Hello, world!"); ``` @@ -171,17 +171,8 @@ Log.Logger = new LoggerConfiguration() ### Contributing -Would you like to help make the Serilog console sink even better? We keep a list of issues that are approachable for newcomers under the [up-for-grabs](https://github.com/serilog/serilog-sinks-console/issues?labels=up-for-grabs&state=open) label. Before starting work on a pull request, we suggest commenting on, or raising, an issue on the issue tracker so that we can help and coordinate efforts. For more details check out our [contributing guide](CONTRIBUTING.md). +Would you like to help make the Serilog console sink even better? We keep a list of issues that are approachable for newcomers under the [up-for-grabs](https://github.com/serilog/serilog-sinks-console/issues?labels=up-for-grabs&state=open) label. Before starting work on a pull request, we suggest commenting on, or raising, an issue on the issue tracker so that we can help and coordinate efforts. For more details check out our [contributing guide](CONTRIBUTING.md). When contributing please keep in mind our [Code of Conduct](CODE_OF_CONDUCT.md). - -### Detailed build status - -Branch | AppVeyor | Travis -------------- | ------------- |------------- -dev | [![Build status](https://ci.appveyor.com/api/projects/status/w1w3m1wyk3in1c96/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-console/branch/dev) | [![Build Status](https://travis-ci.org/serilog/serilog-sinks-console.svg?branch=dev)](https://travis-ci.org/serilog/serilog-sinks-console) -main | [![Build status](https://ci.appveyor.com/api/projects/status/w1w3m1wyk3in1c96/branch/main?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-console/branch/main) | [![Build Status](https://travis-ci.org/serilog/serilog-sinks-console.svg?branch=main)](https://travis-ci.org/serilog/serilog-sinks-console) - - _Copyright © Serilog Contributors - Provided under the [Apache License, Version 2.0](http://apache.org/licenses/LICENSE-2.0.html)._ diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index 0a0eb08..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,22 +0,0 @@ -version: '{build}' -skip_tags: true -image: Visual Studio 2022 -test: off -build_script: -- pwsh: ./Build.ps1 -artifacts: -- path: artifacts/Serilog.*.nupkg -deploy: -- provider: NuGet - api_key: - secure: ZpUO4ECx4c/V0Ecj04cfV1UGd+ZABeEG9DDW2fjG8vITjNYhmbiiJH0qNOnRy2G3 - skip_symbols: true - on: - branch: /^(main|dev)$/ -- provider: GitHub - auth_token: - secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifact: /Serilog.*\.nupkg/ - tag: v$(appveyor_build_version) - on: - branch: main diff --git a/build.sh b/build.sh deleted file mode 100755 index 64d87ea..0000000 --- a/build.sh +++ /dev/null @@ -1,18 +0,0 @@ -#!/bin/bash - -set -e -dotnet --info -dotnet restore - -for path in src/**/*.csproj; do - dotnet build -f netstandard2.0 -c Release ${path} -done - -for path in test/*.Tests/*.csproj; do - dotnet test -f netcoreapp2.2 -c Release ${path} -done - -for path in sample/ConsoleDemo/*.csproj; do - dotnet build -f netcoreapp2.2 -c Release ${path} - dotnet run -f netcoreapp2.2 --project ${path} -done diff --git a/sample/SyncWritesDemo/Program.cs b/sample/SyncWritesDemo/Program.cs index f1015a1..139f1f1 100644 --- a/sample/SyncWritesDemo/Program.cs +++ b/sample/SyncWritesDemo/Program.cs @@ -24,7 +24,7 @@ Console.WriteLine("Expecting one of the following arguments:{0}--sync-root-default{0}--sync-root-separate{0}--sync-root-same", Environment.NewLine); -static void SystemConsoleSyncTest(object syncRootForLogger1, object syncRootForLogger2) +static void SystemConsoleSyncTest(object? syncRootForLogger1, object? syncRootForLogger2) { var logger1 = new LoggerConfiguration() .MinimumLevel.Verbose() diff --git a/serilog-sinks-console.sln b/serilog-sinks-console.sln index e223156..9e93a9c 100644 --- a/serilog-sinks-console.sln +++ b/serilog-sinks-console.sln @@ -5,11 +5,10 @@ VisualStudioVersion = 15.0.26430.12 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{037440DE-440B-4129-9F7A-09B42D00397E}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5E1-DEB9-4A04-8BAB-24EC7240ADAF}" +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "sln", "sln", "{E9D1B5E1-DEB9-4A04-8BAB-24EC7240ADAF}" ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes .gitignore = .gitignore - appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 CHANGES.md = CHANGES.md LICENSE = LICENSE @@ -29,6 +28,17 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ConsoleDemo", "sample\Conso EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SyncWritesDemo", "sample\SyncWritesDemo\SyncWritesDemo.csproj", "{633AE0AD-C9D4-440D-874A-C0F4632DB75F}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{91B268E6-3BCF-49B4-A04D-8467AE23410A}" + ProjectSection(SolutionItems) = preProject + .github\ISSUE_TEMPLATE.md = .github\ISSUE_TEMPLATE.md + .github\PULL_REQUEST_TEMPLATE.md = .github\PULL_REQUEST_TEMPLATE.md + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{F4C015DE-80BA-4D63-9D3D-D687999B4960}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -60,6 +70,7 @@ Global {1D56534C-4009-42C2-A573-789CAE6B8AA9} = {7D0692CD-F95D-4BF9-8C63-B4A1C078DF23} {DBF4907A-63A2-4895-8DEF-59F90C20380B} = {CF817664-4CEC-4B6A-9C57-A0D687757D82} {633AE0AD-C9D4-440D-874A-C0F4632DB75F} = {CF817664-4CEC-4B6A-9C57-A0D687757D82} + {F4C015DE-80BA-4D63-9D3D-D687999B4960} = {91B268E6-3BCF-49B4-A04D-8467AE23410A} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {43C32ED4-D39A-4E27-AE99-7BB8C883833C} diff --git a/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj b/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj index 38a3d4f..f3fd276 100644 --- a/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj +++ b/src/Serilog.Sinks.Console/Serilog.Sinks.Console.csproj @@ -1,24 +1,14 @@ A Serilog sink that writes log events to the console/terminal. - 6.0.0 Serilog Contributors net462;net471 $(TargetFrameworks);netstandard2.0;net6.0;net8.0 - enable - ../../assets/Serilog.snk - true - true serilog;console;terminal icon.png https://github.com/serilog/serilog-sinks-console Apache-2.0 - https://github.com/serilog/serilog-sinks-console - git - true - True Serilog - latest README.md diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs index 2f8c743..6e4f561 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/OutputTemplateRenderer.cs @@ -68,7 +68,11 @@ public OutputTemplateRenderer(ConsoleTheme theme, string outputTemplate, IFormat } else if (pt.PropertyName == OutputProperties.TimestampPropertyName) { - renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider)); + renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: false)); + } + else if (pt.PropertyName == OutputProperties.UtcTimestampPropertyName) + { + renderers.Add(new TimestampTokenRenderer(theme, pt, formatProvider, convertToUtc: true)); } else if (pt.PropertyName == OutputProperties.PropertiesPropertyName) { diff --git a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs index 7c4de83..66bdf4d 100644 --- a/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs +++ b/src/Serilog.Sinks.Console/Sinks/SystemConsole/Output/TimestampTokenRenderer.cs @@ -26,63 +26,85 @@ class TimestampTokenRenderer : OutputTemplateTokenRenderer { readonly ConsoleTheme _theme; readonly PropertyToken _token; + readonly string? _format; readonly IFormatProvider? _formatProvider; + readonly bool _convertToUtc; - public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider) + public TimestampTokenRenderer(ConsoleTheme theme, PropertyToken token, IFormatProvider? formatProvider, bool convertToUtc) { - _theme = theme; - _token = token; - _formatProvider = formatProvider; - } + _theme = theme; + _token = token; + _format = token.Format; + _formatProvider = formatProvider; + _convertToUtc = convertToUtc; + } public override void Render(LogEvent logEvent, TextWriter output) { - var sv = new DateTimeOffsetValue(logEvent.Timestamp); - - var _ = 0; - using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _)) + var _ = 0; + using (_theme.Apply(output, ConsoleThemeStyle.SecondaryText, ref _)) + { + if (_token.Alignment is null) { - if (_token.Alignment is null) - { - sv.Render(output, _token.Format, _formatProvider); - } - else - { - var buffer = new StringWriter(); - sv.Render(buffer, _token.Format, _formatProvider); - var str = buffer.ToString(); - Padding.Apply(output, str, _token.Alignment); - } + Render(output, logEvent.Timestamp); + } + else + { + var buffer = new StringWriter(); + Render(buffer, logEvent.Timestamp); + var str = buffer.ToString(); + Padding.Apply(output, str, _token.Alignment); } } + } - readonly struct DateTimeOffsetValue + private void Render(TextWriter output, DateTimeOffset timestamp) { - public DateTimeOffsetValue(DateTimeOffset value) - { - Value = value; - } + // When a DateTimeOffset is converted to a string, the default format automatically adds the "+00:00" explicit offset to the output string. + // As the TimestampTokenRenderer is also used for rendering the UtcTimestamp which is always in UTC by definition, the +00:00 suffix should be avoided. + // This is done using the same approach as Serilog's MessageTemplateTextFormatter. In case output should be converted to UTC, in order to avoid a zone specifier, + // the DateTimeOffset is converted to a DateTime which then renders as expected. - public DateTimeOffset Value { get; } + var custom = (ICustomFormatter?)_formatProvider?.GetFormat(typeof(ICustomFormatter)); + if (custom != null) + { + output.Write(custom.Format(_format, _convertToUtc ? timestamp.UtcDateTime : timestamp, _formatProvider)); + return; + } - public void Render(TextWriter output, string? format = null, IFormatProvider? formatProvider = null) + if (_convertToUtc) { - var custom = (ICustomFormatter?)formatProvider?.GetFormat(typeof(ICustomFormatter)); - if (custom != null) - { - output.Write(custom.Format(format, Value, formatProvider)); - return; - } + RenderDateTime(output, timestamp.UtcDateTime); + } + else + { + RenderDateTimeOffset(output, timestamp); + } + } + private void RenderDateTimeOffset(TextWriter output, DateTimeOffset timestamp) + { #if FEATURE_SPAN - Span buffer = stackalloc char[32]; - if (Value.TryFormat(buffer, out int written, format, formatProvider ?? CultureInfo.InvariantCulture)) - output.Write(buffer.Slice(0, written)); - else - output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture)); + Span buffer = stackalloc char[32]; + if (timestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture)) + output.Write(buffer.Slice(0, written)); + else + output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture)); #else - output.Write(Value.ToString(format, formatProvider ?? CultureInfo.InvariantCulture)); + output.Write(timestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture)); +#endif + } + + private void RenderDateTime(TextWriter output, DateTime utcTimestamp) + { +#if FEATURE_SPAN + Span buffer = stackalloc char[32]; + if (utcTimestamp.TryFormat(buffer, out int written, _format, _formatProvider ?? CultureInfo.InvariantCulture)) + output.Write(buffer.Slice(0, written)); + else + output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture)); +#else + output.Write(utcTimestamp.ToString(_format, _formatProvider ?? CultureInfo.InvariantCulture)); #endif - } } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs b/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs index 8e5f731..7e6b799 100644 --- a/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs +++ b/test/Serilog.Sinks.Console.Tests/Output/OutputTemplateRendererTests.cs @@ -400,4 +400,36 @@ public void TraceAndSpanAreIncludedWhenPresent() formatter.Format(evt, sw); Assert.Equal($"{traceId}/{spanId}", sw.ToString()); } + + [Theory] + [InlineData("{Timestamp}", "09/03/2024 14:15:16 +02:00")] // Default Format + [InlineData("{Timestamp:o}", "2024-09-03T14:15:16.0790000+02:00")] // Round-trip Standard Format String + [InlineData("{Timestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 14:15:16")] // Custom Format String + public void TimestampTokenRendersLocalTime(string actualToken, string expectedOutput) + { + var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture); + var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture); + var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null, + new MessageTemplate(Enumerable.Empty()), Enumerable.Empty()); + var sw = new StringWriter(); + formatter.Format(evt, sw); + // expect time in local time, unchanged from the input, the +02:00 offset should not affect the output + Assert.Equal(expectedOutput, sw.ToString()); + } + + [Theory] + [InlineData("{UtcTimestamp}", "09/03/2024 12:15:16")] // Default Format + [InlineData("{UtcTimestamp:o}", "2024-09-03T12:15:16.0790000Z")] // Round-trip Standard Format String + [InlineData("{UtcTimestamp:yyyy-MM-dd HH:mm:ss}", "2024-09-03 12:15:16")] // Custom Format String + public void UtcTimestampTokenRendersUtcTime(string actualToken, string expectedOutput) + { + var logTimestampWithTimeZoneOffset = DateTimeOffset.Parse("2024-09-03T14:15:16.079+02:00", CultureInfo.InvariantCulture); + var formatter = new OutputTemplateRenderer(ConsoleTheme.None, actualToken, CultureInfo.InvariantCulture); + var evt = new LogEvent(logTimestampWithTimeZoneOffset, LogEventLevel.Debug, null, + new MessageTemplate(Enumerable.Empty()), Enumerable.Empty()); + var sw = new StringWriter(); + formatter.Format(evt, sw); + // expect time in UTC, the +02:00 offset must be applied to adjust the hour + Assert.Equal(expectedOutput, sw.ToString()); + } } \ No newline at end of file diff --git a/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj b/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj index 472965f..b165f21 100644 --- a/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj +++ b/test/Serilog.Sinks.Console.Tests/Serilog.Sinks.Console.Tests.csproj @@ -1,12 +1,8 @@  - net7.0 + net8.0;net9.0 true - ../../assets/Serilog.snk - true - true - enable