Skip to content

Commit 22d3807

Browse files
committed
fixes
1 parent bf0c6df commit 22d3807

3 files changed

Lines changed: 206 additions & 4 deletions

File tree

src/OpenTelemetry.Exporter.Console/Implementation/ConsoleTagWriter.cs

Lines changed: 99 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -87,14 +87,109 @@ protected override bool TryWriteEmptyTag(ref ConsoleTag consoleTag, string key,
8787

8888
protected override void WriteKvListTag(ref ConsoleTag state, string key, IEnumerable<KeyValuePair<string, object?>> kvList, int? tagValueMaxLength)
8989
{
90-
var stringValue = Convert.ToString(kvList, CultureInfo.InvariantCulture);
90+
var sb = new StringBuilder();
91+
sb.Append('{');
92+
var first = true;
93+
foreach (var kvp in kvList)
94+
{
95+
ConsoleTag nestedTag = default;
96+
if (this.TryWriteTag(ref nestedTag, kvp.Key, kvp.Value, tagValueMaxLength))
97+
{
98+
if (!first)
99+
{
100+
sb.Append(',');
101+
}
102+
103+
first = false;
104+
sb.Append('"');
105+
AppendJsonEscaped(sb, kvp.Key);
106+
sb.Append("\":");
107+
108+
var tagValue = nestedTag.Value;
109+
if (tagValue == null)
110+
{
111+
sb.Append("null");
112+
}
113+
else if (IsRawJsonValue(kvp.Value, tagValue))
114+
{
115+
sb.Append(tagValue);
116+
}
117+
else
118+
{
119+
sb.Append('"');
120+
AppendJsonEscaped(sb, tagValue);
121+
sb.Append('"');
122+
}
123+
}
124+
}
125+
126+
sb.Append('}');
127+
state.Key = key;
128+
state.Value = sb.ToString();
129+
}
130+
131+
/// <summary>
132+
/// Determines whether tagValue is already a valid JSON literal
133+
/// that should be embedded without surrounding quotes.
134+
/// </summary>
135+
private static bool IsRawJsonValue(object? originalValue, string tagValue)
136+
{
137+
if (originalValue is bool
138+
or byte or sbyte or short or ushort or int or uint or long
139+
or float or double)
140+
{
141+
return true;
142+
}
91143

92-
if (stringValue is null)
144+
// KV lists and arrays produce JSON objects/arrays via TryWriteTag.
145+
// However, when the recursion depth limit is reached, TryWriteTag
146+
// falls back to a plain string (the type name). Detect this by
147+
// checking whether the output starts with '{' or '['.
148+
if ((originalValue is IEnumerable<KeyValuePair<string, object?>> or Array)
149+
&& tagValue.Length > 0
150+
&& (tagValue[0] == '{' || tagValue[0] == '['))
93151
{
94-
return;
152+
return true;
95153
}
96154

97-
this.TryWriteTag(ref state, key, stringValue);
155+
return false;
156+
}
157+
158+
private static void AppendJsonEscaped(StringBuilder sb, string value)
159+
{
160+
foreach (var c in value)
161+
{
162+
switch (c)
163+
{
164+
case '"':
165+
sb.Append("\\\"");
166+
break;
167+
case '\\':
168+
sb.Append("\\\\");
169+
break;
170+
case '\n':
171+
sb.Append("\\n");
172+
break;
173+
case '\r':
174+
sb.Append("\\r");
175+
break;
176+
case '\t':
177+
sb.Append("\\t");
178+
break;
179+
default:
180+
if (c < ' ')
181+
{
182+
sb.Append("\\u");
183+
sb.Append(((int)c).ToString("x4", CultureInfo.InvariantCulture));
184+
}
185+
else
186+
{
187+
sb.Append(c);
188+
}
189+
190+
break;
191+
}
192+
}
98193
}
99194

100195
internal struct ConsoleTag

src/Shared/TagWriter/TagWriter.cs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,11 @@ internal abstract class TagWriter<TTagState, TArrayState>
1010
where TTagState : notnull
1111
where TArrayState : notnull
1212
{
13+
internal const int MaxRecursionDepth = 3;
14+
15+
[ThreadStatic]
16+
private static int recursionDepth;
17+
1318
private readonly ArrayTagWriter<TArrayState> arrayWriter;
1419

1520
protected TagWriter(
@@ -67,14 +72,32 @@ public bool TryWriteTag(
6772
case IEnumerable<KeyValuePair<string, object?>> kvList:
6873
try
6974
{
75+
if (recursionDepth >= MaxRecursionDepth)
76+
{
77+
var stringValue = Convert.ToString(value, CultureInfo.InvariantCulture);
78+
this.WriteStringTag(
79+
ref state,
80+
key,
81+
TruncateString(stringValue.AsSpan(), tagValueMaxLength));
82+
83+
break;
84+
}
85+
else
86+
{
87+
recursionDepth++;
88+
}
89+
7090
this.WriteKvListTag(ref state, key, kvList, tagValueMaxLength);
91+
recursionDepth--;
7192
}
7293
catch (Exception ex) when (ex is IndexOutOfRangeException or ArgumentException)
7394
{
95+
recursionDepth = 0;
7496
throw;
7597
}
7698
catch
7799
{
100+
recursionDepth--;
78101
return this.LogUnsupportedTagTypeAndReturnFalse(key, value);
79102
}
80103

test/OpenTelemetry.Exporter.OpenTelemetryProtocol.Tests/OtlpKvListAttributeTests.cs

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
using System.Diagnostics;
55
using System.Diagnostics.CodeAnalysis;
6+
using System.Globalization;
67
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation;
78
using OpenTelemetry.Exporter.OpenTelemetryProtocol.Implementation.Serializer;
89
using OtlpCommon = OpenTelemetry.Proto.Common.V1;
@@ -278,6 +279,81 @@ public void KvListAttributeTriggersMainBufferResize()
278279
Assert.Equal(largeString, values[0].Value.StringValue);
279280
}
280281

282+
[Fact]
283+
public void RecursionHasMaxDepthAndRecursionDepthIsReset()
284+
{
285+
var tags = new ActivityTagsCollection
286+
{
287+
new("kvList", SelfReferencingKvList()),
288+
new("kvList1", SelfReferencingKvList()),
289+
};
290+
291+
using var activitySource = new ActivitySource(nameof(this.RecursionHasMaxDepthAndRecursionDepthIsReset));
292+
using var activity = activitySource.StartActivity("test", ActivityKind.Server, default(ActivityContext), tags);
293+
294+
Assert.NotNull(activity);
295+
296+
var buffer = new byte[1_000_000];
297+
298+
var writePosition = ProtobufOtlpTraceSerializer.WriteSpan(buffer, 0, new SdkLimitOptions(), activity);
299+
300+
using var stream = new MemoryStream(buffer, 0, writePosition);
301+
var scopeSpans = OtlpTrace.ScopeSpans.Parser.ParseFrom(stream);
302+
Assert.Single(scopeSpans.Spans);
303+
var span = scopeSpans.Spans.FirstOrDefault();
304+
305+
Assert.NotNull(span);
306+
Assert.Equal(2, span.Attributes.Count);
307+
308+
var attribute = span.Attributes[0];
309+
Assert.Equal("kvList", attribute.Key);
310+
311+
var attributeValue = attribute.Value;
312+
for (var i = 0; i < 3; i++)
313+
{
314+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.KvlistValue, attributeValue.ValueCase);
315+
Assert.Equal(2, attributeValue.KvlistValue.Values.Count);
316+
317+
Assert.Equal("int", attributeValue.KvlistValue.Values[0].Key);
318+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.IntValue, attributeValue.KvlistValue.Values[0].Value.ValueCase);
319+
320+
Assert.Equal("self", attributeValue.KvlistValue.Values[1].Key);
321+
if (i < 2)
322+
{
323+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.KvlistValue, attributeValue.KvlistValue.Values[1].Value.ValueCase);
324+
attributeValue = attributeValue.KvlistValue.Values[1].Value;
325+
continue;
326+
}
327+
328+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attributeValue.KvlistValue.Values[1].Value.ValueCase);
329+
Assert.Equal(Convert.ToString(SelfReferencingKvList(), CultureInfo.InvariantCulture), attributeValue.KvlistValue.Values[1].Value.StringValue);
330+
}
331+
332+
attribute = span.Attributes[1];
333+
Assert.Equal("kvList1", attribute.Key);
334+
335+
attributeValue = attribute.Value;
336+
for (var i = 0; i < 3; i++)
337+
{
338+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.KvlistValue, attributeValue.ValueCase);
339+
Assert.Equal(2, attributeValue.KvlistValue.Values.Count);
340+
341+
Assert.Equal("int", attributeValue.KvlistValue.Values[0].Key);
342+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.IntValue, attributeValue.KvlistValue.Values[0].Value.ValueCase);
343+
344+
Assert.Equal("self", attributeValue.KvlistValue.Values[1].Key);
345+
if (i < 2)
346+
{
347+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.KvlistValue, attributeValue.KvlistValue.Values[1].Value.ValueCase);
348+
attributeValue = attributeValue.KvlistValue.Values[1].Value;
349+
continue;
350+
}
351+
352+
Assert.Equal(OtlpCommon.AnyValue.ValueOneofCase.StringValue, attributeValue.KvlistValue.Values[1].Value.ValueCase);
353+
Assert.Equal(Convert.ToString(SelfReferencingKvList(), CultureInfo.InvariantCulture), attributeValue.KvlistValue.Values[1].Value.StringValue);
354+
}
355+
}
356+
281357
public void Dispose()
282358
{
283359
this.activityListener.Dispose();
@@ -289,6 +365,14 @@ public void Dispose()
289365
throw new InvalidOperationException("simulated failure");
290366
}
291367

368+
private static List<KeyValuePair<string, object?>> SelfReferencingKvList()
369+
{
370+
var list = new List<KeyValuePair<string, object?>>();
371+
list.Add(new("int", 1));
372+
list.Add(new("self", list));
373+
return list;
374+
}
375+
292376
private static bool TryTransformTag(KeyValuePair<string, object?> tag, [NotNullWhen(true)] out OtlpCommon.KeyValue? attribute)
293377
{
294378
ProtobufOtlpTagWriter.OtlpTagWriterState otlpTagWriterState = new ProtobufOtlpTagWriter.OtlpTagWriterState

0 commit comments

Comments
 (0)