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/Build.ps1 b/Build.ps1 index 06a36af..e798284 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,44 +1,79 @@ +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) { - Write-Output "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) }[$NULL -ne $env:APPVEYOR_REPO_BRANCH]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:APPVEYOR_BUILD_NUMBER]; -$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] + Write-Output "build: Package version prefix is $versionPrefix" -Write-Output "build: Package version suffix is $suffix" + $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 Get-ChildItem src/*) { - Push-Location $src + Write-Output "build: Package version suffix is $suffix" + Write-Output "build: Build version suffix is $buildSuffix" - Write-Output "build: Packaging project in $src" + & dotnet build -c Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true + if($LASTEXITCODE -ne 0) { throw "Build failed" } - if ($suffix) { - & dotnet pack -c Release --include-source -o ../../artifacts --version-suffix=$suffix - } else { - & dotnet pack -c Release --include-source -o ../../artifacts + 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 "Packaging failed" } - Pop-Location -} + foreach ($test in Get-ChildItem test/*.Tests) { + Push-Location $test + + Write-Output "build: Testing project in $test" + + & dotnet test -c Release --no-build --no-restore + if($LASTEXITCODE -ne 0) { throw "Testing failed" } + + Pop-Location + } + + if ($env:NUGET_API_KEY) { + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). -foreach ($test in Get-ChildItem test/*.Tests) { - Push-Location $test + Write-Output "build: Publishing NuGet packages" - Write-Output "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 "Testing 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 diff --git a/Directory.Build.props b/Directory.Build.props index 3724c9c..c114992 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,16 +1,25 @@ + + latest True - true + + true $(MSBuildThisFileDirectory)assets/Serilog.snk false enable enable + true + true + true + true + snupkg - \ No newline at end of file + diff --git a/Directory.Version.props b/Directory.Version.props new file mode 100644 index 0000000..7e88a76 --- /dev/null +++ b/Directory.Version.props @@ -0,0 +1,5 @@ + + + 4.2.0 + + diff --git a/README.md b/README.md index f4cba8e..c6c9a8f 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Sinks.OpenTelemetry [![Build status](https://ci.appveyor.com/api/projects/status/sqmrvw34pcuatwl5/branch/dev?svg=true)](https://ci.appveyor.com/project/serilog/serilog-sinks-opentelemetry/branch/dev) [![NuGet Version](https://img.shields.io/nuget/vpre/Serilog.Sinks.OpenTelemetry.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.OpenTelemetry/) +# Serilog.Sinks.OpenTelemetry [![Build status](https://github.com/serilog/serilog-sinks-opentelemetry/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/serilog/serilog-sinks-opentelemetry/actions) [![NuGet Version](https://img.shields.io/nuget/vpre/Serilog.Sinks.OpenTelemetry.svg?style=flat)](https://www.nuget.org/packages/Serilog.Sinks.OpenTelemetry/) This Serilog sink transforms Serilog events into OpenTelemetry `LogRecord`s and sends them to an OTLP (gRPC or HTTP) endpoint. @@ -142,9 +142,9 @@ Serilog `LogEvent` | OpenTelemetry `LogRecord` | `MessageTemplate` | `Attributes[ "message_template.text"]` | Requires `IncludedData. MessageTemplateText` (enabled by default) | `MessageTemplate` (MD5) | `Attributes[ "message_template.hash.md5"]` | Requires `IncludedData. MessageTemplateMD5 HashAttribute` | `Properties` | `Attributes` | Each property is mapped to an attribute keeping the name; the value's structure is maintained | -`SpanId` (`Activity.Current`) | `SpanId` | Requires `IncludedData.SpanId` (enabled by default) | +`SpanId` (`Activity.Current`) | `SpanId` | Requires `IncludedData.SpanIdField` (enabled by default) | `Timestamp` | `TimeUnixNano` | .NET provides 100-nanosecond precision | -`TraceId` (`Activity.Current`) | `TraceId` | Requires `IncludedData.TraceId` (enabled by default) | +`TraceId` (`Activity.Current`) | `TraceId` | Requires `IncludedData.TraceIdField` (enabled by default) | ### Configuring included data @@ -156,8 +156,8 @@ Log.Logger = new LoggerConfiguration() .WriteTo.OpenTelemetry(options => { options.Endpoint = "http://127.0.0.1:4317"; - options.IncludedData: IncludedData.MessageTemplate | - IncludedData.TraceId | IncludedData.SpanId; + options.IncludedData: IncludedData.MessageTemplateTextAttribute | + IncludedData.SpecRequiredResourceAttributes; }) .CreateLogger(); ``` diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index e523715..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,25 +0,0 @@ -version: '{build}' -skip_tags: true -image: Visual Studio 2022 -build_script: - - pwsh: | - Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1" - ./dotnet-install.ps1 -JsonFile global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet' - ./Build.ps1 -artifacts: - - path: artifacts/Serilog.*.nupkg -deploy: - - provider: NuGet - api_key: - secure: sDnchSg4TZIOK7oIUI6BJwFPNENTOZrGNsroGO1hehLJSvlHpFmpTwiX8+bgPD+Q - 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/example/Example/Example.csproj b/example/Example/Example.csproj index 03a4fda..3459322 100644 --- a/example/Example/Example.csproj +++ b/example/Example/Example.csproj @@ -2,9 +2,8 @@ Exe - net8.0 - enable - enable + net9.0 + false diff --git a/global.json b/global.json index 91296e3..ed7ea04 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.401", + "version": "9.0.200", "allowPrerelease": false, "rollForward": "latestFeature" } diff --git a/serilog-sinks-opentelemetry.sln b/serilog-sinks-opentelemetry.sln index 69e0572..c1b4532 100644 --- a/serilog-sinks-opentelemetry.sln +++ b/serilog-sinks-opentelemetry.sln @@ -9,12 +9,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{E9D1B5 ProjectSection(SolutionItems) = preProject .gitattributes = .gitattributes .gitignore = .gitignore - appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 LICENSE = LICENSE README.md = README.md assets\Serilog.snk = assets\Serilog.snk global.json = global.json + Directory.Build.props = Directory.Build.props + Directory.Version.props = Directory.Version.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{7D0692CD-F95D-4BF9-8C63-B4A1C078DF23}" @@ -27,6 +28,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "example", "example", "{CC7B EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Example", "example\Example\Example.csproj", "{C45B5103-C0CE-40CB-ACB8-4ED17B81AB7B}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{84C182D9-BA28-4E90-B505-1DB18EA1E6C8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{5809900F-4557-4B45-B01A-E3B5C0EB74B1}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +61,7 @@ Global {866A028E-27DB-49A0-AC78-E5FEF247C099} = {037440DE-440B-4129-9F7A-09B42D00397E} {1D56534C-4009-42C2-A573-789CAE6B8AA9} = {7D0692CD-F95D-4BF9-8C63-B4A1C078DF23} {C45B5103-C0CE-40CB-ACB8-4ED17B81AB7B} = {CC7B094D-FD20-4053-9749-F9098927CA5E} + {5809900F-4557-4B45-B01A-E3B5C0EB74B1} = {84C182D9-BA28-4E90-B505-1DB18EA1E6C8} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {43C32ED4-D39A-4E27-AE99-7BB8C883833C} diff --git a/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj b/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj index 0937f4c..6962adc 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj +++ b/src/Serilog.Sinks.OpenTelemetry/Serilog.Sinks.OpenTelemetry.csproj @@ -2,24 +2,19 @@ This Serilog sink transforms Serilog events into OpenTelemetry logs and sends them to an OTLP (gRPC or HTTP) endpoint. - 4.1.1 Serilog Contributors net471;net462 - $(TargetFrameworks);net8.0;net6.0;netstandard2.0 + $(TargetFrameworks);net9.0;net8.0;net6.0;netstandard2.0 serilog;sink;opentelemetry serilog-sink-nuget.png https://github.com/serilog/serilog-sinks-opentelemetry Apache-2.0 - https://github.com/serilog/serilog-sinks-opentelemetry - git - true Serilog README.md - 12 CS8981 @@ -31,6 +26,10 @@ $(DefineConstants);FEATURE_CWT_ADDORUPDATE;FEATURE_ACTIVITY;FEATURE_HALF;FEATURE_DATE_AND_TIME_ONLY;FEATURE_SYNC_HTTP_SEND;FEATURE_SOCKETS_HTTP_HANDLER + + $(DefineConstants);FEATURE_CWT_ADDORUPDATE;FEATURE_ACTIVITY;FEATURE_HALF;FEATURE_DATE_AND_TIME_ONLY;FEATURE_SYNC_HTTP_SEND;FEATURE_SOCKETS_HTTP_HANDLER + + @@ -38,8 +37,8 @@ - - - + + + diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs index 1ebd5cc..fc4013d 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/IncludedData.cs @@ -13,6 +13,8 @@ // limitations under the License. using System.Diagnostics; +using OpenTelemetry.Proto.Common.V1; +using Serilog.Events; namespace Serilog.Sinks.OpenTelemetry; @@ -82,5 +84,11 @@ public enum IncludedData /// Preserve the value of the SourceContext property, in addition to using it as the OTLP InstrumentationScope name. If /// not specified, the SourceContext property will be omitted from the individual log record attributes. /// - SourceContextAttribute = 128 + SourceContextAttribute = 128, + + /// + /// Include as $type when converting event properties to + /// OTLP values. + /// + StructureValueTypeTags = 256, } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OtlpEventBuilder.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OtlpEventBuilder.cs index 5740374..fd5bde8 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OtlpEventBuilder.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/OtlpEventBuilder.cs @@ -120,7 +120,7 @@ ParentSpanIdPropertyName or continue; } - var v = PrimitiveConversions.ToOpenTelemetryAnyValue(property.Value); + var v = PrimitiveConversions.ToOpenTelemetryAnyValue(property.Value, includedData); addAttribute(PrimitiveConversions.NewAttribute(property.Key, v)); } } diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PrimitiveConversions.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PrimitiveConversions.cs index 78aed6e..e105a8a 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PrimitiveConversions.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/PrimitiveConversions.cs @@ -136,7 +136,7 @@ public static AnyValue ToOpenTelemetryScalar(ScalarValue scalar) return ToOpenTelemetryPrimitive(scalar.Value); } - public static AnyValue ToOpenTelemetryMap(StructureValue value) + public static AnyValue ToOpenTelemetryMap(StructureValue value, IncludedData includedData) { var map = new AnyValue(); var kvList = new KeyValueList(); @@ -145,14 +145,27 @@ public static AnyValue ToOpenTelemetryMap(StructureValue value) // Per the OTLP protos, attribute keys MUST be unique. var seen = new HashSet(); + if ((includedData & IncludedData.StructureValueTypeTags) == IncludedData.StructureValueTypeTags && !string.IsNullOrEmpty(value.TypeTag)) + { + kvList.Values.Add(new KeyValue + { + Key = "$type", + Value = new() + { + StringValue = value.TypeTag + } + }); + } + foreach (var prop in value.Properties) { - if (seen.Contains(prop.Name)) + if (!seen.Add(prop.Name)) + { + // Already present continue; + } - seen.Add(prop.Name); - - var v = ToOpenTelemetryAnyValue(prop.Value); + var v = ToOpenTelemetryAnyValue(prop.Value, includedData); var kv = new KeyValue { Key = prop.Name, @@ -164,7 +177,7 @@ public static AnyValue ToOpenTelemetryMap(StructureValue value) return map; } - public static AnyValue ToOpenTelemetryMap(DictionaryValue value) + public static AnyValue ToOpenTelemetryMap(DictionaryValue value, IncludedData includedData) { var map = new AnyValue(); var kvList = new KeyValueList(); @@ -173,7 +186,7 @@ public static AnyValue ToOpenTelemetryMap(DictionaryValue value) foreach (var element in value.Elements) { var k = element.Key.Value?.ToString() ?? "null"; - var v = ToOpenTelemetryAnyValue(element.Value); + var v = ToOpenTelemetryAnyValue(element.Value, includedData); kvList.Values.Add(new KeyValue { Key = k, @@ -184,27 +197,27 @@ public static AnyValue ToOpenTelemetryMap(DictionaryValue value) return map; } - public static AnyValue ToOpenTelemetryArray(SequenceValue value) + public static AnyValue ToOpenTelemetryArray(SequenceValue value, IncludedData includedData) { var array = new AnyValue(); var values = new ArrayValue(); array.ArrayValue = values; foreach (var element in value.Elements) { - var v = ToOpenTelemetryAnyValue(element); + var v = ToOpenTelemetryAnyValue(element, includedData); values.Values.Add(v); } return array; } - internal static AnyValue ToOpenTelemetryAnyValue(LogEventPropertyValue value) + internal static AnyValue ToOpenTelemetryAnyValue(LogEventPropertyValue value, IncludedData includedData) { return value switch { ScalarValue scalar => ToOpenTelemetryScalar(scalar), - StructureValue structure => ToOpenTelemetryMap(structure), - SequenceValue sequence => ToOpenTelemetryArray(sequence), - DictionaryValue dictionary => ToOpenTelemetryMap(dictionary), + StructureValue structure => ToOpenTelemetryMap(structure, includedData), + SequenceValue sequence => ToOpenTelemetryArray(sequence, includedData), + DictionaryValue dictionary => ToOpenTelemetryMap(dictionary, includedData), _ => ToOpenTelemetryPrimitive(value.ToString()), }; } @@ -213,7 +226,7 @@ internal static string OnlyHexDigits(string s) { try { - return Regex.Replace(s, @"[^0-9a-fA-F]", "", RegexOptions.None, TimeSpan.FromSeconds(1.5)); + return Regex.Replace(s, "[^0-9a-fA-F]", "", RegexOptions.None, TimeSpan.FromSeconds(1.5)); } catch (RegexMatchTimeoutException) { diff --git a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs index caa7e33..be06781 100644 --- a/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs +++ b/src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/ProtocolHelpers/RequestTemplateFactory.cs @@ -22,7 +22,7 @@ namespace Serilog.Sinks.OpenTelemetry.ProtocolHelpers; static class RequestTemplateFactory { - const string OpenTelemetrySchemaUrl = "https://opentelemetry.io/schemas/v1.13.0"; + const string OpenTelemetrySchemaUrl = "https://opentelemetry.io/schemas/1.13.0"; public static ScopeLogs CreateScopeLogs(string? scopeName) { diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/OtlpEventBuilderTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/OtlpEventBuilderTests.cs index 53fe04c..5258667 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/OtlpEventBuilderTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/OtlpEventBuilderTests.cs @@ -251,7 +251,7 @@ public void SourceContextCanBePreservedAsAttribute() var (logRecord, scopeName) = OtlpEventBuilder.ToLogRecord(logEvent, null, OpenTelemetrySinkOptions.DefaultIncludedData | IncludedData.SourceContextAttribute); Assert.Equal(contextType.FullName, scopeName); - var ctx = Assert.Single(logRecord.Attributes.Where(a => a.Key == Core.Constants.SourceContextPropertyName)); + var ctx = Assert.Single(logRecord.Attributes, a => a.Key == Core.Constants.SourceContextPropertyName); Assert.Equal(contextType.FullName, ctx.Value.StringValue); } } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/PrimitiveConversionsTests.cs b/test/Serilog.Sinks.OpenTelemetry.Tests/PrimitiveConversionsTests.cs index 8e1bb85..55a667e 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/PrimitiveConversionsTests.cs +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/PrimitiveConversionsTests.cs @@ -23,14 +23,14 @@ namespace Serilog.Sinks.OpenTelemetry.Tests; public class PrimitiveConversionsTests { - public static byte[] GetRandomBytes(int size) { + static byte[] GetRandomBytes(int size) { var bytes = new byte[size]; var rnd = new Random(); rnd.NextBytes(bytes); return bytes; } - public static string ByteArrayToString(byte[] bytes) + static string ByteArrayToString(byte[] bytes) { return BitConverter.ToString(bytes).Replace("-","").ToLower(); } @@ -51,25 +51,16 @@ public void UnixEpochTimePreservesResolution() Assert.Equal(100ul, actual); } - [Fact] - public void TestToSeverityNumber() + [Theory] + [InlineData(LogEventLevel.Verbose, SeverityNumber.Trace)] + [InlineData(LogEventLevel.Debug, SeverityNumber.Debug)] + [InlineData(LogEventLevel.Information, SeverityNumber.Info)] + [InlineData(LogEventLevel.Warning, SeverityNumber.Warn)] + [InlineData(LogEventLevel.Error, SeverityNumber.Error)] + [InlineData(LogEventLevel.Fatal, SeverityNumber.Fatal)] + public void TestToSeverityNumber(LogEventLevel level, Enum expectedSeverityNumber) { - var data = new Dictionary - { - {LogEventLevel.Verbose, SeverityNumber.Trace}, - {LogEventLevel.Debug, SeverityNumber.Debug}, - {LogEventLevel.Information, SeverityNumber.Info}, - {LogEventLevel.Warning, SeverityNumber.Warn}, - {LogEventLevel.Error, SeverityNumber.Error}, - {LogEventLevel.Fatal, SeverityNumber.Fatal}, - }; - - foreach (var kvp in data) - { - var severity = kvp.Value; - var level = kvp.Key; - Assert.Equal(severity, PrimitiveConversions.ToSeverityNumber(level)); - } + Assert.Equal((SeverityNumber)expectedSeverityNumber, PrimitiveConversions.ToSeverityNumber(level)); } [Fact] @@ -114,22 +105,22 @@ public void TestToOpenTelemetrySpanId() Assert.Equal(openTelemetrySpanIdFromScalar?.ToByteArray(), expectedBytes); } - [Fact] - public void TestToOpenTelemetryTraceIdAndSpanIdNulls() + [Theory] + [InlineData("")] + [InlineData("invalid")] + public void RejectsInvalidTraceAndSpanIds(string input) { - Assert.Null(PrimitiveConversions.ToOpenTelemetryTraceId("invalid")); - Assert.Null(PrimitiveConversions.ToOpenTelemetryTraceId("")); - Assert.Null(PrimitiveConversions.ToOpenTelemetrySpanId("invalid")); - Assert.Null(PrimitiveConversions.ToOpenTelemetrySpanId("")); + Assert.Null(PrimitiveConversions.ToOpenTelemetryTraceId(input)); + Assert.Null(PrimitiveConversions.ToOpenTelemetrySpanId(input)); } [Fact] - public void TestNewAttribute() + public void ConstructsNewAttribute() { - var key = "ok"; + const string key = "ok"; var value = new AnyValue { - IntValue = (long)123 + IntValue = 123 }; var attribute = PrimitiveConversions.NewAttribute(key, value); @@ -138,10 +129,10 @@ public void TestNewAttribute() } [Fact] - public void TestNewStringAttribute() + public void ConstructsNewStringAttribute() { - var key = "ok"; - var value = "also-ok"; + const string key = "ok"; + const string value = "also-ok"; var attribute = PrimitiveConversions.NewStringAttribute(key, value); Assert.Equal(key, attribute.Key); @@ -153,140 +144,155 @@ public void TestToOpenTelemetryScalar() { var scalar = new ScalarValue((short)100); var result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); - scalar = new ScalarValue((int)100); + scalar = new ScalarValue(100); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); scalar = new ScalarValue((long)100); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); scalar = new ScalarValue((ushort)100); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); scalar = new ScalarValue((uint)100); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); scalar = new ScalarValue((ulong)100); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((long)100, result?.IntValue); + Assert.Equal(100, result.IntValue); scalar = new ScalarValue((float)3.14); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((double)(float)3.14, result?.DoubleValue); + Assert.Equal((float)3.14, result.DoubleValue); - scalar = new ScalarValue((double)3.14); + scalar = new ScalarValue(3.14); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((double)3.14, result?.DoubleValue); + Assert.Equal(3.14, result.DoubleValue); scalar = new ScalarValue((decimal)3.14); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal((double)(decimal)3.14, result?.DoubleValue); + Assert.Equal((double)(decimal)3.14, result.DoubleValue); scalar = new ScalarValue("ok"); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal("ok", result?.StringValue); + Assert.Equal("ok", result.StringValue); scalar = new ScalarValue(true); result = PrimitiveConversions.ToOpenTelemetryScalar(scalar); - Assert.Equal(true, result?.BoolValue); + Assert.True(result.BoolValue); // indirect conversion scalar = new ScalarValue(true); - result = PrimitiveConversions.ToOpenTelemetryAnyValue(scalar); - Assert.Equal(true, result?.BoolValue); + result = PrimitiveConversions.ToOpenTelemetryAnyValue(scalar, IncludedData.None); + Assert.True(result.BoolValue); } [Fact] public void TestToOpenTelemetryMap() { - var properties = new List(); - properties.Add(new LogEventProperty("a", new ScalarValue(1))); - properties.Add(new LogEventProperty("b", new ScalarValue("2"))); - properties.Add(new LogEventProperty("c", new ScalarValue(true))); - - var input = new StructureValue(properties); + var input = new StructureValue( + [ + new("a", new ScalarValue(1)), + new("b", new ScalarValue("2")), + new("c", new ScalarValue(true)) + ], "Test"); // direct conversion - var result = PrimitiveConversions.ToOpenTelemetryMap(input); - Assert.NotNull(result); - var kvlistValue = result?.KvlistValue; - Assert.Equal(3, kvlistValue?.Values.Count); - var secondPair = kvlistValue?.Values.ElementAt(1); - Assert.Equal("b", secondPair?.Key); - Assert.Equal("2", secondPair?.Value.StringValue); + AssertEquivalentToInput(PrimitiveConversions.ToOpenTelemetryMap(input, IncludedData.StructureValueTypeTags)); // indirect conversion - result = PrimitiveConversions.ToOpenTelemetryAnyValue(input); - Assert.NotNull(result); - kvlistValue = result?.KvlistValue; - Assert.Equal(3, kvlistValue?.Values.Count); - secondPair = kvlistValue?.Values.ElementAt(1); - Assert.Equal("b", secondPair?.Key); - Assert.Equal("2", secondPair?.Value.StringValue); + AssertEquivalentToInput(PrimitiveConversions.ToOpenTelemetryAnyValue(input, IncludedData.StructureValueTypeTags)); + + // no type tag + AssertEquivalentToInput(PrimitiveConversions.ToOpenTelemetryMap(input, IncludedData.None), noTypeTag: true); + + return; + + static void AssertEquivalentToInput(AnyValue result, bool noTypeTag = false) + { + Assert.NotNull(result); + var values = new Queue(result.KvlistValue.Values); + Assert.Equal(noTypeTag ? 3 : 4, values.Count); + + if (!noTypeTag) + { + var type = values.Dequeue(); + Assert.Equal("$type", type.Key); + Assert.Equal("Test", type.Value.StringValue); + } + + var a = values.Dequeue(); + Assert.Equal("a", a.Key); + Assert.Equal(1, a.Value.IntValue); + + var b = values.Dequeue(); + Assert.Equal("b", b.Key); + Assert.Equal("2", b.Value.StringValue); + + var c = values.Dequeue(); + Assert.Equal("c", c.Key); + Assert.True(c.Value.BoolValue); + } } [Fact] public void TestToOpenTelemetryArray() { - var elements = new List(); - elements.Add(new ScalarValue(1)); - elements.Add(new ScalarValue("2")); - elements.Add(new ScalarValue(false)); + List elements = + [ + new ScalarValue(1), + new ScalarValue("2"), + new ScalarValue(false) + ]; var input = new SequenceValue(elements); // direct conversion - var result = PrimitiveConversions.ToOpenTelemetryArray(input); + var result = PrimitiveConversions.ToOpenTelemetryArray(input, IncludedData.None); Assert.NotNull(result); - var arrayValue = result?.ArrayValue; + var arrayValue = result.ArrayValue; Assert.Equal(3, arrayValue?.Values.Count); var secondElement = arrayValue?.Values.ElementAt(1); Assert.Equal("2", secondElement?.StringValue); // indirect conversion - result = PrimitiveConversions.ToOpenTelemetryAnyValue(input); + result = PrimitiveConversions.ToOpenTelemetryAnyValue(input, IncludedData.None); Assert.NotNull(result); - arrayValue = result?.ArrayValue; + arrayValue = result.ArrayValue; Assert.Equal(3, arrayValue?.Values.Count); secondElement = arrayValue?.Values.ElementAt(1); Assert.Equal("2", secondElement?.StringValue); } - [Fact] - public void TestOnlyHexDigits() + [Theory] + [InlineData("0123456789abcdefABCDEF", "0123456789abcdefABCDEF")] + [InlineData("\f\t 123 \t\f", "123")] + [InlineData("wrong", "")] + [InlineData("\"123\"", "123")] + public void TestOnlyHexDigits(string input, string expected) { - var tests = new Dictionary - { - ["0123456789abcdefABCDEF"] = "0123456789abcdefABCDEF", - ["\f\t 123 \t\f"] = "123", - ["wrong"] = "", - ["\"123\""] = "123", - }; - - foreach (var kvp in tests) - { - var input = kvp.Key; - var expected = kvp.Value; - Assert.Equal(expected, PrimitiveConversions.OnlyHexDigits(input)); - } + Assert.Equal(expected, PrimitiveConversions.OnlyHexDigits(input)); } - [Fact] - public void TestMd5Hash() + [Theory] + [InlineData("")] + [InlineData("first string")] + [InlineData("second string")] + public void MD5RegexMatchesMD5Chars(string input) { var md5Regex = new Regex(@"^[a-f\d]{32}$"); + Assert.Matches(md5Regex, PrimitiveConversions.Md5Hash(input)); + } - var inputs = new[] { "", "first string", "second string" }; - foreach (var input in inputs) - { - Assert.Matches(md5Regex, PrimitiveConversions.Md5Hash(input)); - } + [Fact] + public void MD5HashIsComparable() + { Assert.Equal(PrimitiveConversions.Md5Hash("alpha"), PrimitiveConversions.Md5Hash("alpha")); Assert.NotEqual(PrimitiveConversions.Md5Hash("alpha"), PrimitiveConversions.Md5Hash("beta")); } @@ -294,12 +300,11 @@ public void TestMd5Hash() [Fact] public void DictionariesMapToMaps() { - var dict = new DictionaryValue(new[] - { + var dict = new DictionaryValue([ new KeyValuePair(new ScalarValue(0), new ScalarValue("test")) - }); + ]); - var any = PrimitiveConversions.ToOpenTelemetryAnyValue(dict); + var any = PrimitiveConversions.ToOpenTelemetryAnyValue(dict, IncludedData.None); Assert.NotNull(any.KvlistValue); var value = Assert.Single(any.KvlistValue.Values); @@ -310,16 +315,15 @@ public void DictionariesMapToMaps() [Fact] public void StructureKeysAreDeduplicated() { - var structure = new StructureValue(new[] - { + var structure = new StructureValue([ new LogEventProperty("a", new ScalarValue("test")), new LogEventProperty("a", new ScalarValue("test")), new LogEventProperty("b", new ScalarValue("test")) - }); + ]); Assert.Equal(3, structure.Properties.Count); - var any = PrimitiveConversions.ToOpenTelemetryAnyValue(structure); + var any = PrimitiveConversions.ToOpenTelemetryAnyValue(structure, IncludedData.None); Assert.Equal(2, any.KvlistValue.Values.Count); } diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt b/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt index db01986..a1ca91e 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/PublicApiVisibilityTests.approved.txt @@ -3,8 +3,8 @@ namespace Serilog public static class OpenTelemetryLoggerConfigurationExtensions { public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerAuditSinkConfiguration loggerAuditSinkConfiguration, System.Action configure) { } - public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Action configure, bool ignoreEnvironment = false) { } public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Action configure, System.Func? getConfigurationVariable) { } + public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, System.Action configure, bool ignoreEnvironment = false) { } public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerAuditSinkConfiguration loggerAuditSinkConfiguration, string endpoint = "http://localhost:4317", Serilog.Sinks.OpenTelemetry.OtlpProtocol protocol = 0, System.Collections.Generic.IDictionary? headers = null, System.Collections.Generic.IDictionary? resourceAttributes = null, Serilog.Sinks.OpenTelemetry.IncludedData? includedData = default) { } public static Serilog.LoggerConfiguration OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration loggerSinkConfiguration, string endpoint = "http://localhost:4317", Serilog.Sinks.OpenTelemetry.OtlpProtocol protocol = 0, System.Collections.Generic.IDictionary? headers = null, System.Collections.Generic.IDictionary? resourceAttributes = null, Serilog.Sinks.OpenTelemetry.IncludedData? includedData = default, Serilog.Events.LogEventLevel restrictedToMinimumLevel = 0, Serilog.Core.LoggingLevelSwitch? levelSwitch = null) { } } @@ -28,6 +28,7 @@ namespace Serilog.Sinks.OpenTelemetry TemplateBody = 32, MessageTemplateRenderingsAttribute = 64, SourceContextAttribute = 128, + StructureValueTypeTags = 256, } public class OpenTelemetrySinkOptions { diff --git a/test/Serilog.Sinks.OpenTelemetry.Tests/Serilog.Sinks.OpenTelemetry.Tests.csproj b/test/Serilog.Sinks.OpenTelemetry.Tests/Serilog.Sinks.OpenTelemetry.Tests.csproj index 13baf09..dd37683 100644 --- a/test/Serilog.Sinks.OpenTelemetry.Tests/Serilog.Sinks.OpenTelemetry.Tests.csproj +++ b/test/Serilog.Sinks.OpenTelemetry.Tests/Serilog.Sinks.OpenTelemetry.Tests.csproj @@ -5,8 +5,9 @@ - $(TargetFrameworks);net8.0;net6.0 + $(TargetFrameworks);net9.0;net8.0;net6.0 true + false False $(NoWarn);NU1701 12 @@ -22,11 +23,11 @@ - + - - + +