Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Logs] Serilog extensions project #3438

Merged
19 changes: 19 additions & 0 deletions OpenTelemetry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "correlation", "docs\logs\co
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Tests.Stress.Logs", "test\OpenTelemetry.Tests.Stress.Logs\OpenTelemetry.Tests.Stress.Logs.csproj", "{4298057B-24E0-47B3-BB76-C17E81AF6B39}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Examples.LoggingExtensions", "examples\LoggingExtensions\Examples.LoggingExtensions.csproj", "{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Serilog", "src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj", "{0D85558E-15B9-4251-BDBD-9CB7933B57E2}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Serilog.Tests", "test\OpenTelemetry.Extensions.Serilog.Tests\OpenTelemetry.Extensions.Serilog.Tests.csproj", "{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -463,6 +469,18 @@ Global
{4298057B-24E0-47B3-BB76-C17E81AF6B39}.Debug|Any CPU.Build.0 = Debug|Any CPU
{4298057B-24E0-47B3-BB76-C17E81AF6B39}.Release|Any CPU.ActiveCfg = Release|Any CPU
{4298057B-24E0-47B3-BB76-C17E81AF6B39}.Release|Any CPU.Build.0 = Release|Any CPU
{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Debug|Any CPU.Build.0 = Debug|Any CPU
{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Release|Any CPU.ActiveCfg = Release|Any CPU
{F5EFF065-7AF5-4D7D-8038-CC419ABD8777}.Release|Any CPU.Build.0 = Release|Any CPU
{0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Debug|Any CPU.Build.0 = Debug|Any CPU
{0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Release|Any CPU.ActiveCfg = Release|Any CPU
{0D85558E-15B9-4251-BDBD-9CB7933B57E2}.Release|Any CPU.Build.0 = Release|Any CPU
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Debug|Any CPU.Build.0 = Debug|Any CPU
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.ActiveCfg = Release|Any CPU
{6A2C122A-C1CD-4B6B-AE09-2ABB7D3C50CE}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -500,6 +518,7 @@ Global
{41B784AA-3301-4126-AF9F-1D59BD04B0BF} = {3277B1C0-BDFE-4460-9B0D-D9A661FB48DB}
{6C7A1595-36D6-4229-BBB5-5A6B5791791D} = {3862190B-E2C5-418E-AFDC-DB281FB5C705}
{9A07D215-90AC-4BAF-BCDB-73D74FD3A5C5} = {3862190B-E2C5-418E-AFDC-DB281FB5C705}
{F5EFF065-7AF5-4D7D-8038-CC419ABD8777} = {E359BB2B-9AEC-497D-B321-7DF2450C3B8E}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {55639B5C-0770-4A22-AB56-859604650521}
Expand Down
1 change: 1 addition & 0 deletions build/Common.props
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
<MicrosoftSourceLinkGitHubPkgVer>[1.0.0,2.0)</MicrosoftSourceLinkGitHubPkgVer>
<OpenTracingPkgVer>[0.12.1,0.13)</OpenTracingPkgVer>
<OTelPreviousStableVer>1.3.0</OTelPreviousStableVer>
<SerilogPkgVer>[2.8.0,3.0)</SerilogPkgVer>
<StyleCopAnalyzersPkgVer>[1.2.0-beta.354,2.0)</StyleCopAnalyzersPkgVer>
<SystemCollectionsImmutablePkgVer>1.4.0</SystemCollectionsImmutablePkgVer>
<SystemDiagnosticSourcePkgVer>6.0.0</SystemDiagnosticSourcePkgVer>
Expand Down
15 changes: 15 additions & 0 deletions examples/LoggingExtensions/Examples.LoggingExtensions.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net6.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" />
</ItemGroup>

</Project>
47 changes: 47 additions & 0 deletions examples/LoggingExtensions/Program.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
// <copyright file="Program.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using Serilog;

var resourceBuilder = ResourceBuilder.CreateDefault().AddService("Examples.LogEmitter");

// Note: It is important that OpenTelemetryLoggerProvider is disposed when the
// app is shutdown. In this example we allow Serilog to do that by calling CloseAndFlush.
var openTelemetryLoggerProvider = new OpenTelemetryLoggerProvider(options =>
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Instead of assigning this to a variable openTelemetryLoggerProvider and explaining the object ownership/lifecycle, I suggest putting the new OpenTelemetryLoggerProvider expression directly in Ln:35 WriteTo.OpenTelemetry(new OpenTelemetryLoggerProvider(...), disposeProvider: true): it increases the locality when folks read the code, making the code more self-contained rather than highly contextual, it avoids accidental misuse (e.g. code holding a reference to disposed object).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is the version from the other PR: https://github.com/open-telemetry/opentelemetry-dotnet/pull/3305/files#diff-eebf3c37db933ef5eddad43dc03d2553b800b073573f291b7e0ac5999a1391c8

Where I am heading with this is two different types of extensions sharing a provider.

So the question is, what do we want to demo in the example? Sharing a provider or independent providers? I feel like shared will be the more common case. But LMK your thoughts on this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here is my take:

  1. examples/LoggingExtensions/Program.cs looks an example for the end-user - who is using the OpenTelemetry.Extensions.Serilog package to pipe Serilog via OpenTelemetry plumbing. For these users, normally they will only use Serilog in a single application, it makes more sense for them to have something simple, and not to worry about object lifecycle management except for the top level CloseAndFlush.
  2. examples/LogEmitter/Program.cs is more targeting the plumbers, might make sense to expose more complexity / duty.

Copy link
Member Author

@CodeBlanch CodeBlanch Jul 15, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@reyang Let me merge and then I'll follow-up on this on my next PR where the example project is going to get more interesting?

{
options.IncludeFormattedMessage = true;
options
.SetResourceBuilder(resourceBuilder)
.AddConsoleExporter();
});

// Configure Serilog global logger
Log.Logger = new LoggerConfiguration()
.WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) // <- Register OpenTelemetry Serilog sink
.CreateLogger();

// Note: Serilog ForContext API is used to set "CategoryName" on log messages
ILogger programLogger = Log.Logger.ForContext<Program>();

programLogger.Information("Application started {Greeting} {Location}", "Hello", "World");

programLogger.Information("Message {Array}", new string[] { "value1", "value2" });

// Note: For Serilog this call flushes all logs and disposes
// OpenTelemetryLoggerProvider.
Log.CloseAndFlush();
10 changes: 10 additions & 0 deletions examples/LoggingExtensions/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# OpenTelemetry Logging Extensions Example

This project contains examples of the `LogEmitter` API being used to extend
existing logging platforms to write into OpenTelemetry logs.

* Serilog: Using OpenTelemetry.Extensions.Serilog

## References

* [OpenTelemetry Project](https://opentelemetry.io/)
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
#nullable enable
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
#nullable enable
Serilog.OpenTelemetrySerilogExtensions
static Serilog.OpenTelemetrySerilogExtensions.OpenTelemetry(this Serilog.Configuration.LoggerSinkConfiguration! loggerConfiguration, OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, bool disposeProvider = true) -> Serilog.LoggerConfiguration!
35 changes: 35 additions & 0 deletions src/OpenTelemetry.Extensions.Serilog/AssemblyInfo.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
// <copyright file="AssemblyInfo.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

using System;
using System.Runtime.CompilerServices;

[assembly: CLSCompliant(false)]
[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2" + AssemblyInfo.MoqPublicKey)]

#if SIGNED
internal static class AssemblyInfo
{
public const string PublicKey = ", PublicKey=002400000480000094000000060200000024000052534131000400000100010051C1562A090FB0C9F391012A32198B5E5D9A60E9B80FA2D7B434C9E5CCB7259BD606E66F9660676AFC6692B8CDC6793D190904551D2103B7B22FA636DCBB8208839785BA402EA08FC00C8F1500CCEF28BBF599AA64FFB1E1D5DC1BF3420A3777BADFE697856E9D52070A50C3EA5821C80BEF17CA3ACFFA28F89DD413F096F898";
public const string MoqPublicKey = ", PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7";
}
#else
internal static class AssemblyInfo
{
public const string PublicKey = "";
public const string MoqPublicKey = "";
}
#endif
5 changes: 5 additions & 0 deletions src/OpenTelemetry.Extensions.Serilog/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
# Changelog

## Unreleased

Initial release.
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
<TargetFrameworks>netstandard2.0</TargetFrameworks>
<Description>Extensions to enable OpenTelemetry logging when using the Serilog library</Description>
<PackageTags>$(PackageTags);serilog;logging</PackageTags>
<Nullable>enable</Nullable>
<AnalysisMode>AllEnabledByDefault</AnalysisMode>
<AnalysisLevel>latest</AnalysisLevel>
</PropertyGroup>

<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry\OpenTelemetry.csproj" />
</ItemGroup>

<ItemGroup>
<Compile Include="$(RepoRoot)\src\OpenTelemetry.Api\Internal\Guard.cs" Link="Includes\Guard.cs" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Serilog" Version="$(SerilogPkgVer)" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
// <copyright file="OpenTelemetrySerilogExtensions.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

using OpenTelemetry.Internal;
using OpenTelemetry.Logs;
using Serilog.Configuration;

namespace Serilog
{
/// <summary>
/// Contains Serilog extension methods.
/// </summary>
public static class OpenTelemetrySerilogExtensions
{
/// <summary>
/// Adds a sink to Serilog <see cref="LoggerConfiguration"/> which will
/// write to OpenTelemetry.
/// </summary>
/// <param name="loggerConfiguration"><see
/// cref="LoggerSinkConfiguration"/>.</param>
/// <param name="openTelemetryLoggerProvider"><see
/// cref="OpenTelemetryLoggerProvider"/>.</param>
/// <param name="disposeProvider">Controls whether or not the supplied
/// <paramref name="openTelemetryLoggerProvider"/> will be disposed when
/// the logger is disposed. Default value: <see
/// langword="true"/>.</param>
/// <returns>Supplied <see cref="LoggerConfiguration"/> for chaining calls.</returns>
public static LoggerConfiguration OpenTelemetry(
this LoggerSinkConfiguration loggerConfiguration,
OpenTelemetryLoggerProvider openTelemetryLoggerProvider,
bool disposeProvider = true)
{
Guard.ThrowIfNull(loggerConfiguration);
Guard.ThrowIfNull(openTelemetryLoggerProvider);

#pragma warning disable CA2000 // Dispose objects before losing scope
return loggerConfiguration.Sink(new OpenTelemetrySerilogSink(openTelemetryLoggerProvider, disposeProvider));
#pragma warning restore CA2000 // Dispose objects before losing scope
}
}
}
135 changes: 135 additions & 0 deletions src/OpenTelemetry.Extensions.Serilog/OpenTelemetrySerilogSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,135 @@
// <copyright file="OpenTelemetrySerilogSink.cs" company="OpenTelemetry Authors">
// 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.
// </copyright>

using System;
using System.Collections.Generic;
using System.Diagnostics;
using Microsoft.Extensions.Logging;
using Serilog.Core;
using Serilog.Events;

namespace OpenTelemetry.Logs
{
internal sealed class OpenTelemetrySerilogSink : ILogEventSink, IDisposable
{
private readonly OpenTelemetryLoggerProvider openTelemetryLoggerProvider;
private readonly bool includeFormattedMessage;
private readonly LogEmitter logEmitter;
private readonly bool disposeProvider;

public OpenTelemetrySerilogSink(OpenTelemetryLoggerProvider openTelemetryLoggerProvider, bool disposeProvider)
cijothomas marked this conversation as resolved.
Show resolved Hide resolved
{
Debug.Assert(openTelemetryLoggerProvider != null, "openTelemetryLoggerProvider was null");

this.openTelemetryLoggerProvider = openTelemetryLoggerProvider!;
this.disposeProvider = disposeProvider;

var logEmitter = this.openTelemetryLoggerProvider.CreateEmitter();
Debug.Assert(logEmitter != null, "logEmitter was null");

this.logEmitter = logEmitter!;

// TODO: This project can only access IncludeFormattedMessage
// because it can see SDK internals. At some point this is likely
// not to be the case. Need to figure out where to put
// IncludeFormattedMessage so that extensions can see it. Ideas:
// Make it public on OpenTelemetryLoggerProvider or expose it on
// LogEmitter instance.
this.includeFormattedMessage = this.openTelemetryLoggerProvider.IncludeFormattedMessage;
}

public void Emit(LogEvent logEvent)
cijothomas marked this conversation as resolved.
Show resolved Hide resolved
{
Debug.Assert(logEvent != null, "LogEvent was null.");

LogRecordData data = new(Activity.Current)
{
Timestamp = logEvent!.Timestamp.UtcDateTime,
LogLevel = (LogLevel)(int)logEvent.Level,
Message = this.includeFormattedMessage ? logEvent.RenderMessage() : logEvent.MessageTemplate.Text,
Exception = logEvent.Exception,
};

LogRecordAttributeList attributes = default;
foreach (KeyValuePair<string, LogEventPropertyValue> property in logEvent.Properties)
{
// TODO: Serilog supports complex type logging. This is not yet
// supported in OpenTelemetry.
if (property.Key == Constants.SourceContextPropertyName
&& property.Value is ScalarValue sourceContextValue)
{
data.CategoryName = sourceContextValue.Value as string;
}
else if (property.Value is ScalarValue scalarValue)
{
attributes.Add(property.Key, scalarValue.Value);
}
else if (property.Value is SequenceValue sequenceValue)
{
IReadOnlyList<LogEventPropertyValue> elements = sequenceValue.Elements;
if (elements.Count > 0)
{
// Note: The goal here is to build a typed array (eg
// int[]) if all the element types match otherwise
// fallback to object[]
Comment on lines +85 to +87
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@alanwest It was a bit tricky, but we now build a typed array when we can.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, yea looks like this does the thing. Going out on a limb here, but I'd suspect more often than not it'll all be of one type. So, most folks won't hit the perf penalty.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I didn't stress the perf too much. For two reasons. The first is I'm assuming logging arrays is kind of a rare thing? The second is Serilog is already destroying perf 🤣 Whatever you log it allocates/wraps/boxes into a spiderweb of reference types. Joined the darkside Serilog has.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🕷️ 🕸️ 🕶️ 🕸️ 🕷️


Type? elementType = null;
Array? values = null;

for (int i = 0; i < elements.Count; i++)
{
if (elements[i] is ScalarValue value)
{
Type currentElementType = value.Value?.GetType() ?? typeof(object);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If this were of type MyDisposableType then later when this log record is exported it'd throw an object disposed exception when attempting to ToString the object. The exporters catch and swallow this kind of thing today.

We don't do anything special earlier in the pipeline for attributes on traces or metrics, but is there any reason we'd want to consider coercing things into one of the primitive types earlier for log data?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not totally following you. But I don't think a complex type would end up as a ScalarValue.

I have this test at the moment:

Log.Logger.Information("Hello {greeting} {id} {@complexObj} {$complexStr}", "World", 18, complexType, complexType);

That is verifying a "destructed" complex type is currently dropped on the floor, because we don't yet have support for that. What happens in that case is instead of a ScalarValue you get a StructureValue which itself contains all the properties of the complex type represented as ScalarValues or other StructureValues, etc. I don't think disposable/mutable things are a concern because Serilog will capture all the data from the complex type and then represent it using these "value" types it has in here: https://github.com/serilog/serilog/tree/dev/src/Serilog/Events

Right now we have basic support. You can use Serilog to do structured log messaging with primitives + arrays of primitives and it will work into OTel. My goal is to get support into OTel somehow for complex types and then come back to this and properly support fully Serilog's destructuring mechanism 😄

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah thanks I missed that aspect of the test. Definitely seems like a bridge to cross later once complex types are supported. If serilog does all the work then my question is moot. I wasn't sure if a complex type could end up being added to the attributes of a log record.


if (values == null)
{
elementType = currentElementType;
values = Array.CreateInstance(elementType, elements.Count);
}
else if (!elementType!.IsAssignableFrom(currentElementType))
{
// Array with mixed types detected
object[] newValues = new object[elements.Count];
values.CopyTo(newValues, 0);
values = newValues;
elementType = typeof(object);
}

values.SetValue(value.Value, i);
}
}

if (values != null)
{
attributes.Add(property.Key, values);
}
}
}
}

this.logEmitter.Emit(in data, in attributes);
}

public void Dispose()
{
if (this.disposeProvider)
{
this.openTelemetryLoggerProvider.Dispose();
}
}
}
}
Loading