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] OpenTelemetry.Extensions.EventSource extensions project #3454

Merged
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions OpenTelemetry.sln
Original file line number Diff line number Diff line change
Expand Up @@ -235,6 +235,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Se
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
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.Tracing", "src\OpenTelemetry.Extensions.Tracing\OpenTelemetry.Extensions.Tracing.csproj", "{7AFB4975-9680-4668-9F5E-C3F0CA41E982}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -481,6 +483,10 @@ Global
{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
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7AFB4975-9680-4668-9F5E-C3F0CA41E982}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
<ItemGroup>
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Exporter.Console\OpenTelemetry.Exporter.Console.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Serilog\OpenTelemetry.Extensions.Serilog.csproj" />
<ProjectReference Include="$(RepoRoot)\src\OpenTelemetry.Extensions.Tracing\OpenTelemetry.Extensions.Tracing.csproj" />
</ItemGroup>

</Project>
22 changes: 16 additions & 6 deletions examples/LoggingExtensions/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,13 @@
// limitations under the License.
// </copyright>

using System.Diagnostics.Tracing;
using OpenTelemetry.Logs;
using OpenTelemetry.Resources;
using Serilog;

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

// 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 Author

Choose a reason for hiding this comment

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

@alanwest @cijothomas @utpilla One thing I realized working on tests. The Serilog + EventSource APIs ask you to pass in the OpenTelemetryLoggerProvider. If you are creating it manually, that is super easy to do. However if you are using the ILoggingBuilder extension it is tricky/non-obvious how you get the actual OpenTelemetryLoggerProvider. Maybe as a follow-up I'll see if we can smooth that out somehow?

{
options.IncludeFormattedMessage = true;
Expand All @@ -30,9 +29,18 @@
.AddConsoleExporter();
});

// Creates an OpenTelemetryEventSourceLogEmitter for routing EventSources with
// names matching OpenTelemetry* into logs
using var openTelemetryEventSourceLogEmitter = new OpenTelemetryEventSourceLogEmitter(
openTelemetryLoggerProvider, // <- Events will be written to openTelemetryLoggerProvider
(name) => name.StartsWith("OpenTelemetry") ? EventLevel.LogAlways : null,
Copy link
Member

Choose a reason for hiding this comment

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

Was it intentional that "OpenTelemetry" was used? any risk of cyclic/loops?
Might be good to create a custom EventSource-Foo or something, and use that in examples?

Copy link
Member Author

Choose a reason for hiding this comment

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

Ya it was intentional. I just added this comment to explain the behavior:

// Manually dispose OpenTelemetryLoggerProvider since it is being shared. This
// causes a log message to be written to the OpenTelemetry-Sdk EventSource which
// OpenTelemetryEventSourceLogEmitter will capture.

I'm open to changing it LMK what you think about that.

Doesn't have any cyclic or loop issues currently as OpenTelemetry.Extensions.EventSource doesn't have its own EventSource.

Copy link
Member

Choose a reason for hiding this comment

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

I see.
I meant this when I said cyclic loop:

If I enable this EventSourceLogEmitter, it'll subscribe to all ES starting with OpenTelemetry, and pipes them to OTel Logging SDK.

OTel Logging SDK internally uses ES whose name starts with OpenTelemetry. So that EventSource Logs gets piped through this LoggingSDK again, which inturn produce more log....

(or did i got it wrong fully 🏃 )

Copy link
Member

Choose a reason for hiding this comment

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

The enduser for this package is people who already EventSource for their logging, and want those piped through OTel logging. For such use-cases, the best would be to show a custom EventSource, logging to it, but it showing up in OTel logging.
Similar to the example for Serilog...

Copy link
Member Author

Choose a reason for hiding this comment

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

disposeProvider: false); // <- Do not dispose the provider with OpenTelemetryEventSourceLogEmitter since in this case it is shared with Serilog

// Configure Serilog global logger
Log.Logger = new LoggerConfiguration()
.WriteTo.OpenTelemetry(openTelemetryLoggerProvider, disposeProvider: true) // <- Register OpenTelemetry Serilog sink
.WriteTo.OpenTelemetry(
openTelemetryLoggerProvider, // <- Register OpenTelemetry Serilog sink writing to openTelemetryLoggerProvider
disposeProvider: false) // <- Do not dispose the provider with Serilog since in this case it is shared with OpenTelemetryEventSourceLogEmitter
.CreateLogger();

// Note: Serilog ForContext API is used to set "CategoryName" on log messages
Expand All @@ -42,6 +50,8 @@

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

// Note: For Serilog this call flushes all logs and disposes
// OpenTelemetryLoggerProvider.
// Note: For Serilog this call flushes all logs
Log.CloseAndFlush();

// Manually dispose OpenTelemetryLoggerProvider since it is being shared
openTelemetryLoggerProvider.Dispose();
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,4 @@
#nullable enable
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func<string!, System.Diagnostics.Tracing.EventLevel?>! shouldListenToFunc, bool disposeProvider = true) -> void
override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void
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,4 @@
#nullable enable
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter
OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.OpenTelemetryEventSourceLogEmitter(OpenTelemetry.Logs.OpenTelemetryLoggerProvider! openTelemetryLoggerProvider, System.Func<string!, System.Diagnostics.Tracing.EventLevel?>! shouldListenToFunc, bool disposeProvider = true) -> void
override OpenTelemetry.Logs.OpenTelemetryEventSourceLogEmitter.Dispose() -> void
35 changes: 35 additions & 0 deletions src/OpenTelemetry.Extensions.Tracing/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.Tracing/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,19 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- OmniSharp/VS Code requires TargetFrameworks to be in descending order for IntelliSense and analysis. -->
<TargetFrameworks>netstandard2.1;netstandard2.0</TargetFrameworks>
<Description>Extensions for using OpenTelemetry with System.Diagnostics.Tracing</Description>
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 know the name OpenTelemetry.Extensions.Tracing is going to be a lightning rod but I'll explain my thinking here.

My intention was this project would contain extensions for things in the System.Diagnostics.Tracing namespace. EventSource but also EventCounters. IMO OpenTelemetry.Instrumentation.EventCounters is misnamed.

Anyway open to alternatives just thought I would explain myself 😄

Copy link
Contributor

Choose a reason for hiding this comment

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

My intention was this project would contain extensions for things in the System.Diagnostics.Tracing namespace.

Then we could probably add Diagnostics in the package name: OpenTelemetry.Extensions.Diagnostics.Tracing because just OpenTelemetry.Extensions.Tracing might suggest that these would be extensions for OTel Traces covering things such as AutoFlushActivityProcessor

Copy link
Member

Choose a reason for hiding this comment

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

⚡ ⚡ !! Haha, yea naming is a bummer. We should all just stop using words.

Understood that System.Diagnostics.Tracing namespace involves more than EventSource, but were you envisioning that this package might one day involve itself with EventCounters somehow?

OpenTelemetry.Extensions.EventSource was my favorite of my suggestions here
#3305 (comment). But I could get behind the OpenTelemetry.Extensions.Diagnostics.Tracing or even more precisely including the whole namespace OpenTelemetry.Extensions.System.Diagnostics.Tracing ... even though it gives me a minor headache 🥴.

Copy link
Member

Choose a reason for hiding this comment

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

Could we have separate packages for OpenTelemetry.Extensions.EventCounter and OpenTelemetry.Extensions.EventSource? (e.g. I guess there are far more developers who need EventSource integration comparing to EventCounter)

I prefer separate packages because they can be deprecated independently.

Copy link
Member Author

Choose a reason for hiding this comment

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

OK I'll rename this OpenTelemetry.Extensions.EventSource and get to work on tests. @utpilla You want me to open issue to rename OpenTelemetry.Instrumentation.EventCounters -> OpenTelemetry.Extensions.EventCounter?

Copy link
Member Author

@CodeBlanch CodeBlanch Jul 21, 2022

Choose a reason for hiding this comment

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

[This reply is to @utpilla + @cijothomas I had it open for a while and didn't see @alanwest sneak in there. I'm good with punting on this discussion.]

Hmm interesting. Hard time wrapping my mind around this. Is this definition from the spec somewhere? User plugging this stuff in is doing so at the host level so I'm not sure if the API/SDK distinction is useful.

Just eyeballing this list...

image

Which thing is not like the others? 🍎 🍎 🍎 🍌 🤣

OpenTelemetry.Instrumentation.AspNet - Emits telemetry about AspNet
OpenTelemetry.Instrumentation.MassTransit - Emits telemetry about MassTransit
OpenTelemetry.Instrumentation.Runtime - Emits telemetry about.NET runtime
OpenTelemetry.Instrumentation.StackExchangeRedis - Emits telemetry about Redis
...etc...
OpenTelemetry.Instrumentation.EventCounters - Transposes telemetry written about whatever using EventCounters into System.Diagnostics.DiagnosticSource telemetry. Does not emit telemetry about EventCounters

IMO the HOW it is done is less important than WHAT it is doing.

Copy link
Member

Choose a reason for hiding this comment

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

Is this definition from the spec somewhere?

No. My view !
Only spec defined term is https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library

We need to definitely discuss this more.

Copy link
Member

Choose a reason for hiding this comment

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

IMO the HOW it is done is less important than WHAT it is doing.

💯

Copy link
Member

Choose a reason for hiding this comment

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

Yes 💯 to that as well. I think I've captured the spirit of WHAT in the issue I just opened #3469.

Also, per my thoughts in #3469, I've changed my mind and I think this package should be OpenTelemetry.Shims.EventSource 😆.

Copy link
Member

Choose a reason for hiding this comment

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

Which thing is not like the others? 🍎 🍎 🍎 🍌 🤣

Also, rename OpenTelemetry.Extensions.EventCounter -> OpenTelemetry.🍌

<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>

</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,224 @@
// <copyright file="OpenTelemetryEventSourceLogEmitter.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 System.Diagnostics.Tracing;
using System.Globalization;
using System.Linq;
using Microsoft.Extensions.Logging;
using OpenTelemetry.Internal;

namespace OpenTelemetry.Logs
{
/// <summary>
/// Implements an <see cref="EventListener"/> which will convert <see
/// cref="EventSource"/> events into OpenTelemetry logs.
/// </summary>
public sealed class OpenTelemetryEventSourceLogEmitter : EventListener
{
private readonly bool includeFormattedMessage;
private readonly OpenTelemetryLoggerProvider openTelemetryLoggerProvider;
private readonly LogEmitter logEmitter;
private readonly object lockObj = new();
private readonly Func<string, EventLevel?> shouldListenToFunc;
private readonly List<EventSource> eventSources = new();
private readonly List<EventSource>? eventSourcesBeforeConstructor = new();
private readonly bool disposeProvider;

/// <summary>
/// Initializes a new instance of the <see
/// cref="OpenTelemetryEventSourceLogEmitter"/> class.
/// </summary>
/// <param name="openTelemetryLoggerProvider"><see
/// cref="OpenTelemetryLoggerProvider"/>.</param>
/// <param name="shouldListenToFunc">Callback function used to decide if
/// events should be captured for a given <see
/// cref="EventSource.Name"/>. Return <see langword="null"/> if no
/// events should be captured.</param>
/// <param name="disposeProvider">Controls whether or not the supplied
/// <paramref name="openTelemetryLoggerProvider"/> will be disposed when
/// the <see cref="EventListener"/> is disposed. Default value: <see
/// langword="true"/>.</param>
public OpenTelemetryEventSourceLogEmitter(
OpenTelemetryLoggerProvider openTelemetryLoggerProvider,
Func<string, EventLevel?> shouldListenToFunc,
bool disposeProvider = true)
{
Guard.ThrowIfNull(openTelemetryLoggerProvider);
Guard.ThrowIfNull(shouldListenToFunc);

this.includeFormattedMessage = openTelemetryLoggerProvider.IncludeFormattedMessage;
this.openTelemetryLoggerProvider = openTelemetryLoggerProvider!;
this.disposeProvider = disposeProvider;
this.shouldListenToFunc = shouldListenToFunc;

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

this.logEmitter = logEmitter!;

lock (this.lockObj)
{
foreach (EventSource eventSource in this.eventSourcesBeforeConstructor)
cijothomas marked this conversation as resolved.
Show resolved Hide resolved
{
this.ProcessSource(eventSource);
}

this.eventSourcesBeforeConstructor = null;
}
}

/// <inheritdoc/>
public override void Dispose()
{
foreach (EventSource eventSource in this.eventSources)
{
this.DisableEvents(eventSource);
}

this.eventSources.Clear();

if (this.disposeProvider)
{
this.openTelemetryLoggerProvider.Dispose();
}

base.Dispose();
}

#pragma warning disable CA1062 // Validate arguments of public methods
/// <inheritdoc/>
protected override void OnEventSourceCreated(EventSource eventSource)
{
Debug.Assert(eventSource != null, "EventSource was null.");

try
{
if (this.eventSourcesBeforeConstructor != null)
{
lock (this.lockObj)
{
if (this.eventSourcesBeforeConstructor != null)
{
this.eventSourcesBeforeConstructor.Add(eventSource!);
return;
}
}
}

this.ProcessSource(eventSource!);
}
finally
{
base.OnEventSourceCreated(eventSource);
}
}
#pragma warning restore CA1062 // Validate arguments of public methods

#pragma warning disable CA1062 // Validate arguments of public methods
/// <inheritdoc/>
protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
Debug.Assert(eventData != null, "EventData was null.");

string? rawMessage = eventData!.Message;

LogRecordData data = new(Activity.Current)
{
#if NETSTANDARD2_1_OR_GREATER
Timestamp = eventData.TimeStamp,
#endif
EventId = new EventId(eventData.EventId, eventData.EventName),
LogLevel = ConvertEventLevelToLogLevel(eventData.Level),
};

LogRecordAttributeList attributes = default;

attributes.Add("event_source.name", eventData.EventSource.Name);

if (eventData.ActivityId != Guid.Empty)
{
attributes.Add("event_source.activity_id", eventData.ActivityId);
}

if (eventData.RelatedActivityId != Guid.Empty)
{
attributes.Add("event_source.related_activity_id", eventData.RelatedActivityId);
}

int payloadCount = eventData.Payload?.Count ?? 0;

if (payloadCount > 0 && payloadCount == eventData.PayloadNames?.Count)
{
for (int i = 0; i < payloadCount; i++)
{
string name = eventData.PayloadNames[i];

if (!string.IsNullOrEmpty(rawMessage) && !this.includeFormattedMessage)
{
// TODO: This code converts the event message from
// string.Format syntax (eg: "Some message {0} {1}")
// into structured log format (eg: "Some message
// {propertyName1} {propertyName2}") but it is
// expensive. Probably needs a cache.
#if NETSTANDARD2_0
rawMessage = rawMessage.Replace($"{{{i}}}", $"{{{name}}}");
#else
rawMessage = rawMessage.Replace($"{{{i}}}", $"{{{name}}}", StringComparison.Ordinal);
#endif
}

attributes.Add(name, eventData.Payload![i]);
}
}

if (!string.IsNullOrEmpty(rawMessage) && this.includeFormattedMessage && payloadCount > 0)
{
rawMessage = string.Format(CultureInfo.InvariantCulture, rawMessage, eventData.Payload!.ToArray());
}

data.Message = rawMessage;

this.logEmitter.Emit(in data, in attributes);
}
#pragma warning restore CA1062 // Validate arguments of public methods

private static LogLevel ConvertEventLevelToLogLevel(EventLevel eventLevel)
{
return eventLevel switch
{
EventLevel.Informational => LogLevel.Information,
EventLevel.Warning => LogLevel.Warning,
EventLevel.Error => LogLevel.Error,
EventLevel.Critical => LogLevel.Critical,
_ => LogLevel.Trace,
};
}

private void ProcessSource(EventSource eventSource)
{
EventLevel? eventLevel = this.shouldListenToFunc(eventSource.Name);

if (eventLevel.HasValue)
{
this.eventSources.Add(eventSource);
this.EnableEvents(eventSource, eventLevel.Value, EventKeywords.All);
}
}
}
}
Loading