diff --git a/.github/workflows/package-Preview.yml b/.github/workflows/package-Preview.yml new file mode 100644 index 0000000000..6c8c0584f8 --- /dev/null +++ b/.github/workflows/package-Preview.yml @@ -0,0 +1,50 @@ +name: Pack OpenTelemetry.Contrib.Preview + +on: + workflow_dispatch: + inputs: + logLevel: + description: 'Log level' + required: true + default: 'warning' + push: + tags: + - 'Preview-*' # trigger when we create a tag with prefix "Preview-" + +jobs: + build-test-pack: + runs-on: ${{ matrix.os }} + env: + PROJECT: OpenTelemetry.Contrib.Preview + + strategy: + matrix: + os: [windows-latest] + + steps: + - uses: actions/checkout@v2 + with: + fetch-depth: 0 # fetching all + + - name: Install dependencies + run: dotnet restore + + - name: dotnet build ${{env.PROJECT}} + run: dotnet build src/${{env.PROJECT}} --configuration Release --no-restore -p:Deterministic=true + + - name: dotnet test ${{env.PROJECT}} + run: dotnet test test/${{env.PROJECT}}.Tests + + - name: dotnet pack ${{env.PROJECT}} + run: dotnet pack src/${{env.PROJECT}} --configuration Release --no-build + + - name: Publish Artifacts + uses: actions/upload-artifact@v2 + with: + name: ${{env.PROJECT}}-packages + path: '**/${{env.PROJECT}}/bin/**/*.*nupkg' + + - name: Publish Nuget + run: | + nuget setApiKey ${{ secrets.NUGET_TOKEN }} -Source https://api.nuget.org/v3/index.json + nuget push **/${{env.PROJECT}}/bin/**/*.nupkg -Source https://api.nuget.org/v3/index.json \ No newline at end of file diff --git a/CODEOWNERS b/CODEOWNERS index dcd32ecc61..e1c0efabde 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -12,6 +12,7 @@ src/OpenTelemetry.Contrib.Instrumentation.MassTransit/ @ope src/OpenTelemetry.Contrib.Instrumentation.GrpcCore/ @open-telemetry/dotnet-contrib-approvers @pcwiese src/OpenTelemetry.Contrib.Instrumentation.Wcf/ @open-telemetry/dotnet-contrib-approvers @codeblanch src/OpenTelemetry.Contrib.Instrumentation.MySqlData/ @open-telemetry/dotnet-contrib-approvers @moonheart +src/OpenTelemetry.Contrib.Preview/ @open-telemetry/dotnet-contrib-approvers @codeblanch test/OpenTelemetry.Contrib.Exporter.Stackdriver.Tests/ @open-telemetry/dotnet-contrib-approvers @SergeyKanzhelev test/OpenTelemetry.Contrib.Extensions.AWSXRay.Tests/ @open-telemetry/dotnet-contrib-approvers @srprash @lupengamzn @@ -19,5 +20,6 @@ test/OpenTelemetry.Contrib.Instrumentation.Elasticsearch.Tests/ @ope test/OpenTelemetry.Contrib.Instrumentation.EntityFrameworkCoreTests/ @open-telemetry/dotnet-contrib-approvers test/OpenTelemetry.Contrib.Instrumentation.MassTransit.Tests/ @open-telemetry/dotnet-contrib-approvers @alexvaluyskiy test/OpenTelemetry.Contrib.Instrumentation.GrpcCore.Tests/ @open-telemetry/dotnet-contrib-approvers @pcwiese -src/OpenTelemetry.Contrib.Instrumentation.Wcf.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch -src/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/ @open-telemetry/dotnet-contrib-approvers @moonheart +test/OpenTelemetry.Contrib.Instrumentation.Wcf.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch +test/OpenTelemetry.Contrib.Instrumentation.MySqlData.Tests/ @open-telemetry/dotnet-contrib-approvers @moonheart +test/OpenTelemetry.Contrib.Preview.Tests/ @open-telemetry/dotnet-contrib-approvers @codeblanch diff --git a/Directory.Build.props b/Directory.Build.props deleted file mode 100644 index 47342704d0..0000000000 --- a/Directory.Build.props +++ /dev/null @@ -1,10 +0,0 @@ - - - 8.0 - - - - - - - diff --git a/Directory.Build.targets b/Directory.Build.targets deleted file mode 100644 index 63c8e0171e..0000000000 --- a/Directory.Build.targets +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/build/Common.nonprod.props b/build/Common.nonprod.props index 8a9fbf4742..b43fa19ccf 100644 --- a/build/Common.nonprod.props +++ b/build/Common.nonprod.props @@ -14,7 +14,6 @@ - @@ -28,8 +27,7 @@ --> [0.12.1,0.13) [2.3.1,3.0) - [5.0.0-preview.8.20407.11] - [5.0.0-preview.8.20407.11] + [5.0,6.0) [16.7.1,17.0) [4.14.5,5.0) [2.4.3,3.0) diff --git a/build/Common.prod.props b/build/Common.prod.props index 6dad44268e..41dc196e13 100644 --- a/build/Common.prod.props +++ b/build/Common.prod.props @@ -25,12 +25,12 @@ - runtime; build; native; contentfiles; analyzers; buildtransitive all + @@ -58,4 +58,12 @@ true + + + + + + + + diff --git a/build/Common.props b/build/Common.props index 0225580e34..1adbdf0ae9 100644 --- a/build/Common.props +++ b/build/Common.props @@ -1,6 +1,6 @@ - 8.0 + 9.0 true $([System.IO.Directory]::GetParent($(MSBuildThisFileDirectory)).Parent.FullName) $(MSBuildThisFileDirectory)debug.snk @@ -22,27 +22,29 @@ Refer to https://docs.microsoft.com/en-us/nuget/concepts/package-versioning for semver syntax. --> [2.4.0,3.0) - [3.3.0] + [5.0.3] [16.7.1] [2.1.0,5.0) [1.0.0,2.0) + [3.3.2] [1.0.0,2.0) [1.1.118,2.0) + + + $(NoWarn);nullable + + - - All - + - + diff --git a/build/Common.targets b/build/Common.targets new file mode 100644 index 0000000000..6cf892c169 --- /dev/null +++ b/build/Common.targets @@ -0,0 +1,8 @@ + + + + AllEnabledByDefault + latest + + + diff --git a/examples/Directory.Build.targets b/examples/Directory.Build.targets new file mode 100644 index 0000000000..da3eb65ef4 --- /dev/null +++ b/examples/Directory.Build.targets @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/opentelemetry-dotnet-contrib.sln b/opentelemetry-dotnet-contrib.sln index 86f4ba2de6..4856b3ead4 100644 --- a/opentelemetry-dotnet-contrib.sln +++ b/opentelemetry-dotnet-contrib.sln @@ -10,8 +10,6 @@ EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution items", "Solution items", "{07AA0F83-22F6-4B8C-921D-029D3384CB17}" ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig - Directory.Build.props = Directory.Build.props - Directory.Build.targets = Directory.Build.targets NuGet.config = NuGet.config opentelemetry-dotnet-contrib.proj = opentelemetry-dotnet-contrib.proj README.md = README.md @@ -39,6 +37,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{ .github\workflows\package-Instrumentation.MassTransit.yml = .github\workflows\package-Instrumentation.MassTransit.yml .github\workflows\package-Instrumentation.MySqlData.yml = .github\workflows\package-Instrumentation.MySqlData.yml .github\workflows\package-Instrumentation.Wcf.yml = .github\workflows\package-Instrumentation.Wcf.yml + .github\workflows\package-Preview.yml = .github\workflows\package-Preview.yml .github\workflows\pr_build.yml = .github\workflows\pr_build.yml .github\workflows\sanitycheck.yml = .github\workflows\sanitycheck.yml EndProjectSection @@ -48,6 +47,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "build", "build", "{824BD1DE build\Common.nonprod.props = build\Common.nonprod.props build\Common.prod.props = build\Common.prod.props build\Common.props = build\Common.props + build\Common.targets = build\Common.targets build\debug.snk = build\debug.snk build\docfx.cmd = build\docfx.cmd build\opentelemetry-icon-color.png = build\opentelemetry-icon-color.png @@ -133,7 +133,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instr EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests", "test\OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests\OpenTelemetry.Contrib.Instrumentation.AWSLambda.Tests.csproj", "{08EDD935-8B4E-4CF5-8840-200DEBA8E110}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "OpenTelemetry.Contrib.Tests.Shared", "test\OpenTelemetry.Contrib.Tests.Shared\OpenTelemetry.Contrib.Tests.Shared.csproj", "{C33F2D9D-89A6-459C-9A51-79BA5A9EF194}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Tests.Shared", "test\OpenTelemetry.Contrib.Tests.Shared\OpenTelemetry.Contrib.Tests.Shared.csproj", "{C33F2D9D-89A6-459C-9A51-79BA5A9EF194}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Preview", "src\OpenTelemetry.Contrib.Preview\OpenTelemetry.Contrib.Preview.csproj", "{B978939B-278C-43A3-AD12-32EA9BBD27D0}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Contrib.Preview.Tests", "test\OpenTelemetry.Contrib.Preview.Tests\OpenTelemetry.Contrib.Preview.Tests.csproj", "{D2C68560-C252-41A9-B742-2CEB7D760E0F}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -249,6 +253,14 @@ Global {C33F2D9D-89A6-459C-9A51-79BA5A9EF194}.Debug|Any CPU.Build.0 = Debug|Any CPU {C33F2D9D-89A6-459C-9A51-79BA5A9EF194}.Release|Any CPU.ActiveCfg = Release|Any CPU {C33F2D9D-89A6-459C-9A51-79BA5A9EF194}.Release|Any CPU.Build.0 = Release|Any CPU + {B978939B-278C-43A3-AD12-32EA9BBD27D0}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B978939B-278C-43A3-AD12-32EA9BBD27D0}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B978939B-278C-43A3-AD12-32EA9BBD27D0}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B978939B-278C-43A3-AD12-32EA9BBD27D0}.Release|Any CPU.Build.0 = Release|Any CPU + {D2C68560-C252-41A9-B742-2CEB7D760E0F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {D2C68560-C252-41A9-B742-2CEB7D760E0F}.Debug|Any CPU.Build.0 = Debug|Any CPU + {D2C68560-C252-41A9-B742-2CEB7D760E0F}.Release|Any CPU.ActiveCfg = Release|Any CPU + {D2C68560-C252-41A9-B742-2CEB7D760E0F}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -286,6 +298,8 @@ Global {87FE0ED4-56A5-4775-9F63-DD532F2200BD} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} {08EDD935-8B4E-4CF5-8840-200DEBA8E110} = {2097345F-4DD3-477D-BC54-A922F9B2B402} {C33F2D9D-89A6-459C-9A51-79BA5A9EF194} = {2097345F-4DD3-477D-BC54-A922F9B2B402} + {B978939B-278C-43A3-AD12-32EA9BBD27D0} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63} + {D2C68560-C252-41A9-B742-2CEB7D760E0F} = {2097345F-4DD3-477D-BC54-A922F9B2B402} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66} diff --git a/src/Directory.Build.targets b/src/Directory.Build.targets index 0cf8dd57d8..a582103dd0 100644 --- a/src/Directory.Build.targets +++ b/src/Directory.Build.targets @@ -1,6 +1,7 @@ - + + all diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Shipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..c654303a3a --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/net461/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +#nullable enable +Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions +OpenTelemetry.Logs.LogToActivityEventConversionOptions +OpenTelemetry.Logs.LogToActivityEventConversionOptions.LogToActivityEventConversionOptions() -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.get -> System.Action! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.set -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.get -> System.Action>!>! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.set -> void +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AttachLogsToActivityEvent(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action? configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..c654303a3a --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/net5.0/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +#nullable enable +Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions +OpenTelemetry.Logs.LogToActivityEventConversionOptions +OpenTelemetry.Logs.LogToActivityEventConversionOptions.LogToActivityEventConversionOptions() -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.get -> System.Action! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.set -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.get -> System.Action>!>! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.set -> void +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AttachLogsToActivityEvent(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action? configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Shipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Shipped.txt new file mode 100644 index 0000000000..7dc5c58110 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Shipped.txt @@ -0,0 +1 @@ +#nullable enable diff --git a/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt b/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt new file mode 100644 index 0000000000..c654303a3a --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/.publicApi/netstandard2.0/PublicAPI.Unshipped.txt @@ -0,0 +1,9 @@ +#nullable enable +Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions +OpenTelemetry.Logs.LogToActivityEventConversionOptions +OpenTelemetry.Logs.LogToActivityEventConversionOptions.LogToActivityEventConversionOptions() -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.get -> System.Action! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.ScopeConverter.set -> void +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.get -> System.Action>!>! +OpenTelemetry.Logs.LogToActivityEventConversionOptions.StateConverter.set -> void +static Microsoft.Extensions.Logging.OpenTelemetryLoggingExtensions.AttachLogsToActivityEvent(this OpenTelemetry.Logs.OpenTelemetryLoggerOptions! loggerOptions, System.Action? configure = null) -> OpenTelemetry.Logs.OpenTelemetryLoggerOptions! diff --git a/src/OpenTelemetry.Contrib.Preview/AssemblyInfo.cs b/src/OpenTelemetry.Contrib.Preview/AssemblyInfo.cs new file mode 100644 index 0000000000..fb9851b200 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/AssemblyInfo.cs @@ -0,0 +1,19 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; + +[assembly: CLSCompliant(false)] diff --git a/src/OpenTelemetry.Contrib.Preview/CHANGELOG.md b/src/OpenTelemetry.Contrib.Preview/CHANGELOG.md new file mode 100644 index 0000000000..1512c42162 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/CHANGELOG.md @@ -0,0 +1,3 @@ +# Changelog + +## Unreleased diff --git a/src/OpenTelemetry.Contrib.Preview/Internal/ActivityEventAttachingLogProcessor.cs b/src/OpenTelemetry.Contrib.Preview/Internal/ActivityEventAttachingLogProcessor.cs new file mode 100644 index 0000000000..c9e1b936db --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/Internal/ActivityEventAttachingLogProcessor.cs @@ -0,0 +1,110 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +#if NET461_OR_GREATER || NETSTANDARD2_0 || NET5_0_OR_GREATER +using System; +using System.Diagnostics; +using OpenTelemetry.Trace; + +namespace OpenTelemetry.Logs +{ + internal sealed class ActivityEventAttachingLogProcessor : BaseProcessor + { + private static readonly Action ProcessScope = (LogRecordScope scope, State state) => + { + try + { + state.Processor.options.ScopeConverter?.Invoke(state.Tags, state.Depth++, scope); + } + catch (Exception ex) + { + OpenTelemetryExtensionsEventSource.Log.LogProcessorException($"Processing scope of type [{scope.GetType().FullName}]", ex); + } + }; + + private readonly LogToActivityEventConversionOptions options; + + public ActivityEventAttachingLogProcessor(LogToActivityEventConversionOptions options) + { + this.options = options ?? throw new ArgumentNullException(nameof(options)); + } + + public override void OnEnd(LogRecord data) + { + Activity? activity = Activity.Current; + + if (activity?.IsAllDataRequested == true) + { + var tags = new ActivityTagsCollection + { + { nameof(data.CategoryName), data.CategoryName }, + { nameof(data.LogLevel), data.LogLevel }, + }; + + if (data.EventId != 0) + { + tags[nameof(data.EventId)] = data.EventId; + } + + var activityEvent = new ActivityEvent("log", data.Timestamp, tags); + + data.ForEachScope(ProcessScope, new State(tags, this)); + + if (data.StateValues != null) + { + try + { + this.options.StateConverter?.Invoke(tags, data.StateValues); + } +#pragma warning disable CA1031 // Do not catch general exception types + catch (Exception ex) +#pragma warning restore CA1031 // Do not catch general exception types + { + OpenTelemetryExtensionsEventSource.Log.LogProcessorException($"Processing state of type [{data.State.GetType().FullName}]", ex); + } + } + + if (!string.IsNullOrEmpty(data.FormattedMessage)) + { + tags[nameof(data.FormattedMessage)] = data.FormattedMessage; + } + + activity.AddEvent(activityEvent); + + if (data.Exception != null) + { + activity.RecordException(data.Exception); + } + } + } + + private class State + { + public State(ActivityTagsCollection tags, ActivityEventAttachingLogProcessor processor) + { + this.Tags = tags; + this.Processor = processor; + } + + public ActivityTagsCollection Tags { get; } + + public ActivityEventAttachingLogProcessor Processor { get; } + + public int Depth { get; set; } + } + } +} +#endif diff --git a/src/OpenTelemetry.Contrib.Preview/Internal/DefaultLogStateConverter.cs b/src/OpenTelemetry.Contrib.Preview/Internal/DefaultLogStateConverter.cs new file mode 100644 index 0000000000..76d64d64f3 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/Internal/DefaultLogStateConverter.cs @@ -0,0 +1,68 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +#if NET461_OR_GREATER || NETSTANDARD2_0 || NET5_0_OR_GREATER +using System.Collections.Generic; +using System.Diagnostics; + +namespace OpenTelemetry.Logs +{ + internal static class DefaultLogStateConverter + { + public static void ConvertState(ActivityTagsCollection tags, IReadOnlyList> state) + { + for (int i = 0; i < state.Count; i++) + { + KeyValuePair stateItem = state[i]; + + object value = stateItem.Value; + if (value != null) + { + if (string.IsNullOrEmpty(stateItem.Key)) + { + tags["state"] = value; + } + else + { + tags[$"state.{stateItem.Key}"] = value; + } + } + } + } + + public static void ConvertScope(ActivityTagsCollection tags, int depth, LogRecordScope scope) + { + string prefix = $"scope[{depth}]"; + + foreach (KeyValuePair scopeItem in scope) + { + object value = scopeItem.Value; + if (value != null) + { + if (string.IsNullOrEmpty(scopeItem.Key)) + { + tags[prefix] = value; + } + else + { + tags[$"{prefix}.{scopeItem.Key}"] = value; + } + } + } + } + } +} +#endif diff --git a/src/OpenTelemetry.Contrib.Preview/Internal/OpenTelemetryExtensionsEventSource.cs b/src/OpenTelemetry.Contrib.Preview/Internal/OpenTelemetryExtensionsEventSource.cs new file mode 100644 index 0000000000..0815f0b2b0 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/Internal/OpenTelemetryExtensionsEventSource.cs @@ -0,0 +1,46 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; +using System.Diagnostics.Tracing; +using OpenTelemetry.Internal; + +namespace OpenTelemetry +{ + /// + /// EventSource implementation for OpenTelemetry SDK extensions implementation. + /// + [EventSource(Name = "OpenTelemetry-Extensions")] + internal class OpenTelemetryExtensionsEventSource : EventSource + { + public static OpenTelemetryExtensionsEventSource Log = new OpenTelemetryExtensionsEventSource(); + + [NonEvent] + public void LogProcessorException(string @event, Exception ex) + { + if (this.IsEnabled(EventLevel.Error, (EventKeywords)(-1))) + { + this.LogProcessorException(@event, ex.ToInvariantString()); + } + } + + [Event(1, Message = "Unknown error in LogProcessor event '{0}': '{1}'.", Level = EventLevel.Error)] + public void LogProcessorException(string @event, string exception) + { + this.WriteEvent(1, @event, exception); + } + } +} diff --git a/src/OpenTelemetry.Contrib.Preview/Logs/LogToActivityEventConversionOptions.cs b/src/OpenTelemetry.Contrib.Preview/Logs/LogToActivityEventConversionOptions.cs new file mode 100644 index 0000000000..d2342ce410 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/Logs/LogToActivityEventConversionOptions.cs @@ -0,0 +1,40 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +#if NET461_OR_GREATER || NETSTANDARD2_0 || NET5_0_OR_GREATER +using System; +using System.Collections.Generic; +using System.Diagnostics; + +namespace OpenTelemetry.Logs +{ + /// + /// Stores options used to convert log messages into s. + /// + public class LogToActivityEventConversionOptions + { + /// + /// Gets or sets the callback action used to convert log state into tags. + /// + public Action>> StateConverter { get; set; } = DefaultLogStateConverter.ConvertState; + + /// + /// Gets or sets the callback action used to convert log scopes into tags. + /// + public Action ScopeConverter { get; set; } = DefaultLogStateConverter.ConvertScope; + } +} +#endif diff --git a/src/OpenTelemetry.Contrib.Preview/Logs/OpenTelemetryLoggingExtensions.cs b/src/OpenTelemetry.Contrib.Preview/Logs/OpenTelemetryLoggingExtensions.cs new file mode 100644 index 0000000000..7d3049181e --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/Logs/OpenTelemetryLoggingExtensions.cs @@ -0,0 +1,56 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +#if NET461_OR_GREATER || NETSTANDARD2_0 || NET5_0_OR_GREATER +using System; +using System.Diagnostics; +using OpenTelemetry.Logs; + +namespace Microsoft.Extensions.Logging +{ + /// + /// Contains OpenTelemetry logging SDK extensions. + /// + public static class OpenTelemetryLoggingExtensions + { + /// + /// Adds a processor to the OpenTelemetry which will convert log messages + /// into s attached to the active when the message is written. + /// + /// options to use. + /// . + /// The instance of to chain the calls. + /// is null. + public static OpenTelemetryLoggerOptions AttachLogsToActivityEvent( + this OpenTelemetryLoggerOptions loggerOptions, + Action? configure = null) + { + if (loggerOptions == null) + { + throw new ArgumentNullException(nameof(loggerOptions)); + } + + var options = new LogToActivityEventConversionOptions(); + configure?.Invoke(options); +#pragma warning disable CA2000 // Dispose objects before losing scope + return loggerOptions.AddProcessor(new ActivityEventAttachingLogProcessor(options)); +#pragma warning restore CA2000 // Dispose objects before losing scope + } + } +} +#endif diff --git a/src/OpenTelemetry.Contrib.Preview/OpenTelemetry.Contrib.Preview.csproj b/src/OpenTelemetry.Contrib.Preview/OpenTelemetry.Contrib.Preview.csproj new file mode 100644 index 0000000000..7557b2d3f2 --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/OpenTelemetry.Contrib.Preview.csproj @@ -0,0 +1,21 @@ + + + + net461;netstandard2.0 + $(TargetFrameworks);net5.0 + OpenTelemetry .NET SDK preview features and extensions + Preview- + enable + true + true + + + + + + + + + + + diff --git a/src/OpenTelemetry.Contrib.Preview/README.md b/src/OpenTelemetry.Contrib.Preview/README.md new file mode 100644 index 0000000000..90857a6c7f --- /dev/null +++ b/src/OpenTelemetry.Contrib.Preview/README.md @@ -0,0 +1,14 @@ +# OpenTelemetry .NET SDK preview features and extensions + +[![nuget](https://img.shields.io/nuget/v/OpenTelemetry.Contrib.Preview.svg)](https://www.nuget.org/packages/OpenTelemetry.Contrib.Preview/) + +Contains useful features and extensions to the OpenTelemetry .NET SDK that are +not part of the official OpenTelemetry specification but might be added in the +future. + +## Logging + +### AttachLogsToActivityEvent + +Adds a log processor which will convert log messages into events and attach them +to the currently running Activity. diff --git a/test/Directory.Build.targets b/test/Directory.Build.targets index 18398cd3b1..da3eb65ef4 100644 --- a/test/Directory.Build.targets +++ b/test/Directory.Build.targets @@ -1,3 +1,4 @@ + \ No newline at end of file diff --git a/test/OpenTelemetry.Contrib.Preview.Tests/ActivityEventAttachingLogProcessorTests.cs b/test/OpenTelemetry.Contrib.Preview.Tests/ActivityEventAttachingLogProcessorTests.cs new file mode 100644 index 0000000000..455590b28f --- /dev/null +++ b/test/OpenTelemetry.Contrib.Preview.Tests/ActivityEventAttachingLogProcessorTests.cs @@ -0,0 +1,190 @@ +// +// Copyright The OpenTelemetry Authors +// +// 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. +// + +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using Microsoft.Extensions.Logging; +using OpenTelemetry.Logs; +using Xunit; + +namespace OpenTelemetry.Contrib.Preview.Tests +{ + public sealed class ActivityEventAttachingLogProcessorTests : IDisposable + { + private readonly ActivitySource activitySource = new ActivitySource("Test"); + private readonly ActivityListener activityListener = new ActivityListener + { + ShouldListenTo = source => true, + }; + + private readonly ILogger logger; + private readonly ILoggerFactory loggerFactory; + private OpenTelemetryLoggerOptions options; + private bool sampled; + + public ActivityEventAttachingLogProcessorTests() + { + this.activityListener.Sample = (ref ActivityCreationOptions options) => + { + return this.sampled + ? ActivitySamplingResult.AllDataAndRecorded + : ActivitySamplingResult.PropagationData; + }; + + ActivitySource.AddActivityListener(this.activityListener); + + this.loggerFactory = LoggerFactory.Create(builder => + { + builder.AddOpenTelemetry(options => + { + this.options = options; + options.AttachLogsToActivityEvent(); + }); + builder.AddFilter(typeof(ActivityEventAttachingLogProcessorTests).FullName, LogLevel.Trace); + }); + + this.logger = this.loggerFactory.CreateLogger(); + } + + public void Dispose() + { + this.activitySource.Dispose(); + this.activityListener.Dispose(); + this.loggerFactory.Dispose(); + } + + [Theory] + [InlineData(true)] + [InlineData(false)] + [InlineData(true, 18, true, true, true)] + [InlineData(true, 0, false, false, true, true)] + public void AttachLogsToActivityEventTest( + bool sampled, + int eventId = 0, + bool includeFormattedMessage = false, + bool parseStateValues = false, + bool includeScopes = false, + bool recordException = false) + { + this.sampled = sampled; + this.options.IncludeFormattedMessage = includeFormattedMessage; + this.options.ParseStateValues = parseStateValues; + this.options.IncludeScopes = includeScopes; + + Activity activity = this.activitySource.StartActivity("Test"); + + using IDisposable scope = this.logger.BeginScope("{NodeId}", 99); + + this.logger.LogInformation(eventId, "Hello OpenTelemetry {UserId}!", 8); + + Activity innerActivity = null; + if (recordException) + { + innerActivity = this.activitySource.StartActivity("InnerTest"); + + using IDisposable innerScope = this.logger.BeginScope("{RequestId}", "1234"); + + this.logger.LogError(new InvalidOperationException("Goodbye OpenTelemetry."), "Exception event."); + + innerActivity.Dispose(); + } + + activity.Dispose(); + + if (sampled) + { + ActivityEvent? logEvent = activity.Events.FirstOrDefault(); + + Assert.NotNull(logEvent); + Assert.Equal("log", logEvent.Value.Name); + + Dictionary tags = logEvent.Value.Tags?.ToDictionary(i => i.Key, i => i.Value); + Assert.NotNull(tags); + + Assert.Equal("OpenTelemetry.Contrib.Preview.Tests.ActivityEventAttachingLogProcessorTests", tags[nameof(LogRecord.CategoryName)]); + Assert.Equal(LogLevel.Information, tags[nameof(LogRecord.LogLevel)]); + + if (eventId != 0) + { + Assert.Equal((EventId)eventId, tags[nameof(LogRecord.EventId)]); + } + else + { + Assert.DoesNotContain(tags, kvp => kvp.Key == nameof(LogRecord.EventId)); + } + + if (includeFormattedMessage) + { + Assert.Equal("Hello OpenTelemetry 8!", tags[nameof(LogRecord.FormattedMessage)]); + } + else + { + Assert.DoesNotContain(tags, kvp => kvp.Key == nameof(LogRecord.FormattedMessage)); + } + + if (parseStateValues) + { + Assert.Equal(8, tags["state.UserId"]); + } + else + { + Assert.DoesNotContain(tags, kvp => kvp.Key == "state.UserId"); + } + + if (includeScopes) + { + Assert.Equal(99, tags["scope[0].NodeId"]); + } + else + { + Assert.DoesNotContain(tags, kvp => kvp.Key == "scope[0].NodeId"); + } + + if (recordException) + { + ActivityEvent? exLogEvent = innerActivity.Events.FirstOrDefault(); + + Assert.NotNull(exLogEvent); + Assert.Equal("log", exLogEvent.Value.Name); + + Dictionary exLogTags = exLogEvent.Value.Tags?.ToDictionary(i => i.Key, i => i.Value); + Assert.NotNull(exLogTags); + + Assert.Equal(99, exLogTags["scope[0].NodeId"]); + Assert.Equal("1234", exLogTags["scope[1].RequestId"]); + + ActivityEvent? exEvent = innerActivity.Events.Skip(1).FirstOrDefault(); + + Assert.NotNull(exEvent); + Assert.Equal("exception", exEvent.Value.Name); + + Dictionary exTags = exEvent.Value.Tags?.ToDictionary(i => i.Key, i => i.Value); + Assert.NotNull(exTags); + + Assert.Equal("System.InvalidOperationException", exTags["exception.type"]); + Assert.Equal("Goodbye OpenTelemetry.", exTags["exception.message"]); + Assert.Contains(exTags, kvp => kvp.Key == "exception.stacktrace"); + } + } + else + { + Assert.Empty(activity.Events); + } + } + } +} diff --git a/test/OpenTelemetry.Contrib.Preview.Tests/OpenTelemetry.Contrib.Preview.Tests.csproj b/test/OpenTelemetry.Contrib.Preview.Tests/OpenTelemetry.Contrib.Preview.Tests.csproj new file mode 100644 index 0000000000..71a93e8b02 --- /dev/null +++ b/test/OpenTelemetry.Contrib.Preview.Tests/OpenTelemetry.Contrib.Preview.Tests.csproj @@ -0,0 +1,28 @@ + + + + Unit test project for OpenTelemetry .NET SDK preview features and extensions + net5.0 + + + + + + + + all + runtime; build; native; contentfiles; analyzers + + + + + + + + + + + + + +