diff --git a/.github/component_owners.yml b/.github/component_owners.yml index 57fe851901..8bec8de3df 100644 --- a/.github/component_owners.yml +++ b/.github/component_owners.yml @@ -37,6 +37,7 @@ components: - ejsmith src/OpenTelemetry.Instrumentation.EventCounters/: - hananiel + - mic-max src/OpenTelemetry.Instrumentation.GrpcCore/: - pcwiese src/OpenTelemetry.Instrumentation.Hangfire/: @@ -99,6 +100,7 @@ components: - ejsmith test/OpenTelemetry.Instrumentation.EventCounters.Tests/: - hananiel + - mic-max test/OpenTelemetry.Instrumentation.GrpcCore.Tests/: - pcwiese test/OpenTelemetry.Instrumentation.Hangfire.Tests/: diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/CHANGELOG.md b/src/OpenTelemetry.Instrumentation.EventCounters/CHANGELOG.md index 85f97c807a..165812f4e6 100644 --- a/src/OpenTelemetry.Instrumentation.EventCounters/CHANGELOG.md +++ b/src/OpenTelemetry.Instrumentation.EventCounters/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## Unreleased + +* Simplified implementation. EventSources must be explicitly configured to be + listened to now. + ([#620](https://github.com/open-telemetry/opentelemetry-dotnet-contrib/pull/620)) + ## 0.1.0-alpha.1 Released 2022-Jul-12 diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterListener.cs b/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterListener.cs deleted file mode 100644 index b8ee562d94..0000000000 --- a/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterListener.cs +++ /dev/null @@ -1,189 +0,0 @@ -// -// 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.Concurrent; -using System.Collections.Generic; -using System.Diagnostics.Metrics; -using System.Diagnostics.Tracing; -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 bool isInitialized = false; - private readonly Meter meter; - private readonly EventCounterMetricsOptions options; - private readonly ConcurrentDictionary metricInstruments = new(); - private readonly ConcurrentDictionary lastValue = new(); - private readonly ConcurrentBag eventSources = new(); - - public EventCounterListener(EventCounterMetricsOptions options) - { - this.meter = new Meter(InstrumentationName, InstrumentationVersion); - this.options = options ?? throw new ArgumentNullException(nameof(options)); - - this.isInitialized = true; - this.EnablePendingEventSources(); // Some OnEventSourceCreated may have fired before constructor, enable them - } - - private enum InstrumentType - { - ObservableGauge, - ObservableCounter, - } - - private Dictionary EnableEventArgs => new() { ["EventCounterIntervalSec"] = this.options.RefreshIntervalSecs.ToString(), }; - - protected override void OnEventSourceCreated(EventSource source) - { - // TODO: Add Configuration options to selectively subscribe to EventCounters - try - { - if (!this.isInitialized) - { - this.eventSources.Add(source); - } - else - { - this.EnableEvents(source, EventLevel.Verbose, EventKeywords.All, this.EnableEventArgs); - } - } - catch (Exception ex) - { - EventCountersInstrumentationEventSource.Log.ErrorEventCounter(source.Name, ex.Message); - } - } - - protected override void OnEventWritten(EventWrittenEventArgs eventData) - { - if (!eventData.EventName.Equals("EventCounters") || !this.isInitialized) - { - return; - } - - try - { - if (eventData.Payload.Count > 0 && eventData.Payload[0] is IDictionary 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 eventPayload) - { - try - { - bool calculateRate = false; - double actualValue = 0; - - string counterName = string.Empty; - string counterDisplayName = string.Empty; - InstrumentType instrumentType = InstrumentType.ObservableGauge; - - foreach (KeyValuePair 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.ObservableGauge; - actualValue = Convert.ToDouble(payload.Value); - } - else if (key.Equals("Increment", StringComparison.OrdinalIgnoreCase)) - { - // Increment indicates we have to calculate rate. - instrumentType = InstrumentType.ObservableCounter; - calculateRate = true; - actualValue = Convert.ToDouble(payload.Value); - } - } - - this.RecordMetric(eventSourceName, counterName, counterDisplayName, instrumentType, actualValue, calculateRate); - } - catch (Exception ex) - { - EventCountersInstrumentationEventSource.Log.EventCountersInstrumentationWarning("ExtractMetric", ex.Message); - } - } - - private void RecordMetric(string eventSourceName, string counterName, string displayName, InstrumentType instrumentType, double value, bool calculateRate) - { - var metricKey = new MetricKey(eventSourceName, counterName); - var description = string.IsNullOrEmpty(displayName) ? counterName : displayName; - this.lastValue[metricKey] = calculateRate ? value / this.options.RefreshIntervalSecs : value; - switch (instrumentType) - { - case InstrumentType.ObservableCounter: - this.metricInstruments.TryAdd(metricKey, this.meter.CreateObservableCounter(counterName, () => this.ObserveDouble(metricKey), description: description)); - break; - - case InstrumentType.ObservableGauge: - this.metricInstruments.TryAdd(metricKey, this.meter.CreateObservableGauge(counterName, () => this.ObserveDouble(metricKey), description: description)); - break; - } - } - - private double ObserveDouble(MetricKey key) => this.lastValue[key]; - - private void EnablePendingEventSources() - { - foreach (var source in this.eventSources) - { - this.EnableEvents(source, EventLevel.Verbose, EventKeywords.All, this.EnableEventArgs); - } - } - - 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; - } -} diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterMetricsOptions.cs b/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterMetricsOptions.cs deleted file mode 100644 index 0152da017e..0000000000 --- a/src/OpenTelemetry.Instrumentation.EventCounters/EventCounterMetricsOptions.cs +++ /dev/null @@ -1,28 +0,0 @@ -// -// 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. -// - -namespace OpenTelemetry.Instrumentation.EventCounters; - -/// -/// EventCounterMetrics Options. -/// -public class EventCounterMetricsOptions -{ - /// - /// Gets or sets the subscription interval in seconds. - /// - public int RefreshIntervalSecs { get; set; } = 60; -} diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationEventSource.cs b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationEventSource.cs index f3d0d431ce..071f963352 100644 --- a/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationEventSource.cs +++ b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationEventSource.cs @@ -22,25 +22,37 @@ namespace OpenTelemetry.Instrumentation.EventCounters; /// EventSource events emitted from the project. /// [EventSource(Name = "OpenTelemetry-Instrumentation-EventCounters")] -internal class EventCountersInstrumentationEventSource : EventSource +internal sealed class EventCountersInstrumentationEventSource : EventSource { public static readonly EventCountersInstrumentationEventSource Log = new(); - [Event(1, Message = "Error occurred while processing eventCounter, EventCounter: {0}, Exception: {2}", Level = EventLevel.Error)] - public void ErrorEventCounter(string counterName, string exception) + [Event(1, Level = EventLevel.Warning, Message = "Error while writing event from source: {0} - {1}.")] + internal void ErrorWhileWritingEvent(string eventSourceName, string exceptionMessage) { - this.WriteEvent(1, counterName, exception); + this.WriteEvent(1, eventSourceName, exceptionMessage); } - [Event(2, Level = EventLevel.Warning, Message = @"Ignoring event written from EventSource: {0} as payload is not IDictionary to extract metrics.")] - public void IgnoreEventWrittenAsEventPayloadNotParseable(string eventSourceName) + [Event(2, Level = EventLevel.Warning, Message = "Event data payload not parseable from source: {0}.")] + internal void IgnoreEventWrittenEventArgsPayloadNotParseable(string eventSourceName) + { + this.WriteEvent(2, eventSourceName); + } + + [Event(3, Level = EventLevel.Warning, Message = "Event data has no name from source: {0}.")] + internal void IgnoreEventWrittenEventArgsWithoutName(string eventSourceName) + { + this.WriteEvent(3, eventSourceName); + } + + [Event(4, Level = EventLevel.Warning, Message = "Event data payload problem with values of Mean, Increment from source: {0}.")] + internal void IgnoreMeanIncrementConflict(string eventSourceName) { this.WriteEvent(4, eventSourceName); } - [Event(3, Level = EventLevel.Warning, Message = @"EventCountersInstrumentation - {0} failed with exception: {1}.")] - public void EventCountersInstrumentationWarning(string stage, string exceptionMessage) + [Event(5, Level = EventLevel.Warning, Message = "Event data has name other than 'EventCounters' from source: {0}.")] + internal void IgnoreNonEventCountersName(string eventSourceName) { - this.WriteEvent(8, stage, exceptionMessage); + this.WriteEvent(5, eventSourceName); } } diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationOptions.cs b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationOptions.cs new file mode 100644 index 0000000000..87e1a93e75 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersInstrumentationOptions.cs @@ -0,0 +1,59 @@ +// +// 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.Linq; + +namespace OpenTelemetry.Instrumentation.EventCounters; + +/// +/// EventCounterListener Options. +/// +public class EventCountersInstrumentationOptions +{ + private readonly HashSet eventSourceNames = new(); + + /// + /// Gets or sets the subscription interval in seconds for reading values + /// from the configured EventCounters. + /// + public int RefreshIntervalSecs { get; set; } = 1; + + /// + /// Listens to EventCounters from the given EventSource name. + /// + /// The EventSource names to listen to. + public void AddEventSources(params string[] names) + { + if (names.Contains("System.Runtime")) + { + throw new NotSupportedException("Use the `OpenTelemetry.Instrumentation.Runtime` or `OpenTelemetry.Instrumentation.Process` instrumentations."); + } + + this.eventSourceNames.UnionWith(names); + } + + /// + /// Returns whether or not an EventSource should be enabled on the EventListener. + /// + /// The EventSource name. + /// true when an EventSource with the name should be enabled. + internal bool ShouldListenToSource(string eventSourceName) + { + return this.eventSourceNames.Contains(eventSourceName); + } +} diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersMetrics.cs b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersMetrics.cs new file mode 100644 index 0000000000..e01ce8b968 --- /dev/null +++ b/src/OpenTelemetry.Instrumentation.EventCounters/EventCountersMetrics.cs @@ -0,0 +1,137 @@ +// +// 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.Concurrent; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Diagnostics.Tracing; + +namespace OpenTelemetry.Instrumentation.EventCounters; + +/// +/// .NET EventCounters Instrumentation. +/// +internal sealed class EventCountersMetrics : EventListener +{ + internal static readonly Meter MeterInstance = new(typeof(EventCountersMetrics).Assembly.GetName().Name, typeof(EventCountersMetrics).Assembly.GetName().Version.ToString()); + + private readonly EventCountersInstrumentationOptions options; + private readonly ConcurrentQueue preInitEventSources = new(); + private readonly ConcurrentDictionary<(string, string), Instrument> instruments = new(); + private readonly ConcurrentDictionary<(string, string), double> values = new(); + + /// + /// Initializes a new instance of the class. + /// + /// The options to define the metrics. + public EventCountersMetrics(EventCountersInstrumentationOptions options) + { + this.options = options; + + while (this.preInitEventSources.TryDequeue(out EventSource eventSource)) + { + if (this.options.ShouldListenToSource(eventSource.Name)) + { + this.EnableEvents(eventSource, EventLevel.LogAlways, EventKeywords.None, GetEnableEventsArguments(this.options)); + } + } + } + + /// + protected override void OnEventSourceCreated(EventSource source) + { + if (this.options == null) + { + this.preInitEventSources.Enqueue(source); + } + else if (this.options.ShouldListenToSource(source.Name)) + { + this.EnableEvents(source, EventLevel.LogAlways, EventKeywords.None, GetEnableEventsArguments(this.options)); + } + } + + /// + protected override void OnEventWritten(EventWrittenEventArgs eventData) + { + if (this.options == null) + { + return; + } + + var eventSourceName = eventData.EventSource.Name; + + if (eventData.EventName != "EventCounters") + { + EventCountersInstrumentationEventSource.Log.IgnoreNonEventCountersName(eventSourceName); + return; + } + + if (eventData.Payload == null || eventData.Payload.Count == 0 || eventData.Payload[0] is not IDictionary payload) + { + EventCountersInstrumentationEventSource.Log.IgnoreEventWrittenEventArgsPayloadNotParseable(eventSourceName); + return; + } + + var hasName = payload.TryGetValue("Name", out var nameObj); + + if (!hasName) + { + EventCountersInstrumentationEventSource.Log.IgnoreEventWrittenEventArgsWithoutName(eventSourceName); + return; + } + + var name = nameObj.ToString(); + + var hasMean = payload.TryGetValue("Mean", out var mean); + var hasIncrement = payload.TryGetValue("Increment", out var increment); + + if (!(hasIncrement ^ hasMean)) + { + EventCountersInstrumentationEventSource.Log.IgnoreMeanIncrementConflict(eventSourceName); + return; + } + + var value = Convert.ToDouble(hasMean ? mean : increment); + this.UpdateInstrumentWithEvent(hasMean, eventSourceName, name, value); + } + + private static Dictionary GetEnableEventsArguments(EventCountersInstrumentationOptions options) => + new() { { "EventCounterIntervalSec", options.RefreshIntervalSecs.ToString() } }; + + private void UpdateInstrumentWithEvent(bool isGauge, string eventSourceName, string name, double value) + { + try + { + ValueTuple metricKey = new(eventSourceName, name); + _ = this.values.AddOrUpdate(metricKey, value, isGauge ? (_, _) => value : (_, existing) => existing + value); + + var instrumentName = $"EventCounters.{eventSourceName}.{name}"; + + if (!this.instruments.ContainsKey(metricKey)) + { + Instrument instrument = isGauge + ? MeterInstance.CreateObservableGauge(instrumentName, () => this.values[metricKey]) + : MeterInstance.CreateObservableCounter(instrumentName, () => this.values[metricKey]); + _ = this.instruments.TryAdd(metricKey, instrument); + } + } + catch (Exception ex) + { + EventCountersInstrumentationEventSource.Log.ErrorWhileWritingEvent(eventSourceName, ex.Message); + } + } +} diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/MeterProviderBuilderExtensions.cs b/src/OpenTelemetry.Instrumentation.EventCounters/MeterProviderBuilderExtensions.cs index bb13a85281..daf35c18ef 100644 --- a/src/OpenTelemetry.Instrumentation.EventCounters/MeterProviderBuilderExtensions.cs +++ b/src/OpenTelemetry.Instrumentation.EventCounters/MeterProviderBuilderExtensions.cs @@ -31,17 +31,17 @@ public static class MeterProviderBuilderExtensions /// being configured. /// Runtime metrics options. /// The instance of to chain the calls. - public static MeterProviderBuilder AddEventCounterMetrics( + public static MeterProviderBuilder AddEventCountersInstrumentation( this MeterProviderBuilder builder, - Action configure = null) + Action configure = null) { Guard.ThrowIfNull(builder); - var options = new EventCounterMetricsOptions(); + var options = new EventCountersInstrumentationOptions(); configure?.Invoke(options); - var instrumentation = new EventCounterListener(options); - builder.AddMeter(EventCounterListener.InstrumentationName); + var instrumentation = new EventCountersMetrics(options); + builder.AddMeter(EventCountersMetrics.MeterInstance.Name); return builder.AddInstrumentation(() => instrumentation); } } diff --git a/src/OpenTelemetry.Instrumentation.EventCounters/README.md b/src/OpenTelemetry.Instrumentation.EventCounters/README.md index 7ddad17e82..059f79cc32 100644 --- a/src/OpenTelemetry.Instrumentation.EventCounters/README.md +++ b/src/OpenTelemetry.Instrumentation.EventCounters/README.md @@ -1,7 +1,11 @@ -# Event counter instrumentation for OpenTelemetry +# Event Counters Instrumentation for OpenTelemetry .NET -This is an [Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library), -which republishes EventCounters using Metrics Api. +[![NuGet](https://img.shields.io/nuget/v/OpenTelemetry.Instrumentation.EventCounters.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.EventCounters) +[![NuGet](https://img.shields.io/nuget/dt/OpenTelemetry.Instrumentation.EventCounters.svg)](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.EventCounters) + +This is an +[Instrumentation Library](https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/glossary.md#instrumentation-library) +, which **republishes EventCounters using Metrics Api.** ## Steps to enable OpenTelemetry.Instrumentation.EventCounters @@ -9,62 +13,56 @@ which republishes EventCounters using Metrics Api. Add a reference to the [`OpenTelemetry.Instrumentation.EventCounters`](https://www.nuget.org/packages/OpenTelemetry.Instrumentation.EventCounters) -package. Also, add any other instrumentation & exporters you will need. +package. ```shell dotnet add package OpenTelemetry.Instrumentation.EventCounters ``` -### Step 2: Enable EventCounters Instrumentation at application startup - -EventCounters instrumentation must be enabled at application startup. +### Step 2: Enable EventCounters Instrumentation -The following example demonstrates adding EventCounter events to a -console application. This example also sets up the OpenTelemetry Console -exporter, which requires adding the package -[`OpenTelemetry.Exporter.Console`](https://www.nuget.org/packages/OpenTelemetry.Exporter.Console) -to the application. +EventCounters instrumentation should be enabled at application startup using the +`AddEventCountersInstrumentation` extension on the `MeterProviderBuilder`: ```csharp -using OpenTelemetry; -using OpenTelemetry.Metrics; - -namespace DotnetMetrics; - -public class Program -{ - public static void Main(string[] args) - { - using var meterprovider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = 5; - }) - .AddConsoleExporter() - .Build(); - } +using var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => { + options.RefreshIntervalSecs = 1; + options.Sources.Add("MyEventSourceName"); + }) + .AddPrometheusExporter() + .Build(); } ``` -Console Output: - -```console +Additionally, this examples sets up the OpenTelemetry Prometheus exporter, which +requires adding the package +[`OpenTelemetry.Exporter.Prometheus`](https://github.com/open-telemetry/opentelemetry-dotnet/blob/main/src/OpenTelemetry.Exporter.Prometheus.HttpListener/README.md) +to the application. -Export cpu-usage, CPU Usage, Meter: OpenTelemetry.Instrumentation.EventCounters/0.0.0.0 -(2022-07-12T16:40:37.2639447Z, 2022-07-12T16:40:42.2533747Z] DoubleGauge -Value: 0 +### Step 3: Create EventCounters -Export working-set, Working Set, Meter: OpenTelemetry.Instrumentation.EventCounters/0.0.0.0 -(2022-07-12T16:40:37.2666398Z, 2022-07-12T16:40:42.2534452Z] DoubleGauge -Value: 38 +Learn about +[EventCounters in .NET](https://docs.microsoft.com/en-us/dotnet/core/diagnostics/event-counters) +. -Export gc-heap-size, GC Heap Size, Meter: OpenTelemetry.Instrumentation.EventCounters/0.0.0.0 -(2022-07-12T16:40:37.2667389Z, 2022-07-12T16:40:42.2534456Z] DoubleGauge -Value: 7 +```csharp +EventSource eventSource = new("MyEventSource"); +EventCounter eventCounter = new("MyEventCounterName", eventSource); +eventCounter.WriteMetric(0); +eventCounter.WriteMetric(1000); +PollingCounter pollingCounter = new("MyPollingCounterName", eventSource, () => new Random().NextDouble()); ``` +## Notes + +The metrics will only be available after `EventCounterIntervalSec` seconds. +Before that nothing will be exported, if anything is present at the Prometheus +metrics endpoint it is from a prior execution. This is more evident when using +longer polling intervals. + ## References * [OpenTelemetry Project](https://opentelemetry.io/) diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCounterListenerTests.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCounterListenerTests.cs deleted file mode 100644 index eab325222c..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCounterListenerTests.cs +++ /dev/null @@ -1,199 +0,0 @@ -// -// 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.Collections.Generic; -using System.Threading.Tasks; -using OpenTelemetry.Metrics; -using Xunit; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -public class EventCounterListenerTests -{ - private const int MaxTimeToAllowForFlush = 10000; - private MeterProvider meterProvider; - - [Fact] - public async Task SystemMetricsAreCaptured() - { - var metricItems = new List(); - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = 1; - }) - .AddInMemoryExporter(metricItems) - .Build(); - - await Task.Delay(2000); - this.meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - this.meterProvider.Dispose(); - - Assert.True(metricItems.Count > 1); - } - - [Fact(Skip = "Unstable")] - public async Task TestEventCounterMetricsAreCaptured() - { - const int refreshIntervalSeconds = 1; - var metricItems = new List(); - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = 1; - }) - .AddInMemoryExporter(metricItems) - .Build(); - var expected = new[] { 100.3F, 200.1F }; - TestEventCounter.Log.SampleCounter1(expected[0]); - TestEventCounter.Log.SampleCounter2(expected[1]); - - // Wait a little bit over the refresh interval seconds - await Task.Delay((refreshIntervalSeconds * 1000) + 300); - - this.meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - this.meterProvider.Dispose(); - - var counter1 = metricItems.Find(m => m.Name == "mycountername1"); - var counter2 = metricItems.Find(m => m.Name == "mycountername2"); - Assert.NotNull(counter1); - Assert.NotNull(counter2); - Assert.Equal(MetricType.DoubleGauge, counter1.MetricType); // EventCounter CounterType is `Mean` - - Assert.Equal(expected[0], GetActualValue(counter1)); - Assert.Equal(expected[1], GetActualValue(counter2)); - } - - [Fact(Skip = "Unstable")] - public async Task TestIncrementingEventCounterMetricsAreCaptured() - { - const int refreshIntervalSeconds = 1; - var metricItems = new List(); - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = 1; - }) - .AddInMemoryExporter(metricItems) - .Build(); - - TestIncrementingEventCounter.Log.SampleCounter1(1); - TestIncrementingEventCounter.Log.SampleCounter1(1); - TestIncrementingEventCounter.Log.SampleCounter1(1); - - // Wait a little bit over the refresh interval seconds - await Task.Delay((refreshIntervalSeconds * 1000) + 300); - - this.meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - this.meterProvider.Dispose(); - - var counter = metricItems.Find(m => m.Name == TestIncrementingEventCounter.CounterName); - Assert.NotNull(counter); - Assert.Equal(MetricType.DoubleSum, counter.MetricType); // EventCounter CounterType is `Sum` - Assert.Equal(3, GetActualValue(counter)); - } - - [Fact(Skip = "Unstable")] - public async Task TestPollingCounterMetricsAreCaptured() - { - var metricItems = new List(); - const int refreshIntervalSeconds = 1; - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = refreshIntervalSeconds; - }) - .AddInMemoryExporter(metricItems) - .Build(); - - int i = 0; - TestPollingEventCounter.CreateSingleton(() => ++i * 10); - - var duration = (refreshIntervalSeconds * 2 * 1000) + 300; // Wait for two refresh intervals to call the valueProvider twice - await Task.Delay(duration); - - this.meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - this.meterProvider.Dispose(); - - var pollingCounter = metricItems.Find(m => m.Name == TestPollingEventCounter.CounterName); - Assert.NotNull(pollingCounter); - Assert.Equal(MetricType.DoubleGauge, pollingCounter.MetricType); // Polling Counter is EventCounter CounterType of `Mean` - - var expected = i * 10; // The last recorded `Mean` value - Assert.Equal(expected, GetActualValue(pollingCounter)); - } - - [Fact(Skip = "Unstable")] - public async Task TestIncrementingPollingCounterMetrics() - { - var metricItems = new List(); - const int refreshIntervalSeconds = 1; - - this.meterProvider = Sdk.CreateMeterProviderBuilder() - .AddEventCounterMetrics(options => - { - options.RefreshIntervalSecs = refreshIntervalSeconds; - }) - .AddInMemoryExporter(metricItems) - .Build(); - - int i = 1; - - TestIncrementingPollingCounter.CreateSingleton(() => i++); - - var duration = (refreshIntervalSeconds * 2 * 1000) + 300; // Wait for two refresh intervals to call the valueProvider twice - await Task.Delay(duration); - - this.meterProvider.ForceFlush(MaxTimeToAllowForFlush); - - this.meterProvider.Dispose(); - - var pollingCounter = metricItems.Find(m => m.Name == TestIncrementingPollingCounter.CounterName); - Assert.NotNull(pollingCounter); - Assert.Equal(MetricType.DoubleSum, pollingCounter.MetricType); // Polling Counter is EventCounter CounterType of `Sum` - - Assert.Equal(1, GetActualValue(pollingCounter)); - } - - /// - /// Event Counters are always Sum or Mean and are always record with `float`. - /// - /// Metric to Aggregate. - /// The Aggregated value. - private static double GetActualValue(Metric metric) - { - double sum = 0; - foreach (ref readonly var metricPoint in metric.GetMetricPoints()) - { - if (metric.MetricType.IsSum()) - { - sum += metricPoint.GetSumDouble(); - } - else - { - sum += metricPoint.GetGaugeLastValueDouble(); - } - } - - return sum; - } -} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCountersMetricsTests.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCountersMetricsTests.cs new file mode 100644 index 0000000000..748e849ed5 --- /dev/null +++ b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/EventCountersMetricsTests.cs @@ -0,0 +1,232 @@ +// +// 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.Tracing; +using System.Threading.Tasks; +using OpenTelemetry.Metrics; +using Xunit; + +namespace OpenTelemetry.Instrumentation.EventCounters.Tests; + +public class EventCountersMetricsTests +{ + private const int Delay = 1200; + + [Fact(Skip = "Other tests metrics are being exported here")] + public async Task NoMetricsByDefault() + { + // Arrange + List metricItems = new(); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation() + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + await Task.Delay(Delay); + meterProvider.ForceFlush(); + + // Assert + Assert.Empty(metricItems); + } + + [Fact] + public async Task EventCounter() + { + // Arrange + List metricItems = new(); + EventSource source = new("a"); + EventCounter counter = new("c", source); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources(source.Name); + }) + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + counter.WriteMetric(1997.0202); + await Task.Delay(Delay); + meterProvider.ForceFlush(); + + // Assert + var metric = metricItems.Find(x => x.Name == "EventCounters.a.c"); + Assert.NotNull(metric); + Assert.Equal(MetricType.DoubleGauge, metric.MetricType); + Assert.Equal(1997.0202, GetActualValue(metric)); + } + + [Fact] + public async Task IncrementingEventCounter() + { + // Arrange + List metricItems = new(); + EventSource source = new("b"); + IncrementingEventCounter incCounter = new("inc-c", source); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources(source.Name); + }) + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + incCounter.Increment(1); + incCounter.Increment(1); + incCounter.Increment(1); + await Task.Delay(Delay); + meterProvider.ForceFlush(); + + // Assert + var metric = metricItems.Find(x => x.Name == "EventCounters.b.inc-c"); + Assert.NotNull(metric); + Assert.Equal(MetricType.DoubleSum, metric.MetricType); + Assert.Equal(3, GetActualValue(metric)); + } + + [Fact] + public async Task PollingCounter() + { + // Arrange + int i = 0; + List metricItems = new(); + EventSource source = new("c"); + PollingCounter pollCounter = new("poll-c", source, () => ++i * 10); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources(source.Name); + }) + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + await Task.Delay(Delay * 2); + meterProvider.ForceFlush(); + + // Assert + var metric = metricItems.Find(x => x.Name == "EventCounters.c.poll-c"); + Assert.NotNull(metric); + Assert.Equal(MetricType.DoubleGauge, metric.MetricType); + Assert.Equal(20, GetActualValue(metric)); + } + + [Fact] + public async Task IncrementingPollingCounter() + { + // Arrange + int i = 1; + List metricItems = new(); + EventSource source = new("d"); + IncrementingPollingCounter incPollCounter = new("inc-poll-c", source, () => i++); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources(source.Name); + }) + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + await Task.Delay(Delay * 2); + meterProvider.ForceFlush(); + + // Assert + var metric = metricItems.Find(x => x.Name == "EventCounters.d.inc-poll-c"); + Assert.NotNull(metric); + Assert.Equal(MetricType.DoubleSum, metric.MetricType); + Assert.Equal(2, GetActualValue(metric)); + } + + [Fact] + public async Task EventCounterSameNameUsesNewestCreated() + { + // Arrange + List metricItems = new(); + EventSource source = new("a"); + EventCounter counter = new("c", source); + EventCounter counter2 = new("c", source); + + var meterProvider = Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources(source.Name); + }) + .AddInMemoryExporter(metricItems) + .Build(); + + // Act + counter2.WriteMetric(1980.1208); + counter.WriteMetric(1997.0202); + await Task.Delay(Delay); + meterProvider.ForceFlush(); + + // Assert + var metric = metricItems.Find(x => x.Name == "EventCounters.a.c"); + Assert.NotNull(metric); + Assert.Equal(MetricType.DoubleGauge, metric.MetricType); + + // Since `counter2` was created after `counter` it is exported + Assert.Equal(1980.1208, GetActualValue(metric)); + } + + [Fact] + public void ThrowExceptionWhenBuilderIsNull() + { + MeterProviderBuilder builder = null; + Assert.Throws(() => builder.AddEventCountersInstrumentation()); + } + + [Fact] + public void ThrowExceptionForUnsupportedEventSources() + { + var ex = Assert.Throws(() => + { + Sdk.CreateMeterProviderBuilder() + .AddEventCountersInstrumentation(options => + { + options.AddEventSources("System.Runtime"); + }); + }); + + Assert.Equal("Use the `OpenTelemetry.Instrumentation.Runtime` or `OpenTelemetry.Instrumentation.Process` instrumentations.", ex.Message); + } + + // polling and eventcounter with same instrument name? + + private static double GetActualValue(Metric metric) + { + double sum = 0; + + foreach (ref readonly var metricPoint in metric.GetMetricPoints()) + { + sum += metric.MetricType.IsSum() + ? metricPoint.GetSumDouble() + : metricPoint.GetGaugeLastValueDouble(); + } + + return sum; + } +} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/MeterProviderBuilderExtensionsTests.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/MeterProviderBuilderExtensionsTests.cs deleted file mode 100644 index 22020300ed..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/MeterProviderBuilderExtensionsTests.cs +++ /dev/null @@ -1,33 +0,0 @@ -// -// 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 OpenTelemetry.Metrics; -using Xunit; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -public class MeterProviderBuilderExtensionsTests -{ - [Fact] - public void Throws_Exception_When_Builder_Is_Null() - { - MeterProviderBuilder builder = null; - - Func action = () => builder.AddEventCounterMetrics(); - Assert.Throws(action); - } -} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestEventCounter.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestEventCounter.cs deleted file mode 100644 index eeaac2b030..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestEventCounter.cs +++ /dev/null @@ -1,44 +0,0 @@ -// -// 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.Diagnostics.Tracing; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -[EventSource(Name = "OpenTelemetry.Instrumentation.EventCounters.Tests.TestEventCounter")] -public sealed class TestEventCounter : EventSource -{ - // define the singleton instance of the event source - public static TestEventCounter Log = new TestEventCounter(); - private EventCounter testCounter1; - private EventCounter testCounter2; - - private TestEventCounter() - { - this.testCounter1 = new EventCounter("mycountername1", this); - this.testCounter2 = new EventCounter("mycountername2", this); - } - - public void SampleCounter1(float counterValue) - { - this.testCounter1.WriteMetric(counterValue); - } - - public void SampleCounter2(float counterValue) - { - this.testCounter2.WriteMetric(counterValue); - } -} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingEventCounter.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingEventCounter.cs deleted file mode 100644 index 5cd33ba0bd..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingEventCounter.cs +++ /dev/null @@ -1,39 +0,0 @@ -// -// 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.Diagnostics.Tracing; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -[EventSource(Name = "OpenTelemetry.Instrumentation.EventCounters.Tests.TestIncrementingEventCounter")] -public sealed class TestIncrementingEventCounter : EventSource -{ - public const string CounterName = "IncrementingEventCounter"; - - // define the singleton instance of the event source - public static TestIncrementingEventCounter Log = new(); - private IncrementingEventCounter incrementingEventCounter; - - private TestIncrementingEventCounter() - { - this.incrementingEventCounter = new IncrementingEventCounter(CounterName, this); - } - - public void SampleCounter1(float counterValue) - { - this.incrementingEventCounter.Increment(counterValue); - } -} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingPollingCounter.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingPollingCounter.cs deleted file mode 100644 index a60077519e..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestIncrementingPollingCounter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -[EventSource(Name = "OpenTelemetry.Instrumentation.EventCounters.Tests.TestPollingIncrementingEventCounter")] -public sealed class TestIncrementingPollingCounter : EventSource -{ - public const string CounterName = "TestIncrementingPollingCounter"; - - private IncrementingPollingCounter incrementingPollingCounter; - - private TestIncrementingPollingCounter(Func provider) - { - this.incrementingPollingCounter = new IncrementingPollingCounter(CounterName, this, provider); - } - - // define the singleton instance of the event source - public static TestIncrementingPollingCounter CreateSingleton(Func provider) => new(provider); -} diff --git a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestPollingEventCounter.cs b/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestPollingEventCounter.cs deleted file mode 100644 index 12c9102e47..0000000000 --- a/test/OpenTelemetry.Instrumentation.EventCounters.Tests/TestPollingEventCounter.cs +++ /dev/null @@ -1,36 +0,0 @@ -// -// 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; - -namespace OpenTelemetry.Instrumentation.EventCounters.Tests; - -[EventSource(Name = "OpenTelemetry.Instrumentation.EventCounters.Tests.TestPollingEventCounter")] -public sealed class TestPollingEventCounter : EventSource -{ - public const string CounterName = "TestPollingCounter"; - - private PollingCounter pollingCounter; - - private TestPollingEventCounter(Func provider) - { - this.pollingCounter = new PollingCounter(CounterName, this, provider); - } - - // define the singleton instance of the event source - public static TestPollingEventCounter CreateSingleton(Func provider) => new(provider); -}