Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reapply "track total content length and files indexed (#3318)" (#3346) #3349

Merged
merged 3 commits into from
Feb 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;

namespace Microsoft.Health.Dicom.Core.Features.Common;

/// <summary>
/// Metadata on FileProperty table in database
/// </summary>
public readonly struct IndexedFileProperties : IEquatable<IndexedFileProperties>
{
/// <summary>
/// Total indexed FileProperty in database
/// </summary>
public long TotalIndexed { get; init; }

/// <summary>
/// Total sum of all ContentLength rows in FileProperty table
/// </summary>
public long TotalSum { get; init; }

public override bool Equals(object obj) => obj is IndexedFileProperties other && Equals(other);

public bool Equals(IndexedFileProperties other)
=> TotalIndexed == other.TotalIndexed && TotalSum == other.TotalSum;

public override int GetHashCode()
=> HashCode.Combine(TotalIndexed, TotalSum);

public static bool operator ==(IndexedFileProperties left, IndexedFileProperties right)
=> left.Equals(right);

public static bool operator !=(IndexedFileProperties left, IndexedFileProperties right)
=> !(left == right);
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,4 +185,11 @@ public interface IIndexDataStore
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that with list of instance metadata with new watermark.</returns>
Task UpdateFilePropertiesContentLengthAsync(IReadOnlyDictionary<long, FileProperties> filePropertiesByWatermark, CancellationToken cancellationToken = default);

/// <summary>
/// Retrieves total count in FileProperty table and summation of all content length values across FileProperty table.
/// </summary>
/// <param name="cancellationToken">The cancellation token.</param>
/// <returns>A task that gets the count</returns>
Task<IndexedFileProperties> GetIndexedFileMetricsAsync(CancellationToken cancellationToken = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

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

namespace Microsoft.Health.Dicom.Core.Features.Telemetry;

/// <summary>
/// Since only enumerators are exposed publicly for working with tags or getting the collection of
/// metrics, these extension facilitate getting both.
/// </summary>
public static class MetricPointExtensions
{
/// <summary>
/// Get tags key value pairs from metric point.
/// </summary>
public static Dictionary<string, object> GetTags(this MetricPoint metricPoint)
{
var tags = new Dictionary<string, object>();
foreach (var pair in metricPoint.Tags)
{
tags.Add(pair.Key, pair.Value);
}

return tags;
}

/// <summary>
/// Get all metrics emitted after flushing.
/// </summary>
[SuppressMessage("Performance", "CA1859: Use concrete types when possible for improved performance", Justification = "Result should be read-only.")]
public static IReadOnlyList<MetricPoint> GetMetricPoints(this ICollection<Metric> exportedItems, string metricName)
{
MetricPointsAccessor accessor = exportedItems
.Single(item => item.Name.Equals(metricName, StringComparison.Ordinal))
.GetMetricPoints();
var metrics = new List<MetricPoint>();
foreach (MetricPoint metricPoint in accessor)
{
metrics.Add(metricPoint);
}

return metrics;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@
<PackageReference Include="Microsoft.Health.Operations" />
<PackageReference Include="Microsoft.IO.RecyclableMemoryStream" />
<PackageReference Include="Newtonsoft.Json" />
<PackageReference Include="OpenTelemetry" />
<PackageReference Include="SixLabors.ImageSharp" />
<PackageReference Include="System.Linq.Async" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,29 @@
<ProjectReference Include="..\Microsoft.Health.Dicom.Functions\Microsoft.Health.Dicom.Functions.csproj" />
</ItemGroup>

<!--
The Azure Functions host includes its own assemblies and by default will attempt to prevent additional versions from
being added to its app domain by the function app. However, we can export our own versions of common assemblies by using the build flags
_FunctionsSkipCleanOutput or FunctionsPreservedDependencies. For the list of host dependencies, see here:
https://github.com/Azure/azure-functions-host/blob/dev/tools/ExtensionsMetadataGenerator/test/ExtensionsMetadataGeneratorTests/ExistingRuntimeAssemblies.txt
For the list of assemblies and whether extensions receive their own version by default, see here:
https://github.com/Azure/azure-functions-host/blob/dev/src/WebJobs.Script/runtimeassemblies.json
-->
<ItemGroup>
<FunctionsPreservedDependencies Include="Microsoft.ApplicationInsights.dll" />
<FunctionsPreservedDependencies Include="Microsoft.Azure.WebJobs.Extensions.dll" />
<FunctionsPreservedDependencies Include="Microsoft.IdentityModel.Logging.dll" />
<FunctionsPreservedDependencies Include="Microsoft.IdentityModel.Protocols.dll" />
<FunctionsPreservedDependencies Include="Microsoft.IdentityModel.Protocols.OpenIdConnect.dll" />
<FunctionsPreservedDependencies Include="Microsoft.IdentityModel.Tokens.dll" />
<FunctionsPreservedDependencies Include="System.Diagnostics.DiagnosticSource.dll" />
<FunctionsPreservedDependencies Include="System.IdentityModel.Tokens.Jwt.dll" />
<FunctionsPreservedDependencies Include="System.Memory.Data.dll" />
<FunctionsPreservedDependencies Include="System.Text.Encodings.Web.dll" />
<FunctionsPreservedDependencies Include="System.Text.Json.dll" />
</ItemGroup>


<ItemGroup>
<None Update="host.json">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
Expand Down
3 changes: 3 additions & 0 deletions src/Microsoft.Health.Dicom.Functions.App/host.json
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,9 @@
"FirstRetryInterval": "00:01:00",
"MaxNumberOfAttempts": 4
}
},
"IndexMetricsCollection": {
"Frequency": "0 0 * * *"
}
},
"Extensions": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
"IsEncrypted": false,
"Values": {
"AzureFunctionsJobHost__DicomFunctions__Indexing__MaxParallelBatches": "1",
"AzureFunctionsJobHost__DicomFunctions__IndexMetricsCollection__Frequency": "0 0 * * *",
"AzureFunctionsJobHost__Logging__Console__IsEnabled": "true",
"AzureFunctionsJobHost__SqlServer__ConnectionString": "server=(local);Initial Catalog=Dicom;Integrated Security=true;TrustServerCertificate=true",
"AzureFunctionsJobHost__BlobStore__ConnectionString": "UseDevelopmentStorage=true",
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Threading.Tasks;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging.Abstractions;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Features.Common;
using Microsoft.Health.Dicom.Core.Features.Store;
using Microsoft.Health.Dicom.Functions.MetricsCollection;
using NSubstitute;
using Xunit;

namespace Microsoft.Health.Dicom.Functions.UnitTests.IndexMetricsCollection;

public class IndexMetricsCollectionFunctionTests
{
private readonly IndexMetricsCollectionFunction _collectionFunction;
private readonly IIndexDataStore _indexStore;
private readonly TimerInfo _timer;

public IndexMetricsCollectionFunctionTests()
{
_indexStore = Substitute.For<IIndexDataStore>();
_collectionFunction = new IndexMetricsCollectionFunction(
_indexStore,
Options.Create(new FeatureConfiguration { EnableExternalStore = true, }));
_timer = Substitute.For<TimerInfo>(default, default, default);
}

[Fact]
public async Task GivenIndexMetricsCollectionFunction_WhenRun_CollectionExecutedWhenExternalStoreEnabled()
{
_indexStore.GetIndexedFileMetricsAsync().ReturnsForAnyArgs(new IndexedFileProperties());

await _collectionFunction.Run(_timer, NullLogger.Instance);

await _indexStore.ReceivedWithAnyArgs(1).GetIndexedFileMetricsAsync();
}

[Fact]
public async Task GivenIndexMetricsCollectionFunction_WhenRun_CollectionNotExecutedWhenExternalStoreNotEnabled()
{
_indexStore.GetIndexedFileMetricsAsync().ReturnsForAnyArgs(new IndexedFileProperties());
var collectionFunctionWihtoutExternalStore = new IndexMetricsCollectionFunction(
_indexStore,
Options.Create(new FeatureConfiguration { EnableExternalStore = false, }));

await collectionFunctionWihtoutExternalStore.Run(_timer, NullLogger.Instance);

await _indexStore.DidNotReceiveWithAnyArgs().GetIndexedFileMetricsAsync();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Azure.WebJobs;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Features.Common;
using Microsoft.Health.Dicom.Core.Features.Store;
using Microsoft.Health.Functions.Extensions;

namespace Microsoft.Health.Dicom.Functions.MetricsCollection;

/// <summary>
/// A function for collecting index metrics
/// </summary>
public class IndexMetricsCollectionFunction
{
private readonly IIndexDataStore _indexDataStore;
private readonly bool _externalStoreEnabled;
private readonly bool _enableDataPartitions;
private const string RunFrequencyVariable = $"%{AzureFunctionsJobHost.RootSectionName}:DicomFunctions:{IndexMetricsCollectionOptions.SectionName}:{nameof(IndexMetricsCollectionOptions.Frequency)}%";

public IndexMetricsCollectionFunction(
IIndexDataStore indexDataStore,
IOptions<FeatureConfiguration> featureConfiguration)
{
EnsureArg.IsNotNull(featureConfiguration, nameof(featureConfiguration));
_indexDataStore = EnsureArg.IsNotNull(indexDataStore, nameof(indexDataStore));
_externalStoreEnabled = featureConfiguration.Value.EnableExternalStore;
_enableDataPartitions = featureConfiguration.Value.EnableDataPartitions;
}

/// <summary>
/// Asynchronously collects index metrics.
/// </summary>
/// <param name="invocationTimer">The timer which tracks the invocation schedule.</param>
/// <param name="log">A diagnostic logger.</param>
/// <returns>A task that represents the asynchronous metrics collection operation.</returns>
[FunctionName(nameof(IndexMetricsCollectionFunction))]
public async Task Run(
[TimerTrigger(RunFrequencyVariable)] TimerInfo invocationTimer,
ILogger log)
{
EnsureArg.IsNotNull(invocationTimer, nameof(invocationTimer));
EnsureArg.IsNotNull(log, nameof(log));
if (!_externalStoreEnabled)
{
log.LogInformation("External store is not enabled. Skipping index metrics collection.");
return;
}

if (invocationTimer.IsPastDue)
{
log.LogWarning("Current function invocation is running late.");
}
IndexedFileProperties indexedFileProperties = await _indexDataStore.GetIndexedFileMetricsAsync();

log.LogInformation(
"DICOM telemetry - TotalFilesIndexed: {TotalFilesIndexed} , TotalByesIndexed: {TotalContentLengthIndexed} , with ExternalStoreEnabled: {ExternalStoreEnabled} and DataPartitionsEnabled: {PartitionsEnabled}",
indexedFileProperties.TotalIndexed,
indexedFileProperties.TotalSum,
_externalStoreEnabled,
_enableDataPartitions);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System.ComponentModel.DataAnnotations;

namespace Microsoft.Health.Dicom.Functions.MetricsCollection;

/// <summary>
/// Options on collecting indexing metrics
/// </summary>
public class IndexMetricsCollectionOptions
{
/// <summary>
/// The default section name for <see cref="IndexMetricsCollectionOptions"/> in a configuration.
/// </summary>
public const string SectionName = "IndexMetricsCollection";

/// <summary>
/// Gets or sets the cron expression that indicates how frequently to run the index metrics collection function.
/// </summary>
/// <value>A value cron expression</value>
[Required]
public string Frequency { get; set; }
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@
using Microsoft.Health.Dicom.Functions.DataCleanup;
using Microsoft.Health.Dicom.Functions.Export;
using Microsoft.Health.Dicom.Functions.Indexing;
using Microsoft.Health.Dicom.Functions.MetricsCollection;
using Microsoft.Health.Dicom.Functions.Update;
using Microsoft.Health.Dicom.SqlServer.Registration;
using Microsoft.Health.Extensions.DependencyInjection;
Expand Down Expand Up @@ -67,6 +68,7 @@ public static IDicomFunctionsBuilder ConfigureFunctions(
.AddFunctionsOptions<PurgeHistoryOptions>(configuration, PurgeHistoryOptions.SectionName, isDicomFunction: false)
.AddFunctionsOptions<FeatureConfiguration>(configuration, "DicomServer:Features", isDicomFunction: false)
.AddFunctionsOptions<UpdateOptions>(configuration, UpdateOptions.SectionName)
.AddFunctionsOptions<IndexMetricsCollectionOptions>(configuration, IndexMetricsCollectionOptions.SectionName)
.ConfigureDurableFunctionSerialization()
.AddJsonSerializerOptions(o => o.ConfigureDefaultDicomSettings())
.AddSingleton<UpdateMeter>()
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
SET XACT_ABORT ON

BEGIN TRANSACTION
GO
CREATE OR ALTER PROCEDURE dbo.GetIndexedFileMetrics
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
SELECT TotalIndexedFileCount=COUNT_BIG(*),
TotalIndexedBytes=SUM(ContentLength)
FROM dbo.FileProperty;
END
GO

COMMIT TRANSACTION

IF NOT EXISTS
(
SELECT *
FROM sys.indexes
WHERE NAME = 'IXC_FileProperty_ContentLength'
AND Object_id = OBJECT_ID('dbo.FileProperty')
)
BEGIN
CREATE NONCLUSTERED INDEX IXC_FileProperty_ContentLength ON dbo.FileProperty
(
ContentLength
) WITH (DATA_COMPRESSION = PAGE, ONLINE = ON)
END
GO
Loading
Loading