diff --git a/eng/build/Engineering.props b/eng/build/Engineering.props index 796658a032..2a216bceab 100644 --- a/eng/build/Engineering.props +++ b/eng/build/Engineering.props @@ -16,7 +16,7 @@ - latest + preview $(EngResourceRoot)key.snk $(RepoRoot)src.ruleset $(NoWarn);NU1701;NU5104 diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs new file mode 100644 index 0000000000..1374b1f0c2 --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckExtensions.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks +{ + internal static class HealthCheckExtensions + { + /// + /// Registers the telemetry health check publisher with the specified additional tags. + /// NOTE: this is currently not safe to call multiple times. + /// + /// The builder to register to. + /// Registers addition copies of the publisher for these tags. + /// The original health check builder, for call chaining. + public static IHealthChecksBuilder AddTelemetryPublisher( + this IHealthChecksBuilder builder, params string[] additionalTags) + { + ArgumentNullException.ThrowIfNull(builder); + + static void RegisterPublisher(IServiceCollection services, string tag) + { + services.AddSingleton(sp => + { + TelemetryHealthCheckPublisherOptions options = new() { Tag = tag }; + return ActivatorUtilities.CreateInstance(sp, options); + }); + } + + builder.Services.AddLogging(); + builder.Services.AddMetrics(); + builder.Services.AddSingleton(); + RegisterPublisher(builder.Services, null); // always register the default publisher + + additionalTags ??= []; + foreach (string tag in additionalTags.Distinct(StringComparer.OrdinalIgnoreCase)) + { + if (!string.IsNullOrEmpty(tag)) + { + RegisterPublisher(builder.Services, tag); + } + } + + return builder; + } + + /// + /// Filters a health report to include only specified entries. + /// + /// The result to filter. + /// The filter predicate to use. + /// The filtered health report. + public static HealthReport Filter(this HealthReport result, Func filter) + { + ArgumentNullException.ThrowIfNull(result); + ArgumentNullException.ThrowIfNull(filter); + + IReadOnlyDictionary newEntries = result.Entries + .Where(x => filter(x.Key, x.Value)) + .ToDictionary(); + + return new HealthReport(newEntries, result.TotalDuration); + } + } +} diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetrics.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetrics.cs new file mode 100644 index 0000000000..f700856645 --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetrics.cs @@ -0,0 +1,50 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Metrics; + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks +{ + /// + /// Contains metrics for health checks. + /// + /// + /// Code taken from: https://github.com/dotnet/extensions/blob/d32357716a5261509bf7527101b21cb6f94a0f89/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/HealthCheckMetrics.cs. + /// + public sealed partial class HealthCheckMetrics + { + public HealthCheckMetrics(IMeterFactory meterFactory) + { + ArgumentNullException.ThrowIfNull(meterFactory); + +#pragma warning disable CA2000 // Dispose objects before losing scope + // We don't dispose the meter because IMeterFactory handles that + // An issue on analyzer side: https://github.com/dotnet/roslyn-analyzers/issues/6912 + // Related documentation: https://github.com/dotnet/docs/pull/37170 + Meter meter = meterFactory.Create("Microsoft.Azure.WebJobs.Script"); +#pragma warning restore CA2000 // Dispose objects before losing scope + + HealthCheckReport = HealthCheckMetricsGeneration.CreateHealthCheckReportHistogram(meter); + UnhealthyHealthCheck = HealthCheckMetricsGeneration.CreateUnhealthyHealthCheckHistogram(meter); + } + + /// + /// Gets the health check report histogram. + /// + public HealthCheckReportHistogram HealthCheckReport { get; } + + /// + /// Gets the unhealthy health check histogram. + /// + public UnhealthyHealthCheckHistogram UnhealthyHealthCheck { get; } + + public static class Constants + { + public const string ReportMetricName = "az.functions.health_check.reports"; + public const string UnhealthyMetricName = "az.functions.health_check.unhealthy_checks"; + public const string HealthCheckTagTag = "az.functions.health_check.tag"; // Yes, tag tag. A metric tag with 'tag' in the name. + public const string HealthCheckNameTag = "az.functions.health_check.name"; + } + } +} diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetricsGeneration.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetricsGeneration.cs new file mode 100644 index 0000000000..841ab33042 --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckMetricsGeneration.cs @@ -0,0 +1,46 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Diagnostics.Metrics; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.Metrics; +using static Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks.HealthCheckMetrics; + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks +{ + /// + /// Health check metrics generation and extensions. + /// + /// + /// Code adapted from: https://github.com/dotnet/extensions/blob/d32357716a5261509bf7527101b21cb6f94a0f89/src/Libraries/Microsoft.Extensions.Diagnostics.HealthChecks.Common/Metric.cs. + /// + public static partial class HealthCheckMetricsGeneration + { + [Histogram(Constants.HealthCheckTagTag, Name = Constants.ReportMetricName)] + public static partial HealthCheckReportHistogram CreateHealthCheckReportHistogram(Meter meter); + + [Histogram( + Constants.HealthCheckNameTag, Constants.HealthCheckTagTag, + Name = Constants.UnhealthyMetricName)] + public static partial UnhealthyHealthCheckHistogram CreateUnhealthyHealthCheckHistogram(Meter meter); + + public static void Record(this HealthCheckReportHistogram histogram, HealthReport report, string tag) + { + ArgumentNullException.ThrowIfNull(report); + histogram.Record(ToMetricValue(report.Status), tag); + } + + public static void Record(this UnhealthyHealthCheckHistogram histogram, string name, HealthReportEntry entry, string tag) + => histogram.Record(ToMetricValue(entry.Status), name, tag); + + private static double ToMetricValue(HealthStatus status) + => status switch + { + HealthStatus.Unhealthy => 0, + HealthStatus.Degraded => 0.5, + HealthStatus.Healthy => 1, + _ => throw new NotSupportedException($"Unexpected HealthStatus value: {status}"), + }; + } +} diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckTags.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckTags.cs new file mode 100644 index 0000000000..0d7df19e4e --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/HealthChecks/HealthCheckTags.cs @@ -0,0 +1,16 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks +{ + internal static class HealthCheckTags + { + private const string Prefix = "az.functions"; + + public const string Liveness = Prefix + ".liveness"; + + public const string Readiness = Prefix + ".readiness"; + + public const string Configuration = Prefix + ".configuration"; + } +} diff --git a/src/WebJobs.Script/Diagnostics/HealthChecks/TelemetryHealthCheckPublisher.cs b/src/WebJobs.Script/Diagnostics/HealthChecks/TelemetryHealthCheckPublisher.cs new file mode 100644 index 0000000000..5022dc2984 --- /dev/null +++ b/src/WebJobs.Script/Diagnostics/HealthChecks/TelemetryHealthCheckPublisher.cs @@ -0,0 +1,124 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Linq; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Azure.WebJobs.Script.Pools; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks +{ + public partial class TelemetryHealthCheckPublisher : IHealthCheckPublisher + { + private readonly HealthCheckMetrics _metrics; + private readonly TelemetryHealthCheckPublisherOptions _options; + private readonly ILogger _logger; + + public TelemetryHealthCheckPublisher( + HealthCheckMetrics metrics, + TelemetryHealthCheckPublisherOptions options, + ILogger logger) + { + _metrics = metrics ?? throw new ArgumentNullException(nameof(metrics)); + _options = options ?? throw new ArgumentNullException(nameof(options)); + _logger = logger ?? throw new ArgumentNullException(nameof(logger)); + } + + /// + /// Gets the tag for this health check publisher. For unit test purposes only. + /// + internal string Tag => _options.Tag; + + public Task PublishAsync(HealthReport report, CancellationToken cancellationToken) + { + ArgumentNullException.ThrowIfNull(report); + + if (_options.Tag is string tag) + { + report = report.Filter(FilterForTag); + } + + if (report.Entries.Count == 0) + { + // No entries to report, so we can skip logging and metrics. + return Task.CompletedTask; + } + + tag = _options.Tag ?? string.Empty; // for logs/metrics later on. + if (report.Status == HealthStatus.Healthy) + { + if (!_options.LogOnlyUnhealthy) + { + Log.Healthy(_logger, tag, report.Status); + } + } + else + { + // Construct string showing list of all health entries status and description for logs + using PoolRental rental = PoolFactory.SharedStringBuilderPool.Rent(); + string separator = string.Empty; + foreach (var entry in report.Entries) + { + if (entry.Value.Status != HealthStatus.Healthy) + { + _metrics.UnhealthyHealthCheck.Record(entry.Key, entry.Value, tag); + } + + rental.Value.Append(separator) + .Append(entry.Key) + .Append(": {") + .Append("status: ") + .Append(entry.Value.Status.ToString()) + .Append(", description: ") + .Append(entry.Value.Description) + .Append('}'); + + separator = ", "; + } + + Log.Unhealthy(_logger, tag, report.Status, rental.Value); + } + + _metrics.HealthCheckReport.Record(report, tag); + return Task.CompletedTask; + } + + private bool FilterForTag(string name, HealthReportEntry entry) + { + return entry.Tags.Contains(_options.Tag); + } + + internal static partial class Log + { + [LoggerMessage(0, LogLevel.Warning, "[Tag='{Tag}'] Process reporting unhealthy: {Status}. Health check entries are {Entries}")] + public static partial void Unhealthy( + ILogger logger, + string tag, + HealthStatus status, + StringBuilder entries); + + [LoggerMessage(1, LogLevel.Debug, "[Tag='{Tag}'] Process reporting healthy: {Status}.")] + public static partial void Healthy( + ILogger logger, + string tag, + HealthStatus status); + } + } + + public class TelemetryHealthCheckPublisherOptions + { + /// + /// Gets or sets a value indicating whether to log for only non-healthy values or not. Default true. + /// + public bool LogOnlyUnhealthy { get; set; } = true; + + /// + /// Gets or sets the tag to filter this health check for. null will perform no filtering. + /// + public string Tag { get; set; } + } +} diff --git a/src/WebJobs.Script/Pools/ObjectPoolExtensions.cs b/src/WebJobs.Script/Pools/ObjectPoolExtensions.cs new file mode 100644 index 0000000000..9bd857b53c --- /dev/null +++ b/src/WebJobs.Script/Pools/ObjectPoolExtensions.cs @@ -0,0 +1,28 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Azure.WebJobs.Script.Pools +{ + internal static class ObjectPoolExtensions + { + /// + /// Rents a from the pool. The object is returned on disposal + /// of the . + /// + /// The object type the pool holds. + /// The pool to rent from. + /// + /// A disposable struct to return the object to the pool. + /// DO NOT dispose multiple times. + /// + public static PoolRental Rent(this ObjectPool pool) + where T : class + { + ArgumentNullException.ThrowIfNull(pool); + return new PoolRental(pool); + } + } +} diff --git a/src/WebJobs.Script/Pools/PoolFactory.cs b/src/WebJobs.Script/Pools/PoolFactory.cs new file mode 100644 index 0000000000..6ed866b8a2 --- /dev/null +++ b/src/WebJobs.Script/Pools/PoolFactory.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Text; +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Azure.WebJobs.Script.Pools; + +/// +/// A factory of object pools. +/// +/// +/// This class makes it easy to create efficient object pools used to improve performance by reducing +/// strain on the garbage collector. +/// +/// Code taken from: https://github.com/dotnet/extensions/blob/d32357716a5261509bf7527101b21cb6f94a0f89/src/Shared/Pools/PoolFactory.cs. +/// +[System.Diagnostics.CodeAnalysis.ExcludeFromCodeCoverage] +internal static class PoolFactory +{ + internal const int DefaultCapacity = 1024; + private const int DefaultMaxStringBuilderCapacity = 64 * 1024; + private const int InitialStringBuilderCapacity = 128; + + private static readonly IPooledObjectPolicy _defaultStringBuilderPolicy = new StringBuilderPooledObjectPolicy + { + InitialCapacity = InitialStringBuilderCapacity, + MaximumRetainedCapacity = DefaultCapacity + }; + + /// + /// Gets the shared pool of instances. + /// + public static ObjectPool SharedStringBuilderPool { get; } = CreateStringBuilderPool(); + + /// + /// Creates a pool of instances. + /// + /// The maximum number of items to keep in the pool. This defaults to 1024. This value is a recommendation, the pool may keep more objects than this. + /// The maximum capacity of the string builders to keep in the pool. This defaults to 64K. + /// The pool. + public static ObjectPool CreateStringBuilderPool(int maxCapacity = DefaultCapacity, int maxStringBuilderCapacity = DefaultMaxStringBuilderCapacity) + { + ArgumentOutOfRangeException.ThrowIfLessThan(maxCapacity, 1); + ArgumentOutOfRangeException.ThrowIfLessThan(maxStringBuilderCapacity, 1); + + if (maxStringBuilderCapacity == DefaultMaxStringBuilderCapacity) + { + return MakePool(_defaultStringBuilderPolicy, maxCapacity); + } + + return MakePool( + new StringBuilderPooledObjectPolicy + { + InitialCapacity = InitialStringBuilderCapacity, + MaximumRetainedCapacity = maxStringBuilderCapacity + }, maxCapacity); + } + + private static DefaultObjectPool MakePool(IPooledObjectPolicy policy, int maxRetained) + where T : class + => new(policy, maxRetained); +} \ No newline at end of file diff --git a/src/WebJobs.Script/Pools/PoolRental.cs b/src/WebJobs.Script/Pools/PoolRental.cs new file mode 100644 index 0000000000..d0a257f1b4 --- /dev/null +++ b/src/WebJobs.Script/Pools/PoolRental.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using Microsoft.Extensions.ObjectPool; + +namespace Microsoft.Azure.WebJobs.Script.Pools +{ + /// + /// Returns a pool rental back to the object pool on disposal. + /// IMPORTANT: do not dispose multiple times. + /// + /// + /// This is a ref struct to try and enforce only inlining in a using statement. + /// + /// The callback to return the object. + /// The object to return. + internal readonly ref struct PoolRental(ObjectPool pool) + where T : class + { + /// + /// Gets the value of this rental. + /// + public T Value { get; } = pool.Get(); + + /// + /// Returns the object back to the pool. + /// + public void Dispose() + { + pool.Return(Value); + } + } +} diff --git a/src/WebJobs.Script/WebJobs.Script.csproj b/src/WebJobs.Script/WebJobs.Script.csproj index 2117fb7ad7..1079554ca3 100644 --- a/src/WebJobs.Script/WebJobs.Script.csproj +++ b/src/WebJobs.Script/WebJobs.Script.csproj @@ -49,6 +49,7 @@ + diff --git a/src/WebJobs.Script/runtimeassemblies.json b/src/WebJobs.Script/runtimeassemblies.json index bdd53b5e22..8bc5f7b933 100644 --- a/src/WebJobs.Script/runtimeassemblies.json +++ b/src/WebJobs.Script/runtimeassemblies.json @@ -71,6 +71,14 @@ "name": "Grpc.Net.Common", "resolutionPolicy": "private" }, + { + "name": "HealthChecks.UI.Client", + "resolutionPolicy": "private" + }, + { + "name": "HealthChecks.UI.Core", + "resolutionPolicy": "private" + }, { "name": "Microsoft.AI.DependencyCollector", "resolutionPolicy": "private" @@ -607,6 +615,10 @@ "name": "Microsoft.Extensions.Caching.Memory", "resolutionPolicy": "minorMatchOrLower" }, + { + "name": "Microsoft.Extensions.Compliance.Abstractions", + "resolutionPolicy": "minorMatchOrLower" + }, { "name": "Microsoft.Extensions.Configuration", "resolutionPolicy": "minorMatchOrLower" @@ -779,6 +791,10 @@ "name": "Microsoft.Extensions.Primitives", "resolutionPolicy": "minorMatchOrLower" }, + { + "name": "Microsoft.Extensions.Telemetry.Abstractions", + "resolutionPolicy": "minorMatchOrLower" + }, { "name": "Microsoft.Extensions.WebEncoders", "resolutionPolicy": "minorMatchOrLower" diff --git a/test/WebJobs.Script.Tests.Shared/TestHelpers.cs b/test/WebJobs.Script.Tests.Shared/TestHelpers.cs index ca98a157e7..94efbcd013 100644 --- a/test/WebJobs.Script.Tests.Shared/TestHelpers.cs +++ b/test/WebJobs.Script.Tests.Shared/TestHelpers.cs @@ -33,7 +33,28 @@ namespace Microsoft.Azure.WebJobs.Script.Tests public static partial class TestHelpers { private const string Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; - private static readonly Random Random = new Random(); + + /// + /// Helper method to inline an action delegate. + /// + /// The action. + /// The provided action. + /// + /// This is intended to be used with a fluent assertion. + /// Act(() => { }).Should().Something();. + /// + public static Action Act(Action act) => act; + + /// + /// Helper method to inline an action delegate. + /// + /// The action. + /// The provided action. + /// + /// This is intended to be used with a fluent assertion. + /// Act(() => { }).Should().Something();. + /// + public static Func Act(Func act) => act; public static Task WaitOneAsync(this WaitHandle waitHandle) { @@ -77,7 +98,7 @@ public static string NewRandomString(int length = 10) { return new string( Enumerable.Repeat('x', length) - .Select(c => Chars[Random.Next(Chars.Length)]) + .Select(c => Chars[Random.Shared.Next(Chars.Length)]) .ToArray()); } @@ -141,23 +162,24 @@ public static async Task WaitForBlobAndGetStringAsync(CloudBlockBlob blo public static async Task WaitForBlobAsync(CloudBlockBlob blob, Func userMessageCallback = null) { - StringBuilder sb = new StringBuilder(); + StringBuilder sb = new(); - await TestHelpers.Await(async () => - { - bool exists = await blob.ExistsAsync(); - sb.AppendLine($"{blob.Name} exists: {exists}."); - return exists; - }, - pollingInterval: 500, - userMessageCallback: () => - { - if (userMessageCallback != null) + await TestHelpers.Await( + async () => { - sb.AppendLine().Append(userMessageCallback()); - } - return sb.ToString(); - }); + bool exists = await blob.ExistsAsync(); + sb.AppendLine($"{blob.Name} exists: {exists}."); + return exists; + }, + pollingInterval: 500, + userMessageCallback: () => + { + if (userMessageCallback != null) + { + sb.AppendLine().Append(userMessageCallback()); + } + return sb.ToString(); + }); } public static void ClearFunctionLogs(string functionName) @@ -187,13 +209,9 @@ public static void WaitForWebHost(HttpClient client) private static bool IsHostRunning(HttpClient client) { - using (HttpRequestMessage request = new HttpRequestMessage(HttpMethod.Get, string.Empty)) - { - using (HttpResponseMessage response = client.SendAsync(request).Result) - { - return response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.OK; - } - } + using HttpRequestMessage request = new(HttpMethod.Get, string.Empty); + using HttpResponseMessage response = client.SendAsync(request).GetAwaiter().GetResult(); + return response.StatusCode == HttpStatusCode.NoContent || response.StatusCode == HttpStatusCode.OK; } public static void ClearHostLogs() diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/HealthCheckExtensionsTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/HealthCheckExtensionsTests.cs new file mode 100644 index 0000000000..c24fe09269 --- /dev/null +++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/HealthCheckExtensionsTests.cs @@ -0,0 +1,164 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Linq; +using AwesomeAssertions; +using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks +{ + public class HealthCheckExtensionsTests + { + public static TheoryData AddTelemetryPublisherData => new() + { + { null, [null] }, + { [null], [null] }, + { [string.Empty], [null] }, + { ["tag1"], [null, "tag1"] }, + { ["tag1", "tag2"], [null, "tag1", "tag2"] }, + }; + + [Fact] + public void Filter_ReturnsFilteredHealthReport() + { + const string tag = "test.tag.1"; + static HealthReportEntry CreateEntry(HealthStatus status, string tag) + { + return tag == null + ? new HealthReportEntry(status, null, TimeSpan.Zero, null, null) + : new HealthReportEntry(status, null, TimeSpan.Zero, null, null, [tag]); + } + + // arrange + Dictionary entries = new() + { + ["test.check.1"] = CreateEntry(HealthStatus.Healthy, null), + ["test.check.2"] = CreateEntry(HealthStatus.Healthy, tag), + ["test.check.3"] = CreateEntry(HealthStatus.Unhealthy, tag), + ["test.check.4"] = CreateEntry(HealthStatus.Healthy, "test.tag.2"), + }; + + HealthReport healthReport = new(entries, TimeSpan.FromSeconds(Random.Shared.Next(0, 10))); + + // act + HealthReport filteredReport = healthReport.Filter((key, entry) => entry.Tags.Contains(tag)); + + // assert + static void Verify(HealthReportEntry actual, HealthStatus status) + { + actual.Status.Should().Be(status); + actual.Data.Should().BeEmpty(); + actual.Description.Should().BeNull(); + actual.Exception.Should().BeNull(); + actual.Duration.Should().Be(TimeSpan.Zero); + actual.Tags.Should().Contain(tag); + } + + filteredReport.Should().NotBeSameAs(healthReport); + filteredReport.TotalDuration.Should().Be(healthReport.TotalDuration); + filteredReport.Status.Should().Be(HealthStatus.Unhealthy); + filteredReport.Entries.Should().HaveCount(2); + filteredReport.Entries.Should().ContainKey("test.check.2") + .WhoseValue.Should().Satisfy( + entry => Verify(entry, HealthStatus.Healthy)); + filteredReport.Entries.Should().ContainKey("test.check.3") + .WhoseValue.Should().Satisfy( + entry => Verify(entry, HealthStatus.Unhealthy)); + } + + [Fact] + public void Filter_NoMatch_ReturnsEmptyHealthReport() + { + const string tag = "test.tag.1"; + static HealthReportEntry CreateEntry(HealthStatus status, string tag) + { + return tag == null + ? new HealthReportEntry(status, null, TimeSpan.Zero, null, null) + : new HealthReportEntry(status, null, TimeSpan.Zero, null, null, [tag]); + } + + // arrange + Dictionary entries = new() + { + ["test.check.1"] = CreateEntry(HealthStatus.Healthy, null), + ["test.check.2"] = CreateEntry(HealthStatus.Healthy, tag), + ["test.check.3"] = CreateEntry(HealthStatus.Unhealthy, tag), + ["test.check.4"] = CreateEntry(HealthStatus.Healthy, tag), + }; + + HealthReport healthReport = new(entries, TimeSpan.FromSeconds(Random.Shared.Next(0, 10))); + + // act + HealthReport filteredReport = healthReport.Filter((key, entry) => entry.Tags.Contains("nonexistant.tag")); + + // assert + filteredReport.Should().NotBeSameAs(healthReport); + filteredReport.TotalDuration.Should().Be(healthReport.TotalDuration); + filteredReport.Status.Should().Be(HealthStatus.Healthy); + filteredReport.Entries.Should().BeEmpty(); + } + + [Fact] + public void AddTelemetryPublisher_ReturnsOriginalBuilder() + { + // arrange + ServiceCollection services = new(); + HealthChecksBuilder builder = new(services); + + // act + IHealthChecksBuilder returned = builder.AddTelemetryPublisher(); + + // assert + returned.Should().BeSameAs(builder); + } + + [Theory] + [MemberData(nameof(AddTelemetryPublisherData))] + public void AddTelemetryPublisher_RegistersExpected(string[] tags, string[] expected) + { + // arrange + ServiceCollection services = new(); + HealthChecksBuilder builder = new(services); + + // act + builder.AddTelemetryPublisher(tags); + + // assert + services.Where(x => x.ServiceType == typeof(IHealthCheckPublisher)).Should().HaveCount(expected.Length) + .And.AllSatisfy(x => x.Lifetime.Should().Be(ServiceLifetime.Singleton)); + + ServiceProvider provider = services.BuildServiceProvider(); + IEnumerable publishers = provider.GetServices(); + + publishers.Should().HaveCount(expected.Length); + foreach (string tag in expected) + { + publishers.Should().ContainSingle(p => VerifyPublisher(p, tag)); + } + } + + private static bool VerifyPublisher(IHealthCheckPublisher publisher, string tag) + { + return publisher is TelemetryHealthCheckPublisher telemetryPublisher + && telemetryPublisher.Tag == tag; + } + + private class HealthChecksBuilder(IServiceCollection services) : IHealthChecksBuilder + { + public IServiceCollection Services { get; } = services; + + public List Registrations { get; } = []; + + public IHealthChecksBuilder Add(HealthCheckRegistration registration) + { + Registrations.Add(registration); + return this; + } + } + } +} diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ScriptHostHealthCheckTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ScriptHostHealthCheckTests.cs index 2a9203271e..2c71b21765 100644 --- a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ScriptHostHealthCheckTests.cs +++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/ScriptHostHealthCheckTests.cs @@ -3,7 +3,7 @@ using System; using System.Threading.Tasks; -using FluentAssertions; +using AwesomeAssertions; using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Moq; diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/TelemetryHealthCheckPublisherTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/TelemetryHealthCheckPublisherTests.cs new file mode 100644 index 0000000000..c41a01ad7e --- /dev/null +++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/TelemetryHealthCheckPublisherTests.cs @@ -0,0 +1,272 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System; +using System.Collections.Generic; +using System.Diagnostics.Metrics; +using System.Threading; +using System.Threading.Tasks; +using AwesomeAssertions; +using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.HealthChecks; +using Microsoft.Extensions.Diagnostics.Metrics.Testing; +using Microsoft.Extensions.Logging; +using Microsoft.Extensions.Logging.Testing; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Diagnostics.HealthChecks +{ + public class TelemetryHealthCheckPublisherTests + { + public static TheoryData, int, string, LogLevel, double> PublishAsyncArgs => new() + { + { + CreateOptions(false, null), + [HealthStatus.Healthy], + 1, + "Process reporting healthy: Healthy.", + LogLevel.Debug, + 1 + }, + { + CreateOptions(true, null), + [HealthStatus.Degraded], + 1, + "Process reporting unhealthy: Degraded. Health check entries are id0: {status: Degraded, description: desc0}", + LogLevel.Warning, + 0.5 + }, + { + CreateOptions(false, null), + [HealthStatus.Unhealthy], + 1, + "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Unhealthy, description: desc0}", + LogLevel.Warning, + 0 + }, + { + CreateOptions(true, null), + [HealthStatus.Healthy, (HealthStatus.Healthy, "some.tag")], + 0, + "Process reporting healthy: Healthy.", + LogLevel.Debug, + 1 + }, + { + CreateOptions(true, null), + [HealthStatus.Healthy, HealthStatus.Unhealthy], + 1, + "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Healthy, description: desc0}, id1: {status: Unhealthy, description: desc1}", + LogLevel.Warning, + 0 + }, + { + CreateOptions(true, null), + [HealthStatus.Healthy, (HealthStatus.Unhealthy, "some.tag")], + 1, + "Process reporting unhealthy: Unhealthy. Health check entries are id0: {status: Healthy, description: desc0}, id1: {status: Unhealthy, description: desc1}", + LogLevel.Warning, + 0 + }, + { + CreateOptions(false, null), + [HealthStatus.Healthy, (HealthStatus.Degraded, "some.tag"), HealthStatus.Unhealthy], + 1, + "Process reporting unhealthy: Unhealthy. Health check entries are " + + "id0: {status: Healthy, description: desc0}, id1: {status: Degraded, description: desc1}, id2: {status: Unhealthy, description: desc2}", + LogLevel.Warning, + 0 + }, + { + CreateOptions(false, "some.tag"), + [(HealthStatus.Healthy, "some.tag"), HealthStatus.Degraded, HealthStatus.Unhealthy, (HealthStatus.Unhealthy, "some.other.tag")], + 1, + "Process reporting healthy: Healthy.", + LogLevel.Debug, + 1 + }, + { + CreateOptions(false, "some.tag"), + [HealthStatus.Healthy, (HealthStatus.Degraded, "some.tag"), HealthStatus.Unhealthy], + 1, + "Process reporting unhealthy: Degraded. Health check entries are " + + "id1: {status: Degraded, description: desc1}", + LogLevel.Warning, + 0.5 + }, + { + CreateOptions(false, "some.tag"), + [(HealthStatus.Healthy, "some.tag"), (HealthStatus.Degraded, "some.tag"), (HealthStatus.Unhealthy, "some.other.tag"), (HealthStatus.Unhealthy, "some.tag")], + 1, + "Process reporting unhealthy: Unhealthy. Health check entries are " + + "id0: {status: Healthy, description: desc0}, id1: {status: Degraded, description: desc1}, id3: {status: Unhealthy, description: desc3}", + LogLevel.Warning, + 0 + } + }; + + [Fact] + public void Ctor_ThrowsWhenOptionsValueNull() + { + using Meter meter = new(nameof(Ctor_ThrowsWhenOptionsValueNull)); + HealthCheckMetrics metrics = GetMockedMetrics(meter); + FakeLogger logger = new(); + + TestHelpers.Act(() => new TelemetryHealthCheckPublisher(metrics, null, logger)) + .Should().Throw().WithParameterName("options"); + } + + [Theory] + [MemberData(nameof(PublishAsyncArgs))] + public async Task Publish_WritesMetricsAndLogs( + TelemetryHealthCheckPublisherOptions options, + IList checks, + int expectedLogCount, + string expectedLogMessage, + LogLevel expectedLogLevel, + double expectedMetricValue) + { + using Meter meter = new(nameof(Publish_WritesMetricsAndLogs)); + HealthCheckMetrics metrics = GetMockedMetrics(meter); + using MetricCollector healthyMetricCollector = new( + meter, HealthCheckMetrics.Constants.ReportMetricName); + using MetricCollector unhealthyMetricCollector = new( + meter, HealthCheckMetrics.Constants.UnhealthyMetricName); + + expectedLogMessage = $"[Tag='{options.Tag}'] {expectedLogMessage}"; + + FakeLogger logger = new(); + FakeLogCollector collector = logger.Collector; + + TelemetryHealthCheckPublisher publisher = new(metrics, options, logger); + + await publisher.PublishAsync(CreateHealthReport(checks), CancellationToken.None); + + collector.Count.Should().Be(expectedLogCount); + if (expectedLogCount > 0) + { + collector.LatestRecord.Message.Should().Be(expectedLogMessage); + collector.LatestRecord.Level.Should().Be(expectedLogLevel); + } + + CollectedMeasurement latest = healthyMetricCollector.LastMeasurement; + + latest.Should().NotBeNull(); + latest.Value.Should().Be(expectedMetricValue); + latest.Tags.Should().ContainKey(HealthCheckMetrics.Constants.HealthCheckTagTag) + .WhoseValue.Should().Be(options.Tag ?? string.Empty); + + IReadOnlyList> unhealthyCounters = unhealthyMetricCollector + .GetMeasurementSnapshot(); + + for (int i = 0; i < checks.Count; i++) + { + CheckDescriptor check = checks[i]; + double? value = GetValue(unhealthyCounters, GetKey(i), options.Tag ?? string.Empty); + if (check.Status == HealthStatus.Healthy) + { + // If the check is healthy, we should not have a value for it + value.Should().BeNull(); + } + else if (options.Tag != null && options.Tag != check.Tag) + { + // If the tag is set and does not match, we should not have a value for this check + value.Should().BeNull(); + } + else + { + // Otherwise, we should have a value for the check + value.Should().Be(ToMetricValue(check.Status)); + } + } + } + + [Fact] + public async Task Publish_NoMatchOnTags_NoTelemetry() + { + using Meter meter = new(nameof(Publish_NoMatchOnTags_NoTelemetry)); + HealthCheckMetrics metrics = GetMockedMetrics(meter); + using MetricCollector healthyMetricCollector = new( + meter, HealthCheckMetrics.Constants.ReportMetricName); + using MetricCollector unhealthyMetricCollector = new( + meter, HealthCheckMetrics.Constants.UnhealthyMetricName); + FakeLogger logger = new(); + + TelemetryHealthCheckPublisherOptions options = CreateOptions(false, "nonexistent.tag"); + TelemetryHealthCheckPublisher publisher = new(metrics, options, logger); + + List checks = [HealthStatus.Healthy, (HealthStatus.Unhealthy, "some.tag")]; + await publisher.PublishAsync(CreateHealthReport(checks), CancellationToken.None); + + logger.Collector.Count.Should().Be(0); + healthyMetricCollector.GetMeasurementSnapshot().Should().BeEmpty(); + unhealthyMetricCollector.GetMeasurementSnapshot().Should().BeEmpty(); + } + + private static TelemetryHealthCheckPublisherOptions CreateOptions(bool logOnlyUnhealthy, string tag) + { + return new TelemetryHealthCheckPublisherOptions + { + LogOnlyUnhealthy = logOnlyUnhealthy, + Tag = tag + }; + } + + private static double? GetValue( + IReadOnlyCollection> counters, string name, string tag) + { + foreach (CollectedMeasurement counter in counters) + { + if (counter.Tags[HealthCheckMetrics.Constants.HealthCheckNameTag]?.ToString() == name && + counter.Tags[HealthCheckMetrics.Constants.HealthCheckTagTag]?.ToString() == tag) + { + return counter.Value; + } + } + + return null; + } + + private static HealthReport CreateHealthReport(IEnumerable checks) + { + Dictionary healthStatusRecords = []; + + int index = 0; + foreach (CheckDescriptor check in checks) + { + IEnumerable tags = check.Tag is null ? [] : [check.Tag]; + HealthReportEntry entry = new(check.Status, $"desc{index}", TimeSpan.Zero, null, null, tags); + healthStatusRecords.Add(GetKey(index), entry); + index++; + } + + return new HealthReport(healthStatusRecords, TimeSpan.Zero); + } + + private static string GetKey(int index) => $"id{index}"; + + private static HealthCheckMetrics GetMockedMetrics(Meter meter) + { + Mock meterFactoryMock = new(); + meterFactoryMock.Setup(x => x.Create(It.IsAny())).Returns(meter); + return new HealthCheckMetrics(meterFactoryMock.Object); + } + + private static double ToMetricValue(HealthStatus status) + => status switch + { + HealthStatus.Unhealthy => 0, + HealthStatus.Degraded => 0.5, + HealthStatus.Healthy => 1, + _ => throw new NotSupportedException($"Unexpected HealthStatus value: {status}"), + }; + + public record CheckDescriptor(HealthStatus Status, string Tag) + { + public static implicit operator CheckDescriptor(HealthStatus status) => new(status, null); + + public static implicit operator CheckDescriptor((HealthStatus Status, string Tag) tuple) => new(tuple.Status, tuple.Tag); + } + } +} diff --git a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/WebHostHealthCheckTests.cs b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/WebHostHealthCheckTests.cs index e374e0f816..f01d0186e0 100644 --- a/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/WebHostHealthCheckTests.cs +++ b/test/WebJobs.Script.Tests/Diagnostics/HealthChecks/WebHostHealthCheckTests.cs @@ -3,7 +3,7 @@ using System.Threading; using System.Threading.Tasks; -using FluentAssertions; +using AwesomeAssertions; using Microsoft.Azure.WebJobs.Script.Diagnostics.HealthChecks; using Microsoft.Extensions.Diagnostics.HealthChecks; using Microsoft.Extensions.Hosting; diff --git a/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs b/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs index 101375605b..34e4ee996e 100644 --- a/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs +++ b/test/WebJobs.Script.Tests/Diagnostics/OpenTelemetry/OpenTelemetryConfigurationExtensionsTests.cs @@ -6,9 +6,9 @@ using System.Diagnostics; using System.Linq; using System.Reflection; +using AwesomeAssertions; using Azure.Identity; using Azure.Monitor.OpenTelemetry.Exporter; -using FluentAssertions; using Microsoft.ApplicationInsights; using Microsoft.ApplicationInsights.Extensibility; using Microsoft.Azure.WebJobs.Script.Configuration; diff --git a/test/WebJobs.Script.Tests/Microsoft.Azure.WebJobs.Script.WebHost.deps.json b/test/WebJobs.Script.Tests/Microsoft.Azure.WebJobs.Script.WebHost.deps.json index 3403280b35..d6d9948ac2 100644 --- a/test/WebJobs.Script.Tests/Microsoft.Azure.WebJobs.Script.WebHost.deps.json +++ b/test/WebJobs.Script.Tests/Microsoft.Azure.WebJobs.Script.WebHost.deps.json @@ -6,7 +6,7 @@ "compilationOptions": {}, "targets": { ".NETCoreApp,Version=v8.0": { - "Microsoft.Azure.WebJobs.Script.WebHost/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script.WebHost/4.1042.100-pr.25364.3": { "dependencies": { "Azure.Data.Tables": "12.8.3", "Azure.Identity": "1.11.4", @@ -22,18 +22,18 @@ "Microsoft.Azure.AppService.Middleware.Functions": "1.5.5", "Microsoft.Azure.AppService.Proxy.Client": "2.3.20240307.67", "Microsoft.Azure.Functions.DotNetIsolatedNativeHost": "1.0.12", - "Microsoft.Azure.Functions.JavaWorker": "2.19.0", + "Microsoft.Azure.Functions.JavaWorker": "2.19.2", "Microsoft.Azure.Functions.NodeJsWorker": "3.10.1", "Microsoft.Azure.Functions.Platform.Metrics.LinuxConsumption": "1.0.5", "Microsoft.Azure.Functions.PowerShellWorker.PS7.0": "4.0.3148", "Microsoft.Azure.Functions.PowerShellWorker.PS7.2": "4.0.4025", "Microsoft.Azure.Functions.PowerShellWorker.PS7.4": "4.0.4206", - "Microsoft.Azure.Functions.PythonWorker": "4.37.0", + "Microsoft.Azure.Functions.PythonWorker": "4.38.0", "Microsoft.Azure.Storage.File": "11.1.7", "Microsoft.Azure.WebJobs": "3.0.41", "Microsoft.Azure.WebJobs.Host.Storage": "5.0.1", - "Microsoft.Azure.WebJobs.Script": "4.1040.100-pr.25225.14", - "Microsoft.Azure.WebJobs.Script.Grpc": "4.1040.100-pr.25225.14", + "Microsoft.Azure.WebJobs.Script": "4.1042.100-pr.25364.3", + "Microsoft.Azure.WebJobs.Script.Grpc": "4.1042.100-pr.25364.3", "Microsoft.Azure.WebSites.DataProtection": "2.1.91-alpha", "Microsoft.IdentityModel.Protocols.OpenIdConnect": "6.35.0", "Microsoft.IdentityModel.Tokens": "6.35.0", @@ -44,8 +44,8 @@ "System.IdentityModel.Tokens.Jwt": "6.35.0", "System.Memory.Data": "8.0.1", "System.Net.NameResolution": "4.3.0", - "Microsoft.Azure.WebJobs.Script.Reference": "4.1040.0.0", - "Microsoft.Azure.WebJobs.Script.Grpc.Reference": "4.1040.0.0" + "Microsoft.Azure.WebJobs.Script.Reference": "4.1042.0.0", + "Microsoft.Azure.WebJobs.Script.Grpc.Reference": "4.1042.0.0" }, "runtime": { "Microsoft.Azure.WebJobs.Script.WebHost.dll": {} @@ -237,7 +237,7 @@ "Grpc.Net.Client/2.55.0": { "dependencies": { "Grpc.Net.Common": "2.55.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" }, "runtime": { "lib/net7.0/Grpc.Net.Client.dll": { @@ -341,7 +341,7 @@ "Microsoft.ApplicationInsights.SnapshotCollector/1.4.4": { "dependencies": { "Microsoft.ApplicationInsights": "2.22.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options": "8.0.2", "System.IO.FileSystem.AccessControl": "5.0.0" }, "runtime": { @@ -396,14 +396,14 @@ "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.AspNetCore.Http.Extensions": "2.2.0", "Microsoft.AspNetCore.WebUtilities": "2.2.0", - "Microsoft.Extensions.ObjectPool": "2.2.0" + "Microsoft.Extensions.ObjectPool": "8.0.10" } }, "Microsoft.AspNetCore.Authentication.Abstractions/2.2.0": { "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.AspNetCore.Authentication.Core/2.2.0": { @@ -426,8 +426,8 @@ }, "Microsoft.AspNetCore.Authorization/2.2.0": { "dependencies": { - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.AspNetCore.Authorization.Policy/2.2.0": { @@ -440,9 +440,9 @@ "dependencies": { "Microsoft.AspNetCore.Http.Extensions": "2.2.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.AspNetCore.Cryptography.Internal/2.2.0": {}, @@ -451,9 +451,9 @@ "Microsoft.AspNetCore.Cryptography.Internal": "2.2.0", "Microsoft.AspNetCore.DataProtection.Abstractions": "2.2.0", "Microsoft.AspNetCore.Hosting.Abstractions": "2.2.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "Microsoft.Win32.Registry": "4.5.0", "System.Security.Cryptography.Xml": "4.7.1", "System.Security.Principal.Windows": "5.0.0" @@ -473,7 +473,7 @@ "Microsoft.Extensions.FileProviders.Physical": "3.1.0", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options": "8.0.2", "System.Diagnostics.DiagnosticSource": "8.0.0", "System.Reflection.Metadata": "1.6.0" } @@ -500,8 +500,8 @@ "dependencies": { "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.AspNetCore.WebUtilities": "2.2.0", - "Microsoft.Extensions.ObjectPool": "2.2.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.ObjectPool": "8.0.10", + "Microsoft.Extensions.Options": "8.0.2", "Microsoft.Net.Http.Headers": "2.2.0" } }, @@ -540,8 +540,8 @@ "dependencies": { "Microsoft.AspNetCore.Http.Extensions": "2.2.0", "Microsoft.Extensions.Localization.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.AspNetCore.Mvc/2.2.0": { @@ -587,7 +587,7 @@ "Microsoft.Extensions.DependencyInjection": "8.0.0", "Microsoft.Extensions.DependencyModel": "2.1.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "System.Diagnostics.DiagnosticSource": "8.0.0", "System.Threading.Tasks.Extensions": "4.5.4" } @@ -725,9 +725,9 @@ "dependencies": { "Microsoft.AspNetCore.Http.Extensions": "2.2.0", "Microsoft.AspNetCore.Routing.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.ObjectPool": "2.2.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.ObjectPool": "8.0.10", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.AspNetCore.Routing.Abstractions/2.2.0": { @@ -773,7 +773,7 @@ "Microsoft.Extensions.Caching.Memory": "5.0.0", "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "Microsoft.IdentityModel.Validators": "6.32.0", "Newtonsoft.Json": "13.0.3", "System.IdentityModel.Tokens.Jwt": "6.35.0", @@ -823,7 +823,7 @@ "Microsoft.AspNetCore.Http": "2.2.2", "Microsoft.AspNetCore.Mvc": "2.2.0", "Microsoft.AspNetCore.Razor.Language": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "Newtonsoft.Json": "13.0.3", "System.ComponentModel.Annotations": "4.5.0", "System.Configuration.ConfigurationManager": "6.0.0", @@ -851,7 +851,7 @@ } }, "Microsoft.Azure.Functions.DotNetIsolatedNativeHost/1.0.12": {}, - "Microsoft.Azure.Functions.JavaWorker/2.19.0": {}, + "Microsoft.Azure.Functions.JavaWorker/2.19.2": {}, "Microsoft.Azure.Functions.NodeJsWorker/3.10.1": {}, "Microsoft.Azure.Functions.Platform.Metrics.LinuxConsumption/1.0.5": { "dependencies": { @@ -868,7 +868,7 @@ "Microsoft.Azure.Functions.PowerShellWorker.PS7.0/4.0.3148": {}, "Microsoft.Azure.Functions.PowerShellWorker.PS7.2/4.0.4025": {}, "Microsoft.Azure.Functions.PowerShellWorker.PS7.4/4.0.4206": {}, - "Microsoft.Azure.Functions.PythonWorker/4.37.0": {}, + "Microsoft.Azure.Functions.PythonWorker/4.38.0": {}, "Microsoft.Azure.KeyVault.Core/2.0.4": { "dependencies": { "System.Runtime": "4.3.1", @@ -915,7 +915,7 @@ "Microsoft.Extensions.Configuration.Json": "3.1.0", "Microsoft.Extensions.Hosting": "2.1.0", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "Microsoft.Extensions.Logging.Configuration": "8.0.0", "Newtonsoft.Json": "13.0.3", "System.Memory.Data": "8.0.1", @@ -1008,7 +1008,7 @@ "Microsoft.AspNetCore.Http.Abstractions": "2.2.0", "Microsoft.AspNetCore.Http.Extensions": "2.2.0", "Microsoft.Azure.WebJobs": "3.0.41", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "System.Diagnostics.TraceSource": "4.3.0", "System.Threading": "4.3.0", "System.Threading.Thread": "4.3.0" @@ -1044,7 +1044,7 @@ "runtime": { "lib/netstandard2.0/Microsoft.Azure.WebJobs.Script.Abstractions.dll": { "assemblyVersion": "1.0.0.0", - "fileVersion": "1.0.21962.0" + "fileVersion": "1.0.22000.0" } } }, @@ -1325,9 +1325,9 @@ "Azure.Identity": "1.11.4", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" }, "runtime": { "lib/netstandard2.0/Microsoft.Extensions.Azure.dll": { @@ -1344,12 +1344,24 @@ "Microsoft.Extensions.Caching.Memory/5.0.0": { "dependencies": { "Microsoft.Extensions.Caching.Abstractions": "5.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "Microsoft.Extensions.Primitives": "8.0.0" } }, + "Microsoft.Extensions.Compliance.Abstractions/8.10.0": { + "dependencies": { + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.ObjectPool": "8.0.10" + }, + "runtime": { + "lib/net8.0/Microsoft.Extensions.Compliance.Abstractions.dll": { + "assemblyVersion": "8.10.0.0", + "fileVersion": "8.1000.24.50207" + } + } + }, "Microsoft.Extensions.Configuration/8.0.0": { "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", @@ -1391,10 +1403,17 @@ }, "Microsoft.Extensions.DependencyInjection/8.0.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" + } + }, + "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": { + "runtime": { + "lib/net8.0/Microsoft.Extensions.DependencyInjection.Abstractions.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46610" + } } }, - "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": {}, "Microsoft.Extensions.DependencyModel/2.1.0": { "dependencies": { "Microsoft.DotNet.PlatformAbstractions": "2.1.0", @@ -1412,8 +1431,8 @@ }, "Microsoft.Extensions.Diagnostics.Abstractions/8.0.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "System.Diagnostics.DiagnosticSource": "8.0.0" } }, @@ -1446,38 +1465,44 @@ "Microsoft.Extensions.Hosting.Abstractions/8.0.0": { "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Diagnostics.Abstractions": "8.0.0", "Microsoft.Extensions.FileProviders.Abstractions": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2" } }, "Microsoft.Extensions.Http/3.0.3": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.Extensions.Localization/2.2.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Localization.Abstractions": "2.2.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, "Microsoft.Extensions.Localization.Abstractions/2.2.0": {}, "Microsoft.Extensions.Logging/8.0.0": { "dependencies": { "Microsoft.Extensions.DependencyInjection": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0" + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2" } }, - "Microsoft.Extensions.Logging.Abstractions/8.0.0": { + "Microsoft.Extensions.Logging.Abstractions/8.0.2": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0" + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2" + }, + "runtime": { + "lib/net8.0/Microsoft.Extensions.Logging.Abstractions.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46610" + } } }, "Microsoft.Extensions.Logging.ApplicationInsights/2.22.0": { @@ -1497,44 +1522,71 @@ "Microsoft.Extensions.Configuration": "8.0.0", "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "Microsoft.Extensions.Options.ConfigurationExtensions": "8.0.0" } }, "Microsoft.Extensions.Logging.Console/8.0.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Logging": "8.0.0", - "Microsoft.Extensions.Logging.Abstractions": "8.0.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", "Microsoft.Extensions.Logging.Configuration": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options": "8.0.2", "System.Text.Json": "8.0.5" } }, - "Microsoft.Extensions.ObjectPool/2.2.0": {}, - "Microsoft.Extensions.Options/8.0.0": { + "Microsoft.Extensions.ObjectPool/8.0.10": { + "runtime": { + "lib/net8.0/Microsoft.Extensions.ObjectPool.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.1024.46804" + } + } + }, + "Microsoft.Extensions.Options/8.0.2": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "Microsoft.Extensions.Primitives": "8.0.0" + }, + "runtime": { + "lib/net8.0/Microsoft.Extensions.Options.dll": { + "assemblyVersion": "8.0.0.0", + "fileVersion": "8.0.224.6711" + } } }, "Microsoft.Extensions.Options.ConfigurationExtensions/8.0.0": { "dependencies": { "Microsoft.Extensions.Configuration.Abstractions": "8.0.0", "Microsoft.Extensions.Configuration.Binder": "8.0.1", - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "Microsoft.Extensions.Primitives": "8.0.0" } }, "Microsoft.Extensions.Primitives/8.0.0": {}, + "Microsoft.Extensions.Telemetry.Abstractions/8.10.0": { + "dependencies": { + "Microsoft.Extensions.Compliance.Abstractions": "8.10.0", + "Microsoft.Extensions.Logging.Abstractions": "8.0.2", + "Microsoft.Extensions.ObjectPool": "8.0.10", + "Microsoft.Extensions.Options": "8.0.2" + }, + "runtime": { + "lib/net8.0/Microsoft.Extensions.Telemetry.Abstractions.dll": { + "assemblyVersion": "8.10.0.0", + "fileVersion": "8.1000.24.50207" + } + } + }, "Microsoft.Extensions.WebEncoders/2.2.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", + "Microsoft.Extensions.Options": "8.0.2", "System.Text.Encodings.Web": "8.0.0" } }, @@ -1944,7 +1996,7 @@ }, "OpenTelemetry.Api.ProviderBuilderExtensions/1.9.0": { "dependencies": { - "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.0", + "Microsoft.Extensions.DependencyInjection.Abstractions": "8.0.2", "OpenTelemetry.Api": "1.9.0" }, "runtime": { @@ -2007,7 +2059,7 @@ "OpenTelemetry.Instrumentation.Http/1.9.0": { "dependencies": { "Microsoft.Extensions.Configuration": "8.0.0", - "Microsoft.Extensions.Options": "8.0.0", + "Microsoft.Extensions.Options": "8.0.2", "OpenTelemetry.Api.ProviderBuilderExtensions": "1.9.0" }, "runtime": { @@ -2911,7 +2963,7 @@ } } }, - "Microsoft.Azure.WebJobs.Script/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script/4.1042.100-pr.25364.3": { "dependencies": { "Azure.Core": "1.45.0", "Azure.Data.Tables": "12.8.3", @@ -2936,6 +2988,7 @@ "Microsoft.Extensions.Azure": "1.7.1", "Microsoft.Extensions.Hosting.Abstractions": "8.0.0", "Microsoft.Extensions.Logging.Console": "8.0.0", + "Microsoft.Extensions.Telemetry.Abstractions": "8.10.0", "Mono.Posix.NETStandard": "1.0.0", "Newtonsoft.Json": "13.0.3", "NuGet.ProjectModel": "5.11.6", @@ -2958,13 +3011,14 @@ "System.Text.Encodings.Web": "8.0.0", "System.Text.Json": "8.0.5", "System.Text.RegularExpressions": "4.3.1", - "System.Threading.Channels": "8.0.0" + "System.Threading.Channels": "8.0.0", + "Yarp.ReverseProxy": "2.0.1" }, "runtime": { "Microsoft.Azure.WebJobs.Script.dll": {} } }, - "Microsoft.Azure.WebJobs.Script.Grpc/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script.Grpc/4.1042.100-pr.25364.3": { "dependencies": { "Grpc.AspNetCore": "2.55.0", "Microsoft.ApplicationInsights": "2.22.0", @@ -2973,35 +3027,34 @@ "Microsoft.ApplicationInsights.WindowsServer": "2.22.0", "Microsoft.ApplicationInsights.WindowsServer.TelemetryChannel": "2.22.0", "Microsoft.Azure.WebJobs.Rpc.Core": "3.0.37", - "Microsoft.Azure.WebJobs.Script": "4.1040.100-pr.25225.14", + "Microsoft.Azure.WebJobs.Script": "4.1042.100-pr.25364.3", "System.IO.FileSystem.Primitives": "4.3.0", - "System.Threading.Channels": "8.0.0", - "Yarp.ReverseProxy": "2.0.1" + "System.Threading.Channels": "8.0.0" }, "runtime": { "Microsoft.Azure.WebJobs.Script.Grpc.dll": {} } }, - "Microsoft.Azure.WebJobs.Script.Reference/4.1040.0.0": { + "Microsoft.Azure.WebJobs.Script.Reference/4.1042.0.0": { "runtime": { "Microsoft.Azure.WebJobs.Script.dll": { - "assemblyVersion": "4.1040.0.0", - "fileVersion": "4.1040.100.25225" + "assemblyVersion": "4.1042.0.0", + "fileVersion": "4.1042.100.25364" } } }, - "Microsoft.Azure.WebJobs.Script.Grpc.Reference/4.1040.0.0": { + "Microsoft.Azure.WebJobs.Script.Grpc.Reference/4.1042.0.0": { "runtime": { "Microsoft.Azure.WebJobs.Script.Grpc.dll": { - "assemblyVersion": "4.1040.0.0", - "fileVersion": "4.1040.100.25225" + "assemblyVersion": "4.1042.0.0", + "fileVersion": "4.1042.100.25364" } } } } }, "libraries": { - "Microsoft.Azure.WebJobs.Script.WebHost/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script.WebHost/4.1042.100-pr.25364.3": { "type": "project", "serviceable": false, "sha512": "" @@ -3573,12 +3626,12 @@ "path": "microsoft.azure.functions.dotnetisolatednativehost/1.0.12", "hashPath": "microsoft.azure.functions.dotnetisolatednativehost.1.0.12.nupkg.sha512" }, - "Microsoft.Azure.Functions.JavaWorker/2.19.0": { + "Microsoft.Azure.Functions.JavaWorker/2.19.2": { "type": "package", "serviceable": true, - "sha512": "sha512-sZbpZQQ+4CSmoKCwZTryI1oJ8rvU2TwvA5u4+Mn/VOt5C1wTZLSiRX2w7fCY34LF5PUhJsi++YzQg8+RGt0LdA==", - "path": "microsoft.azure.functions.javaworker/2.19.0", - "hashPath": "microsoft.azure.functions.javaworker.2.19.0.nupkg.sha512" + "sha512": "sha512-wxlvZ8c4rofeOY2d3CXvjQwI+HhGKZ/OmZpPu6iYm78hKsOhishO8TWeDQUISz077vgLQsqUG+a7EW769YBqqw==", + "path": "microsoft.azure.functions.javaworker/2.19.2", + "hashPath": "microsoft.azure.functions.javaworker.2.19.2.nupkg.sha512" }, "Microsoft.Azure.Functions.NodeJsWorker/3.10.1": { "type": "package", @@ -3615,12 +3668,12 @@ "path": "microsoft.azure.functions.powershellworker.ps7.4/4.0.4206", "hashPath": "microsoft.azure.functions.powershellworker.ps7.4.4.0.4206.nupkg.sha512" }, - "Microsoft.Azure.Functions.PythonWorker/4.37.0": { + "Microsoft.Azure.Functions.PythonWorker/4.38.0": { "type": "package", "serviceable": true, - "sha512": "sha512-QWKmhG7xBdDEDBwkXLT7idrKWAtj5w8dDnhB1g3GdFtVXgfJo7EVg14crNf3eBxAn7S7wdDeQj0sKU2HkkarhQ==", - "path": "microsoft.azure.functions.pythonworker/4.37.0", - "hashPath": "microsoft.azure.functions.pythonworker.4.37.0.nupkg.sha512" + "sha512": "sha512-EYInb3chrmhVADbgWxLkD6clfEYY00nqquKfxITuZi1tjZJ6PtflHAVwM4h6FzvIDR9nDL6/2Lhd+/P4uDc+kQ==", + "path": "microsoft.azure.functions.pythonworker/4.38.0", + "hashPath": "microsoft.azure.functions.pythonworker.4.38.0.nupkg.sha512" }, "Microsoft.Azure.KeyVault.Core/2.0.4": { "type": "package", @@ -3702,7 +3755,7 @@ "Microsoft.Azure.WebJobs.Script.Abstractions/1.0.4-preview": { "type": "package", "serviceable": true, - "sha512": "sha512-6yQIbQWV+Js168FJFPu0aIdwaVl6IkaIqZOuq9RT/8QgNFjIiHYE6w/bJjTDWzf4FccVgbYAz9nXWXd5u45OEg==", + "sha512": "sha512-U/aVn/i0P9MdPsca9TrLmTlKuGOJ8okTdifrBeNviwd80Xbv/+dIM/GGReQXaVVtUM5/SlbUHRAhvA9w0yuEyA==", "path": "microsoft.azure.webjobs.script.abstractions/1.0.4-preview", "hashPath": "microsoft.azure.webjobs.script.abstractions.1.0.4-preview.nupkg.sha512" }, @@ -3797,6 +3850,13 @@ "path": "microsoft.extensions.caching.memory/5.0.0", "hashPath": "microsoft.extensions.caching.memory.5.0.0.nupkg.sha512" }, + "Microsoft.Extensions.Compliance.Abstractions/8.10.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-SlVnMqZjYh7409vddtcoQ1N2HEtEQUHbYPuOOJZ/zNSvbJT4x78BBHaWUTKNEfwdg+MEftRVL63BLB6CVdOCRg==", + "path": "microsoft.extensions.compliance.abstractions/8.10.0", + "hashPath": "microsoft.extensions.compliance.abstractions.8.10.0.nupkg.sha512" + }, "Microsoft.Extensions.Configuration/8.0.0": { "type": "package", "serviceable": true, @@ -3846,12 +3906,12 @@ "path": "microsoft.extensions.dependencyinjection/8.0.0", "hashPath": "microsoft.extensions.dependencyinjection.8.0.0.nupkg.sha512" }, - "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.0": { + "Microsoft.Extensions.DependencyInjection.Abstractions/8.0.2": { "type": "package", "serviceable": true, - "sha512": "sha512-cjWrLkJXK0rs4zofsK4bSdg+jhDLTaxrkXu4gS6Y7MAlCvRyNNgwY/lJi5RDlQOnSZweHqoyvgvbdvQsRIW+hg==", - "path": "microsoft.extensions.dependencyinjection.abstractions/8.0.0", - "hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.0.nupkg.sha512" + "sha512": "sha512-3iE7UF7MQkCv1cxzCahz+Y/guQbTqieyxyaWKhrRO91itI9cOKO76OHeQDahqG4MmW5umr3CcCvGmK92lWNlbg==", + "path": "microsoft.extensions.dependencyinjection.abstractions/8.0.2", + "hashPath": "microsoft.extensions.dependencyinjection.abstractions.8.0.2.nupkg.sha512" }, "Microsoft.Extensions.DependencyModel/2.1.0": { "type": "package", @@ -3937,12 +3997,12 @@ "path": "microsoft.extensions.logging/8.0.0", "hashPath": "microsoft.extensions.logging.8.0.0.nupkg.sha512" }, - "Microsoft.Extensions.Logging.Abstractions/8.0.0": { + "Microsoft.Extensions.Logging.Abstractions/8.0.2": { "type": "package", "serviceable": true, - "sha512": "sha512-arDBqTgFCyS0EvRV7O3MZturChstm50OJ0y9bDJvAcmEPJm0FFpFyjU/JLYyStNGGey081DvnQYlncNX5SJJGA==", - "path": "microsoft.extensions.logging.abstractions/8.0.0", - "hashPath": "microsoft.extensions.logging.abstractions.8.0.0.nupkg.sha512" + "sha512": "sha512-nroMDjS7hNBPtkZqVBbSiQaQjWRDxITI8Y7XnDs97rqG3EbzVTNLZQf7bIeUJcaHOV8bca47s1Uxq94+2oGdxA==", + "path": "microsoft.extensions.logging.abstractions/8.0.2", + "hashPath": "microsoft.extensions.logging.abstractions.8.0.2.nupkg.sha512" }, "Microsoft.Extensions.Logging.ApplicationInsights/2.22.0": { "type": "package", @@ -3965,19 +4025,19 @@ "path": "microsoft.extensions.logging.console/8.0.0", "hashPath": "microsoft.extensions.logging.console.8.0.0.nupkg.sha512" }, - "Microsoft.Extensions.ObjectPool/2.2.0": { + "Microsoft.Extensions.ObjectPool/8.0.10": { "type": "package", "serviceable": true, - "sha512": "sha512-gA8H7uQOnM5gb+L0uTNjViHYr+hRDqCdfugheGo/MxQnuHzmhhzCBTIPm19qL1z1Xe0NEMabfcOBGv9QghlZ8g==", - "path": "microsoft.extensions.objectpool/2.2.0", - "hashPath": "microsoft.extensions.objectpool.2.2.0.nupkg.sha512" + "sha512": "sha512-u7gAG7JgxF8VSJUGPSudAcPxOt+ymJKQCSxNRxiuKV+klCQbHljQR75SilpedCTfhPWDhtUwIJpnDVtspr9nMg==", + "path": "microsoft.extensions.objectpool/8.0.10", + "hashPath": "microsoft.extensions.objectpool.8.0.10.nupkg.sha512" }, - "Microsoft.Extensions.Options/8.0.0": { + "Microsoft.Extensions.Options/8.0.2": { "type": "package", "serviceable": true, - "sha512": "sha512-JOVOfqpnqlVLUzINQ2fox8evY2SKLYJ3BV8QDe/Jyp21u1T7r45x/R/5QdteURMR5r01GxeJSBBUOCOyaNXA3g==", - "path": "microsoft.extensions.options/8.0.0", - "hashPath": "microsoft.extensions.options.8.0.0.nupkg.sha512" + "sha512": "sha512-dWGKvhFybsaZpGmzkGCbNNwBD1rVlWzrZKANLW/CcbFJpCEceMCGzT7zZwHOGBCbwM0SzBuceMj5HN1LKV1QqA==", + "path": "microsoft.extensions.options/8.0.2", + "hashPath": "microsoft.extensions.options.8.0.2.nupkg.sha512" }, "Microsoft.Extensions.Options.ConfigurationExtensions/8.0.0": { "type": "package", @@ -3993,6 +4053,13 @@ "path": "microsoft.extensions.primitives/8.0.0", "hashPath": "microsoft.extensions.primitives.8.0.0.nupkg.sha512" }, + "Microsoft.Extensions.Telemetry.Abstractions/8.10.0": { + "type": "package", + "serviceable": true, + "sha512": "sha512-lnXq37FJZHJPbSJNEFoQVndG8cJvRbzeaUE9eoY1i6yfQUACHnEhuMnkZm7zPCkSqn0tylF0fITQn/RuDvt+0g==", + "path": "microsoft.extensions.telemetry.abstractions/8.10.0", + "hashPath": "microsoft.extensions.telemetry.abstractions.8.10.0.nupkg.sha512" + }, "Microsoft.Extensions.WebEncoders/2.2.0": { "type": "package", "serviceable": true, @@ -5022,22 +5089,22 @@ "path": "yarp.reverseproxy/2.0.1", "hashPath": "yarp.reverseproxy.2.0.1.nupkg.sha512" }, - "Microsoft.Azure.WebJobs.Script/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script/4.1042.100-pr.25364.3": { "type": "project", "serviceable": false, "sha512": "" }, - "Microsoft.Azure.WebJobs.Script.Grpc/4.1040.100-pr.25225.14": { + "Microsoft.Azure.WebJobs.Script.Grpc/4.1042.100-pr.25364.3": { "type": "project", "serviceable": false, "sha512": "" }, - "Microsoft.Azure.WebJobs.Script.Reference/4.1040.0.0": { + "Microsoft.Azure.WebJobs.Script.Reference/4.1042.0.0": { "type": "reference", "serviceable": false, "sha512": "" }, - "Microsoft.Azure.WebJobs.Script.Grpc.Reference/4.1040.0.0": { + "Microsoft.Azure.WebJobs.Script.Grpc.Reference/4.1042.0.0": { "type": "reference", "serviceable": false, "sha512": "" diff --git a/test/WebJobs.Script.Tests/Middleware/AppServiceHeaderFixupMiddlewareTests.cs b/test/WebJobs.Script.Tests/Middleware/AppServiceHeaderFixupMiddlewareTests.cs index f8feff6581..9ee0b6c545 100644 --- a/test/WebJobs.Script.Tests/Middleware/AppServiceHeaderFixupMiddlewareTests.cs +++ b/test/WebJobs.Script.Tests/Middleware/AppServiceHeaderFixupMiddlewareTests.cs @@ -2,7 +2,7 @@ // Licensed under the MIT License. See License.txt in the project root for license information. using System.Threading.Tasks; -using FluentAssertions; +using AwesomeAssertions; using Microsoft.AspNetCore.Http; using Microsoft.Azure.WebJobs.Script.WebHost.Middleware; using Microsoft.Extensions.Primitives; diff --git a/test/WebJobs.Script.Tests/Pools/PoolFactoryTests.cs b/test/WebJobs.Script.Tests/Pools/PoolFactoryTests.cs new file mode 100644 index 0000000000..47be90574f --- /dev/null +++ b/test/WebJobs.Script.Tests/Pools/PoolFactoryTests.cs @@ -0,0 +1,44 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using System.Text; +using AwesomeAssertions; +using Microsoft.Azure.WebJobs.Script.Pools; +using Microsoft.Extensions.ObjectPool; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Pools +{ + public class PoolFactoryTests + { + [Fact] + public void SharedStringBuilderPool() + { + // arrange + ObjectPool pool = PoolFactory.SharedStringBuilderPool; + + // act + StringBuilder sb = pool.Get(); + + // assert + sb.Should().NotBeNull(); + pool.Return(sb); + } + + [Fact] + public void StringBuilderPool() + { + // arrange + ObjectPool pool = PoolFactory.CreateStringBuilderPool(123, 2048); + + // act + StringBuilder sb = pool.Get(); + sb.Append('x', 4096); + pool.Return(sb); + StringBuilder sb2 = pool.Get(); + + // assert + sb.Should().NotBeSameAs(sb2); + } + } +} diff --git a/test/WebJobs.Script.Tests/Pools/PoolRentalTests.cs b/test/WebJobs.Script.Tests/Pools/PoolRentalTests.cs new file mode 100644 index 0000000000..a2a5136a67 --- /dev/null +++ b/test/WebJobs.Script.Tests/Pools/PoolRentalTests.cs @@ -0,0 +1,39 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See License.txt in the project root for license information. + +using AwesomeAssertions; +using Microsoft.Azure.WebJobs.Script.Pools; +using Microsoft.Extensions.ObjectPool; +using Moq; +using Xunit; + +namespace Microsoft.Azure.WebJobs.Script.Tests.Pools +{ + public class PoolRentalTests + { + [Fact] + public void Gets_On_Creation_And_Returns_On_Dispose() + { + // arrange + object value = new(); + Mock> pool = new(MockBehavior.Strict); + pool.Setup(m => m.Get()).Returns(value); + pool.Setup(m => m.Return(value)); + + // act pt.1 + PoolRental rental = new(pool.Object); + + // assert pt.1 + rental.Value.Should().BeSameAs(value); + pool.Verify(m => m.Get(), Times.Once); + pool.Verify(m => m.Return(It.IsAny()), Times.Never); + + // act pt.2 + rental.Dispose(); + + // assert pt.2 + pool.Verify(m => m.Get(), Times.Once); + pool.Verify(m => m.Return(It.IsAny()), Times.Once); + } + } +} diff --git a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj index 03f6e9b02f..5d21f73b89 100644 --- a/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj +++ b/test/WebJobs.Script.Tests/WebJobs.Script.Tests.csproj @@ -16,13 +16,14 @@ + + + - - diff --git a/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExistingRuntimeAssemblies.txt b/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExistingRuntimeAssemblies.txt index 7f8faf537c..77f6af7d93 100644 --- a/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExistingRuntimeAssemblies.txt +++ b/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExistingRuntimeAssemblies.txt @@ -98,6 +98,7 @@ Microsoft.CSharp.dll Microsoft.DotNet.PlatformAbstractions.dll Microsoft.Extensions.Caching.Abstractions.dll Microsoft.Extensions.Caching.Memory.dll +Microsoft.Extensions.Compliance.Abstractions.dll Microsoft.Extensions.Configuration.dll Microsoft.Extensions.Configuration.Abstractions.dll Microsoft.Extensions.Configuration.Binder.dll @@ -140,6 +141,7 @@ Microsoft.Extensions.Options.dll Microsoft.Extensions.Options.ConfigurationExtensions.dll Microsoft.Extensions.Options.DataAnnotations.dll Microsoft.Extensions.Primitives.dll +Microsoft.Extensions.Telemetry.Abstractions.dll Microsoft.Extensions.WebEncoders.dll Microsoft.IdentityModel.Logging.dll Microsoft.IdentityModel.Protocols.dll