Skip to content

Commit

Permalink
MetricCollector revamp.
Browse files Browse the repository at this point in the history
- This now has a considerably simpler API surface allowing you to collect
data for a single instrument at a time.

- You can now snapshot all recorded measurements and apply various
filters over lists of measurements.

- General shape of the API looks similar in style to the FakeLogger.
You can now take snapshots of existing state, as a client you never get
your hands on "live" state, which helps with racy code.

- There is now a WaitForMeasurementsAsync function which is helpful to
simplify testing async logic.
  • Loading branch information
Martin Taillefer committed Jun 20, 2023
1 parent 44ea85b commit 4a41c34
Show file tree
Hide file tree
Showing 31 changed files with 1,591 additions and 3,846 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
{
"Name": "Microsoft.Extensions.ObjectPool.DependencyInjection, Version=8.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35",
"Types": [
{
"Type": "sealed class Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions",
"Stage": "Experimental",
"Methods": [
{
"Member": "Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions.DependencyInjectionPoolOptions();",
"Stage": "Experimental"
}
],
"Properties": [
{
"Member": "int Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions.Capacity { get; set; }",
"Stage": "Experimental"
}
]
},
{
"Type": "static class Microsoft.Extensions.ObjectPool.ObjectPoolServiceCollectionExtensions",
"Stage": "Experimental",
"Methods": [
{
"Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.ObjectPool.ObjectPoolServiceCollectionExtensions.AddPooled<TService>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions>? configure = null);",
"Stage": "Experimental"
},
{
"Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.ObjectPool.ObjectPoolServiceCollectionExtensions.AddPooled<TService, TImplementation>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions>? configure = null);",
"Stage": "Experimental"
},
{
"Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.ObjectPool.ObjectPoolServiceCollectionExtensions.ConfigurePool<TService>(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, System.Action<Microsoft.Extensions.ObjectPool.DependencyInjectionPoolOptions> configure);",
"Stage": "Experimental"
},
{
"Member": "static Microsoft.Extensions.DependencyInjection.IServiceCollection Microsoft.Extensions.ObjectPool.ObjectPoolServiceCollectionExtensions.ConfigurePools(this Microsoft.Extensions.DependencyInjection.IServiceCollection services, Microsoft.Extensions.Configuration.IConfigurationSection section);",
"Stage": "Experimental"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Options;
using Microsoft.Shared.Diagnostics;

Expand Down Expand Up @@ -63,15 +62,21 @@ public void Clear()
/// <summary>
/// Gets the records that are held by the collector.
/// </summary>
/// <param name="clear">Setting this to <see langword="true"/> will atomically clear the set of accumulated log records.</param>
/// <returns>
/// The list of records tracked to date by the collector.
/// </returns>
[SuppressMessage("Minor Code Smell", "S4049:Properties should be preferred", Justification = "Not suitable for a property since it allocates.")]
public IReadOnlyList<FakeLogRecord> GetSnapshot()
public IReadOnlyList<FakeLogRecord> GetSnapshot(bool clear = false)
{
lock (_records)
{
return _records.ToArray();
var records = _records.ToArray();
if (clear)
{
_records.Clear();
}

return records;
}
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Globalization;
using Microsoft.Shared.Diagnostics;

namespace Microsoft.Extensions.Telemetry.Testing.Metering;

/// <summary>
/// Represents a single measurement performed by an instrument.
/// </summary>
/// <typeparam name="T">The type of metric measurement value.</typeparam>
[Experimental]
[DebuggerDisplay("{DebuggerToString(),nq}")]
public sealed class CollectedMeasurement<T>
where T : struct
{
/// <summary>
/// Initializes a new instance of the <see cref="CollectedMeasurement{T}"/> class.
/// </summary>
/// <param name="value">The measurement's value.</param>
/// <param name="tags">The dimensions of this measurement.</param>
/// <param name="timestamp">The time that the measurement occurred at.</param>
internal CollectedMeasurement(T value, ReadOnlySpan<KeyValuePair<string, object?>> tags, DateTimeOffset timestamp)
{
var d = new Dictionary<string, object?>();
foreach (var tag in tags)
{
d[tag.Key] = tag.Value;
}

Tags = d;
Timestamp = timestamp;
Value = value;
}

/// <summary>
/// Gets a measurement's value.
/// </summary>
public T Value { get; }

/// <summary>
/// Gets a timestamp indicating when the measurement was recorded.
/// </summary>
public DateTimeOffset Timestamp { get; }

/// <summary>
/// Gets the measurement's dimensions.
/// </summary>
public IReadOnlyDictionary<string, object?> Tags { get; }

/// <summary>
/// Checks that the measurement includes a specific set of tags with specific values.
/// </summary>
/// <param name="tags">The set of tags to check.</param>
/// <returns><see langword="true"/> if all the tags exist in the measurement with matching values, otherwise <see langword="false"/>.</returns>
public bool ContainsTags(params KeyValuePair<string, object?>[] tags)
{
foreach (var kvp in Throw.IfNull(tags))
{
if (!Tags.TryGetValue(kvp.Key, out var value))
{
return false;
}

if (!object.Equals(kvp.Value, value))
{
return false;
}
}

return true;
}

/// <summary>
/// Checks that the measurement includes a specific set of tags with any value.
/// </summary>
/// <param name="tags">The set of tag names to check.</param>
/// <returns><see langword="true"/> if all the tags exist in the measurement, otherwise <see langword="false"/>.</returns>
public bool ContainsTags(params string[] tags)
{
foreach (var key in Throw.IfNull(tags))
{
if (!Tags.ContainsKey(key))
{
return false;
}
}

return true;
}

/// <summary>
/// Checks that the measurement has an exactly matching set of tags with specific values.
/// </summary>
/// <param name="tags">The set of tags to check.</param>
/// <returns><see langword="true"/> if all the tags exist in the measurement with matching values, otherwise <see langword="false"/>.</returns>
public bool MatchesTags(params KeyValuePair<string, object?>[] tags) => ContainsTags(tags) && (Tags.Count == tags.Length);

/// <summary>
/// Checks that the measurement has a exactly matching set of tags with any value.
/// </summary>
/// <param name="tags">The set of tag names to check.</param>
/// <returns><see langword="true"/> if all the tag names exist in the measurement, otherwise <see langword="false"/>.</returns>
public bool MatchesTags(params string[] tags) => ContainsTags(Throw.IfNull(tags)) && (Tags.Count == tags.Length);

internal string DebuggerToString() => $"{Value} @ {Timestamp.ToString("HH:mm:ss.ffff", CultureInfo.InvariantCulture)}";
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Metrics;
using System.Linq;

namespace Microsoft.Extensions.Telemetry.Testing.Metering;

/// <summary>
/// Extensions to simplify working with lists of measurements.
/// </summary>
[Experimental]
public static class MeasurementExtensions
{
/// <summary>
/// Filters a list of measurements based on subset tags matching.
/// </summary>
/// <typeparam name="T">The type of measurement value.</typeparam>
/// <param name="measurements">The original full list of measurements.</param>
/// <param name="tags">The set of tags to match against. Only measurements that have at least these matching tags are returned.</param>
/// <returns>A list of matching measurements.</returns>
public static IEnumerable<CollectedMeasurement<T>> ContainsTags<T>(this IEnumerable<CollectedMeasurement<T>> measurements, params KeyValuePair<string, object?>[] tags)
where T : struct
=> measurements.Where(m => m.ContainsTags(tags));

/// <summary>
/// Filters a list of measurements based on subset tag matching.
/// </summary>
/// <typeparam name="T">The type of measurement value.</typeparam>
/// <param name="measurements">The original full list of measurements.</param>
/// <param name="tags">The set of tags to match against. Only measurements that have at least these matching tag names are returned.</param>
/// <returns>A list of matching measurements.</returns>
public static IEnumerable<CollectedMeasurement<T>> ContainsTags<T>(this IEnumerable<CollectedMeasurement<T>> measurements, params string[] tags)
where T : struct
=> measurements.Where(m => m.ContainsTags(tags));

/// <summary>
/// Filters a list of measurements based on exact tag matching.
/// </summary>
/// <typeparam name="T">The type of measurement value.</typeparam>
/// <param name="measurements">The original full list of measurements.</param>
/// <param name="tags">The set of tags to match against. Only measurements that have exactly those matching tags are returned.</param>
/// <returns>A list of matching measurements.</returns>
public static IEnumerable<CollectedMeasurement<T>> MatchesTags<T>(this IEnumerable<CollectedMeasurement<T>> measurements, params KeyValuePair<string, object?>[] tags)
where T : struct
=> measurements.Where(m => m.MatchesTags(tags));

/// <summary>
/// Filters a list of measurements based on exact tag name matching.
/// </summary>
/// <typeparam name="T">The type of measurement value.</typeparam>
/// <param name="measurements">The original full list of measurements.</param>
/// <param name="tags">The set of tags to match against. Only measurements that have exactly those matching tag names are returned.</param>
/// <returns>A list of matching measurements.</returns>
public static IEnumerable<CollectedMeasurement<T>> MatchesTags<T>(this IEnumerable<CollectedMeasurement<T>> measurements, params string[] tags)
where T : struct
=> measurements.Where(m => m.MatchesTags(tags));

/// <summary>
/// Process the series of measurements adding all values together to produce a final count, identical to what a <see cref="Counter{T}" /> instrument would produce.
/// </summary>
/// <typeparam name="T">The type of measurement value.</typeparam>
/// <param name="measurements">The list of measurements to process.</param>
/// <returns>The resulting count.</returns>
public static T EvaluateAsCounter<T>(this IEnumerable<CollectedMeasurement<T>> measurements)
where T : struct
{
#pragma warning disable CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).
return measurements switch
{
IEnumerable<CollectedMeasurement<byte>> l => (T)(object)ByteSum(l),
IEnumerable<CollectedMeasurement<short>> l => (T)(object)ShortSum(l),
IEnumerable<CollectedMeasurement<int>> l => (T)(object)l.Sum(m => m.Value),
IEnumerable<CollectedMeasurement<long>> l => (T)(object)l.Sum(m => m.Value),
IEnumerable<CollectedMeasurement<float>> l => (T)(object)l.Sum(m => m.Value),
IEnumerable<CollectedMeasurement<double>> l => (T)(object)l.Sum(m => m.Value),
IEnumerable<CollectedMeasurement<decimal>> l => (T)(object)l.Sum(m => m.Value),
};
#pragma warning restore CS8509 // The switch expression does not handle all possible values of its input type (it is not exhaustive).

static byte ByteSum(IEnumerable<CollectedMeasurement<byte>> measurements)
{
byte sum = 0;
foreach (var measurement in measurements)
{
sum += measurement.Value;
}

return sum;
}

static short ShortSum(IEnumerable<CollectedMeasurement<short>> measurements)
{
short sum = 0;
foreach (var measurement in measurements)
{
sum += measurement.Value;
}

return sum;
}
}
}
Loading

0 comments on commit 4a41c34

Please sign in to comment.