//----------------------------------------------------------------------------- // // 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.Globalization; using System.Linq; using System.Text; using Amazon.Runtime.Internal.Util; using Amazon.XRay.Recorder.Core.Sampling; namespace Amazon.XRay.Recorder.Core.Internal.Entities { /// /// Represent the embedded trace header in HTTP request/response header /// public class TraceHeader { /// /// Field name for trace header in HTTP header /// public const string HeaderKey = "X-Amzn-Trace-Id"; private const string RootKey = "Root"; private const string ParentKey = "Parent"; private const string SampledKey = "Sampled"; private static readonly Logger _logger = Logger.GetLogger(typeof(TraceHeader)); private static readonly char[] _validSeparators = { ';' }; /// /// Gets or sets the trace id /// public string RootTraceId { get; set; } /// /// Gets or sets the parent segment id /// public string ParentId { get; set; } /// /// Gets or sets the sample decision /// public SampleDecision Sampled { get; set; } /// /// Convert the string representation of a trace header to an instance of . /// /// A string from HTTP request containing a trace header /// When the method returns, contains the object converted from , /// if the conversion succeeded, or null if the conversion failed. The conversion fails if the is null or empty, /// is not of the correct format. This parameter is passed uninitialized; any value originally supplied will be overwritten. /// true if converted successfully; otherwise, false. Root trace id /// required while parent id and sample decision is optional. public static bool TryParse(string rawHeader, out TraceHeader header) { header = null; try { if (string.IsNullOrEmpty(rawHeader)) { return false; } var result = new TraceHeader(); Dictionary keyValuePairs = rawHeader.Split(_validSeparators, StringSplitOptions.RemoveEmptyEntries) .Select(value => value.Trim().Split('=')) .ToDictionary(pair => pair[0], pair => pair[1]); // Root trace id is required if (!keyValuePairs.TryGetValue(RootKey, out string tmpValue)) { return false; } if (!TraceId.IsIdValid(tmpValue)) { return false; } result.RootTraceId = tmpValue; // Parent id is optional if (keyValuePairs.TryGetValue(ParentKey, out tmpValue)) { if (!Entity.IsIdValid(tmpValue)) { return false; } result.ParentId = tmpValue; } // Sample decision is optional if (keyValuePairs.TryGetValue(SampledKey, out tmpValue) && char.TryParse(tmpValue, out char tmpChar)) { if (Enum.IsDefined(typeof(SampleDecision), (int)tmpChar)) { result.Sampled = (SampleDecision)tmpChar; } } header = result; return true; } catch (IndexOutOfRangeException e) { _logger.Error(e, "Invalid trace header from string: {0}", rawHeader); return false; } } /// /// Convert the string representation of a trace header to an instance of . /// /// A string from HTTP request containing a trace header /// When the method returns, contains the object converted from , /// if the conversion succeeded, or null if the conversion failed. The conversion fails if the is null or empty, /// is not of the correct format. This parameter is passed uninitialized; any value originally supplied will be overwritten. RootId, ParentId /// and Sampling decision are all required in valid form /// true if converted successfully; otherwise, false. public static bool TryParseAll(string rawHeader, out TraceHeader traceHeader) { traceHeader = FromString(rawHeader); if(string.IsNullOrEmpty(rawHeader) || string.IsNullOrEmpty(traceHeader.RootTraceId) || string.IsNullOrEmpty(traceHeader.ParentId) || traceHeader.Sampled == SampleDecision.Unknown) { return false; } return true; } /// /// Converts the string representation of a trace header to an instance of . /// /// A string from HTTP request containing a trace header. /// Contains the object converted from , /// It only extracts non-null and valid values. public static TraceHeader FromString(string rawHeader) { TraceHeader result = new TraceHeader(); try { if (string.IsNullOrEmpty(rawHeader)) { return result; } Dictionary keyValuePairs = rawHeader.Split(_validSeparators, StringSplitOptions.RemoveEmptyEntries) .Select(value => value.Trim().Split('=')) .ToDictionary(pair => pair[0], pair => pair[1]); if (keyValuePairs.TryGetValue(RootKey, out string tmpValue) && TraceId.IsIdValid(tmpValue)) { result.RootTraceId = tmpValue; } if (keyValuePairs.TryGetValue(ParentKey, out tmpValue) && Entity.IsIdValid(tmpValue)) { result.ParentId = tmpValue; } if (keyValuePairs.TryGetValue(SampledKey, out tmpValue) && char.TryParse(tmpValue, out char tmpChar)) { if (Enum.IsDefined(typeof(SampleDecision), (int)tmpChar)) { result.Sampled = (SampleDecision)tmpChar; } } return result; } catch (IndexOutOfRangeException e) { _logger.Error(e, "Invalid trace header from string: {0}", rawHeader); return result; } } /// /// Convert a Segment to an instance of . /// /// A instance of that will be used to convert to . /// When the method returns, contains the object converted from , /// if the conversion succeeded, or null if the conversion failed. The conversion fails if the is null, or /// is not of the correct format. This parameter is passed uninitialized; any value originally supplied will be overwritten. /// true if converted successfully; otherwise, false. public static bool TryParse(Entity entity, out TraceHeader header) { header = null; if (entity == null) { _logger.DebugFormat("Failed to parse TraceHeader because segment is null."); return false; } if (string.IsNullOrEmpty(entity.Id)) { _logger.DebugFormat("Failed to parse TraceHeader because segment id is null or empty."); return false; } if (string.IsNullOrEmpty(entity.RootSegment.TraceId)) { _logger.DebugFormat("Failed to parse TraceHeader because trace id is null or empty."); return false; } var newHeader = new TraceHeader(); // Trace id doesn't exist in subsegment, so get it from rootsegment newHeader.RootTraceId = entity.RootSegment.TraceId; newHeader.ParentId = entity.Id; newHeader.Sampled = entity.Sampled; header = newHeader; return true; } /// /// Generate a string out of this instance of the class /// /// The string generated from current object public override string ToString() { var sb = new StringBuilder(); sb.Append(string.Format(CultureInfo.InvariantCulture, "{0}={1}", RootKey, RootTraceId)); // Exclude parent id if the request is not sampled if (!string.IsNullOrEmpty(ParentId) && Sampled != SampleDecision.NotSampled) { sb.Append(string.Format(CultureInfo.InvariantCulture, "; {0}={1}", ParentKey, ParentId)); } // Exclude sampled decision if the decision is unknown if (Sampled != SampleDecision.Unknown) { sb.Append(string.Format(CultureInfo.InvariantCulture, "; {0}={1}", SampledKey, Convert.ToChar(Sampled, CultureInfo.InvariantCulture))); } return sb.ToString(); } } }