Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
173 changes: 173 additions & 0 deletions tracer/src/Datadog.Trace/Propagators/OtelTraceStateHelpers.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
// <copyright file="OtelTraceStateHelpers.cs" company="Datadog">
// Unless explicitly stated otherwise all files in this repository are licensed under the Apache 2 License.
// This product includes software developed at Datadog (https://www.datadoghq.com/). Copyright 2017 Datadog, Inc.
// </copyright>

#nullable enable

using System;
using System.Collections.Generic;
using Datadog.Trace.Util;

namespace Datadog.Trace.Propagators
{
/// <summary>
/// String-surgery helpers over the raw content of the W3C tracestate "ot=" list-member
/// (OpenTelemetry consistent-probability-sampling sub-keys "rv"/"th"). The value is never
/// decoded into a typed struct: these two helpers are the only code that inspects or
/// rewrites the "rv"/"th" sub-keys; every other sub-key (recognized or not) round-trips
/// byte-for-byte through <see cref="SetRvTh"/> in its original order.
/// </summary>
internal static class OtelTraceStateHelpers
{
private const int MaxRvHexDigits = 14;

/// <summary>
/// Finds the "rv" item in the raw "ot=" value (items separated by ';', key/value by ':')
/// and returns its value parsed as 1-14 lowercase hex digits, or null if absent or malformed.
/// Never throws.
/// </summary>
internal static ulong? ExtractRv(string? raw)
{
if (StringUtil.IsNullOrEmpty(raw))
{
return null;
}

foreach (var item in raw.Split(';'))
{
var colonIndex = item.IndexOf(':');

if (colonIndex <= 0 || colonIndex == item.Length - 1)
{
continue;
}

var key = item.Substring(0, colonIndex);

if (key != "rv")
{
continue;
}

var value = item.Substring(colonIndex + 1);
return TryParseLowercaseHex(value, MaxRvHexDigits, out var rv) ? rv : null;
}

return null;
}

/// <summary>
/// Drops any existing "rv"/"th" items from <paramref name="raw"/> (whether well-formed
/// or not), then emits "rv:&lt;14-hex-digits&gt;" (if <paramref name="rv"/> is non-null)
/// followed by "th:&lt;hex, trailing zero nibbles trimmed&gt;" (if <paramref name="th"/>
/// is non-null), followed by every other item from <paramref name="raw"/> in its original
/// order. Returns null when nothing is left to emit.
/// </summary>
internal static string? SetRvTh(string? raw, ulong? rv, ulong? th)
{
List<string>? otherItems = null;

if (!StringUtil.IsNullOrEmpty(raw))
{
foreach (var item in raw!.Split(';'))
{
var colonIndex = item.IndexOf(':');
var key = colonIndex > 0 ? item.Substring(0, colonIndex) : item;

if (key is "rv" or "th")
{
continue;
}

(otherItems ??= new List<string>()).Add(item);
}
}

if (rv is null && th is null && otherItems is null)
{
return null;
}

var sb = StringBuilderCache.Acquire();

try
{
if (rv is { } rvValue)
{
sb.Append("rv:").Append(rvValue.ToString("x14"));
}

if (th is { } thValue)
{
if (sb.Length > 0)
{
sb.Append(';');
}

sb.Append("th:").Append(FormatThresholdHex(thValue));
}

if (otherItems is not null)
{
foreach (var item in otherItems)
{
if (sb.Length > 0)
{
sb.Append(';');
}

sb.Append(item);
}
}

return sb.Length == 0 ? null : StringBuilderCache.GetStringAndRelease(sb);
}
finally
{
StringBuilderCache.Release(sb);
}
}

private static string FormatThresholdHex(ulong th)
{
// Format as hex (up to 14 hex digits for a 56-bit value), then trim trailing zero nibbles.
// A fully-zero threshold trims to the empty string; represent it as a single "0".
var hex = th.ToString("x");
var trimmed = hex.TrimEnd('0');
return trimmed.Length == 0 ? "0" : trimmed;
}

private static bool TryParseLowercaseHex(string value, int maxDigits, out ulong result)
{
result = 0;

if (value.Length == 0 || value.Length > maxDigits)
{
return false;
}

foreach (var c in value)
{
int digit;

if (c is >= '0' and <= '9')
{
digit = c - '0';
}
else if (c is >= 'a' and <= 'f')
{
digit = c - 'a' + 10;
}
else
{
return false;
}

result = (result << 4) | (uint)digit;
}

return true;
}
}
}
125 changes: 68 additions & 57 deletions tracer/src/Datadog.Trace/Propagators/W3CTraceContextPropagator.cs
Original file line number Diff line number Diff line change
Expand Up @@ -199,6 +199,21 @@ internal static string CreateTraceStateHeader(SpanContext context)
sb.Length--;
}

// OTel consistent-probability-sampling sub-keys ("ot=rv:...;th:..."), placed
// immediately after "dd=" so both survive right-side truncation of a crowded
// tracestate (W3C permits dropping members past 32).
var otelTraceState = context.OtelTraceState;

if (!string.IsNullOrWhiteSpace(otelTraceState))
{
if (sb.Length > 0)
{
sb.Append(TraceStateHeaderValuesSeparator);
}

sb.Append("ot=").Append(otelTraceState);
}

var additionalState = context.AdditionalW3CTraceState;

if (!string.IsNullOrWhiteSpace(additionalState))
Expand Down Expand Up @@ -311,17 +326,17 @@ internal static W3CTraceState ParseTraceState(string? header)
// header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,*]"
if (string.IsNullOrWhiteSpace(header))
{
return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues: null);
return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues: null, otTraceState: null);
}

SplitTraceStateValues(header!, out var ddValues, out var additionalValues);
SplitTraceStateValues(header!, out var ddValues, out var otTraceState, out var additionalValues);

if (ddValues is null or { Length: < 6 })
if (ddValues is null or { Length: < 3 })
{
// "dd" section not found or it is too short
// shortest valid length is 6 as in "dd=a:b"
// shortest valid length is 3 as in "a:b" ("dd=" prefix already stripped)
// note for this case the p will be viewed as 0 if added as a span tag
return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues);
return new W3CTraceState(samplingPriority: null, origin: null, lastParent: ZeroLastParent, propagatedTags: null, additionalValues, otTraceState);
}

int? samplingPriority = null;
Expand All @@ -331,8 +346,7 @@ internal static W3CTraceState ParseTraceState(string? header)

try
{
// skip "dd="
var startIndex = 3;
var startIndex = 0;

// name1:value1;
// ^ endIndex
Expand Down Expand Up @@ -449,99 +463,95 @@ internal static W3CTraceState ParseTraceState(string? header)

lastParent ??= ZeroLastParent;

return new W3CTraceState(samplingPriority, origin, lastParent, propagatedTags, additionalValues);
return new W3CTraceState(samplingPriority, origin, lastParent, propagatedTags, additionalValues, otTraceState);
}
finally
{
StringBuilderCache.Release(propagatedTagsBuilder);
}
}

internal static void SplitTraceStateValues(string header, out string? ddValues, out string? additionalValues)
internal static void SplitTraceStateValues(string header, out string? ddValues, out string? otValues, out string? additionalValues)
{
// header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,*]"

// header format: "[*,]dd=s:1;o:rum;t.dm:-4;t.usr.id:12345[,ot=rv:...;th:...][,*]"
if (string.IsNullOrWhiteSpace(header))
{
ddValues = null;
otValues = null;
additionalValues = null;
return;
}

header = header.Trim();
int ddStartIndex;

if (header.StartsWith("dd=", StringComparison.Ordinal))
ExtractMember(header, "dd=", out ddValues, out var afterDd);
ExtractMember(afterDd, "ot=", out otValues, out var afterOt);
additionalValues = string.IsNullOrEmpty(afterOt) ? null : afterOt;
}

// Finds the list-member with the given "<key>=" prefix anywhere in a comma-separated
// tracestate value, returns its value (without the prefix) via "value", and returns
// every other member (still comma-separated, original order preserved) via "remainder".
private static void ExtractMember(string? header, string prefix, out string? value, out string? remainder)
{
if (string.IsNullOrEmpty(header))
{
ddStartIndex = 0;
value = null;
remainder = header;
return;
}

int startIndex;

if (header!.StartsWith(prefix, StringComparison.Ordinal))
{
startIndex = 0;
}
else
{
// if "dd=" is not at start of header, make sure we find the one preceded by comma
// in case there is something like "key1=valuedd=whatisthis,dd=..."
// ^ take this one
// ^ ignore this one
ddStartIndex = header.IndexOf(",dd=", StringComparison.Ordinal);
startIndex = header.IndexOf("," + prefix, StringComparison.Ordinal);

if (ddStartIndex >= 0)
if (startIndex >= 0)
{
// if ",dd=" was found, skip the ','
ddStartIndex++;
startIndex++; // skip the ','
}
}

if (ddStartIndex < 0)
if (startIndex < 0)
{
// "dd=" was not found in header, the entire header is "additional values"
// example tracestate: "foo=bar"
// ^^^^^^^
ddValues = null;
additionalValues = header;
value = null;
remainder = header;
return;
}

// search for end of "dd="
var ddEndIndex = header.IndexOf(TraceStateHeaderValuesSeparator, ddStartIndex + 3);
var endIndex = header.IndexOf(TraceStateHeaderValuesSeparator, startIndex + prefix.Length);

if (ddEndIndex < 0)
if (endIndex < 0)
{
// "dd=" reaches the end of header
ddEndIndex = header.Length;
endIndex = header.Length;
}

ddValues = header.Substring(ddStartIndex, ddEndIndex - ddStartIndex);
value = header.Substring(startIndex + prefix.Length, endIndex - startIndex - prefix.Length);

if (ddStartIndex == 0 && ddEndIndex == header.Length)
if (startIndex == 0 && endIndex == header.Length)
{
// "dd" was the only key, no additional values
// example tracestate: "dd=s:1;o:rum"
additionalValues = null;
remainder = null;
}
else if (ddStartIndex == 0)
else if (startIndex == 0)
{
// "dd" first, additional values later
// example tracestate: "dd=s:1;o:rum,foo=bar"
// ^^^^^^^
additionalValues = header.Substring(ddEndIndex + 1, header.Length - ddEndIndex - 1);
remainder = header.Substring(endIndex + 1, header.Length - endIndex - 1);
}
else if (ddEndIndex == header.Length)
else if (endIndex == header.Length)
{
// additional values first, "dd" later
// example tracestate: "foo=bar,dd=s:1;o:rum"
// ^^^^^^^
additionalValues = header.Substring(0, ddStartIndex - 1);
remainder = header.Substring(0, startIndex - 1);
}
else
{
// additional values on both sides, "dd" in the middle
// example tracestate: "foo1=bar1,dd=s:1;o:rum,foo2=bar2" => "foo1=bar1,foo2=bar2"
// ^^^^^^^^^ ^^^^^^^^^
var otherValuesLeft = header.Substring(0, ddStartIndex - 1);
var otherValuesRight = header.Substring(ddEndIndex + 1, header.Length - ddEndIndex - 1);

var sb = StringBuilderCache.Acquire(otherValuesLeft.Length + otherValuesRight.Length + 1);
sb.Append(otherValuesLeft).Append(TraceStateHeaderValuesSeparator).Append(otherValuesRight);
additionalValues = StringBuilderCache.GetStringAndRelease(sb);
var left = header.Substring(0, startIndex - 1);
var right = header.Substring(endIndex + 1, header.Length - endIndex - 1);
var sb = StringBuilderCache.Acquire(left.Length + right.Length + 1);
sb.Append(left).Append(TraceStateHeaderValuesSeparator).Append(right);
remainder = StringBuilderCache.GetStringAndRelease(sb);
}
}

Expand Down Expand Up @@ -631,6 +641,7 @@ public bool TryExtract<TCarrier, TCarrierGetter>(

spanContext.PropagatedTags = traceTags;
spanContext.AdditionalW3CTraceState = traceState.AdditionalValues;
spanContext.OtelTraceState = traceState.OtTraceState;
spanContext.LastParentId = traceState.LastParent;

context = new PropagationContext(spanContext, baggage: null);
Expand Down
Loading
Loading