diff --git a/src/libraries/Common/src/System/Diagnostics/DiagnosticsHelper.cs b/src/libraries/Common/src/System/Diagnostics/DiagnosticsHelper.cs new file mode 100644 index 00000000000000..6d018effde82ae --- /dev/null +++ b/src/libraries/Common/src/System/Diagnostics/DiagnosticsHelper.cs @@ -0,0 +1,68 @@ +// 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; + +namespace System.Diagnostics +{ + internal static class DiagnosticsHelper + { + internal static bool CompareTags(IEnumerable>? tags1, IEnumerable>? tags2) + { + if (tags1 == tags2) + { + return true; + } + + if (tags1 is null || tags2 is null) + { + return false; + } + + if (tags1 is ICollection> firstCol && tags2 is ICollection> secondCol) + { + int count = firstCol.Count; + if (count != secondCol.Count) + { + return false; + } + + if (firstCol is IList> firstList && secondCol is IList> secondList) + { + for (int i = 0; i < count; i++) + { + KeyValuePair pair1 = firstList[i]; + KeyValuePair pair2 = secondList[i]; + if (pair1.Key != pair2.Key || !object.Equals(pair1.Value, pair2.Value)) + { + return false; + } + } + + return true; + } + } + + using (IEnumerator> e1 = tags1.GetEnumerator()) + using (IEnumerator> e2 = tags2.GetEnumerator()) + { + while (e1.MoveNext()) + { + KeyValuePair pair1 = e1.Current; + if (!e2.MoveNext()) + { + return false; + } + + KeyValuePair pair2 = e2.Current; + if (pair1.Key != pair2.Key || !object.Equals(pair1.Value, pair2.Value)) + { + return false; + } + } + + return !e2.MoveNext(); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln new file mode 100644 index 00000000000000..076dba22be613e --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/Microsoft.Extensions.Diagnostics.Abstractions.sln @@ -0,0 +1,63 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{E5B6A94F-615E-4DAF-8110-E5A776BB8296}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "ref\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{696D69C9-C5F6-405E-A8B5-5375D08BBADC}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Abstractions", "src\Microsoft.Extensions.Diagnostics.Abstractions.csproj", "{2AED2951-7724-4EFC-8E16-6DF877C6B4A6}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{155C3F40-F63D-49DF-87D3-A3EEA27036E8}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{50BA55F5-BD05-4C05-910F-2BFD20BD3465}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{5725D7DF-DC33-47D2-90C9-D8736C579E77}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "gen", "gen", "{55D04C80-4A8F-40AC-967D-3FA77C814D7B}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E5B6A94F-615E-4DAF-8110-E5A776BB8296}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E5B6A94F-615E-4DAF-8110-E5A776BB8296}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E5B6A94F-615E-4DAF-8110-E5A776BB8296}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E5B6A94F-615E-4DAF-8110-E5A776BB8296}.Release|Any CPU.Build.0 = Release|Any CPU + {696D69C9-C5F6-405E-A8B5-5375D08BBADC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {696D69C9-C5F6-405E-A8B5-5375D08BBADC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {696D69C9-C5F6-405E-A8B5-5375D08BBADC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {696D69C9-C5F6-405E-A8B5-5375D08BBADC}.Release|Any CPU.Build.0 = Release|Any CPU + {2AED2951-7724-4EFC-8E16-6DF877C6B4A6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2AED2951-7724-4EFC-8E16-6DF877C6B4A6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2AED2951-7724-4EFC-8E16-6DF877C6B4A6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2AED2951-7724-4EFC-8E16-6DF877C6B4A6}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {E5B6A94F-615E-4DAF-8110-E5A776BB8296} = {155C3F40-F63D-49DF-87D3-A3EEA27036E8} + {F5E89C81-2D78-4F86-ABD2-7D983F46EB58} = {155C3F40-F63D-49DF-87D3-A3EEA27036E8} + {5351C410-530D-4724-A8E6-430831E7332B} = {155C3F40-F63D-49DF-87D3-A3EEA27036E8} + {696D69C9-C5F6-405E-A8B5-5375D08BBADC} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {6D9C22DB-C4E3-483E-AF78-C1DCE6ED8DD6} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {C8C58A2A-B254-4A74-8C7B-9B0CA0A9E67A} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {CC0E8CC8-88F5-4D72-9B3F-0281DD58461A} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {7323E18C-BF40-4236-9AEF-273A94F3DB03} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {536A305D-DC57-4E7F-B21B-B8DE916A63C3} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {50F89560-1449-44E4-844E-72815256534B} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {8E212A9D-391B-4EFA-943D-7D104A9D3D7E} = {50BA55F5-BD05-4C05-910F-2BFD20BD3465} + {2AED2951-7724-4EFC-8E16-6DF877C6B4A6} = {5725D7DF-DC33-47D2-90C9-D8736C579E77} + {5C580568-6072-4F27-B5C6-FA04556E3B98} = {5725D7DF-DC33-47D2-90C9-D8736C579E77} + {7902A0CA-E94D-4C96-A112-455A1E5E2390} = {5725D7DF-DC33-47D2-90C9-D8736C579E77} + {FA7201FE-097D-4197-BDEC-329986814D8D} = {5725D7DF-DC33-47D2-90C9-D8736C579E77} + {7B4B86A8-EF62-45F0-A431-05F4C11A2E0D} = {55D04C80-4A8F-40AC-967D-3FA77C814D7B} + {6F992337-EAEE-4F7A-BF3B-DDC526F054F7} = {55D04C80-4A8F-40AC-967D-3FA77C814D7B} + {240E7B1C-6D7D-4932-9598-815C2EA420AF} = {55D04C80-4A8F-40AC-967D-3FA77C814D7B} + {B4C5A0BE-3E21-4464-875C-E6EBED176EE3} = {55D04C80-4A8F-40AC-967D-3FA77C814D7B} + {30DEAC25-AFDB-4E91-90A4-7B94E1809C59} = {55D04C80-4A8F-40AC-967D-3FA77C814D7B} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {810D114A-26F4-4151-9EFE-29A7F1BF62AA} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md new file mode 100644 index 00000000000000..8c38bc32e00d4d --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/README.md @@ -0,0 +1,14 @@ +# Microsoft.Extensions.Diagnostics.Abstractions + +`Microsoft.Extensions.Diagnostics.Abstractions` includes the various abstraction types for telemetry, such as IMeterFactory. + +Commonly Used Types: +- Microsoft.Extensions.Metrics.IMeterFactory + +## Contribution Bar +- [x] [We consider new features, new APIs, bug fixes, and performance changes](https://github.com/dotnet/runtime/tree/main/src/libraries#contribution-bar) + +The APIs and functionality are mature, but do get extended occasionally. + +## Deployment +[Microsoft.Extensions.Diagnostics.Abstractions](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics.Abstractions) is included in the ASP.NET Core shared framework. The package is deployed as out-of-band (OOB) too and can be referenced into projects directly. \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs new file mode 100644 index 00000000000000..121f454cc12653 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.cs @@ -0,0 +1,10 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + public interface IMeterFactory : System.IDisposable + { + System.Diagnostics.Metrics.Meter Create(System.Diagnostics.Metrics.MeterOptions options); + } +} \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj new file mode 100644 index 00000000000000..940f26f4a1f2df --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/ref/Microsoft.Extensions.Diagnostics.Abstractions.csproj @@ -0,0 +1,16 @@ + + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + false + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMeterFactory.cs b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMeterFactory.cs new file mode 100644 index 00000000000000..dd8702288bdf9b --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Metrics/IMeterFactory.cs @@ -0,0 +1,31 @@ +// 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.Diagnostics.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// A factory for creating instances. + /// + /// + /// Meter factories will be accountable for the following responsibilities: + /// - Creating a new meter. + /// - Attaching the factory instance as a scope to the Meter constructor for all created Meter objects. + /// - Storing created meters in a cache and returning a cached instance if a meter with the same parameters (name, version, and tags) is requested. + /// - Disposing of all cached Meter objects upon factory disposal. + /// + public interface IMeterFactory : IDisposable + { + /// + /// Creates a new instance. + /// + /// The to use when creating the meter. + /// A new instance. + /// + /// The instance returned by this method should be cached by the factory and returned for subsequent requests for a meter with the same parameters (name, version, and tags). + /// + Meter Create(MeterOptions options); + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj new file mode 100644 index 00000000000000..3be20179c491f8 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics.Abstractions/src/Microsoft.Extensions.Diagnostics.Abstractions.csproj @@ -0,0 +1,19 @@ + + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + true + false + + true + true + Abstraction types such as IMeterFactory are employed by the telemetry framework extensions. + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln b/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln new file mode 100644 index 00000000000000..b753e17b3b7736 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/Microsoft.Extensions.Diagnostics.sln @@ -0,0 +1,61 @@ +Microsoft Visual Studio Solution File, Format Version 12.00 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestUtilities", "..\Common\tests\TestUtilities\TestUtilities.csproj", "{B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics", "ref\Microsoft.Extensions.Diagnostics.csproj", "{EF75497C-6CB7-4471-980A-619EA1AB8CF6}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics", "src\Microsoft.Extensions.Diagnostics.csproj", "{09E28D94-B771-48EB-800C-5A80C2C0055C}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Diagnostics.Tests", "tests\Microsoft.Extensions.Diagnostics.Tests.csproj", "{43DBAD84-A865-4F5F-AB76-7F3EB6784E99}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tests", "tests", "{76DC9C4C-EE53-47E6-B6BF-7B135EA8CAF3}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "ref", "ref", "{9BF048D0-411D-4C2A-8C32-3A3255501D27}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{A447D0CB-601B-479E-A2B2-76E48F5D4D61}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE}.Release|Any CPU.Build.0 = Release|Any CPU + {EF75497C-6CB7-4471-980A-619EA1AB8CF6}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {EF75497C-6CB7-4471-980A-619EA1AB8CF6}.Debug|Any CPU.Build.0 = Debug|Any CPU + {EF75497C-6CB7-4471-980A-619EA1AB8CF6}.Release|Any CPU.ActiveCfg = Release|Any CPU + {EF75497C-6CB7-4471-980A-619EA1AB8CF6}.Release|Any CPU.Build.0 = Release|Any CPU + {09E28D94-B771-48EB-800C-5A80C2C0055C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {09E28D94-B771-48EB-800C-5A80C2C0055C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {09E28D94-B771-48EB-800C-5A80C2C0055C}.Release|Any CPU.ActiveCfg = Release|Any CPU + {09E28D94-B771-48EB-800C-5A80C2C0055C}.Release|Any CPU.Build.0 = Release|Any CPU + {43DBAD84-A865-4F5F-AB76-7F3EB6784E99}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {43DBAD84-A865-4F5F-AB76-7F3EB6784E99}.Debug|Any CPU.Build.0 = Debug|Any CPU + {43DBAD84-A865-4F5F-AB76-7F3EB6784E99}.Release|Any CPU.ActiveCfg = Release|Any CPU + {43DBAD84-A865-4F5F-AB76-7F3EB6784E99}.Release|Any CPU.Build.0 = Release|Any CPU + {87AA8A4F-2785-4E1D-BAB2-3C6414C8D387}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {87AA8A4F-2785-4E1D-BAB2-3C6414C8D387}.Debug|Any CPU.Build.0 = Debug|Any CPU + {87AA8A4F-2785-4E1D-BAB2-3C6414C8D387}.Release|Any CPU.ActiveCfg = Release|Any CPU + {87AA8A4F-2785-4E1D-BAB2-3C6414C8D387}.Release|Any CPU.Build.0 = Release|Any CPU + {959348BC-2D38-41F4-9F3D-2E1CD18D9477}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {959348BC-2D38-41F4-9F3D-2E1CD18D9477}.Debug|Any CPU.Build.0 = Debug|Any CPU + {959348BC-2D38-41F4-9F3D-2E1CD18D9477}.Release|Any CPU.ActiveCfg = Release|Any CPU + {959348BC-2D38-41F4-9F3D-2E1CD18D9477}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {B6663ACE-6FE4-4BB4-8B35-AB98EF62EAAE} = {76DC9C4C-EE53-47E6-B6BF-7B135EA8CAF3} + {43DBAD84-A865-4F5F-AB76-7F3EB6784E99} = {76DC9C4C-EE53-47E6-B6BF-7B135EA8CAF3} + {EF75497C-6CB7-4471-980A-619EA1AB8CF6} = {9BF048D0-411D-4C2A-8C32-3A3255501D27} + {09E28D94-B771-48EB-800C-5A80C2C0055C} = {A447D0CB-601B-479E-A2B2-76E48F5D4D61} + {87AA8A4F-2785-4E1D-BAB2-3C6414C8D387} = {069D6714-DE43-45C4-BABC-C8246B974CA7} + {959348BC-2D38-41F4-9F3D-2E1CD18D9477} = {069D6714-DE43-45C4-BABC-C8246B974CA7} + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {7D279EE5-E38F-4125-AE82-6ADE52D72F26} + EndGlobalSection +EndGlobal diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/README.md b/src/libraries/Microsoft.Extensions.Diagnostics/README.md new file mode 100644 index 00000000000000..776b53340b7833 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/README.md @@ -0,0 +1,15 @@ +# Microsoft.Extensions.Diagnostics + +`Microsoft.Extensions.Diagnostics` contains the default implementation of meter factory and extension methods for registering this default meter factory to the DI. + +Commonly Used APIS: +- MetricsServiceExtensions.AddMetrics(this IServiceCollection services) +- MeterFactoryExtensions.Create(this IMeterFactory, string name, string? version = null, IEnumerable> tags = null, object? scope = null) + +## Contribution Bar +- [x] [We consider new features, new APIs, bug fixes, and performance changes](https://github.com/dotnet/runtime/tree/main/src/libraries#contribution-bar) + +The APIs and functionality are mature, but do get extended occasionally. + +## Deployment +[Microsoft.Extensions.Diagnostics](https://www.nuget.org/packages/Microsoft.Extensions.Diagnostics) is included in the ASP.NET Core shared framework. The package is deployed as out-of-band (OOB) too and can be referenced into projects directly. \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs new file mode 100644 index 00000000000000..6e034a5ba4e1e9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.cs @@ -0,0 +1,14 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + public static class MetricsServiceExtensions + { + public static Microsoft.Extensions.DependencyInjection.IServiceCollection AddMetrics(this Microsoft.Extensions.DependencyInjection.IServiceCollection services) { return null!; } + } + public static class MeterFactoryExtensions + { + public static System.Diagnostics.Metrics.Meter Create(this Microsoft.Extensions.Diagnostics.Metrics.IMeterFactory meterFactory, string name, string? version = null, System.Collections.Generic.IEnumerable>? tags = null, object? scope = null) { return null!; } + } +} \ No newline at end of file diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj new file mode 100644 index 00000000000000..b0f9d0a8a9c732 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/ref/Microsoft.Extensions.Diagnostics.csproj @@ -0,0 +1,18 @@ + + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + false + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DefaultMeterFactory.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DefaultMeterFactory.cs new file mode 100644 index 00000000000000..c6ceaeeae9e2eb --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/DefaultMeterFactory.cs @@ -0,0 +1,79 @@ +// 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.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + internal sealed class DefaultMeterFactory : IMeterFactory + { + private readonly Dictionary> _cachedMeters = new(); + private bool _disposed; + + public DefaultMeterFactory() { } + + public Meter Create(MeterOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + Debug.Assert(options.Name is not null); + + lock (_cachedMeters) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(DefaultMeterFactory)); + } + + if (_cachedMeters.TryGetValue(options.Name, out List? meterList)) + { + foreach (Meter meter in meterList) + { + if (meter.Version == options.Version && DiagnosticsHelper.CompareTags(meter.Tags, options.Tags)) + { + return meter; + } + } + } + else + { + meterList = new List(); + _cachedMeters.Add(options.Name, meterList); + } + + Meter m = new Meter(options.Name, options.Version, options.Tags, scope: this); + meterList.Add(m); + return m; + } + } + + public void Dispose() + { + lock (_cachedMeters) + { + if (_disposed) + { + return; + } + + _disposed = true; + + foreach (List meterList in _cachedMeters.Values) + { + foreach (Meter meter in meterList) + { + meter.Dispose(); + } + } + + _cachedMeters.Clear(); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MeterFactoryExtensions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MeterFactoryExtensions.cs new file mode 100644 index 00000000000000..ad1e82e6f22210 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MeterFactoryExtensions.cs @@ -0,0 +1,39 @@ +// 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.Metrics; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Extension methods for and . + /// + public static class MeterFactoryExtensions + { + /// + /// Creates a with the specified , , , and . + /// + /// The to use to create the . + /// The name of the . + /// The version of the . + /// The tags to associate with the . + /// The scope to associate with the . + /// A with the specified , , , and . + public static Meter Create(this IMeterFactory meterFactory, string name, string? version = null, IEnumerable>? tags = null, object? scope = null) + { + if (meterFactory is null) + { + throw new ArgumentNullException(nameof(meterFactory)); + } + + return meterFactory.Create(new MeterOptions(name) + { + Version = version, + Tags = tags, + Scope = scope + }); + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs new file mode 100644 index 00000000000000..08435c29ea3350 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Metrics/MetricsServiceExtensions.cs @@ -0,0 +1,32 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using System; + +namespace Microsoft.Extensions.Diagnostics.Metrics +{ + /// + /// Extension methods for setting up metrics services in an . + /// + public static class MetricsServiceExtensions + { + /// + /// Adds metrics services to the specified . + /// + /// The to add services to. + /// The so that additional calls can be chained. + public static IServiceCollection AddMetrics(this IServiceCollection services) + { + if (services is null) + { + throw new ArgumentNullException(nameof(services)); + } + + services.TryAddSingleton(); + + return services; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj new file mode 100644 index 00000000000000..2ebe0d6018ef43 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/src/Microsoft.Extensions.Diagnostics.csproj @@ -0,0 +1,25 @@ + + + + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) + true + false + + true + true + This package includes the default implementation of IMeterFactory and additional extension methods to easily register it with the Dependency Injection framework. + + + + + + + + + + + + + diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs b/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs new file mode 100644 index 00000000000000..bb9e91d26cb673 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/MetricsTests.cs @@ -0,0 +1,155 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Microsoft.DotNet.RemoteExecutor; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; +using Microsoft.Extensions.Diagnostics.Metrics; +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace Microsoft.Extensions.Diagnostics.Metrics.Tests +{ + public class MetricsTests + { + [Fact] + public void FactoryDITest() + { + ServiceCollection services = new ServiceCollection(); + var sp = services.BuildServiceProvider(); + Assert.Throws(() => sp.GetRequiredService()); + + services.AddMetrics(); + + sp = services.BuildServiceProvider(); + using IMeterFactory meterFactory = sp.GetRequiredService(); + Assert.NotNull(meterFactory); + + MeterOptions options = new MeterOptions("name") + { + Version = "version", + Tags = new TagList() { { "key1", "value1" }, { "key2", "value2" } } + }; + + Meter meter1 = meterFactory.Create(options); + Assert.Same(meterFactory, meter1.Scope); + + Meter meter2 = meterFactory.Create(options.Name, options.Version, options.Tags); // calling the extension method + Assert.Same(meterFactory, meter2.Scope); + + Assert.Same(meter1, meter2); + + Meter meter3 = meterFactory.Create(options.Name, options.Version, new TagList() { { "key1", "value1" }, { "key2", "value1" } }); + Assert.Same(meterFactory, meter3.Scope); + + Assert.NotSame(meter1, meter3); + } + + [Fact] + public void NegativeTest() + { + ServiceCollection services = new ServiceCollection(); + services.AddMetrics(); + var sp = services.BuildServiceProvider(); + using IMeterFactory meterFactory = sp.GetRequiredService(); + + Assert.Throws(() => meterFactory.Create(name: null)); + } + + [Fact] + public void InstrumentRecorderTest() + { + ServiceCollection services = new ServiceCollection(); + services.AddMetrics(); + var sp = services.BuildServiceProvider(); + using IMeterFactory meterFactory = sp.GetRequiredService(); + + MeterOptions options = new MeterOptions("name") + { + Version = "version", + Tags = new TagList() { { "key1", "value1" }, { "key2", "value2" } } + }; + + Meter meter = meterFactory.Create("MyMeter", "1.0.0", new TagList() { { "key1", "value1" }, { "key2", "value2" } }); + Assert.Same(meterFactory, meter.Scope); + + Counter counter = meter.CreateCounter("MyCounter"); + + InstrumentRecorder recorder1 = new InstrumentRecorder(counter); + Assert.Same(counter, recorder1.Instrument); + + InstrumentRecorder recorder2 = new InstrumentRecorder(meter, "MyCounter"); + Assert.Same(counter, recorder2.Instrument); + + InstrumentRecorder recorder3 = new InstrumentRecorder(scopeFilter: meterFactory, "MyMeter", "MyCounter"); + Assert.Same(counter, recorder3.Instrument); + + counter.Add(100, new KeyValuePair("k", "v")); + Assert.Equal(1, recorder1.GetMeasurements().Count()); + Assert.Equal(1, recorder2.GetMeasurements().Count()); + Assert.Equal(1, recorder3.GetMeasurements().Count()); + + Assert.Equal(100, recorder1.GetMeasurements().ElementAt(0).Value); + Assert.Equal(100, recorder2.GetMeasurements().ElementAt(0).Value); + Assert.Equal(100, recorder2.GetMeasurements().ElementAt(0).Value); + + KeyValuePair[] tags = new KeyValuePair[] { new KeyValuePair("k", "v") }; + Assert.Equal(tags, recorder1.GetMeasurements().ElementAt(0).Tags.ToArray()); + Assert.Equal(tags, recorder2.GetMeasurements().ElementAt(0).Tags.ToArray()); + Assert.Equal(tags, recorder3.GetMeasurements().ElementAt(0).Tags.ToArray()); + } + + [Fact] + public void CustomMeterFactoryTest() + { + ServiceCollection services = new ServiceCollection(); + services.TryAddSingleton(); + var sp = services.BuildServiceProvider(); + using IMeterFactory meterFactory = sp.GetRequiredService(); + Assert.True(meterFactory is NoCachingMeterFactory); + + MeterOptions options = new MeterOptions("name") + { + Version = "version", + Tags = new TagList() { { "key1", "value1" }, { "key2", "value2" } } + }; + + Meter meter1 = meterFactory.Create(options); + Meter meter2 = meterFactory.Create(options); + + Meter meter3 = meterFactory.Create(options.Name, options.Version, options.Tags); + + Assert.NotSame(meter1, meter2); + Assert.NotSame(meter1, meter3); + Assert.NotSame(meter2, meter3); + } + + public class NoCachingMeterFactory : IMeterFactory + { + List _meterList = new List(); + + public Meter Create(MeterOptions options) + { + var meter = new Meter(options.Name, options.Version, options.Tags, scope: this); + _meterList.Add(meter); + return meter; + } + + public void Dispose() + { + foreach (var meter in _meterList) + { + meter.Dispose(); + } + + _meterList.Clear(); + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj b/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj new file mode 100644 index 00000000000000..847a99176daddd --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Diagnostics/tests/Microsoft.Extensions.Diagnostics.Tests.csproj @@ -0,0 +1,18 @@ + + + + $(NetCoreAppCurrent);$(NetFrameworkMinimum) + true + true + false + + + + + + + + + + + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs index 71c2a9d7b8b59f..c2608ee2dddfc2 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/ref/System.Diagnostics.DiagnosticSourceActivity.cs @@ -370,15 +370,18 @@ public abstract class Instrument public string? Description { get {throw null;} } public bool Enabled { get {throw null; } } protected Instrument(Meter meter, string name, string? unit, string? description) {throw null;} + protected Instrument(Meter meter, string name, string? unit, string? description, System.Collections.Generic.IEnumerable>? tags) {throw null;} public virtual bool IsObservable { get {throw null; } } public Meter Meter { get {throw null;} } public string Name { get {throw null;} } protected void Publish() {throw null;} public string? Unit { get {throw null; } } + public System.Collections.Generic.IEnumerable>? Tags { get; } } public abstract class Instrument : Instrument where T : struct { protected Instrument(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) { throw null; } + protected Instrument(Meter meter, string name, string? unit, string? description, System.Collections.Generic.IEnumerable>? tags) : base(meter, name, unit, description, tags) {throw null;} protected void RecordMeasurement(T measurement) { throw null; } protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair tag) { throw null; } protected void RecordMeasurement(T measurement, System.Collections.Generic.KeyValuePair tag1, System.Collections.Generic.KeyValuePair tag2) { throw null; } @@ -386,6 +389,15 @@ public abstract class Instrument : Instrument where T : struct protected void RecordMeasurement(T measurement, in TagList tagList) { throw null; } protected void RecordMeasurement(T measurement, ReadOnlySpan> tags) { throw null; } } + public sealed class InstrumentRecorder : IDisposable where T : struct + { + public InstrumentRecorder(Instrument instrument) { throw null; } + public InstrumentRecorder(object? scopeFilter, string meterName, string instrumentName) { throw null; } + public InstrumentRecorder(Meter meter, string instrumentName) { throw null; } + public Instrument? Instrument { get { throw null; } } + public System.Collections.Generic.IEnumerable> GetMeasurements(bool clear = false) { throw null; } + public void Dispose() { throw null; } + } public readonly struct Measurement where T : struct { public Measurement(T value) { throw null; } @@ -399,58 +411,120 @@ public abstract class Instrument : Instrument where T : struct public class Meter : IDisposable { public Counter CreateCounter(string name, string? unit = null, string? description = null) where T : struct { throw null; } + public Counter CreateCounter(string name, string? unit, string? description, System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public UpDownCounter CreateUpDownCounter(string name, string? unit = null, string? description = null) where T : struct { throw null; } + public UpDownCounter CreateUpDownCounter(string name, string? unit, string? description, System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public Histogram CreateHistogram(string name, string? unit = null, string? description = null) where T : struct { throw null; } + public Histogram CreateHistogram(string name, string? unit, string? description, System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableCounter CreateObservableCounter( string name, Func observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableCounter CreateObservableCounter( + string name, + Func observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableCounter CreateObservableCounter( string name, Func> observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableCounter CreateObservableCounter( + string name, + Func> observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableCounter CreateObservableCounter( string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableCounter CreateObservableCounter( + string name, + Func>> observeValues, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableUpDownCounter CreateObservableUpDownCounter( string name, Func observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableUpDownCounter CreateObservableUpDownCounter( + string name, + Func observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableUpDownCounter CreateObservableUpDownCounter( string name, Func> observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableUpDownCounter CreateObservableUpDownCounter( + string name, + Func> observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableUpDownCounter CreateObservableUpDownCounter( string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableUpDownCounter CreateObservableUpDownCounter( + string name, + Func>> observeValues, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableGauge CreateObservableGauge( string name, Func observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableGauge CreateObservableGauge( + string name, + Func observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableGauge CreateObservableGauge( string name, Func> observeValue, string? unit = null, string? description = null) where T : struct { throw null; } + public ObservableGauge CreateObservableGauge( + string name, + Func> observeValue, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } public ObservableGauge CreateObservableGauge( string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct { throw null; } - public void Dispose() { throw null; } + public ObservableGauge CreateObservableGauge( + string name, + Func>> observeValues, + string? unit, + string? description, + System.Collections.Generic.IEnumerable> tags) where T : struct { throw null; } + protected virtual void Dispose(bool disposing) { throw null; } + public void Dispose() { throw null; } + public Meter(MeterOptions options) { throw null; } public Meter(string name) { throw null; } public Meter(string name, string? version) { throw null; } + public Meter(string name, string? version, System.Collections.Generic.IEnumerable>? tags, object? scope = null) { throw null; } public string Name { get { throw null; } } public string? Version { get { throw null; } } + public System.Collections.Generic.IEnumerable>? Tags { get { throw null; } } + public object? Scope { get { throw null; } } } public sealed class MeterListener : IDisposable { @@ -464,6 +538,14 @@ public sealed class MeterListener : IDisposable public void SetMeasurementEventCallback(MeasurementCallback? measurementCallback) where T : struct { throw null; } public void Start() { throw null; } } + public class MeterOptions + { + public string Name { get { throw null;} set { throw null;} } + public string? Version { get { throw null;} set { throw null;} } + public System.Collections.Generic.IEnumerable>? Tags { get { throw null;} set { throw null;} } + public object? Scope { get { throw null;} set { throw null;} } + public MeterOptions(string name) { throw null;} + } public sealed class ObservableCounter : ObservableInstrument where T : struct { internal ObservableCounter(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) { throw null; } @@ -482,7 +564,8 @@ public sealed class ObservableGauge : ObservableInstrument where T : struc public abstract class ObservableInstrument : Instrument where T : struct { public override bool IsObservable { get { throw null; } } - protected ObservableInstrument(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) { throw null; } + protected ObservableInstrument(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, tags: null) { throw null; } + protected ObservableInstrument(Meter meter, string name, string? unit, string? description, System.Collections.Generic.IEnumerable> tags) : base(meter, name, unit, description) { throw null; } protected abstract System.Collections.Generic.IEnumerable> Observe(); } } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx index 93a345d4a979a2..6ad3bfd0b2a118 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/Resources/Strings.resx @@ -171,4 +171,7 @@ Destination buffer is not long enough to copy all the items in the list. + + The instrument is of different generic type. + \ No newline at end of file diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj index 8b01133f758818..17d2489283e0f4 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System.Diagnostics.DiagnosticSource.csproj @@ -1,4 +1,5 @@ + $(NetCoreAppCurrent);$(NetCoreAppPrevious);$(NetCoreAppMinimum);netstandard2.0;$(NetFrameworkMinimum) true @@ -51,12 +52,14 @@ System.Diagnostics.DiagnosticSource + + @@ -68,6 +71,7 @@ System.Diagnostics.DiagnosticSource + @@ -122,6 +126,7 @@ System.Diagnostics.DiagnosticSource + diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs index bb14c06b2c164c..1d311d62196391 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Counter.cs @@ -15,7 +15,11 @@ namespace System.Diagnostics.Metrics /// public sealed class Counter : Instrument where T : struct { - internal Counter(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) + internal Counter(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, null) + { + } + + internal Counter(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { Publish(); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs index 73eeda8c8ac69b..ed3431edfce8be 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Histogram.cs @@ -8,14 +8,18 @@ namespace System.Diagnostics.Metrics /// /// The histogram is a metrics Instrument which can be used to report arbitrary values that are likely to be statistically meaningful. /// e.g. the request duration. - /// Use method to create the Histogram object. + /// Use method to create the Histogram object. /// /// /// This class supports only the following generic parameter types: , , , , , , and /// public sealed class Histogram : Instrument where T : struct { - internal Histogram(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) + internal Histogram(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, tags: null) + { + } + + internal Histogram(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { Publish(); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.common.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.common.cs index 7f81ab13a7c92a..6129c0686b250d 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.common.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.common.cs @@ -21,7 +21,20 @@ public abstract partial class Instrument : Instrument where T : struct /// The instrument name. cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. - protected Instrument(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) + protected Instrument(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, tags: null) + { + } + + /// + /// Create the metrics instrument using the properties meter, name, description, and unit. + /// All classes extending Instrument{T} need to call this constructor when constructing object of the extended class. + /// + /// The meter that created the instrument. + /// The instrument name. cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// Optional instrument tags. + protected Instrument(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { ValidateTypeParameter(); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs index 8d867045006663..be33619291e466 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Instrument.cs @@ -2,6 +2,7 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; +using System.Linq; namespace System.Diagnostics.Metrics { @@ -34,12 +35,29 @@ public abstract class Instrument /// The instrument name. cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. - protected Instrument(Meter meter, string name, string? unit, string? description) + protected Instrument(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, null) { } + + /// + /// Protected constructor to initialize the common instrument properties like the meter, name, description, and unit. + /// All classes extending Instrument need to call this constructor when constructing object of the extended class. + /// + /// The meter that created the instrument. + /// The instrument name. cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// Optional instrument tags. + protected Instrument(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) { Meter = meter ?? throw new ArgumentNullException(nameof(meter)); Name = name ?? throw new ArgumentNullException(nameof(name)); Description = description; Unit = unit; + if (tags is not null) + { + var tagsArray = tags.ToArray(); + // Array.Sort(tagsArray, (left, right) => string.Compare(left.Key, right.Key, StringComparison.Ordinal)); + Tags = tagsArray; + } } /// @@ -87,6 +105,11 @@ protected void Publish() /// public string? Unit { get; } + /// + /// Returns the tags associated with the Meter. + /// + public IEnumerable>? Tags { get; } + /// /// Checks if there is any listeners for this instrument. /// diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/InstrumentRecorder.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/InstrumentRecorder.cs new file mode 100644 index 00000000000000..4b45167bbecd6c --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/InstrumentRecorder.cs @@ -0,0 +1,181 @@ +// 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.Metrics; +using System.Threading; + +namespace System.Diagnostics.Metrics +{ + /// + /// A helper class to record the measurements published from or . + /// + public sealed class InstrumentRecorder : IDisposable where T : struct + { + private bool _isObservableInstrument; + private bool _disposed; + private Instrument? _instrument; + + private MeterListener _meterListener; + private List> _measurements; + + /// + /// Initialize a new instance to record the measurements published by . + /// + /// The to record measurements from. + public InstrumentRecorder(Instrument instrument) + { + if (instrument is null) + { + throw new ArgumentNullException(nameof(instrument)); + } + + if (instrument is not Instrument and not ObservableInstrument) + { + throw new InvalidOperationException(SR.InvalidInstrumentType); + } + + _measurements = new List>(); + + _meterListener = new MeterListener(); + + _instrument = instrument; + _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); + _meterListener.EnableMeasurementEvents(instrument, state: null); + _meterListener.Start(); + } + + /// + /// Initialize a new instance to record the measurement published by an with the name + /// and a meter having the name and scope + /// + /// The name of the meter. + /// The name of the meter. + /// The name of the instrument. + public InstrumentRecorder(object? scopeFilter, string meterName, string instrumentName) + { + if (meterName is null) + { + throw new ArgumentNullException(nameof(meterName)); + } + + if (instrumentName is null) + { + throw new ArgumentNullException(nameof(instrumentName)); + } + + Initialize((instrument) => object.Equals(instrument.Meter.Scope, scopeFilter) && + instrument.Meter.Name == meterName && + instrument.Name == instrumentName); + + Debug.Assert(_meterListener is not null); + Debug.Assert(_measurements is not null); + } + + /// + /// Initialize a new instance to record the measurement published by an with the name + /// and the meter . + /// + /// The meter that produced the instrument required for recording the published measurements. + /// The name of the instrument. + public InstrumentRecorder(Meter meter, string instrumentName) + { + if (meter is null) + { + throw new ArgumentNullException(nameof(meter)); + } + + if (instrumentName is null) + { + throw new ArgumentNullException(nameof(instrumentName)); + } + + Initialize((instrument) => object.ReferenceEquals(instrument.Meter, meter) && instrument.Name == instrumentName); + + Debug.Assert(_meterListener is not null); + Debug.Assert(_measurements is not null); + } + + private void Initialize(Func instrumentPredicate) + { + _measurements = new List>(); + + _meterListener = new MeterListener(); + _meterListener.InstrumentPublished = (instrument, listener) => + { + if (instrumentPredicate(instrument) && (instrument is ObservableInstrument || instrument is Instrument)) + { + if (Interlocked.CompareExchange(ref _instrument, instrument, null) is null) + { + _isObservableInstrument = instrument is ObservableInstrument; + listener.EnableMeasurementEvents(instrument, state: null); + } + } + }; + + _meterListener.SetMeasurementEventCallback(OnMeasurementRecorded); + _meterListener.Start(); + } + + /// + /// Gets the that is being recorded. + /// + public Instrument? Instrument + { + get => _instrument; + private set => _instrument = value; + } + + private void OnMeasurementRecorded(Instrument instrument, T measurement, ReadOnlySpan> tags, object? state) + { + var m = new Measurement(measurement, tags); + + lock (_measurements) + { + _measurements.Add(m); + } + } + + /// + /// Gets the measurements recorded by this . + /// + /// If true, the previously recorded measurements will be cleared. + /// The measurements recorded by this . + public IEnumerable> GetMeasurements(bool clear = false) + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(InstrumentRecorder)); + } + + if (_isObservableInstrument) + { + _meterListener.RecordObservableInstruments(); + } + + lock (_measurements) + { + IEnumerable> measurements = _measurements.ToArray(); + if (clear) + { + _measurements.Clear(); + } + return measurements; + } + } + + /// + /// Disposes the and stops recording measurements. + /// + public void Dispose() + { + if (_disposed) + { + return; + } + _disposed = true; + _meterListener.Dispose(); + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Meter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Meter.cs index 431272393a0d3a..6fe7ac17c93a70 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Meter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/Meter.cs @@ -2,8 +2,9 @@ // The .NET Foundation licenses this file to you under the MIT license. using System.Collections.Generic; -using System.Threading; using System.Diagnostics; +using System.Linq; +using System.Threading; namespace System.Diagnostics.Metrics { @@ -14,23 +15,68 @@ public class Meter : IDisposable { private static readonly List s_allMeters = new List(); private List _instruments = new List(); + private Dictionary> _nonObservableInstrumentsCache = new(); internal bool Disposed { get; private set; } + /// + /// Initialize a new instance of the Meter using the . + /// + public Meter(MeterOptions options) + { + if (options is null) + { + throw new ArgumentNullException(nameof(options)); + } + + Debug.Assert(options.Name is not null); + + Initialize(options.Name, options.Version, options.Tags, options.Scope); + + Debug.Assert(Name is not null); + } + /// /// Initializes a new instance of the Meter using the meter name. /// /// The Meter name. - public Meter(string name) : this (name, null) {} + public Meter(string name) : this (name, null, null, null) {} /// /// Initializes a new instance of the Meter using the meter name and version. /// /// The Meter name. /// The optional Meter version. - public Meter(string name, string? version) + public Meter(string name, string? version) : this (name, version, null, null) {} + + /// + /// Initializes a new instance of the Meter using the meter name and version. + /// + /// The Meter name. + /// The optional Meter version. + /// The optional Meter tags. + /// The optional Meter scope. + /// + /// You can use the scope object to link several Meters with a particular scope. + /// For instance, a dependency injection container can choose to associate all Meters that are created within the container with its own scope. + /// If the scope object is null, it indicates that the Meter is not linked to any particular scope. + /// + public Meter(string name, string? version, IEnumerable>? tags, object? scope = null) + { + Initialize(name, version, tags, scope); + Debug.Assert(Name is not null); + } + + private void Initialize(string name, string? version, IEnumerable>? tags, object? scope = null) { Name = name ?? throw new ArgumentNullException(nameof(name)); Version = version; + if (tags is not null) + { + var tagsArray = tags.ToArray(); + // Array.Sort(tagsArray, (left, right) => string.Compare(left.Key, right.Key, StringComparison.Ordinal)); + Tags = tagsArray; + } + Scope = scope; lock (Instrument.SyncObject) { @@ -44,12 +90,34 @@ public Meter(string name, string? version) /// /// Returns the Meter name. /// - public string Name { get; } + public string Name { get; private set; } /// /// Returns the Meter Version. /// - public string? Version { get; } + public string? Version { get; private set; } + + /// + /// Returns the tags associated with the Meter. + /// + public IEnumerable>? Tags { get; private set; } + + /// + /// Returns the Meter scope object. + /// + public object? Scope { get; private set; } + + /// + /// Create a metrics Counter object. + /// + /// The instrument name. cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// + /// Counter is an Instrument which supports non-negative increments. + /// Example uses for Counter: count the number of bytes received, count the number of requests completed, count the number of accounts created, count the number of checkpoints run, and count the number of HTTP 5xx errors. + /// + public Counter CreateCounter(string name, string? unit = null, string? description = null) where T : struct => CreateCounter(name, unit, description, tags: null); /// /// Create a metrics Counter object. @@ -57,11 +125,24 @@ public Meter(string name, string? version) /// The instrument name. cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. + /// tags to attach to the counter. /// /// Counter is an Instrument which supports non-negative increments. /// Example uses for Counter: count the number of bytes received, count the number of requests completed, count the number of accounts created, count the number of checkpoints run, and count the number of HTTP 5xx errors. /// - public Counter CreateCounter(string name, string? unit = null, string? description = null) where T : struct => new Counter(this, name, unit, description); + public Counter CreateCounter(string name, string? unit, string? description, IEnumerable>? tags) where T : struct + => (Counter)GetOrCreateInstrument(typeof(Counter), name, unit, description, tags, () => new Counter(this, name, unit, description, tags)); + + /// + /// Histogram is an Instrument which can be used to report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile. + /// + /// The instrument name. cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// + /// Example uses for Histogram: the request duration and the size of the response payload. + /// + public Histogram CreateHistogram(string name, string? unit = null, string? description = null) where T : struct => CreateHistogram(name, unit, description, tags: null); /// /// Histogram is an Instrument which can be used to report arbitrary values that are likely to be statistically meaningful. It is intended for statistics such as histograms, summaries, and percentile. @@ -69,10 +150,25 @@ public Meter(string name, string? version) /// The instrument name. cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. + /// tags to attach to the counter. /// /// Example uses for Histogram: the request duration and the size of the response payload. /// - public Histogram CreateHistogram(string name, string? unit = null, string? description = null) where T : struct => new Histogram(this, name, unit, description); + public Histogram CreateHistogram(string name, string? unit, string? description, IEnumerable>? tags) where T : struct + => (Histogram)GetOrCreateInstrument(typeof(Histogram), name, unit, description, tags, () => new Histogram(this, name, unit, description, tags)); + + /// + /// Create a metrics UpDownCounter object. + /// + /// The instrument name. Cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// + /// UpDownCounter is an Instrument which supports reporting positive or negative metric values. + /// Example uses for UpDownCounter: reporting the change in active requests or queue size. + /// + public UpDownCounter CreateUpDownCounter(string name, string? unit = null, string? description = null) where T : struct + => CreateUpDownCounter(name, unit, description, tags: null); /// /// Create a metrics UpDownCounter object. @@ -80,11 +176,13 @@ public Meter(string name, string? version) /// The instrument name. Cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. + /// tags to attach to the counter. /// /// UpDownCounter is an Instrument which supports reporting positive or negative metric values. /// Example uses for UpDownCounter: reporting the change in active requests or queue size. /// - public UpDownCounter CreateUpDownCounter(string name, string? unit = null, string? description = null) where T : struct => new UpDownCounter(this, name, unit, description); + public UpDownCounter CreateUpDownCounter(string name, string? unit, string? description, IEnumerable>? tags) where T : struct + => (UpDownCounter)GetOrCreateInstrument(typeof(UpDownCounter), name, unit, description, tags, () => new UpDownCounter(this, name, unit, description, tags)); /// /// Create an ObservableUpDownCounter object. ObservableUpDownCounter is an Instrument which reports increasing or decreasing value(s) when the instrument is being observed. @@ -97,7 +195,21 @@ public Meter(string name, string? version) /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. /// public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableUpDownCounter(this, name, observeValue, unit, description); + new ObservableUpDownCounter(this, name, observeValue, unit, description, tags: null); + + /// + /// Create an ObservableUpDownCounter object. ObservableUpDownCounter is an Instrument which reports increasing or decreasing value(s) when the instrument is being observed. + /// + /// The instrument name. Cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. + /// + public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableUpDownCounter(this, name, observeValue, unit, description, tags); /// @@ -111,7 +223,21 @@ public ObservableUpDownCounter CreateObservableUpDownCounter(string name, /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. /// public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func> observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableUpDownCounter(this, name, observeValue, unit, description); + new ObservableUpDownCounter(this, name, observeValue, unit, description, tags: null); + + /// + /// Create an ObservableUpDownCounter object. ObservableUpDownCounter is an Instrument which reports increasing or decreasing value(s) when the instrument is being observed. + /// + /// The instrument name. Cannot be null. + /// The callback to call to get the measurements when the is called by + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. + /// + public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableUpDownCounter(this, name, observeValue, unit, description, tags); /// /// Create an ObservableUpDownCounter object. ObservableUpDownCounter is an Instrument which reports increasing or decreasing value(s) when the instrument is being observed. @@ -124,8 +250,21 @@ public ObservableUpDownCounter CreateObservableUpDownCounter(string name, /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. /// public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct => - new ObservableUpDownCounter(this, name, observeValues, unit, description); + new ObservableUpDownCounter(this, name, observeValues, unit, description, tags: null); + /// + /// Create an ObservableUpDownCounter object. ObservableUpDownCounter is an Instrument which reports increasing or decreasing value(s) when the instrument is being observed. + /// + /// The instrument name. Cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableUpDownCounter: the process heap size or the approximate number of items in a lock-free circular buffer. + /// + public ObservableUpDownCounter CreateObservableUpDownCounter(string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableUpDownCounter(this, name, observeValues, unit, description, tags); /// /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. @@ -138,8 +277,21 @@ public ObservableUpDownCounter CreateObservableUpDownCounter(string name, /// Example uses for ObservableCounter: The number of page faults for each process. /// public ObservableCounter CreateObservableCounter(string name, Func observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableCounter(this, name, observeValue, unit, description); + new ObservableCounter(this, name, observeValue, unit, description, tags: null); + /// + /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableCounter: The number of page faults for each process. + /// + public ObservableCounter CreateObservableCounter(string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableCounter(this, name, observeValue, unit, description, tags); /// /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. @@ -152,7 +304,22 @@ public ObservableCounter CreateObservableCounter(string name, Func obse /// Example uses for ObservableCounter: The number of page faults for each process. /// public ObservableCounter CreateObservableCounter(string name, Func> observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableCounter(this, name, observeValue, unit, description); + new ObservableCounter(this, name, observeValue, unit, description, tags: null); + + /// + /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableCounter: The number of page faults for each process. + /// + public ObservableCounter CreateObservableCounter(string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableCounter(this, name, observeValue, unit, description, tags); + /// /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. @@ -165,8 +332,21 @@ public ObservableCounter CreateObservableCounter(string name, Func public ObservableCounter CreateObservableCounter(string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct => - new ObservableCounter(this, name, observeValues, unit, description); + new ObservableCounter(this, name, observeValues, unit, description, tags: null); + /// + /// ObservableCounter is an Instrument which reports monotonically increasing value(s) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + /// + /// Example uses for ObservableCounter: The number of page faults for each process. + /// + public ObservableCounter CreateObservableCounter(string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableCounter(this, name, observeValues, unit, description, tags); /// /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. @@ -176,7 +356,18 @@ public ObservableCounter CreateObservableCounter(string name, FuncOptional instrument unit of measurements. /// Optional instrument description. public ObservableGauge CreateObservableGauge(string name, Func observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableGauge(this, name, observeValue, unit, description); + new ObservableGauge(this, name, observeValue, unit, description, tags: null); + + /// + /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + public ObservableGauge CreateObservableGauge(string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableGauge(this, name, observeValue, unit, description, tags); /// /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. @@ -186,7 +377,18 @@ public ObservableGauge CreateObservableGauge(string name, Func observeV /// Optional instrument unit of measurements. /// Optional instrument description. public ObservableGauge CreateObservableGauge(string name, Func> observeValue, string? unit = null, string? description = null) where T : struct => - new ObservableGauge(this, name, observeValue, unit, description); + new ObservableGauge(this, name, observeValue, unit, description, tags: null); + + /// + /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + public ObservableGauge CreateObservableGauge(string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableGauge(this, name, observeValue, unit, description, tags); /// /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. @@ -196,13 +398,35 @@ public ObservableGauge CreateObservableGauge(string name, FuncOptional instrument unit of measurements. /// Optional instrument description. public ObservableGauge CreateObservableGauge(string name, Func>> observeValues, string? unit = null, string? description = null) where T : struct => - new ObservableGauge(this, name, observeValues, unit, description); + new ObservableGauge(this, name, observeValues, unit, description, tags: null); + + /// + /// ObservableGauge is an asynchronous Instrument which reports non-additive value(s) (e.g. the room temperature - it makes no sense to report the temperature value from multiple rooms and sum them up) when the instrument is being observed. + /// + /// The instrument name. cannot be null. + /// The callback to call to get the measurements when the is called by . + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + public ObservableGauge CreateObservableGauge(string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) where T : struct => + new ObservableGauge(this, name, observeValues, unit, description, tags); + + /// + /// Dispose the Meter which will disable all instruments created by this meter. + /// + public void Dispose() => Dispose(true); /// /// Dispose the Meter which will disable all instruments created by this meter. /// - public void Dispose() + /// True if called from Dispose(), false if called from a finalizer. + protected virtual void Dispose(bool disposing) { + if (!disposing) + { + return; + } + List? instruments = null; lock (Instrument.SyncObject) @@ -218,6 +442,11 @@ public void Dispose() _instruments = new List(); } + lock (_nonObservableInstrumentsCache) + { + _nonObservableInstrumentsCache.Clear(); + } + if (instruments is not null) { foreach (Instrument instrument in instruments) @@ -227,7 +456,67 @@ public void Dispose() } } + private static Instrument? GetCachedInstrument(List instrumentList, Type instrumentType, string? unit, string? description, IEnumerable>? tags) + { + Debug.Assert(instrumentList is not null); + foreach (Instrument instrument in instrumentList) + { + if (instrument.GetType() == instrumentType && instrument.Unit == unit && + instrument.Description == description && DiagnosticsHelper.CompareTags(instrument.Tags, tags)) + { + return instrument; + } + } + + return null; + } + + // AddInstrument will be called when publishing the instrument (i.e. calling Instrument.Publish()). + private Instrument GetOrCreateInstrument(Type instrumentType, string name, string? unit, string? description, IEnumerable>? tags, Func instrumentCreator) + { + List? instrumentList; + + lock (_nonObservableInstrumentsCache) + { + if (!_nonObservableInstrumentsCache.TryGetValue(name, out instrumentList)) + { + instrumentList = new List(); + _nonObservableInstrumentsCache.Add(name, instrumentList); + } + } + + Debug.Assert(instrumentList is not null); + + lock (instrumentList) + { + // Find out if the instrument is already created. + Instrument? cachedInstrument = GetCachedInstrument(instrumentList, instrumentType, unit, description, tags); + if (cachedInstrument is not null) + { + return cachedInstrument; + } + } + + Instrument newInstrument = instrumentCreator.Invoke(); + + lock (instrumentList) + { + // It is possible GetOrCreateInstrument get called synchronously from different threads with same instrument name. + // we need to ensure only one instrument is added to the list. + Instrument? cachedInstrument = GetCachedInstrument(instrumentList, instrumentType, unit, description, tags); + if (cachedInstrument is not null) + { + return cachedInstrument; + } + + instrumentList.Add(newInstrument); + } + + return newInstrument; + } + // AddInstrument will be called when publishing the instrument (i.e. calling Instrument.Publish()). + // This method is called inside the lock Instrument.SyncObject internal bool AddInstrument(Instrument instrument) { if (!_instruments.Contains(instrument)) @@ -239,6 +528,7 @@ internal bool AddInstrument(Instrument instrument) } // Called from MeterListener.Start + // This method is called inside the lock Instrument.SyncObject internal static List? GetPublishedInstruments() { List? instruments = null; diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterOptions.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterOptions.cs new file mode 100644 index 00000000000000..9697c4edda35bc --- /dev/null +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/MeterOptions.cs @@ -0,0 +1,58 @@ +// 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; + +namespace System.Diagnostics.Metrics +{ + /// + /// Options for creating a . + /// + public class MeterOptions + { + private string _name; + + /// + /// The Meter name. + /// + public string Name + { + get => _name; + set + { + if (value is null) + { + throw new ArgumentNullException(nameof(value)); + } + + _name = value; + } + } + + /// + /// The optional Meter version. + /// + public string? Version { get; set; } + + /// + /// The optional list of key-value pair tags associated with the Meter. + /// + public IEnumerable>? Tags { get; set; } + + /// + /// The optional opaque object to attach to the Meter. The scope object can be attached to multiple meters for scoping purposes. + /// + public object? Scope { get; set; } + + /// + /// Constructs a new instance of . + /// + /// The Meter name. + public MeterOptions(string name) + { + Name = name; + + Debug.Assert(_name is not null); + } + } +} diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableCounter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableCounter.cs index 01541bca455f8c..52d64194087ca7 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableCounter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableCounter.cs @@ -17,19 +17,31 @@ public sealed class ObservableCounter : ObservableInstrument where T : str { private readonly object _callback; - internal ObservableCounter(Meter meter, string name, Func observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableCounter(Meter meter, string name, Func observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableCounter(Meter meter, string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableCounter(Meter meter, string name, Func> observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableCounter(Meter meter, string name, Func> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableCounter(Meter meter, string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description) : this(meter, name, observeValues, unit, description, tags: null) + { + } + + internal ObservableCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValues ?? throw new ArgumentNullException(nameof(observeValues)); Publish(); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableGauge.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableGauge.cs index db57db06cfc030..8f6107c321d835 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableGauge.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableGauge.cs @@ -17,19 +17,31 @@ public sealed class ObservableGauge : ObservableInstrument where T : struc { private readonly object _callback; - internal ObservableGauge(Meter meter, string name, Func observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableGauge(Meter meter, string name, Func observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableGauge(Meter meter, string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableGauge(Meter meter, string name, Func> observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableGauge(Meter meter, string name, Func> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableGauge(Meter meter, string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableGauge(Meter meter, string name, Func>> observeValues, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableGauge(Meter meter, string name, Func>> observeValues, string? unit, string? description) : this(meter, name, observeValues, unit, description, tags: null) + { + } + + internal ObservableGauge(Meter meter, string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValues ?? throw new ArgumentNullException(nameof(observeValues)); Publish(); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableInstrument.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableInstrument.cs index 79cc57314a8e99..d718dd9867155f 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableInstrument.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableInstrument.cs @@ -22,7 +22,20 @@ public abstract class ObservableInstrument : Instrument where T : struct /// The instrument name. cannot be null. /// Optional instrument unit of measurements. /// Optional instrument description. - protected ObservableInstrument(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) + protected ObservableInstrument(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, tags: null) + { + } + + /// + /// Create the metrics observable instrument using the properties meter, name, description, and unit. + /// All classes extending ObservableInstrument{T} need to call this constructor when constructing object of the extended class. + /// + /// The meter that created the instrument. + /// The instrument name. cannot be null. + /// Optional instrument unit of measurements. + /// Optional instrument description. + /// tags to attach to the counter. + protected ObservableInstrument(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { ValidateTypeParameter(); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableUpDownCounter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableUpDownCounter.cs index 9d3b32b2cb1164..fc472320e2b5d8 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableUpDownCounter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/ObservableUpDownCounter.cs @@ -17,19 +17,31 @@ public sealed class ObservableUpDownCounter : ObservableInstrument where T { private readonly object _callback; - internal ObservableUpDownCounter(Meter meter, string name, Func observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableUpDownCounter(Meter meter, string name, Func observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableUpDownCounter(Meter meter, string name, Func observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableUpDownCounter(Meter meter, string name, Func> observeValue, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableUpDownCounter(Meter meter, string name, Func> observeValue, string? unit, string? description) : this(meter, name, observeValue, unit, description, tags: null) + { + } + + internal ObservableUpDownCounter(Meter meter, string name, Func> observeValue, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValue ?? throw new ArgumentNullException(nameof(observeValue)); Publish(); } - internal ObservableUpDownCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description) : base(meter, name, unit, description) + internal ObservableUpDownCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description) : this(meter, name, observeValues, unit, description, tags: null) + { + } + + internal ObservableUpDownCounter(Meter meter, string name, Func>> observeValues, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { _callback = observeValues ?? throw new ArgumentNullException(nameof(observeValues)); Publish(); diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/UpDownCounter.cs b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/UpDownCounter.cs index 8a908e9d665950..79d0ed29d8f417 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/UpDownCounter.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/src/System/Diagnostics/Metrics/UpDownCounter.cs @@ -14,7 +14,11 @@ namespace System.Diagnostics.Metrics /// public sealed class UpDownCounter : Instrument where T : struct { - internal UpDownCounter(Meter meter, string name, string? unit, string? description) : base(meter, name, unit, description) + internal UpDownCounter(Meter meter, string name, string? unit, string? description) : this(meter, name, unit, description, tags: null) + { + } + + internal UpDownCounter(Meter meter, string name, string? unit, string? description, IEnumerable>? tags) : base(meter, name, unit, description, tags) { Publish(); } diff --git a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs index 6f9438ee0ccc1b..aafbbedf2f8f4e 100644 --- a/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs +++ b/src/libraries/System.Diagnostics.DiagnosticSource/tests/MetricsTests.cs @@ -1,12 +1,13 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using Xunit; -using System.Threading; -using System.Threading.Tasks; -using System.Diagnostics.Metrics; using Microsoft.DotNet.RemoteExecutor; using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Xunit; namespace System.Diagnostics.Metrics.Tests { @@ -24,7 +25,7 @@ public void MeterConstructionTest() Assert.Equal("meter2", meter.Name); Assert.Equal("v1.0", meter.Version); - Assert.Throws(() => new Meter(null)); + Assert.Throws(() => new Meter(name: null)); }).Dispose(); } @@ -182,7 +183,6 @@ public void ListeningToInstrumentsPublishingTest() // MeasurementsCompleted should be called 4 times for every instrument. Assert.Equal(0, instrumentsEncountered); } - }).Dispose(); } @@ -928,10 +928,10 @@ public void NullMeasurementEventCallbackTest() } [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] - public void EnableListeneingMultipleTimesWithDifferentState() + public void EnableListeningMultipleTimesWithDifferentState() { RemoteExecutor.Invoke(() => { - Meter meter = new Meter("EnableListeneingMultipleTimesWithDifferentState"); + Meter meter = new Meter("EnableListeningMultipleTimesWithDifferentState"); Counter counter = meter.CreateCounter("Counter"); @@ -1166,6 +1166,301 @@ public void TestRecordingMeasurementsWithTagList() }).Dispose(); } + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestMeterCreationWithOptions() + { + RemoteExecutor.Invoke(() => + { + using Meter meter1 = new Meter("TestMeterCreationWithOptions1"); + Assert.Equal("TestMeterCreationWithOptions1", meter1.Name); + Assert.Null(meter1.Version); + Assert.Null(meter1.Tags); + Assert.Null(meter1.Scope); + + using Meter meter2 = new Meter("TestMeterCreationWithOptions2", "2.0", new TagList() { { "Key1", "Value1" } }); + Assert.Equal("TestMeterCreationWithOptions2", meter2.Name); + Assert.Equal("2.0", meter2.Version); + Assert.Equal(new[] { new KeyValuePair("Key1", "Value1") }, meter2.Tags); + Assert.Null(meter2.Scope); + + using Meter meter3 = new Meter("TestMeterCreationWithOptions3", "3.0", new TagList() { { "Key3", "Value3" } }, "Scope"); + Assert.Equal("TestMeterCreationWithOptions3", meter3.Name); + Assert.Equal("3.0", meter3.Version); + Assert.Equal(new[] { new KeyValuePair("Key3", "Value3") }, meter3.Tags); + Assert.Equal("Scope", meter3.Scope); + + Assert.Throws(() => new MeterOptions(null!)); + Assert.Throws(() => new MeterOptions("Something").Name = null!); + + using Meter meter4 = new Meter(new MeterOptions("TestMeterCreationWithOptions4")); + Assert.Equal("TestMeterCreationWithOptions4", meter4.Name); + Assert.Null(meter4.Version); + Assert.Null(meter4.Tags); + Assert.Null(meter4.Scope); + + using Meter meter5 = new Meter(new MeterOptions("TestMeterCreationWithOptions5") { Version = "5.0" }); + Assert.Equal("TestMeterCreationWithOptions5", meter5.Name); + Assert.Equal("5.0", meter5.Version); + Assert.Null(meter5.Tags); + Assert.Null(meter5.Scope); + + using Meter meter6 = new Meter(new MeterOptions("TestMeterCreationWithOptions6") { Tags = new TagList() { { "Key6", "Value6"} } }); + Assert.Equal("TestMeterCreationWithOptions6", meter6.Name); + Assert.Null(meter6.Version); + Assert.Equal(new[] { new KeyValuePair("Key6", "Value6") }, meter6.Tags); + Assert.Null(meter5.Scope); + + using Meter meter7 = new Meter(new MeterOptions("TestMeterCreationWithOptions7") { Scope = "Scope7" }); + Assert.Equal("TestMeterCreationWithOptions7", meter7.Name); + Assert.Null(meter7.Version); + Assert.Null(meter7.Tags); + Assert.Equal("Scope7", meter7.Scope); + + using Meter meter8 = new Meter(new MeterOptions("TestMeterCreationWithOptions8") { Version = "8.0", Tags = new TagList() { { "Key8", "Value8" } }, Scope = "Scope8" }); + Assert.Equal("TestMeterCreationWithOptions8", meter8.Name); + Assert.Equal("8.0", meter8.Version); + Assert.Equal(new[] { new KeyValuePair("Key8", "Value8") }, meter8.Tags); + Assert.Equal("Scope8", meter8.Scope); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestCachedInstruments() + { + RemoteExecutor.Invoke(() => + { + using Meter meter = new Meter("TestCachedInstruments"); + + Counter counter1 = meter.CreateCounter("name1"); + Counter counter2 = meter.CreateCounter("name1"); + + Assert.True(object.ReferenceEquals(counter1, counter2)); + + Counter counter3 = meter.CreateCounter("name1", "unique"); + Assert.False(object.ReferenceEquals(counter1, counter3)); + + var list1 = new List> + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", null) + }; + + Counter counter4 = meter.CreateCounter("name", null, null, list1); + Counter counter5 = meter.CreateCounter("name", null, null, list1); + + Assert.True(object.ReferenceEquals(counter4, counter5)); + + Counter counter6 = meter.CreateCounter("name", "diff", null, list1); + + Assert.False(object.ReferenceEquals(counter4, counter6)); + + Counter counter7 = meter.CreateCounter("name", null, null, list1); + + Assert.False(object.ReferenceEquals(counter4, counter7)); + + var list2 = new List> + { + new KeyValuePair("key1", "value1"), + new KeyValuePair("key2", "value2") + }; + + Counter counter8 = meter.CreateCounter("name", null, null, list2); + + Assert.False(object.ReferenceEquals(counter4, counter8)); + + Histogram histogram1 = meter.CreateHistogram("name", null, null, list2); + + Assert.False(object.ReferenceEquals(counter8, histogram1)); + + Histogram histogram2 = meter.CreateHistogram("name", null, null, list2); + + Assert.True(object.ReferenceEquals(histogram2, histogram1)); + + var list3 = new List> + { + new KeyValuePair("key1", "value3"), + new KeyValuePair("key2", "value2") + }; + + Histogram histogram3 = meter.CreateHistogram("name", null, null, list3); + + Assert.False(object.ReferenceEquals(histogram3, histogram1)); + + UpDownCounter upDownCounter1 = meter.CreateUpDownCounter("name", null, null, list2); + + Assert.False(object.ReferenceEquals(counter8, upDownCounter1)); + + UpDownCounter upDownCounter2 = meter.CreateUpDownCounter("name", null, null, list2); + + Assert.True(object.ReferenceEquals(upDownCounter2, upDownCounter1)); + + UpDownCounter upDownCounter3 = meter.CreateUpDownCounter("name", null, null, list3); + + Assert.False(object.ReferenceEquals(upDownCounter3, upDownCounter1)); + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestInstrumentCreationWithTags() + { + RemoteExecutor.Invoke(() => { + using Meter meter = new Meter("TestInstrumentCreationWithTags"); + + Instrument ins1 = meter.CreateCounter("counter", null, null, new TagList() { { "c1", "cv-1" } }); + Assert.Equal(new[] { new KeyValuePair("c1", "cv-1") }, ins1.Tags); + + Instrument ins2 = meter.CreateHistogram("histogram", null, null, new TagList() { { "h1", "hv-1" } }); + Assert.Equal(new[] { new KeyValuePair("h1", "hv-1") }, ins2.Tags); + + Instrument ins3 = meter.CreateUpDownCounter("UpDownCounter", null, null, new TagList() { { "udc1", "udc-v1" } }); + Assert.Equal(new[] { new KeyValuePair("udc1", "udc-v1") }, ins3.Tags); + + Instrument ins4 = meter.CreateObservableCounter("ObservableCounter1", () => 1, null, null, new TagList() { { "oc1", "oc-v1" } }); + Assert.Equal(new[] { new KeyValuePair("oc1", "oc-v1") }, ins4.Tags); + + Instrument ins5 = meter.CreateObservableCounter("ObservableCounter2", () => new Measurement(2), null, null, new TagList() { { "oc2", "oc-v2" } }); + Assert.Equal(new[] { new KeyValuePair("oc2", "oc-v2") }, ins5.Tags); + + Instrument ins6 = meter.CreateObservableCounter("ObservableCounter3", () => new Measurement[] { new Measurement(3) }, null, null, new TagList() { { "oc3", "oc-v3" } }); + Assert.Equal(new[] { new KeyValuePair("oc3", "oc-v3") }, ins6.Tags); + + Instrument ins7 = meter.CreateObservableGauge("ObservableGauge1", () => 1, null, null, new TagList() { { "og1", "og-v1" } }); + Assert.Equal(new[] { new KeyValuePair("og1", "og-v1") }, ins7.Tags); + + Instrument ins8 = meter.CreateObservableGauge("ObservableGauge2", () => new Measurement(2), null, null, new TagList() { { "og2", "og-v2" } }); + Assert.Equal(new[] { new KeyValuePair("og2", "og-v2") }, ins8.Tags); + + Instrument ins9 = meter.CreateObservableGauge("ObservableGauge3", () => new Measurement[] { new Measurement(3) }, null, null, new TagList() { { "og3", "og-v3" } }); + Assert.Equal(new[] { new KeyValuePair("og3", "og-v3") }, ins9.Tags); + + Instrument ins10 = meter.CreateObservableUpDownCounter("ObservableUpDownCounter1", () => 1, null, null, new TagList() { { "oudc1", "oudc-v1" } }); + Assert.Equal(new[] { new KeyValuePair("oudc1", "oudc-v1") }, ins10.Tags); + + Instrument ins11 = meter.CreateObservableGauge("ObservableUpDownCounter2", () => new Measurement(2), null, null, new TagList() { { "oudc2", "oudc-v2" } }); + Assert.Equal(new[] { new KeyValuePair("oudc2", "oudc-v2") }, ins11.Tags); + + Instrument ins12 = meter.CreateObservableGauge("ObservableUpDownCounter3", () => new Measurement[] { new Measurement(3) }, null, null, new TagList() { { "oudc3", "oudc-v3" } }); + Assert.Equal(new[] { new KeyValuePair("oudc3", "oudc-v3") }, ins12.Tags); + + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestInstrumentRecorderNegativeCases() + { + RemoteExecutor.Invoke(() => { + using Meter meter = new Meter("TestInstrumentRecorderNegativeCases"); + + Assert.Throws(() => new InstrumentRecorder(instrument: null)); + Assert.Throws(() => new InstrumentRecorder(scopeFilter: null, meterName: null, instrumentName: "instrumentName")); + Assert.Throws(() => new InstrumentRecorder(scopeFilter:null, meterName:"meterName", instrumentName: null)); + Assert.Throws(() => new InstrumentRecorder(meter: null, instrumentName: "instrumentName")); + Assert.Throws(() => new InstrumentRecorder(meter: meter, instrumentName: null)); + + // Test InstrumentRecorder generic type mismatch the instrument generic type + Instrument instrument = meter.CreateCounter("counter"); + Assert.Throws(() => new InstrumentRecorder(instrument: instrument)); + + }).Dispose(); + } + + [ConditionalFact(typeof(RemoteExecutor), nameof(RemoteExecutor.IsSupported))] + public void TestInstrumentRecorder() + { + RemoteExecutor.Invoke(() => { + using Meter meter = new Meter("TestInstrumentRecorder1"); + + // + // Test listening using instrument object + // + + Counter instrument = meter.CreateCounter("counter"); + InstrumentRecorder recorder1 = new InstrumentRecorder(instrument); + Assert.Equal(instrument, recorder1.Instrument); + Assert.Equal(new Measurement[0], recorder1.GetMeasurements()); + instrument.Add(1); + Assert.Equal(1, recorder1.GetMeasurements().Count()); + Assert.Equal(1, recorder1.GetMeasurements().ElementAt(0).Value); + Assert.Equal(0, recorder1.GetMeasurements().ElementAt(0).Tags.Length); + + recorder1.GetMeasurements(clear: true); // clear previous collected measurements + Assert.Equal(0, recorder1.GetMeasurements().Count()); + instrument.Add(2, new KeyValuePair("k1", "v1")); + Assert.Equal(1, recorder1.GetMeasurements().Count()); + Assert.Equal(2, recorder1.GetMeasurements().ElementAt(0).Value); + Assert.Equal(1, recorder1.GetMeasurements().ElementAt(0).Tags.Length); + Assert.Equal(new KeyValuePair("k1", "v1"), recorder1.GetMeasurements().ElementAt(0).Tags[0]); + + // + // Test listening using instrument name + // + + InstrumentRecorder recorder2 = new InstrumentRecorder(meter, "counter"); + Assert.Equal(instrument, recorder2.Instrument); + Assert.Equal(0, recorder2.GetMeasurements().Count()); + instrument.Add(3); + Assert.Equal(2, recorder1.GetMeasurements().Count()); + Assert.Equal(2, recorder1.GetMeasurements().ElementAt(0).Value); + Assert.Equal(3, recorder1.GetMeasurements().ElementAt(1).Value); + Assert.Equal(1, recorder2.GetMeasurements().Count()); + Assert.Equal(3, recorder2.GetMeasurements().ElementAt(0).Value); + + // + // Test listening using instrument name with different generic type + // + + InstrumentRecorder recorder3 = new InstrumentRecorder(meter, "counter"); + Assert.Null(recorder3.Instrument); + Counter instrument1 = meter.CreateCounter("counter"); + Assert.Equal(instrument1, recorder3.Instrument); + Assert.Equal(0, recorder3.GetMeasurements().Count()); + instrument1.Add(4); + Assert.Equal(1, recorder3.GetMeasurements().Count()); + Assert.Equal(4, recorder3.GetMeasurements().ElementAt(0).Value); + + // + // Test using scope filter + // + + // using same existing meter name + using Meter meter1 = new Meter("TestInstrumentRecorder1", null, null, "Scope1"); + + InstrumentRecorder recorder4 = new InstrumentRecorder(scopeFilter: null, meterName: "TestInstrumentRecorder1", instrumentName: "counter"); + Assert.Equal(instrument, recorder4.Instrument); + + InstrumentRecorder recorder5 = new InstrumentRecorder(scopeFilter: "Scope1", meterName: "TestInstrumentRecorder1", instrumentName: "counter"); + Assert.Null(recorder5.Instrument); + Counter instrument2 = meter1.CreateCounter("counter"); + Assert.Equal(instrument2, recorder5.Instrument); + + // + // Test meter creating 2 instruments with same name but different unit and ensure listening to the first created one + // + + Counter instrument3 = meter.CreateCounter("counter", "myUnit"); + InstrumentRecorder recorder6 = new InstrumentRecorder(meter: meter, instrumentName: "counter"); + Assert.Equal(instrument, recorder6.Instrument); + + // + // Ensure can listen to the observable instrument + // + + InstrumentRecorder recorder7 = new InstrumentRecorder(meter: meter, instrumentName: "observableCounter"); + Assert.Null(recorder7.Instrument); + ObservableCounter instrument4 = meter.CreateObservableCounter("observableCounter", () => 10); + Assert.Equal(instrument4, recorder7.Instrument); + Measurement measurementWith10Value = new Measurement(10, default); + Assert.True(recorder7.GetMeasurements().Same(new Measurement[] { measurementWith10Value })); + Assert.True(recorder7.GetMeasurements().Same(new Measurement[] { measurementWith10Value, measurementWith10Value })); + Assert.True(recorder7.GetMeasurements(true).Same(new Measurement[] { measurementWith10Value, measurementWith10Value, measurementWith10Value })); + Assert.True(recorder7.GetMeasurements().Same(new Measurement[] { measurementWith10Value })); + // Assert.Equal(new Measurement[] { measurementWith10Value, measurementWith10Value }, recorder7.GetMeasurements()); + // Assert.Equal(new Measurement[] { measurementWith10Value, measurementWith10Value, measurementWith10Value }, recorder7.GetMeasurements(clear: true)); + // Assert.Equal(new Measurement[] { measurementWith10Value }, recorder7.GetMeasurements()); + + }).Dispose(); + } + private void PublishCounterMeasurement(Counter counter, T value, KeyValuePair[] tags) where T : struct { switch (tags.Length) @@ -1438,5 +1733,31 @@ private T ConvertValue(short value) where T : struct public static class DiagnosticsCollectionExtensions { public static void Add(this ICollection> collection, T1 item1, T2 item2) => collection?.Add(new KeyValuePair(item1, item2)); + public static bool Same(this IEnumerable> measurements, IEnumerable> expected) where T : struct + { + IEnumerator> enumerator = measurements.GetEnumerator(); + IEnumerator> expectedEnumerator = expected.GetEnumerator(); + + while (enumerator.MoveNext()) + { + if (!expectedEnumerator.MoveNext()) + { + return false; + } + + Measurement measurement = enumerator.Current; + Measurement expectedMeasurement = expectedEnumerator.Current; + + if (measurement.Value.Equals(expectedMeasurement.Value) && + measurement.Tags.ToArray().SequenceEqual(expectedMeasurement.Tags.ToArray())) + { + continue; + } + + return false; + } + + return !expectedEnumerator.MoveNext(); + } } }