Skip to content

Commit

Permalink
Customized FHIR service configuration exceptions (#150)
Browse files Browse the repository at this point in the history
* Added FHIR service configuration exceptions.

* Renamed FhirServerError to FhirServiceError for consistency with product name.

* Updated comment.

* Localized exception messages.

* Added unit tests.

* Updated where to catch exception for FHIR service authorization failure.

* Addressed review comment.

* Addressed review comments.

* Updated validation of 'ErrorSource' metric dimension.

* Removed underscores in error names for consistency with other error names and updated redundant string interpolations.

* Nit change to error message when FHIR service doesn't exist.

* Updated unit test as per PR comment.

* Re-throw exception in case it's being retried on.
  • Loading branch information
kyclai authored Jan 22, 2022
1 parent 6fb7b0a commit 6b160d7
Show file tree
Hide file tree
Showing 14 changed files with 743 additions and 31 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ public static class ErrorType
/// <summary>
/// A metric type for errors that occur when interacting with the FHIR server.
/// </summary>
public static string FHIRServerError => nameof(FHIRServerError);
public static string FHIRServiceError => nameof(FHIRServiceError);

/// <summary>
/// A metric type for errors of unknown type (e.g. unhandled exceptions)
Expand Down
27 changes: 15 additions & 12 deletions src/lib/Microsoft.Health.Extensions.Fhir.R4/FhirClientFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
using Microsoft.Health.Common.Auth;
using Microsoft.Health.Common.Telemetry;
using Microsoft.Health.Extensions.Fhir.Config;
using Microsoft.Health.Extensions.Fhir.Telemetry.Exceptions;
using Microsoft.Health.Extensions.Fhir.Telemetry.Metrics;
using Microsoft.Health.Extensions.Host.Auth;
using Microsoft.Health.Logging.Telemetry;
Expand Down Expand Up @@ -64,18 +65,27 @@ public FhirClient Create()

private static FhirClient CreateClient(TokenCredential tokenCredential, ITelemetryLogger logger)
{
EnsureArg.IsNotNull(tokenCredential, nameof(tokenCredential));

var url = Environment.GetEnvironmentVariable("FhirService:Url");
EnsureArg.IsNotNullOrEmpty(url, nameof(url));
var uri = new Uri(url);

EnsureArg.IsNotNull(tokenCredential, nameof(tokenCredential));

var fhirClientSettings = new FhirClientSettings
{
PreferredFormat = ResourceFormat.Json,
};

var client = new FhirClient(url, fhirClientSettings, new BearerTokenAuthorizationMessageHandler(uri, tokenCredential, logger));
FhirClient client = null;
try
{
client = new FhirClient(url, fhirClientSettings, new BearerTokenAuthorizationMessageHandler(uri, tokenCredential, logger));
FhirServiceValidator.ValidateFhirService(client, logger);
}
catch (Exception ex)
{
FhirServiceExceptionProcessor.ProcessException(ex, logger);
}

return client;
}
Expand Down Expand Up @@ -118,15 +128,8 @@ protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage
if (Logger != null && !response.IsSuccessStatusCode)
{
var statusDescription = response.ReasonPhrase.Replace(" ", string.Empty);

if (response.StatusCode == System.Net.HttpStatusCode.TooManyRequests)
{
Logger.LogMetric(FhirClientMetrics.HandledException($"FhirServerError{statusDescription}", ErrorSeverity.Informational, ConnectorOperation.FHIRConversion), 1);
}
else
{
Logger.LogMetric(FhirClientMetrics.HandledException($"FhirServerError{statusDescription}", ErrorSeverity.Critical, ConnectorOperation.FHIRConversion), 1);
}
var severity = response.StatusCode == System.Net.HttpStatusCode.TooManyRequests ? ErrorSeverity.Informational : ErrorSeverity.Critical;
Logger.LogMetric(FhirClientMetrics.HandledException($"{ErrorType.FHIRServiceError}{statusDescription}", severity), 1);
}

return response;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// -------------------------------------------------------------------------------------------------
// 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 EnsureThat;
using Hl7.Fhir.Rest;
using Microsoft.Health.Extensions.Fhir.Telemetry.Exceptions;
using Microsoft.Health.Logging.Telemetry;

namespace Microsoft.Health.Extensions.Fhir
{
public static class FhirServiceValidator
{
public static bool ValidateFhirService(FhirClient client, ITelemetryLogger logger)
{
EnsureArg.IsNotNull(client, nameof(client));

try
{
client.CapabilityStatement(SummaryType.True);
return true;
}
catch (Exception exception)
{
FhirServiceExceptionProcessor.ProcessException(exception, logger);
return false;
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// -------------------------------------------------------------------------------------------------
// 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.Extensions.Fhir.Telemetry.Exceptions
{
public enum FhirServiceErrorCode
{
/// <summary>
/// Error code that categorizes invalid configurations (e.g. invalid FHIR service URL)
/// </summary>
ConfigurationError,

/// <summary>
/// Error code that categorizes authorization errors (e.g. missing role with permission to write FHIR data)
/// </summary>
AuthorizationError,

/// <summary>
/// Error code that categorizes invalid arguments (i.e. exceptions encountered of type ArgumentException), which may occur when FhirClient's endpoint is validated
/// </summary>
ArgumentError,

/// <summary>
/// Error code that categorizes HTTP request exceptions (i.e. exceptions encountered of type HttpRequestException)
/// </summary>
HttpRequestError,

/// <summary>
/// Error code that categorizes MSAL.NET exceptions (i.e. exceptions encountered of type MsalServiceException)
/// </summary>
MsalServiceError,

/// <summary>
/// Error code that categorizes all other generic exceptions
/// </summary>
GeneralError,
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
// -------------------------------------------------------------------------------------------------
// 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.Net;
using System.Net.Http;
using EnsureThat;
using Hl7.Fhir.Rest;
using Microsoft.Health.Common.Telemetry;
using Microsoft.Health.Extensions.Fhir.Resources;
using Microsoft.Health.Extensions.Fhir.Telemetry.Metrics;
using Microsoft.Health.Logging.Telemetry;
using Microsoft.Identity.Client;

namespace Microsoft.Health.Extensions.Fhir.Telemetry.Exceptions
{
public static class FhirServiceExceptionProcessor
{
private static readonly IExceptionTelemetryProcessor _exceptionTelemetryProcessor = new ExceptionTelemetryProcessor();

public static void ProcessException(Exception exception, ITelemetryLogger logger)
{
EnsureArg.IsNotNull(logger, nameof(logger));

var (customException, errorName) = CustomizeException(exception);

logger.LogError(customException);

string exceptionName = customException.Equals(exception) ? $"{ErrorType.FHIRServiceError}{errorName}" : customException.GetType().Name;
_exceptionTelemetryProcessor.LogExceptionMetric(customException, logger, FhirClientMetrics.HandledException(exceptionName, ErrorSeverity.Critical));
}

public static (Exception customException, string errorName) CustomizeException(Exception exception)
{
EnsureArg.IsNotNull(exception, nameof(exception));

string message;
string errorName;

switch (exception)
{
case FhirOperationException _:
var status = ((FhirOperationException)exception).Status;
switch (status)
{
case HttpStatusCode.Forbidden:
message = FhirResources.FhirServiceAccessForbidden;
string helpLink = "https://docs.microsoft.com/azure/healthcare-apis/iot/deploy-iot-connector-in-azure#accessing-the-iot-connector-from-the-fhir-service";
errorName = nameof(FhirServiceErrorCode.AuthorizationError);
return (new UnauthorizedAccessFhirServiceException(message, exception, helpLink, errorName), errorName);
case HttpStatusCode.NotFound:
message = FhirResources.FhirServiceNotFound;
errorName = nameof(FhirServiceErrorCode.ConfigurationError);
return (new InvalidFhirServiceException(message, exception, errorName), errorName);
default:
return (exception, status.ToString());
}

case ArgumentException _:
var paramName = ((ArgumentException)exception).ParamName;
if (paramName.Contains("endpoint", StringComparison.OrdinalIgnoreCase))
{
message = FhirResources.FhirServiceEndpointInvalid;
errorName = nameof(FhirServiceErrorCode.ConfigurationError);
return (new InvalidFhirServiceException(message, exception, errorName), errorName);
}

return (exception, $"{FhirServiceErrorCode.ArgumentError}{paramName}");

case UriFormatException _:
message = FhirResources.FhirServiceUriFormatInvalid;
errorName = nameof(FhirServiceErrorCode.ConfigurationError);
return (new InvalidFhirServiceException(message, exception, errorName), errorName);

case HttpRequestException _:
// TODO: In .NET 5 and later, check HttpRequestException's StatusCode property instead of the Message property
if (exception.Message.Contains(FhirResources.HttpRequestErrorNotKnown, StringComparison.CurrentCultureIgnoreCase))
{
message = FhirResources.FhirServiceHttpRequestError;
errorName = nameof(FhirServiceErrorCode.ConfigurationError);
return (new InvalidFhirServiceException(message, exception, errorName), errorName);
}

return (exception, nameof(FhirServiceErrorCode.HttpRequestError));

case MsalServiceException _:
var errorCode = ((MsalServiceException)exception).ErrorCode;
if (string.Equals(errorCode, "invalid_resource", StringComparison.OrdinalIgnoreCase)
|| string.Equals(errorCode, "invalid_scope", StringComparison.OrdinalIgnoreCase))
{
message = FhirResources.FhirServiceMsalServiceError;
errorName = nameof(FhirServiceErrorCode.ConfigurationError);
return (new InvalidFhirServiceException(message, exception, errorName), errorName);
}

return (exception, $"{FhirServiceErrorCode.MsalServiceError}{errorCode}");

default:
return (exception, nameof(FhirServiceErrorCode.GeneralError));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// -------------------------------------------------------------------------------------------------
// 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 Microsoft.Health.Common.Telemetry;
using Microsoft.Health.Common.Telemetry.Exceptions;

namespace Microsoft.Health.Extensions.Fhir.Telemetry.Exceptions
{
public sealed class InvalidFhirServiceException : IomtTelemetryFormattableException
{
private static readonly string _errorType = ErrorType.FHIRServiceError;

public InvalidFhirServiceException()
{
}

public InvalidFhirServiceException(string message)
: base(message)
{
}

public InvalidFhirServiceException(string message, Exception innerException)
: base(message, innerException)
{
}

public InvalidFhirServiceException(
string message,
Exception innerException,
string errorName)
: base(
message,
innerException,
name: $"{_errorType}{errorName}",
operation: ConnectorOperation.FHIRConversion)
{
}

public override string ErrType => _errorType;

public override string ErrSeverity => ErrorSeverity.Critical;

public override string ErrSource => nameof(ErrorSource.User);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
// -------------------------------------------------------------------------------------------------
// 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 Microsoft.Health.Common.Telemetry;
using Microsoft.Health.Common.Telemetry.Exceptions;

namespace Microsoft.Health.Extensions.Fhir.Telemetry.Exceptions
{
public sealed class UnauthorizedAccessFhirServiceException : IomtTelemetryFormattableException
{
private static readonly string _errorType = ErrorType.FHIRServiceError;

public UnauthorizedAccessFhirServiceException()
{
}

public UnauthorizedAccessFhirServiceException(string message)
: base(message)
{
}

public UnauthorizedAccessFhirServiceException(string message, Exception innerException)
: base(message, innerException)
{
}

public UnauthorizedAccessFhirServiceException(
string message,
Exception innerException,
string helpLink,
string errorName)
: base(
message,
innerException,
name: $"{_errorType}{errorName}",
operation: ConnectorOperation.FHIRConversion)
{
HelpLink = helpLink;
}

public override string ErrType => _errorType;

public override string ErrSeverity => ErrorSeverity.Critical;

public override string ErrSource => nameof(ErrorSource.User);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,9 @@ public class FhirClientMetrics
/// </summary>
/// <param name="exceptionName">The name of the exception</param>
/// <param name="severity">The severity of the error</param>
/// <param name="connectorStage">The stage of the connector</param>
public static Metric HandledException(string exceptionName, string severity, string connectorStage)
public static Metric HandledException(string exceptionName, string severity)
{
return exceptionName.ToErrorMetric(connectorStage, ErrorType.GeneralError, severity);
return exceptionName.ToErrorMetric(ConnectorOperation.FHIRConversion, ErrorType.FHIRServiceError, severity);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,4 +30,17 @@
<ItemGroup>
<ProjectReference Include="..\Microsoft.Health.Common\Microsoft.Health.Common.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="Resources\FhirResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>FhirResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="Resources\FhirResources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>FhirResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>
Loading

0 comments on commit 6b160d7

Please sign in to comment.