Skip to content

Commit

Permalink
Create a EventCounter Listener that subscribes to all event counters
Browse files Browse the repository at this point in the history
  • Loading branch information
hananiel committed May 18, 2022
1 parent 0b96d6f commit aac78af
Show file tree
Hide file tree
Showing 10 changed files with 572 additions and 1 deletion.
16 changes: 15 additions & 1 deletion opentelemetry-dotnet-contrib.sln
Original file line number Diff line number Diff line change
Expand Up @@ -33,8 +33,8 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{
.github\workflows\package-Exporter.Geneva.yml = .github\workflows\package-Exporter.Geneva.yml
.github\workflows\package-Exporter.Stackdriver.yml = .github\workflows\package-Exporter.Stackdriver.yml
.github\workflows\package-Extensions.AWSXRay.yml = .github\workflows\package-Extensions.AWSXRay.yml
.github\workflows\package-Extensions.PersistentStorage.yml = .github\workflows\package-Extensions.PersistentStorage.yml
.github\workflows\package-Extensions.PersistentStorage.Abstractions.yml = .github\workflows\package-Extensions.PersistentStorage.Abstractions.yml
.github\workflows\package-Extensions.PersistentStorage.yml = .github\workflows\package-Extensions.PersistentStorage.yml
.github\workflows\package-Extensions.yml = .github\workflows\package-Extensions.yml
.github\workflows\package-Instrumentation.AWS.yml = .github\workflows\package-Instrumentation.AWS.yml
.github\workflows\package-Instrumentation.AWSLambda.yml = .github\workflows\package-Instrumentation.AWSLambda.yml
Expand Down Expand Up @@ -185,6 +185,10 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentati
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Extensions.PersistentStorage.Abstractions", "src\OpenTelemetry.Extensions.PersistentStorage.Abstractions\OpenTelemetry.Extensions.PersistentStorage.Abstractions.csproj", "{17E3936A-265A-4C9F-9DD5-4568F80E6D91}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.EventCounters.Tests", "test\OpenTelemetry.Instrumentation.EventCounters.Tests\OpenTelemetry.Instrumentation.EventCounters.Tests.csproj", "{2801A9AD-3229-45E0-9758-E108E2230DC7}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "OpenTelemetry.Instrumentation.EventCounters", "src\OpenTelemetry.Instrumentation.EventCounters\OpenTelemetry.Instrumentation.EventCounters.csproj", "{7312465A-4903-425E-9DAA-44C641B94033}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -379,6 +383,14 @@ Global
{17E3936A-265A-4C9F-9DD5-4568F80E6D91}.Debug|Any CPU.Build.0 = Debug|Any CPU
{17E3936A-265A-4C9F-9DD5-4568F80E6D91}.Release|Any CPU.ActiveCfg = Release|Any CPU
{17E3936A-265A-4C9F-9DD5-4568F80E6D91}.Release|Any CPU.Build.0 = Release|Any CPU
{2801A9AD-3229-45E0-9758-E108E2230DC7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{2801A9AD-3229-45E0-9758-E108E2230DC7}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2801A9AD-3229-45E0-9758-E108E2230DC7}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2801A9AD-3229-45E0-9758-E108E2230DC7}.Release|Any CPU.Build.0 = Release|Any CPU
{7312465A-4903-425E-9DAA-44C641B94033}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{7312465A-4903-425E-9DAA-44C641B94033}.Debug|Any CPU.Build.0 = Debug|Any CPU
{7312465A-4903-425E-9DAA-44C641B94033}.Release|Any CPU.ActiveCfg = Release|Any CPU
{7312465A-4903-425E-9DAA-44C641B94033}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -437,6 +449,8 @@ Global
{BE5FFBBB-D73F-4071-92F4-F1694881604F} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63}
{ED774FC3-C1C0-44CD-BA41-686C04BEB3E5} = {2097345F-4DD3-477D-BC54-A922F9B2B402}
{17E3936A-265A-4C9F-9DD5-4568F80E6D91} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63}
{2801A9AD-3229-45E0-9758-E108E2230DC7} = {2097345F-4DD3-477D-BC54-A922F9B2B402}
{7312465A-4903-425E-9DAA-44C641B94033} = {22DF5DC0-1290-4E83-A9D8-6BB7DE3B3E63}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {B0816796-CDB3-47D7-8C3C-946434DE3B66}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,243 @@
// <copyright file="EventCounterListener.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.Concurrent;
using System.Collections.Generic;
using System.Diagnostics.Metrics;
using System.Diagnostics.Tracing;
using System.Globalization;
using System.Reflection;

namespace OpenTelemetry.Instrumentation.EventCounters
{
internal class EventCounterListener : EventListener
{
internal static readonly AssemblyName AssemblyName = typeof(EventCounterListener).Assembly.GetName();
internal static readonly string InstrumentationName = AssemblyName.Name;
internal static readonly string InstrumentationVersion = AssemblyName.Version.ToString();
private readonly EventCounterMetricsOptions options;
private readonly bool isInitialized = false;
private readonly Meter meter;
private readonly ConcurrentDictionary<MetricKey, Instrument> metricInstruments = new();
private readonly ConcurrentDictionary<MetricKey, long> lastLongValue = new();
private readonly ConcurrentDictionary<MetricKey, double> lastDoubleValue = new();

public EventCounterListener(EventCounterMetricsOptions options)
{
this.options = options;
this.meter = new Meter(InstrumentationName, InstrumentationVersion);

this.isInitialized = true;
}

private enum InstrumentType
{
Gauge,
Counter,
}

protected override void OnEventSourceCreated(EventSource source)
{
// TODO: Add Configuration options to selectively subscribe to EventCounters

try
{
var arguments = new Dictionary<string, string>
{
["EventCounterIntervalsec"] = $"{this.options.RefreshIntervalSecs}",
};
this.EnableEvents(source, EventLevel.Verbose, EventKeywords.All, arguments);
}
catch (Exception ex)
{
EventCountersInstrumentationEventSource.Log.ErrorEventCounter(source.Name, ex.Message);
}
}

protected override void OnEventWritten(EventWrittenEventArgs eventData)
{
if (!this.isInitialized || !eventData.EventName.Equals("EventCounters"))
{
return;
}

try
{

if (eventData.Payload.Count > 0 && eventData.Payload[0] is IDictionary<string, object> eventPayload)
{
this.ExtractAndPostMetric(eventData.EventSource.Name, eventPayload);
}
else
{
EventCountersInstrumentationEventSource.Log.IgnoreEventWrittenAsEventPayloadNotParseable(eventData.EventSource.Name);
}
}
catch (Exception ex)
{
EventCountersInstrumentationEventSource.Log.ErrorEventCounter(eventData.EventName, ex.ToString());
}
}

private void ExtractAndPostMetric(string eventSourceName, IDictionary<string, object> eventPayload)
{
try
{
bool calculateRate = false;
double actualValue = 0.0;
double actualInterval = 0.0;
double recordedValue = 0.0;

string counterName = string.Empty;
string counterDisplayName = string.Empty;
InstrumentType instrumentType = InstrumentType.Counter;

foreach (KeyValuePair<string, object> payload in eventPayload)
{
var key = payload.Key;
if (key.Equals("Name", StringComparison.OrdinalIgnoreCase))
{
counterName = payload.Value.ToString();
}
else
if (key.Equals("DisplayName", StringComparison.OrdinalIgnoreCase))
{
counterDisplayName = payload.Value.ToString();
}
else if (key.Equals("Mean", StringComparison.OrdinalIgnoreCase))
{
instrumentType = InstrumentType.Counter;
}
else if (key.Equals("Increment", StringComparison.OrdinalIgnoreCase))
{
// Increment indicates we have to calculate rate.
instrumentType = InstrumentType.Gauge;
calculateRate = true;
}
else if (key.Equals("IntervalSec", StringComparison.OrdinalIgnoreCase))
{
// Even though we configure 60 sec, we parse the actual duration from here. It'll be very close to the configured interval of 60.
// If for some reason this value is 0, then we default to 60 sec.
actualInterval = Convert.ToDouble(payload.Value, CultureInfo.InvariantCulture);
if (actualInterval < this.options.RefreshIntervalSecs)
{
EventCountersInstrumentationEventSource.Log.EventCounterRefreshIntervalLessThanConfigured(actualInterval, this.options.RefreshIntervalSecs);
}
}
}

if (calculateRate)
{
if (actualInterval > 0)
{
recordedValue = actualValue / actualInterval;
}
else
{
recordedValue = actualValue / this.options.RefreshIntervalSecs;
EventCountersInstrumentationEventSource.Log.EventCounterIntervalZero(counterName);
}
}
else
{
recordedValue = actualValue;
}

this.RecordMetric(eventSourceName, counterName, counterDisplayName, instrumentType, recordedValue);
}
catch (Exception ex)
{
EventCountersInstrumentationEventSource.Log.EventCountersInstrumentationWarning("ExtractMetric", ex.Message);
}
}

private void RecordMetric(string eventSourceName, string counterName, string displayName, InstrumentType instrumentType, double recordedValue)
{
var metricKey = new MetricKey(eventSourceName, counterName);
var description = string.IsNullOrEmpty(displayName) ? counterName : displayName;
bool isLong = long.TryParse(recordedValue.ToString(), out long longValue);
bool isDouble = double.TryParse(recordedValue.ToString(), out double doubleValue);

if (isLong)
{
this.lastLongValue[metricKey] = longValue;
}
else if (isDouble)
{
this.lastDoubleValue[metricKey] = doubleValue;
}

switch (instrumentType)
{
case InstrumentType.Counter when isLong:

if (!this.metricInstruments.ContainsKey(metricKey))
{
this.metricInstruments[metricKey] = this.meter.CreateObservableCounter<long>(counterName, () => this.ObserveLong(metricKey), description: description);
}

break;

case InstrumentType.Counter when isDouble:
if (!this.metricInstruments.ContainsKey(metricKey))
{
this.metricInstruments[metricKey] = this.meter.CreateObservableCounter<double>(counterName, () => this.ObserveDouble(metricKey), description: description);
}

break;

case InstrumentType.Gauge when isLong:
if (!this.metricInstruments.ContainsKey(metricKey))
{
this.metricInstruments[metricKey] = this.meter.CreateObservableGauge<long>(counterName, () => this.ObserveLong(metricKey), description: description);
}

break;
case InstrumentType.Gauge when isDouble:

if (!this.metricInstruments.TryGetValue(metricKey, out Instrument instrument))
{
this.metricInstruments[metricKey] = this.meter.CreateObservableGauge<double>(counterName, () => this.ObserveDouble(metricKey), description: description);
}

break;
}
}

private long ObserveLong(MetricKey key) => this.lastLongValue[key];

private double ObserveDouble(MetricKey key) => this.lastDoubleValue[key];

private class MetricKey
{
public MetricKey(string eventSourceName, string counterName)
{
this.EventSourceName = eventSourceName;
this.CounterName = counterName;
}

public string EventSourceName { get; private set; }

public string CounterName { get; private set; }

public override int GetHashCode() => (this.EventSourceName, this.CounterName).GetHashCode();

public override bool Equals(object obj) =>
obj is MetricKey nextKey && this.EventSourceName == nextKey.EventSourceName && this.CounterName == nextKey.CounterName;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// <copyright file="EventCounterMetricsOptions.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>

namespace OpenTelemetry.Instrumentation.EventCounters
{
/// <summary>
/// EventCounterMetrics Options.
/// </summary>
public class EventCounterMetricsOptions
{
/// <summary>
/// Gets or sets the subscription interval in seconds.
/// </summary>
public int RefreshIntervalSecs { get; set; } = 60;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
// <copyright file="EventCountersInstrumentationEventSource.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.Diagnostics.Tracing;

namespace OpenTelemetry.Instrumentation.EventCounters
{
/// <summary>
/// EventSource events emitted from the project.
/// </summary>
[EventSource(Name = "OpenTelemetry-Instrumentation-EventCounters")]
internal class EventCountersInstrumentationEventSource : EventSource
{
public static readonly EventCountersInstrumentationEventSource Log = new EventCountersInstrumentationEventSource();

[Event(1, Message = "Error occurred while processing eventCounter, EventCounter: {0}, Exception: {2}", Level = EventLevel.Error)]
public void ErrorEventCounter(string counterName, string exception)
{
this.WriteEvent(1, counterName, exception);
}

[Event(4, Level = EventLevel.Warning, Message = @"Ignoring event written from EventSource: {0} as payload is not IDictionary to extract metrics.")]
public void IgnoreEventWrittenAsEventPayloadNotParseable(string eventSourceName)
{
this.WriteEvent(4, eventSourceName);
}

[Event(6, Level = EventLevel.Warning, Message = @"EventCounter actual interval of {0} secs is less than configured interval of {1} secs.")]
public void EventCounterRefreshIntervalLessThanConfigured(double actualInterval, int configuredInterval)
{
this.WriteEvent(6, actualInterval, configuredInterval);
}

[Event(7, Level = EventLevel.Warning, Message = @"EventCounter IntervalSec is 0. Using default interval. Counter Name: {0}.")]
public void EventCounterIntervalZero(string counterName)
{
this.WriteEvent(7, counterName);
}

[Event(8, Level = EventLevel.Warning, Message = @"EventCountersInstrumentation - {0} failed with exception: {1}.")]
public void EventCountersInstrumentationWarning(string stage, string exceptionMessage)
{
this.WriteEvent(8, stage, exceptionMessage);
}
}
}
Loading

0 comments on commit aac78af

Please sign in to comment.