//----------------------------------------------------------------------------- // // Copyright 2016 Amazon.com, Inc. or its affiliates. All Rights Reserved. // // Licensed under the Apache License, Version 2.0 (the "License"). // You may not use this file except in compliance with the License. // A copy of the License is located at // // http://aws.amazon.com/apache2.0 // // or in the "license" file accompanying this file. This file is distributed // on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either // express or implied. See the License for the specific language governing // permissions and limitations under the License. // //----------------------------------------------------------------------------- using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Collections; using System.IO; using System.Reflection; using Amazon.Runtime; using Amazon.Runtime.Internal.Util; using Amazon.XRay.Recorder.Core; using Amazon.XRay.Recorder.Core.Internal.Entities; using Amazon.XRay.Recorder.Core.Internal.Utils; using Amazon.XRay.Recorder.Handlers.AwsSdk.Entities; using ThirdParty.LitJson; using Amazon.Runtime.Internal; using System.Threading; using Amazon.Runtime.Internal.Transform; using Amazon.XRay.Recorder.Core.Exceptions; namespace Amazon.XRay.Recorder.Handlers.AwsSdk.Internal { /// /// The handler to register which can intercept downstream requests and responses. /// Note: This class should not be instantiated or used in anyway. It is used internally within SDK. /// public class XRayPipelineHandler : PipelineHandler { private const string DefaultAwsWhitelistManifestResourceName = "Amazon.XRay.Recorder.Handlers.AwsSdk.DefaultAWSWhitelist.json"; private static readonly Logger _logger = Runtime.Internal.Util.Logger.GetLogger(typeof(AWSXRayRecorder)); private AWSXRayRecorder _recorder; /// /// Gets AWS service manifest of operation parameter whitelist. /// public AWSServiceHandlerManifest AWSServiceHandlerManifest { get; private set; } /// /// Initializes a new instance of the class. /// public XRayPipelineHandler() { _recorder = AWSXRayRecorder.Instance; AWSServiceHandlerManifest = GetDefaultAWSWhitelist(); } /// /// Initializes a new instance of the class. /// /// Path to the file which contains the operation parameter whitelist configuration. /// Thrown when recorder is null. public XRayPipelineHandler(string path) { _recorder = AWSXRayRecorder.Instance; if (_recorder == null) { throw new ArgumentNullException("recorder"); } AWSServiceHandlerManifest = GetAWSServiceManifest(path); } /// /// Initializes a new instance of the class. /// /// stream for manifest which contains the operation parameter whitelist configuration. /// Thrown when recorder is null. public XRayPipelineHandler(Stream stream) { _recorder = AWSXRayRecorder.Instance; if (_recorder == null) { throw new ArgumentNullException("recorder"); } AWSServiceHandlerManifest = GetAWSServiceManifest(stream); } /// /// Creates instance of with provided AWS service manifest instance. /// /// Instance of public XRayPipelineHandler(AWSServiceHandlerManifest awsServiceManifest) { _recorder = AWSXRayRecorder.Instance; if (_recorder == null) { throw new ArgumentNullException("recorder"); } AWSServiceHandlerManifest = awsServiceManifest; } /// /// Extracts instance from provided path of AWS Service manifest file. /// /// Absolute path to AWS Service Manifest file /// Instance of public static AWSServiceHandlerManifest GetAWSServiceManifest(string path) { if (string.IsNullOrEmpty(path)) { _logger.DebugFormat("The path is null or empty, initializing with default AWS whitelist."); return GetDefaultAWSWhitelist(); } else { using (Stream stream = new FileStream(path, FileMode.Open, FileAccess.Read)) { return GetAWSServiceHandlerManifest(stream); } } } /// /// Extracts instance from provided aws service manifest stream. /// /// Absolute path to AWS Service Manifest file /// Instance of public static AWSServiceHandlerManifest GetAWSServiceManifest(Stream stream) { if (stream == null) { _logger.DebugFormat("The provided stream is null, initializing with default AWS whitelist."); return GetDefaultAWSWhitelist(); } else { return GetAWSServiceHandlerManifest(stream); } } private static bool TryReadPropertyValue(object obj, string propertyName, out object value) { value = 0; try { if (obj == null || propertyName == null) { return false; } var property = obj.GetType().GetProperty(propertyName); if (property == null) { _logger.DebugFormat("Property doesn't exist. {0}", propertyName); return false; } value = property.GetValue(obj); return true; } catch (ArgumentNullException e) { _logger.Error(e, "Failed to read property because argument is null."); return false; } catch (AmbiguousMatchException e) { _logger.Error(e, "Failed to read property because of duplicate property name."); return false; } } /// /// Removes amazon prefix from service name. There are two type of service name. /// Amazon.DynamoDbV2 /// AmazonS3 /// /// Name of the service. /// String after removing Amazon prefix. private static string RemoveAmazonPrefixFromServiceName(string serviceName) { return RemovePrefix(RemovePrefix(serviceName, "Amazon"), "."); } private static string RemovePrefix(string originalString, string prefix) { if (prefix == null) { throw new ArgumentNullException(nameof(prefix)); } if (originalString == null) { throw new ArgumentNullException(nameof(originalString)); } if (originalString.StartsWith(prefix)) { return originalString.Substring(prefix.Length); } return originalString; } private static string RemoveSuffix(string originalString, string suffix) { if (suffix == null) { throw new ArgumentNullException(nameof(suffix)); } if (originalString == null) { throw new ArgumentNullException(nameof(originalString)); } if (originalString.EndsWith(suffix)) { return originalString.Substring(0, originalString.Length - suffix.Length); } return originalString; } private static void AddMapKeyProperty(IDictionary aws, object obj, string propertyName, string renameTo = null) { if (!TryReadPropertyValue(obj, propertyName, out object propertyValue)) { _logger.DebugFormat("Failed to read property value: {0}", propertyName); return; } var dictionaryValue = propertyValue as IDictionary; if (dictionaryValue == null) { _logger.DebugFormat("Property value does not implements IDictionary: {0}", propertyName); return; } var newPropertyName = string.IsNullOrEmpty(renameTo) ? propertyName : renameTo; aws[newPropertyName.FromCamelCaseToSnakeCase()] = dictionaryValue.Keys; } private static void AddListLengthProperty(IDictionary aws, object obj, string propertyName, string renameTo = null) { if (!TryReadPropertyValue(obj, propertyName, out object propertyValue)) { _logger.DebugFormat("Failed to read property value: {0}", propertyName); return; } var listValue = propertyValue as IList; if (listValue == null) { _logger.DebugFormat("Property value does not implements IList: {0}", propertyName); return; } var newPropertyName = string.IsNullOrEmpty(renameTo) ? propertyName : renameTo; aws[newPropertyName.FromCamelCaseToSnakeCase()] = listValue.Count; } private static AWSServiceHandlerManifest GetDefaultAWSWhitelist() { using (var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(DefaultAwsWhitelistManifestResourceName)) { return GetAWSServiceHandlerManifest(stream); } } private static AWSServiceHandlerManifest GetAWSServiceHandlerManifest(Stream stream) { using (var reader = new StreamReader(stream)) { try { return JsonMapper.ToObject(reader); } catch (JsonException e) { _logger.Error(e, "Failed to load AWSServiceHandlerManifest."); } return null; } } /// /// Processes Begin request by starting subsegment. /// private void ProcessBeginRequest(IExecutionContext executionContext) { var request = executionContext.RequestContext.Request; Entity entity = null; try { entity = _recorder.GetEntity(); } catch (EntityNotAvailableException e) { _recorder.TraceContext.HandleEntityMissing(_recorder, e, "Cannot get entity while processing AWS SDK request"); } var serviceName = RemoveAmazonPrefixFromServiceName(request.ServiceName); _recorder.BeginSubsegment(AWSXRaySDKUtils.FormatServiceName(serviceName)); _recorder.SetNamespace("aws"); entity = entity == null ? null : _recorder.GetEntity(); if (TraceHeader.TryParse(entity, out TraceHeader traceHeader)) { request.Headers[TraceHeader.HeaderKey] = traceHeader.ToString(); } else { _logger.DebugFormat("Failed to inject trace header to AWS SDK request as the segment can't be converted to TraceHeader."); } } /// /// Processes End request by ending subsegment. /// private void ProcessEndRequest(IExecutionContext executionContext) { Entity subsegment; try { subsegment = _recorder.GetEntity(); } catch(EntityNotAvailableException e) { _recorder.TraceContext.HandleEntityMissing(_recorder,e,"Cannot get entity from the trace context while processing response of AWS SDK request."); return; } var responseContext = executionContext.ResponseContext; var requestContext = executionContext.RequestContext; if (responseContext == null) { _logger.DebugFormat("Failed to handle AfterResponseEvent, because response is null."); return; } var client = executionContext.RequestContext.ClientConfig; if (client == null) { _logger.DebugFormat("Failed to handle AfterResponseEvent, because client from the Response Context is null"); return; } var serviceName = RemoveAmazonPrefixFromServiceName(requestContext.Request.ServiceName); var operation = RemoveSuffix(requestContext.OriginalRequest.GetType().Name, "Request"); subsegment.Aws["region"] = client.RegionEndpoint?.SystemName; subsegment.Aws["operation"] = operation; if (responseContext.Response == null) { if (requestContext.Request.Headers.TryGetValue("x-amzn-RequestId", out string requestId)) { subsegment.Aws["request_id"] = requestId; } // s3 doesn't follow request header id convention else { if (requestContext.Request.Headers.TryGetValue("x-amz-request-id", out requestId)) { subsegment.Aws["request_id"] = requestId; } if (requestContext.Request.Headers.TryGetValue("x-amz-id-2", out requestId)) { subsegment.Aws["id_2"] = requestId; } } } else { subsegment.Aws["request_id"] = responseContext.Response.ResponseMetadata.RequestId; // try getting x-amz-id-2 if dealing with s3 request if (responseContext.Response.ResponseMetadata.Metadata.TryGetValue("x-amz-id-2", out string extendedRequestId)) { subsegment.Aws["id_2"] = extendedRequestId; } AddResponseSpecificInformation(serviceName, operation, responseContext.Response, subsegment.Aws); } if (responseContext.HttpResponse != null) { AddHttpInformation(responseContext.HttpResponse); } AddRequestSpecificInformation(serviceName, operation, requestContext.OriginalRequest, subsegment.Aws); _recorder.EndSubsegment(); } private void AddHttpInformation(IWebResponseData httpResponse) { var responseAttributes = new Dictionary(); int statusCode = (int)httpResponse.StatusCode; if (statusCode >= 400 && statusCode <= 499) { _recorder.MarkError(); if (statusCode == 429) { _recorder.MarkThrottle(); } } else if (statusCode >= 500 && statusCode <= 599) { _recorder.MarkFault(); } responseAttributes["status"] = statusCode; responseAttributes["content_length"] = httpResponse.ContentLength; _recorder.AddHttpInformation("response", responseAttributes); } private void ProcessException(AmazonServiceException ex, Entity subsegment) { int statusCode = (int)ex.StatusCode; var responseAttributes = new Dictionary(); if (statusCode >= 400 && statusCode <= 499) { _recorder.MarkError(); if (statusCode == 429) { _recorder.MarkThrottle(); } } else if (statusCode >= 500 && statusCode <= 599) { _recorder.MarkFault(); } responseAttributes["status"] = statusCode; _recorder.AddHttpInformation("response", responseAttributes); subsegment.Aws["request_id"] = ex.RequestId; // AmazonId2 property in AmazonS3Exception corresponds to the x-amz-id-2 Http header var property = ex.GetType().GetProperty("AmazonId2"); if (property != null) { subsegment.Aws["id_2"] = (string)property.GetValue(ex, null); } } private void AddRequestSpecificInformation(string serviceName, string operation, AmazonWebServiceRequest request, IDictionary aws) { if (serviceName == null) { throw new ArgumentNullException(nameof(serviceName)); } if (operation == null) { throw new ArgumentNullException(nameof(operation)); } if (request == null) { throw new ArgumentNullException(nameof(request)); } if (aws == null) { throw new ArgumentNullException(nameof(aws)); } if (AWSServiceHandlerManifest == null) { _logger.DebugFormat("AWSServiceHandlerManifest doesn't exist."); return; } if (!AWSServiceHandlerManifest.Services.TryGetValue(serviceName, out AWSServiceHandler serviceHandler)) { _logger.DebugFormat("Service name doesn't exist in AWSServiceHandlerManifest: serviceName = {0}.", serviceName); return; } if (!serviceHandler.Operations.TryGetValue(operation, out AWSOperationHandler operationHandler)) { _logger.DebugFormat("Operation doesn't exist in AwsServiceInfo: serviceName = {0}, operation = {1}.", serviceName, operation); return; } if (operationHandler.RequestParameters != null) { foreach (string parameter in operationHandler.RequestParameters) { if (TryReadPropertyValue(request, parameter, out object propertyValue)) { aws[parameter.FromCamelCaseToSnakeCase()] = propertyValue; } } } if (operationHandler.RequestDescriptors != null) { foreach (KeyValuePair kv in operationHandler.RequestDescriptors) { var propertyName = kv.Key; var descriptor = kv.Value; if (descriptor.Map && descriptor.GetKeys) { AddMapKeyProperty(aws, request, propertyName, descriptor.RenameTo); } else if (descriptor.List && descriptor.GetCount) { AddListLengthProperty(aws, request, propertyName, descriptor.RenameTo); } } } } private void AddResponseSpecificInformation(string serviceName, string operation, AmazonWebServiceResponse response, IDictionary aws) { if (serviceName == null) { throw new ArgumentNullException(nameof(serviceName)); } if (operation == null) { throw new ArgumentNullException(nameof(operation)); } if (response == null) { throw new ArgumentNullException(nameof(response)); } if (aws == null) { throw new ArgumentNullException(nameof(aws)); } if (AWSServiceHandlerManifest == null) { _logger.DebugFormat("AWSServiceHandlerManifest doesn't exist."); return; } if (!AWSServiceHandlerManifest.Services.TryGetValue(serviceName, out AWSServiceHandler serviceHandler)) { _logger.DebugFormat("Service name doesn't exist in AWSServiceHandlerManifest: serviceName = {0}.", serviceName); return; } if (!serviceHandler.Operations.TryGetValue(operation, out AWSOperationHandler operationHandler)) { _logger.DebugFormat("Operation doesn't exist in AwsServiceInfo: serviceName = {0}, operation = {1}.", serviceName, operation); return; } if (operationHandler.ResponseParameters != null) { foreach (string parameter in operationHandler.ResponseParameters) { if (TryReadPropertyValue(response, parameter, out object propertyValue)) { aws[parameter.FromCamelCaseToSnakeCase()] = propertyValue; } } } if (operationHandler.ResponseDescriptors != null) { foreach (KeyValuePair kv in operationHandler.ResponseDescriptors) { var propertyName = kv.Key; var descriptor = kv.Value; if (descriptor.Map && descriptor.GetKeys) { XRayPipelineHandler.AddMapKeyProperty(aws, response, propertyName, descriptor.RenameTo); } else if (descriptor.List && descriptor.GetCount) { XRayPipelineHandler.AddListLengthProperty(aws, response, propertyName, descriptor.RenameTo); } } } } /// /// Process Synchronous operations. A subsegment is started at the beginning of /// the request and ended at the end of the request. /// public override void InvokeSync(IExecutionContext executionContext) { if (XRayPipelineHandler.IsTracingDisabled() || XRayPipelineHandler.ExcludeServiceOperation(executionContext)) { base.InvokeSync(executionContext); } else { ProcessBeginRequest(executionContext); try { base.InvokeSync(executionContext); } catch (Exception e) { PopulateException(e); throw; } finally { ProcessEndRequest(executionContext); } } } private void PopulateException(Exception e) { Entity subsegment; try { subsegment = _recorder.GetEntity(); } catch (EntityNotAvailableException ex) { _recorder.TraceContext.HandleEntityMissing(_recorder, ex, "Cannot get entity from trace context while processing exception for AWS SDK request."); return; } subsegment.AddException(e); // record exception if (e is AmazonServiceException amazonServiceException) { ProcessException(amazonServiceException, subsegment); } return; } private static bool ExcludeServiceOperation(IExecutionContext executionContext) { var requestContext = executionContext.RequestContext; var serviceName = RemoveAmazonPrefixFromServiceName(requestContext.Request.ServiceName); var operation = RemoveSuffix(requestContext.OriginalRequest.GetType().Name, "Request"); return AWSXRaySDKUtils.IsBlacklistedOperation(serviceName,operation); } private static bool IsTracingDisabled() { if (AWSXRayRecorder.Instance.IsTracingDisabled()) { _logger.DebugFormat("X-Ray tracing is disabled, do not handle AWSSDK request / response."); return true; } return false; } /// /// Process Asynchronous operations. A subsegment is started at the beginning of /// the request and ended at the end of the request. /// public override async Task InvokeAsync(IExecutionContext executionContext) { T ret = null; if (XRayPipelineHandler.IsTracingDisabled() || XRayPipelineHandler.ExcludeServiceOperation(executionContext)) { ret = await base.InvokeAsync(executionContext).ConfigureAwait(false); } else { ProcessBeginRequest(executionContext); try { ret = await base.InvokeAsync(executionContext).ConfigureAwait(false); } catch (Exception e) { PopulateException(e); throw; } finally { ProcessEndRequest(executionContext); } } return ret; } } /// /// Pipeline Customizer for registering instances with AWS X-Ray. /// Note: This class should not be instantiated or used in anyway. It is used internally within SDK. /// public class XRayPipelineCustomizer : IRuntimePipelineCustomizer { public string UniqueName { get { return "X-Ray Registration Customization"; } } private Boolean registerAll; private List types = new List(); private ReaderWriterLockSlim rwLock = new ReaderWriterLockSlim(); public bool RegisterAll { get => registerAll; set => registerAll = value; } public string Path { get; set; } // TODO :: This is not used anymore, remove in next breaking change public XRayPipelineHandler XRayPipelineHandler { get; set; } = null; // TODO :: This is not used anymore, remove in next breaking change public AWSServiceHandlerManifest AWSServiceHandlerManifest { get; set; } = null; public void Customize(Type serviceClientType, RuntimePipeline pipeline) { if (serviceClientType.BaseType != typeof(AmazonServiceClient)) return; bool addCustomization = this.RegisterAll; if (!addCustomization) { addCustomization = ProcessType(serviceClientType, addCustomization); } if (addCustomization && AWSServiceHandlerManifest == null) { pipeline.AddHandlerBefore(new XRayPipelineHandler()); } else if (addCustomization && AWSServiceHandlerManifest != null) { pipeline.AddHandlerBefore(new XRayPipelineHandler(AWSServiceHandlerManifest)); // Custom AWS Manifest file path/stream provided } } private bool ProcessType(Type serviceClientType, bool addCustomization) { rwLock.EnterReadLock(); try { foreach (var registeredType in types) { if (registeredType.IsAssignableFrom(serviceClientType)) { addCustomization = true; break; } } } finally { rwLock.ExitReadLock(); } return addCustomization; } /// /// Adds type to the list of . /// /// Type of to be registered with X-Ray. public void AddType(Type type) { rwLock.EnterWriteLock(); try { types.Add(type); } finally { rwLock.ExitWriteLock(); } } } }