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

Customized FHIR service configuration exceptions #150

Merged
merged 15 commits into from
Jan 22, 2022
Merged
Show file tree
Hide file tree
Changes from 11 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
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);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So currently if this fails, we just log the exception and return false, so what happens in the rest of the execution flow in that case?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this case (when ValidateFhirService catches an exception, gets the exception logged, and returns false), FhirClient is returned (Line 90), as before these code changes.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sure but I was trying to understand that if this validation failed and we still returned the client, then what happens in the ingestion flow? Do we encounter other exceptions that block the ingestion?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, what happens when an invalid client is returned is something to further investigate later. For now, the previous behaviour is still retained: a client, whether valid or not, gets returned.

}
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)
pallar-ms marked this conversation as resolved.
Show resolved Hide resolved
{
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);
pallar-ms marked this conversation as resolved.
Show resolved Hide resolved
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