diff --git a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs b/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs
index 3d3fc27f1d..d61f224bb8 100644
--- a/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs
+++ b/src/Microsoft.Health.Dicom.Blob/Features/Storage/BlobFileStore.cs
@@ -243,7 +243,7 @@ await ExecuteAsync(
fileProperties.Path,
fileProperties.ETag);
- _telemetryClient.ForwardLogTrace(message, partition, fileProperties);
+ _telemetryClient.ForwardLogTrace(message, partition, fileProperties, ApplicationInsights.DataContracts.SeverityLevel.Warning);
_logger.LogInformation(
"Can not delete blob in external store as it has changed or been deleted. File from watermark: '{Version}' and PartitionKey: {PartitionKey}. Dangling SQL Index detected. Will not retry",
diff --git a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs
index dc678fd048..0a87a0a794 100644
--- a/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs
+++ b/src/Microsoft.Health.Dicom.Core.UnitTests/Features/Diagnostic/LogForwarderExtensionsTests.cs
@@ -1,4 +1,4 @@
-// -------------------------------------------------------------------------------------------------
+// -------------------------------------------------------------------------------------------------
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License (MIT). See LICENSE in the repo root for license information.
// -------------------------------------------------------------------------------------------------
@@ -120,14 +120,15 @@ public void GivenClientUsingForwardTelemetry_whenForwardOperationLogTraceWithSiz
var operationId = Guid.NewGuid().ToString();
var input = "input";
var message = "a message";
- telemetryClient.ForwardOperationLogTrace(message, operationId, input);
+ telemetryClient.ForwardOperationLogTrace(message, operationId, input, "update");
Assert.Single(channel.Items);
#pragma warning disable CS0618 // Type or member is obsolete
- Assert.Equal(3, channel.Items[0].Context.Properties.Count);
+ Assert.Equal(4, channel.Items[0].Context.Properties.Count);
Assert.Equal(Boolean.TrueString, channel.Items[0].Context.Properties["forwardLog"]);
Assert.Equal(operationId, channel.Items[0].Context.Properties["dicomAdditionalInformation_operationId"]);
Assert.Equal(input, channel.Items[0].Context.Properties["dicomAdditionalInformation_input"]);
+ Assert.Equal("update", channel.Items[0].Context.Properties["operationName"]);
#pragma warning restore CS0618 // Type or member is obsolete
}
@@ -141,22 +142,24 @@ public void GivenClientUsingForwardTelemetry_whenForwardOperationLogTraceWithSiz
var expectedSecondItemInput = "b".PadRight(32 * 1024); // split occurs at 32 kb
var fullInput = expectedFirstItemInput + expectedSecondItemInput;
var message = "a message";
- telemetryClient.ForwardOperationLogTrace(message, operationId, fullInput);
+ telemetryClient.ForwardOperationLogTrace(message, operationId, fullInput, "update");
Assert.Equal(2, channel.Items.Count);
#pragma warning disable CS0618 // Type or member is obsolete
var firstItem = channel.Items[0];
- Assert.Equal(3, firstItem.Context.Properties.Count);
+ Assert.Equal(4, firstItem.Context.Properties.Count);
Assert.Equal(Boolean.TrueString, firstItem.Context.Properties["forwardLog"]);
Assert.Equal(operationId, firstItem.Context.Properties["dicomAdditionalInformation_operationId"]);
Assert.Equal(expectedFirstItemInput, firstItem.Context.Properties["dicomAdditionalInformation_input"]);
+ Assert.Equal("update", firstItem.Context.Properties["operationName"]);
var secondItem = channel.Items[1];
- Assert.Equal(3, secondItem.Context.Properties.Count);
+ Assert.Equal(4, secondItem.Context.Properties.Count);
Assert.Equal(Boolean.TrueString, secondItem.Context.Properties["forwardLog"]);
Assert.Equal(operationId, secondItem.Context.Properties["dicomAdditionalInformation_operationId"]);
Assert.Equal(expectedSecondItemInput, secondItem.Context.Properties["dicomAdditionalInformation_input"]);
+ Assert.Equal("update", firstItem.Context.Properties["operationName"]);
#pragma warning restore CS0618 // Type or member is obsolete
}
@@ -175,4 +178,4 @@ private static (TelemetryClient, MockTelemetryChannel) CreateTelemetryClientWith
return (new TelemetryClient(configuration), channel);
}
-}
\ No newline at end of file
+}
diff --git a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs b/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs
index d771cf9516..1ca290e86b 100644
--- a/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs
+++ b/src/Microsoft.Health.Dicom.Core/Features/Audit/AuditEventSubType.cs
@@ -59,4 +59,6 @@ public static class AuditEventSubType
public const string BulkImportStore = "bulk-import-store";
public const string UpdateStudy = "update-study";
+
+ public const string UpdateStudyOperation = "update-study-operation";
}
diff --git a/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs b/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs
index 389740bead..7a28646da1 100644
--- a/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs
+++ b/src/Microsoft.Health.Dicom.Core/Features/Diagnostic/LogForwarderExtensions.cs
@@ -21,6 +21,7 @@ internal static class LogForwarderExtensions
private const int MaxShoeboxPropertySize = 32 * 1024;
private const string ForwardLogFlag = "forwardLog";
private const string Prefix = "dicomAdditionalInformation_";
+ private const string OperationName = "operationName";
private const string StudyInstanceUID = $"{Prefix}studyInstanceUID";
private const string SeriesInstanceUID = $"{Prefix}seriesInstanceUID";
private const string SOPInstanceUID = $"{Prefix}sopInstanceUID";
@@ -36,16 +37,18 @@ internal static class LogForwarderExtensions
/// client to use to emit the trace
/// message to set on the trace log
/// identifier to use to set UIDs on log and telemetry properties
+ /// Severity level of the message
public static void ForwardLogTrace(
this TelemetryClient telemetryClient,
string message,
- InstanceIdentifier instanceIdentifier)
+ InstanceIdentifier instanceIdentifier,
+ SeverityLevel severityLevel = SeverityLevel.Information)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
EnsureArg.IsNotNull(message, nameof(message));
EnsureArg.IsNotNull(instanceIdentifier, nameof(instanceIdentifier));
- var telemetry = new TraceTelemetry(message);
+ var telemetry = new TraceTelemetry(message, severityLevel);
telemetry.Properties.Add(StudyInstanceUID, instanceIdentifier.StudyInstanceUid);
telemetry.Properties.Add(SeriesInstanceUID, instanceIdentifier.SeriesInstanceUid);
telemetry.Properties.Add(SOPInstanceUID, instanceIdentifier.SopInstanceUid);
@@ -64,14 +67,16 @@ public static void ForwardLogTrace(
/// NOTE - do not use this if reporting on any specific instance. Only use as high level remarks. Attempt to use identifiers wherever possible
/// client to use to emit the trace
/// message to set on the trace log
+ /// Severity level of the message
public static void ForwardLogTrace(
this TelemetryClient telemetryClient,
- string message)
+ string message,
+ SeverityLevel severityLevel = SeverityLevel.Information)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
EnsureArg.IsNotNull(message, nameof(message));
- var telemetry = new TraceTelemetry(message);
+ var telemetry = new TraceTelemetry(message, severityLevel);
telemetry.Properties.Add(ForwardLogFlag, bool.TrueString);
telemetryClient.TrackTrace(telemetry);
@@ -84,18 +89,20 @@ public static void ForwardLogTrace(
/// message to set on the trace log
/// Partition within which file is residing
/// File properties of file this message is regarding
+ /// Severity level of the message
public static void ForwardLogTrace(
this TelemetryClient telemetryClient,
string message,
Partition partition,
- FileProperties fileProperties)
+ FileProperties fileProperties,
+ SeverityLevel severityLevel = SeverityLevel.Information)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
EnsureArg.IsNotNull(message, nameof(message));
EnsureArg.IsNotNull(partition, nameof(partition));
EnsureArg.IsNotNull(fileProperties, nameof(fileProperties));
- var telemetry = new TraceTelemetry(message);
+ var telemetry = new TraceTelemetry(message, severityLevel);
telemetry.Properties.Add(ForwardLogFlag, bool.TrueString);
telemetry.Properties.Add(FilePropertiesPath, fileProperties.Path);
telemetry.Properties.Add(FilePropertiesETag, fileProperties.ETag);
@@ -109,26 +116,32 @@ public static void ForwardLogTrace(
///
/// Emits a trace log with forwarding flag set for operations and adds the required properties to telemetry.
+ /// For Audit shoebox the operation name are automatically populated from HttpContext. For internal operation, the operation name needs to be passed in.
///
/// client to use to emit the trace
/// message to set on the trace log
/// operation id
/// Input payload to pass to the forward logger
+ /// Operation name of the trace event
+ /// Severity level of the message
public static void ForwardOperationLogTrace(
this TelemetryClient telemetryClient,
string message,
string operationId,
- string input)
+ string input,
+ string operationName,
+ SeverityLevel severityLevel = SeverityLevel.Information)
{
EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
EnsureArg.IsNotNull(message, nameof(message));
+ EnsureArg.IsNotNull(operationId, nameof(operationId));
// Shoebox property size has a limitation of 32 KB which is why the diagnostic log is split into multiple messages
- int startIndex = 0, offset = 0, inputSize = input.Length;
+ int startIndex = 0, inputSize = input.Length;
while (startIndex < inputSize)
{
- offset = Math.Min(MaxShoeboxPropertySize, input.Length - startIndex);
- ForwardOperationLogTraceWithSizeLimit(telemetryClient, message, operationId, input.Substring(startIndex, offset));
+ int offset = Math.Min(MaxShoeboxPropertySize, input.Length - startIndex);
+ ForwardOperationLogTraceWithSizeLimit(telemetryClient, message, operationId, input.Substring(startIndex, offset), operationName, severityLevel);
startIndex += offset;
}
}
@@ -137,12 +150,15 @@ private static void ForwardOperationLogTraceWithSizeLimit(
TelemetryClient telemetryClient,
string message,
string operationId,
- string input)
+ string input,
+ string operationName,
+ SeverityLevel severityLevel)
{
- var telemetry = new TraceTelemetry(message);
+ var telemetry = new TraceTelemetry(message, severityLevel);
telemetry.Properties.Add(InputPayload, input);
telemetry.Properties.Add(OperationId, operationId);
telemetry.Properties.Add(ForwardLogFlag, bool.TrueString);
+ telemetry.Properties.Add(OperationName, operationName);
telemetryClient.TrackTrace(telemetry);
}
diff --git a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
index 78f6cc4c1f..0f1846472c 100644
--- a/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
+++ b/src/Microsoft.Health.Dicom.Core/Features/Store/StoreService.cs
@@ -273,7 +273,8 @@ private void DropInvalidMetadata(StoreValidationResult storeValidatorResult, Dic
_telemetryClient.ForwardLogTrace(
$"{message}. This attribute will not be present when retrieving study, series, or instance metadata resources, nor can it be used in searches." +
" However, it will still be present when retrieving study, series, or instance resources.",
- identifier);
+ identifier,
+ ApplicationInsights.DataContracts.SeverityLevel.Warning);
}
}
}
diff --git a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs
index 4086ff0661..8683d18736 100644
--- a/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs
+++ b/src/Microsoft.Health.Dicom.Core/Features/Update/UpdateInstanceOperationService.cs
@@ -15,6 +15,7 @@
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Exceptions;
using Microsoft.Health.Dicom.Core.Extensions;
+using Microsoft.Health.Dicom.Core.Features.Audit;
using Microsoft.Health.Dicom.Core.Features.Common;
using Microsoft.Health.Dicom.Core.Features.Context;
using Microsoft.Health.Dicom.Core.Features.Diagnostic;
@@ -103,7 +104,7 @@ public async Task QueueUpdateOperationAsync(
var operation = await _client.StartUpdateOperationAsync(operationId, updateSpecification, partition, cancellationToken);
string input = JsonSerializer.Serialize(updateSpecification, _jsonSerializerOptions.Value);
- _telemetryClient.ForwardOperationLogTrace("Dicom update operation started successfully.", operationId.ToString(), input);
+ _telemetryClient.ForwardOperationLogTrace("Dicom update operation started successfully.", operationId.ToString(), input, AuditEventSubType.UpdateStudyOperation);
return new UpdateInstanceResponse(operation);
}
catch (Exception ex)
diff --git a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs b/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs
index c0381541be..b3f1a183b1 100644
--- a/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs
+++ b/src/Microsoft.Health.Dicom.Functions.UnitTests/Update/UpdateDurableFunctionTests.cs
@@ -11,6 +11,7 @@
using Microsoft.ApplicationInsights.Extensibility;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
+using Microsoft.Health.Dicom.Core.Features.Audit;
using Microsoft.Health.Dicom.Core.Features.Common;
using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag;
using Microsoft.Health.Dicom.Core.Features.Retrieve;
@@ -68,6 +69,7 @@ public UpdateDurableFunctionTests()
Substitute.For(),
_updateMeter,
telemetryClient,
+ Substitute.For(),
Options.Create(_jsonSerializerOptions),
Options.Create(new FeatureConfiguration()));
_updateDurableFunctionWithExternalStore = new UpdateDurableFunction(
@@ -80,6 +82,7 @@ public UpdateDurableFunctionTests()
Substitute.For(),
_updateMeter,
telemetryClient,
+ Substitute.For(),
Options.Create(_jsonSerializerOptions),
Options.Create(new FeatureConfiguration { EnableExternalStore = true, }));
InitializeMetricExporter();
diff --git a/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs b/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs
index c04755bbf0..cf55e5f36d 100644
--- a/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs
+++ b/src/Microsoft.Health.Dicom.Functions/Registration/ServiceCollectionExtensions.cs
@@ -14,6 +14,7 @@
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Health.Dicom.Core.Configs;
using Microsoft.Health.Dicom.Core.Extensions;
+using Microsoft.Health.Dicom.Core.Features.Audit;
using Microsoft.Health.Dicom.Core.Features.FellowOakDicom;
using Microsoft.Health.Dicom.Core.Features.Telemetry;
using Microsoft.Health.Dicom.Core.Modules;
@@ -66,7 +67,8 @@ public static IDicomFunctionsBuilder ConfigureFunctions(
.AddFunctionsOptions(configuration, UpdateOptions.SectionName)
.ConfigureDurableFunctionSerialization()
.AddJsonSerializerOptions(o => o.ConfigureDefaultDicomSettings())
- .AddSingleton());
+ .AddSingleton()
+ .AddSingleton());
}
///
diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs
index 72616ef278..5bce1186f2 100644
--- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs
+++ b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.Orchestration.cs
@@ -6,13 +6,17 @@
using System;
using System.Collections.Generic;
using System.Data;
+using System.Diagnostics;
using System.Linq;
+using System.Net;
using System.Text.Json;
using System.Threading.Tasks;
using EnsureThat;
using Microsoft.Azure.WebJobs;
using Microsoft.Azure.WebJobs.Extensions.DurableTask;
using Microsoft.Extensions.Logging;
+using Microsoft.Health.Core.Features.Audit;
+using Microsoft.Health.Dicom.Core.Features.Audit;
using Microsoft.Health.Dicom.Core.Features.Diagnostic;
using Microsoft.Health.Dicom.Core.Features.Model;
using Microsoft.Health.Dicom.Core.Features.Partitioning;
@@ -50,6 +54,16 @@ public async Task UpdateInstancesV5Async(
UpdateCheckpoint input = context.GetInput();
input.Partition ??= new Partition(input.PartitionKey, Partition.UnknownName);
+ _auditLogger.LogAudit(
+ AuditAction.Executing,
+ AuditEventSubType.UpdateStudyOperation,
+ null,
+ null,
+ Activity.Current?.RootId,
+ null,
+ null,
+ null);
+
if (input.NumberOfStudyCompleted < input.TotalNumberOfStudies)
{
string studyInstanceUid = input.StudyInstanceUids[input.NumberOfStudyCompleted];
@@ -156,7 +170,22 @@ await context.CallActivityWithRetryAsync(
input.NumberOfStudyFailed,
input.TotalNumberOfInstanceUpdated);
- _telemetryClient.ForwardOperationLogTrace("Update operation completed with errors", context.InstanceId, serializedInput);
+ _telemetryClient.ForwardOperationLogTrace(
+ "Update operation completed with errors",
+ context.InstanceId,
+ serializedInput,
+ AuditEventSubType.UpdateStudyOperation,
+ ApplicationInsights.DataContracts.SeverityLevel.Error);
+
+ _auditLogger.LogAudit(
+ AuditAction.Executed,
+ AuditEventSubType.UpdateStudyOperation,
+ null,
+ HttpStatusCode.BadRequest,
+ Activity.Current?.RootId,
+ null,
+ null,
+ null);
// Throwing the exception so that it can set the operation status to Failed
throw new OperationErrorException("Update operation completed with errors.");
@@ -167,7 +196,17 @@ await context.CallActivityWithRetryAsync(
input.NumberOfStudyCompleted,
input.TotalNumberOfInstanceUpdated);
- _telemetryClient.ForwardOperationLogTrace("Update operation completed successfully", context.InstanceId, serializedInput);
+ _telemetryClient.ForwardOperationLogTrace("Update operation completed successfully", context.InstanceId, serializedInput, AuditEventSubType.UpdateStudyOperation);
+
+ _auditLogger.LogAudit(
+ AuditAction.Executed,
+ AuditEventSubType.UpdateStudyOperation,
+ null,
+ HttpStatusCode.OK,
+ Activity.Current?.RootId,
+ null,
+ null,
+ null);
}
}
}
@@ -201,7 +240,7 @@ private async Task HandleException(
foreach (string error in instanceErrors)
{
- _telemetryClient.ForwardOperationLogTrace(error, context.InstanceId, string.Empty);
+ _telemetryClient.ForwardOperationLogTrace(error, context.InstanceId, string.Empty, AuditEventSubType.UpdateStudyOperation, ApplicationInsights.DataContracts.SeverityLevel.Error);
}
}
diff --git a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs
index 8ca27ad6a9..ea0014f9a9 100644
--- a/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs
+++ b/src/Microsoft.Health.Dicom.Functions/Update/UpdateDurableFunction.cs
@@ -8,6 +8,7 @@
using Microsoft.ApplicationInsights;
using Microsoft.Extensions.Options;
using Microsoft.Health.Dicom.Core.Configs;
+using Microsoft.Health.Dicom.Core.Features.Audit;
using Microsoft.Health.Dicom.Core.Features.Common;
using Microsoft.Health.Dicom.Core.Features.ExtendedQueryTag;
using Microsoft.Health.Dicom.Core.Features.Retrieve;
@@ -31,6 +32,7 @@ public partial class UpdateDurableFunction
private readonly IQueryTagService _queryTagService;
private readonly UpdateMeter _updateMeter;
private readonly TelemetryClient _telemetryClient;
+ private readonly IAuditLogger _auditLogger;
private readonly JsonSerializerOptions _jsonSerializerOptions;
private readonly bool _externalStoreEnabled;
@@ -44,6 +46,7 @@ public UpdateDurableFunction(
IQueryTagService queryTagService,
UpdateMeter updateMeter,
TelemetryClient telemetryClient,
+ IAuditLogger auditLogger,
IOptions jsonSerializerOptions,
IOptions featureConfiguration)
{
@@ -57,6 +60,7 @@ public UpdateDurableFunction(
_jsonSerializerOptions = EnsureArg.IsNotNull(jsonSerializerOptions?.Value, nameof(jsonSerializerOptions));
_updateMeter = EnsureArg.IsNotNull(updateMeter, nameof(updateMeter));
_telemetryClient = EnsureArg.IsNotNull(telemetryClient, nameof(telemetryClient));
+ _auditLogger = EnsureArg.IsNotNull(auditLogger, nameof(auditLogger));
_options = EnsureArg.IsNotNull(configOptions?.Value, nameof(configOptions));
_externalStoreEnabled = featureConfiguration.Value.EnableExternalStore;
}