Skip to content

Commit

Permalink
track telemetry on api requests (#3355)
Browse files Browse the repository at this point in the history
* start tracking min,max,total bytes seen on stow again and # of instances

* add useragent from request header to api request telemetry
  • Loading branch information
esmadau authored Feb 12, 2024
1 parent 58c762c commit 90ea691
Show file tree
Hide file tree
Showing 8 changed files with 167 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

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

internal static class DicomTelemetry
{
public const string ContextItemPrefix = "Dicom_";
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using EnsureThat;
using Microsoft.ApplicationInsights;
using Microsoft.AspNetCore.Http;
using Microsoft.Health.Dicom.Core.Features.Telemetry;

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

internal class HttpDicomTelemetryClient : IDicomTelemetryClient
{
private readonly TelemetryClient _telemetryClient;
private readonly IHttpContextAccessor _httpContextAccessor;

public HttpDicomTelemetryClient(TelemetryClient telemetryClient, IHttpContextAccessor httpContextAccessor)
{
_telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
_httpContextAccessor = EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor));
}

public void TrackMetric(string name, int value)
{
// Note: Context Items are prefixed so that the telemetry initializer knows which items to include in the telemetry
_httpContextAccessor.HttpContext.Items[DicomTelemetry.ContextItemPrefix + name] = value;
_telemetryClient.GetMetric(name).TrackValue(value);
}

public void TrackMetric(string name, long value)
{
// Note: Context Items are prefixed so that the telemetry initializer knows which items to include in the telemetry
_httpContextAccessor.HttpContext.Items[DicomTelemetry.ContextItemPrefix + name] = value;
_telemetryClient.GetMetric(name).TrackValue(value);
}
}
51 changes: 48 additions & 3 deletions src/Microsoft.Health.Dicom.Api/Logging/TelemetryInitializer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,18 @@
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using System;
using System.Collections.Generic;
using System.Linq;
using EnsureThat;
using Microsoft.ApplicationInsights.Channel;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc.Versioning;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Api.Features.Telemetry;
using Microsoft.Health.Dicom.Core.Configs;

namespace Microsoft.Health.Dicom.Api.Logging;

Expand All @@ -18,19 +24,32 @@ namespace Microsoft.Health.Dicom.Api.Logging;
internal class TelemetryInitializer : ITelemetryInitializer
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly bool _enableDataPartitions;
private readonly bool _enableExport;
private readonly bool _enableExternalStore;
private const string ApiVersionColumnName = "ApiVersion";
private const string EnableDataPartitions = "EnableDataPartitions";
private const string EnableExport = "EnableExport";
private const string EnableExternalStore = "EnableExternalStore";
private const string UserAgent = "UserAgent";

public TelemetryInitializer(IHttpContextAccessor httpContextAccessor)
public TelemetryInitializer(IHttpContextAccessor httpContextAccessor, IOptions<FeatureConfiguration> featureConfiguration)
{
_httpContextAccessor = EnsureArg.IsNotNull(httpContextAccessor, nameof(httpContextAccessor));
EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration));
_enableDataPartitions = featureConfiguration.Value.EnableDataPartitions;
_enableExport = featureConfiguration.Value.EnableExport;
_enableExternalStore = featureConfiguration.Value.EnableExternalStore;
}

public void Initialize(ITelemetry telemetry)
{
AddApiVersionColumn(telemetry);
AddMetadataColumns(telemetry);
if (telemetry is RequestTelemetry requestTelemetry)
AddPropertiesFromHttpContextItems(requestTelemetry);
}

private void AddApiVersionColumn(ITelemetry telemetry)
private void AddMetadataColumns(ITelemetry telemetry)
{
var requestTelemetry = telemetry as RequestTelemetry;
if (requestTelemetry == null)
Expand All @@ -52,5 +71,31 @@ private void AddApiVersionColumn(ITelemetry telemetry)
}

requestTelemetry.Properties[ApiVersionColumnName] = version;
requestTelemetry.Properties[EnableDataPartitions] = _enableDataPartitions.ToString();
requestTelemetry.Properties[EnableExport] = _enableExport.ToString();
requestTelemetry.Properties[EnableExternalStore] = _enableExternalStore.ToString();
requestTelemetry.Properties[UserAgent] = _httpContextAccessor.HttpContext?.Request.Headers.UserAgent;
}

private void AddPropertiesFromHttpContextItems(RequestTelemetry requestTelemetry)
{
if (_httpContextAccessor.HttpContext == null)
{
return;
}

IEnumerable<(string Key, string Value)> properties = _httpContextAccessor.HttpContext
.Items
.Select(x => (Key: x.Key.ToString(), x.Value))
.Where(x => x.Key.StartsWith(DicomTelemetry.ContextItemPrefix, StringComparison.Ordinal))
.Select(x => (x.Key[DicomTelemetry.ContextItemPrefix.Length..], x.Value?.ToString()));

foreach ((string key, string value) in properties)
{
if (requestTelemetry.Properties.ContainsKey(key))
requestTelemetry.Properties["DuplicateDimension"] = bool.TrueString;

requestTelemetry.Properties[key] = value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@
using Microsoft.Health.Dicom.Api.Features.Partitioning;
using Microsoft.Health.Dicom.Api.Features.Routing;
using Microsoft.Health.Dicom.Api.Features.Swagger;
using Microsoft.Health.Dicom.Api.Features.Telemetry;
using Microsoft.Health.Dicom.Api.Logging;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Extensions;
using Microsoft.Health.Dicom.Core.Features.Context;
using Microsoft.Health.Dicom.Core.Features.FellowOakDicom;
using Microsoft.Health.Dicom.Core.Features.Routing;
using Microsoft.Health.Dicom.Core.Features.Telemetry;
using Microsoft.Health.Dicom.Core.Registration;
using Microsoft.Health.Encryption.Customer.Configs;
using Microsoft.Health.Encryption.Customer.Extensions;
Expand Down Expand Up @@ -174,6 +176,7 @@ public static IDicomServerBuilder AddDicomServer(
services.AddRecyclableMemoryStreamManager(configurationRoot);

services.AddSingleton<ITelemetryInitializer, TelemetryInitializer>();
services.AddSingleton<IDicomTelemetryClient, HttpDicomTelemetryClient>();

CustomDicomImplementation.SetDicomImplementationClassUIDAndVersion();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ public class DicomStoreServiceTests
TelemetryChannel = Substitute.For<ITelemetryChannel>(),
});

private readonly IDicomTelemetryClient _dicomTelemetryClient = Substitute.For<IDicomTelemetryClient>();
private readonly StoreService _storeService;
private readonly StoreService _storeServiceDropData;

Expand All @@ -94,6 +95,7 @@ public DicomStoreServiceTests()
_storeMeter,
NullLogger<StoreService>.Instance,
Options.Create(new FeatureConfiguration { }),
_dicomTelemetryClient,
_telemetryClient);

_storeServiceDropData = new StoreService(
Expand All @@ -104,6 +106,7 @@ public DicomStoreServiceTests()
_storeMeter,
NullLogger<StoreService>.Instance,
Options.Create(new FeatureConfiguration { }),
_dicomTelemetryClient,
_telemetryClient);

DicomValidationBuilderExtension.SkipValidation(null);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------

using EnsureThat;
using Microsoft.Health.Dicom.Core.Features.Telemetry;

namespace Microsoft.Health.Dicom.Core.Extensions;

internal static class DicomTelemetryClientExtensions
{
public static void TrackInstanceCount(this IDicomTelemetryClient telemetryClient, int count)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
telemetryClient.TrackMetric("InstanceCount", count);
}

public static void TrackTotalInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
telemetryClient.TrackMetric("TotalInstanceBytes", bytes);
}

public static void TrackMinInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
telemetryClient.TrackMetric("MinInstanceBytes", bytes);
}

public static void TrackMaxInstanceBytes(this IDicomTelemetryClient telemetryClient, long bytes)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
telemetryClient.TrackMetric("MaxInstanceBytes", bytes);
}
}
16 changes: 15 additions & 1 deletion src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ public class StoreService : IStoreService
private readonly IStoreDatasetValidator _dicomDatasetValidator;
private readonly IStoreOrchestrator _storeOrchestrator;
private readonly IDicomRequestContextAccessor _dicomRequestContextAccessor;
private readonly IDicomTelemetryClient _dicomTelemetryClient;
private readonly TelemetryClient _telemetryClient;
private readonly StoreMeter _storeMeter;
private readonly ILogger _logger;
Expand All @@ -80,14 +81,16 @@ public StoreService(
StoreMeter storeMeter,
ILogger<StoreService> logger,
IOptions<FeatureConfiguration> featureConfiguration,
IDicomTelemetryClient dicomTelemetryClient,
TelemetryClient telemetryClient)
{
EnsureArg.IsNotNull(featureConfiguration?.Value, nameof(featureConfiguration));
_storeResponseBuilder = EnsureArg.IsNotNull(storeResponseBuilder, nameof(storeResponseBuilder));
_dicomDatasetValidator = EnsureArg.IsNotNull(dicomDatasetValidator, nameof(dicomDatasetValidator));
_storeOrchestrator = EnsureArg.IsNotNull(storeOrchestrator, nameof(storeOrchestrator));
_dicomRequestContextAccessor = EnsureArg.IsNotNull(dicomRequestContextAccessor, nameof(dicomRequestContextAccessor));
_telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
_dicomTelemetryClient = EnsureArg.IsNotNull(dicomTelemetryClient, nameof(dicomTelemetryClient));
_telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(_telemetryClient));
_storeMeter = EnsureArg.IsNotNull(storeMeter, nameof(storeMeter));
_logger = EnsureArg.IsNotNull(logger, nameof(logger));
}
Expand All @@ -104,6 +107,9 @@ public async Task<StoreResponse> ProcessAsync(
_dicomRequestContextAccessor.RequestContext.PartCount = instanceEntries.Count;
_dicomInstanceEntries = instanceEntries;
_requiredStudyInstanceUid = requiredStudyInstanceUid;
_dicomTelemetryClient.TrackInstanceCount(instanceEntries.Count);

long totalLength = 0, minLength = 0, maxLength = 0;

for (int index = 0; index < instanceEntries.Count; index++)
{
Expand All @@ -113,13 +119,21 @@ public async Task<StoreResponse> ProcessAsync(
if (length != null)
{
long len = length.GetValueOrDefault();
totalLength += len;
minLength = Math.Min(minLength, len);
maxLength = Math.Max(maxLength, len);
// Update Telemetry
_storeMeter.InstanceLength.Record(len);
_dicomRequestContextAccessor.RequestContext.TotalDicomEgressToStorageBytes += len;
}
}
finally
{
// Update Requests Telemetry
_dicomTelemetryClient.TrackTotalInstanceBytes(totalLength);
_dicomTelemetryClient.TrackMinInstanceBytes(minLength);
_dicomTelemetryClient.TrackMaxInstanceBytes(maxLength);

// Fire and forget.
int capturedIndex = index;

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------


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

public interface IDicomTelemetryClient
{
void TrackMetric(string name, int value);

void TrackMetric(string name, long value);
}

0 comments on commit 90ea691

Please sign in to comment.