Skip to content

Commit 2d28390

Browse files
authored
Merge pull request #163 from nblumhardt/header-parsing
Parse `OTEL_` header and resource attribute vars according to spec
2 parents 7b0cfe5 + 799a878 commit 2d28390

File tree

4 files changed

+89
-14
lines changed

4 files changed

+89
-14
lines changed
Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
namespace Serilog.Sinks.OpenTelemetry.Configuration;
2+
3+
static class BaggageFormat
4+
{
5+
/// <summary>
6+
/// Decode W3C Baggage-formatted key-value pairs as specified for handling of the `OTEL_EXPORTER_OTLP_HEADERS` and
7+
/// `OTEL_RESOURCE_ATTRIBUTES` environment variables.
8+
/// </summary>
9+
/// <returns>The property names and values encoded in the supplied <paramref name="baggageString"/>.</returns>
10+
public static IEnumerable<(string, string)> DecodeBaggageString(string baggageString, string environmentVariableName)
11+
{
12+
// See: https://github.com/open-telemetry/opentelemetry-specification/blob/main/specification/resource/sdk.md#specifying-resource-information-via-an-environment-variable
13+
// See: https://www.w3.org/TR/baggage/#header-content
14+
15+
if (string.IsNullOrWhiteSpace(baggageString)) yield break;
16+
17+
foreach (var listMember in baggageString.Split(','))
18+
{
19+
// The baggage spec allows list members to carry additional key-value pair metadata after the initial
20+
// key and value and a trailing semicolon, but this is disallowed by the OTel spec. We're pretty loose with
21+
// validation, here, but could tighten up handling of invalid values in the future.
22+
23+
var eq = listMember.IndexOf('=');
24+
if (eq == -1) RejectInvalidListMember(listMember, environmentVariableName, nameof(baggageString));
25+
26+
var key = listMember.Substring(0, eq).Trim();
27+
if (string.IsNullOrEmpty(key)) RejectInvalidListMember(listMember, environmentVariableName, nameof(baggageString));
28+
29+
var escapedValue = eq == listMember.Length - 1 ? "" : listMember.Substring(eq + 1).Trim();
30+
var value = Uri.UnescapeDataString(escapedValue);
31+
32+
yield return (key, value);
33+
}
34+
}
35+
36+
static void RejectInvalidListMember(string listMember, string environmentVariableName, string paramName)
37+
{
38+
throw new ArgumentException($"Invalid item format `{listMember}` in {environmentVariableName} environment variable.", paramName);
39+
}
40+
}

src/Serilog.Sinks.OpenTelemetry/Sinks/OpenTelemetry/Configuration/OpenTelemetryEnvironment.cs

Lines changed: 6 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -46,23 +46,19 @@ public static void Configure(BatchedOpenTelemetrySinkOptions options, Func<strin
4646

4747
static void FillHeadersIfPresent(string? config, IDictionary<string, string> headers)
4848
{
49-
foreach (var part in config?.Split(',') ?? [])
49+
if (config == null) return;
50+
foreach (var (key, value) in BaggageFormat.DecodeBaggageString(config, HeaderVarName))
5051
{
51-
if (part.Split('=') is { Length: 2 } parts)
52-
headers[parts[0]] = parts[1];
53-
else
54-
throw new InvalidOperationException($"Invalid header format `{part}` in {HeaderVarName} environment variable.");
52+
headers[key] = value;
5553
}
5654
}
5755

5856
static void FillHeadersResourceAttributesIfPresent(string? config, IDictionary<string, object> resourceAttributes)
5957
{
60-
foreach (var part in config?.Split(',') ?? [])
58+
if (config == null) return;
59+
foreach (var (key, value) in BaggageFormat.DecodeBaggageString(config, ResourceAttributesVarName))
6160
{
62-
if (part.Split('=') is { Length: 2 } parts)
63-
resourceAttributes[parts[0]] = parts[1];
64-
else
65-
throw new InvalidOperationException($"Invalid resource attributes format `{part}` in {ResourceAttributesVarName} environment variable.");
61+
resourceAttributes[key] = value;
6662
}
6763
}
6864
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using Serilog.Sinks.OpenTelemetry.Configuration;
2+
using Serilog.Sinks.OpenTelemetry.Tests.Support;
3+
using Xunit;
4+
5+
namespace Serilog.Sinks.OpenTelemetry.Tests;
6+
7+
public class BaggageFormatTests
8+
{
9+
public static TheoryData<string, (string, string)[]> Cases => new()
10+
{
11+
{ "", [] },
12+
{ " ", [] },
13+
{ "a=", [("a", "")] },
14+
{ "abc=def", [("abc", "def")] },
15+
{ "abc= def ", [("abc", "def")] },
16+
{ "abc=def,ghi=jkl", [("abc", "def"), ("ghi", "jkl")] },
17+
{ "a=1%202", [("a", "1 2")] },
18+
{ "a=b=c,d=e", [("a", "b=c"), ("d", "e")] },
19+
{ "a=%2C,b=c", [("a", ","), ("b", "c")] }
20+
};
21+
22+
[Theory, MemberData(nameof(Cases))]
23+
public void BaggageStringsAreDecoded(string baggageString, IEnumerable<(string, string)> expected)
24+
{
25+
var actual = BaggageFormat.DecodeBaggageString(baggageString, Some.String());
26+
Assert.Equal(expected, actual);
27+
}
28+
29+
[Theory]
30+
[InlineData(",")]
31+
[InlineData(", ")]
32+
[InlineData("a")]
33+
[InlineData("=")]
34+
[InlineData(",=")]
35+
public void InvalidBaggageStringsAreRejected(string baggageString)
36+
{
37+
Assert.Throws<ArgumentException>(() => BaggageFormat.DecodeBaggageString(baggageString, Some.String()).ToList());
38+
}
39+
}

test/Serilog.Sinks.OpenTelemetry.Tests/OpenTelemetryEnvironmentTests.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,9 +88,9 @@ public void ConfigureThrowsIfHeaderEnvIsInvalidFormat()
8888
BatchedOpenTelemetrySinkOptions options = new();
8989
var headers = "header1";
9090

91-
var exception = Assert.Throws<InvalidOperationException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));
91+
var exception = Assert.Throws<ArgumentException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));
9292

93-
Assert.Equal("Invalid header format `header1` in OTEL_EXPORTER_OTLP_HEADERS environment variable.", exception.Message);
93+
Assert.StartsWith("Invalid item format `header1` in OTEL_EXPORTER_OTLP_HEADERS environment variable.", exception.Message);
9494

9595
string? GetEnvVar(string name)
9696
=> name switch
@@ -106,9 +106,9 @@ public void ConfigureThrowsIfResourceAttributesEnvIsInvalidFormat()
106106
BatchedOpenTelemetrySinkOptions options = new();
107107
var resourceAttributes = "resource1";
108108

109-
var exception = Assert.Throws<InvalidOperationException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));
109+
var exception = Assert.Throws<ArgumentException>(() => OpenTelemetryEnvironment.Configure(options, GetEnvVar));
110110

111-
Assert.Equal("Invalid resource attributes format `resource1` in OTEL_RESOURCE_ATTRIBUTES environment variable.", exception.Message);
111+
Assert.StartsWith("Invalid item format `resource1` in OTEL_RESOURCE_ATTRIBUTES environment variable.", exception.Message);
112112

113113
string? GetEnvVar(string name)
114114
=> name switch

0 commit comments

Comments
 (0)